티스토리 뷰
이 글에서는 트랜잭션 과다 생성으로 인한 성능 저하 문제와 배치 인서트를 활용한 성능 개선 방법을 설명합니다. Spring Framework 공식 문서를 기준으로 트랜잭션 관리와 배치 처리의 원리를 기술합니다.
소개 · 배경
트랜잭션은 데이터베이스에서 일련의 작업을 하나의 단위로 묶어 처리하는 메커니즘입니다. Spring Framework 공식 트랜잭션 문서에 따르면, 트랜잭션은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 보장하기 위해 설계되었습니다.
트랜잭션 생성에는 비용이 발생합니다. Spring Framework 공식 문서에 따르면, 트랜잭션 시작 시 데이터 소스 리소스가 트랜잭션 컨텍스트에 바인딩되며, 이 과정에서 커넥션 획득 및 동기화 비용이 발생합니다. 또한 각 트랜잭션마다 커밋 또는 롤백 작업이 수행되며, 이러한 작업은 데이터베이스와의 통신 비용을 포함합니다.
대량의 데이터를 처리할 때 각 레코드마다 개별 트랜잭션을 생성하면, 트랜잭션 생성 비용이 누적되어 성능 저하가 발생합니다. 예를 들어, 1,000건의 데이터를 처리할 때 각 건마다 트랜잭션을 생성하면 1,000번의 트랜잭션 생성과 커밋 작업이 발생합니다. 이러한 문제를 해결하기 위해 배치 인서트(Batch Insert) 기법을 사용하여 여러 레코드를 하나의 트랜잭션으로 묶어 처리할 수 있습니다.
배치성 insert가 필요한 이유는 다음과 같습니다. 여러 INSERT 문을 하나의 배치로 묶어 실행하면 데이터베이스와의 통신 횟수를 줄일 수 있습니다. 또한 하나의 트랜잭션으로 여러 레코드를 처리하면 트랜잭션 생성 비용을 분산시킬 수 있습니다. 이러한 최적화는 대량 데이터 처리 시 성능 향상에 중요한 역할을 합니다.
Spring Framework 트랜잭션 관리 공식 정의
트랜잭션 경계
Spring Framework 공식 트랜잭션 문서에 따르면, 트랜잭션 경계(Transaction Boundary)는 트랜잭션이 시작되고 종료되는 지점을 의미합니다. @Transactional 어노테이션이 적용된 메서드가 호출되면 트랜잭션이 시작되며, 메서드가 정상적으로 완료되면 커밋되고 예외가 발생하면 롤백됩니다.
트랜잭션 경계는 선언적 트랜잭션 관리(Declarative Transaction Management)를 통해 설정됩니다. Spring Framework는 AOP(Aspect-Oriented Programming)를 사용하여 트랜잭션 경계를 관리하며, TransactionInterceptor가 트랜잭션의 시작과 종료를 제어합니다.
Spring 트랜잭션은 AOP 기반 프록시를 통해 관리됩니다. 따라서 @Transactional이 적용된 메서드는 프록시 외부에서 호출될 때만 트랜잭션이 생성됩니다. 동일 클래스 내부에서 다른 @Transactional 메서드를 호출하는 경우(Self-invocation)에는 트랜잭션이 적용되지 않으므로 주의가 필요합니다. 이를 통해 트랜잭션 경계와 실제 트랜잭션 적용 범위를 명확히 이해할 수 있습니다.
커밋과 롤백
Spring Framework 공식 트랜잭션 문서에 따르면, 커밋(Commit)은 트랜잭션 내에서 수행된 모든 변경사항을 데이터베이스에 영구적으로 반영하는 작업입니다. 롤백(Rollback)은 트랜잭션 내에서 수행된 모든 변경사항을 취소하는 작업입니다.
커밋과 롤백은 데이터베이스와의 통신을 필요로 하며, 이러한 통신은 네트워크 지연과 데이터베이스 처리 시간을 포함합니다. 각 트랜잭션마다 커밋 또는 롤백 작업이 수행되므로, 트랜잭션 수가 증가하면 커밋/롤백 오버헤드도 비례하여 증가합니다.
실무 코드 설명
XML 트랜잭션 설정
Spring Framework에서 트랜잭션을 선언적으로 관리하기 위한 XML 설정입니다:
... (생략)
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="retrieve*" read-only="true" />
<tx:method name="insert*" rollback-for="Exception" />
<tx:method name="update*" rollback-for="Exception" />
<tx:method name="delete*" rollback-for="Exception" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(*.service..impl.*ServiceImpl.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
</aop:config>
코드 설명:
tx:advice는 Spring Framework 공식 트랜잭션 문서에 정의된 요소로, 트랜잭션 어드바이스를 정의합니다. transaction-manager 속성은 사용할 트랜잭션 매니저를 지정하며, tx:attributes 내부의 tx:method는 각 메서드에 적용할 트랜잭션 속성을 정의합니다.
propagation="REQUIRED"는 Spring Framework 공식 트랜잭션 문서에 정의된 전파 속성으로, 기존 트랜잭션이 있으면 참여하고 없으면 새로운 트랜잭션을 생성합니다. aop:config는 AOP 설정을 정의하며, aop:pointcut은 트랜잭션이 적용될 메서드를 지정하고, aop:advisor는 트랜잭션 어드바이스를 적용합니다.
개선 전: 컨트롤러 반복 호출
컨트롤러에서 반복문을 통해 개별적으로 서비스 메서드를 호출하는 코드입니다:
// *Controller.java
for(int i=0; i<customerModelArray.length; i++){
... 생략
warrantyService.insertWtysvcingInfoModel(input);
}
코드 설명:
위 코드는 각 고객 모델마다 개별적으로 서비스 메서드를 호출합니다. Spring Framework 공식 트랜잭션 문서에 따르면, propagation=REQUIRED가 적용된 @Transactional 서비스 메서드는, 기존 트랜잭션이 없을 경우 새로운 트랜잭션을 생성합니다.
반복문을 통해 1,000건의 데이터를 처리하면 1,000개의 트랜잭션이 생성되고 각각 커밋됩니다. 각 트랜잭션마다 데이터베이스 연결 획득, 트랜잭션 로그 초기화, 커밋 작업 등의 오버헤드가 발생하므로, 전체 처리 시간이 크게 증가합니다.
개선 후: 배치 insert 코드
배치 인서트를 사용하여 여러 레코드를 하나의 트랜잭션으로 처리하는 코드입니다:
// *Controller.java
int cnt = warrantyService.insertWtysvcingInfoModelList(input);
mv.addObject("cnt", cnt);
// *ServiceImpl.java
@Override
public int insertWtysvcingInfoModelList(Map<String, Object> input) {
int count = 0;
try{
... (생략)
count = commonDao.batchInsert("Warranty.insertWtysvcingInfoModel", list, "publ");
} catch(Exception e) {
throw new AjaxException(e);
}
return count;
}
코드 설명:
컨트롤러에서는 모든 데이터를 리스트로 수집하여 서비스 메서드에 한 번에 전달합니다. 서비스 메서드는 @Transactional 어노테이션이 적용되어 있으므로, 전체 리스트를 하나의 트랜잭션으로 처리합니다.
commonDao.batchInsert 메서드는 MyBatis의 배치 처리 기능을 활용합니다. MyBatis는 ExecutorType.BATCH를 통해 여러 SQL 문을 하나의 배치로 묶어 실행하도록 설계되어 있으며, 이를 통해 JDBC 레벨에서 배치 실행이 수행됩니다. 이를 통해 데이터베이스와의 통신 횟수를 줄이고, 하나의 트랜잭션으로 여러 레코드를 처리하여 트랜잭션 오버헤드를 감소시킵니다.
Spring Framework 공식 트랜잭션 문서에 따르면, 하나의 트랜잭션으로 여러 작업을 처리하면 트랜잭션 생성 비용을 분산시킬 수 있습니다. 1,000건의 데이터를 하나의 트랜잭션으로 처리하면 트랜잭션 생성과 커밋 작업이 각각 1회만 발생하므로, 성능이 크게 향상됩니다.
결과적으로, 이러한 패턴의 코드들을 모두 개선한 후에 해당하는 백오피스 페이지의 검색 속도가 기존 9초에서 1초, 30초에서 3초 내외로 약 9배 속도 향상하는 효과가 있었습니다.
MyBatis에서 배치 모드(ExecutorType.BATCH)는 SQL 실행을 지연시켜 여러 INSERT 문을 모은 뒤, 트랜잭션 커밋 시점이나 명시적 flush 시 한 번에 실행합니다. 이를 통해 JDBC 레벨에서 DB 통신 횟수를 줄이고, 하나의 트랜잭션으로 여러 레코드를 처리함으로써 트랜잭션 오버헤드를 크게 감소시킬 수 있습니다. Spring과 연동 시, @Transactional 범위 내에서 배치가 커밋되므로, 트랜잭션 경계와 배치 실행 시점을 일관되게 관리할 수 있습니다.
부하 발생 원인 공식 분석
DB Round-trip 오버헤드
데이터베이스와의 통신은 네트워크 지연을 포함합니다. 각 SQL 문 실행 시 데이터베이스로 요청을 보내고 응답을 받는 과정에서 네트워크 지연이 발생하며, 이러한 지연은 레코드 수에 비례하여 증가합니다.
개별 트랜잭션 방식에서는 각 레코드마다 INSERT 문이 실행되므로, 데이터베이스와의 통신 횟수가 레코드 수와 동일합니다. 예를 들어, 1,000건의 데이터를 처리하면 1,000번의 데이터베이스 통신이 발생합니다. 배치 인서트 방식에서는 여러 INSERT 문을 하나의 배치로 묶어 실행하므로, 데이터베이스 통신 횟수가 크게 감소합니다.
트랜잭션 커밋 비용
Spring Framework 공식 트랜잭션 문서에 따르면, 트랜잭션 커밋은 다음과 같은 작업을 수행합니다. 트랜잭션 로그를 디스크에 기록하고, 데이터베이스 버퍼의 변경사항을 디스크에 반영하며, 락을 해제합니다. 이러한 작업은 디스크 I/O를 포함하므로 상대적으로 비용이 높습니다.
개별 트랜잭션 방식에서는 각 레코드마다 커밋 작업이 수행되므로, 커밋 비용이 레코드 수에 비례하여 증가합니다. 배치 인서트 방식에서는 하나의 트랜잭션으로 여러 레코드를 처리하므로, 커밋 작업이 1회만 수행되어 커밋 비용이 크게 감소합니다.
실무적 고려사항 및 팁
트랜잭션 최적화 시 점검 포인트
트랜잭션 최적화를 위해 다음 사항을 점검해야 합니다. 서비스 메서드의 트랜잭션 경계를 확인하여 불필요한 트랜잭션 생성이 발생하지 않는지 확인합니다. 컨트롤러에서 반복적으로 서비스 메서드를 호출하는 대신, 데이터를 수집하여 한 번의 서비스 메서드 호출로 처리하는지 확인합니다.
배치 처리 시 트랜잭션 크기를 적절히 조정해야 합니다. 트랜잭션이 너무 크면 롤백 시 영향 범위가 넓어지고, 메모리 사용량이 증가할 수 있습니다. 반면 트랜잭션이 너무 작으면 트랜잭션 오버헤드가 증가합니다. 따라서 데이터 특성과 시스템 리소스를 고려하여 적절한 배치 크기를 결정해야 합니다.
대량 insert 실행 시 주의사항
대량 insert를 실행할 때는 다음 사항을 주의해야 합니다. 하나의 트랜잭션으로 너무 많은 레코드를 처리하면 트랜잭션 로그가 증가하여 메모리 사용량이 증가할 수 있습니다. 또한 롤백 시 영향 범위가 넓어지므로, 배치 크기를 적절히 조정하여 중간 중간 커밋하는 방식을 고려할 수 있습니다.
데이터베이스 락 경합을 최소화하기 위해 인덱스 설계를 최적화해야 합니다. 대량 insert 시 인덱스 업데이트 비용이 증가하므로, 필요에 따라 인덱스를 일시적으로 비활성화하거나 배치 처리 후 재생성하는 방식을 고려할 수 있습니다.
맺음말 및 요약
이 글에서는 트랜잭션 과다 생성으로 인한 성능 저하 문제와 배치 인서트를 활용한 성능 개선 방법을 설명했습니다. Spring Framework 공식 트랜잭션 문서에 따르면, 트랜잭션 생성과 커밋에는 비용이 발생하며, 이러한 비용은 트랜잭션 수에 비례하여 증가합니다.
핵심 체크 포인트는 다음과 같습니다. 컨트롤러에서 반복적으로 서비스 메서드를 호출하는 대신, 데이터를 수집하여 한 번의 서비스 메서드 호출로 처리해야 합니다. MyBatis의 배치 처리 기능을 활용하여 여러 INSERT 문을 하나의 배치로 묶어 실행해야 합니다. 하나의 트랜잭션으로 여러 레코드를 처리하여 트랜잭션 오버헤드를 감소시켜야 합니다.
이러한 최적화를 통해 기존에 90초 이상 소요되던 작업을 10초 내외로 단축할 수 있으며, 이는 본 사례에서 측정된 실무 환경 기준 성능 개선 결과이며, 배치 인서트 적용 시 트랜잭션 및 DB 호출 횟수 감소 효과를 보여줍니다. 트랜잭션 관리와 데이터 처리 로직을 최적화하여 안정적이고 효율적인 시스템을 구축할 수 있습니다.
'WORK-RELATED' 카테고리의 다른 글
| [Spring Batch 5] API 호출 Item Reader 커스텀과 csv 데이터 생성 (0) | 2025.03.28 |
|---|---|
| [Spring Batch 5] Meta Data 관리와 reader-writer datasource 분리 (0) | 2025.03.28 |
| [Spring WebFlux] 마이바티스 배치업데이트, 스프링 HTTP API 비동기 요청 적용해 배치 잡 속도 개선하기 (0) | 2023.04.03 |
| [Spring boot 3] 분산 데이터베이스 트랜잭션 관리 (0) | 2023.02.23 |
| [Spring Boot 3] XML 파일을 JSON 문자열로 변환하기 (0) | 2023.01.31 |
- Total
- Today
- Yesterday
- TTL 설계
- 동시성처리
- Hot Key 문제
- 트랜잭션 관리
- Eager Initialization
- Initialization-on-Demand Holder Idiom
- 백엔드 성능 설계
- 트래픽 처리
- 캐시 성능 비교
- Redis 캐시 전략
- Double-Checked Locking
- InterruptedException
- 캐시 장애
- Cache Aside
- Redis 성능 개선
- 백엔드 아키텍처
- 백엔드 성능 튜닝
- 백엔드 성능
- Cache Avalanche
- DB 인덱스 성능
- Java Performance
- Spring Batch
- Cache Penetration
- Enum 기반 싱글톤
- 스레드 생명주기
- mybatis
- spring batch 5
- Redis vs DB
- 캐시와 인덱스
- DB 트랜잭션
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

