728x90
320x100
[서론]
2025년 1월, 새해가 되니 내가 담당자?!
작년에 클라이언트가 개선사항을 요청했고 현재 '운영중'이니 내년에 개선하겠다고 약속을 했으며, 그 약속을 했던 전임자는 떠나갔다. 그럼 개선은...? 올해 담당자인 내가 해야 한다. 그래서 써보는 개선 일지.
[요청사항]
- "1,000건 이상 엑셀 업로드를 할 때 자꾸 에러가 떠요! 1000건 이상 업로드 되게끔 해주세요!"
[문제상황 및 원인]
- [상황] 대량 데이터 DB INSERT 시 성능 저하 및 오류 발생 (ORA-01653 등)
- [원인] 기존에는 개별 INSERT 문을 실행했으며, 1건씩 처리했기 때문에 성능 저하와 에러가 발생함.
- [상황] API 통신을 위한 데이터 변환시 속도 저하 발생 (1,500건 2분 가량 소요)
- [원인] 그리드 데이터를 가공하여 API로 전송할 때 for문을 사용하여 변환 → 데이터가 많아질수록 API 요청 전 가공 시간이 급격히 증가
[해결하기]
1.대량 데이터 DB INSERT 시 성능 저하 및 오류 발생
→ SQL SESSION을 활용하여 배치 인서트(BATCH INSERT) 방식으로 변경하여 INSERT 실행
좀 더 쉽게 풀어보자면!
[기존 방식]
INSERT → COMMIT → INSERT → COMMIT → INSERT → COMMIT
👉 1000건이면 1000번 반복 → 성능 저하, 네트워크 부하 증가
[배치 인서트(BATCH INSERT) 방식]
INSERT, INSERT, INSERT .. (1000건 모아둔 후 한꺼번에 실행) COMMIT (1번만!)
👉 즉, 1000번을 따로 실행하는 대신, 1000개를 모아 한꺼번에 처리하는 방식!
[MyBatis sqlSession을 활용한 배치 INSERT 예제]
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
public void insertBatch(List<MyData> dataList) {
SqlSessionFactory sqlSessionFactory = MyBatisUtil.getSqlSessionFactory();
try (SqlSession sqlSession = sqlSessionFactory.openSession(false)) { // autoCommit=false
for (int i = 0; i < dataList.size(); i++) {
sqlSession.insert("myMapper.insertData", dataList.get(i));
// 1000개마다 플러시 및 커밋
if (i % 1000 == 0) {
sqlSession.flushStatements();
sqlSession.commit();
}
}
sqlSession.commit(); // 마지막 커밋
} catch (Exception e) {
sqlSession.rollback();
throw e;
}
}
- 1000건 단위로 flush + commit 하여 성능 최적화.
- rollback 포함하여 예외 발생 시 처리 가능.
[왜 배치 인서트 방식을 사용했는가?]
1. SQL 파싱 비용 절감
- 개별적으로 INSERT를 실행하면 매번 SQL을 파싱, 실행 계획 수립, 바인딩 등의 과정이 필요. 배치 배치 처리 시 한 번만 SQL을 파싱하고 여러 데이터를 한 번에 처리할 수 있어 성능이 향상됨.
2. 네트워크 오버헤드 감소
- 개별 INSERT는 각각의 SQL이 데이터베이스와 통신해야 하지만, 배치 방식은 한 번에 여러 개를 전송하여 네트워크 부하를 줄임.
3. 트랜잭션 처리 효율성
- 개별 INSERT 실행 시마다 COMMIT 하면 Redo Log와 Undo Log가 반복적으로 기록됨.
- 배치 방식으로 COMMIT을 줄이면 디스크 I/O를 줄여 성능이 좋아짐.
4. Redo Log와 Undo Log 사용량 감소
- Oracle은 INSERT 연산 시 **Redo Log(복구 로그)와 Undo Log(롤백 정보)**를 저장해야 함.
- 배치 INSERT는 여러 건을 한꺼번에 처리하여 로그 기록을 최소화할 수 있음.
[배치 인서트 방식으로 왜 SQL SESSION을 사용했는가?]
🔥 배치 INSERT 방식 비교
방식 | 설명 | 장점 | 단점 |
JDBC Batch (executeBatch()) |
PreparedStatement.addBatch()로 여러 개의 SQL을 모아서 한 번에 실행 | ✅ 빠름 (SQL 실행 횟수 감소) ✅ DBMS 독립적 (MyBatis, Hibernate 등과 같이 사용 가능) |
❌ 직접 커넥션, 트랜잭션 관리 필요 ❌ 예외 발생 시 개별 처리 어려움 |
MyBatis Batch (sqlSession) |
MyBatis의 flushStatements()를 사용하여 여러 개의 INSERT를 한 번에 실행 | ✅ JDBC보다 편리한 트랜잭션 관리 ✅ flushStatements()로 적절한 시점에 커밋 가능 |
❌ 배치 크기 튜닝 필요 (flushStatements() 주기 조절해야 함) ❌ 일부 DBMS에서 성능 최적화가 다를 수 있음 |
PL/SQL Bulk Insert (FORALL, BULK COLLECT) |
Oracle에서 PL/SQL을 이용하여 대량 데이터를 빠르게 삽입하는 방식 | ✅ 가장 빠름 (PL/SQL 엔진에서 직접 처리) ✅ 네트워크 부하 최소화 ✅ 커밋 제어 가능 |
❌ Oracle 전용 (다른 DB에서는 사용 불가) ❌ 트랜잭션 롤백 시 개별 행 처리 어려움 |
💡 MyBatis 배치 INSERT를 선택한 이유
1️⃣ JDBC executeBatch()보다 MyBatis가 편리
- JDBC는 직접 커넥션을 관리해야 하지만, MyBatis는 `sqlSession.flushStatements()`로 쉽게 처리 가능.
2️⃣ 트랜잭션 관리가 용이
- `commit()`을 적절한 타이밍에 실행할 수 있어, 대량 데이터 삽입 시 안정성이 높음.
3️⃣ SQL과 로직 분리
- XML 기반으로 SQL을 관리할 수 있어 유지보수가 쉬움.
[개선 결과]
비교 항목 | 기존 방식 (개별 INSERT) | 개선 후 (SQL Session 배치) |
실행 시간 (1,500 건) | 에러 발생 | 20초 |
메모리 사용량 | 높음 (OutOfMemory 가능) | 낮음 (적절한 flush로 관리) |
2. API 통신을 위한 데이터 변환시 속도 저하 발생(1,500건 2분 가량 소요)
→ 성능 최적화 코드 개선 (for → map)
[기존 Javascript 코드 예시]
let transformedData = [];
for (let i = 0; i < gridData.length; i++) {
transformedData.push({
id: gridData[i].id,
name: gridData[i].name,
value: gridData[i].value
});
}
- 기존에는 API에서 for문을 사용하여 데이터를 변환 후 전송 → 데이터 개수가 많아질수록 성능이 급격히 저하됨
- 1,500건 기준 약 1분 소요
[개선 Javascript 코드 예시]
let transformedData = gridData.map(item => ({
id: item.id,
name: item.name,
value: item.value
}));
- map 사용 후, 1,500건 기준 1~2초 소요.
[왜 map이 더 빠를까?]
1. map()최적화가 잘되어 있음.
- V8 엔진(Chrome, Node.js)에서 map()은 내부적으로 최적화 되어 있어 엔진이 map()을 실행할 때 배열의 크기를 미리 할당하고, 반복 중에 추가적인 연산을 최소화하도록 최적화함.
- for 문은 매번 배열 길이를 확인해야 할 수도 있지만, map()은 최적화된 방식으로 전체 배열을 처리함.
2. map()은 배열의 크기를 미리 할당함
- map()은 새로운 배열을 반환할 때, 기존 배열과 동일한 크기의 메모리를 미리 예약하고 실행됨.
- 반면, for 문에서는 push()를 사용할 때 배열 크기를 동적으로 변경하므로, 메모리 할당과 GC(Garbage Collection) 오버헤드가 추가됨.
3. map()은 순수 함수(Pure Function) 방식으로 실행됨
- map()은 원본 배열을 변경하지 않고 새로운 배열을 반환함.
- for 문은 반복마다 원본 배열을 변경할 수도 있어서, 불필요한 연산이 많아질 가능성이 있음.
4. map()은 병렬 처리(병렬 연산) 최적화 가능
- JavaScript 엔진(V8, SpiderMonkey 등)에서 map()은 내부적으로 최적화된 병렬 연산을 수행할 수 있음.
- 반면, for 문은 명령형 방식이므로 한 번에 하나씩 처리하게 되어 실행 속도가 더 느려질 가능성이 있음.
❌ 하지만, 무조건 map()이 빠른 건 아님!
- map()은 새로운 배열을 생성하므로, 메모리 사용량이 증가할 수 있음.
- 단순 반복(console.log(), 카운트 증가)이라면 for 문이 더 빠를 수도 있음.
[개선 결과]
비교 항목 | 기존 방식 (for문) | 개선 후 (map) |
실행 시간 (1,500 건) | 2분 | 1~2초 |
728x90
320x100
'💻 하나씩 차곡차곡 > Back-end' 카테고리의 다른 글
[Java] IllegalArgumentException VS IllegalStateException : 어떨 때 뭘 써야 하지? (0) | 2025.01.17 |
---|---|
[JPA] 데이터베이스 연결이 안된다, 콘솔창에 DDL문이 안보인다 등등 (0) | 2025.01.06 |
[스프링(Spirng)] Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. 해결방법 (0) | 2024.12.24 |
시스템 설계 기초 (로드밸런스, 메세지 큐, CDN, DNS 등) (0) | 2024.08.12 |
[HTTP] HTTP상태코드 / HTTP헤더 (0) | 2024.08.09 |