F-LAB JAVA · 5주차 · Phase 3 · 전통 DAO의 문제
🏆 Phase 3 완주 — 전통 DAO 문제 진단
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
전통 DAO 의 한 메서드에는 DB 연결 정보 관리·드라이버 로딩·SQL 작성·자원 해제·예외 처리가 모두 섞여 있어, 어느 하나가 바뀌면 그것이 박힌 모든 메서드를 수정해야 하는 "변경 1번 = 수정 N곳" 의 유지보수 지옥을 만든다.
한 메서드의 책임은 (1) DB 연결 정보 관리, (2) DB 드라이버 로딩, (3) SQL 작성과 실행, (4) 자원 해제, (5) 예외 처리로 나눌 수 있는데, 이 책임들이 변경의 이유가 서로 다름에도 한 곳에 모여 있다.
그 결과 DB 종류가 바뀌면, 접속 정보가 바뀌면, 매번 모든 메서드를 수정 해야 하고, 같은 연결 코드가 메서드마다 반복되는 중복이 발생한다.
코드 중복은 단순한 미관 문제가 아니라, 한 곳의 변경을 여러 곳에 일일이 반영해야 하고 (수정 비용), 일부를 빠뜨리면 버그가 되는 (정합성 위험) 실질적 위협이다.
이 코드는 SOLID 의 SRP (단일 책임)·OCP (개방폐쇄)·DIP (의존 역전) 를 모두 위반하며, 다음 Phase 부터 관심사 분리로 하나씩 해결해 나간다.
책임 혼재 = 흩어진 전화번호:
같은 번호를 여기저기 적어둠 (중복):
- 명함첩에
- 다이어리에
- 휴대폰에
- 메모지에
번호 바뀌면 (변경):
- 명함첩 수정
- 다이어리 수정
- 휴대폰 수정
- 메모지 수정
→ 한 번 바뀌었는데 4곳 수정
하나 빠뜨리면 (정합성):
- 옛 번호로 전화 (버그)
- 어디가 최신인지 모름
해결:
- 한 곳에서 관리 (분리)
- 변경 한 곳
→ 중복은 미관 문제가 아니라
→ 변경 비용 + 정합성 위험
→ 책임 혼재 = 변경 1번에 N곳 수정 + 중복 (정합성 위험), SOLID 위반.
1. 섞인 책임들
2. 변경 1번 = 수정 N곳
3. 중복이 미관 문제가 아닌 이유
4. SRP 위반
5. OCP 위반
6. DIP 위반
7. 유지보수 지옥
8. Phase 3 완주 정리
9. 면접 + 자기 점검
한 메서드의 책임:
1. DB 연결 정보 관리 (URL, user, password)
2. DB 드라이버 로딩
3. SQL 작성과 실행
4. 자원 해제
5. 예외 처리
public void add(Shipment shipment) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // [2] 드라이버
Connection c = DriverManager.getConnection( // [1] 연결 정보
"jdbc:mysql://localhost/ilic", "root", "password");
PreparedStatement ps = c.prepareStatement( // [3] SQL
"insert into shipments ...");
ps.setLong(1, shipment.getId()); // [3] 실행
ps.executeUpdate();
ps.close(); c.close(); // [4] 자원 해제
// [5] 예외 처리 (throws)
}
// 5가지 책임 한 메서드
변경 이유 다름:
[1] 연결 정보: DB 서버/계정 변경
[2] 드라이버: DB 종류 변경
[3] SQL: 쿼리/스키마 변경
[4] 자원 해제: (거의 불변)
[5] 예외 처리: 정책 변경
→ 각자 다른 이유
→ 한 곳에 모이면 안 됨
public class ShipmentDao {
public void add(Shipment shipment) throws Exception {
// 책임 [2] 드라이버 (DB 종류 변경 시)
Class.forName("com.mysql.jdbc.Driver");
// 책임 [1] 연결 정보 (서버/계정 변경 시)
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password");
// 책임 [3] 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();
// 책임 [4] 자원 해제
ps.close();
c.close();
}
// 5가지 변경 이유가 한 메서드에 (문제)
}
한 메서드에 섞인 책임들은?
답:
1. 5가지:
변경 이유:
문제:
결과:
변경 1번 = 수정 N곳:
한 가지 변경 (예: DB 종류):
- 모든 메서드에 박힘
- 메서드 N개 모두 수정
→ 변경 비용 = 메서드 수
변경 시나리오:
DB 종류 변경:
→ 드라이버/URL 박힌 N곳 수정
접속 정보 변경:
→ 연결 정보 박힌 N곳 수정
SQL 정책 변경:
→ 해당 SQL 수정 (그나마 분리적)
수정 비용:
메서드 10개:
- DB 변경 = 10곳 수정
- 시간 ↑
- 실수 가능
- 누락 위험
메서드 100개:
- 100곳 (악몽)
누락 위험:
N곳 수정 중:
- 하나 빠뜨림
- 일부는 옛 설정
- 일부는 새 설정
- 정합성 깨짐 (버그)
public class ShipmentDao {
// 메서드마다 연결 정보 박힘
public void add(Shipment s) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 수정 1
// ...
}
public Shipment get(Long id) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 수정 2
return null;
}
public void update(Shipment s) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 수정 3
}
public void delete(Long id) throws Exception {
Connection c = DriverManager.getConnection(
"jdbc:mysql://localhost/ilic", "root", "password"); // 수정 4
}
// DB 비밀번호 변경 → 4곳 모두 수정
// 하나 빠뜨리면 → 그 메서드만 연결 실패 (버그)
}
"변경 1번 = 수정 N곳" 의 의미는?
답:
1. 핵심:
시나리오:
비용:
누락 위험:
중복의 진짜 위험:
단순 미관 X:
- 변경 비용 (N곳 수정)
- 정합성 위험 (누락)
- 버그 확산
- 이해 비용
변경 비용:
중복 코드:
- 한 곳 변경
- 다른 곳도 수동 반영
- 시간 + 실수
단일 코드:
- 한 곳만 변경
- 자동 반영
정합성 위험:
중복 N곳:
- 일부만 변경
- 나머지 옛 코드
- 불일치 (버그)
"어디가 진짜?"
버그 확산:
중복 코드에 버그:
- 모든 복사본에 버그
- 하나 고쳐도 나머지 버그
- 추적 어려움
DRY (Don't Repeat Yourself):
"모든 지식은 시스템 내에서
단 하나의 명확한 표현을 가져야"
중복 제거:
- 변경 한 곳
- 정합성 보장
public class ShipmentDao {
// 중복된 연결 + 자원 해제 패턴
public void add(Shipment s) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // 중복
Connection c = DriverManager.getConnection(...); // 중복
// SQL...
c.close(); // 중복
}
public Shipment get(Long id) throws Exception {
Class.forName("com.mysql.jdbc.Driver"); // 중복 (같은 코드)
Connection c = DriverManager.getConnection(...); // 중복
// SQL...
c.close(); // 중복
return null;
}
// 만약 연결 방식에 버그가 있다면:
// → add, get, update, delete 모두 버그
// → 하나만 고치면 나머지 여전히 버그
// → 중복은 버그 확산 (미관 X)
// 해결: getConnection() 추출 (Phase 4.2)
}
코드 중복이 단순 미관 문제가 아닌 이유는?
답:
1. 진짜 위험:
변경 비용:
정합성:
버그 확산:
SRP (Single Responsibility Principle):
"클래스/메서드는 단 하나의 책임만"
- 변경의 이유가 하나
- 한 가지 일만
전통 DAO SRP 위반:
한 메서드:
- 5가지 책임
- 5가지 변경 이유
→ SRP 위반
변경 이유로 SRP 판단:
add 메서드 변경 이유:
1. DB 종류 변경
2. 접속 정보 변경
3. SQL 변경
→ 변경 이유 여럿
→ SRP 위반
SRP 준수:
연결 관리 → ConnectionMaker
SQL 실행 → DAO
설정 → 외부
각자 하나의 책임
변경 이유 하나
// ❌ SRP 위반 (한 메서드 여러 책임)
public class ShipmentDaoBad {
public void add(Shipment s) throws Exception {
Class.forName("..."); // 책임 1: 드라이버
Connection c = DriverManager.getConnection(...); // 책임 2: 연결
PreparedStatement ps = c.prepareStatement(...); // 책임 3: SQL
ps.executeUpdate();
ps.close(); c.close(); // 책임 4: 해제
}
}
// ✓ SRP 준수 방향 (Phase 6 에서 도달)
interface ConnectionMaker { // 책임: 연결 생성만
Connection makeConnection();
}
class ShipmentDaoGood { // 책임: 데이터 접근만
private final ConnectionMaker connectionMaker;
public ShipmentDaoGood(ConnectionMaker cm) {
this.connectionMaker = cm;
}
public void add(Shipment s) {
Connection c = connectionMaker.makeConnection(); // 연결은 위임
// SQL 만 (자기 책임)
}
}
SOLID의 SRP 위반은?
답:
1. SRP:
위반:
판단:
준수:
OCP (Open-Closed Principle):
"확장에는 열려있고
변경에는 닫혀있어야"
- 새 기능 = 추가 (확장)
- 기존 코드 = 안 건드림 (닫힘)
전통 DAO OCP 위반:
새 DB 지원하려면:
- 기존 코드 수정
- 드라이버/URL 변경
→ 변경에 열림 (위반)
→ 확장에 닫힘
// OCP 위반 신호 — 분기
public Connection getConnection(String dbType) {
if (dbType.equals("MySQL")) {
return mysqlConnection();
} else if (dbType.equals("Oracle")) {
return oracleConnection();
}
// 새 DB 추가 시 → 이 메서드 수정 (OCP 위반)
return null;
}
OCP 준수:
새 DB = 새 구현 추가:
- ConnectionMaker 구현
- 기존 코드 변경 X
→ 확장 (추가)
→ 닫힘 (수정 X)
// ❌ OCP 위반 (새 DB = 기존 수정)
public class ShipmentDaoBad {
public void add(Shipment s) throws Exception {
// MySQL 하드코딩 → Oracle 추가 시 수정
Class.forName("com.mysql.jdbc.Driver");
// ...
}
}
// ✓ OCP 준수 방향 (Phase 6)
interface ConnectionMaker {
Connection makeConnection();
}
class MySqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; /* MySQL */ }
}
class OracleConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; /* Oracle */ }
}
// 새 DB = 새 구현 추가 (확장)
// ShipmentDao 코드 변경 X (닫힘)
SOLID의 OCP 위반은?
답:
1. OCP:
위반:
신호:
준수:
DIP (Dependency Inversion Principle):
"구체가 아닌 추상에 의존하라"
- 고수준 모듈이 저수준에 의존 X
- 둘 다 추상화에 의존
전통 DAO DIP 위반:
DAO 가 직접:
- DriverManager (구체)
- 특정 드라이버 (구체)
→ 구체에 의존 (위반)
// DIP 위반 — 구체 의존
public class ShipmentDao {
public void add(Shipment s) throws Exception {
// 구체 클래스 직접 의존
Class.forName("com.mysql.jdbc.Driver"); // 구체
Connection c = DriverManager.getConnection(...); // 구체
// DAO 가 특정 DB 구현에 묶임
}
}
// DIP 준수 — 추상 의존
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 추상 (인터페이스)
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm; // 추상에 의존
}
public void add(Shipment s) {
Connection c = connectionMaker.makeConnection();
// 구체 모름 (추상에 의존)
}
}
// DAO → ConnectionMaker (추상)
// 구체 구현은 외부에서 주입 (DI)
// 진화 방향: 구체 의존 → 추상 의존
// ❌ 현재 (DIP 위반)
// ShipmentDao → DriverManager + 특정 드라이버 (구체)
// ✓ 목표 (DIP 준수, Phase 6~8)
interface ConnectionMaker { // 추상
Connection makeConnection();
}
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 추상 의존
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm;
}
}
// ShipmentDao (고수준) → ConnectionMaker (추상) ← 구현 (저수준)
// 고수준이 저수준에 직접 의존 X (역전)
SOLID의 DIP 위반은?
답:
1. DIP:
위반:
구체 의존:
준수:
유지보수 지옥:
변경 1번:
- 여러 곳 수정
- 실수 가능
- 누락 위험
- 테스트 부담
→ 변경이 두려운 코드
구체적 모습:
"DB 비밀번호 바꿔야 해"
→ DAO 메서드 20개 다 열어서
→ 일일이 수정
→ 하나 빠뜨림
→ 그 메서드만 장애
→ 디버깅 (어디 빠뜨렸지?)
→ 단순 변경이 큰 작업
변경 두려움:
코드 건드리기 무서움:
- 어디 영향?
- 뭐가 깨지지?
- 테스트 부담
→ 개선 안 함 (악순환)
해결 방향 (다음 Phase):
Phase 4: 관심사 분리
- 메서드 추출
- 추상 클래스
Phase 5: 디자인 패턴
- 템플릿 메소드/팩토리
Phase 6: OCP/전략 패턴
- 인터페이스
Phase 7~8: IoC/DI
- 외부 주입
// 유지보수 지옥의 모습
public class ShipmentDao {
// 20개 메서드, 각각 연결 정보 박힘
public void add(Shipment s) throws Exception { /* 연결 정보 */ }
public Shipment get(Long id) throws Exception { /* 연결 정보 */ return null; }
public void update(Shipment s) throws Exception { /* 연결 정보 */ }
public void delete(Long id) throws Exception { /* 연결 정보 */ }
public List<Shipment> findAll() throws Exception { /* 연결 정보 */ return List.of(); }
public List<Shipment> findByStatus(String status) throws Exception { /* 연결 정보 */ return List.of(); }
// ... 14개 더
// DB 비밀번호 변경 → 20곳 수정
// 하나 빠뜨림 → 그 메서드만 장애 → 디버깅 지옥
// → Phase 4~8 에서 단계적 해결
}
유지보수 지옥의 구체적 모습은?
답:
1. 지옥:
구체적:
변경 두려움:
해결:
Phase 3 — 전통 DAO의 문제
Unit 3.1 — DAO란 무엇인가
- 데이터 접근 전담
- 비즈니스 분리
Unit 3.2 — 전통 DAO의 코드
- 6단계, 5가지 관심사
- 중복
Unit 3.3 — 책임 혼재
- 변경 N곳
- SOLID 위반
Phase 3 핵심 메시지:
"Spring 없이 짠 DAO 는
모든 책임을 한 곳에 떠안아
변경 1번에 N곳을 수정하는
유지보수 지옥이다.
→ 이것을 해결하는 게 Spring 의 정신"
전통 DAO SOLID 위반:
SRP: 5가지 책임 한 메서드
OCP: 새 DB = 기존 수정
DIP: 구체(DriverManager) 의존
→ 다음 Phase 에서 해결
Phase 3 → Phase 4:
- 문제 진단 → 관심사 분리
Phase 4 — 관심사의 분리 (★ 깊이):
- 관심사 분리 개념
- 메서드 추출
- 추상 클래스 확장
Phase 3의 종합은?
답:
1. 3 Unit:
메시지:
SOLID 위반:
다음:
| Q | 핵심 답변 |
|---|---|
| 섞인 책임? | 연결정보/드라이버/SQL/해제/예외 |
| 변경 1번 = N곳? | 모든 메서드 수정 |
| 중복 위험? | 변경 비용, 정합성 |
| SRP 위반? | 5가지 책임 |
| OCP 위반? | 새 DB = 기존 수정 |
| DIP 위반? | 구체 의존 |
| 유지보수 지옥? | 변경 두려움 |
| DRY? | 중복 제거 |
| 해결? | 관심사 분리 (Phase 4) |
| Spring 정신? | 책임 분리 |
답:
답:
답:
답:
답:
1. 섞인 책임
2. 변경의 고통
3. SOLID 위반
🌱 Phase 3 — 전통 DAO의 문제
✅ Unit 3.1 DAO란 무엇인가
✅ Unit 3.2 전통 DAO의 코드
✅ Unit 3.3 책임 혼재 ← 여기, Phase 3 완주
→ DAO 개념
→ 전통 코드의 문제 (5가지 책임)
→ SOLID 위반 (SRP/OCP/DIP)
Phase 4 — 관심사의 분리 (★ 깊이 파기)
Unit 4.1 — 관심사 분리 개념
Unit 4.2 — 1단계: 메서드 추출
Unit 4.3 — 2단계: 추상클래스로 확장
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3 — 전통 DAO (3 Unit) ← 완주
⏭ Phase 4 — 관심사의 분리 (3 Unit, ★ 깊이)
총: 10/26 Unit
🏆 Phase 3 완주 — 전통 DAO 문제 진단