반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 toString 메서드는 오브젝트 내 멤버필드를 출력하는 용도로 사용될 것이라 예상하지만, 실제로 '클래스이름@16진수 해시코드'를 반환한다. 언뜻보면 불필요해보이는 이 메서드는 무엇을 위해 사용되는 걸까?

 


2. toString의 규약

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

 

 toString 메서드의 목적은 위에 기재된 toString의 규약과 같이 유익한 정보를 제공하기 위함이다. 그런데 최상위 클래스인 Object의 toString 메서드를 호출하면 '클래스이름@16진수 해시코드' 를 반환한다. 이는 간결하다 할 수 있지만 유익한 정보는 아니다. 결국 규약을 지키려면 Object에 정의된 toString 을 재정의하여 유익한 정보를 반환하도록 해야한다. 실제로 toString의 다른 규약에서도 재정의를 강조하고 있다.

 

모든 하위 클래스에서 이 메서드를 재정의해야 한다.

 

3. toString의 목적

 규약을 통해 필자가 이해한 toString 메서드의 목적은 사람이 읽을 수 있는 정보를 간결하게 제공하는 것이다. 그리고 이를 위해서는 재정의라는 작업이 반드시 필요하다.


4. 자동 호출되는 toString

 클래스를 println, printf 등의 메서드로 출력할 경우 자동으로 해당 클래스의 toString 메서드가 호출된다. 결국 잘 정의된 toString을 통해 개발자에게 정보를 제공하게 된다면, 디버깅이나 로깅 시 유용하게 활용될 수 있는 것이다.

 

class Human{
    private final String name;
    private final int age;

    public Human(String name, int age){
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Human{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}


...

public static void main(String[] args) {
	Human human = new Human("테스터",19);
    System.out.println(human); // 자동으로 toString 메서드가 호출됨.
}

자동 호출된 toString()

 

 


5. 잘 정의된 toString 

 

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

 주요 정보를 모두 반환해야 하는게 좋다. 만약 일부 정보만 담겨 있다면, 특정 상황에서의 혼란을 야기할 수 있다. 아래는 Human 클래스의 멤버필드 중 일부만 반환하도록 toString()을 재정의하였다.

 (※ 테스트 시 비교에 사용될 equals와 hashCode 메서드는 올바르게 재정의하였다.)

 

public class Human {

    private final String name;
    private final int age;
    private final long height;
    private final long weight;

    public Human(String name, int age, long height ,long weight){
        this.name = name;
        this.age = age;
        this.height = height;
        this.weight = weight;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Human human = (Human) o;
        return age == human.age && height == human.height && weight == human.weight && Objects.equals(name, human.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, height, weight);
    }


    // 일부 정보만 반환하는 toString
    @Override
    public String toString() {
        return "Human{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

 

 assertj를 통해 두 Human 클래스를 weight만 다르게 하여 생성 후 isEquals를 통해 비교해보자.

@Test
public void test(){

    Human human1 = new Human("홍길동", 34, 203, 110);
    Human human2 = new Human("홍길동", 34, 203, 80);

    assertThat(human1).isEqualTo(human2);
}

weight 값이 다르기 때문에 테스트가 실패하는 건 당연하다. 하지만 테스트에 대한 실패 메시지를 보면 "값은 똑같은데 왜 실패하지?" 라는 의문이 들 것이다. 이는 단순히 객체의 일부 정보만 반환해서 발생했다.

 

 

 

테스트 결과

 


5.2. 너무 많은 정보를 갖는 객체는 요약 정보를 반환하라.

 객체의 정보가 너무 많다면 요약 정보를 반환하도록 해야 한다. 아래와 같이 거주자 리스트를 담고있는 Town 클래스에 대해 모든 정보를 출력하도록 toString을 재정의한다면 불필요하게 너무 많은 정보를 반환할 수도 있다. 

 

public class Town {
    private String name;
    private List<Human> residentList;

    private Town(String name, List<Human> residentList){
        this.name = name;
        this.residentList = residentList;
    }
    public static Town of(String name, List<Human> residentList){
        return new Town(name, residentList);
    }

    @Override
    public String toString() {
        return "Town{" +
                "name='" + name + '\'' +
                ", residentList=" + residentList +
                '}';
    }
}

 

 이때는 아래와 같이 요약정보를 반환할 수 있도록 재정의하자.

@Override
    public String toString() {
        return "Town{" +
                "name='" + name + '\'' +
                ", residentList Count =" + residentList.size() +
                '}';
    }
}

출력 결과

 


5.3. 반환 값의 포맷을 지정하지 말자

 전화번호나 행렬 같은 값 클래스의 경우 반환 값이 데이터의 성질에 따라 포맷팅 될 수 있다. 예를들어 전화번호의 경우 아래의 포맷을 가질 수 있다.

@Override
public String toString(){
    return String.format("%03d-%03d-%04d",
            areaCode, prefix, lineNum);
}

 이렇게 포맷이 정의되어 있으면 포맷 형식에 맞게 문서 형식을 만들고 재가공하는 2차 작업이 있을 수 있다. 이때 포맷이 바뀌게 된다면 이러한 작업에 영향을 미치게 된다. 반대로 2차적인 작업에 영향을 미치게 하지 않기 위해 데이터가 포맷에 의존하도록 한정될 수도 있다.

 만약 작업이 있다면 toString을 통해 포맷된 데이터를 가져오거나, toString을 통해 원본 데이터를 가져오고 이를 포맷팅하지는 메서드를 따로 만들지 말고, toString이 반환할 값을 얻어올 수 있는 API를 따로 제공하는게 바람직하다.

 


5.4. IDE에서 제공하는 기능을 활용하자.

 객체의 모든 정보를 반환해주는 toString 메서드는 여러 IDE에서 기본으로 제공한다. 객체의 정보를 알려주지 않는 Object의 toString 메서드를 사용하지 말고, IDE에서 제공하는 toString을 사용하는 것도 좋은 방법이다.

 


6. 정리

 모든 클래스에서 toString을 재정의하자. 단, 상위 클래스에서 이미 알맞게 재정의한 경우는 예외이다. toString은 객체에 관한 명확하고 유용한 정보를 읽기 좋은 형태로 반환해야 하며, 이러한 toString은 디버깅와 로깅에 유용하게 사용될 수 있다.

 

반응형

+ Recent posts