가끔 개발을 하면서 클라이언트가 요청에 담아 보낸 데이터(즉, 매개변수(Parameter))를 컨트롤러에서 받기 전에 필터 혹은 인터셉터에서 수정하고 싶을 수 있다. 물론 그 중에서도 공통의 관심사와 관련한 로직을 따로 분리하는 경우에 해당할 것이다. 필자는 다음과 같은 몇 가지 편의 기능을 제공하기 위해서 이러한 작업을 수행하고자 하였다:
1. 특정 String 매개변수의 좌우 끝에 있는 공백을 제거하기(String 클래스의 strip() 메소드 이용).
2. 특정 Enum 클래스 값과 관련된 String 매개변수의 입력이 영문 소문자면 영문 대문자로 바꿔 주고, 만약 값이 한글이라면 관련된 Enum 클래스 값으로 바꿔 주기.
다만 요청의 매개변수는 원래대로라면 변경할 수 없다. setParameter가 구현되어 있지 않기 때문이다. 그래서 그런지 파인드(Phind) 혹은 클로드(Claude)에서 알려준 방법을 사용해도 제대로 해당 기능이 동작하지 않았다. 게다가 AI가 알려준 코드들은 무수히 많은 공부를 요하는, 복잡한 코드였다. 그래서 사실 지금도 어떤 원리인지 잘 모른다 ㅋㅋㅋ 필자가 원하는 기능에 비해 너무 많은 자원을 요하는 것 같아 다른 방법을 찾게 되었고, 해답의 결정적인 키는 아래의 블로그를 통해 쥘 수 있었다. 다만 관련 내용을 부연할 것도 있고, 코드도 수정할 부분이 있었다.
https://ajdahrdl.tistory.com/105
일단 HttpServletRequestWrapper는 이름만 보고도 유추할 수 있듯이 HttpServletRequest의 일종의 Wrapper라고 할 수 있을 것이다. 즉, 생성자로 HttpServletRequest를 포함하는 클래스이다. 또한 이 클래스는 동시에 HttpServletRequest를 구현하고 있는 것을 아래와 같이 직접 클래스 파일을 뜯어봄으로써 확인할 수 있다. 그렇다면 이 클래스는 HttpServletRequest로 어떤 기능을 제공할 수 있을까?
// HttpServletRequestWrapper.class
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
public HttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
쉽게 말해서, HttpServletRequestWrapper는 ServletRequest과 관련한 Spring 프레임워크의 구조를 전혀 손볼 필요 없이 사용자 지정 로직을 더할 수 있도록 설계되어 있다. 이를 통해 개발자는 프로젝트의 구조에 어떤 변화도 주지 않으면서 자신이 원하는 기능을 요청에 적용할 수 있게 되고, 이는 인터셉터와 차별화되는 필터의 기능인 '요청 바꿔치기 기능'(즉, 요청과 응답을 원래 전송되던 것이 아닌 새로운 요청 또는 응답으로 바꿔서 전송할 수 있는 기능)과 더하여 개발자의 편의성과 프로젝트의 가독성 및 유지보수성을 향상할 수 있다.
이번 포스팅에서 바꿀 것은 매개변수이므로 이에 중점을 둔다면, 해당 매개변수와 관련한 HttpServletRequest의 메소드 전부를 HttpServletRequestWrapper의 구현체(ModifiableHttpServletRequest)에서 오버라이드(@Override)하고 맨 처음에 ModifiableHttpServletRequest를 생성할 때 HttpServletRequestWrapper에 들어 있던 매개변수를 ModifiableHttpServletRequest에 저장함으로써, 불필요하게 HttpServletRequestWrapper로 나아가는 일 없이 ModifiableHttpServletRequest 안에서 매개변수와 관련한 모든 기능을 처리할 수 있다.
아래는 해당 클래스의 구현체이며, setParameter를 추가했다는 특징이 있다.
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
public class ModifiableHttpServletRequest extends HttpServletRequestWrapper {
private final HashMap<String, String[]> paramMap;
public ModifiableHttpServletRequest(HttpServletRequest request) {
super(request);
this.paramMap = new HashMap<>(request.getParameterMap());
}
@Override
public String getParameter(String name) {
String[] paramArray = getParameterValues(name);
return paramArray != null && paramArray.length > 0 ? paramArray[0] : null;
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(paramMap);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(paramMap.keySet());
}
@Override
public String[] getParameterValues(String name) {
String[] paramArray = paramMap.get(name);
return paramArray != null ? paramArray.clone() : null;
}
public void setParameter(String name, String value) {
paramMap.put(name, new String[]{value});
}
}
해당 클래스를 조금만 더 설명하자면, 우선 눈에 띄는 것은 paramMap이 HashMap<String, String[]>로 정의되어 있다는 점이다. 이는 ServletRequest에서 정의된 다음의 네 가지 형태의 메소드를 구현하는 가장 쉬운 방법을 택한 것이다. 즉,
ㅡ getParameterMap()은 paramMap 그 자체가 되는 것이고,
ㅡ getParameter는 특정 키(var1)의 paramMap 값(String[])에 들어 있는 유일한 값
ㅡ getParameterNames()는 paramMap의 keySet
ㅡ getParameterValues는 특정 키(name)의 paramMap 값(String[])이 되는 것이다.
참고로 해당 클래스에서 paramMap이 HashMap으로 선언되어 있으므로 특정 키에 대한 값이 두 개 이상 들어갈 일이 없어 getParameter()에서 배열의 0번째 인덱스 값을 뽑는 것이 가능하며, getParameterValues() 메소드의 반환 값으로 나온 배열 또한 길이가 null이 아닌 이상 1일 수밖에 없다는 점도 인지할 수 있을 것이다.
// ServletRequest
String getParameter(String var1);
Enumeration<String> getParameterNames();
String[] getParameterValues(String var1);
Map<String, String[]> getParameterMap();
더하여, ServletRequest의 의존성은 다음과 같이 구성되어 있다.
// HttpServletRequestWrapper
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest {
// HttpServletRequest
public interface HttpServletRequest extends ServletRequest {
// ServletRequestWrapper
public class ServletRequestWrapper implements ServletRequest {
또한 NullPointerException을 막기 위해 getParameter에서 null 체크 이후에 길이를 확인하고 있고(&& AND 연산자가 사용될 때, 앞의 조건이 실패하면 반드시 전체 연산도 실패하므로 뒤의 로직이 실행되지 않는다), getParameterValues에서는 Array의 깊은 복사를 위해 clone() 메소드를 사용하고 있다(참조형 변수를 반환할 때는 특수한 경우가 아닌 이상 항상 깊은 복사를 사용하자!).
마지막으로 필터에서 자신만의 사용자 지정 로직을 작성한 후 해당 로직이 적용된 ModifiableHttpServletRequest를 생성하여 다음 필터(혹은 리소스)로 해당 요청을 보내주면 작업이 완료된다. 아래에는 필자가 맨 처음에 제시한 목적에 부합하도록 코드를 작성한 예시를 참고로 제시하겠다. 이번 포스팅은 필터에 관한 게 아니므로 OncePerRequestFilter에 관한 내용은 생략하겠다.
public class CompanyArticleDtoSupportFilter extends OncePerRequestFilter {
@Override
public void doFilterInternal(HttpServletRequest requestBefore, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(getModifiableHttpServletRequest(requestBefore), response);
}
private ModifiableHttpServletRequest getModifiableHttpServletRequest(HttpServletRequest requestBefore) {
ModifiableHttpServletRequest request = new ModifiableHttpServletRequest(requestBefore);
String name = request.getParameter(NAME);
if (name != null) request.setParameter(NAME, name.strip());
String press = request.getParameter(PRESS);
if (press != null) {
request.setParameter(PRESS, press.toUpperCase());
if (containsWithPressValue(press))
request.setParameter(PRESS, convertToPress(press).name());
}
return request;
}
}
끝!
'웹 개발 (Spring Boot)' 카테고리의 다른 글
| 레이어 아키텍처에서 클린 아키텍처 + DDD로 리팩토링하기 (1) (0) | 2025.12.04 |
|---|---|
| 사이드 프로젝트 팀장으로서의 깨달음 (2) | 2025.06.28 |
| 리플렉션(Reflection)으로 Enum 관련 유틸리티 정적 메소드 만들어 보기 (0) | 2024.10.27 |
| @TestPropertySource를 활용해서 테스트 DB 분리하기 (0) | 2024.09.27 |
| 상수를 관리할 때 중요하게 고려할 점 (3) | 2024.09.17 |