본문 바로가기
멘토링/Codesoom

테스트 방식에 대한 고찰

by Alex_beom 2024. 3. 10.
이 글은 필자의 이전 블로그인 https://velog.io/@beomdrive/CodeSoom-6에서 핵심 내용을 중심으로 요약한 후에, 내용을 추가 보완하였습니다.

 

이번 주에는 지금까지 우당탕탕 학습하고 개발했던 부분들이 머릿속에 뒤죽박죽 섞인 한 주였다.

나는 머릿속에 정리하면서 체득하는 시간이 조금 걸리는 편인데, 그 시간을 충분히 할애하지 못한 것 같다. 아마 멘토님들이 감사하게도 아주 많은 것들을 쏟아내주셔서 더욱 정리할 시간이 필요했다.

코드숨 과정이 끝나면 한번에 정리하려고 했지만, 계속 무의식적으로 이해가 안되는 부분이 신경이 쓰였고 자연스럽게 고민으로 이어졌다. 따라서 이번 주는 복잡한 생각 탓에 개발을 그렇게까지 많이 하지는 못한 것 같다.

하지만 일단 지금까지의 정보들을 내 것으로 만드는 것이 중요하기 때문에, 이번 주는 머릿속에 둥둥 떠다니고 있는 모든 것들을 다시 정리해보려고 한다.


TDD에 관하여

나는 코드숨 과정 6주동안 다양한 방법으로 TDD를 연습했다.

Mock을 사용한 Mockist TDD와 실객체나 FakeObject를 통한 Classicist TDD 등, 한가지에만 집중하지 않고 여러가지를 연습해보자는 마음이었다.
하지만 마음 한켠에는 계속 의문이 있었다. “꼭 한가지로만 해야되나..?”
영속성 계층(JPA)의 로직을 FakeObject로 만들어 실제 객체를 사용하는 TDD와 mock을 사용하는 TDD 두개를 다 해보면서 느낀 건 두 가지의 방법은 장단점이 뚜렷했다.

mock으로 사용할 때의 장점은 TDD를 하는 메서드에 대한 의존 객체에 대해서는 굳이 바로 구현을 하지 않아도 된다. given을 통해서 정답을 주기 때문이다. 또한 테스트 코드가 바로바로 피드백을 주기 때문에 빠르게 기능 구현을 할 수 있다.
하지만 이번 주에 mock만을 사용하면서 계속 들었던 생각은 실제 객체를 사용하는 것 만큼 신뢰가 가지 않았다. (물론 내가 mock 테스트를 능숙하게 못짜서 그런걸수도…)
또한 실제 메서드에서 내부 구현이 바뀔 경우 mock 테스트도 같이 수정해줘야 한다는 부분이 더욱 나를 힘들게 했다. 또한, 내부 구현이 바뀔 때 테스트가 깨지면 무의식적으로 테스트가 통과하는데에만 집중해서 테스트 코드를 작성하는 나를 발견했다.

즉 주체가 바뀐 것이다. 테스트 코드를 통해 메서드의 동작을 검증해야되는데, 테스트가 통과되도록 무의식적으로 생각 없이 코드를 짜는 것이다.
그렇다면 실제 객체를 통한 테스트를 짠다면?
나는 mock을 사용하지 않을 때는 영속성 계층(JPA) 로직을 FakeObject로 하나 만들어서 실제 DB를 대신하도록 만들었었다. 이랬을 때의 장점은 일단 실제 객체를 직접 사용하니까 테스트에 대한 신뢰도는 컸다. 또한 메서드의 결과 값만 바뀌지 않는다면 내부 구현은 수정되어도 작성해 놓은 테스트가 안깨지는 것도 마음에 들었다.

하지만 안 좋았던 점은 JPA가 알아서 구현해주는 그런 기능들을 FakeObject에서 직접 구현해주어야 되고, 직접 구현한 것들을 따로 테스트까지 해줘야 됐던 것이었다. 특히 페이징 쪽을 구현할 때는 약간 머리가 아팠다.

그럼 FakeObject 없이 진짜 실제 JPA를 사용하고, 실제 스프링 환경을 통해 테스트를 작성한다면?

실제 JPA와 스프링은 자체적으로 매우 많은 테스트 코드가 이미 존재하기에 매우 신뢰도가 높지만. 테스트 자체가 무거워진다는 단점이 있다.

자 그럼 어떻게 적당한 타협점을 찾아야할까… 이 부분에서 진짜 이번 주 내내 시도 때도 없이 문득 문득 생각에 잠긴 것 같다. 그 때 지인이 NEXTSTEP에서 ATDD과정을 들으면서 사용했던 흐름을 얘기해줬다. ATDD는 내가 코드숨에서 학습했던 TDD 방식에 인수 테스트가 더해진 방법인 것 같았다.

지인이 말하길 제일 먼저 인수 테스트를 작성하고 그 다음에 단위 테스트들을 통해 인수 테스트가 성공되도록 하는 흐름을 가져간다고 했다. 그러면서 단위 테스트는 domain 영역만 하고 필요 시에 application 영역까지 작성한다는 것이었다. 컨트롤러 웹 테스트는 인수 테스트가 대체한다고 한다.

따라서 다음 주부터 남은 2주간 아래와 같은 방식으로 TDD를 트레이닝해보려고 한다.

  1. Controller 웹 테스트를 MockMvc를 사용하여 작성한다.
  2. domain 계층 TDD를 진행한다.
  3. mock을 사용하여 application 계층 TDD를 진행한다.
  4. 스프링 환경을 띄워 실제 객체를 사용한 application 계층 TDD를 진행한다.

 

이렇게 한 싸이클로 해서 TDD를 진행해보려고 한다. 영속성 계층 테스트는 진행하지 않을 예정이다.
또한 7주 차에서는 일반 MVC 구조를 사용할 예정이고, 8주차에서는 헥사고날 아키텍처를 사용하면서 코드숨 과정을 잘 마무리해보려 한다.


로그인 인증 Interceptor

이번 주 과제 미션은 특정 API 요청에서는 요청 헤더에 인증 토큰이 없다면 401 에러를 반환하는 것이었다.

해당 요구사항을 보고 바로 인터셉터를 사용해야겠다는 생각이 들었다.

 

A HandlerInterceptor gets called before the appropriate HandlerAdapter triggers the execution of the handler itself. This mechanism can be used for a large field of preprocessing aspects, e.g. for authorization checks, or common handler behavior like locale or theme changes. Its main purpose is to allow for factoring out repetitive handler code.

참고자료: HandlerInterceptor Document

 

공식 문서에서는 핸들러인터셉터가 적절한 핸들러어댑터가 핸들러 자체의 실행을 트리거하기 전에 호출된다고 설명되어 있다. 즉 우리가 흔히 사용하는 Controller가 호출되기 전에 인터셉터가 실행된다는 의미이다. 또한, 이 메커니즘은 권한 검사 등에서 사용할 수 있다고 설명하고 있다.


즉 간단하게 리소스의 요청이 들어왔을 때 DispatcherServlet에서 핸들러 매핑을 통해 요청에 맞는 handler를 찾고, handler로 요청을 위임하기 전에 Interceptor가 잠깐 요청을 가로챈다.
따라서 인터셉터를 통해 해당 요청이 특정 path라면 로그인 인증 토큰을 검증하는 로직을 추가하면 되었다.

하지만 일일히 path 하나하나 추가해주기에는 너무 번거롭기도 하고 더 좋은 방식이 있을 것 같다는 생각이 들었다.
따라서 @LoginRequired라는 커스텀 어노테이션을 생성한 후, 로그인 인증이 필요한 핸들러 메서드 상단에 해당 어노테이션을 추가해주도록 구현하였다.
그 결과 인터셉터의 preHandle 메서드에 들어올 때 관련 요청의 핸들러도 같이 매개변수로 받을 수 있으므로, 해당 핸들러 어노테이션에 @LoginRequired가 선언되어 있을 경우 로그인 인증 토큰을 검사하는 로직을 수행하도록 구현함으로써 인터셉터와, 핸들러의 관심사를 분리하며 코드도 깔끔하게 유지할 수 있었다 😄

 

관련 코드는 아래 링크에서 참고할 수 있다.