공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 04 / 22

JDBC 기초와 트랜잭션 (H2)

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

문제 1

SimpleJdbc 에 batchUpdate(String sql, List<Object[]> argsList, int batchSize) 를 추가하세요. 하나의 트랜잭션 안에서 addBatch() 를 쌓다가 batchSize 마다 executeBatch() 하고, 꼬리를 처리한 뒤 커밋해 총 영향 행 수를 반환합니다. 예외 시 롤백합니다.

정답 보기
java
public int batchUpdate(String sql, List<Object[]> argsList, int batchSize) {
    return inTransaction(c -> {
        int total = 0;
        try (PreparedStatement ps = c.prepareStatement(sql)) {
            int pending = 0;
            for (Object[] args : argsList) {
                bind(ps, args);
                ps.addBatch();
                if (++pending == batchSize) {
                    for (int n : ps.executeBatch()) total += Math.max(n, 0);   // SUCCESS_NO_INFO(-2) 는 0 으로
                    pending = 0;
                }
            }
            if (pending > 0) for (int n : ps.executeBatch()) total += Math.max(n, 0);
        }
        return total;
    });
}

// 사용
List<Object[]> rows = new ArrayList<>();
for (int i = 0; i < 10_000; i++) rows.add(new Object[]{i, "row-" + i});
int affected = jdbc.batchUpdate("INSERT INTO bulk_log(seq, body) VALUES (?, ?)", rows, 1000);
System.out.println(affected);   // 10000

문제 2

OrderService.placeOrder 를 확장해 회원의 balance 로 결제하는 payMethod = "BALANCE" 를 지원하세요.

회원 행을 FOR UPDATE 로 잠그고 잔액이 부족하면 InsufficientBalanceException 을 던져 재고 차감까지 롤백되게 하고, 충분하면 balance 를 차감한 뒤 결제 행을 넣습니다.

성공 후 member.balance 와 product.stock 이 함께 줄었는지, 실패 시 둘 다 그대로인지 확인하는 코드까지 작성하세요.

정답 보기
java
public static class InsufficientBalanceException extends RuntimeException {
    public InsufficientBalanceException(String m) { super(m); }
}

public long placeOrder(long memberId, long productId, int qty, String payMethod) {
    return jdbc.inTransaction(c -> {
        List<Object[]> rows = SimpleJdbc.query(c, "SELECT price, stock FROM product WHERE id = ? FOR UPDATE",
                rs -> new Object[]{rs.getBigDecimal("price"), rs.getInt("stock")}, productId);
        if (rows.isEmpty()) throw new IllegalArgumentException("상품 없음: " + productId);
        BigDecimal price = (BigDecimal) rows.get(0)[0];
        int stock = (int) rows.get(0)[1];
        if (stock < qty) throw new OutOfStockException("재고 부족");
        SimpleJdbc.update(c, "UPDATE product SET stock = stock - ? WHERE id = ?", qty, productId);

        BigDecimal amount = price.multiply(BigDecimal.valueOf(qty));
        if ("BALANCE".equals(payMethod)) {
            BigDecimal balance = SimpleJdbc.query(c, "SELECT balance FROM member WHERE id = ? FOR UPDATE",
                    rs -> rs.getBigDecimal(1), memberId).stream().findFirst()
                    .orElseThrow(() -> new IllegalArgumentException("회원 없음: " + memberId));
            if (balance.compareTo(amount) < 0)
                throw new InsufficientBalanceException("잔액 " + balance.toPlainString() + " < 주문액 " + amount.toPlainString());
            SimpleJdbc.update(c, "UPDATE member SET balance = balance - ? WHERE id = ?", amount, memberId);
        }
        long orderId = SimpleJdbc.insertReturningKey(c,
                "INSERT INTO orders(member_id, product_id, qty, amount, status) VALUES (?,?,?,?,'PAID')", memberId, productId, qty, amount);
        SimpleJdbc.update(c, "INSERT INTO payment(order_id, amount, method) VALUES (?,?,?)", orderId, amount, payMethod);
        return orderId;
    });
}

// 확인
RowMapper<String> state = rs -> "balance=" + rs.getBigDecimal(1).toPlainString() + " stock=" + rs.getInt(2);
String q = "SELECT m.balance, p.stock FROM member m, product p WHERE m.id = 1 AND p.id = 2";
System.out.println(jdbc.query(q, state).get(0));                 // balance=150000.50 stock=100
svc.placeOrder(1, 2, 2, "BALANCE");                              // 마우스 2개 = 50000
System.out.println(jdbc.query(q, state).get(0));                 // balance=100000.50 stock=98
try { svc.placeOrder(1, 2, 5, "BALANCE"); }                      // 125000 > 100000.50
catch (OrderService.InsufficientBalanceException e) { System.out.println(e.getMessage()); }
System.out.println(jdbc.query(q, state).get(0));                 // balance=100000.50 stock=98 (재고도 원복)

문제 3

키셋 페이징으로 member 전체를 200 건씩 순회하며 grade 를 재산정하는 배치를 작성하세요. 페이지마다 하나의 트랜잭션으로 UPDATE 를 배치 실행하고 커밋하며, 마지막으로 처리한 id 를 batch_checkpoint(job_name, last_id) 테이블에 같은 트랜잭션으로 기록해 재시작 시 이어서 처리되게 합니다(배치 04 레슨 2.4 의 "같은 DB 체크포인트").

정답 보기
java
// CREATE TABLE batch_checkpoint(job_name VARCHAR(50) PRIMARY KEY, last_id BIGINT NOT NULL);

static String gradeOf(BigDecimal balance) {
    if (balance.compareTo(new BigDecimal("100000")) >= 0) return "GOLD";
    if (balance.compareTo(new BigDecimal("10000")) >= 0) return "SILVER";
    return "BRONZE";
}

static void regradeAll(SimpleJdbc jdbc, String jobName, int pageSize) {
    long lastId = jdbc.queryOne("SELECT last_id FROM batch_checkpoint WHERE job_name = ?", rs -> rs.getLong(1), jobName).orElse(0L);
    if (lastId > 0) System.out.println("[restart] id " + lastId + " 이후부터");
    while (true) {
        final long from = lastId;
        Long next = jdbc.inTransaction(c -> {
            List<MemberDao.Member> page = SimpleJdbc.query(c,
                    "SELECT * FROM member WHERE id > ? ORDER BY id LIMIT ?", MemberDao.MAPPER, from, pageSize);
            if (page.isEmpty()) return null;
            try (PreparedStatement ps = c.prepareStatement("UPDATE member SET grade = ? WHERE id = ?")) {
                for (MemberDao.Member m : page) { ps.setString(1, gradeOf(m.balance())); ps.setLong(2, m.id()); ps.addBatch(); }
                ps.executeBatch();
            }
            long last = page.get(page.size() - 1).id();
            SimpleJdbc.update(c, "MERGE INTO batch_checkpoint(job_name, last_id) KEY(job_name) VALUES (?, ?)", jobName, last);   // 데이터와 같은 tx
            return last;
        });
        if (next == null) break;
        lastId = next;
        System.out.println("페이지 커밋: last_id=" + lastId);
    }
    jdbc.update("DELETE FROM batch_checkpoint WHERE job_name = ?", jobName);   // 정상 종료: 다음 실행은 처음부터
}

체크포인트 MERGE 가 UPDATE 들과 같은 commit() 에 묶이므로 "커밋됐는데 체크포인트가 없는" 창이 없습니다. grade = calc(balance) 는 절대값 갱신이라 재실행에도 멱등합니다.

연습 문제
  • 문제 1
  • 문제 2
  • 문제 3
이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리