Java프로그래밍

[JAVA, SPRINGBOOT] 대량 1만건 데이터 insert 처리시 for문을 순회해서 처리하는게 맞을까? 속도 이슈 해결방법

java springboot 에서 select한 데이터가 1만건이야.. 이걸 for문 돌리면서 insert하는데, 너무 느려.. 병렬로 처리하거나.. 개선 방법이 있을까?

개선 방법

1. Batch Insert (배치 처리)

  • Spring JDBC, JPA, MyBatis 모두 배치 기능을 제공합니다.
  • 1건씩 insert 하는 대신, 일정 개수(예: 500건, 1000건)를 모아서 한 번에 insert.
  • 네트워크 round-trip / DB parsing cost가 크게 줄어듭니다.

예시 (Spring JDBC):

@Autowired
private JdbcTemplate jdbcTemplate;

public void batchInsert(List<MyEntity> list) {
    String sql = "INSERT INTO my_table (col1, col2) VALUES (?, ?)";

    jdbcTemplate.batchUpdate(sql, list, 1000, 
        (ps, entity) -> {
            ps.setString(1, entity.getCol1());
            ps.setString(2, entity.getCol2());
        });
}

2. 멀티스레드 / 병렬 처리

  • 1만건을 여러 청크(chunk)로 나누고, ExecutorService / Parallel Stream 등을 활용해 병렬로 insert.
  • 단, DB connection pool 사이즈 고려 필요 (HikariCP maxPoolSize).
  • DB에 부하가 커질 수 있으므로, 적절한 쓰레드 수(예: CPU 코어수 × 2)로 제한.

예시 (Parallel Stream):

int chunkSize = 1000;
List<List<MyEntity>> chunks = Lists.partition(list, chunkSize);

chunks.parallelStream().forEach(chunk -> {
    batchInsert(chunk);
});

3. Native Bulk Insert (DB 특정 기능 활용)

  • DBMS에 따라 대량 데이터 insert를 최적화하는 기능이 있음.
    • PostgreSQL → COPY 명령어
    • MySQL → INSERT ... VALUES (...), (...), ... 구문
    • Oracle → INSERT /*+ APPEND */
  • Java 코드 대신, DB native 기능으로 넣으면 훨씬 빠릅니다.

4. 비동기 큐 기반 처리

  • 1만건을 한 번에 넣는 게 아니라, 큐(Kafka, RabbitMQ 등)에 넣고 Consumer가 배치 단위로 DB insert.
  • 실시간성이 덜 중요하고, 안정적으로 대용량 데이터를 처리할 때 유용.

5. JPA 사용 시 주의

  • save() 반복 호출은 절대 금물 (1건마다 flush, dirty checking 발생 → 느림).
  • saveAll() + flush() 조합을 배치로 돌려야 함.
  • hibernate.jdbc.batch_size 설정으로 insert batching 활성화.

✅ 정리:

  • 최우선은 Batch Insert 적용 (속도 10배 이상 개선 가능).
  • 그 후 성능이 더 필요하다면 병렬 처리 + DB Native Bulk 기능 검토.
  • 1만 건 정도는 Batch Insert만 해도 보통 수 초 이내에 끝납니다.

Local variable isBoxCnt defined in an enclosing scope must be final or effectively final 오류 처리

지금 보신 에러는 람다(lambda)나 익명 클래스 안에서 지역 변수를 변경하려고 할 때 발생하는 전형적인 문제예요. 람다/내부 클래스 안에서 쓰려면, isBoxCntfinal 또는 사실상 final(값을 변경하지 않는 변수) 이어야 한다는 뜻입니다.
하지만 지금 isBoxCnt++ 처럼 값을 변경하려 하니까 컴파일러가 막는 거예요.

AtomicInteger isBoxCnt = new AtomicInteger(0);

list.forEach(item -> {
    if (조건) {
        // ...
    } else {
        isBoxCnt.incrementAndGet();
    }
});

→ 멀티스레드 환경에서도 안전합니다.


✅ 2. int[] 같은 mutable wrapper 사용

간단히 1칸짜리 배열에 담아서 쓰는 방법.

int[] isBoxCnt = {0};

list.forEach(item -> {
    if (조건) {
        // ...
    } else {
        isBoxCnt[0]++;
    }
});
System.out.println(isBoxCnt[0]);

→ 빠른 테스트용으로는 괜찮지만, 권장되는 방식은 아님.


✅ 3. Stream 연산으로 count 처리

단순히 조건에 맞는 갯수 세는 거라면 직접 ++ 할 필요 없이 stream().count() 쓰는 게 더 깔끔합니다.

long isBoxCnt = list.stream()
    .filter(item -> /* 조건 */ true)
    .count();

📌 정리:

  • 값 변경하면서 람다 안에서 쓰고 싶다 → AtomicInteger 추천
  • 단순 개수 세기 → stream().count() 더 깔끔

The operator > is undefined for the argument type(s) AtomicInteger, int 오류 처리

AtomicInteger 를 썼는데, 비교 연산을 바로 > 로 하려니까 이런 오류가 난 거예요.

👉 이유: AtomicInteger는 객체라서 바로 > 같은 산술/비교 연산자를 쓸 수 없습니다.
값을 꺼내서(.get()) 비교해야 해요.


해결 방법

AtomicInteger isBoxCnt = new AtomicInteger(0);

// 값 증가
isBoxCnt.incrementAndGet();

// 비교
if (isBoxCnt.get() > 0) {
    // ...
}

📌 요약

  • AtomicInteger → 내부 값 접근 시 항상 .get()
  • 증가/감소는 .incrementAndGet(), .decrementAndGet(), .addAndGet(n) 등 사용
  • 비교할 땐 isBoxCnt.get() > 0

객체(AtomicInteger)라서 += 연산자를 직접 쓸 수가 없어요.
👉 내부 값을 갱신하려면 반드시 get(), set(), addAndGet() 같은 메서드를 써야 합니다.

수정 방법

1. addAndGet() 사용 (추천)

AtomicInteger isBoxCnt = new AtomicInteger(0);
isBoxCnt.addAndGet(myMapper.accessDataTransferInsert(customerCardUserInfoVo));

2. get() + set() 조합

AtomicInteger isBoxCnt = new AtomicInteger(0);
int result = myMapper.accessDataTransferInsert(customerCardUserInfoVo);
isBoxCnt.set(isBoxCnt.get() + result);

3. 단순히 합계만 필요하다면 int로 충분

만약 동시성 제어 필요 없고, 그냥 합계만 구하는 용도라면 AtomicInteger 말고 int 또는 long 쓰는 게 더 단순합니다.

int isBoxCnt = 0;
isBoxCnt += myMapper.accessDataTransferInsert(customerCardUserInfoVo);

👉 질문: 지금 isBoxCnt멀티스레드 환경에서 공유하려고 AtomicInteger 쓰신 건가요, 아니면 단순히 forEach 안에서 값을 더하려고 쓰신 건가요?
(둘 중 어떤 경우냐에 따라 AtomicInteger가 필요 없을 수도 있습니다.)

나의 말:

chunks.parallelStream().forEach(rBox -> {

ChatGPT의 말:

좋습니다 👍 지금은 chunks.parallelStream().forEach(rBox -> { ... }) 형태로 병렬 실행 중이군요.

여기서 람다 안에서 값 누적을 하고 싶으니까 isBoxCnt++ 같은 걸 쓰다 보니 final/effectively final 문제 → AtomicInteger로 바꾼 거고요.


핵심 포인트

  • parallelStream() 은 여러 스레드에서 동시에 실행됩니다.
  • 따라서 공유 변수(int, long)에 ++ 하면 race condition(경쟁 조건) 문제가 생겨서 값이 꼬일 수 있습니다.
  • 이런 경우는 AtomicInteger 또는 Stream API의 reduce/collect 기능을 쓰는 게 안전합니다.

해결 방법

✅ 1. AtomicInteger (멀티스레드 안전)

AtomicInteger isBoxCnt = new AtomicInteger(0);

chunks.parallelStream().forEach(rBox -> {
    int result = myMapper.accessDataTransferInsert(rBox);
    isBoxCnt.addAndGet(result);
});

System.out.println("총 카운트 = " + isBoxCnt.get());

✅ 2. mapToInt().sum() (더 깔끔)

만약 myMapper.accessDataTransferInsert() 가 int 를 리턴한다면, 아예 합계를 stream 연산으로 처리할 수 있어요.

int isBoxCnt = chunks.parallelStream()
    .mapToInt(rBox -> myMapper.accessDataTransferInsert(rBox))
    .sum();

System.out.println("총 카운트 = " + isBoxCnt);

👉 이 방식이 훨씬 깔끔하고, AtomicInteger 안 써도 됩니다.


✅ 3. reduce() 활용

int isBoxCnt = chunks.parallelStream()
    .reduce(0,
        (sum, rBox) -> sum + myMapper.accessDataTransferInsert(rBox),
        Integer::sum);

System.out.println("총 카운트 = " + isBoxCnt);

📌 정리:

  • 병렬 처리라면 int 직접 누적은 위험 → AtomicInteger or Stream API sum 사용.
  • 단순 합계라면 mapToInt().sum() 이 제일 깔끔합니다.

여기서 rBox실제로는 List 타입인데, 무조건 BaseBox라고 캐스팅해버린 게 문제입니다.
chunks.parallelStream().forEach(rBox -> { ... }) 에서 chunks가 아마 List<List<BaseBox>> 같은 형태일 가능성이 높아요.
rBoxBox가 아니라 리스트의 부분(sublist) 라는 거죠.


해결 방법

✅ 1. 제네릭 타입 확인하기

먼저 chunks 정의 부분 확인하세요. 아마 이런 식일 겁니다:

List<List<BaseBox>> chunks = Lists.partition(list, chunkSize);

그렇다면 parallelStream()에서 꺼내오는 건 List<BaseBox>이지 BaseBox가 아닙니다.
따라서 forEach 내부에서는 다시 반복문을 돌려야 합니다.

chunks.parallelStream().forEach(subList -> {
    subList.forEach(rBox -> {
        customerCardUserInfoVo.setCustomerCardEmpNo(rBox.getString("customerCardEmpNo"));
        // insert 등 처리
    });
});

✅ 2. 만약 list가 애초에 List<BaseBox> 였다면

chunks를 만들지 않고 바로 stream 돌리면 캐스팅 문제가 사라집니다.

list.parallelStream().forEach(rBox -> {
    customerCardUserInfoVo.setCustomerCardEmpNo(rBox.getString("customerCardEmpNo"));
});

✅ 3. 디버깅 팁

chunks.get(0).getClass().getName() 찍어보면 지금 rBox가 뭔 타입인지 바로 확인됩니다.
아마 java.util.ArrayList$SubList 라고 나올 거예요.


📌 정리:

  • 지금 (BaseBox) rBox 캐스팅 오류는, rBoxBaseBox가 아니라 List였기 때문.
  • 해결책은 제네릭 타입을 확인하고 forEach 구조를 바꿔서 리스트 안의 BaseBox를 꺼내 처리해야 합니다.

ChatGPT의 말:

좋습니다 👍 코드 전체 맥락이 이제 명확해졌네요.

List<Box> fromList = myMapper.accessDataTransferList(srchBox);

int chunkSize = 1000;
List<List<Box>> chunks = Lists.partition(fromList, chunkSize);

AtomicInteger isBoxCnt = new AtomicInteger(0); // 이미 데이터 있는 수
AtomicInteger affected = new AtomicInteger(0);

chunks.parallelStream().forEach(rBox -> {
    // 여기서 rBox 는 Box 가 아니라 List<Box> !!
});

문제 원인

  • chunksList<List<Box>>
  • 따라서 parallelStream().forEach(rBox -> { ... }) 안의 rBox 타입은 List<Box>
  • 그런데 코드를 ((BaseBox) rBox).getString(...)처럼 쓰면서 캐스팅 오류 발생


해결 방법

✅ 1. subList 안에서 다시 loop 돌리기

chunks.parallelStream().forEach(subList -> {
    subList.forEach(box -> {
        // box는 BaseBox의 자식인 Box
        customerCardUserInfoVo.setCustomerCardEmpNo(box.getString("customerCardEmpNo"));
        
        // insert 처리
        int result = myMapper.accessDataTransferInsert(customerCardUserInfoVo);
        affected.addAndGet(result);
    });
});

✅ 2. partition 안 쓰고 바로 parallelStream

만약 굳이 1000건 단위로 나누는 이유가 없다면 그냥 한 줄로 해결할 수 있어요.

fromList.parallelStream().forEach(box -> {
    customerCardUserInfoVo.setCustomerCardEmpNo(box.getString("customerCardEmpNo"));
    int result = myMapper.accessDataTransferInsert(customerCardUserInfoVo);
    affected.addAndGet(result);
});

✅ 3. 성능 최적화 (Batch Insert)

병렬로 insert 하는 것보다, List<Box> 단위로 모아서 batchInsert를 호출하는 게 훨씬 빠릅니다.

예시 (MyBatis):

chunks.parallelStream().forEach(subList -> {
    int result = myMapper.accessDataTransferBatchInsert(subList);
    affected.addAndGet(result);
});

📌 정리:

  • rBoxBox가 아니라 List<Box>였음 → 내부에서 다시 loop 돌려야 함
  • 단순 반복이면 parallelStream() + forEach 중첩
  • 성능 최적화 원하면 batchInsert 고려

for문안에 for문을 실행하는데.. 병렬처리가 되는건가? List<Box> delList = mobileCardMapper.accessCardUserListForAllDel(params); int chunkSize = 1000; List<List<Box>> chunks = Lists.partition(delList, chunkSize); chunks.parallelStream().forEach(subList -> { subList.forEach(delUserBox -> {

ChatGPT의 말:

좋은 질문이에요 👍

올려주신 코드의 흐름을 보면:

List<Box> delList = mobileCardMapper.accessCardUserListForAllDel(params);
int chunkSize = 1000;
List<List<Box>> chunks = Lists.partition(delList, chunkSize);

// 바깥쪽 parallelStream
chunks.parallelStream().forEach(subList -> {
    // 안쪽은 일반 forEach (순차 실행)
    subList.forEach(delUserBox -> {
        ...
    });
});

여기서 중요한 포인트는:

  1. chunks.parallelStream()
    chunks 리스트(즉, 잘라낸 subList 단위)를 병렬로 처리합니다.
    즉, subList 단위에서 여러 쓰레드가 동시에 작업하게 됩니다.
  2. subList.forEach(...)
    → 이 부분은 순차 실행입니다. 즉, 같은 subList 안에서는 병렬 처리가 되지 않아요.
    (단일 쓰레드가 subList 내 요소를 처음부터 끝까지 순차적으로 도는 것)

✅ 정리하면

  • 병렬처리 단위: chunks (즉, 1000건씩 끊은 묶음)
  • 순차처리 단위: subList 내부 (Box 단위)

따라서 실제로는 “1000건 단위로 병렬 실행 → 각 묶음 안에서는 순차 실행” 구조입니다.


👉 만약 안쪽 forEach도 병렬로 돌리고 싶다면 이렇게 해야 합니다:

chunks.parallelStream().forEach(subList -> {
    subList.parallelStream().forEach(delUserBox -> {
        ...
    });
});

하지만 이렇게 이중 parallelStream을 쓰는 건 비효율적일 수 있고, 일반적으로 권장되지 않습니다.
대신 아예 delList.parallelStream()으로 전체를 병렬 돌려버리는 게 더 단순하고 빠른 경우가 많습니다:

delList.parallelStream().forEach(delUserBox -> {
    ...
});

👉 질문:
지금 subList 단위로 끊으신 이유가 DB batch insert/delete 때문인가요?
그렇다면 병렬은 chunks 단위까지만 쓰고, 안쪽은 순차로 두는 게 맞습니다.
만약 단순 반복 작업이라면 delList.parallelStream() 한 줄이 더 깔끔할 수도 있어요.

혹시 현재 케이스가 DB 작업(batch insert/delete) 맞나요?

나의 말:

데이터 1만건 조회 후 저장하는 로직이야 batch insert는 아니야.. 그냥 insert야

ChatGPT의 말:

오케이 👍 상황이 더 명확해졌네요.
정리하면 1만 건을 DB에서 조회 → for문 돌면서 한 건씩 insert 하는 로직이라는 거죠.


지금 코드 구조

List<Box> delList = mobileCardMapper.accessCardUserListForAllDel(params);
int chunkSize = 1000;
List<List<Box>> chunks = Lists.partition(delList, chunkSize);

chunks.parallelStream().forEach(subList -> {
    subList.forEach(delUserBox -> {
        // insert(delUserBox)
    });
});
  • 병렬 단위: chunks (즉, 1000개씩 묶음)
  • 순차 단위: subList 내부 (1000개씩 순차 insert)

즉, 1만 건이면 최대 10개의 subList가 병렬로 실행됩니다.
(스레드풀의 크기 = CPU 코어 수, 보통 8~16 정도라면 10개 subList는 동시에 돌 수 있음)


문제 포인트

  1. 한 건씩 insert → 병렬이라도 DB 부하 + 네트워크 round-trip 비용이 큼.
  2. 사실 chunk 나눠서 병렬 돌리는 것보다, 그냥 delList.parallelStream()으로 바로 돌려도 동일 효과: delList.parallelStream().forEach(delUserBox -> { insert(delUserBox); }); → 이러면 따로 partition 안 해도 자동으로 데이터가 코어 수만큼 분배돼서 병렬 실행됨.

권장 방법

  • 성능 최적화: 가능하다면 batch insert로 바꾸는 게 훨씬 빠름.
    (JDBC batch, MyBatis batch, JPA saveAll 등)
    → 네트워크 + DB I/O 줄여줌.
  • 단순 insert만 가능하다면:
    parallelStream()으로 한 번만 돌리면 충분함.

예시:

delList.parallelStream().forEach(delUserBox -> {
    mobileCardMapper.insertUser(delUserBox);
});

🚨 주의할 점

  • 트랜잭션: 병렬 스트림은 각 쓰레드가 독립적으로 실행되므로,
    한 트랜잭션 안에서 모든 insert를 보장하려면 병렬이 맞지 않음.
  • DB connection pool: 병렬 실행 시 동시에 많은 커넥션을 요청하게 됨.
    → pool 사이즈 확인 필요 (기본 HikariCP는 10개).
    → pool이 작으면 병렬 효과가 거의 없음.

👉 제가 보기엔 지금은 chunk 쪼개는 이유가 없음.
그냥 parallelStream으로 전체를 돌리면 더 깔끔합니다.


혹시 궁금한 게 두 가지 있는데요:

  1. DB connection pool 사이즈 (예: HikariCP 10개 그대로 쓰시나요?)
  2. insert 시 성능 병목이 CPU 연산 쪽인지, DB 네트워크 쪽인지 확인된 게 있나요?

이거에 따라 최적화 방향이 달라질 수 있어요.

나의 말:

너의 말대로 아래처럼 변경해도 병렬 스트림은 각 쓰레드가 독립적으로 실행되므로,한 트랜잭션 안에서 모든 insert를 보장하려면 병렬이 맞지 않아? delList.parallelStream().forEach(delUserBox -> { insert() }

ChatGPT의 말:

네 맞습니다 👌 정확히 짚으셨어요.

delList.parallelStream().forEach(delUserBox -> {
    insert(delUserBox);
});

이렇게 하면 병렬 스트림은 내부적으로 fork-join pool의 여러 스레드에서 독립적으로 실행되기 때문에:

  • 각 스레드마다 같은 트랜잭션을 공유하지 않습니다.
    → 즉, @Transactional을 붙여도 “전체 1만 건이 하나의 트랜잭션으로 커밋/롤백” 되지 않습니다.
  • 보통 Spring + MyBatis/JPA 환경에서는, 병렬 실행하면 트랜잭션 경계가 깨져서
    → 결과적으로 커밋/롤백 단위가 보장되지 않음.

🚨 트랜잭션과 병렬의 관계

  1. 단일 트랜잭션 보장이 필요하다면 → 순차 실행을 해야 함. @Transactional public void insertAll(List<Box> delList) { delList.forEach(this::insert); } → 여기서는 전체 1만 건이 하나의 트랜잭션 안에서 처리됨.
  2. 병렬 실행 + 성능 극대화가 목적이라면 → 트랜잭션을 나눠야 함.
    • 보통 insert() 호출 시 마다 개별 트랜잭션이 생기거나,
    • chunk 단위로 트랜잭션을 묶어 처리하는 방법을 씀.

예시 (chunk 단위 트랜잭션):

Lists.partition(delList, 1000).parallelStream().forEach(subList -> {
    // 각각의 subList는 별도 트랜잭션에서 실행됨
    myService.insertChunk(subList);
});

@Transactional
public void insertChunk(List<Box> subList) {
    subList.forEach(this::insert);
}

이렇게 하면 병렬로 실행되지만, subList 단위 트랜잭션이 보장돼요.


✅ 정리

  • parallelStream()한 트랜잭션 안에서 전체 insert 보장 불가능.
  • 트랜잭션 무결성(원자성)이 중요하다면 → 순차 처리해야 함.
  • 성능(속도)이 더 중요하다면 → 병렬 처리하면서 트랜잭션 경계는 chunk 단위로 가져가는 게 좋음.

👉 혹시 이 로직에서 트랜잭션 원자성이 중요한 상황일까요?
예를 들어, “1만 건 중 하나라도 insert 실패하면 전부 롤백”이 필요하신가요,
아니면 “가능한 건 다 넣고 실패 건만 무시/로그” 하는 쪽이신가요?

“1만 건 중 하나라도 insert 실패하면 전부 롤백”이 필요해

ChatGPT의 말:

좋습니다 👍 그러면 답은 명확해요.

“1만 건 중 하나라도 insert 실패하면 전부 롤백”단일 트랜잭션으로 묶어야 합니다.
그런데 parallelStream()은 여러 스레드에서 돌기 때문에, 스프링 @Transactional 경계가 깨져버립니다.


왜 병렬 처리에서 단일 트랜잭션이 안 되는가?

  • Spring의 @Transactional은 보통 ThreadLocal 기반으로 트랜잭션을 관리합니다.
  • parallelStream()은 여러 스레드를 쓰기 때문에, 각 스레드는 독립적으로 트랜잭션 컨텍스트를 갖습니다.
  • 따라서 “하나라도 실패하면 전체 롤백”을 보장할 수 없습니다.

해결 방법

1. 순차 실행 (권장)

@Transactional
public void insertAll(List<Box> delList) {
    delList.forEach(delUserBox -> {
        insert(delUserBox); // 여기서 예외 터지면 전체 롤백
    });
}

👉 1만 건이 한 트랜잭션으로 묶임.
👉 하나라도 실패하면 전체 rollback.
👉 하지만 성능은 느림 (DB round-trip 1만 번).


2. Batch Insert로 성능 보완 (더 권장)

MyBatis / JDBC 배치 모드를 쓰면 PreparedStatement.addBatch()executeBatch()로 한꺼번에 insert가 가능합니다.

예시 (MyBatis ExecutorType.BATCH):

@Autowired
private SqlSessionFactory sqlSessionFactory;

@Transactional
public void insertAll(List<Box> delList) {
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        MyMapper mapper = session.getMapper(MyMapper.class);
        delList.forEach(mapper::insertUser);
        session.flushStatements(); // 한 번에 실행
    }
}

👉 여전히 단일 트랜잭션 보장.
👉 네트워크/DB round-trip 최소화 → 성능 개선.


3. 병렬 + 단일 트랜잭션은 불가능

병렬 스트림 안에서 단일 트랜잭션을 유지할 방법은 없습니다.
(트랜잭션이 스레드 기반이라, 스레드를 분리하는 순간 끊어져버림)


✅ 따라서 요약:

  • 원자성(전부 성공 or 전부 실패)이 최우선이라면 → 순차 실행 + 트랜잭션
  • 성능까지 고려해야 한다면 → Batch Insert + 트랜잭션

👉 혹시 지금 쓰시는 DB가 Oracle / PostgreSQL / MySQL 중 어떤 건가요?
DB마다 batch insert 최적화 방법이 좀 달라서, 제가 구체적인 예시를 맞춰드릴 수 있어요.

나의 말:

PostgreSQL

ChatGPT의 말:

좋습니다 🙆 PostgreSQL이라면 더 유리해요.
Postgres는 multi-row insert 구문 (INSERT INTO ... VALUES (...), (...), ...)을 지원하고,
또한 JDBC / MyBatis의 batch insert도 아주 잘 먹힙니다.


🚀 선택 가능한 전략

1. 순차 실행 + 단일 트랜잭션

@Transactional
public void insertAll(List<Box> delList) {
    delList.forEach(delUserBox -> {
        mobileCardMapper.insertUser(delUserBox);
    });
}
  • 전체가 하나의 트랜잭션
  • 1건이라도 실패하면 전부 롤백
  • 단점: 1만 번 네트워크 round-trip → 느림

2. Batch Insert (권장)

MyBatis + PostgreSQL 조합에서는 ExecutorType.BATCH를 쓰는 게 가장 깔끔합니다.

@Autowired
private SqlSessionFactory sqlSessionFactory;

@Transactional
public void insertAll(List<Box> delList) {
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        MobileCardMapper mapper = session.getMapper(MobileCardMapper.class);

        for (Box box : delList) {
            mapper.insertUser(box);
        }

        // 모든 쿼리를 모아서 한 번에 실행
        session.flushStatements();
    }
}
  • 네트워크 round-trip이 획기적으로 줄어듦 (수천 건 단위로 묶여서 전달)
  • 여전히 하나의 트랜잭션
  • 하나라도 실패하면 전체 롤백

3. 다중 VALUES insert (SQL 레벨에서 최적화)

Postgres는 이런 식으로 여러 row를 한 SQL로 삽입 가능:

INSERT INTO user_table (id, name, email)
VALUES
  (1, '홍길동', 'hong@test.com'),
  (2, '김철수', 'kim@test.com'),
  (3, '이영희', 'lee@test.com');

MyBatis에서 foreach를 써서 이런 SQL을 만들 수도 있습니다:

<insert id="bulkInsertUsers">
  INSERT INTO user_table (id, name, email)
  VALUES
  <foreach collection="list" item="item" separator=",">
    (#{item.id}, #{item.name}, #{item.email})
  </foreach>
</insert>
  • SQL 한 번만 날리니 성능 최강
  • 단일 트랜잭션에서 전체 롤백 보장
  • 단점: Postgres의 max query size / bind parameter 개수 제한 고려해야 함
    (보통 65535개 파라미터 제한 있음 → 안전하게 1000~2000 row 단위로 잘라 insert)

✅ 결론

  • 지금 요구사항이 “1만 건 중 하나라도 실패하면 전부 롤백”병렬은 못 씀
  • PostgreSQL이라면 Batch Insert 또는 Multi-row Insert가 최적화된 답


혹시 지금 MyBatis 쓰고 계시니까, 제가 실제로 쓸 수 있는 MyBatis <insert> XML 예제 (multi-row insert 버전) 짜드릴까요?

좋습니다 🙆 PostgreSQL + MyBatis 환경에서 1만 건 insert를 처리할 수 있는
multi-row insert XML 예제를 드릴게요.


1. Mapper XML 예제

<mapper namespace="com.example.mapper.UserMapper">

  <!-- 다중 행 insert -->
  <insert id="bulkInsertUsers" parameterType="java.util.List">
    INSERT INTO user_table (id, name, email)
    VALUES
    <foreach collection="list" item="item" separator=",">
      (#{item.id}, #{item.name}, #{item.email})
    </foreach>
  </insert>

</mapper>
  • listList<User> 전달 가능
  • <foreach>가 반복문처럼 돌면서 (?, ?, ?), (?, ?, ?), ... 형태로 SQL 생성

2. Mapper 인터페이스

@Mapper
public interface UserMapper {
    void bulkInsertUsers(@Param("list") List<User> users);
}

3. Service 레벨에서 사용 예시

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserMapper userMapper;

    @Transactional
    public void saveAllUsers(List<User> users) {
        // PostgreSQL 파라미터 제한 고려 → 안전하게 chunk 단위로 나눔
        int chunkSize = 1000; // row 1000개씩 insert
        for (int i = 0; i < users.size(); i += chunkSize) {
            List<User> subList = users.subList(i, Math.min(i + chunkSize, users.size()));
            userMapper.bulkInsertUsers(subList);
        }
    }
}

4. PostgreSQL 주의사항

  • Postgres는 바인딩 파라미터 65535개 제한 있음.
    → 컬럼이 10개면, 한 번에 최대 6553 row만 insert 가능.
    → 보통 1000~2000개 단위로 끊어서 실행하는 게 안전합니다.
  • @Transactional 붙이면 전체 1만 건 insert가 단일 트랜잭션으로 처리됨.
    → 하나라도 실패하면 전체 롤백 보장.

✅ 이렇게 하면:

  • 병렬 스트림 없이도 빠르게 처리 가능
  • 단일 트랜잭션 보장 (원자성 확보)
  • PostgreSQL의 장점(멀티 VALUES insert)을 살림

error: Content is protected !!