어느 날 누가 내게 toString()은 언제, 어떻게 써야 하냐고 물었는데 대답할 수가 없었다.
그동안 생각 없이 toString()을 사용했구나! 생각이 들면서 마침 이펙티브 자바에 설명이 잘 되어있어서 정리 + 예제를 작성했다.
모든 하위 클래스에서 toString()을 재정의하라
toString의 규약은 "모든 하위 클래스에서 이 메서드를 재정의하라"고 한다.
toString()을 재정의할 경우 이점으로는 아래와 같다.
- 로깅, 디버깅 또는 문자열로 만나는 모든 객체를 렌더링 할 수 있어야하는 상황에서 유용하다.
- 직접 호출하지 않더라도 다른 어딘가에서 쓰일 수 있다.
- 오류 메시지를 로깅할 때 자동으로 호출할 수 있다. (toString을 제대로 재정의하지 않는다면 쓸모없는 메시지만 로그에 남을 것이다.
toString을 재정의 했을 경우
디버깅할 때 Piece의 정보를 한 눈에 볼 수 있어서 편하다.
toString을 재정의하지 않았을 경우
반면 재정의를 하지 않으면 디버깅할 때 클래스이름@16진수로 표시한 해시코드로 반한 되므로 Piece의 정보를 알기가 어렵다. 이 경우 정보를 알기 위해서는 아래에 있는 이미지처럼 디버깅 창에서 일일이 확인해줘야 한다.
간결하면서 사람이 읽기 쉬운 형태의 유익한 정보를 반환해야 한다.
ex)
PhoneNumber@adbbd → 010-1234-5678
User@ab8 → User(id=1, email=dpudpu11@gmail.com, name=Daejun)
toString은 객체가 가진 주요 정보 모두를 반환하는 게 좋다.
@Builder
static class Address {
private final String detail;
private final String street;
private final String city;
private final int zipCode;
@Override
public String toString() {
return "Address{" +
"street='" + street + '\'' +
", city='" + city + '\'' +
'}';
}
}
위에 있는 toString()처럼 일부 필드만 반환하면 안 된다.
- 객체가 거대하거나 객체의 상태가 문자열로 표현하기에 적합하지 않다면 무리가 있다. 이런 상황이라면 맨해튼 거주자 전화번호부(총 1487536개) 나 Thread[main, 5, main] 같은 요약정보를 담아야 한다.
toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다.
전화번호부나 행렬 같은 값 클래스라면 문서화하기를 권한다. 포맷을 명시하면 그 객체는 표준적이고, 명확하고, 사람이 읽을 수 있게 된다.
ex)
class Address {
private final String detail;
private final String street;
private final String city;
private final int zipCode;
@Override
public String toString() {
return String.format("%s, %s, %s, %d", detail, street, city, zipCode);
}
}
ex)
Address(zipCode=3006, city=Seoul, street=Songpa-daero 38-gil, detail=1)
PhoneNumber(areaCode=010, prefix=1234, lineNum=5678)
위 방식 보다는 아래 방식이 낫다.
1, Songpa-daero 38-gil, Seoul, 3006
010-1234-5678
포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공 해주면 좋다. ex) BigInteger, BigDecimal
ex)
@Test
void 정적팩터리_상호_전환_테스트() {
Address actual = Address.builder()
.zipCode(05676)
.city("Seoul")
.street("Songpa-daero 38-gil")
.detail("1")
.build();
Address expected = Address.valueOf(actual.toString());
assertEquals(expected.toString(), actual.toString());
}
@Builder
static class Address{
private final String detail;
private final String street;
private final String city;
private final int zipCode;
public static Address valueOf(String address){
String[] split = address.split(", ");
return Address.builder()
.detail(split[0])
.street(split[1])
.city(split[2])
.zipCode(Integer.parseInt(split[3]))
.build();
}
@Override
public String toString() {
return String.format("%s, %s, %s, %d", detail, street, city, zipCode);
}
}
포맷을 하면 읽기 편하고 좋지만, 포맷을 한번 명시하면 (그 클래스가 많이 쓰인다면) 평생 그 포맷에 얽매이게 된다. (수정이 어렵다)
반대로 포맷을 명시하지 않는다면 향후 릴리스에서 정보를 더 넣거나 포맷을 개선할 수 있는 유연성을 얻게 된다.
포맷 명시 여부와 상관없이 toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하자. 접근자를 제공하지 않으면 이 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수 밖에 없다. 성능이 나빠지고 필요하지도 않은 작업이다. 게다가 향후 포맷을 바꾸면 시스템이 망가지는 결과를 초래할 수 있다. (toString이 getter로 사용되면 안 된다.)
toString()을 재정의 하지 않아도 되는 경우
- 정적 유틸리티 클래스
- enum 타입도 자바가 이미 toString을 제공한다.
- Lombok의 @ToString을 사용하는 경우
정리
- toString()을 재정의하자
- 디버깅, 로깅이 편해진다.
- 직접 호출하지 않더라도 다른 어딘가에서 사용될 수 있다.
- 간결하면서 사람이 읽기 쉬운 형태야 한다.
- 객체가 가진 모든 정보를 반환하는 게 좋다.
- 포맷을 할지 고민해본다.
피드백 언제든지 환영합니다.
Reference
이펙티브 자바 - 아이템 12 toString을 항상 재정의하라
'Java' 카테고리의 다른 글
[객체지향] OCP (개방 폐쇄의 원칙 : Open Close Principle) (0) | 2019.12.28 |
---|---|
[객체지향] if-else의 문제점 (0) | 2019.12.28 |
if문 없이 문자열 계산하기 (feat. 전략패턴, Enum) (0) | 2019.05.12 |
Java는 Call by Value? Call by Reference? (0) | 2019.03.05 |
자바 입출력 IO (0) | 2018.11.30 |