-
이펙티브 자바 : 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나 다름없어진다.
'개발 나누고 더하기 > 자바, 스프링' 카테고리의 다른 글
스프링 가이드 - 간단한 RESTful API 만들어보기 (0) 2024.02.13 이펙티브 자바 : 제네릭과 배열 그리고 리스트 (0) 2023.07.06 이펙티브 자바 - equals & hashcode (0) 2023.06.28 Set에 대하여 2 : 주요 구현체 (0) 2023.06.25 Set에 대하여 1 : 구조와 주요 메서드 (0) 2023.06.23