문제 상황
회사에서 담당하고 있는 프로젝트에서 특정 데이터가 조회되는 않는 문제가 발생했다.
해당 문제를 해결하기 위해서 출력되는 로그를 살펴보니, 요청에서 DB의 특정 칼럼에 '·' (특수기호)가 포함되어 있었는데
화면에서 서버로 넘어올 때, '·' 문자가 디코딩되지 않은 상태인 ' · '의 형태로 넘어오고 있었다.
예시
🟢 DB에 저장된 값
- A · B
🟢 넘어오는 값의 형태
- A · B
원인 분석
문제의 핵심은 요청 파라미터에 포함된 특수문자가 HTML 엔티티 형태로 전달되고 있었다는 점이었다.
이 값이 별도의 디코딩 없이 그대로 조회 조건으로 사용되어 DB의 실제 값과 문자열이 달라지기 때문에 조회 실패가 발생했던 것이다.
즉, 표현 계층(HTML)에서 인코딩된 값이 애플리케이션/DB 조회 계층까지 그대로 흘러들어온 문제라고 볼 수 있다.
문제 해결
해당 문제를 해결하기 위해서는 해당 값을 디코딩하는 로직이 필요하였다.
다만, 현재 구조에서 어떤 계층이 맡아 해결하는 것이 적절한가에 대한 고민을 하였고, 다음과 같은 선택 사항을 고려하였다.
1. Controller에서 처리
2. Service 계층에서 처리
3. SQL에서 처리
4. 표현 계층에서 처리
5. ArgumentResolver에서 처리
1. Controller에서 처리하기
첫 번째 방법은 Controller에서 요청 파라미터를 받아 직접 디코딩하는 방식이다.
해당 방법에는 다음과 같은 문제점들이 고려되었다.
- 같은 문제가 발생하는 Controller가 여러 곳일 경우 중복 코드가 발생한다.
따라서 일부 Controller에서는 처리하고 일부에서는 누락될 가능성이 있다.
- 입력 데이터 정제 로직이 Controller에 섞이면서 역할이 모호해질 수 있다.
2. Service에서 처리하기
두 번째 방법은 Service 계층에서 조회 조건을 만들기 전에 값을 디코딩하는 방식이다.
이 방법을 사용한다면, 비즈니스 규칙이 아닌 입력 데이터 정제 문제에 가까운 해당 사항을 service 계층에서 맡기면 적절하지 않다고 판단되었다.
3. SQL에서 처리하기
세 번쨰 방법은 SQL 조회 조건에서 문자열을 변환하는 방식이다.
예를 들어 REPLACE 등을 사용해 조회 조건을 맞추는 방법이 있겠다.
하지만 이 사항에서는 아래와 같은 사항으로 적절하지 않다고 판단되었다.
- SQL이 복잡해진다.
- DB 계층에서 표현 계층 문제를 해결함으로 계층 분리가 흐려진다.
4. 표현 계층에서 처리 하기
네 번째 방법은 표현 계층(Frontend)에서 HTML 엔티티를 디코딩하여 서버로 전달하는 방식이다.
예를 들어 JavaScript에서 요청을 보내기 전에 ·를 ·로 변환하는 방식이다.
이 방법은 문제의 발생 지점이 표현 계층이기 때문에 문제의 근본 원인에 가장 가까운 위치에서 해결한다는 장점이 있다.
하지만 다음과 같은 이유로 이번 문제에서는 적절하지 않다고 판단하였다.
- 현재 프로젝트는 JSP 기반의 서버 렌더링 구조로 다양한 화면에서 동일한 요청 파라미터가 사용되고 있었다.
- 특정 화면에서만 디코딩 로직을 추가할 경우 다른 화면에서는 동일한 문제가 반복될 가능성이 있다.
- 프론트엔드 코드 수정 범위가 넓어질 수 있으며 화면별 관리 포인트가 증가할 수 있다.
따라서 표현 계층에서 처리하는 방식은 구조적으로 가능하지만, 현재 프로젝트 구조에서는 유지보수 비용이 증가할 수 있는 방법이라고 판단하였다.
5. ArgumentResolver에서 처리
마지막 방법은 ArgumentResolver를 사용하여 요청 파라미터를 변환하여 Contoller에 전달하는 방식이다.
현재 해당 프로젝트에서는 표현계층에서 흘러오는 모든 요청값을 HandlerMethodArgumentResolver 구현한 단일 객체로 받고 있었다.
(사실상 map과 다를게 없다.)
즉, 모든 요청에 대해 단일 객체로 변환하여 Controller에 전달하고 있기에 요청 파라미터가 Controller에 전달되기 전에 공통적으로 가공할 수 있는 지점인 ArgumentResolver 계층에서 디코딩 변환 작업을 처리해야 겠다고 판단하였다.
해당 판단에 대한 근거는 다음과 같았다.
- 현재 프로젝트 구조에서 모든 요청 파라미터가 ArgumentResolver를 통해 전달되고 있었다. 따라서 이 지점에서 처리하면 Controller마다 별도의 로직을 추가할 필요가 없었다.(표현 계층에서 처리하는 것과도 같은 이유에 장점을 지닌다.)
- 입력 데이터 정제 로직을 공통 처리 지점에 모을 수 있다. 동일한 문제가 발생하더라도 한 곳만 수정하면 전체에 반영할 수 있다.
- Service / DAO 계층으로 잘못된 데이터가 전달되는 것을 사전에 차단할 수 있다.
즉, 요청 파라미터가 시스템 내부로 들어오는 초입에서 데이터를 정제할 수 있는 구조이면서, 공통 처리가 가능한
ArgumentResolver에서 처리하는 것이 가장 합리적인 선택이라고 판단하였다.
구현
위와 같은 판단을 바탕으로 HandlerMethodArgumentResolver 구현체에서 HTML 엔티티를 디코딩하는 로직을 추가하였다.
while(enumeration.hasMoreElements()){
key = (String) enumeration.nextElement();
values = request.getParameterValues(key);
/* html 특수기호 디코딩 */
if(key.equals("fileCtgry3") || key.equals("codeId")) {
String refineValueString = decodeHtml(values[0]);
values[0] = refineValueString;
}
if(values != null){
requestResolver.put(key, (values.length > 1) ? values : values[0]);
}
}
디코딩 로직은 다음과 같이 구현하였다.
private String decodeHtml(String str) {
if(str == null || str.isEmpty()) {
return str;
}
return str.replace("·", "·")
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"");
}
이를 통해, 요청 파라미터로 A · B 형태의 값이 전달되더라도 내부적으로는 A · B로 변환되어 DB에 저장된 값과 동일한 문자열로 조회가 수행되도록 처리하였다.
해당 로직을 적용한 이후 정상적으로 데이터가 조회되는 것을 확인할 수 있었다.
정리
이번 문제는 특수문자가 HTML 엔티티 형태로 전달되면서 DB 조회 조건이 일치하지 않아 발생한 문제였다.
문제를 해결하기 위해 단순히 디코딩 로직을 추가하는 것이 아니라 아래와 같은 절차에 따라 판단하였다.
- 어떤 계층에서 해결하는 것이 적절한지 검토하고
- 각 방법의 장단점을 비교한 뒤
- 현재 구조에서 가장 합리적인 위치를 선택하였다.
최종적으로 HandlerMethodArgumentResolver에서 HTML 특수 문자를 디코딩을 수행하도록 처리하여 문제를 해결하는 것이 현재 구조에서 가장 유지보수 비용이 낮은 선택이라고 판단하였다.
댓글