이번 주에는 지금까지 우당탕탕 학습하고 개발했던 부분들이 머릿속에 뒤죽박죽 섞인 한 주였다.
나는 머릿속에 정리하고 체득할 시간이 조금 걸리는 편인데, 그 시간을 충분히 할애하지 못한 것 같다. 아마 멘토님들이 감사하게도 아주 많은 것들을 알려주셔서 더욱 정리할 시간이 필요했다.
코드숨 과정이 끝나면 한번에 정리하려고 했지만, 계속 무의식적으로 이해가 안되는 부분이 신경이 쓰였고 자연스럽게 고민으로 이어졌다. 따라서 이번 주는 복잡한 생각 탓에 개발을 그렇게까지 많이 하지는 못한 것 같다.
하지만 생각 정리가 된 것도 있고, 이번 주 리뷰어이신 영환 멘토님이 알려주신 것들도 있으니 간단하게 회고를 통해 정리해보려 한다.
나는 코드숨 과정 6주동안 다양한 방법으로 TDD를 연습했다.
Mock을 사용한 Mockist TDD와 실객체나 FakeObject를 통한 Classicist TDD 등, 한가지에만 집중하지 않고 여러가지를 연습해보자는 마음이었다.
하지만 마음 한켠에는 계속 의문이 있었다. “꼭 한가지로만 해야되나..?”
영속성 계층(JPA)의 로직을 FakeObject로 만들어 실제 객체를 사용하는 TDD와 mock을 사용하는 TDD 두개를 다 해보면서 느낀 건 두 가지의 방법은 장단점이 뚜렷했다.
mock으로 사용할 때의 장점은 TDD를 하는 메서드에 대한 의존 객체에 대해서는 굳이 바로 구현을 하지 않아도 된다. given을 통해서 정답을 주기 때문이다. 또한 테스트 코드가 바로바로 피드백을 주기 때문에 빠르게 기능 구현을 할 수 있다.
하지만 이번 주에 mock만을 사용하면서 계속 들었던 생각은 실제 객체를 사용하는 것 만큼 신뢰가 가지 않았다. (물론 내가 mock 테스트를 능숙하게 못짜서 그런걸수도…)
또한 실제 메서드에서 내부 구현이 바뀔 경우 mock 테스트도 같이 수정해줘야 한다는 부분이 더욱 신뢰를 깎아갔다. 신뢰가 깎인 이유 역시 내가 아직 mock 테스트를 능숙하게 짜지 못해서 그런거 같은데, 내부 구현이 바뀔 때 테스트가 깨지면 무의식적으로 테스트가 통과되도록 테스트 코드를 작성하는 나를 발견했다.
즉 주체가 바뀐 것이다. 테스트 코드를 통해 메서드의 동작을 “검증”해야되는데, 테스트가 통과되도록 무의식적으로 생각 없이 코드를 짜는 것이다.
그렇다면 실제 객체를 통한 테스트를 짠다면?
나는 mock을 사용하지 않을 때는 영속성 계층(JPA) 로직을 FakeObject로 하나 만들어서 실제 DB를 대신하도록 만들었었다. 이랬을 때의 장점은 일단 실제 객체를 직접 사용하니까 테스트에 대한 신뢰도는 컸다. 또한 메서드의 결과 값만 바뀌지 않는다면 내부 구현은 수정되어도 작성해 놓은 테스트가 안깨지는 것도 마음에 들었다.
하지만 안 좋았던 점은 JPA가 알아서 구현해주는 그런 기능들을 FakeObject에서 직접 짜주고, 그 짠 로직들을 따로 테스트까지 해줘야 됐던 것이었다. 특히 페이징 쪽을 구현할 때는 약간 머리가 아팠다. (만약 FakeObject를 이렇게 사용하는 게 아니라면 누구든 저한테 말 좀해주세요… 😂😂)
그럼 FakeObject 없이 진짜 실제 JPA를 사용하고, 실제 스프링 환경을 통해 테스트를 작성한다면?
이건 테스트에 대한 신뢰는 만땅일텐데, 테스트가 너무 무거워진다는 단점이 있다.
자 그럼 어떻게 적당한 타협점을 찾아야할까… 이 부분에서 진짜 이번 주 내내 시도 때도 없이 문득 문득 생각에 잠긴 것 같다. 그 때 지인이 NextStep에서 ATDD과정을 들으면서 사용했던 흐름을 얘기해줬다. ATDD는 내가 코드숨에서 했던 tdd에 인수 테스트가 더해진 방법인 것 같았다.
지인이 말하길 제일 먼저 인수 테스트를 작성하고 그 다음에 단위 테스트들을 통해 인수 테스트가 성공되도록 하는 흐름을 가져간다고 했다. 그러면서 단위 테스트는 application과 domain만 하는 대신 mock 테스트와 실제 객체 테스트 두개를 다 작성한다는 것이었다. 컨트롤러 웹 테스트는 인수 테스트가 대신한다고 한다.
따라서 다음 주부터 남은 2주간 아래와 같은 방식으로 TDD를 진행해보려고 한다.
이렇게 한 싸이클로 해서 TDD를 진행해보려고 한다. 영속성 계층 테스트는 진행하지 않을 예정이다.
또한 7주 차에서는 일반 MVC 구조를 사용할 예정이고, 8주차에서는 헥사고날 아키텍처를 사용하면서 코드숨 과정을 잘 마무리해보려 한다.
만약 이 글을 읽어주시는 분이 있다면 언제든 다이렉트로 의견과 조언을 주시면 너무 환영입니다😊
이번 주 과제 미션은 특정 API 요청에서는 요청 헤더에 인증 토큰이 없다면 401 에러를 반환하는 것이었다.
여기서 딱 들었던 생각은 “인터셉터를 사용해야겠다” 였다.
Applications can register any number of existing or custom interceptors for certain groups of handlers, to add common preprocessing behavior without needing to modify each handler implementation.
참고자료: https://docs.spring.io/spring-framework/docs/HandlerInterceptor
인터셉터 관련 내용은 블로그에 따로 정리할 예정인데, 공식 문서에서는 애플리케이션의 각 핸들러의 사전 처리 동작을 핸들러에 일일히 구현하지 않아도 추가할 수 있도록 지원해주는 기능이라고 한다. 또한 특정 핸들러 그룹에 대한 커스텀 인터셉터도 원하는 수만큼 등록할 수 있다고 한다.
즉 간단하게 리소스의 요청이 들어왔을 때 DispatcherServlet에서 핸들러 매핑을 통해 요청에 맞는 handler를 찾고, handler로 요청을 위임하기 전에 Interceptor가 잠깐 요청을 가로챈다.
따라서 인터셉터를 통해 해당 요청이 특정 path라면 로그인 인증 토큰을 검증하는 로직을 추가하면 되었다.
하지만 일일히 path 하나하나 추가해주기에는 너무 번거롭기도 하고 더 좋은 방식이 있을 것 같다는 생각이 들었다.
따라서 @LoginRequired
라는 커스텀 어노테이션을 생성한 후, 로그인 인증이 필요한 핸들러 메서드 상단에 해당 어노테이션을 추가해주도록 구현하였다.
그 결과 인터셉터의 preHandle 메서드에 들어올 때 관련 요청의 핸들러도 같이 매개변수로 받을 수 있으므로, 해당 핸들러 어노테이션에 @LoginRequired
가 있을 경우 로그인 인증 토큰을 검사하는 로직을 수행하도록 구현함으로써 인터셉터와, 핸들러의 관심사를 분리하며 코드도 깔끔하게 유지할 수 있었다!