공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

5. 자주 하는 실수 (Tip)

❌ 실수 1: 문자열 결합으로 SQL 을 만든다

java
String sql = "SELECT * FROM member WHERE email = '" + email + "'";   // 인젝션 + 매번 파싱
st.executeQuery(sql);

예제 2 처럼 ' OR '1'='1 하나로 전원이 노출됩니다. 정수라서 안전하다는 생각도 틀립니다 — 1 OR 1=1 은 정수 자리에 그대로 들어갑니다.

✅ 모든 값은 ? 로 바인딩합니다. ? 를 쓸 수 없는 식별자(정렬 컬럼, 테이블명)는 화이트리스트로 검증합니다.

java
try (PreparedStatement ps = c.prepareStatement("SELECT * FROM member WHERE email = ?")) {
    ps.setString(1, email);
    ...
}
// 정렬 컬럼처럼 바인딩이 안 되는 자리
Set<String> allowed = Set.of("id", "name", "joined_at");
if (!allowed.contains(sortCol)) throw new IllegalArgumentException("정렬 불가 컬럼: " + sortCol);
String sql = "SELECT * FROM member ORDER BY " + sortCol;

❌ 실수 2: 커넥션·Statement·ResultSet 을 닫지 않는다

java
Connection c = ds.getConnection();
PreparedStatement ps = c.prepareStatement(sql);
ResultSet rs = ps.executeQuery();
while (rs.next()) { ... }
c.close();               // 예외가 나면 여기 도달 못 함 → 풀에서 커넥션 하나가 영원히 사라짐

수백 번 반복되면 풀이 고갈되어 서비스 전체가 멈춥니다. 커넥션만 닫고 ResultSet 을 안 닫으면 일부 드라이버는 서버 커서를 유지합니다.

✅ 세 자원 모두 try-with-resources 에 넣습니다. 역순 해제가 자동으로 보장됩니다.

java
try (Connection c = ds.getConnection();
     PreparedStatement ps = c.prepareStatement(sql)) {
    ps.setLong(1, id);
    try (ResultSet rs = ps.executeQuery()) {      // 바인딩 후에 열어야 하므로 별도 블록
        while (rs.next()) { ... }
    }
}

❌ 실수 3: autoCommit 상태에서 여러 SQL 을 "트랜잭션인 줄 알고" 실행한다

java
try (Connection c = ds.getConnection()) {                     // autoCommit = true
    update(c, "UPDATE product SET stock = stock - ? WHERE id = ?", qty, pid);   // 즉시 커밋됨
    update(c, "INSERT INTO orders ...");                       // 여기서 실패해도 재고는 이미 차감
}

"같은 커넥션이니까 같은 트랜잭션"이 아닙니다. autoCommit 이 켜져 있으면 SQL 하나하나가 독립 트랜잭션입니다.

✅ setAutoCommit(false) 로 시작하고 commit()/rollback() 을 명시하며, 반납 전 원복합니다. 예제 5 의 inTransaction 처럼 헬퍼에 가두면 빠뜨릴 수 없습니다.

java
jdbc.inTransaction(c -> {
    SimpleJdbc.update(c, "UPDATE product SET stock = stock - ? WHERE id = ?", qty, pid);
    SimpleJdbc.update(c, "INSERT INTO orders ...");
    return null;
});

❌ 실수 4: 예외 시 rollback 없이 다음 청크로 넘어간다

java
for (List<Row> chunk : chunks) {
    try {
        for (Row r : chunk) insert(c, r);
        c.commit();
    } catch (Exception e) {
        log.warn("청크 실패", e);      // rollback 없음
    }
}

배치 04 시뮬레이션은 begin() 이 "이미 트랜잭션 진행 중"으로 막아 줬지만, JDBC 에는 begin() 이 없습니다. 실패 청크의 절반이 다음 청크의 commit() 에 묻어 들어갑니다.

✅ catch 에서 반드시 rollback(). PostgreSQL 은 오류 후 롤백 전까지 모든 SQL 을 거부하므로 더욱 필수입니다.

java
} catch (Exception e) {
    c.rollback();
    log.warn("청크 실패, 롤백", e);
}

❌ 실수 5: NULL 을 0 으로 읽고 넘어간다

java
int discount = rs.getInt("discount_rate");   // NULL 이면 0
price = price * (100 - discount) / 100;      // "할인 없음"과 "할인율 미정"을 구분 못 함

원시형 getter 는 NULL 을 0/false 로 바꿉니다. "미정"을 "0%" 로 계산해 버리면 조용한 데이터 오류입니다.

✅ wasNull() 로 확인하거나, 래퍼 타입으로 받습니다.

java
Integer discount = rs.getObject("discount_rate", Integer.class);   // NULL → null
if (discount == null) throw new IllegalStateException("할인율 미정: " + rs.getLong("id"));

❌ 실수 6: 돈을 double 로, 날짜를 java.sql.Date 로 다룬다

java
double amount = rs.getDouble("amount");     // 0.1 + 0.2 = 0.30000000000000004
java.sql.Date d = rs.getDate("joined_at");  // JVM 기본 타임존으로 변환 → 서버 타임존이 다르면 하루 밀림

✅ BigDecimal 과 getObject(col, LocalDate.class).

java
BigDecimal amount = rs.getBigDecimal("amount");
LocalDate joined = rs.getObject("joined_at", LocalDate.class);
ps.setObject(1, LocalDate.now());            // 바인딩도 대칭

❌ 실수 7: 커넥션을 쥔 채 오래 걸리는 일을 한다

java
try (Connection c = ds.getConnection()) {
    List<Order> orders = query(c, ...);
    for (Order o : orders) paymentApi.charge(o);   // 외부 API 3초 × N 건 동안 커넥션 점유
    update(c, ...);
}

풀 크기 10 에 이런 요청 10개가 동시에 오면 11번째 요청부터 풀 고갈로 실패합니다. DB 는 한가한데 서비스가 멈추는 전형적 장애입니다.

✅ 커넥션은 SQL 실행 직전에 빌리고 직후에 반납합니다. 외부 호출은 트랜잭션 밖에서 하고, 결과를 다시 짧은 트랜잭션으로 기록합니다(배치 04 레슨 실수 5 참고).

자주 하는 실수 (Tip)
  • ❌ 실수 1: 문자열 결합으로 SQL 을 만든다
  • ❌ 실수 2: 커넥션·Statement·ResultSet 을 닫지 않는다
  • ❌ 실수 3: autoCommit 상태에서 여러 SQL 을 "트랜잭션인 줄 알고" 실행한다
  • ❌ 실수 4: 예외 시 rollback 없이 다음 청크로 넘어간다
  • ❌ 실수 5: NULL 을 0 으로 읽고 넘어간다
  • ❌ 실수 6: 돈을 double 로, 날짜를 java.sql.Date 로 다룬다
  • ❌ 실수 7: 커넥션을 쥔 채 오래 걸리는 일을 한다
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제