Object의 기본 toString 메서드는 단순히 클래스이름@16진수해시코드를 반환한다.

Object.toString

toString의 일반 규약에 따르면 `간결하면서 사람이 읽기 쉬운 형태의 유익한 정보`를 반환해야 한다.

또한 toString의 규약에서는 `모든 하위 클래스에서 이 메서드를 재정의하라`고 한다.

 

toString을 잘 구현한 클래스는 디버깅하기 쉽다.

toString 메서드는 객체를 println, printf, 문자열 연결 연산자(+), assert 구문에 넘길 때, 혹은 디버거가 객체를 출력할 때 자동으로 불린다.

오류 메시지 등을 로깅할 때도 자동으로 호출될 수 있다.

 

 

따라서 실전에서 toString은 그 객체가 가진 주요 정보 모두를 반환하는 게 좋다. 

하지만 객체가 거대하거나 객체의 상태가 문자열 로 표현하기에 적합하지 않다면 요약 정보를 담아야 한다. (사이즈나 id 등)

 

toString을 구현할 때면 반환값의 포맷을 문서화할지 정해야 한다. (값 클래스라면 문서화하는 것이 좋다)

포맷을 명시하기로 했다면, 명시한 포맷에 맞는 문자열과 객체를 상호 전환할 수 있는 정적 팩터리나 생성자를 함께 제공해주면 좋다.

자바 플랫폼의 많은 값 클래스가 따르는 방식이기도 하다. BigInteger, BigDecimal과 대부분의 기본 타입 클래스가 여기 해당한다.

BigInteger.toString
BigInteger String 생성자

 

 

toString을 제공할 때에는 포맷 명시를 해 두는 것이 좋고, toString이 반환한 값에 포함된 정보를 얻어올 수 있는 API를 제공하도록 하자.

그렇지 않으면 이 정보가 필요한 프로그래머는 toString의 반환값을 파싱할 수밖에 없다. 이 경우 성능이 나빠지고, 작업량이 늘어나며, 향후 포맷을 바꾸면 시스템이 망가질 수 있다.

정적 유틸리티 클래스(아이템 4)는 toString을 제공할 이유가 없다. 또한, 대 부분의 열거 타입(아이템 34)도 자바가 이미 완벽한 toString을 제공하니 따로 재정의하지 않아도 된다. 하지만 하위 클래스들이 공유해야 할 문자열 표현이 있는 추상 클래스라면 toString을 재정의해줘야 한다. (대다수의 컬렉션 구현체는 추상 컬렉션 클래스들의 toString 메서드를 상속)

 

기본적으로 Java에서는 모든 클래스가 toString() 함수를 지원한다. 따라서 라이브러리에 선언되어 있는 클래스들은 .toString() 함수만 호출하면 해당 클래스 내부에 있는 값들을 출력해 낼 수 있다. 하지만 사용자가 직접 선언한 클래스는 toString() 함수 호출만 가지고 내부 변수에 가지고 있는 값들을 확인하기가 어렵다.

 

예를 들어 아래와 같은 클래스를 선언하면

class TestClass{
    short s = 1;
    int i = 10;
    long l = 20;
    String name = "nameString";
    Long L = new Long(100);
    List<String> list = new ArrayList<String>();
    Map<String, String> map = new HashMap<String, String>();

    public TestClass(){
        list.add("string1");
        list.add("string2");
        list.add("string3");

        map.put("key1", "value1");
        map.put("key2", "value2");
        map.put("key3", "value3");
    }
}

이후 toString() 함수를 호출해보면

TestClass tc = new TestClass();
System.out.println(tc.toString()); // MyPattern.reflection.TestClass@15db9742

패키지 경로에 따라서 조금씩 다를 수는 있지만 기본적으로는 내부에 있는 변수들의 값이 나오지는 않는다. 그래서 새로 클래스를 정의할 때마다 toString() 함수를 재정의(override) 할 필요가 있다. 하지만 Java 프로그래밍을 하면서 엄청나게 많은 클래스들을 만들텐데 새로 만드는 클래스마다 각 클래스에 필요한 toString() 함수를 재정의 하기는 너무 힘든 일이다.

 

따라서 재정의를 하지 않으면서 범용으로 사용할 toString 클래스를 만들어 보자.

 

public class ToString {  
    public static String toString(Object object){
        Field []fields = object.getClass().getDeclaredFields();

        String str = "{";

        for(Field field : fields){
            field.setAccessible(true);
            try {
                String type =               
                    field.getType().toString().
                    substring(field.getType().toString().lastIndexOf(".") + 1);

                if(field.getType().toString().startsWith("class ") &&           
                   !field.getType().toString().startsWith("class java.lang.")){
                   // primitive 타입이나 라이브러리에서 제공하는 객체가 아닐때 toString을 재귀적 호출
                   str += type + " " + field.getName() + 
                          toString(field.get(object)) + " ";
                }
                else{
                   str += type + " " + field.getName() + ":" + 
                          field.get(object) + " ";   
                }
            } catch (IllegalArgumentException e) {
            } catch (IllegalAccessException e) {
            }
        }
        return str + "} ";
    }
    public static void main(String[] args) {
        AllTypes types = new AllTypes();
        System.out.println(ToString.toString(types));
        /* 실행 결과
        {short s:1 int i:1 long l:1 Short S:0 Integer I:0 Long L:0 InnerClass inner{int a:100 long b:200 String name:innerClass }  List list:[] Map map:{} } 
        */
    }
}

getClass()는 Reflection에 활용되는 클래스 객체를 가지고 오는 함수이다. 즉, object 객체는 .class 객체를 가지고 있는데, 객체의 본래 타입에 따라서 .class 객체는 모두 다르다. getDeclaredFields() 함수는 클래스 객체로부터 선언된 모든 Field 객체들을 배열 형태로 가져오는 함수다. .class 객체가 객체의 본래 타입에 따라 다르기 때문에 getDeclaredFields() 함수를 통해 나오게 되는 Field[]도 모두 다르다.

 

for문 안에 setAccessible(true) 함수 호출되는 부분의 경우는, 만약 Field가 public이 아닌 경우 캡슐화로 인하여 그 값에 접근하는 것이 불가능한데 Reflection은 이 함수를 통해서 접근이 가능하도록 만들 수 있다.(이 부분은 아직도 이슈가 많은 부분이라 프로그래머 마다 찬반 의견이 다르다. 하지만 유용성에 대해서는 아무도 부인하지는 못할 것이다.)

 

  • String getType() : type에 대한 클래스 객체(Class object)를 반환하는 함수  --> 타입을 반환하긴 하는데 primitive 타입이 아닌 경우 타입이 장황하게 출력될 수 있다. 예를 들어 Long 타입의 경우 class java.lang.Long 이라고 출력됨. 따라서 위와 같이 줄이는 부분을 만든다.
  • String getName() : 필드명을 String 객체로 반환하는 함수
  • Object get(object) : Field가 값으로 가지고 있는 객체를 반환하는 함수

약간 덧붙이자면, 일단 field가 null인 경우 동작하지 않을 수 있으므로 null 체크도 들어가는게 좋다.

그리고 배열의 경우 위의 경우에서 모두 벗어나기 때문에 제대로 출력이 안될 수 있다. 이 부분은 필요시 직접 처리하기 바란다.

만일 ToString 클래스를 사용해야 하는 것이 불편하고, toString()을 재정의 해서 사용하고 싶다고 할 때에도 이 ToString 클래스는 유용하다. 클래스마다 toString() 함수를 재정의 할 때 다음과 같이 동일하게 해주면 된다.

@Override
public String toString() {
    return ToString.toString(this);
}

 

 

 

'Java (+ Spring)' 카테고리의 다른 글

Spring: Spring MVC 설정 하기  (0) 2020.07.26
Android: tutorial page  (0) 2020.07.18
Android: Launch screen  (0) 2020.07.16
Java 라이브러리(.jar) 동적 로딩: DynamicJarLoader  (0) 2020.07.08
enum의 활용법 - 계산기  (0) 2020.07.07

+ Recent posts