본문 바로가기
☕️ JAVA/🦋 Effective Java

모든 구현 클래스에서 Object의 toString 재정의를 고려하자 🗣

by kukim 2022. 1. 15.

이 글은 책 '이펙티브 자바 3판 Item 12'와 하단 Reference 참고했습니다. 잘못된 내용이 있다면 편하게 말씀해주세요 🙏🏻

 

주제

공통 메서드 : 자바의 클래스들은 최상위 클래스 “Object”를 모두 상속받고 있다. Object에서 final 메서드가 아닌 메서드 (equals, hashCode, toString, clone, finlized)가 있다. 최상위 클래스에서 final 메서드를 제외한 메서드를 하위 클래스에 상속시킬까? 바로 클래스를 만드는 프로그래머, 사용자가 재정의하여 사용하라고 설계했기 때문이다. 하지만 규칙 없이 사용자 마음대로 사용하면 치명적일 수 있기 때문에 Object API 명세에 일반 규약이 명확히 정의되어 있다.

 

그중 toString() 메서드를 어떻게 오버라이딩하고 사용해야 하는지 알아보자

 

결론

모든 구현 클래스에서 Object의 toString을 재정의하여 사용하자. (정적 유틸리티 클래스, 열거 타입, 상위 클래스 등 toString()가 이미 잘 구현되어있는 경우를 제외하고)


Object 클래스 toString() 메서드의 실제 구현 코드

먼저 최상위 클래스 Object의 toString() 구현 코드를 살펴보자.

public class Object {
// ... 생략

	public String toString() {
	    return getClass().getName() + "@" + Integer.toHexString(hashCode());
	}

// ... 생략
}

public class Object클래스toString확인 {
    public static void main(String[] args) {
        Object object = new Object();
        System.out.println(object.toString());
    }
}

// 출력 결과
java.lang.Object@515f550a // 알아보기 힘들다.

toString()의 기본값은 “클래스이름_이름@16진수_해시코드” 으로 사람이 알아보기 힘들다. 

아래 예는 Book 클래스에서 toString() 메서드를 오버라이딩하지 않고 Object의 결과를 그대로 사용했다. toString()의 결과로 사용자에게 크게 의미가 없다. 내용을 알기란 쉽지 않다.

package item12;

public class Book {
    private String title;
    private String author;

    public Book() {

    }

    public static Book createBookWithTitleAndAuthor(String title, String author) {
        Book book = new Book();
        book.title = title;
        book.author = author;
        return book;
    }

    public static void main(String[] args) {
        Book effectiveJava = Book.createBookWithTitleAndAuthor("이펙티브자바", "조슈아 블로크");
        System.out.println(effectiveJava.toString()); // 위아래 출력 코드 결과 동일
        System.out.println(effectiveJava);
    }
}


// 출력 결과
// 출력 결과는 toString() 오버라이딩 하지 않으면 기본값의 형태로 나온다.
item12.Book@515f550a
item12.Book@515f550a

 

잠깐

System.out.println(Object)와 System.out.println(Object.toString())의 결과는 동일하다. 왜 그럴까?

System.out.println()의 println() 메서드는 많은 매개변수의 경우의 수로 오버로딩 되어있다. 따라서 입력 매개변수에 따라 메서드가 다르다(오버로딩)

그중 Object 객체가 들어온다면 System.out.println(Object x) 메서드가 실행된다. 그 내부 코드를 살펴보면 아래와 같다.

public static void main(String[] args) {
    Object object = new Object();
	System.out.println(object);
}

// System.out.println(Object x)
public void println(Object x) { 
        String s = String.valueOf(x); // <-
        synchronized (this) {
            print(s);
            newLine();
        }
    }

// String.valeOf(Object obj)
public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString(); // obj의 toString을 리턴
    }

println(Object)에서 valueOf(Object)를 호출하고 그 리턴값이 toString() 메서드이다.

 

toString() 재정의의 장점

오버라이딩 하지 않는다면 ‘item12.Book@515f550a’ 처럼 내용이 없다. toString()의 일반 규약에 따르면 ‘간결하면서 사람이 읽기 쉬운 형태의 유익한 정보’를 반환하여 사용한다.

package item12;

public class Book {
    private String title;
    private String author;

    public Book() {

    }

    public static Book createBookWithTitleAndAuthor(String title, String author) {
        Book book = new Book();
        book.title = title;
        book.author = author;
        return book;
    }

    @Override
    public String toString() {
        return "제목 : " + title + " / 저자 : " + author;
    }

    public static void main(String[] args) {
        Book effectiveJava = Book.createBookWithTitleAndAuthor("이펙티브자바", "조슈아 블로크");
        System.out.println(effectiveJava);

    }
}

// 출력 결과
제목 : 이펙티브자바 / 저자 : 조슈아 블로크

재정의 전 : ‘item12.Book@515f550a’

재정의 후 : ‘제목 : 이펙티브자바 / 저자 : 조슈아 블로크’

재정의 후 사람이 읽기 쉬운 형태의 정보가 되었다. 이는 사용자 입장에서 toString()의 결과를 입출력에 사용하기 쉽고 디버깅할 때도 장점이 있다.(println, 문자열 연결 연산자(+), assert 넘길 때 모두 toString()으로 인식한다)

 

만약 toString()를 재정의하지 않는다면 쓸모없는 정보를 얻거나 만약 위와 같은 ‘제목 : 이펙티브자바 / 저자 : 조슈아 블로크’ 의 결과를 디버깅하기 위해서는 사용자가 직접 아래와 같이 작성해야 하는 불상사가 발생한다.

System.out.println("제목 : " + effectiveJava.getTitle() + " / 저자 : " + effectiveJava.getAuthor());

 

자바 컬렉션의 경우 이미 toString이 오버라이딩이 되어있어 편리하게 데이터를 볼 수 있다.

public class collectionTostringTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        list.add(1);
        list.add(2);
        list.add(3);

        System.out.println(list); 
    }
}

// 출력결과
[1,2,3]

toString() 재정의, 고려사항

어떤 값을 return 해야 하는가?

toString은 그 객체가 가진 주요 정보를 모두 반환하는 게 좋다. 하지만 객체가 거대하거나 객체의 상태가 문자열로서 표현하기에 문제가 있다면 요약해서 반환한다. e.g. PhoneBook 객체에 전화번호가 10000개가 들어있다면 “전화번호(총 10000개)” 식으로 말이다.

 

return 값의 포맷을 문서화 하자

전화번호나 행렬 같은 값 클래스라면 문서화를 권한다. 왜냐하면 return 값의 포맷을 명시하면 표준적이고 사용자 입장에서 값 그대로 입출력에 사용하기 용이하기 때문이다. (+a 문서에 정적 팩터리나 생성자를 함께 작성해주면 이해가 쉽다. (사진 예)

BigInteger의 toString API 문서

문서화의 유의사항

포맷을 한번 명시하면 평생 그 포맷에 얽매이게 된다. 이미 사용자들이 해당 클래스를 많이 사용하는 중 릴리스 되어 포맷이 바뀐다면 기존 코드에 문제가 발생한다.

 

따라서 포맷의 정도, 의도를 명확히 밝혀야 한다. 포맷이 정확한 경우(위), 모호한 경우(아래)

	/**
     * 이 전화번호의 문자열 표현을 반환한다.
     * 이 문자열은 "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() { ... }

 

결론

모든 구현 클래스에서 Object의 toString을 재정의하여 사용하자. (정적 유틸리티 클래스, 열거 타입, 상위 클래스 등 toString()가 이미 잘 구현되어있는 경우를 제외하고)

 Reference

https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

https://docs.oracle.com/javase/7/docs/api/java/math/BigInteger.html

http://www.yes24.com/Product/Goods/65551284

https://github.com/Meet-Coder-Study/book-effective-java

댓글