🔍 트러블 발생

image

시터와 부모 간 매칭을 전담하시는 매칭 매니저분들이 새롭게 사용하실 어드민 페이지 오픈을 앞두고 있다. 어드민 페이지에서 날짜, 신청 상태, 담당 매니저라는 3가지 조건으로 원하는 결과를 빨리 찾으실 수 있도록 검색 기능을 구현 중이다. 검색 기능 구현을 위해서 QueryDSL 사용을 고려했는데, 검색 조건으로 사용되는 데이터들이 엔티티 4개를 조합(JOIN)한 분량이라는 점과 그것들을 모두 반환하기엔 불필요하게 노출되는 정보가 너무 많다는 점 때문이다. 때문에 QueryDSL을 사용해 조회된 결과를 별도의 data class(CareApplyAndScheduleAndSitterDTO)에 담아 반환하도록 기능을 구현했다.

image

조회에 필요한 정보만 프로퍼티로 가진 data class를 준비하고 그 data class에 정보를 담아 전달 할 수 있는 쿼리를 작성한 뒤, 조회가 성공할 것이라는 확신과 함께 기쁜 마음으로 테스트를 실행했는데 예상치 못한 부분에서 테스트가 실패했다.

image

QueryDSL은 WHERE절에 null을 전달하는 경우 조건문을 아예 생성하지 않는다. 때문에 검색 조건을 모두 null로 전달했을 경우 성공할 수밖에 없는 테스트다. 마침 조회되어야 할 원소의 개수는 7개고, 조회결과_리스트의 크기도 7이다. 그런데도 테스트는 실패했다.

data class 내부 모든 원소 역시 primitive type 이거나 data class 였기 때문에 동등성 비교로 문제가 생길 리 없다. 분명 논리적으로 문제가 되는 부분이 없는데, 컴파일러는 계속해서 데이터가 일치하지 않는다고 우기고 있었다.

홧김에 ‘테스트고 뭐고 데이터 잘 나오는 건 확실한데 그냥 배포할까…’ 생각도 들었지만, 혹여나 검색 로직이 변경된다면 든든한 방패가 되어줄 테스트임이 분명했다. 결국 모든 원소를 하나하나 비교해가며 테스트를 통과시키기 위한 여정을 시작했다.


🔍 왜 동등성 비교에 실패하지?

image

우선 ‘어떤 녀석이 일치하지 않는가?’부터 시작했다. shouldContainExactlyInAnyOrder의 대상이 되는 원소들을 하나씩 분리 후 shouldContain를 통해 리스트에 포함되어 있는지 테스트를 진행해보았다. 그러자 공통으로 돌봄희망일을 가진 경우에 동등성 비교에 실패하는 걸 확인할 수 있었다. 돌봄희망일 == 스케줄이었으므로, 스케줄 정보를 가지고 있는 data class에서 동등성 비교가 실패함을 추측 가능했다.

이번에는 shouldBe로 대놓고 동등성 비교를 테스트해보았다. 그러나 동등성 비교를 테스트하면서 더욱 미궁속으로 빠져들었는데, 동등성 비교 실패로 보여준 정보가 서로 완전히 완전히 동일했기 때문이다.

image

미치고 펄쩍 뛸 노릇이다. 일반 class가 아닌 data class라서 동등성 비교를 진행할 게 분명하고, 보기엔 완전히 동일한 데이터인데 동등성 비교에 실패한다니. ‘혹시 data class를 잘못 사용한 게 아닐까?’ 걱정되어 완전히 동일한 data class 인스턴스를 2개 만들어 비교도 해보았다.

image

그러자 이번에는 통과하는 모습을 보여줬다. 이즈음부터 ‘영속성 컨텍스트를 다녀온 data class에는 무언가 변화가 생기는가?’ 의심이 피어났지만, ‘영속성 컨텍스트를 다녀와 봐야 data class는 data class지.’ 라며 의심을 접고 다시 data class 내부 원소들을 하나씩 뜯어보기 시작했다.

image image

우선 스케줄 data class인 RegularScheduleDto와 그 내부 프로퍼티들을 하나하나 테스트해 보았다. 그 결과 RegularSchedule 세부 정보에 해당하는 RegularScheduleOption data class 에서 문제가 발생하고 있었다.

image

그러나 RegularScheduleOption data class만 별도로 동등성 비교를 진행해봐도 여전히 원인을 파악할 수 없었다. ‘혹시나 DayOfWeek 클래스 간 동등성 비교가 안 되는 건 아닐까?’, ‘LocalTime 소수점 값에 차이가 있던 건 아닐까?’ 등 여러 가지 테스트를 추가로 진행해보았지만, 진전이 없었다.

image

RegularScheduleDto 짜증난다…


🔍 팀원분들께 도움을 받자

결국 문제를 해결하지 못하고 하루를 통째로 날렸다. 다음 날 점심시간 내려가는 엘리베이터에서 팀원분들께 현 상황을 툴툴거리며 말씀드렸더니 곰곰이 듣고 계시던 20년 차 백엔드 개발자 현웅님께서 여러 가지 접근 방법을 제시해주셨다. 점심식사 후 현웅님이라면 해결의 실마리를 잡아주실 수도 있을 거라는 기대감에 무작정 노트북을 들고 찾아갔다. 한참 채팅 기능 개선으로 바쁘신 와중에도 흔쾌히 문제를 같이 살펴봐 주셨고, 10분도 안 되어 실마리를 잡아내 주셨다.

image

현웅님은 디버깅 모드로 데이터 타입부터 확인해주셨다. ‘눈에 보이는 데이터가 모두 일치하는 데 동등성 비교가 실패한다면, 데이터의 타입이 달라서 동등성 비교를 시도조차 하지 않는다.’는 접근이셨다. 실제로 RegularScheduleDto 내부 timeSlots 컬렉션의 데이터 타입이 달랐다.

image

영속성 컨텍스트에 진입하지 않은 컬렉션은 본래 데이터 타입을 유지하고 있지만,

image

영속성 컨텍스트에 진입한 컬렉션은 PersistentBag으로 감싸진다. JPA가 영속성 컨텍스트에 속한 컬렉션을 보다 쉽게 관리하기 위해서 감싸는 것이다.

image

결국 PersistentBagArrayList로 데이터 타입부터가 다르니까 동등성 비교에 실패하는 게 당연했다.


🔍 해결 방법

원인이 명확히 밝혀지니 해결은 수월했다. 엔티티 -> data class로 변환할 때 toList() 메서드를 통해 컬렉션으로 변환을 명시적으로 하거나, 테스트 코드에서 data class간 동등성 비교가 아닌 data class 내부 값을 직접 꺼내서 비교를 진행하면 문제를 해결할 수 있었다.

나의 경우 실제로 API 호출 시 CareApplyAndScheduleAndSitterDTO를 반환하므로 CareApplyAndScheduleAndSitterDTO를 비교하는 테스트가 더 의미 있다고 생각해서 toList() 메서드를 통해 명시적으로 컬렉션 반환을 하도록 했다.

image image

감사합니다 현웅님! 🙇‍♂️


🔍 무얼 배웠는가

‘왜 동등성 비교가 안 될까?’, ‘equalsshouldBe가 다르게 동작하는 부분이 있나?’ 와 같이 익숙한 부분에서만 원인을 찾으려 했던 점이 아쉬웠다. 수 많은 개발자가 믿고 사용하는 equalsshouldBe 등을 의심했고, 의심 과정에서 그간 알고 있던 개념이 흐트러질까 봐 혼란스러워하고 스트레스를 받았다. 그러나 현웅님께서는 자연스럽게 hashCode 부터 확인하셨다. 모두가 믿고 사용하는 라이브러리에는 원인이 없을게 당연하니 디버깅 모드로 문제가 발생할 수 있는 영역만 체크하신 거 같다. 결국 정리하면 아래와 같다.

  • 모두가 믿고 사용하는 것에는 이유가 있다.
  • 디버깅 모드와 더욱 친해져 보자.
  • 모르는 게 있다면 주변 동료분들께 더더욱 적극적으로 도움을 요청하자

이렇게 해피엔딩이 되나 싶었지만, 배포 후 또 다른 문제를 마주치게 되었다.


References

댓글남기기