본문 바로가기

[Java] toString()에 대해서

어느 날 누가 내게 toString()은 언제, 어떻게 써야 하냐고 물었는데 대답할 수가 없었다.
그동안 생각 없이 toString()을 사용했구나! 생각이 들면서 마침 이펙티브 자바에 설명이 잘 되어있어서 정리 + 예제를 작성했다.

모든 하위 클래스에서 toString()을 재정의하라

toString의 규약은 "모든 하위 클래스에서 이 메서드를 재정의하라"고 한다.
toString()을 재정의할 경우 이점으로는 아래와 같다.

  • 로깅, 디버깅 또는 문자열로 만나는 모든 객체를 렌더링 할 수 있어야하는 상황에서 유용하다.
  • 직접 호출하지 않더라도 다른 어딘가에서 쓰일 수 있다.
    • 오류 메시지를 로깅할 때 자동으로 호출할 수 있다. (toString을 제대로 재정의하지 않는다면 쓸모없는 메시지만 로그에 남을 것이다.

toString을 재정의 했을 경우

Untitled-d07a7763-1487-4550-b305-dee30312db75.png

디버깅할 때 Piece의 정보를 한 눈에 볼 수 있어서 편하다.

toString을 재정의하지 않았을 경우

Untitled-89627595-ed6c-4037-babf-0c22482567f2.png

반면 재정의를 하지 않으면 디버깅할 때 클래스이름@16진수로 표시한 해시코드로 반한 되므로 Piece의 정보를 알기가 어렵다. 이 경우 정보를 알기 위해서는 아래에 있는 이미지처럼 디버깅 창에서 일일이 확인해줘야 한다.

Untitled-7a7cc821-87d9-4405-83af-aa067a906966.png

간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.

ex)

PhoneNumber@adbbd   →   010-1234-5678
User@ab8            →   User(id=1, email=dpudpu11@gmail.com, name=Daejun)

toString은 객체가 가진 주요 정보 모두를 반환하는 게 좋다.

    @Builder
    static class Address {
        private final String detail;
        private final String street;
        private final String city;
        private final int zipCode;

        @Override
        public String toString() {
            return "Address{" +
                    "street='" + street + '\'' +
                    ", city='" + city + '\'' +
                    '}';
        }
    }

위에 있는 toString()처럼 일부 필드만 반환하면 안 된다.

  • 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다. 이런 상황이라면 맨해튼 거주자 전화번호부(총 1487536개)Thread[main, 5, main] 같은 요약정보를 담아야 한다.

toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다.

전화번호부나 행렬 같은 값 클래스라면 문서화하기를 권한다. 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다.

ex)

class Address {
        private final String detail;
        private final String street;
        private final String city;
        private final int zipCode;

        @Override
        public String toString() {
            return String.format("%s, %s, %s, %d", detail, street, city, zipCode);
        }
}

ex)

Address(zipCode=3006, city=Seoul, street=Songpa-daero 38-gil, detail=1)
PhoneNumber(areaCode=010, prefix=1234, lineNum=5678)

위 방식 보다는 아래 방식이 낫다.

1, Songpa-daero 38-gil, Seoul, 3006
010-1234-5678

포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공 해주면 좋다. ex) BigInteger, BigDecimal

ex)

@Test
void 정적팩터리_상호_전환_테스트() {
    Address actual = Address.builder()
            .zipCode(05676)
            .city("Seoul")
            .street("Songpa-daero 38-gil")
            .detail("1")
            .build();

    Address expected = Address.valueOf(actual.toString());

    assertEquals(expected.toString(), actual.toString());
}

@Builder
static class Address{
    private final String detail;
    private final String street;
    private final String city;
    private final int zipCode;

    public static Address valueOf(String address){
        String[] split = address.split(", ");
        return Address.builder()
                .detail(split[0])
                .street(split[1])
                .city(split[2])
                .zipCode(Integer.parseInt(split[3]))
                .build();
    }

    @Override
    public String toString() {
        return String.format("%s, %s, %s, %d", detail, street, city, zipCode);
    }
}

포맷을 하면 읽기 편하고 좋지만, 포맷을 한번 명시하면 (그 클래스가 많이 쓰인다면) 평생 그 포맷에 얽매이게 된다. (수정이 어렵다)

반대로 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.

포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자. 접근자를 제공하지 않으면 이 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수 밖에 없다. 성능이 나빠지고 필요하지도 않은 작업이다. 게다가 향후 포맷을 바꾸면 시스템이 망가지는 결과를 초래할 수 있다. (toString이 getter로 사용되면 안 된다.)

toString()을 재정의 하지 않아도 되는 경우

  • 정적 유틸리티 클래스
  • enum 타입도 자바가 이미 toString을 제공한다.
  • Lombok의 @ToString을 사용하는 경우

정리

  • toString()을 재정의하자
  • 디버깅, 로깅이 편해진다.
  • 직접 호출하지 않더라도 다른 어딘가에서 사용될 수 있다.
  • 간결하면서 사람이 읽기 쉬운 형태야 한다.
  • 객체가 가진 모든 정보를 반환하는 게 좋다.
  • 포맷을 할지 고민해본다.

피드백 언제든지 환영합니다.

Reference

이펙티브 자바 - 아이템 12 toString을 항상 재정의하라