ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 12: Always override toString
    독서/Effective Java 2021. 12. 12. 15:28

    항상 toString을 재정의하라.

     

    재정의의 이유

    Object는 toString 메서드의 구현을 제공하지만 반환하는 문자열은 "일반적으로는" 내가 원하는 형태는 아니다.

    package com.tistory.povia.effectivejava.item1;
    
    public class User {
    
      private static final int FIRST_AGE = 1;
    
      private final String firstName;
    
      private final String lastName;
    
      private int age;
    
      private User(String firstName, String lastName, int age){
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
      }
    
      public static User newBaby(String firstName, User father){
        return new User(firstName, father.lastName, FIRST_AGE);
      }
    
      public static User immigration(String firstName, String lastName, int age){
        return new User(firstName, lastName, age);
      }
    
      public String firstName() {
        return firstName;
      }
    
      public String lastName() {
        return lastName;
      }
    }
    

     

    이 클래스의 toString()을 바로 실행해보면 어떨까?(String 형태로 사용하기만 하면 자동으로 내부적으로 toString을 사용한다.)

    father = User.immigration("boris", "Chak", 32);
    log.info("father = {}", father);
    
    
    // 실제 출력된 로그
    father = com.tistory.povia.effectivejava.item1.User@b1712f3

    출력된 결과물은 다음과 같은 구조로 이루어져 있다.

    * 클래스 위치, 클래스 이름, @, 해시 코드의 부호 없는 16진수 표현

    toString에 대한 일반 규약 = "간단하지만 사람이 읽기 쉬운 정보를 제공하는 표현"

    여기에서의 toString이라면 결과가 "boris Chak, 32" 등으로 나와야 하지 않을까?

     

    재정의, 그리고 장점

    toString을 직접 구현했을 때의 장점은 다음과 같다.

    1) 클래스를 훨씬 더 사용하기 편해진다.

    2) 클래스를 디버그하기가 더 쉬워진다.


    만약 위의 User 클래스에서 toString()를 재정의했다면?

    // 재정의
      @Override
      public String toString() {
        return "User{" +
          "Name='" + firstName + ' ' + lastName + '\'' +
          ", age=" + age +
          '}';
      }
      
    // 실제 로그
    User{Name='boris Chak', age=32}

    위의 Object에 대한 정보'만' 출력되던 것에서 벗어나 해당 클래스의 속성을 명시적으로 파악할 수 있게 되었다.

    이로 인해 추가되는 장점은 다음과 같다.

    예시로 하나의 가족에 3명의 구성원이 있다고 가정해보자.

      @Test
      @DisplayName("자손이 태어나는 경우")
      void birthTest(){
        List<User> family = new ArrayList<>();
        family.add(father);
        family.add(User.immigration("cally", "hu", 30));
        User son = User.newBaby("sonas", father);
        family.add(son);
        log.info("family = {}", family);
        Assertions.assertThat(son.lastName()).isEqualTo("Chak");
      }
      
    // 실제 출력
    family = [User{Name='boris Chak', age=32}, User{Name='cally hu', age=30}, User{Name='sonas Chak', age=1}]

    toString을 재정의한 이후 family를 출력하면 원하던 대로 User를 한 눈에 파악할 수 있는 결과가 출력되었다.

     

    재정의 시 주의해야할 점

    1. 그 Object가 가진 주요 정보 모두(여기서는 이름, 나이)를 반환하는게 좋다.
    2. 하지만 객체가 거대하거나 문자열로 표현하기 적합하지 않다면 => "family members count = 3" 등으로 요약 정보를 표시하는 것이 좋다.
    3. 반환값의 포맷을 문서화할지 정해야 한다
      1. 전화번호나 행렬 같은 값 클래스 => 문서화 하기를 권한다.
      2. 포맷을 명시하게 된다면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다.
      3. 포맷을 명시하든 아니든 의도는 명확히 밝혀야 한다. 명시하려면 아주 정확하게 명시하자. 
      4. 명시하기로 정했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩토리나 생성자를 함께 제공해주면 좋다. ex) BigInteger, BigDecimal
        // 명시화를 한 예시
         /**
           * 사용자 정보를 문자열로 변환한다.
           * Name 은 이름 + " " + 성으로 구성한다.
           * age 는 나이이다.
           * 
           * 출력 형식은 다음과 같다.
           * @return "User{Name=firstName + ' ' + lastName, age=age} 
           */
          @Override
          public String toString() {
            return "User{" +
              "Name='" + firstName + ' ' + lastName + '\'' +
              ", age=" + age +
              '}';
          }​
    4.  포맷 명시의 단점
      1. 한번 명시하면 평생 그 포맷에 얽매이게 된다.
      2. 처음에 명시한대로 프로그래머들이 코드를 작성할 것인데 추후에 포맷을 바꾼다면 이를 사용하던 코드들과 데이터들은 엉망이 될 것이다.
      3. 반대로 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.

     

    불필요한 경우

    • 정적 유틸리티 클래스
    • 대부분의 열거 타입
    • Annotation(자동 생성) 사용(구글의 @Autovalue, Lombok의 @ToString) 
      • Lombok @ToString은 주의해서 써야한다. 재정의시 순환참조를 주의하자.

     

    댓글

Designed by Tistory.