이 글은 프로젝트를 진행하며 발생한 직렬화 과정에서 DTO boolean 변수의 is 접미사가 사라지는 문제를 해결하기 위해 고민한 과정을 적은 글입니다.
❗️ 문제 상황 ❗️
저희 팀은 백엔드에서 프런트엔드로 요청에 대한 성공 및 실패의 공통된 응답 형식을 반환하기 위해 `ApiResponse`라는 공통 응답 포맷을 개발하여 활용하고 있습니다.
- isSuccess : API 호출의 성공 여부를 true, false로 반환합니다.
- message : API 호출과 관련된 메시지를 반환합니다. 호출 성공 시에는 "API 호출 성공"이라는 메시지로 고정되지만, 실패 시에는 상황에 맞는 다양한 메시지가 적용됩니다.
- data : API 호출이 성공했다면, 프런트엔드에 반환해야 할 데이터들을 제너릭을 통해 반환합니다.
이를 통해 예상하는 응답 형식은 아래와 같습니다.
{
"isSuccess" : true,
"message" : "API 호출 성공",
"data" : ~~~~
}
하지만 실제로 API를 호출하면 아래와 같이 "isSuccess"가 접미사 is가 사라진 "success"로 나타나게 됩니다.
처음에는 오타가 발생한 줄 알고 다시 봤지만, 오타는 없었기에 왜 이런 문제가 발생했는지 파악하기 시작했습니다.
🧐 문제 분석 🧐
디버깅 과정에서 응답을 반환하는 controller에서 return 할 때의 ApiResponse를 살펴보니 역시 isSuccess로 잘 설정되어 있습니다.
이를 통해 저는 ApiReponse 객체가 JSON으로 직렬화되는 과정에서 발생한 문제라고 판단할 수 있었습니다.
직렬화 과정
Spring에서는 HTTP 응답의 본문을 JSON 형식으로 직렬화하기 위해 Jackson 라이브러리를 활용합니다.
그중에서 직렬화되는 객체의 프로퍼티에 대한 정보를 정의되어 있는 public getter를 통해 수집합니다.
POJOPropertiesCollector
다음은 Jackson 라이브러리에 포함된 POJOPropertiesCollector 클래스입니다.
클래스 이름에서도 알 수 있듯이 POJO(Plain Old Java Object)의 프로퍼티들을 수집하는 역할을 수행합니다.
collectAll()
POJOPropertiesCollector의 collectAll()이란 메서드를 통해서 프로퍼티 정보를 수집합니다.
먼저 _addFields(), _addMethods(), _addCreators()로 모든 정보를 수집한 다음,
_removeUnwantedProperties(), _removeUnwantedAccessor()로 원치 않는 정보를 제거하는 모습을 확인했습니다.
_addFields()
_addFields()를 통해서, 필드인 isSuccess, data, message 3개의 프로퍼티의 후보로 추가 되었을 것입니다.
_addMethods()
_addMethods()에서는 AnnotatedMethod를 확인하며, 파라미터의 개수를 통해 어떠한 메서드가 getter인지 setter인지 파악하는 모습을 확인할 수 있었습니다.
저는 getter에 대한 정보가 필요하니, argument가 0개일 때, 수행되는 _addGetterMethod()로 들어가 보겠습니다.
이때, _addGetterMethod의 인자로, 프로퍼티들을 나타내는 props, 특정 메서드를 나타내는 m, 그리고 _annotationIntroSpector를 넘겨줍니다.
isSuccess(), getMessage(), getData()는 ApiResponse 클래스에서 LomBok의 @Getter를 통해서 생긴 3개의 getter입니다.
이것들이 m이 되어 넘겨질 것입니다.
_addGetterMethod()에서는 String 타입의 implName과 boolean 타입의 visible이라는 변수를 갖고 있었습니다.
그리고 다양한 조건에서
findNameForRegularGetter(), findNameForIsGetter()를 통해 implName을 정하고,
isIsGetterVisible(), isGetterVisible()을 통해 getter가 visible 한 지 확인하고 있었습니다.
여기서 놀라운 사실이 발견됩니다. 디버깅을 통해 발견한 사실입니다.
ApiResponse의 추가된 프로퍼티는 _addFileds()로 현재 isSuccess, message, date 3개이고, _addMethods()의 시작 전 당연히 props size도 3이었습니다.
하지만 놀랍게도, _addMethods()의 for문이 끝나자 props size가 4로 변경되었습니다.
어떻게 이런일이 생겼을까요?
도대체 어떻게 된 것일까요?
isSuccess()의 경우에는 POJO의 is-getter 규칙에 따라, findNameForIsGetter()가 호출되어, implName이 sucess가 됐습니다.
그리고 이 implName이, _property()로 들어가게 됩니다.
_property()에서 success로 get을 호출하지만, ApiResponse에 그런 프로퍼티는 없기에, null이 나올 것입니다.
그래서 if문 내부로 들어가게 되고, success란 이름의 프로퍼티가 추가되어 프로퍼티의 사이즈가 4가 된 것입니다.
즉, collectAll()에서 추가된 모든 프로퍼티들의 후보는 isSuccess, data, message에 success가 추가됐을 것입니다.
이후 호출되는, collectAll() 내부 removeUnwantedProperties()를 살펴보겠습니다.
추가된 프로퍼티 후보군 isSuccess, data, message, success가 존재합니다.
여기서 주목해야 할 부분은, isSuccess의 isVisible이 false입니다.
따라서 while문의 첫 번째 if문 내부에서 isSuccess란 프로퍼티는 삭제될 것입니다.
그렇기 때문에 API 응답 직렬화 결과로 의도한 isSuccess가 아닌 success가 나오게 된 것입니다.
정리하자면
- Lombok의 boolean getter 생성 규칙:
- Lombok은 boolean 타입 필드에 대해 getter 메서드를 생성할 때, 일반 필드와 다르게 get 대신 is 접두사를 붙입니다.
- 예를 들어, private boolean success; 필드는 isSuccess()라는 메서드로 getter가 생성됩니다.
- is 접두사의 중복 방지:
- 필드 이름이 이미 is로 시작하는 경우, Lombok은 is 접두사를 중복으로 추가하지 않습니다.
- 즉, private boolean isSuccess;라면, getter는 isIsSuccess()가 아니라 기존 필드 이름을 유지하여 isSuccess()로 생성됩니다.
- Jackson의 POJO 처리 규칙:
- Jackson은 boolean 타입의 getter 메서드를 처리할 때, is로 시작하는 메서드(isSuccess())를 기본적으로 "is-getter"로 인식합니다.
- 이 과정에서 isSuccess는 success로 변환되어 프로퍼티 이름이 설정됩니다.
- 프로퍼티 후보 수집 과정:
- _addFields()에서 isSuccess, data, message가 수집됩니다.
- _addMethods()에서 isSuccess() 메서드가 처리될 때, findNameForIsGetter()를 통해 이름이 success로 변경됩니다.
- 이로 인해 success라는 새로운 프로퍼티가 추가됩니다.
- 불필요한 프로퍼티 제거 과정:
- removeUnwantedProperties() 단계에서 isSuccess는 isVisible 속성이 false로 설정되어 제거됩니다.
- 결국, success만 남게 되면서 의도한 isSuccess 대신 success로 직렬화됩니다.
- 결과적으로:
- Jackson의 기본 동작으로 인해 isSuccess가 success로 직렬화되는 문제가 발생합니다.
🚧 해결 방법 작성 공사 중🚧