ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 이펙티브 자바 : toString
    개발 나누고 더하기/자바, 스프링 2023. 6. 30. 21:16

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

    Object의 toString

     아래 PhoneNumber 클래스를 예로 들면 PhoneNumber@adbbd처럼 단순히 클래스_이름@16진수로_표시한_해시코드로 반환하기 보다는 707-867-5309처럼 전화번호를 직접 알려주는 형태가 훨씬 유익하다. 또한 toSting의 규약은 "모든 하위 클래스에서 이 메서드를 재정의하라"고 한다. 새겨들어야 할 조언이다.

    public final class PhoneNumber {
        private final int areaCode, prefix, lineNum;
        
        //...
    }

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

     PhoneNumber용 toString을 제대로 재정의했다면 다음 코드만으로 문제를 진단하기에 충분한 메세지를 남길 수 있다.

    System.out.println(phoneNumber + "에 연결할 수 없습니다.");

    toString을 재정의했든 아니든 프로그래머 대부분은 진단 메세지를 이렇게 만들 것이다. 재정의를 하지 않았다면 그다지 쓸모가 없는 메세지가 출력된다. 좋은 toString은 (특히 컬렉션처럼) 이 인스턴스를 포함하는 객체에서 유용하게 쓰인다. map 객체를 출력했을 때 {Jenny=PhoneNumber@adbbd} 보다는 {Jenny=707-867-5309}라는 메세지가 나오는게 훨씬 낫다.

     실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다. 앞서의 전화번호처럼 말이다. 하지만 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다. 이런 상황이라면 "맨해튼 거주자 전화번호부(총 1487536개)"나 "Thread[main,5,main]" 같은 요약 정보를 담아야한다. 이상적으로는 스스로를 완벽히 설명하는 문자열이어야 한다(방금의 스레드 예는 이 조건에는 맞지 않다). 다음의 테스트 실패 메세지는 toString에 주요 정보가 담기지 않았을 때 문제가 되는 대표적인 예다.

    // 내부적으로 다른 값이 존재하여 단언에 실패하였지만, toString으로는 같은 값을 표현해준다. 어 뭐지?라고 혼란스러울 것이다.
    Assertion failure: expected {abc, 123} but was {abc, 123}

     

     toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다. 이는 아주 중요한 선택이다. 전화번호나 행렬 같은 값 클래스라면 문서화하기를 권한다. 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다. 단점도 있는데, 포맷을 한번 명시하면 (그 클래스가 많이 쓰인다면) 평상 그 포맷에 얽매이게 된다. 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.

     포맷을 명시하든 아니든 의도는 명확히 밝혀야 한다. 포맷을 명시하려면 아주 정확하게 해야 한다. PhoneNumber 클래스용 toString 메서드를 보자.

    /**
     * 이 전화번호의 문자열 표현을 반환한다.
     * 이 문자열은 "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를 제공하자. 예컨대 PhoneNumber 클래스는 지역 코드, 프리픽스, 가입자 번호용 접근자를 제공해야 한다. 그렇지 않으면 이 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수밖에 없다. 성능이 나빠지고, 필요하지도 않은 작업이다. 게다가 향후 포맷을 바꾸면 시스템이 망가지는 결과를 초래할 수 있다. 접근자를 제공하지 않으면 (변경될 수 있다고 문서화했더라도) 그 포맷이 사실상 준-표준 API나 다름없어진다.

     

    댓글

Designed by Tistory.