회고/TIL

에러 해결에 한걸음 한걸음, 다시 멈추고 이제야 발견한 오류, 해결 후 다시 앞으로

KEEMSY 2023. 7. 30. 23:10

오늘은 지난 금요일, 그리고 어제 못한 에러 해결을 위해 책상에 앉아 고민했다. 하나 하나 해결해 가면서, 내가 모르는 부분을 정말 많이 공부 할 수 있었다. 비록 지금 이렇게 많은 시간을 사용했지만, 이것을 지금 고민하고 스스로 직접 해결해 볼 수 있어서 정말 다행이라는 생각이들었다. 물론 다른 사람들과 함께 했다면, 더 빨리 해결 할 수도 있었겠지만, 지금 은 그런 상황이 안되고.. 스스로라도 포기하지 않고 문제를 해결했다는 것에 만족한다.
 


테스트 방법의 문제: Stub

 
나는 웹계층을 테스트하면서 계속해서 새로운 문제를 겪어왔다.처음에는 웹계층에서 사용하는 애플리케이션 계층을 Mocking 을 사용하여, 테스트를 진행하고자 했으나, 이는 Application 계층의 ProductQueryService 자체를 Mocking 했다.

지난 에러 해결을 위한 나의 기록의 일부분

 
그리고 나는 그날 문제의 원인으로, Bean 으로 등록된 ProductQueryService 를 MockBean으로 등록하는 것이 문제라는 것을 알게되었다.(하지만 이 또한 완벽한 문제의 원인은 아님이었음을 나중가서 깨달았다.)
 
관련 기록

 

알고리즘 문제풀이, 웹계층 테스트 에러 해결?, 도메인주도설계로 시작하는 마이크로서비스 개

어제는 이제 여름 한정, 여자친구와 주중에 데이트를하고, 주말에는 공부를 하기로 하여, 오전에 독서와 문제를 풀이하고서 일탈을 했다. 좋아하는 신발들도 잔뜩 보고, 또 교보문고에서 수 많

sykeem.tistory.com

 
 


Stub(스텁) 기법을 활용한 문제의 해결(?)

 
하지만 문제는 아무리 생각해도, 웹계층을 테스트하는데, 애플리케이션계층을 애플리케이션 테스트할 때처럼 설정을 해야한다는 것이 잘못됬다는 생각을 지울 수 없었다. 그래서 나는 과거 공부했던 테스트 기법 중 하나인 Stub(스텁) 기법을 사용하게되었다.
 

스텁(stub)은 원래 없던 행위를 부여하는 과정을 말한다. 스텁의 간단한 예시를 이야기 한다면 대상 함수가 반환할 값을 지정한다고 할 떄, 이를 '반환값을 뭉갠다.(스텁한다.)' 라고 말할 수 있다.

 
해당 내용은 개발관련 책를 보고서 정리해둔 내용을 다시 참고했다.
https://github.com/KEEMSY/STUDY/tree/main/Books/Software-Engineering-at-Google
 
 

다시 작성한 테스트

나는 지금 테스트를 작성하면서, 다음과 같은 생각을 했다.

  • 애플리케이션 계층에 해당하는 ProductQueryService 의 trackProduct 가 호출될 때, 예상되는 TrackProductReponse 를 반환하도록 한다. (나는 productQueryService 의 trackProduct 메서드를 스텁하고자 하였다.)
  • 따라서 when, then 구절에서, 정상적으로 200 상태코드를 반환한다.

 
하지만 내 생각대로 쉽게 코드가 흘러가지 않았다. 이럴 것이라는 것은 예상했으나, 정말 나를 힘들게 한 것은 에러 메시지 였다.

com.shoes.ordering.system.domains.product.domain.application.dto.track.TrackProductResponse@71d8794f
2023-07-30 21:50:04.821 ERROR 59037 --- [    Test worker] c.s.o.s.c.h.GlobalExceptionHandler       : Cannot invoke "com.shoes.ordering.system.domains.product.domain.application.dto.track.TrackProductResponse.getProductId()" because "trackProductResponse" is null

 
애플리케이션 계층인 productQueryService 의 trackProduct 가 다시 null 을 반환했을까? 하는 의문이 들었다. 그리고 이 에러 메시지는 첫 에러메시지와 거의 유사했다.
 
왜일까.. 나는 한참을 고민했다. 무엇 때문에 null을 반환한 것일까? 분명 스텁을 통해 해당 메서드가 호출될 경우, 특정 객체를 반환하도록 설정하였다. 근데 왜..? 이전의 첫 null 을 반환 할 때는, 반환 값을 설정하지 않았다고 치고..  지금까지 내가 잘못된 부분을 원인으로 알고있을 수 도 있겠다는 생각이들었따.
 

 when(productQueryService.trackProduct(trackProductQuery)).thenReturn(trackProductResponse);

문제는 이 코드에 있었다. 내가 파악한 원인은 지금 이 코드는 내가 만든 trackProductQuery 객체를 사용하는 경우에만 trackProductResponse 를 반환한다. 나는 productQueryService 의 trackProduct 메서드에서 TrackProductQuery 객체가 내가 만든trackProductQuery를 사용할 것이라고 생각했다. 하지만 이럴 수 있는가? 아니다.
 
 이를 어떻게 해결해야할까 고민에 빠졌다. 그리고 이에 대한 해결책은 이전에 작성한 테스트 코드에서 찾을 수 있었다.

지난 Application 계층 테스트간 작성한 테스트 코드의 일부분

 

이 부분 또한, 영속성 계층에 대한 부분(productRepository)은 알 필요가 없다고 판단하였고, 해당 부분은 심지어 개발도 아직 되지 않았기에, productRepository 를 Mocking 하여 다음과 같은 방법을 사용했다. 그리고 이 코드는  GPT의 도움을 받았었고, 디테일한 부분(any 부분)에 대한 생각은 크게 하지 않았다. 단지, productRepositoy의 메서드를 호출할 때, 해당 객체를 사용하면, 내가 지정한 객체를 반환한다. 라고만 이해했었다. 왜 any 키워드가 사용되는지 를 생각하지 않았다.
 
 


진짜 문제의 해결

그리고 나는 메서드를 개선하여 문제를 해결하였다.

// 문제의 메서드
when(productQueryService.trackProduct(trackProductQuery)).thenReturn(trackProductResponse);

// 개선된 메서드
when(productQueryService.trackProduct(any(TrackProductQuery.class))).thenReturn(expectedResponse);

 
그렇다면 두 메서드의 차이점은 무엇일까? 앞서 언급했듯이, 문제의 메서드에서는 trackProduct 메서드가 정확히 동일한 trackProductQuery 객체(즉, productQueryService.trackProduct(trackProductQuery))로 호출될 때 Mockito가 trackProductResponse 객체를 결과로 반환한다.
 
반면, 개선된 메서드의 경우 trackProduct 메서드가 TrackProductQuery 개체와 함께 호출될 때 Mockito가 expectedResponse 개체를 결과로 반환한다. 즉, 특정 TrackProductQuery 개체가 전달되는 것은 중요하지 않다. 모든 입력에 대해 동일한 expectedResponse가 반환된다. 그래서 any() 키워드가 사용된 것이다!

 
그래서 나는 최종적으로 다음과 같이 테스트 코드를 작성하여, 테스트를 진행했다.
 
 

개선된 테스트, 초록불 까지 확인했다.

하지만 나는 이 테스트가 검증 부분이 많이 부족한 것 같다는 생각이 들었다.

  • 입력값이랑 같은 데이터를 반환한것이 맞는가?(정말 제대로된 응답을 반환 한 것이 맞는가?)

 
그래서 해당 테스트를 개선하고자 검증방법을 공부해보았다. 나는 추가적인 검증 방법으로, 반환 값(Json)을 검증하고, 내가 하고자 했던, trackProduct 메서드에 전달된 객체(TrackProductQuery)를 검증하고자 하였다.
 

완성된 테스트 코드. jsonPath 와 ArgumentCaptor 를 통해 전달된 객체가 동일한 productId 를 가지는지 확인했다.

나는 jsonPath 메서드를 통해 해당 값이 존재하는지, 값이 동일하는지를 검증했다. 그리고 내가 검증하고싶었던, ArgmentCaptor 를 통해 전달된 TrackProductQuery가 정말 같은 정보를 담고서 TrackProductResponse 를 만들어냈음을 증명해낼 수 있었다.
 
그리고 이 테스트 이후에 다음 테스트, 조회하는 Product 가 존재하지 않는 경우 정상적인 에러를 반환하는지를 테스트 작성하였다. 그리고 여기서 새로운 문제점을 식별했다.
 
 


ProductGlobalExceptionHandler의 장애(?)

 
나는 존재하지 않는 상품을 검색하는 테스트를 작성하는 것은 어렵지 않았다. 하지만, 테스트를 작성하면서 문제를 식별했다.
 

내가 작성한 Product 가 존재하니 않을 경우 에러를 반환하는 테스트

나는 존재하지 않는 ProductId 를 통해 조회할 경우, ProductNotFoundException을 반환하도록 설정하였다.그리고 ProductNotFoundException 은 ProductGlobalExceptionHandler 를 통해 다음과 같이 관리되고 있었다.

ProductNotFoundException 과 ProductDomainException, ProductDTOException을 다룬다.

 
이렇게 작성한 나의 의도는 ProductNotFoundException이 발생할 경우, HttpStatus.Not_Found(404) 가 발생할 수 있도록 설계하는 것이었다. 그리고 나는 이에 맞게 테스트를 작성했으나, 테스트는 실패했다.
 

예상한 ProductGlovbalExceptionHandler 가 아닌 직접적인 ProductDomainException 이 발생했다.

나는 의문이 들었다. 엥?? 이게어떻게 된 것이지..? 제대로된 상태코드 확인을 위해, 애플리케이션 서버를 키고, 포스맨을 통해 확인했다.
 
결과는 500 을 뱉었다. 현재 개발중인 Product 뿐만 아니라 Member 또한 500 을 뱉고있었다. 나는 이 사실을 모르고 있었다. 나는 지금까지 반환되는 에러메시지만을 확인했다. 그리고 나의 완전한 잘못이다.. 전혀모르고있었다니.. 그래도 지금이라도 이 문제를 스스로 인지해서 정말 다행이다.
 
나는 이 문제의 원인을 고민했다. 분명 에러핸들링은 되고 있는데.. 왜 내가 지정한 에러에서 핸들링이 되지 않는 것이지? 한참을 고민했는데 진짜 도저히 원인을 알 수가 없어서, 원초적인 방법을 사용했다.

나는 print 를 통해 어느 에러핸들링에서 처리되는 지 확인했다. 그리고 처리되는 부분은 이부분이었다.

 
나는 에러가 핸들링 되는 부분을 식별하고나서 더 혼돈에 빠졌다. 왜냐하면, 분명, ProductGlobalExceptionHandler 에서 @ExceptionHandler(value = {ProductNotFoundExecption.class}) 를 분명하게 지정하고서 이 경우, @ResponseStatus 로 NOT FOUND 에러를 반환하도록 설정했는데, 어째서, ProductNotFoundException 은 발생하면서, 해당 에러 핸들링을 찾지 못하는 것인가 하는 의문이 들었다.
 

ProductGlobalExceptionHandler 내의 ProductNotFoundExecption 에러를 다루는 메서드

 
그래서 다시 또 고민하다, GPT 선생님께 해결방법을 물어보았다. 역시 G선생님 G리는 답변을 해주셨다.
 

GPT 의 답변

정리하자면, Ordered.HIGHEST_PRECEDENCE를 설정하여, ProductGlobalExceptionHandler가 다른 @ControllerAdvice 클래스보다 먼저 처리되고 예외 처리 방법이 우선되도록한다. 진짜 GPT가 신기한게 이렇게하니 에러가 해결되었다는 것이다.
 
나는 이것을 보고서, 우선순위가 잘못설정 될 수 있음을 깨닫게 되었다. 그리고 이것을 각 도메인GlobalExceptionHandler 에 추가하려고 하였다. 그런데 생각을 해보면, 문제의 GlobalExceptionHandler 의 우선순위를 가장 낮게 설정하면 되는게 아닌가? 하는 생각에 GlobalExceptionHandler 에 @Order(Ordered.LOWEST_PERCEDENCE)를 설정해보았다.

 

이미 이렇게 설정이 되어있다고 중복이라고한다.

 
안내문을 보니, 이미 설정된 값이라, 중복 기본 파라미터 값을 할당 하는 것이라고 한다. 그래서 처음 알려준 방법으로 이 문제를 해결하기로 하였다.. 다른 더 좋은 방법을 알아내고싶은데.. 시간을 정말 너무 많이 소비하여.. 이걸 기록으로 남겨두고서, 추후에 개선의 기회가 생긴다면 개선할 수 있도록 해야겠다.
 


여기까지 오는데 몇일이 걸린지 모르겠다. 다른 공부도, 여러 일도 생겨 하루 완전히 프로젝트를 하고 있지 못한 요즘이지만(그래도 4시간 이상씩은 프로젝트만 했다..!) 요즘엔 정말 많이 경험의 중요성을 많이 느낀다. 그리고 동시에 내 경험의 부족이 더 크게 느껴지는 것같다.
 
경험의 부족으로 인한 삽질(?)은 당연한데, 자꾸만 욕심때문인지 이 시간이 조금은 허탈하다.. 미래에 이렇게 해메는 것보다 백배 천배 낫지만, 이렇게 시간을 많이 써서 다른 공부.. 독서.. 정리를 못하는 것이 너무 속상하다. 특히 계획이 더 틀어지는 것같아 내 스스로 불안감이 생긴다. 하지만 이 또한 꾸준함으로 극복해 낼 수 있을 것이라 믿는다. 다가오는 한주, 그리고 8월.. 얼마남지 않은 23년 화이팅!!

728x90