0. 들어가며
2026.01.04 - [개발관련] - [MySQL] 복합 인덱스 적용 및 성능테스트 트러블 슈팅
지난 포스팅에서 MySQL 복합 인덱스 적용으로 조회 성능을 개선했습니다.
성능테스트를 진행하기 전, 다양한 최적화 방안을 고민했습니다. 그중 하나가 애플리케이션 레벨에서의 쿼리 전송 방식을 점검하는 것이었습니다.
현재 프로젝트는 JPA(Hibernate)를 사용하고 있습니다. 이에 JPA가 내부적으로 어떤 방식을 사용하여 DB와 통신하는지, 그리고 그것이 대용량 트래픽 처리에 적합한 방식인지 검증하기 위해 JDBC 인터페이스(Statement, PreparedStatement, CallableStatement) 개념들을 다시 한번 정리해 보았습니다.
java.sql.Statement (부모: 기본 쿼리 실행)
└── java.sql.PreparedStatement (자식: + 캐싱, 파라미터 바인딩)
└── java.sql.CallableStatement (손자: + 프로시저 호출, Output 파라미터)
1. Statement
가장 기본적인 JDBC 인터페이스로 SQL문을 단순 문자열 그대로 DB에 전송합니다.
- 특징: 주로 정적 SQL을 만들 때 사용합니다. 여기서 정적 SQL이란 매개변수가 없어 쿼리 문장 자체가 변하지 않는 상태를 의미합니다.
- 동작 방식: 문자열로 완성된 쿼리를 전달하기 때문에, 값이 바뀔 때마다 쿼리 문장 전체를 새로 생성해야 합니다.
- 문제점
- 성능 저하: 값이 바뀔 때마다 DB는 매번 새로운 쿼리로 인식하여 파싱과 컴파일을 반복합니다.
- 보안 취약: 문자열 연결 방식(+ 또는 String.format)을 사용하므로 SQL Injection 공격에 매우 취약합니다.
*SQL Injection: 악의적인 사용자가 입력창에 SQL 구문을 주입하여, 데이터베이스를 조작하거나 비정상적으로 데이터를 유출시키는 공격 기법입니다.
// 1. 매개변수가 없는 순수 정적 쿼리
String sql = "INSERT INTO tblAddress VALUES (seq.nextVal, 'Sopia', 21, 'f', '서울시', default)";
// 2. 값을 외부에서 받는 경우 (문자열 결합 방식 -> Injection 위험)
String name = "Sopia";
int age = 21;
String sql = String.format("INSERT INTO tblAddress VALUES (seqAddress.nextVal, '%s', %s, ...)", name, age);
Statement stmt = conn.createStatement();
stmt.executeUpdate(sql);
2. PreparedStatement (JPA의 선택)
Statement를 상속받은 인터페이스로 동적 SQL을 처리하는 데 최적화되어 있습니다.
- 특징: 매개변수(?)가 포함된 동적 쿼리를 실행할 때 사용합니다. 쿼리의 뼈대는 고정하되 데이터만 실시간으로 갈아 끼우는 방식입니다.
- 동작 방식: 쿼리를 미리 컴파일해두고, 실행 시에는 바인딩된 매개변수 값만 전달합니다.
- 장점
- 성능 최적화: DB가 실행 계획을 재사용하므로 반복 요청 시 매우 빠릅니다.
- 보안성: 매개변수로 전달된 값을 단순 텍스트로 처리하기 때문에 SQL Injection을 원천 차단합니다.
// 매개변수(?)를 사용하는 동적 쿼리 틀 정의
String sql = "INSERT INTO tblAddress VALUES (seqAddress.nextVal, ?, ?, ?, ?, default)";
PreparedStatement pstmt = conn.prepareStatement(sql);
// 실행 시점에 동적으로 값 바인딩 (안전함)
pstmt.setString(1, "Sopia");
pstmt.setInt(2, 21);
pstmt.executeUpdate();
3. CallableStatement
DB 내부에 정의된 스토어드 프로시저를 호출하기 위한 인터페이스입니다. PreparedStatement를 상속받아 파라미터 바인딩 기능을 그대로 사용하면서, 프로시저만의 Output 파라미터 처리 기능을 추가로 가집니다.
- 특징: 복잡한 로직을 DB 서버 내부 함수(프로시저)로 정의해두고 Java에서는 호출만 합니다.
- *스토어드 프로시저: 일련의 쿼리문들을 하나의 함수처럼 선언하여 데이터베이스에 저장해 둔 것입니다.
- 장점(네트워크 비용 감소): 여러 번의 쿼리 요청을 한 번의 호출로 처리하므로 네트워크 왕복 시간을 줄여 응답 속도를 단축할 수 있습니다.
- 단점 및 고려 대상 제외 이유 (Trade-off)
- DB CPU 사용률 급증: 로직 수행 주체가 WAS에서 DB 서버로 넘어갑니다. WAS는 확장이 쉽지만 DB는 리소스 확장이 어렵고 비용이 비쌉니다. 연산 부하가 DB에 집중되면 CPU 사용률이 급증하여 전체 시스템의 병목이 될 위험이 있습니다.
- 유지보수성 저하: 로직이 DB 내부에 숨어 있어 형상 관리와 디버깅이 까다롭습니다.
// DB에 저장된 프로시저 호출 (?는 파라미터)
String runProcedure = "{call get_stock_history(?)}";
CallableStatement cstmt = conn.prepareCall(runProcedure);
cstmt.setString(1, "005930");
cstmt.execute();
4. 요약 및 결론
| 구분 | Statement | PreparedStatement | CallableStatement |
| 주요용도 | 정적 SQL (단순 실행) | 동적 SQL (파라미터 중심) | 스토어드 프로시저 호출 |
| 쿼리해석 | 실행 시마다 매번 파싱 | 최초 1회만 파싱 (캐싱) | DB에 이미 저장됨 |
| 보안성 | SQL Injection에 취약 | 매우 안전 | 안전 |
| 리소스부하 | DB 파싱 부하 발생 | WAS/DB 리소스 균형 | DB CPU 부하 집중 |
JPA(Hibernate)는 내부적으로 PreparedStatement를 기본 채택하고 있습니다.
결국 성능 최적화는 단순히 실행 시간을 줄이는 것뿐만 아니라 시스템 전체 리소스(CPU, 메모리, 네트워크)의 균형을 맞춰 고려해야하는 과정임을 확인했습니다.
단순히 프레임워크의 기능을 사용하는 것에 그치지 않고 동작 원리와 리소스 사용량 사이의 Trade-off를 고민할 수 있었습니다.
참고
https://www.datacadamia.com/lang/java/jdbc/callable_statement
JDBC - Callable Statement (Stored Procedure)
The CallableStatement objects interface adds methods to the statement interface for retrieving output parameter values returned from stored procedures. See java/sql/CallableStatementCallableStatement objects java/sql/CallableStatementCallableStatement obje
www.datacadamia.com
'개발관련' 카테고리의 다른 글
| [MySQL] 복합 인덱스 적용 및 성능테스트 트러블 슈팅 (2) | 2026.01.04 |
|---|---|
| WebSocket (2) | 2025.11.10 |
| [Spring Boot] Authentication & Authorization (2) | 2025.09.08 |
| Session, JWT, OAuth (0) | 2025.08.12 |
| static, Thread-safe (4) | 2025.08.04 |