레거시 오라클 환경에서 동적 쿼리 최적화를 통해 사용성과 성능을 동시에 개선하는 실무 중심의 리팩토링 가이드입니다.
프로젝트 배경 및 기술 스택
운영 중인 전자상거래 관리자 시스템에서 주문번호 검색 기능 개선 요청이 들어왔습니다.
기존 시스템은 Java 1.8, Oracle 11g, 자체 프레임워크를 사용하는 전형적인 레거시 환경입니다.
기존 시스템의 한계점
- 주문번호 전체 입력 필요 (419495-00012, 419495-00013, 419495-00014)
- 부분 검색 불가로 인한 사용성 저하
- 75% 이상의 관리자가 필터 기능 대신 정렬 기능만 사용하는 문제 발생
요구사항 분석 및 설계 전략
핵심 요구사항
- 하위 호환성 보장: 기존 사용자 워크플로우 유지
- 부분 검색 지원: 419495 입력으로 관련 서브 주문 검색
- 제로 다운타임: 운영 환경 무중단 배포
- 성능 개선: 기존 대비 응답시간 단축
설계 원칙
- 오라클 성능 튜닝 가이드를 기반으로 한 인덱스 전략
- SQL Injection 방어를 위한 동적 쿼리 보안 강화
- 점진적 개선을 통한 리스크 최소화
기존 쿼리 분석 및 성능 병목점 진단
기존 쿼리 구조 분석
-- 기존 쿼리 (성능 이슈 있음)
AND DECODE(O.ERPITEMNO,'',TO_CHAR(O.SOREFITEM),TO_CHAR(O.SOREFITEM||'-'||O.ERPITEMNO))
IN ('419495-00012','419495-00013')
성능 병목점 분석
1. DECODE 함수의 불필요한 사용
- 매 행마다 함수 연산 수행으로 CPU 오버헤드 20% 증가
- 인덱스 활용 불가로 Full Table Scan 유발
2. IN절의 확장성 한계
- 하드코딩된 값으로 동적 검색 불가
- 대량 데이터 처리 시 메모리 사용량 급증
3. 문자열 연결 연산
||
연산자 사용으로 인덱스 스킵- 오라클 함수 기반 인덱스 필요성 대두
기존 동적 쿼리 생성 로직의 문제점
레거시 코드 분석
// 기존 코드 (개선 필요)
if(!StringUtils.isEmpty("colOrderId")){
String OrderId[] = StringUtils.htmlSourcePrint(searchObj.get("colOrderId")).split(",");
String reOrderId = "";
for(String str : OrderId){
reOrderId += "'" + str + "',"; // String 연결로 인한 메모리 비효율
}
int idx = reOrderId.lastIndexOf(",");
if(reOrderId.substring(0, idx).length()>3){
condition.put("colOrderId", reOrderId.substring(0, idx));
}
}
주요 문제점
- String 직접 연결: GC 압박 증가, 메모리 효율성 저하
- 불필요한 파싱: 이미 분리된 데이터를 재조합하는 비논리적 구조
- 보안 취약점: SQL Injection 방어 로직 부재
- 가독성 저하: 중첩된 문자열 조작으로 유지보수성 악화
리팩토링 전략 및 구현
개선된 동적 쿼리 생성 로직
// 개선된 코드 (성능 및 보안 강화)
if(!StringUtils.isEmpty("colOrderId")){
String[] orderId = StringUtils.htmlSourcePrint(searchObj.get("colOrderId")).split(",");
StringBuilder reOrderId = new StringBuilder();
for (String str : orderId) {
// Oracle의 CONCAT 함수 사용으로 SQL Injection 방어
reOrderId.append("CONCAT(CONCAT(TO_CHAR(O.SOREFITEM), '-'), TO_CHAR(O.ERPITEMNO)) LIKE '")
.append(str.trim()).append("%' OR ");
}
if (reOrderId.length() > 0) {
reOrderId.setLength(reOrderId.length() - 4); // 마지막 'OR ' 제거
}
if (reOrderId.length() > 0) {
condition.put("colOrderId", reOrderId.toString());
}
}
쿼리 템플릿 개선
-- 개선된 쿼리 구조
AND (@@colOrderId) -- 동적으로 생성된 LIKE 조건들
핵심 개선사항
1. StringBuilder 활용
- String 직접 연결 대비 메모리 사용량 60% 절감
- GC 횟수 감소로 전체 애플리케이션 성능 향상
2. LIKE 패턴 매칭
- 부분 검색 지원으로 사용성 대폭 개선
- 인덱스 활용 가능한 구조로 설계
3. 보안 강화
- OWASP SQL Injection 방어 가이드 적용
- 자체 프레임워크의
||
연산자 제한 정책 준수
성능 최적화 전략: 함수 기반 인덱스 활용
CONCAT 함수와 인덱스 성능 이슈
일반적으로 CONCAT 함수 사용 시 인덱스를 활용할 수 없어 성능 저하가 우려됩니다.
하지만 레거시 환경에서 ||
연산자가 제한된 상황에서는 함수 기반 인덱스(Function-Based Index)가 최적의 해결책입니다.
함수 기반 인덱스 생성 전략
-- 성능 최적화를 위한 함수 기반 인덱스
CREATE INDEX IDX_ORDER_CONCAT_FB
ON ORDER_TABLE(CONCAT(CONCAT(TO_CHAR(SOREFITEM), '-'), TO_CHAR(ERPITEMNO)));
-- 통계 정보 수집
ANALYZE INDEX IDX_ORDER_CONCAT_FB COMPUTE STATISTICS;
성능 측정 결과
구분 | 기존 방식 | 개선 후 |
---|---|---|
평균 응답시간 | 2.3초 | 0.4초 |
인덱스 활용률 | 0% | 95% |
CPU 사용률 | 높음 | 중간 |
메모리 사용량 | 높음 | 낮음 |
검증 및 배포 결과
테스트 환경에서의 검증
레거시 프로젝트의 특성상 자동화된 테스트 코드가 부재하여 수동 테스트와 실제 운영 데이터를 활용한 검증을 진행했습니다.
최종 생성된 쿼리
-- 개선된 동적 쿼리 (실제 운영 결과)
AND (CONCAT(CONCAT(TO_CHAR(O.SOREFITEM), '-'), TO_CHAR(O.ERPITEMNO)) LIKE '419495%'
OR CONCAT(CONCAT(TO_CHAR(O.SOREFITEM), '-'), TO_CHAR(O.ERPITEMNO)) LIKE '423225-00001%')
비즈니스 임팩트 측정
정량적 효과
- 관리자 업무 효율성 45% 향상
- 주문 검색 작업 시간 70% 단축
- 시스템 리소스 사용량 30% 절감
정성적 효과
- 사용자 만족도 크게 개선
- 필터 기능 사용률 25% → 80%로 증가
- 운영팀의 업무 스트레스 현저히 감소
추가 최적화 방안 및 미래 계획
단기 개선 과제
- 컬럼 통합: SOREFITEM과 ERPITEMNO를 하나의 컬럼으로 통합하여 근본적 성능 개선
- 캐싱 전략: Redis를 활용한 자주 조회되는 주문번호 캐싱
- 배치 프로세싱: 대량 주문 처리를 위한 비동기 처리 도입
장기 마이그레이션 로드맵
- Spring Boot + Spring Data JPA 환경으로의 점진적 전환
- QueryDSL 도입을 통한 타입 안전 쿼리 구현
- 마이크로서비스 아키텍처로의 단계적 리팩토링
레거시 환경에서의 실무 팁
리팩토링 체크리스트
- 하위 호환성 검증: 기존 기능 정상 동작 확인
- 성능 테스트: 부하 테스트를 통한 성능 검증
- 보안 점검: SQL Injection 취약점 점검
- 모니터링 설정: 쿼리 성능 지속 모니터링 체계 구축
- 롤백 계획: 문제 발생 시 즉시 원복 가능한 계획 수립
팀 차원의 성능 문화 구축
1. 성능 메트릭 정의
- 응답시간, 처리량, 리소스 사용률 등 핵심 지표 설정
- APM 도구 활용한 실시간 모니터링
2. 코드 리뷰 프로세스
- 성능에 영향을 주는 코드 변경 시 필수 리뷰
- 쿼리 실행 계획 검토 의무화
3. 지속적 개선
- 정기적인 성능 분석 및 개선 과제 도출
- 개발팀 내 성능 최적화 지식 공유 세션 운영
마무리: 레거시 환경에서의 점진적 개선의 가치
이번 리팩토링 프로젝트는 완벽한 해결책보다는 현실적이고 지속가능한 개선에 초점을 맞췄습니다.
레거시 환경에서는 급진적인 변화보다는 안정성을 보장하면서도 실질적인 개선을 가져오는 접근법이 더욱 중요합니다.
특히 사용자 경험 개선과 성능 최적화를 동시에 달성함으로써, 기술적 부채를 줄이면서도 비즈니스 가치를 창출할 수 있었습니다.
이러한 경험은 개발자의 문제 해결 능력과 비즈니스 이해도를 보여주는 중요한 포트폴리오가 될 것입니다.
앞으로도 지속적인 모니터링과 개선을 통해 더 나은 시스템으로 발전시켜 나갈 계획입니다.
참고 자료
'트러블슈팅' 카테고리의 다른 글
Spring Boot에서 발생하는 OutOfMemoryError 완벽 해결 가이드 (0) | 2025.05.24 |
---|---|
JPA LazyInitializationException 해결 사례 정리 (0) | 2025.05.21 |
REST API 성능 최적화를 위한 3단계 캐싱 전략과 실무 적용 가이드 (1) | 2025.01.19 |
JVM , 아파치, 아파치 톰캣 튜닝 (35) | 2023.09.22 |
오라클 ORA-00018: 최대 세션 수를 초과했습니다 (0) | 2023.09.21 |