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

cchoijjinyoung·2023년 9월 1일
0

이펙티브자바

목록 보기
4/5

들어가기 앞서

아이템12는 모든 객체의 공통 메서드 중 toString() 의 재정의에 대해 설명하고 있다.

toString 메서드에 대해 간단히 알아보자면,

  • Java의 Object클래스에 정의된 메서드이다.
  • 객체의 정보를 문자열로 반환할 때 사용한다.
  • 모든 Java 객체는 Object를 상속받기 때문에 toString 메서드를 가지고 있다.

하지만 Object의 기본 toString 메서드가 우리가 작성한 클래스에 적합한 문자열을 반환하는 경우는 거의 없다.

이 메서드는 PhoneNumber@adbdb 처럼 단순히 클래스_이름@16진수로 표시한 해시코드를 반환할 뿐이다.

toString의 일반 규약에 따르면,
'간결하면서 사람이 읽기 쉬운 형태의 유익한 정보' 를 반환해야 한다.

예를 들면, PhoneNumber@adbdb 보다는 010-1234-5678 처럼 전화번호로 직접 표현하는게 유익하다.

또 다른 규약으로는,
'모든 하위 클래스에서 이 메서드를 재정의하라' 이다.

저자는 해당 규약을 "정말 새겨들어야 할 조언이다!" 라고 말한다.
. . .
* 개인적인 생각으로는 객체의 내부 상태를 외부로 노출시키고 싶지 않은 경우나 문자열 표현이 큰 의미를 갖지 않는 경우는 toString을 재정의할 필요가 없다고 생각한다.


좋은 예와 나쁜 예

toString 메서드는 아래 상황에서 자동으로 호출한다.

  • println, printf
  • 문자열 연결 연산자 (+)
  • assert 구문에 넘길 때
  • 디버거가 객체를 출력할 때

좋은 toString은 이 인스턴스를 포함하는 객체에서 유용하게 쓰인다.
예를 들어 map 객체를 출력했을 때 {Jenny = 010-1234-5678} 처럼 말이다.

다음으로는 toString에 주요 정보가 담기지 않았을 때 문제가 되는 대표적인 예인 테스트 실패 메시지이다.

Assertions failure: expected {abc, 123}, but was {abe, 123}
// 단언 실패: 예상 값 {abc, 123}, 실젯값 {abc, 123}

아래와 같은 경우에 위와 같은 메시지가 나올 수 있다.
다른 예시로 보겠다.

public class Wizard {
    private String lastName;
    private String firstName;
    
    @Override
    public String toString() {
        return "Wizard{" +
                "성 ='" + lastName + '\'' +
                '}';
    } // toString 에 lastName 정보만 담겨져 있다.
}
class WizardTest {
    @Test
    void toString_테스트() {
        Wizard w1 = new Wizard("포터", "제임스");
        Wizard w2 = new Wizard("포터", "해리");

        Assertions.assertEquals(w1, w2); // 둘의 equals 비교
    }
}
❌ toString_테스트() // 서로 다르다!
Expected :Wizard{='포터'}
Actual   :Wizard{='포터'} // 그러나 보기에는 같아보임.

포맷과 문서화

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

전화번호나 행렬 같은 값 클래스는 문서화하길 권장한다.
포맷을 명시하면('010-1234-5678' 와 같이) 사람이 읽기 쉽다.
따라서 이 값 그대로 입출력에 사용하거나, 사람이 읽을 수 있는 데이터 객체로 저장할 수도 있다.
포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 서로 전환할 수 있는 정적 팩터리나 생성자를 함께 제공하면 좋다.

// BigInteger, BigDecimal과 대부분의 기본 타입 클래스가 여기 해당한다.
String s = "10000000000000";
BigInteger b1 = new BigInteger(s);

포맷을 명시하면 단점도 있는데,

포맷을 한 번 명시 후에, 프로그래머들이 해당 포맷에 맞춰 의존적인 코드를 작성한다면
포맷을 바꿨을 때 그 코드들은 엉망이 돼버린다.
반대로 포맷을 명시하지 않는다면, 이 부분에서는 유연성을 얻게 된다.

포맷을 명시하든 아니든 의도를 명확히 밝히자.

이펙티브 자바에서 가져온 코드다.

  • 명시한 경우
/**
* 이 전화번호의 문자열 표현을 반환한다.
* 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
* XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
* 각각의 대문자는 10진수 숫자 하나를 나타낸다.
* 
* 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
* 앞에서부터 0으로 채워나간다. 예컨데 가입자 번호가 123이라면
* 전화번호의 마지막 네 문자는 "0123"이 된다.
*/
@Override 
public String toString() {
    return String.format("%03d-%03d-%04d", areaCode, prefix, lineNum);
}
  • 명시하지 않은 경우
/**
* 이 약물에 관한 대략적인 설명을 반환한다.
* 다음은 이 설명의 일반적인 형태이나,
* 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
* 
* "[약물 #9: 유형-사랑, 냄새=테러빈유, 겉모습=먹물]"
*/
@Override
public String toString() { ... }

이러한 설명이 있어야 프로그래머들이 이 포맷에 맞춰 코드를 작성할 것이다.
아래 예시를 보면 상세 형식은 정해지지 않았으며 향후 변경될 수 있다. 라고 적혀있다.
그럼에도 불구하고 해당 포맷의 값을 가공한 코드를 작성하면 자신을 탓할 수 밖에 없다.

toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자.

이건 getter 와 같은 메서드를 제공하라는 의미이다.
어차피 toString으로 공개된 데이터라면, toString을 구성하는 각각의 데이터를 따로따로 받을 수 있는 메서드들을 제공하자는 것이다.
만약 PhoneNumber.class 의 각 필드에 대한 getter가 없다면,
각 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수 밖에 없다. 상당히 비효율적이고 필요없는 작업이다.


자동 생성

equals(), hashCode() 메서드들과 마찬가지로 toString()도 구글의 AutoValue 프레임워크나 IDE에서 생성해주는 기능이 있다. 하지만 '클래스의 특성' 까지는 파악하지 못한다. 예컨대 앞서의 PhoneNumber 클래스가 이런 경우이다. 하지만, 이런 경우라도 Object의 toString 보다는 훨씬 유용하다.

profile
반갑습니다 :)

0개의 댓글

관련 채용 정보