개발 시 요구사항은 간단하지만, 시스템 제약으로 구현이 복잡해지는 경우가 많다.
최근 직원 목록 조회 API에 '다중 스킬' 검색 기능을 추가해야 했다.
요구사항
- 스킬 코드를 `/`로 구분된 하나의 문자열(예: "LANGU001/LANGU002/ LANGU003")로 전달한다.
- 해당 스킬 중 하나라도 보유한 직원을 모두 검색해야 한다.
기존 쿼리는 `GROUP_CONCAT` 함수를 사용해 각 직원의 모든 스킬을 하나의 문자열로 가지고 있었다. 이 프로젝트를 해결하는 과정에서 마주친 제약과 그 해결책을 공유한다.
첫 번째 : `REGEXP`를 이용한 간단한 접근
가장 먼저 떠오른 방법은 `GROUP_CONCAT`으로 생성된 문자열을 직접 검색하는 것이다. MySQL의 정규표현식 함수 `REGEXP`를 사용하면 간단히 구현할 수 있다.
검색어 "LANGU001/LANGU002"에서 구분자 `/`를 정규표현식의 `OR`를 의미하는 `|`로 치환한다.
이 "LANGU001|LANGU002" 패턴이 DB 컬럼 값에 포함되어 있는지 확인한다.
<if test=" dto.skillCd != null and !dto.skillCd.equals('') ">
AND es.skill_cd REGEXP REPLACE(#{dto.skillCd}, '/', '|')
</if>
장점 구현이 간단하고 기존 쿼리 구조를 거의 변경하지 않는다.
단점 문자열 검색은 인덱스를 사용할 수 없어 데이터가 많아지면 성능 저하를 유발할 수 있다.
두 번째 : 성능 최적화를 위한 `EXISTS`와 `<foreach>`
문자열 검색의 성능 한계를 극복하기 위한 정석적인 방법은, 정규화된 `employees_skill` 테이블을 직접 조회하여 조건에 맞는 스킬이 존재하는지(EXISTS) 확인하는 것이다. MyBatis의 `<foreach>`로 동적 `IN` 절을 생성한다.
Service단에서 `/`로 구분된 문자열을 `List<String>`으로 변환한다.
이 리스트를 `<foreach>` 구문에 넘겨 `IN ('LANGU001', 'LANGU002')`와 같은 SQL을 동적으로 생성한다.
<if test=" skillCdList != null and !skillCdList.isEmpty() ">
AND EXISTS (
SELECT 1
FROM employees_skill s
WHERE s.employee_num = e.employee_num
AND s.skill_cd IN
<foreach item="skill" collection="skillCdList" open="(" separator="," close=")">
#{skill}
</foreach>
)
</if>
장점 `employees_skill` 테이블의 인덱스를 효과적으로 사용해 성능이 매우 뛰어나다.
단점 Service단 로직 추가 및 Mapper 파라미터 타입 변경이 필요할 수 있다.
이것이 가장 이상적인 해결책이었으나, 새로운 제약 조건이 등장했다.
제약 조건 1: "Mapper에는 DTO만 전달해야 한다."
프로젝트의 아키텍처 규칙상 Mapper에 `Map`을 넘길 수 없고, 반드시 정해진 DTO 객체만 사용해야 했다.
해결책 DTO 클래스에 `List<String>` 타입의 쿼리용 필드를 새로 추가한다. Service에서 문자열을 `List`로 변환한 뒤, 같은 DTO 객체의 새로운 필드에 값을 설정하여 Mapper로 전달한다.
제약 조건 2: "DTO에 `List` 타입을 추가할 수 없다."
더 강력한 제약이 나타났다. DTO는 순수 데이터 객체(POD)여야 하며, 다른 시스템과의 호환성 문제로 `List` 같은 컬렉션 타입을 필드로 가질 수 없다는 규칙이었다.
이로 인해 `<foreach>` 사용의 전제 조건인 '반복 가능한 컬렉션 전달' 이 원천적으로 불가능해졌다.
최종 결론 제약 속에서 찾은 최선의 선택
모든 시도와 제약 조건을 종합한 결과, 첫 번째 방법이었던 `REGEXP`를 사용해야 한다는 결론에 도달했다.
| 방법 | DTO에 List 추가 | Mapper에 DTO 전달 | 성능 | 최종 결론 |
| `EXISTS` + `<foreach>` | 필수 | 가능 | 최상 | 제약 조건으로 사용 불가 |
| `REGEXP` | 불필요 | 가능 | 상황에 따라 다름 | 모든 제약 조건을 만족하는 유일한 대안 |
성능은 아쉽지만, 주어진 프로젝트의 규칙 안에서 요구사항을 만족시키는 유일한 방법이기 때문이다.
개발자는 주어진 제약 안에서 가장 합리적인 해결책을 찾는 문제 해결사이다. 이런 상황에서는 다음 순서로 접근하는 것을 권장한다.
1. 가장 이상적이고 성능이 좋은 방법을 먼저 설계한다.
2. 프로젝트의 제약 조건과 부딪히는 부분을 파악한다.
3. 제약 내에서 구현 가능한 차선책을 선택한다. (`REGEXP`)
4. 차선책의 성능 이슈를 인지하고, 실제 운영 환경에서 모니터링 후 문제가 될 경우 제약 조건 자체의 개선을 팀과 논의한다.
'Java' 카테고리의 다른 글
| @RequestBody의 유무에 따른 DTO 매핑 차이와 타임리프/React 컨트롤러 비교 (1) | 2025.07.10 |
|---|---|
| [JAVA] Logger 사용하기 (0) | 2024.10.28 |