일전에 와디즈 클론코딩 프로젝트를 진행한 경험이 있다.
불과 네달전? 에 진행한 프로젝트인데 지금보면 코드가 상당히 기계적으로 작성했다는 느낌을 받는다.
왜 이렇게 했을까 지금 다시 바라보면 당시 내가 알고 있던 전부였고 할 수 있던 전부였다.
너무나 지금보면 우스운 코드들이 많다 어이없기도 하고 지금은 어디 들이밀기엔 굉장히 낯부끄럽기도 하다.
그러나 어쩌겠는가. 그렇다고 케케묵혀둘 수 없고 먼지묻은 프로젝트를 다시 꺼내 리팩토링해서 내맘에 들게 뜯어 고쳐봐야 하지 않을까?
그런데, 지금 내가 알고 있는 정보와 지식에는 한계가 있다. 다른 사람들의 코드를 보고 영감을 받아서 새로운 리팩토링을 진행해보는건 어떨가 생각했다. 그렇게 생각해서 여러 프로젝트를 보며 조금 흥미로운 리팩토링을 해보고자 한다.
다른 팀들의 프로젝트 중 비슷한 도메인의 CURD 요청이 반복적이어서 불편함을 겪은 이야기를 들었다. 그래서 생성된 api만 28개였는데, 이를 단 4개의 API로 줄여냈다는 이야기를 들었다.
어떻게 그렇게 했느냐 물어봤더니 RequestBodyAdvice를 사용했다고 하더라. 이 키워드를 바탕으로 공부를 해보려고 한다.
사전학습 - RequestBodyAdvice
공식문서에 따르면, RequestBodyAdvice는 요청의 본문을 읽고 Object로 변환하기 전 요청을 커스터마이징 할 수 있다.
그리고 @RequestBody 혹은 HttpEntity 메서드 인수로 컨트롤러 메서드로 전달되기 전에 결과 Obejct를 처리할 수 있다.
다소 의역해보자면 내맘대로 요청 body를 커스텀할 수 있다.
이 클래스를 구현하면 다음과같은 메소드를 재정의해야 한다.
- supports : 이 인터셉터가 적용되어야 하는지 먼저 결정된다. (1순위)
- beforeBodyRead : 요청 body를 읽거나 변환하기 전에 실행된다.(2순위)
- afterBodyRead : 요청 body를 읽은 후에 실행된다. (3순위 혹은 마지막)
- handleEmptyBody : 이 바디가 비어있는지 확인한다.(2순위 혹은 마지막)
이중에서 특정 요청에 대해서 처리해야 하기때문에 suppoprts, 그리고 Body를 읽기 전에 사전에 처리해야될 내용이 있으니 beforeBodyRead를 사용하려고 한다.
추가 지식 : ArgumentResolver
스프링 디스패처 서블릿은 컨트롤러로 요청을 전달한다. 이때 컨트롤러에서 필요로 하는 객체를 만들고 값을 바인딩하는 역할이 ArgumenetResolver다.
종류 : @ModelAttribute, @CookieValue, @RequestParam, @RequestHeader, @RequestBody
json으로 온 요청은 객체로 변환되어 컨트롤러로 전달되는데, 객체 생성에 2개 이상의 argumentResolver가 붙으면 우선순위에 따라 하나의 argumentResolver만 객체로 변환 처리를 하게 된다. 그래서 객체 생성에 부가적인 역할을 하기 위해선 RequestBodyAdvice를 사용한다.
준비
일단 요구사항을 먼저 준비해보자.
와디즈에는 메이커와 서포터가 있다. 메이커의 경우 판매자를 의미한다.
판매자는 여러 등록 양식을 거쳐 펀딩을 진행할 수 있다. 이때 최상위 개념을 project라고 하자.
우리 팀의 경우 다음과 같은 조건이 있다.
- Funding(펀딩과 관련된 정보 : 목표 금액, 성공여부 등),
- Post(제목, 설명 섬네일 이미지 등),
- Reward(상품 : 어떤 상품인지와 상품에 대한 옵션이 담겨있음)
- 이 funding, reward, post를 사전에 생성해야만 Project를 생성할 수 있다.

생각해보면, Project를 생성하기 위해서 Funding, Post, Reward가 필요하다면 이들을 하위 도메인으로 봐도 좋지 않을까 싶었다.
Funding,Post,Rewarad에 대해 각각의 CURD가 존재할텐데, 이들을 공통된 하나의 '요소'로 보면 각 API마다 있던 CURD를 4개로 줄일 수 있고 동적인 처리를 함께 가져갈 수 있겠다 싶었다.
치환해보자면 이런 이야기다.
Funding -> 하나의 등록 요소
Post -> 하나의 등록 요소
Reward -> 하나의 등록 요소
등록 요소가 모두 갖춰져 있다면. => 프로젝트 생성!
RegistrationAdvice.java

위에서 언급한 메소드를 재정의한다.
Supports
특정 요청으로 오는 값들에 대해서 오는 요청을 인터셉트 한다. 지금의 경우 RegistrationRequest가 대상이 된다.
BeforeBodyRead
들어오는 요청을 우리가 사용하기 쉽게 가공한다. HttpInputMessage는 들어오는 요청 헤더와 바디 값을 의미한다.
objectMapper로 값을 읽고, 들어오는 url의 pathVariable값에 따라서 타입을 결정하고(post,reward,funding) 그 타입에 해당하는 요청 내용을 입력받는다.
그리고 가공된 내용을 다시 body로 전달한다. 코드를 보면 HttpInputMessage를 정의해서 사용하는 것을 볼 수 있다. (인터페이스!)

AfterBodyRead, HandleEmptyBody
해당 요청에 대해서는 특별히 처리해야할 내용이 없으므로 딱히 수정하지 않는다.
getTypeFromPathVariable
pathVariable로부터 값을 받아와서 타입으로 지정하기 위해 사용한다. 여기에는 RequestContextHolder라는 개념이 사용된다.
RequestContextHolder

스프링에서 전역적으로 Request정보를 가져오는 역할을 한다. 여기에선 requestParam이나 지금과 같이 pathVariable정보, 혹은 헤더,쿠키 정보 등을 가져와서 어디서든 사용할 수 있다.
조금 더 깊이 알아보자면, Servlet이 생성되는 순간(Request를 클라이언트로부터 받을 때) 초기화되고, Servlet이 삭제되면 그때 같이 삭제된다.
동작 원리
requestContextHolder는 static하기 때문에 ThreadLocal에 값을 읽고 쓸 수 있다. 하지만, 다른 스레드에서는 사용할 수 없다.
왜냐하면 새로운 요청이 생성되면 새로운 스레드가 생성된다.(new Thread, 스레드 풀 참조). 그래서 DispatcherServlet 범위에서 벗어나 값을 읽어올 수 없다.
만약, 새로운 스레드를 생성해서 사요하고 싶다면 아래 글을 참조해보자.
Spring RequestContextHolder
RequestContextHolder 개요 RequestContextHolder 는 Spring에서 전역으로 Request에 대한 정보를 가져오고자 할 때 사용하는 유틸성 클래스이다. 주로, Controller가 아닌 Business Layer 등에서 Request 객체를 참고하려
gompangs.tistory.com
URL요청에서 값을 가져오는 방법
HandlerMapping
설명에 따르면, handlerMapping은 HandlerInterceptor의 preHandle 메소드를 호출하고 preHandle 메소드가 true로 반환되면 마지막으로 핸들러 자체를 호출한다. 세션, 쿠키 등 다양한 변수를 매핑하는 것이 가능하다. (MVC 프레임워크의 장점)
URI_TEMPLATE_VARIABLES_ATTRIBUTES
httpServletRequest 특성의 이름으로 변수 이름을 값에 매핑한다.
예를 들어 Pathvariable이 {hobby} 이고 실제로 사용되는 값이 basketball이라면, hobby:basketball로 값이 매핑될 수 있습니다.
https://medium.com/@truongbui95/requestbodyadvice-in-spring-boot-a8a40ea59e6d
https://velog.io/@kylekim2123/SpringBoot-ResponseBodyAdvice를-이용한-공통-응답-처리와-관련-트러블-슈팅
'프로젝트 일기' 카테고리의 다른 글
| [LIME 리팩토링] SSE 알림 리팩토링(HeartBeat, 비동기, 옵저버 패턴) (1) | 2024.02.14 |
|---|---|
| [Bucket - Back] 프로젝트 트러블 슈팅 (1) | 2024.02.12 |
| Prometheus, Loki, Grafana 구조 이해하기 (0) | 2024.02.12 |
| [LIME 리팩토링] - 연관관계 제거 (2) | 2024.01.27 |
| [빅데이터] 도시 물류 혁신 계획 (0) | 2022.11.22 |