F-LAB JAVA · 6주차 · Phase 7 · JdbcTemplate (반복 제거)
🛠️ Phase 7 시작 — 5주차 패턴의 실현
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
JDBC 만 쓰면 매 쿼리마다 Connection·PreparedStatement·ResultSet 의 try/catch/finally 자원 관리 코드가 본 로직보다 더 길어지고 close 누락 시 Connection 누수 위험까지 있는데, 이는 5주차의 "변하지 않는 흐름 + 변하는 부분" 패턴 (템플릿 메소드 / 전략 패턴) 으로 해결할 수 있는 문제로 JdbcTemplate (다음 Unit) 이 그 답이다.
JDBC 만 쓰면 매 쿼리마다 똑같은 try/catch/finally 패턴이 반복된다 — Connection 획득, PreparedStatement 생성, 파라미터 바인딩, ResultSet 처리, 그리고 역순으로 자원 해제.
문제는 — 본 로직 (SQL 실행/결과 처리) 보다 자원 관리 코드 (try/catch/finally, null 체크, 중첩 try) 가 더 길고 복잡 하다는 것이다.
실수로close()를 누락하면 Connection 누수 가 발생해 풀의 모든 Connection 이 소진되어 서비스가 멈출 수도 있다.
5주차에서 배운 "변하지 않는 흐름 + 변하는 부분" 패턴 — 흐름 (연결/해제) 은 변하지 않고 SQL·파라미터·매핑만 변하므로 템플릿 메소드 + 전략 패턴 으로 분리할 수 있으며 (try-with-resources 로 일부만 해결), 이것이 다음 Unit 의 JdbcTemplate 의 정확한 동기다.
JDBC 반복 = 실험실 매번 같은 절차:
매 실험 (매 쿼리):
1. 가운 입기 (Connection 획득)
2. 비커 가져오기 (PreparedStatement)
3. 시약 준비 (파라미터)
4. 실험 (SQL 실행) ← 핵심 단 한 줄
5. 결과 측정 (ResultSet)
6. 비커 씻기 (ResultSet close)
7. 시약 정리 (Statement close)
8. 가운 벗기 (Connection close)
→ 6, 7, 8 도 실패 시 처리 (try/catch)
본 작업 < 준비/정리:
- 실험은 30초
- 준비+정리는 5분
- 매번 똑같음
누수 위험:
- 가운 안 벗고 가면? (close 누락)
- 다음 사람 가운 없음
- 실험실 마비
5주차 패턴:
- 흐름 (입기/벗기) = 변하지 않음
- 실험 (SQL) = 변함
- → 분리하면? (다음 Unit JdbcTemplate)
자동화 가능:
- 로봇 (템플릿) 이 입기/벗기 담당
- 사람은 실험 (SQL) 만
- 효율 + 안전
→ JDBC 반복 = 자원 관리 보일러플레이트, 본 로직 < 인프라 코드, 다음 — JdbcTemplate.
1. JDBC 코드의 반복
2. try/catch/finally 부담
3. 본 로직 < 자원 관리
4. close 누락 누수
5. 변하는 / 변하지 않는 부분
6. 5주차 패턴 적용
7. try-with-resources 의 한계
8. 예외 처리의 부담
9. 다음 — JdbcTemplate
// 전형적 JDBC 조회
public Shipment get(Long id) throws Exception {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(
"select * from shipments where id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
s.setBlNo(rs.getString("bl_no"));
return s;
}
return null;
} catch (SQLException e) {
throw e;
} finally {
if (rs != null) try { rs.close(); } catch (SQLException e) { }
if (stmt != null) try { stmt.close(); } catch (SQLException e) { }
if (conn != null) try { conn.close(); } catch (SQLException e) { }
}
}
반복 부분:
- Connection/Statement/ResultSet 변수 선언
- try/catch/finally 구조
- finally 의 자원 해제 (역순)
- null 체크
- 자원 해제 시 SQLException 처리 (중첩)
→ 모든 메서드에 같은 패턴
매 메서드마다:
add() 메서드: 같은 패턴
update() 메서드: 같은 패턴
get() 메서드: 같은 패턴
delete() 메서드: 같은 패턴
findAll() 메서드: 같은 패턴
...
// 반복 (ILIC, 102 테이블 × 여러 메서드)
public class ShipmentDao {
private final DataSource dataSource;
public Shipment get(Long id) throws Exception {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try { /* SQL: select */ } catch (SQLException e) { throw e; }
finally { /* close 들 */ }
return null;
}
public void add(Shipment s) throws Exception {
Connection conn = null;
PreparedStatement stmt = null;
try { /* SQL: insert */ } catch (SQLException e) { throw e; }
finally { /* close 들 */ }
}
public void update(Shipment s) throws Exception {
// 또 같은 패턴
}
public void delete(Long id) throws Exception {
// 또 같은 패턴
}
// 431 API 마다 이걸?
}
class Shipment {
void setId(Long id) {}
void setBlNo(String s) {}
}
JDBC 코드의 반복은?
답:
1. 반복:
부분:
매 메서드:
누적:
try/catch/finally 구조:
try {
// 본 로직
} catch (SQLException e) {
// 예외 처리
} finally {
// 자원 해제 (역순)
if (rs != null) try { rs.close(); } catch (...) { }
if (stmt != null) try { stmt.close(); } catch (...) { }
if (conn != null) try { conn.close(); } catch (...) { }
}
중첩 try/catch:
finally 안의 close():
- SQLException 던질 수
- 처리 필요 (또 try/catch)
- 중첩
→ 가독성 ↓
자원 해제 순서:
생성 역순:
1. ResultSet (마지막 생성, 먼저 close)
2. Statement
3. Connection (첫 생성, 마지막 close)
→ 역순 (LIFO)
null 체크:
자원이 만들어졌는지 확인:
- 만들기 전 예외 시 null
- close() 시 NullPointerException 방지
→ 추가 코드
// try/catch/finally 부담 (ILIC)
public Shipment get(Long id) throws Exception {
Connection conn = null; // 선언
PreparedStatement stmt = null;
ResultSet rs = null;
try {
// 본 로직 (간단)
conn = dataSource.getConnection();
stmt = conn.prepareStatement("select * from shipments where id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
// 매핑
return new Shipment();
}
return null;
} catch (SQLException e) {
// 예외 처리
throw e;
} finally {
// 자원 해제 (역순, 중첩 try)
if (rs != null) {
try { rs.close(); }
catch (SQLException e) { /* 무시? 로깅? */ }
}
if (stmt != null) {
try { stmt.close(); }
catch (SQLException e) { /* ... */ }
}
if (conn != null) {
try { conn.close(); }
catch (SQLException e) { /* ... */ }
}
}
}
// 본 로직 5줄, 자원 관리 20줄
class Shipment {}
DataSource dataSource;
interface DataSource { Connection getConnection() throws SQLException; }
try/catch/finally 의 부담은?
답:
1. 구조:
중첩:
순서:
null 체크:
코드 비율:
본 로직:
- SQL 실행: 1 줄
- 결과 처리: 2-3 줄
- 매핑: 5-10 줄
자원 관리:
- 변수 선언: 3 줄
- try/catch: 2 줄
- finally: 9-15 줄 (자원 해제)
- 중첩 try/catch
→ 자원 관리가 더 김
본질 vs 인프라:
본질 (비즈니스):
- SQL
- 파라미터
- 매핑
인프라 (기술):
- 자원 관리
- 예외 처리
- 트랜잭션
→ 인프라가 본질 가림
가독성:
본 로직 묻힘:
- 자원 관리 코드 사이
- 핵심 파악 어려움
- 유지보수 ↓
// 본 로직 vs 자원 관리 비율 (ILIC)
public Shipment get(Long id) throws Exception {
Connection conn = null; // 인프라
PreparedStatement stmt = null; // 인프라
ResultSet rs = null; // 인프라
try { // 인프라
conn = dataSource.getConnection(); // 인프라
stmt = conn.prepareStatement( // 본질 (SQL)
"select * from shipments where id = ?");
stmt.setLong(1, id); // 본질 (파라미터)
rs = stmt.executeQuery(); // 본질 (실행)
if (rs.next()) { // 본질
Shipment s = new Shipment(); // 본질 (매핑)
s.setId(rs.getLong("id")); // 본질
s.setBlNo(rs.getString("bl_no")); // 본질
return s; // 본질
}
return null; // 본질
} catch (SQLException e) { // 인프라
throw e; // 인프라
} finally { // 인프라
if (rs != null) try { rs.close(); } catch (SQLException e) { } // 인프라
if (stmt != null) try { stmt.close(); } catch (SQLException e) { } // 인프라
if (conn != null) try { conn.close(); } catch (SQLException e) { } // 인프라
}
}
// 본질: ~8 줄 / 인프라: ~12 줄
// → 인프라가 더 많음
class Shipment { void setId(Long id) {} void setBlNo(String s) {} }
DataSource dataSource;
interface DataSource { Connection getConnection() throws SQLException; }
본 로직 < 자원 관리 코드의 의미는?
답:
1. 비율:
본질 vs 인프라:
가독성:
유지보수:
close 누락:
실수로 finally 누락:
- try 블록만
- 예외 시 자원 해제 X
또는:
- finally 있지만 null 체크 빠뜨려 NPE
- close 가 예외 던져 다음 close X
Connection 누수:
close 안 됨:
- 풀에 안 돌아옴
- 사용 중 표시 유지
- 새 요청에 못 줌
→ 풀 소진
누적 효과:
요청 1000회 처리:
- 매번 누수 1개
- 풀 크기 10
- 10번 만에 풀 소진
→ 11번째 요청 대기/실패
→ 서비스 멈춤
발견의 어려움:
- 정상 처리는 보임
- 누수는 누적되어야 보임
- 시간 지나 서비스 다운
- 추적 어려움
→ 사고 가능
// 누수 위험 (ILIC)
// ❌ finally 빠뜨림
public Shipment getBad(Long id) throws Exception {
Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("...");
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
// ↑ 만약 위에서 예외 발생하면:
// → conn.close() 호출 안 됨 → 누수
if (rs.next()) return new Shipment();
rs.close(); // 정상 시만 호출
stmt.close();
conn.close();
return null;
}
// 누적:
// - 매 요청 누수 1
// - 풀 크기 10
// - 10번 만에 풀 소진
// - 새 요청 connection-timeout
// → 서비스 장애
class Shipment {}
DataSource dataSource;
interface DataSource { Connection getConnection() throws SQLException; }
close 누락 → Connection 누수 위험은?
답:
1. 누락:
누수:
누적:
발견:
변하지 않는 부분 (모든 메서드 공통):
- Connection 획득
- Statement 생성
- 자원 해제 (역순)
- 예외 처리
→ 흐름이 동일
변하는 부분 (메서드마다 다름):
- SQL 문자열
- 파라미터 바인딩
- 결과 매핑 (ResultSet → 객체)
→ 비즈니스 다름
분리 가능:
변하지 않는 = 공통 (재사용)
- 한 곳에 두면
- 모두가 같이 씀
변하는 = 메서드별
- 인자로 받으면
- 다양화
→ 5주차 패턴!
패턴의 신호:
"변하지 않는 + 변하는":
- 템플릿 메소드 패턴
- 또는 전략 패턴
- 5주차 단골
→ 분리 + 추상화
// 변하는 / 변하지 않는 (ILIC)
// 변하지 않는 (흐름)
// - Connection 획득
// - PreparedStatement 생성/실행
// - ResultSet 처리
// - 자원 해제
// 변하는 (메서드별)
public Shipment get(Long id) throws Exception {
Connection conn = null; /* ... */ try { /* ... */
// 변하는 SQL
stmt = conn.prepareStatement("select * from shipments where id = ?");
stmt.setLong(1, id); // 변하는 파라미터
rs = stmt.executeQuery();
if (rs.next()) {
// 변하는 매핑
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
}
/* ... */ } finally { /* ... */ }
return null;
}
public Booking getBooking(Long id) throws Exception {
Connection conn = null; /* ... */ try { /* ... */
// 변하는 SQL (다름)
stmt = conn.prepareStatement("select * from bookings where id = ?");
stmt.setLong(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
// 변하는 매핑 (다름)
Booking b = new Booking();
b.setId(rs.getLong("id"));
return b;
}
/* ... */ } finally { /* ... */ }
return null;
}
// → 흐름 같음, SQL/매핑만 다름
// → 5주차 패턴으로 해결!
class Shipment { void setId(Long id) {} }
class Booking { void setId(Long id) {} }
PreparedStatement stmt;
ResultSet rs;
DataSource dataSource;
interface DataSource {}
변하는 부분 vs 변하지 않는 부분은?
답:
1. 변하지 않는:
변하는:
분리 가능:
신호:
템플릿 메소드 패턴:
부모 클래스:
- 변하지 않는 흐름 (템플릿)
- 변하는 부분은 추상 메서드
자식 클래스:
- 변하는 부분 구현 (오버라이드)
→ 5주차에서 본 패턴
전략 패턴:
컨텍스트:
- 변하지 않는 흐름
전략 (인터페이스):
- 변하는 부분
- 외부 주입 (DI)
→ 더 유연
JDBC 에 적용:
변하지 않는 흐름:
- Connection 획득/해제
- PreparedStatement 생성/해제
- 예외 처리
→ 템플릿 안
변하는 부분:
- SQL (전략)
- 파라미터 설정 (전략)
- 결과 매핑 (전략, RowMapper)
→ 외부 주입
→ JdbcTemplate 이 그것
OCP 달성:
새 쿼리 추가:
- 새 전략 (Lambda/메서드)
- 템플릿 그대로 (변경 X)
→ 확장 ✅ 변경 닫힘 ✅
// 5주차 패턴 적용 (개념)
// 변하지 않는 흐름 (템플릿)
public abstract class JdbcOperationTemplate<T> {
public T execute(String sql) throws Exception {
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(sql);
// 변하는 부분 (자식이 구현)
setParameters(stmt);
rs = stmt.executeQuery();
return mapResult(rs); // 변하는 부분
} finally {
// 자원 해제 (한 곳)
if (rs != null) try { rs.close(); } catch (SQLException e) { }
if (stmt != null) try { stmt.close(); } catch (SQLException e) { }
if (conn != null) try { conn.close(); } catch (SQLException e) { }
}
}
protected abstract void setParameters(PreparedStatement stmt) throws SQLException;
protected abstract T mapResult(ResultSet rs) throws SQLException;
DataSource dataSource;
}
// 자식 (메서드별)
class ShipmentGetTemplate extends JdbcOperationTemplate<Shipment> {
private Long id;
public ShipmentGetTemplate(Long id) { this.id = id; }
@Override
protected void setParameters(PreparedStatement stmt) throws SQLException {
stmt.setLong(1, id);
}
@Override
protected Shipment mapResult(ResultSet rs) throws SQLException {
if (rs.next()) {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
}
return null;
}
}
// → 5주차 템플릿 메소드 적용
// → Spring 의 JdbcTemplate 은 더 정교 (다음 Unit)
class Shipment { void setId(Long id) {} }
interface DataSource { Connection getConnection() throws SQLException; }
5주차의 어떤 패턴 적용?
답:
1. 템플릿 메소드:
전략 패턴:
적용:
OCP:
// try-with-resources (Java 7+)
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("...");
ResultSet rs = stmt.executeQuery()) {
// 본 로직
if (rs.next()) {
return new Shipment();
}
} // 자동 close (역순)
class Shipment {}
개선 효과:
- finally 불필요 (자동 close)
- null 체크 X
- 역순 자동
- 가독성 ↑
→ Java 7+ 의 큰 개선
한계 (try-with-resources):
여전히 남는 부담:
- 매 메서드마다 작성 (반복)
- 예외 변환 X (SQLException 그대로)
- 트랜잭션 통합 X
- DataSource 통합 X
- 풀 + 트랜잭션 + 매핑 등은 직접
→ 충분치 않음
비교:
전통 JDBC:
- try/catch/finally
- 자원 해제 코드 많음
- 누수 위험
try-with-resources:
- 자원 자동 해제
- 코드 짧음
- 누수 X
JdbcTemplate:
- 위 + 예외 변환
- + 매핑 자동
- + 트랜잭션 통합
- + 더 짧음
→ try-with-resources < JdbcTemplate
// try-with-resources (ILIC)
public Shipment getWithTry(Long id) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"select * from shipments where id = ?")) {
stmt.setLong(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
}
return null;
}
}
}
// 더 짧긴 하지만:
// - 매번 작성
// - SQLException 그대로 던짐 (Spring 은 DataAccessException 으로 변환)
// - 트랜잭션 (autoCommit 처리) 별도
// - 매핑 직접
// → JdbcTemplate 이 한 단계 더 (다음 Unit)
class Shipment { void setId(Long id) {} }
DataSource dataSource;
interface DataSource { Connection getConnection() throws SQLException; }
try-with-resources 로 줄여도 한계는?
답:
1. try-with-resources:
개선:
한계:
JdbcTemplate:
SQLException:
Checked Exception:
- 반드시 처리 (try/catch 또는 throws)
- 매 메서드에 throws SQLException
- 또는 catch
→ 메서드 시그니처 오염
// 의미 없는 catch
try {
// ...
} catch (SQLException e) {
e.printStackTrace(); // 실용성 ↓
throw new RuntimeException(e);
}
// 의미 있는 처리 어려움
DB 별 예외 다름:
같은 의미라도:
- MySQL: SQLException (errorCode)
- Oracle: SQLException (다른 errorCode)
- PostgreSQL: PSQLException
→ 일관 처리 어려움
Spring 의 해결 (JdbcTemplate):
SQLException → DataAccessException:
- Runtime Exception (Unchecked)
- DB 무관 일관
- 의미 있는 분류
- EmptyResultDataAccessException
- DuplicateKeyException
- DataIntegrityViolationException
→ Phase 7 후속
// 예외 처리 부담 (ILIC)
// JDBC 직접 (Checked, throws 폭증)
public class ShipmentDao {
public Shipment get(Long id) throws SQLException {
// throws 필요
}
public void add(Shipment s) throws SQLException {
// throws
}
}
// Service 도 영향
public class ShipmentService {
public void process(Shipment s) throws SQLException {
// throws 전파
}
}
// Controller 도?
@RestController
public class ShipmentController {
// throws SQLException 도 처리 (?)
public Shipment get(Long id) throws SQLException {
// 어색
}
}
// Spring JdbcTemplate:
// - DataAccessException (RuntimeException)
// - throws 불필요
// - 전역 예외 핸들러로 처리
class Shipment {}
예외 처리의 부담은?
답:
1. SQLException:
부담:
DB 별:
Spring 해결:
JdbcTemplate (Unit 7.2):
Spring 의 해결:
- 반복 코드 제거
- 자동 자원 관리
- 예외 변환
- 람다와 잘 맞음
→ 5주차 패턴의 결정체
// JdbcTemplate (미리보기, 다음 Unit)
@Repository
public class ShipmentDao {
private final JdbcTemplate jdbcTemplate;
public ShipmentDao(JdbcTemplate jt) {
this.jdbcTemplate = jt;
}
public Shipment get(Long id) {
return jdbcTemplate.queryForObject(
"select * from shipments where id = ?",
(rs, n) -> {
Shipment s = new Shipment();
s.setId(rs.getLong("id"));
return s;
},
id
);
}
// 본 로직만 (SQL/매핑/파라미터)
// 자원 관리/예외 처리 X
// 5줄!
}
class Shipment { void setId(Long id) {} }
class JdbcTemplate {
<T> T queryForObject(String sql, RowMapper<T> m, Object... args) {
return null;
}
interface RowMapper<T> { T mapRow(ResultSet rs, int n) throws SQLException; }
}
5주차 정신:
JdbcTemplate:
- 템플릿 메소드 + 전략
- DI (DataSource 받음)
- OCP (새 쿼리 = 새 람다)
- 관심사 분리
→ 5주차의 결정판
| Q | 핵심 답변 |
|---|---|
| JDBC 반복? | try/catch/finally |
| 부담? | 자원 관리 |
| 본 로직 비율? | 적음 |
| 누수? | close 누락 |
| 변하는/변하지 않는? | SQL/흐름 |
| 5주차 패턴? | 템플릿/전략 |
| try-with-resources? | 일부 해결 |
| SQLException? | Checked 부담 |
| 다음? | JdbcTemplate |
| 5주차 정신? | 관심사 분리 |
답:
답:
답:
답:
답:
1. JDBC 의 반복
2. 변하는 / 변하지 않는 부분
3. try-with-resources 의 한계와 다음
이번 Unit에서 JDBC 반복 문제를 봤다면, 다음은 JdbcTemplate (★ 깊이 파기).
🛠️ Phase 7 — JdbcTemplate
✅ Unit 7.1 JDBC 만 쓸 때 반복 코드 ← 여기
⏭ Unit 7.2 JdbcTemplate 등장 ★깊이
⏭ Unit 7.3 update/queryForObject/query
⏭ Unit 7.4 RowMapper
⏭ Unit 7.5 JdbcTemplate 구조적 의미
🧪 Part A (9 Unit) ✅
💾 Part B — DB 접근의 진화
✅ Phase 3 — JDBC (3)
✅ Phase 4 — Connection Pool (4)
✅ Phase 5 — DataSource (4)
✅ Phase 6 — ACID (6)
🛠️ Phase 7 — JdbcTemplate (1/5 진행)
총: 27/28 Unit (96%!)
🛠️ Phase 7 시작 — JdbcTemplate