F-LAB JAVA · 5주차 · Phase 3 · 전통 DAO의 문제
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
전통 JDBC DAO 의 한 메서드는 드라이버 로딩·DB 접속·SQL 작성·파라미터 바인딩·실행·자원 해제까지 모든 책임을 떠안고 있어, 변경 한 번에 여러 곳을 수정해야 하는 유지보수 부담을 만든다.
전통 DAO 의add()한 메서드 안에는 (1) 드라이버 로딩, (2) DB 접속 정보 (URL·계정·비밀번호), (3) SQL 작성, (4) 파라미터 바인딩, (5) 실행, (6) 자원 해제가 모두 들어 있다.
이렇게 모든 관심사가 한 메서드에 모이면, DB 종류를 바꾸면 (MySQL → Oracle) 드라이버·URL 이 박힌 모든 메서드를 수정해야 하고, 접속 정보가 바뀌면 역시 모든 메서드를 손봐야 한다.
또한getConnection같은 동일한 코드가 메서드마다 반복되어 중복 이 발생하고, try-catch-finally 로 자원을 해제하는 코드가 복잡하게 얽힌다.
이 "모든 책임을 떠안은 코드" 가 바로 다음 Phase 들에서 리팩토링으로 풀어갈 문제의 출발점이다.
전통 DAO = 모든 걸 혼자 하는 만물상:
한 직원이 모든 일:
1. 가게 문 열기 (드라이버 로딩)
2. 거래처 연결 (DB 접속 + 계정/비번)
3. 주문서 작성 (SQL)
4. 물건 채우기 (파라미터 바인딩)
5. 거래 실행 (executeUpdate)
6. 정리/마감 (자원 해제)
문제:
- 거래처 바뀌면 (DB 변경)
→ 모든 업무 매뉴얼 수정
- 비밀번호 바뀌면 (접속 정보)
→ 모든 매뉴얼 수정
- 같은 "문 열기" 가 매 업무마다 반복 (중복)
- 한 명이 다 하니 복잡
→ 모든 책임이 한 곳에
→ 변경 1번 = 수정 N곳
→ 전통 DAO = 한 메서드에 모든 책임 (드라이버~자원해제), 변경 시 N곳 수정.
1. 전통 JDBC DAO 코드
2. 6단계 흐름
3. 한 메서드의 5가지 관심사
4. DB 종류 변경 시
5. 접속 정보 변경 시
6. 메서드 간 중복
7. 자원 해제
8. 예외 처리
9. 면접 + 자기 점검
public class ShipmentDao {
public void add(Shipment shipment) throws ClassNotFoundException, SQLException {
// ① 드라이버 로딩
Class.forName("com.mysql.jdbc.Driver");
// ② DB 접속
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ③ SQL 작성
PreparedStatement ps = c.prepareStatement(
"insert into shipments(id, bl_no, weight) values(?, ?, ?)");
// ④ 파라미터 바인딩
ps.setLong(1, shipment.getId());
ps.setString(2, shipment.getBlNo());
ps.setBigDecimal(3, shipment.getWeight());
// ⑤ 실행
ps.executeUpdate();
// ⑥ 자원 해제
ps.close();
c.close();
}
}
한 메서드에:
① 드라이버 로딩
② 접속 정보
③ SQL
④ 바인딩
⑤ 실행
⑥ 자원 해제
→ 모든 책임
public Shipment get(Long id) throws ClassNotFoundException, SQLException {
// ① 드라이버 로딩 (중복!)
Class.forName("com.mysql.jdbc.Driver");
// ② DB 접속 (중복!)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ③ SQL
PreparedStatement ps = c.prepareStatement(
"select * from shipments where id = ?");
ps.setLong(1, id);
// ⑤ 실행
ResultSet rs = ps.executeQuery();
Shipment shipment = null;
if (rs.next()) {
shipment = new Shipment();
shipment.setId(rs.getLong("id"));
shipment.setBlNo(rs.getString("bl_no"));
shipment.setWeight(rs.getBigDecimal("weight"));
}
// ⑥ 자원 해제
rs.close();
ps.close();
c.close();
return shipment;
}
중복 확인:
add 와 get:
- 드라이버 로딩 (동일)
- DB 접속 (동일)
- 자원 해제 (유사)
→ 메서드마다 반복
전통 JDBC DAO 코드의 단계는?
답:
1. 6단계:
한 곳에:
중복:
출발점:
JDBC 6단계:
1. 드라이버 로딩
Class.forName(...)
2. 연결 (Connection)
DriverManager.getConnection(...)
3. SQL (PreparedStatement)
c.prepareStatement(...)
4. 파라미터 바인딩
ps.setXxx(...)
5. 실행
ps.executeUpdate() / executeQuery()
6. 자원 해제
close()
각 단계:
① 드라이버: JDBC 드라이버 등록
② 연결: DB 와 연결 수립
③ SQL: 쿼리 준비
④ 바인딩: 파라미터 설정
⑤ 실행: 쿼리 실행
⑥ 해제: 연결/자원 반납
변하는 것 vs 변하지 않는 것:
변하지 않는 흐름 (공통):
- 드라이버 로딩
- 연결
- 자원 해제
변하는 부분:
- SQL (메서드마다)
- 바인딩 (다름)
JDBC 흐름:
[드라이버 로딩] ← 공통
↓
[연결] ← 공통
↓
[SQL 준비] ← 변함
↓
[바인딩] ← 변함
↓
[실행] ← 변함
↓
[자원 해제] ← 공통
public class ShipmentDao {
public void updateStatus(Long id, String status)
throws ClassNotFoundException, SQLException {
// ① 드라이버 (공통)
Class.forName("com.mysql.jdbc.Driver");
// ② 연결 (공통)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ③ SQL (변함)
PreparedStatement ps = c.prepareStatement(
"update shipments set status = ? where id = ?");
// ④ 바인딩 (변함)
ps.setString(1, status);
ps.setLong(2, id);
// ⑤ 실행 (변함)
ps.executeUpdate();
// ⑥ 해제 (공통)
ps.close();
c.close();
}
// 공통 부분이 매번 반복
}
드라이버 로딩 / 연결 / SQL / 자원 해제의 흐름은?
답:
1. 6단계:
각 역할:
변함 vs 공통:
반복:
한 메서드의 관심사:
1. DB 연결 정보 관리
- URL, 계정, 비밀번호
2. DB 드라이버 로딩
- Class.forName
3. SQL 작성과 실행
- PreparedStatement
4. 파라미터 바인딩
- setXxx
5. 자원 해제
- close
관심사 (Concern):
코드가 다루는 하나의 주제·책임.
- 변경의 이유
- SRP 와 직결
여러 관심사 한 곳 = 문제
관심사별 변경 이유:
연결 정보: DB 서버 변경
드라이버: DB 종류 변경
SQL: 쿼리 변경
바인딩: 컬럼 변경
자원 해제: (보통 불변)
→ 각자 다른 이유로 변경
한 곳에 모인 문제:
여러 관심사 한 메서드:
- 한 이유로 변경해도
- 다른 관심사 코드 건드림
- 영향 범위 넓음
→ SRP 위반
public class ShipmentDao {
public void add(Shipment shipment) throws Exception {
// [관심사 1] DB 드라이버 로딩
Class.forName("com.mysql.jdbc.Driver");
// [관심사 2] DB 연결 정보 (URL, 계정, 비밀번호)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// [관심사 3] SQL 작성
PreparedStatement ps = c.prepareStatement(
"insert into shipments(id, bl_no, weight) values(?, ?, ?)");
// [관심사 4] 파라미터 바인딩
ps.setLong(1, shipment.getId());
ps.setString(2, shipment.getBlNo());
ps.setBigDecimal(3, shipment.getWeight());
ps.executeUpdate(); // [관심사 3] 실행
// [관심사 5] 자원 해제
ps.close();
c.close();
}
// 5가지 관심사가 한 메서드에
// → 분리 필요 (다음 Phase)
}
한 메서드가 신경 쓰는 관심사 5개는?
답:
1. 5가지:
관심사:
변경 이유:
문제:
DB 종류 변경 (MySQL → Oracle):
수정해야 할 것:
- 드라이버: com.mysql → oracle.jdbc
- URL: jdbc:mysql → jdbc:oracle
- (일부 SQL 문법)
모든 메서드에 박혀 있음
수정 범위:
드라이버/URL 이:
- add 에 박힘
- get 에 박힘
- update 에 박힘
- delete 에 박힘
- ...
→ 모든 메서드 수정
// MySQL 버전
Class.forName("com.mysql.jdbc.Driver");
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", ...);
// Oracle 로 변경 → 모든 메서드 수정
Class.forName("oracle.jdbc.OracleDriver");
Connection c = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:1521:ilic", ...);
// add, get, update, delete ... 전부 수정
변경 비용:
메서드 N개:
- N곳 수정
- 실수 가능
- 누락 위험
→ "변경 1번 = 수정 N곳"
// ❌ DB 변경이 모든 메서드에 영향
public class ShipmentDao {
public void add(Shipment s) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // 수정 대상
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 수정 대상
// ...
}
public Shipment get(Long id) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // 또 수정
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 또 수정
// ...
return null;
}
public void update(Shipment s) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // 또또 수정
// ...
}
// MySQL → Oracle: 모든 메서드 수정 (유지보수 지옥)
}
DB 종류 변경 시 수정 범위는?
답:
1. 변경 대상:
범위:
이유:
비용:
접속 정보:
- URL
- 계정 (user)
- 비밀번호 (password)
메서드마다 하드코딩
변경 시나리오:
- 비밀번호 변경 (보안 정책)
- DB 서버 이전 (URL)
- 계정 변경
→ 모든 메서드 수정
// 하드코딩 (문제)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", // URL 하드코딩
"root", // 계정 하드코딩
"password"); // 비밀번호 하드코딩!
// 비밀번호 변경:
// - 모든 메서드 수정
// - 보안 위험 (코드에 노출)
보안 문제:
비밀번호 하드코딩:
- 소스 코드에 노출
- 버전 관리에 포함
- 보안 취약
→ 외부 설정 분리 필요
// ❌ 접속 정보 하드코딩 (모든 메서드)
public class ShipmentDao {
public void add(Shipment s) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 하드코딩
}
public Shipment get(Long id) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 또 하드코딩
return null;
}
}
// 비밀번호 변경 → 모든 메서드 수정 + 보안 위험
// 해결 방향:
// - 연결 생성 분리 (다음 Phase)
// - 외부 설정 (application.yml)
접속 정보 변경 시 수정 범위는?
답:
1. 접속 정보:
변경 시나리오:
하드코딩 문제:
보안:
메서드 간 중복:
공통 코드 반복:
- 드라이버 로딩
- 연결 생성
- 자원 해제
add, get, update, delete ... 모두
중복의 문제:
- 변경 시 모두 수정
- 실수/누락 위험
- 코드 비대
- DRY 위반
DRY (Don't Repeat Yourself):
중복을 제거하라:
- 한 곳에서 관리
- 변경 한 곳
- 일관성
중복 = 유지보수 부담
// 중복 (getConnection 부분)
public void add(...) {
Class.forName("com.mysql.jdbc.Driver"); // 중복
Connection c = DriverManager.getConnection(...); // 중복
// ...
}
public User get(...) {
Class.forName("com.mysql.jdbc.Driver"); // 중복
Connection c = DriverManager.getConnection(...); // 중복
// ...
}
// 같은 코드 반복
// → 메서드 추출로 해결 (Phase 4)
public class ShipmentDao {
// 중복되는 연결 코드
public void add(Shipment s) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // ↓ 중복
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ...
}
public Shipment get(Long id) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // ↓ 동일 중복
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ...
return null;
}
public List<Shipment> findAll() throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // ↓ 또 중복
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// ...
return List.of();
}
// 연결 코드가 3번 중복
// → getConnection() 메서드 추출 (Phase 4.2)
}
메서드 간 중복 코드는?
답:
1. 중복:
문제:
DRY:
해결:
자원 해제 중요성:
Connection, Statement, ResultSet:
- 사용 후 반드시 close
- 안 하면 자원 누수
→ 연결 고갈, 메모리 누수
자원 누수 위험:
close 안 하면:
- 연결 풀 고갈
- "Too many connections"
- 메모리 누수
- 서버 다운
→ 반드시 해제
// finally 로 자원 해제 (전통)
Connection c = null;
PreparedStatement ps = null;
try {
c = getConnection();
ps = c.prepareStatement(sql);
ps.executeUpdate();
} catch (SQLException e) {
// 예외 처리
} finally {
// 반드시 해제
if (ps != null) try { ps.close(); } catch (SQLException e) {}
if (c != null) try { c.close(); } catch (SQLException e) {}
}
// 복잡하고 장황
// try-with-resources (Java 7+, 개선)
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.executeUpdate();
} // 자동 close (AutoCloseable)
// 훨씬 간결
public class ShipmentDao {
// ❌ 자원 해제 누락 (위험)
public void addUnsafe(Shipment s) throws Exception {
Connection c = getConnection();
PreparedStatement ps = c.prepareStatement("insert ...");
ps.executeUpdate();
// 예외 발생 시 close 안 됨 → 누수!
ps.close();
c.close();
}
// ✓ try-with-resources (안전)
public void addSafe(Shipment s) throws Exception {
String sql = "insert into shipments(id, bl_no, weight) values(?, ?, ?)";
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, s.getId());
ps.setString(2, s.getBlNo());
ps.setBigDecimal(3, s.getWeight());
ps.executeUpdate();
} // 자동 해제 (예외에도)
}
private Connection getConnection() throws Exception {
return DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
}
}
자원 해제의 중요성은?
답:
1. 중요성:
누수 위험:
finally:
try-with-resources:
JDBC checked 예외:
- ClassNotFoundException (드라이버)
- SQLException (DB 작업)
→ throws 또는 try-catch 강제
// 예외 throws (전파)
public void add(Shipment s)
throws ClassNotFoundException, SQLException {
// checked 예외 던짐
// 호출자가 처리해야
}
// 호출하는 비즈니스 코드:
try {
dao.add(shipment);
} catch (ClassNotFoundException | SQLException e) {
// 비즈니스 코드에 기술 예외 노출 (문제)
}
기술 예외 노출 문제:
SQLException 이:
- 비즈니스 코드에 전파
- 기술 세부사항 노출
- 비즈니스가 DB 예외 처리
→ 추상화 누수
// unchecked 로 변환 (개선)
public void add(Shipment s) {
try {
// JDBC 작업
} catch (SQLException e) {
throw new RuntimeException("DB 오류", e); // unchecked
}
}
// Spring: DataAccessException (unchecked)
// 비즈니스가 기술 예외 안 봄
public class ShipmentDao {
// ❌ checked 예외 전파 (비즈니스에 부담)
public void addChecked(Shipment s)
throws ClassNotFoundException, SQLException {
// 호출자가 기술 예외 처리해야
}
// ✓ unchecked 변환 (추상화)
public void addUnchecked(Shipment shipment) {
String sql = "insert into shipments(id, bl_no, weight) values(?, ?, ?)";
try (Connection c = getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, shipment.getId());
ps.setString(2, shipment.getBlNo());
ps.setBigDecimal(3, shipment.getWeight());
ps.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("배송 저장 실패", e); // unchecked
}
}
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
}
static class DataAccessException extends RuntimeException {
DataAccessException(String msg, Throwable cause) { super(msg, cause); }
}
}
예외 처리의 복잡성은?
답:
1. checked:
전파:
기술 예외 노출:
개선:
| Q | 핵심 답변 |
|---|---|
| 전통 DAO 단계? | 드라이버~자원해제 6단계 |
| 6단계? | 로딩/연결/SQL/바인딩/실행/해제 |
| 관심사 5개? | 연결정보/드라이버/SQL/바인딩/해제 |
| DB 변경? | 모든 메서드 수정 |
| 접속 정보? | 하드코딩, 보안 위험 |
| 중복? | 연결 코드 반복 |
| 자원 해제? | 누수 방지 (try-with-resources) |
| 예외? | checked, unchecked 변환 |
| 문제 출발점? | 모든 책임 한 곳 |
답:
답:
답:
답:
답:
1. 전통 DAO 코드
2. 변경의 고통
3. 문제의 출발점
이번 Unit에서 전통 DAO 코드를 봤다면, 다음은 책임 혼재 진단 (Phase 3 마지막).
🌱 Phase 3 — 전통 DAO의 문제
✅ Unit 3.1 DAO란 무엇인가
✅ Unit 3.2 전통 DAO의 코드 ← 여기
⏭ Unit 3.3 책임 혼재
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
Phase 3 — 전통 DAO (2/3 진행)
총: 9/26 Unit