F-LAB JAVA · 5주차 · Phase 5 · 디자인 패턴의 적용
🏆 Phase 5 완주 — 상속의 한계와 합성으로의 전환
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
템플릿 메소드·팩토리 메소드 패턴은 상속을 기반으로 하기에 단일 상속 제약·컴파일 타임 결합·상속의 깨지기 쉬움이라는 한계가 있으며, 이를 극복하려면 "상속보다 합성 (Composition over Inheritance)" 원칙에 따라 인터페이스 + 합성으로 전환해야 한다.
상속 기반의 세 가지 단점은 (1) 단일 상속 제약 — UserDao 를 상속하면 다른 클래스를 상속할 수 없고, (2) 컴파일 타임 결합 — 새 DB 를 추가하려면 새 클래스를 만들어 컴파일해야 하며, (3) 상속 관계의 깨지기 쉬움 — 부모가 변경되면 모든 자식이 영향을 받는다.
이 한계의 근본 원인은 상속이 컴파일 시점에 부모-자식 관계가 고정되는 강한 결합 이기 때문이다.
"Composition over Inheritance" 는 상속 (is-a, 강결합) 대신 합성 (has-a, 느슨한 결합) 을 우선하라는 원칙으로, 변하는 부분을 별도 객체로 분리해 보유 (합성) 하면 런타임에 교체할 수 있다.
따라서 다음 Phase 에서는 getConnection 을 추상 메서드 대신 ConnectionMaker 인터페이스 로 분리하고 이를 주입 (합성) 받아, 상속의 한계를 극복한다.
상속 vs 합성 = 붙박이 가구 vs 모듈 가구:
상속 (붙박이 — is-a):
- 책장이 벽에 붙박이
- 한 벽에만 (단일 상속)
- 벽 바꾸면 책장 다 영향 (깨지기 쉬움)
- 설치 시 고정 (컴파일 타임)
합성 (모듈 — has-a):
- 책장을 방에 "놓음" (보유)
- 여러 가구 조합 가능
- 책장만 교체 가능 (런타임)
- 독립적 (느슨한 결합)
Composition over Inheritance:
- 붙박이보다 모듈 우선
- 유연한 조합
인터페이스 + 합성:
- "연결 담당" 을 부품으로 (인터페이스)
- DAO 가 보유 (합성)
- 부품 교체 가능
→ 상속(붙박이) 한계 → 합성(모듈) 전환, Composition over Inheritance.
1. 상속 분리의 단점 개요
2. 단일 상속 제약
3. 컴파일 타임 결합
4. 깨지기 쉬움 (fragile base class)
5. Composition over Inheritance
6. 상속(is-a) vs 합성(has-a)
7. 인터페이스로 대체하면
8. Phase 5 완주 정리
9. 면접 + 자기 점검
상속 기반 분리 단점:
1. 단일 상속 제약
- 한 클래스만 상속
2. 컴파일 타임 결합
- 새 클래스 컴파일
3. 깨지기 쉬움
- 부모 변경 → 자식 영향
근본 원인:
상속 = 강한 결합:
- 컴파일 시 고정
- 부모-자식 묶임
- 유연성 ↓
템플릿/팩토리 공통 한계:
둘 다 상속 기반:
- 서브클래스 만듦
- 상속 단점 공유
→ 같은 한계
// 상속 기반 (한계 내포)
public abstract class ShipmentDao {
protected abstract Connection getConnection() throws Exception;
}
// 새 DB = 새 서브클래스 (상속)
class MySqlShipmentDao extends ShipmentDao { /* ... */ }
class OracleShipmentDao extends ShipmentDao { /* ... */ }
// 한계:
// 1. ShipmentDao 상속 → 다른 클래스 못 상속
// 2. 새 DB → 새 클래스 컴파일
// 3. ShipmentDao 변경 → 모든 자식 영향
상속을 이용한 분리의 단점 3가지는?
답:
1. 세 단점:
근본 원인:
공통:
상속 기반:
단일 상속 제약:
자바:
- 클래스 하나만 상속
- extends 하나
ShipmentDao 상속하면:
- 다른 클래스 상속 불가
// 단일 상속 문제
public class CustomerShipmentDao
extends ShipmentDao { // ShipmentDao 상속
// extends BaseEntity // ❌ 불가 (이미 상속)
// extends CommonRepository // ❌ 불가
}
// 공통 기능 상속 못 함
상속 슬롯 낭비:
상속 = 1회용 슬롯:
- 연결 위해 써버림
- 다른 용도 불가
→ 귀한 상속을 연결에 소비
// 합성은 여러 개 가능
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 합성 1
private final QueryLogger queryLogger; // 합성 2
private final CacheManager cacheManager; // 합성 3
// 여러 협력 객체 보유 (제약 X)
}
// 단일 상속 한계
public abstract class ShipmentDao {
protected abstract Connection getConnection() throws Exception;
}
// 고객사 DAO 가 다른 공통 베이스도 상속하고 싶다면?
public class CustomerShipmentDao extends ShipmentDao {
// extends AuditableEntity // ❌ 이미 ShipmentDao 상속
// → 감사 기능 상속 불가
}
// ✓ 합성으로 해결 (여러 개)
public class ShipmentDaoComposed {
private final ConnectionMaker connectionMaker; // 연결
private final AuditLogger auditLogger; // 감사
private final MetricsCollector metrics; // 메트릭
// 여러 기능 조합 (단일 상속 제약 X)
public ShipmentDaoComposed(ConnectionMaker cm,
AuditLogger al,
MetricsCollector mc) {
this.connectionMaker = cm;
this.auditLogger = al;
this.metrics = mc;
}
}
interface ConnectionMaker { Connection makeConnection(); }
interface AuditLogger { void log(String s); }
interface MetricsCollector { void collect(String s); }
단일 상속 제약의 문제는?
답:
1. 단일 상속:
문제:
슬롯 낭비:
합성:
컴파일 타임 결합:
상속 관계:
- 컴파일 시 고정
- 런타임 변경 X
새 DB:
- 새 클래스 작성
- 컴파일
- 배포
정적 결정:
extends 는 컴파일 타임:
- MySqlShipmentDao extends ShipmentDao
- 코드에 고정
→ 런타임 교체 불가
새 DB 추가 (상속):
1. 새 클래스 작성
class PostgreSqlShipmentDao extends ShipmentDao
2. getConnection 구현
3. 컴파일
4. 배포
→ 코드 + 컴파일 필요
// 합성은 런타임 교체
ShipmentDao dao = new ShipmentDao(mysqlConnectionMaker); // 런타임 결정
// 또는
ShipmentDao dao = new ShipmentDao(oracleConnectionMaker); // 런타임
// 설정/조건에 따라 런타임에 주입
ConnectionMaker cm = loadFromConfig(); // 동적
ShipmentDao dao = new ShipmentDao(cm);
// 컴파일 타임 (상속)
// 새 DB → 새 클래스 컴파일
class PostgreSqlShipmentDao extends ShipmentDao {
protected Connection getConnection() { return null; }
}
// 컴파일 + 배포 필요
// ✓ 런타임 (합성)
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm; // 런타임 주입
}
}
// 설정으로 런타임 결정
String dbType = config.get("db.type");
ConnectionMaker cm = switch (dbType) {
case "mysql" -> new MySqlConnectionMaker();
case "postgres" -> new PostgreSqlConnectionMaker();
default -> throw new IllegalArgumentException();
};
ShipmentDao dao = new ShipmentDao(cm); // 런타임 조립
// 코드 수정/컴파일 없이 설정만으로
interface ConnectionMaker { Connection makeConnection(); }
class MySqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
class PostgreSqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
컴파일 타임 결합의 문제는?
답:
1. 컴파일 타임:
정적 결정:
새 DB:
합성:
깨지기 쉬운 베이스 클래스:
부모(베이스) 변경:
- 모든 자식 영향
- 예상치 못한 버그
→ 부모 수정이 위험
강한 결합:
상속:
- 자식이 부모 내부 의존
- 부모 구현 변경 시
- 자식 깨질 수 있음
→ 캡슐화 약화
// 부모 변경이 자식 깨뜨림
public abstract class ShipmentDao {
public void add(Shipment s) throws Exception {
Connection c = getConnection();
validate(s); // 부모가 validate 추가 (변경)
// ...
}
protected void validate(Shipment s) { } // 새 메서드
}
// 자식이 우연히 같은 이름 메서드 있으면?
class CustomerShipmentDao extends ShipmentDao {
protected Connection getConnection() { return null; }
// protected void validate(...) 충돌/오버라이드 의도치 않게
}
// 부모 변경 → 자식 예상치 못한 동작
캡슐화 약화:
상속:
- 자식이 부모 protected 접근
- 내부 노출
- 변경 영향 ↑
합성:
- public 인터페이스만
- 내부 숨김
- 변경 격리
// 깨지기 쉬운 상속
public abstract class ShipmentDao {
// 부모에 메서드 추가 (변경)
public void add(Shipment s) throws Exception {
Connection c = getConnection();
beforeSave(s); // 새로 추가한 훅
}
protected void beforeSave(Shipment s) { } // 추가
protected abstract Connection getConnection() throws Exception;
}
// 기존 자식이 영향 받을 수 있음
class ExistingShipmentDao extends ShipmentDao {
protected Connection getConnection() { return null; }
// beforeSave 가 새로 추가됨 → 동작 변경 가능성
}
// ✓ 합성은 안전
public class ShipmentDaoSafe {
private final ConnectionMaker connectionMaker; // 인터페이스 (계약)
// ConnectionMaker 인터페이스만 의존
// 구현 변경되어도 계약 같으면 안전
public ShipmentDaoSafe(ConnectionMaker cm) {
this.connectionMaker = cm;
}
}
interface ConnectionMaker { Connection makeConnection(); }
상속의 깨지기 쉬움 (fragile base class) 은?
답:
1. fragile base class:
강한 결합:
예시:
캡슐화:
Composition over Inheritance:
"상속보다 합성을 우선하라"
- 상속 (is-a): 강결합
- 합성 (has-a): 느슨
→ 합성 권장
의미:
기능 재사용/확장 시:
- 상속보다
- 합성 (객체 보유)
→ 유연성 ↑
왜 합성:
- 런타임 교체
- 다중 조합
- 느슨한 결합
- 캡슐화 유지
- 테스트 용이
// 합성 (객체 보유)
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // has-a (합성)
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm; // 주입
}
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection(); // 위임
}
}
// 상속 대신 보유 + 위임
// Composition over Inheritance 적용
// ❌ 상속 (강결합)
public abstract class ShipmentDaoInheritance {
protected abstract Connection getConnection() throws Exception;
}
// ✓ 합성 (느슨)
public class ShipmentDaoComposition {
private final ConnectionMaker connectionMaker; // 합성
public ShipmentDaoComposition(ConnectionMaker cm) {
this.connectionMaker = cm; // 주입 (런타임)
}
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection(); // 위임
// ...
}
}
// 효과:
// - 런타임 ConnectionMaker 교체
// - 단일 상속 제약 X
// - 부모 변경 영향 X (인터페이스 계약)
// → 다음 Phase (전략 패턴)
interface ConnectionMaker { Connection makeConnection() throws Exception; }
"Composition over Inheritance" 원칙은?
답:
1. 원칙:
의미:
왜:
예시:
관계 차이:
상속 (is-a):
- "MySqlDao 는 ShipmentDao 다"
- 종류 관계
합성 (has-a):
- "ShipmentDao 는 ConnectionMaker 를 가진다"
- 보유 관계
// is-a (상속)
class MySqlShipmentDao extends ShipmentDao {
// MySqlShipmentDao IS-A ShipmentDao
}
// has-a (합성)
class ShipmentDao {
private ConnectionMaker connectionMaker;
// ShipmentDao HAS-A ConnectionMaker
}
선택 기준:
상속 (is-a):
- 진짜 종류 관계
- 부모 모두 물려받음
합성 (has-a):
- 기능 사용/보유
- 유연성 필요
- 대부분 권장
잘못된 상속:
코드 재사용만 위해 상속:
- is-a 아닌데 상속
- 강결합
- 깨지기 쉬움
→ 합성이 맞음
// is-a 판단
// "ShipmentDao 는 ConnectionMaker 인가?" → NO (is-a X)
// → 상속 부적절
// "ShipmentDao 는 ConnectionMaker 를 가지는가?" → YES (has-a)
// → 합성 적절
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // has-a
// ShipmentDao 가 연결 기능을 "사용" (보유)
// 연결의 한 종류가 아님
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm;
}
}
// 연결은 기능 (has-a) → 합성이 자연스러움
interface ConnectionMaker { Connection makeConnection() throws Exception; }
상속(is-a) vs 합성(has-a)의 차이는?
답:
1. is-a:
has-a:
선택:
잘못된 상속:
인터페이스 + 합성:
변하는 부분:
- 인터페이스로 추상화
- DAO 가 보유 (합성)
- 주입
→ 상속 한계 극복
인터페이스로 대체하면:
1. 다중 구현
- 단일 상속 X
2. 런타임 교체
- 컴파일 타임 X
3. 느슨한 결합
- 깨지기 쉬움 X
4. 테스트
- Mock 구현
// 인터페이스 + 합성
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 인터페이스 보유
public ShipmentDao(ConnectionMaker cm) {
this.connectionMaker = cm; // 주입
}
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection();
}
}
// 구현체들
class MySqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
class OracleConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
전략 패턴 예고:
인터페이스 + 합성 + 주입:
= 전략 패턴
- Strategy: ConnectionMaker
- Context: ShipmentDao
- 주입
→ Phase 6
// 인터페이스 + 합성 (Phase 6 미리보기)
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker; // 합성 + 주입
}
public void add(Shipment shipment) throws Exception {
Connection c = connectionMaker.makeConnection();
// SQL...
}
}
// 좋아진 점:
// 1. ShipmentDao 가 다른 클래스 상속 가능 (단일 상속 X)
// 2. 런타임에 ConnectionMaker 교체
// 3. ConnectionMaker 구현 변경되어도 계약 같으면 안전
// 4. Mock ConnectionMaker 로 테스트
// 고객사별 (런타임 주입)
ConnectionMaker cm = new MySqlConnectionMaker();
ShipmentDao dao = new ShipmentDao(cm);
// → 전략 패턴 (Phase 6)
class MySqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
상속을 인터페이스로 대체하면 좋아지는 것은?
답:
1. 다중 구현:
런타임 교체:
느슨한 결합:
테스트:
Phase 5 — 디자인 패턴의 적용
Unit 5.1 — 템플릿 메소드 패턴
- 흐름(부모) + 변동(자식)
- Phase 4.3 의 정체
Unit 5.2 — 팩토리 메소드 패턴
- 객체 생성 위임
- 같은 코드 두 패턴
Unit 5.3 — 두 패턴의 한계
- 상속 단점
- 합성으로
Phase 5 핵심 메시지:
"Phase 4 의 리팩토링은
템플릿/팩토리 메소드 패턴이었다.
하지만 상속 기반이라 한계가 있고,
인터페이스 + 합성으로 넘어가야 한다."
진화 과정:
추상클래스 (Phase 4.3)
↓ 패턴 인식
템플릿/팩토리 메소드 (Phase 5.1~5.2)
↓ 한계 발견
인터페이스 + 합성 (Phase 5.3 → Phase 6)
Phase 5 → Phase 6:
- 상속 한계 → 인터페이스/전략
Phase 6 — OCP & 전략 패턴 (6.2 ★깊이):
- 인터페이스로 결합도 ↓
- OCP (개방폐쇄)
- 전략 패턴
Phase 5의 종합은?
답:
1. 3 Unit:
메시지:
진화:
다음:
| Q | 핵심 답변 |
|---|---|
| 상속 단점 3가지? | 단일상속/컴파일타임/깨지기쉬움 |
| 단일 상속? | 다른 베이스 불가 |
| 컴파일 타임? | 새 클래스 컴파일 |
| 깨지기 쉬움? | 부모 변경 → 자식 영향 |
| Composition over Inheritance? | 합성 우선 |
| is-a vs has-a? | 종류 vs 보유 |
| 인터페이스 대체? | 다중/런타임/느슨 |
| 합성 장점? | 유연, 교체 |
| 다음? | 전략 패턴 |
| Phase 5? | 패턴 → 합성 |
답:
답:
답:
답:
답:
1. 상속의 세 단점
2. Composition over Inheritance
3. 인터페이스 + 합성
🌱 Phase 5 — 디자인 패턴의 적용
✅ Unit 5.1 템플릿 메소드 패턴
✅ Unit 5.2 팩토리 메소드 패턴
✅ Unit 5.3 두 패턴의 한계 ← 여기, Phase 5 완주
→ 템플릿 메소드 (흐름/변동)
→ 팩토리 메소드 (객체 생성)
→ 상속 한계 → 합성으로
Phase 6 — OCP & 전략 패턴 (6.2 ★깊이)
Unit 6.1 — 인터페이스로 결합도 낮추기
Unit 6.2 — OCP (개방폐쇄원칙) ★깊이
Unit 6.3 — 전략 패턴
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3 — 전통 DAO (3 Unit)
✅ Phase 4 — 관심사의 분리 (3 Unit)
✅ Phase 5 — 디자인 패턴 (3 Unit) ← 완주
⏭ Phase 6 — OCP & 전략 패턴 (3 Unit)
총: 16/26 Unit
🏆 Phase 5 완주 — 상속의 한계와 합성으로의 전환