최근 프로젝트에서 MyBatis를 사용하여 여러 자산(asset) 정보를 기준으로 태그(tag) 데이터를 조회하는 기능을 구현하던 중, 성능 저하 문제가 발생했습니다. 문제의 원인을 추적해보니 foreach 루프 안에서 UNION ALL로 다수의 SELECT 구문을 반복 생성하는 방식이 주요 원인이었습니다. 본 글에서는 해당 구조의 문제점과 PostgreSQL에 적합한 최적화 방법을 실제 경험을 바탕으로 정리하였습니다.
문제 상황: MyBatis foreach + UNION ALL 사용
처음 작성한 쿼리는 아래와 같은 형태였습니다.
<select id="getTagList" resultType="java.util.HashMap">
<foreach item="item" index="index" collection="itemList" separator="UNION ALL">
SELECT
item_code,
tag_code,
tag_name,
#{item.assetId} AS asset_physical_name,
#{item.logicalId} AS asset_name
FROM
tag_efficiency_table
WHERE
logical_id = #{item.logicalId}
AND item_code = #{item.itemCode}
</foreach>
</select>
이 방식은 각 조건마다 개별 SELECT 쿼리를 만들어 UNION ALL로 연결합니다. 데이터 건수가 적을 때는 문제없이 동작하지만, 50건 이상 넘어가면 SQL 자체가 길어지고 DB 처리 시간이 급격히 증가했습니다.
원인 분석
- 반복된 SELECT 생성
MyBatis의<foreach>는 루프마다 SQL 문을 생성하므로, 100개의 조건이 있다면 100개의 SELECT 문이 생성됩니다. - 캐시 비효율
바인딩 값이 SQL 문장 내 직접 삽입되므로, DB 캐시가 제대로 작동하지 않게 됩니다. - SQL 과부하
PostgreSQL은 SQL의 길이나 복잡도에 따라 실행 계획 비용이 증가할 수 있으므로,UNION ALL남용은 오히려 병목이 됩니다.
해결 방향: 서브쿼리 + JOIN 방식
PostgreSQL에서는 서브쿼리와 JOIN을 통한 조건 필터링을 매우 효율적으로 처리합니다. 이를 활용해 다음과 같이 구조를 변경하였습니다.
최적화된 MyBatis 쿼리
<select id="getTagList" resultType="java.util.HashMap">
SELECT
t.item_code,
t.tag_code,
t.tag_name,
m.asset_id AS asset_physical_name,
m.logical_id AS asset_name
FROM
tag_efficiency_table t
INNER JOIN (
<foreach item="item" index="index" collection="itemList" separator="UNION ALL">
SELECT
#{item.assetId} AS asset_id,
#{item.logicalId} AS logical_id,
#{item.itemCode} AS item_code
</foreach>
) m ON t.item_code = m.item_code
AND t.logical_id = m.logical_id
</select>
결과 및 성능 개선
| 항목 | 개선 전 | 개선 후 |
|---|---|---|
| SQL 실행 횟수 | N회 (N개의 SELECT) | 1회 |
| SQL 문자열 길이 | N배 증가 | 일정 |
| PostgreSQL 쿼리 실행 시간 | 수 초 | 수십~수백 ms |
| 유지보수성 | 낮음 (반복 구조 복잡) | 높음 (단일 쿼리 구조) |
또한 tag_efficiency_table의 logical_id, item_code에 복합 인덱스를 추가함으로써 쿼리 성능을 더욱 끌어올릴 수 있었습니다.
CREATE INDEX idx_logical_item ON tag_efficiency_table (logical_id, item_code);
마무리하며
이번 최적화 경험을 통해 다음과 같은 인사이트를 얻을 수 있었습니다:
- MyBatis의
foreach + UNION ALL은 간편하지만, 반복 수가 많아지면 반드시 성능 병목을 점검해야 한다. - PostgreSQL에서는
UNION ALL보다JOIN을 활용한 조건 매칭이 훨씬 효율적이다. - 가능한 한 SQL을 정적이고 단일 구조로 구성하여 DB 캐시 효율을 높이는 것이 좋다.
MyBatis를 활용한 복합 조건 처리 시, 위와 같은 방식으로 리팩토링하면 운영 중 성능 문제를 예방할 수 있습니다.
반응형
'개발 (Development) > PostgreSQL' 카테고리의 다른 글
| [PostgreSQL] View와 Materialized View의 차이점과 사용법 (0) | 2025.08.03 |
|---|---|
| [PostgreSQL] 문자열을 timestamp with time zone으로 변환하는 방법 (0) | 2025.07.27 |
| [PostgreSQL] PostgreSQL에서 threshold 값 이력 관리 및 최신값 조회 테이블 설계하기 (0) | 2025.06.28 |
| [PostgreSQL] 두 테이블 비교 시 기준 테이블의 데이터를 모두 유지하는 방법 (0) | 2025.05.25 |
| [PostgreSQL] 시퀀스 자동 증가 설정과 오류 해결 방법 정리 (0) | 2025.05.18 |