12. toString을 항상 재정의하라

12. toString을 항상 재정의하라

🔑 핵심 내용

✔ toString은 객체를 글자로 표현하는 방법이다

✔ 좋은 toString은 디버깅과 로그 확인에 정말 도움이 된다

toString이란 무엇일까?

  • toString은 객체를 문자열(글자)로 바꿔주는 메서드다

  • 모든 자바 객체는 toString 메서드를 가지고 있다

  • Object의 기본 toString은 클래스이름@16진수해시코드 형태로 반환한다 ➡ 예: Student@1b6d3586

🚫 기본 toString의 문제점

public class Student {
    private String name;
    private int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    // toString을 재정의하지 않음
}

// 사용 예시
Student student = new Student("민수", 3);
System.out.println(student); // 출력: Student@1b6d3586

➡ 이 출력값은 "민수"가 3학년이라는 중요한 정보를 알려주지 않는다!

✅ 올바르게 toString()을 재정의한 예시

public class Student {
    private String name;
    private int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    // toString 재정의
    @Override
    public String toString() {
        return name + "(" + grade + "학년)";
    }
}

// 사용 예시
Student student = new Student("민수", 3);
System.out.println(student); // 출력: 민수(3학년)

➡ 이제 출력값이 훨씬 유용해졌다!

toString이 자동으로 호출되는 경우

✔ System.out.println() 에서 객체를 출력할 때

✔ 디버깅 할 때 객체를 볼 때

✔ 문자열 연결(+) 연산에서

✔ 로그 메시지를 출력할 때

✔ assert 구문에서

toString 만들 때 고려할 점

  1. 모든 중요한 정보를 포함한다

    • 객체가 가진 핵심 데이터를 모두 보여준다

  2. 포맷을 문서화할지 결정한다

    • 포맷을 정해두면: 일관된 형태로 사용할 수 있다

    • 포맷을 정하지 않으면: 나중에 마음대로 바꿀 수 있다

좋은 toString의 이점

  1. 디버깅이 쉬워진다

    // 좋지 않은 toString:
    Found bug in: Student@1b6d3586
    
    // 좋은 toString:
    Found bug in: 민수(3학년)
  2. 로그 확인이 편해진다

    // 좋지 않은 toString:
    User Student@1b6d3586 logged in at 14:32
    
    // 좋은 toString:
    User 민수(3학년) logged in at 14:32
  3. 컬렉션을 사용할 때 유용하다

    Map<Student, String> attendance = new HashMap<>();
    attendance.put(new Student("민수", 3), "출석");
    attendance.put(new Student("영희", 3), "결석");
    
    // 좋지 않은 toString: {Student@776ec8df=출석, Student@4eec7777=결석}
    // 좋은 toString: {민수(3학년)=출석, 영희(3학년)=결석}
    System.out.println(attendance);

좋은 toString()을 작성하는 원칙

  1. 간결하고 이해하기 쉬운 정보 제공

    • toString()은 사람이 읽기 쉽게 작성해야 함.

    • 예제: 전화번호 클래스를 위한 toString()

    class PhoneNumber {
        int areaCode, prefix, lineNumber;
    
        PhoneNumber(int areaCode, int prefix, int lineNumber) {
            this.areaCode = areaCode;
            this.prefix = prefix;
            this.lineNumber = lineNumber;
        }
    
        @Override
        public String toString() {
            return String.format("(%03d) %03d-%04d", areaCode, prefix, lineNumber);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            PhoneNumber phone = new PhoneNumber(123, 456, 7890);
            System.out.println(phone); // 출력: (123) 456-7890 -> 실제 전화번호처럼보이게 포맷팅
        }
    }
  2. 객체의 중요한 정보를 포함

    • 너무 많은 정보를 넣으면 안 됨. 핵심 정보만 포함해야 함.

  3. 포맷을 문서화할지 고민

    • 포맷을 문서화하면 표준화되지만, 변경하기 어려워짐.

    • 포맷을 정하지 않으면 유연성 증가 (하지만 예측하기 어려움).

toString()을 재정의하지 않으면 발생하는 문제

나쁜 예시

import java.util.HashMap;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        map.put(new Person("Alice"), "Developer");

        System.out.println(map); // 출력: {Person@6d06d69c=Developer}
    }
}

➡ 사람이 읽기 어려움!

해결 방법: toString() 재정의

@Override
public String toString() {
    return "이름: " + name;
} // 출력: {이름: Alice=Developer}

➡ 사람이 읽기 쉽게 개선됨

toString()을 잘못 구현했을 때의 문제

나쁜 예시

@Override
public String toString() {
    return "항상 같은 값";
}

➡ 모든 객체가 같은 값을 반환해서 객체를 구분할 수 없음

좋은 예제

@Override
public String toString() {
    return "이름: " + name;
}

➡ 각 객체의 정보를 정확히 표현

toString() 반환값의 포맷을 문서화할지 결정해야 한다

포맷을 명시하면 좋은 점

  1. 표준화 & 가독성 향상

    • toString()의 포맷을 미리 정하면 일관된 형식으로 유지 가능

    • 사람이 읽기 쉬워지고, 디버깅이 편리해짐

  2. 데이터 저장 및 변환 가능

    • CSV, JSON 등으로 쉽게 변환하여 저장할 수 있음

    • 네트워크 통신 시 문자열 기반 데이터 전송 가능

  3. 객체 ↔ 문자열 변환 가능

    • 특정 포맷을 가진 문자열을 객체로 다시 변환 가능

    • 정적 팩터리 메서드(fromString())를 함께 제공하면 좋음

포맷을 명시하면 생기는 문제점

  1. 유연성이 떨어짐

    • 한 번 포맷을 정하면 변경하기 어려움

    • 기존 시스템이 그 포맷을 따르고 있을 가능성이 높음

  2. 향후 확장성 제한

    • 더 많은 정보를 추가하고 싶어도 기존 포맷을 유지해야 함

포맷을 문서화한 예시

/**
 * 전화번호를 문자열로 표현한다.
 * 형식은 "(XXX) YYY-ZZZZ" 이다.
 * XXX는 지역번호, YYY는 국번, ZZZZ는 선번호다.
 */
@Override
public String toString() {
    return String.format("(%03d) %03d-%04d",
                        areaCode, prefix, lineNum);
}

➡ 전화번호 형식이 표준화됨

포맷을 문서화하지 않은 예시

/**
 * 이 학생의 정보를 담은 문자열을 반환한다.
 * 정확한 형식은 정해지지 않았으며 변경될 수 있다.
 */
@Override
public String toString() {
    return name + "(" + grade + "학년)";
}

➡ 포맷이 정해지지 않아 미래에 수정 가능

toString 재정의가 필요 없는 경우

✔ 정적 유틸리티 클래스 - 예: Math, Arrays 같은 정적 메서드만 있는 클래스

✔ 대부분의 열거 타입(enum) - 자바가 이미 적절한 toString을 제공하기에 굳이 재정의할 필요가 없음

🧩 어려웠던 점

  • toString()을 잘못 구현하면 오히려 디버깅이 더 어려워질 수 있다

  • 포맷을 정하는 것이 유연성과 표준화 사이에서 고민해야 하는 부분이 어려웠다

💭 느낀 점

  • toString()은 작은 기능 같지만, 디버깅과 유지보수에서 엄청난 도움이 된다

  • 특히, System.out.println(객체)를 사용할 때 가독성이 훨씬 좋아짐을 경험할 수 있었다

Last updated