5주차 Unit 3.3 — 무엇이 문제인가, 책임 혼재

Psj·2026년 5월 27일

F-lab

목록 보기
165/240

Unit 3.3 — 무엇이 문제인가, 책임 혼재

F-LAB JAVA · 5주차 · Phase 3 · 전통 DAO의 문제
🏆 Phase 3 완주 — 전통 DAO 문제 진단


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 한 메서드에 섞인 책임 들은?
  • "변경 1번 = 수정 N곳" 의 의미는?
  • 코드 중복이 단순 미관 문제가 아닌 이유는?
  • SOLID 의 SRP 위반 은?
  • SOLID 의 OCP 위반 은?
  • SOLID 의 DIP 위반 은?
  • 유지보수 지옥 의 구체적 모습은?
  • 변경의 이유로 책임을 나누는 원칙은?
  • Phase 3 전체 의 종합은?

🎯 핵심 한 문장

전통 DAO 의 한 메서드에는 DB 연결 정보 관리·드라이버 로딩·SQL 작성·자원 해제·예외 처리가 모두 섞여 있어, 어느 하나가 바뀌면 그것이 박힌 모든 메서드를 수정해야 하는 "변경 1번 = 수정 N곳" 의 유지보수 지옥을 만든다.
한 메서드의 책임은 (1) DB 연결 정보 관리, (2) DB 드라이버 로딩, (3) SQL 작성과 실행, (4) 자원 해제, (5) 예외 처리로 나눌 수 있는데, 이 책임들이 변경의 이유가 서로 다름에도 한 곳에 모여 있다.
그 결과 DB 종류가 바뀌면, 접속 정보가 바뀌면, 매번 모든 메서드를 수정 해야 하고, 같은 연결 코드가 메서드마다 반복되는 중복이 발생한다.
코드 중복은 단순한 미관 문제가 아니라, 한 곳의 변경을 여러 곳에 일일이 반영해야 하고 (수정 비용), 일부를 빠뜨리면 버그가 되는 (정합성 위험) 실질적 위협이다.
이 코드는 SOLID 의 SRP (단일 책임)·OCP (개방폐쇄)·DIP (의존 역전) 를 모두 위반하며, 다음 Phase 부터 관심사 분리로 하나씩 해결해 나간다.

비유 — 모든 정보가 흩어진 주소록

책임 혼재 = 흩어진 전화번호:

같은 번호를 여기저기 적어둠 (중복):
  - 명함첩에
  - 다이어리에
  - 휴대폰에
  - 메모지에

번호 바뀌면 (변경):
  - 명함첩 수정
  - 다이어리 수정
  - 휴대폰 수정
  - 메모지 수정
  → 한 번 바뀌었는데 4곳 수정

하나 빠뜨리면 (정합성):
  - 옛 번호로 전화 (버그)
  - 어디가 최신인지 모름

해결:
  - 한 곳에서 관리 (분리)
  - 변경 한 곳

→ 중복은 미관 문제가 아니라
→ 변경 비용 + 정합성 위험

→ 책임 혼재 = 변경 1번에 N곳 수정 + 중복 (정합성 위험), SOLID 위반.


🧭 9개 섹션 로드맵

1. 섞인 책임들
2. 변경 1번 = 수정 N곳
3. 중복이 미관 문제가 아닌 이유
4. SRP 위반
5. OCP 위반
6. DIP 위반
7. 유지보수 지옥
8. Phase 3 완주 정리
9. 면접 + 자기 점검

1️⃣ 섞인 책임들

1.1 5가지 책임

한 메서드의 책임:

1. DB 연결 정보 관리 (URL, user, password)
2. DB 드라이버 로딩
3. SQL 작성과 실행
4. 자원 해제
5. 예외 처리

1.2 책임 식별

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.3 변경 이유 다름

변경 이유 다름:

[1] 연결 정보: DB 서버/계정 변경
[2] 드라이버: DB 종류 변경
[3] SQL: 쿼리/스키마 변경
[4] 자원 해제: (거의 불변)
[5] 예외 처리: 정책 변경

→ 각자 다른 이유
→ 한 곳에 모이면 안 됨

1.4 ILIC 의 맥락

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. 5가지:

  • 연결 정보
  • 드라이버 로딩
  • SQL 작성/실행
  • 자원 해제
  • 예외 처리
  1. 변경 이유:

    • 각자 다름
  2. 문제:

    • 한 곳에 모임
  3. 결과:

    • 변경 영향 확산

2️⃣ 변경 1번 = 수정 N곳

2.1 핵심 문제

변경 1번 = 수정 N곳:

  한 가지 변경 (예: DB 종류):
    - 모든 메서드에 박힘
    - 메서드 N개 모두 수정

→ 변경 비용 = 메서드 수

2.2 변경 시나리오

변경 시나리오:

DB 종류 변경:
  → 드라이버/URL 박힌 N곳 수정

접속 정보 변경:
  → 연결 정보 박힌 N곳 수정

SQL 정책 변경:
  → 해당 SQL 수정 (그나마 분리적)

2.3 수정 비용

수정 비용:

  메서드 10개:
    - DB 변경 = 10곳 수정
    - 시간 ↑
    - 실수 가능
    - 누락 위험

  메서드 100개:
    - 100곳 (악몽)

2.4 누락 위험

누락 위험:

  N곳 수정 중:
    - 하나 빠뜨림
    - 일부는 옛 설정
    - 일부는 새 설정
    - 정합성 깨짐 (버그)

2.5 ILIC 의 맥락

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곳 모두 수정
    // 하나 빠뜨리면 → 그 메서드만 연결 실패 (버그)
}

2.6 자기 점검 답변

"변경 1번 = 수정 N곳" 의 의미는?

:
1. 핵심:

  • 한 변경 → 모든 메서드
  1. 시나리오:

    • DB 종류, 접속 정보
  2. 비용:

    • 메서드 수만큼
  3. 누락 위험:

    • 일부 빠뜨림 → 버그

3️⃣ 중복이 미관 문제가 아닌 이유

3.1 중복의 진짜 위험

중복의 진짜 위험:

  단순 미관 X:
    - 변경 비용 (N곳 수정)
    - 정합성 위험 (누락)
    - 버그 확산
    - 이해 비용

3.2 변경 비용

변경 비용:

  중복 코드:
    - 한 곳 변경
    - 다른 곳도 수동 반영
    - 시간 + 실수

  단일 코드:
    - 한 곳만 변경
    - 자동 반영

3.3 정합성 위험

정합성 위험:

  중복 N곳:
    - 일부만 변경
    - 나머지 옛 코드
    - 불일치 (버그)

  "어디가 진짜?"

3.4 버그 확산

버그 확산:

  중복 코드에 버그:
    - 모든 복사본에 버그
    - 하나 고쳐도 나머지 버그
    - 추적 어려움

3.5 DRY 원칙

DRY (Don't Repeat Yourself):

  "모든 지식은 시스템 내에서
   단 하나의 명확한 표현을 가져야"

  중복 제거:
    - 변경 한 곳
    - 정합성 보장

3.6 ILIC 의 맥락

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)
}

3.7 자기 점검 답변

코드 중복이 단순 미관 문제가 아닌 이유는?

:
1. 진짜 위험:

  • 변경 비용
  • 정합성
  1. 변경 비용:

    • N곳 수동 반영
  2. 정합성:

    • 일부만 변경 → 불일치
  3. 버그 확산:

    • 모든 복사본에

4️⃣ SRP 위반

4.1 SRP

SRP (Single Responsibility Principle):

  "클래스/메서드는 단 하나의 책임만"
  - 변경의 이유가 하나
  - 한 가지 일만

4.2 위반

전통 DAO SRP 위반:

  한 메서드:
    - 5가지 책임
    - 5가지 변경 이유

  → SRP 위반

4.3 변경 이유로 판단

변경 이유로 SRP 판단:

  add 메서드 변경 이유:
    1. DB 종류 변경
    2. 접속 정보 변경
    3. SQL 변경

  → 변경 이유 여럿
  → SRP 위반

4.4 SRP 준수하면

SRP 준수:

  연결 관리 → ConnectionMaker
  SQL 실행 → DAO
  설정 → 외부

  각자 하나의 책임
  변경 이유 하나

4.5 ILIC 의 맥락

// ❌ 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 만 (자기 책임)
    }
}

4.6 자기 점검 답변

SOLID의 SRP 위반은?

:
1. SRP:

  • 단일 책임
  • 변경 이유 하나
  1. 위반:

    • 5가지 책임
  2. 판단:

    • 변경 이유 여럿
  3. 준수:

    • 책임 분리

5️⃣ OCP 위반

5.1 OCP

OCP (Open-Closed Principle):

  "확장에는 열려있고
   변경에는 닫혀있어야"

  - 새 기능 = 추가 (확장)
  - 기존 코드 = 안 건드림 (닫힘)

5.2 위반

전통 DAO OCP 위반:

  새 DB 지원하려면:
    - 기존 코드 수정
    - 드라이버/URL 변경

  → 변경에 열림 (위반)
  → 확장에 닫힘

5.3 if-else 신호

// OCP 위반 신호 — 분기
public Connection getConnection(String dbType) {
    if (dbType.equals("MySQL")) {
        return mysqlConnection();
    } else if (dbType.equals("Oracle")) {
        return oracleConnection();
    }
    // 새 DB 추가 시 → 이 메서드 수정 (OCP 위반)
    return null;
}

5.4 OCP 준수하면

OCP 준수:

  새 DB = 새 구현 추가:
    - ConnectionMaker 구현
    - 기존 코드 변경 X

  → 확장 (추가)
  → 닫힘 (수정 X)

5.5 ILIC 의 맥락

// ❌ 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 (닫힘)

5.6 자기 점검 답변

SOLID의 OCP 위반은?

:
1. OCP:

  • 확장 열림, 변경 닫힘
  1. 위반:

    • 새 DB = 기존 수정
  2. 신호:

    • if-else 분기
  3. 준수:

    • 새 구현 추가

6️⃣ DIP 위반

6.1 DIP

DIP (Dependency Inversion Principle):

  "구체가 아닌 추상에 의존하라"
  - 고수준 모듈이 저수준에 의존 X
  - 둘 다 추상화에 의존

6.2 위반

전통 DAO DIP 위반:

  DAO 가 직접:
    - DriverManager (구체)
    - 특정 드라이버 (구체)

  → 구체에 의존 (위반)

6.3 구체 의존

// DIP 위반 — 구체 의존
public class ShipmentDao {
    public void add(Shipment s) throws Exception {
        // 구체 클래스 직접 의존
        Class.forName("com.mysql.jdbc.Driver");   // 구체
        Connection c = DriverManager.getConnection(...);  // 구체
        // DAO 가 특정 DB 구현에 묶임
    }
}

6.4 DIP 준수하면

// 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)

6.5 ILIC 의 맥락

// 진화 방향: 구체 의존 → 추상 의존

// ❌ 현재 (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 (역전)

6.6 자기 점검 답변

SOLID의 DIP 위반은?

:
1. DIP:

  • 추상에 의존
  1. 위반:

    • DriverManager 구체 의존
  2. 구체 의존:

    • 특정 DB 묶임
  3. 준수:

    • ConnectionMaker (추상)

7️⃣ 유지보수 지옥

7.1 유지보수 지옥

유지보수 지옥:

  변경 1번:
    - 여러 곳 수정
    - 실수 가능
    - 누락 위험
    - 테스트 부담

  → 변경이 두려운 코드

7.2 구체적 모습

구체적 모습:

  "DB 비밀번호 바꿔야 해"
    → DAO 메서드 20개 다 열어서
    → 일일이 수정
    → 하나 빠뜨림
    → 그 메서드만 장애
    → 디버깅 (어디 빠뜨렸지?)

  → 단순 변경이 큰 작업

7.3 변경 두려움

변경 두려움:

  코드 건드리기 무서움:
    - 어디 영향?
    - 뭐가 깨지지?
    - 테스트 부담

  → 개선 안 함 (악순환)

7.4 해결 방향

해결 방향 (다음 Phase):

Phase 4: 관심사 분리
  - 메서드 추출
  - 추상 클래스

Phase 5: 디자인 패턴
  - 템플릿 메소드/팩토리

Phase 6: OCP/전략 패턴
  - 인터페이스

Phase 7~8: IoC/DI
  - 외부 주입

7.5 ILIC 의 맥락

// 유지보수 지옥의 모습
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 에서 단계적 해결
}

7.6 자기 점검 답변

유지보수 지옥의 구체적 모습은?

:
1. 지옥:

  • 변경 1번 → 여러 곳
  1. 구체적:

    • 비밀번호 변경 = 20곳
    • 누락 → 장애
  2. 변경 두려움:

    • 영향 불안
  3. 해결:

    • Phase 4~8 단계적

8️⃣ Phase 3 완주 정리

8.1 Phase 3 학습 종합

Phase 3 — 전통 DAO의 문제

Unit 3.1 — DAO란 무엇인가
  - 데이터 접근 전담
  - 비즈니스 분리

Unit 3.2 — 전통 DAO의 코드
  - 6단계, 5가지 관심사
  - 중복

Unit 3.3 — 책임 혼재
  - 변경 N곳
  - SOLID 위반

8.2 핵심 메시지

Phase 3 핵심 메시지:

  "Spring 없이 짠 DAO 는
   모든 책임을 한 곳에 떠안아
   변경 1번에 N곳을 수정하는
   유지보수 지옥이다.
   → 이것을 해결하는 게 Spring 의 정신"

8.3 SOLID 위반 정리

전통 DAO SOLID 위반:

SRP: 5가지 책임 한 메서드
OCP: 새 DB = 기존 수정
DIP: 구체(DriverManager) 의존

→ 다음 Phase 에서 해결

8.4 다음 Phase 예고

Phase 3 → Phase 4:
  - 문제 진단 → 관심사 분리

Phase 4 — 관심사의 분리 (★ 깊이):
  - 관심사 분리 개념
  - 메서드 추출
  - 추상 클래스 확장

8.5 자기 점검 답변

Phase 3의 종합은?

:
1. 3 Unit:

  • DAO 개념
  • 전통 코드
  • 책임 혼재
  1. 메시지:

    • 유지보수 지옥
    • Spring 이 해결
  2. SOLID 위반:

    • SRP/OCP/DIP
  3. 다음:

    • 관심사 분리

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
섞인 책임?연결정보/드라이버/SQL/해제/예외
변경 1번 = N곳?모든 메서드 수정
중복 위험?변경 비용, 정합성
SRP 위반?5가지 책임
OCP 위반?새 DB = 기존 수정
DIP 위반?구체 의존
유지보수 지옥?변경 두려움
DRY?중복 제거
해결?관심사 분리 (Phase 4)
Spring 정신?책임 분리

9.2 자기 점검 체크리스트

책임

  • 5가지

변경 N곳

  • 수정 비용

중복

  • 미관 아님

SOLID

  • SRP
  • OCP
  • DIP

유지보수

  • 지옥

Phase 3

  • 종합

9.3 추가 심화 질문

Q1: SOLID 5원칙 전체?

답:

  • SRP: 단일 책임
  • OCP: 개방폐쇄
  • LSP: 리스코프 치환
  • ISP: 인터페이스 분리
  • DIP: 의존 역전

Q2: 응집도와 결합도?

답:

  • 응집도: 한 모듈 내 관련성 (높을수록 좋음)
  • 결합도: 모듈 간 의존 (낮을수록 좋음)
  • 전통 DAO: 낮은 응집(여러 책임), 높은 결합(구체 의존)
  • 목표: 높은 응집, 낮은 결합

Q3: 변경의 이유로 책임 나누기?

답:

  • SRP 의 핵심
  • "이 코드가 바뀌는 이유는?"
  • 이유가 여럿 → 분리
  • 한 이유 = 한 책임

Q4: 기술 부채?

답:

  • 빠른 개발로 쌓인 부담
  • 책임 혼재 = 기술 부채
  • 나중에 갚음 (리팩토링)
  • 이자 (변경 비용 ↑)

Q5: 리팩토링 타이밍?

답:

  • 변경이 잦은 곳
  • 중복 발견 시
  • 새 기능 추가 전
  • 테스트 있을 때

🎯 핵심 요약 — 3줄 정리

1. 섞인 책임

  • 연결정보/드라이버/SQL/자원해제/예외 (5가지)
  • 변경 이유 각자 다른데 한 곳에

2. 변경의 고통

  • 변경 1번 = 수정 N곳 (유지보수 지옥)
  • 중복은 미관이 아닌 변경 비용 + 정합성 위험

3. SOLID 위반

  • SRP (여러 책임), OCP (새 DB = 수정), DIP (구체 의존)
  • → 다음 Phase 부터 관심사 분리로 해결

🏆 Phase 3 완주 — 전통 DAO의 문제

🌱 Phase 3 — 전통 DAO의 문제
  ✅ Unit 3.1 DAO란 무엇인가
  ✅ Unit 3.2 전통 DAO의 코드
  ✅ Unit 3.3 책임 혼재 ← 여기, Phase 3 완주

→ DAO 개념
→ 전통 코드의 문제 (5가지 책임)
→ SOLID 위반 (SRP/OCP/DIP)

📚 다음으로...

Phase 4 — 관심사의 분리 (★ 깊이)

Phase 4 — 관심사의 분리 (★ 깊이 파기)
  Unit 4.1 — 관심사 분리 개념
  Unit 4.2 — 1단계: 메서드 추출
  Unit 4.3 — 2단계: 추상클래스로 확장

5주차 누적 진행

✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
  ✅ Phase 3 — 전통 DAO (3 Unit) ← 완주
  ⏭ Phase 4 — 관심사의 분리 (3 Unit, ★ 깊이)

총: 10/26 Unit

🏆 Phase 3 완주 — 전통 DAO 문제 진단

profile
Software Developer

0개의 댓글