오늘은 String에서, Object의 valueOf는 어떤 식으로 동작하는지 알아보겠습니다. lombok을 쓰시다 보면, ToString 어노테이션을 잘못 쓰다가 stackoverflow 에러가 났다는 글을 보실 수 있습니다.이는 왜 그런 것일까요? 어떤 메서드 때문에 사이클이 문제가 된 것일까요? 결론부터 말하면 toString이 모든 필드에 대한 정보를 출력하게 만들었습니다. 그리고, 이들에 대한 정보를 출력하기 위해 호출하는 valueOf 메서드 때문입니다. 그래서, 이 메서드에 대해서 작성을 하였습니다. 문제 상황을 재현해 보면 아래와 같습니다.

 


 A.java입니다.

 

 

 A 안에는 필드 b가 들어 있습니다. A 객체 a를 하나 생성했다고 해 보겠습니다. 생성자가 호출이 되면, b에는 새로 생성한 B 객체의 참조값을 넣을 겁니다.

 

 

 이 상황을 그림으로 그리면 위와 같습니다. 이제 B의 생성자를 보겠습니다.

 

 

 이것은 필드 a를 가지고 있는데요. 생성자는 참조값을 받습니다. A 생성자에서 this를 넘겨 주었으니, B의 생성자 호출이 끝난 뒤에는 아래와 같은 그림이 그려질 겁니다.

 

 

 A 객체 aa의 필드 b는 B의 bb 객체를, B의 bb 객체의 필드 a는 aa 객체를 참조하는 이상한 상황이 벌어집니다.

 

 

 ToString 어노테이션이 적용된 클래스 중 하나인, A를 decompile을 해 보면, toString이 아래와 같이 정의되어 있는 것을 확인해 볼 수 있는데요. 여기서 중요한 것은, 뭔가 String이 +로 계속 append가 되어 있다는 것이고, 객체의 참조 값에 대한 무언가가 append를 할 때 사용이 되었다는 것입니다.

 

 ToString 어노테이션이 적용된 다른 클래스인 B도 decompile을 해 보면, this.a에 대한 정보를 출력하는 로직이 나올 겁니다.

 


 간단하게, 해당 상황을 단순하게 도식화 시켜서, 어떻게 동작하는지 보도록 하겠습니다.

 

 

 위 프로그램은 sq 객체의 정보와, ""를 append 해서 출력하는 프로그램입니다.

 

 

 trace를 해 보면, StringBuilder의 append 메서드에 걸립니다.

 

 

 여러 메서드가 정의되어 있는데, CharSequence도 아니고, StringBuffer도 아니고, String도 아니고, 기본 타입도 아닌 것이 걸릴 것은 Object를 받는 메서드일 겁니다. 계속 trace를 해 봅시다.

 

 

 2994번째 줄을 보시면, obj가 null이면 "null"을, 아니면 obj의 toString 메서드를 호출한 결과를 return 하라고 합니다. 여기서 인자로 들어온 obj는 아래와 같습니다.

 

 

 저는 sq+""을 하는 과정에서, String의 valueOf를 호출하였고, 이것은 문제의 sq를 인자로 받았습니다. 그리고 sq의 toString 메서드를 호출할 겁니다. 여기서, sq + ""은, sq의 toString 메서드를 호출한다. 정도만 간략하게 정리하시면 됩니다.

 


 다시, lombok의 ToString으로 돌아와 보겠습니다.

 

 

 A 객체 aa와 B 객체 bb가 아래와 같은 순환 관계가 되었을 때, 롬복의 ToString 어노테이션 (추가 설정이 없는)만 태우면 어떤 일이 일어나는지 보겠습니다. 객체 aa의 toString 메서드를 호출하였습니다.

 

 

 그러면, 위와 같이 문자열을 append를 할 겁니다. 중요한 것은 this.b도 append 하는 데 포함이 된다는 것입니다. 이렇게 정리해 보겠습니다. aa의 toString 메서드를 수행하기 위해서는, this.b를 인자로 받는 String.ValueOf 메서드를 수행해야 하고, 이것을 위해서는, this.b.toString()이 필요하다.

 

 이 관계를 그림으로 그려 보겠습니다.

 

 

 그런데, bb는 B의 객체이니, B의 toString도 보아야 할 듯 싶습니다. ~를 하기 위해서는 ~가 필요하더라. 코딩 테스트를 준비하시는 분은 익숙한 냄새가 나실 겁니다. 위상 정렬과 관련된 겁니다.

 

 

 보니, this.a가 필요한데, 중요한 것은, A 객체 aa를 새로 생성할 때, 내부적으로 this (aa의 참조값)를 B의 생성자게 넘겨주었다는 것입니다. bb가 생성되었다면, 필드 a는 aa의 참조값을 들고 있을 겁니다. B 클래스의 toString이 완료되기 위해서는 this.a를 인자로 받는 String의 valueOf 메서드가 완료되어야 합니다. 그리고, 그 작업이 끝나기 위해서는 aa.toString이 완료되어야 한다는 것을 알 수 있습니다.

 

 

 그러면, 이런 식으로 사이클이 돌 겁니다. 사이클, 재귀, 종료 조건, dfs. 스택 오버 플로우 냄새가 납니다. 이런 식으로 끝도 없이 호출되어 버리면, 스택이 남아날 리가 없습니다.

 

 

 흔히 보던 StackOverflowError가 납니다.