toString을 항상 재정의 하자

2021-01-13

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

PhoneNumber 클래스 toString 호출

PhoneNumber myHone = new PhoneNumber(02, 444, 7777);
System.out.println(myHone); 

위에 코드를 실행하면 PhoneNumber@23fc625e 이 출력이 된다. 클래스_이름@16진수로_표시한_해시코드가 반환되는 것이다.

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

toString을 잘 구현한 클래스는 사용하기에 훨씬 즐겁고, 그 클래스를 사용한 시스템은 디버깅 하기 쉽다. toString 메서드는 객체를 println, printf, 문자열 연결 연산자(+), assert 구문에 넘길 때 혹흔 디버거가 객체를 출력할 때 자동으로 불린다. 우리가 직접 호출하지 않더라도 다른 어딘가에서 쓰일 거란 이야기다. 예컨대 우리가 작성한 객체를 참조하는 컴포넌트가 오류 메세지를 로깅할 때 자동으로 호출할 수 있다. toString을 제대로 재정의하지 않는다면 쓸모없는 메세지만 로그에 남을 것이다.

toString 구현

 toString을 구현할 때 반환값의 포맷을 문서화할지 정해야 한다. 전화번호나 행렬 같은 값 클래스라면 문서화하기를 권한다. 포맷을 명시하면 그 객체는 표준적이고 명확하고, 사람이 읽을 수 있게 된다. 따라서 그 값 그대로 입출력에 사용하거나 CSV 파일 처럼 사람이 읽을 수 있는 데이터 객체로 저장할 수도 있다. 포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋다. 자바 플랫폼의 많은 값 클래스가 따르는 방식이기도 하다. BigInteger, BigDecimal과 대부분의 기본 타입 클래스가 여기 해당한다.

단점도 있다. 포맷을 한번 명시하면 평생 그 포맷에 얽히게 된다. 이를 사용하는 프로그래머들이 그 포맷에 맞춰 파싱하고, 새로운 객체를 만들고, 영속 데이터로 저장하는 코드를 작성할 것이다. 만약 향후 릴리스에서 포맷을 바꾼다면 이를 사용하던 코드들과 데이터들은 엉망이 될 것이고, 프로그래머들은 절규할 것이다. 반대로 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.

포맷을 명시하든 아니든 의도를 명확히 밝혀야 한다.

		/**
     * 이 전화번호를 문자열 표현을 반환한다.
     * 이 문자열은 "XXX-YYY-ZZZZ" 형태의 12글자로 구성된다.
     * XXX는 지역코드, YYY는 프리픽스, ZZZZ는 가입자 번호다.
     * 각각의 대문자는 10진수 숫자 하나를 나타낸다.
     * 
     * 전화번호의 각 부분의 값이 너무 작아서 자릿수를 채울 수 없다면,
     * 앞에서부터 0으로 채워나간다. 예컨대 가입자 번호가 123이라면
     * 전화번호의 마지막 네 문자는 "0123"이 된다.
     */
@Override
public String toString() {
  return String.format("%03d-%03d-%04d",
                       areaCode,prefix,lineNum);
}

포맷을 명시하지 않기로 했다면 다음처럼 작성할 수 있다.

		/**
     * 이 약물에 관한 대략적인 설명을 반환한다.
     * 다음은 이 설명의 일반적인 형태이나,
     * 상세 형식은 정해지지 않았으며 향후 변경될 수 있다.
     * 각각의 대문자는 10진수 숫자 하나를 나타낸다.
     * 
     * "[약물 #9 : 유형=사랑, 냄새=테레빈유, 겉모습=먹물]"
     */
@Override
public String toString() {...}

이펙티브자바 저자는, 포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 값에 포함된 정보를 얻어올 수 있는 API를 제공하자. 예컨대 PhoneNumber 클래스는 지역 코드, 프리픽스, 가입자 번호용 접근자를 제공해야 한다 라고 나와 있다.

public class PhoneNumber {
    private int areaCode;
    private int prefix;
    private int lineNum;

  	//toString() 때문에 접근자를 제공해줘야 할까? 
		public int getAreaCode {return this.areadCode;}
  	public int getPrefix {return this.prefix;}
  	public int getLineNum {return this.lineNum;}
}

나는 이 말에 반대를 하고 싶다. 예를 들어, 지역코드는 PhoneNumber 클래스 내에서만 필요하므로, 외부에서 이 데이터에 접근할 필요가 없어서, getter를 제공해주지 않았는데, toString을 오버라이드 할 때, 이 데이터도 함께 toString의 반환값에 포함되었다는 이유로 getter를 만들어 줘야 하나? 아니면 애초에 toString()을 만들 때, 반환 값에서 제외를 했었어야 했나? 라는 역질문을 할 수 있다.

개인적으로는 로그기록이나, 디버깅 때문에 toString에는 모든 필드를 제공해야 한다라고는 생각한다. 그러나 객체지향 적인 관점에서 봤을 때 toString에서 해당 필드를 사용한다고 해서 꼭 getter나 파싱할 수 있는 API를 제공해줘야 하는 것은 필수가 아니라고 생각한다.