F-LAB JAVA · 5주차 · Phase 6 · 객체지향 설계 원칙 (OCP & 전략 패턴)
🌱 Phase 6 시작 — 인터페이스 + 합성의 본격 도입
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
ConnectionMaker 인터페이스를 도입하고 ShipmentDao 가 이 인터페이스에만 의존하여 외부에서 구현체를 주입받으면, 어떤 DB 구현체가 와도 ShipmentDao 코드를 변경하지 않아 결합도가 낮아진다.
변하는 부분 (DB 연결) 을ConnectionMaker인터페이스로 추상화하고, ShipmentDao 는 구체 구현이 아닌 인터페이스에만 의존 한다.
구체 구현체 (NConnectionMaker,DConnectionMaker) 는 외부에서 생성자로 주입 받으므로, ShipmentDao 는 어떤 구현체가 오는지 모른 채 인터페이스의 메서드만 호출한다.
그 결과 새로운 DB 를 지원하려면 새 ConnectionMaker 구현체를 추가하기만 하면 되고, ShipmentDao 코드는 전혀 변경되지 않는다 (결합도가 낮아짐).
이때 "어떤 구현체를 쓸지 결정하는 책임" 은 ShipmentDao 가 아니라 외부 (조립하는 곳) 로 넘어가며, 이것이 DIP (의존 역전 원칙) 와 이후 IoC/DI 로 발전한다.
인터페이스 = 표준 충전 단자 (USB-C):
전 (전용 충전기 — 강결합):
- 기기마다 전용 충전기 (상속)
- 충전기 바꾸면 기기 못 씀
후 (USB-C 표준 — 인터페이스):
- 기기는 USB-C 단자만 (인터페이스 의존)
- 어떤 USB-C 충전기든 OK
- 충전기 = ConnectionMaker 구현체
주입:
- 충전기를 "꽂음" (외부 주입)
- 기기는 어떤 충전기인지 모름
- 단자(인터페이스)만 맞으면 OK
결합도 ↓:
- 새 충전기 나와도 기기 그대로
- 기기는 USB-C 만 의존
결정 책임:
- 어떤 충전기 꽂을지 = 사용자(외부)
- 기기가 결정 X
→ 인터페이스 도입 = USB-C 표준, 외부 주입, 결합도 ↓, 새 DB 에도 ShipmentDao 불변.
1. ConnectionMaker 인터페이스 도입
2. 인터페이스에만 의존
3. 외부에서 주입 (생성자)
4. 새 DB에도 변경 안 됨
5. 결합도란
6. 구현체 결정 책임
7. DIP와의 연결
8. 상속에서 합성으로
9. 면접 + 자기 점검
// ConnectionMaker 인터페이스
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
변하는 부분 추상화:
DB 연결 (변하는 부분):
- 인터페이스로 추상화
- makeConnection()
→ 계약만 정의
→ 구현은 별도
// N사 연결 구현
public class NConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
Class.forName("com.mysql.jdbc.Driver");
return DriverManager.getConnection(
"jdbc:mysql://N-server/ilic", "nuser", "npass");
}
}
// D사 연결 구현
public class DConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
// D사 연결 방식
return null;
}
}
추상클래스 (Phase 4) → 인터페이스 (Phase 6):
추상클래스:
- 상속 (단일)
- 강결합
인터페이스:
- 구현 (다중)
- 합성 (주입)
- 느슨
// ConnectionMaker 인터페이스 (변하는 부분)
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
// 고객사별 구현
public class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
return DriverManager.getConnection(
"jdbc:mysql://customerA/ilic", "userA", "passA");
}
}
public class CustomerBConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
return DriverManager.getConnection(
"jdbc:oracle:thin:@customerB:1521:ilic", "userB", "passB");
}
}
// 연결 방식이 인터페이스 구현으로
ConnectionMaker 인터페이스 도입의 효과는?
답:
1. 인터페이스:
추상화:
구현체:
vs 추상클래스:
// ShipmentDao 는 인터페이스에만 의존
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 인터페이스
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection(); // 인터페이스 메서드
// 구체 구현 모름
}
}
구체 모름:
ShipmentDao:
- ConnectionMaker (인터페이스) 만 앎
- NConnectionMaker (구체) 모름
→ 어떤 구현이든 OK
의존의 방향:
ShipmentDao → ConnectionMaker (인터페이스)
↑ 구현
N/DConnectionMaker
- ShipmentDao 는 인터페이스만
- 구체는 인터페이스 구현
효과:
- 구체 변경 무관
- 새 구현 추가 무관
- ShipmentDao 안정
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(); // 인터페이스만
PreparedStatement ps = c.prepareStatement(
"insert into shipments(id, bl_no, weight) values(?, ?, ?)");
ps.setLong(1, shipment.getId());
ps.executeUpdate();
ps.close(); c.close();
}
public Shipment get(Long id) throws Exception {
Connection c = connectionMaker.makeConnection(); // 인터페이스만
// ...
return null;
}
// ShipmentDao 는 어떤 ConnectionMaker 구현인지 모름
// → 구체 무관
}
인터페이스에만 의존한다는 의미는?
답:
1. 인터페이스 의존:
구체 모름:
방향:
효과:
// 생성자 주입
public class ShipmentDao {
private final ConnectionMaker connectionMaker;
public ShipmentDao(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker; // 외부에서 받음
}
}
외부 결정:
구현체 선택:
- ShipmentDao 가 X
- 외부 (조립하는 곳)
→ 생성 시 주입
// 외부에서 조립
ConnectionMaker connectionMaker = new NConnectionMaker(); // 구현 선택
ShipmentDao dao = new ShipmentDao(connectionMaker); // 주입
// D사로 바꾸려면 (ShipmentDao 변경 X)
ConnectionMaker dMaker = new DConnectionMaker();
ShipmentDao dDao = new ShipmentDao(dMaker);
주입의 의미:
의존성 (ConnectionMaker):
- 외부에서 넣어줌 (inject)
- DAO 가 생성 X
→ DI (의존성 주입)
→ Phase 8
// 외부 조립 (main 또는 팩토리)
public class ShipmentApp {
public static void main(String[] args) throws Exception {
// 외부에서 구현체 결정 + 주입
ConnectionMaker connectionMaker = new CustomerAConnectionMaker();
ShipmentDao dao = new ShipmentDao(connectionMaker); // 주입
dao.add(new Shipment());
// 다른 고객사 (ShipmentDao 코드 변경 X)
ConnectionMaker bMaker = new CustomerBConnectionMaker();
ShipmentDao bDao = new ShipmentDao(bMaker); // 다른 구현 주입
// ShipmentDao 는 그대로, 주입만 다름
}
}
// 구현체 선택/조립은 외부 (main)
// → 나중에 IoC 컨테이너가 (Phase 8)
외부에서 주입받기 (생성자) 의 방식은?
답:
1. 생성자 주입:
외부 결정:
조립:
의미:
새 DB → ShipmentDao 불변:
새 DB 지원:
- 새 ConnectionMaker 구현
- ShipmentDao 코드 변경 X
→ 확장에 열림, 변경에 닫힘 (OCP)
// 새 DB = 새 구현만
public class PostgreSqlConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
// PostgreSQL 연결
return null;
}
}
// ShipmentDao 는 전혀 안 건드림
ShipmentDao dao = new ShipmentDao(new PostgreSqlConnectionMaker());
변경 격리:
DB 추가/변경:
- ConnectionMaker 영역만
- ShipmentDao 격리
→ 영향 차단
상속 (Phase 4) 대비:
상속:
- 새 DB = 새 서브클래스
- 단일 상속 제약
- 컴파일 타임
인터페이스 + 합성:
- 새 DB = 새 구현
- 제약 없음
- 런타임 주입
// 새 DB 추가 시나리오
// 기존
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();
}
}
// 새 DB (MariaDB) 추가
// → 새 구현만 작성 (ShipmentDao 안 건드림)
public class MariaDbConnectionMaker implements ConnectionMaker {
public Connection makeConnection() throws Exception {
return DriverManager.getConnection(
"jdbc:mariadb://localhost/ilic", "root", "pass");
}
}
// 사용 (주입만)
ShipmentDao dao = new ShipmentDao(new MariaDbConnectionMaker());
// ShipmentDao 코드 변경 0줄 (OCP)
ShipmentDao가 새 DB 추가에도 변경 안 되는 이유는?
답:
1. 불변:
새 구현만:
격리:
OCP:
결합도 (Coupling):
모듈 간 의존 정도.
- 높으면: 강하게 묶임
- 낮으면: 느슨
→ 낮을수록 좋음
강결합:
구체 클래스 직접 의존:
- new NConnectionMaker()
- 변경 시 영향
→ 묶임
느슨한 결합:
인터페이스 의존:
- ConnectionMaker
- 구현 무관
→ 유연
결합도 낮추기:
1. 인터페이스 의존
2. 주입 (외부 결정)
3. 구체 분리
→ 변경 영향 ↓
응집도와 함께:
좋은 설계:
- 높은 응집도 (관련 모음)
- 낮은 결합도 (모듈 느슨)
인터페이스 + 합성:
- 결합도 ↓
// 강결합 (전)
public class ShipmentDaoTight {
public void add(Shipment s) throws Exception {
// 구체 직접 (강결합)
Connection c = new NConnectionMaker().makeConnection();
// NConnectionMaker 에 묶임
}
}
// 느슨한 결합 (후)
public class ShipmentDaoLoose {
private final ConnectionMaker connectionMaker; // 인터페이스
public ShipmentDaoLoose(ConnectionMaker cm) {
this.connectionMaker = cm; // 주입
}
public void add(Shipment s) throws Exception {
Connection c = connectionMaker.makeConnection(); // 인터페이스
// 어떤 구현인지 모름 (느슨)
}
}
class NConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
결합도 (coupling) 란?
답:
1. 결합도:
강결합:
느슨:
낮추기:
결정 책임 이동:
"어떤 ConnectionMaker?"
전 (DAO 가 결정):
- new NConnectionMaker()
- DAO 가 구체 선택
후 (외부가 결정):
- 외부에서 주입
- DAO 는 모름
누구에게:
구현체 결정:
- ShipmentDao X
- 외부 (조립자)
- main
- 팩토리
- IoC 컨테이너 (나중)
관심사 분리:
ShipmentDao:
- 데이터 접근 (사용)
- 구현 선택 책임 X
외부:
- 구현 선택/조립 책임
→ 책임 분리
// 조립 책임 (외부)
public class DaoFactory {
public ShipmentDao shipmentDao() {
// 구현체 결정 책임 (외부)
return new ShipmentDao(connectionMaker());
}
public ConnectionMaker connectionMaker() {
return new NConnectionMaker(); // 여기서 결정
}
}
// ShipmentDao 는 사용만, DaoFactory 가 조립
// 구현체 결정 책임 = 외부 (DaoFactory)
public class DaoFactory {
// 결정 + 조립 책임
public ShipmentDao shipmentDao() {
return new ShipmentDao(connectionMaker()); // 주입
}
public ConnectionMaker connectionMaker() {
// 어떤 구현? 여기서 결정
return new CustomerAConnectionMaker();
}
public BookingDao bookingDao() {
return new BookingDao(connectionMaker()); // 같은 연결 재사용
}
}
// ShipmentDao 는 결정 책임 없음 (사용만)
// DaoFactory 가 결정/조립
// → IoC 컨테이너로 발전 (Phase 8)
class BookingDao {
BookingDao(ConnectionMaker cm) { }
}
class CustomerAConnectionMaker implements ConnectionMaker {
public Connection makeConnection() { return null; }
}
구현체 결정 책임은 누구에게인가?
답:
1. 이동:
누구:
관심사 분리:
조립 책임:
DIP (Dependency Inversion Principle):
"구체가 아닌 추상에 의존하라"
- 고수준 ↛ 저수준
- 둘 다 추상화에
의존 역전:
전 (직접):
ShipmentDao → NConnectionMaker (구체)
후 (역전):
ShipmentDao → ConnectionMaker (추상)
↑
NConnectionMaker (구체)
→ 구체가 추상에 의존 (역전)
고수준/저수준:
고수준: ShipmentDao (정책)
저수준: 연결 구현 (세부)
DIP:
- 고수준이 저수준 직접 의존 X
- 둘 다 ConnectionMaker (추상)
인터페이스 소유:
ConnectionMaker:
- 고수준 (ShipmentDao) 가 정의
- 저수준이 구현
→ 고수준이 계약 정의
→ 진정한 역전
// DIP 적용
// 추상 (고수준이 정의한 계약)
public interface ConnectionMaker {
Connection makeConnection() throws Exception;
}
// 고수준 (정책) — 추상에 의존
public class ShipmentDao {
private final ConnectionMaker connectionMaker; // 추상 의존
public ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
}
// 저수준 (세부) — 추상 구현
public class NConnectionMaker implements ConnectionMaker { // 추상 구현
public Connection makeConnection() throws Exception {
return null;
}
}
// 의존 방향:
// ShipmentDao (고수준) → ConnectionMaker (추상) ← NConnectionMaker (저수준)
// 고수준이 저수준에 직접 의존 X
// → DIP 달성
DIP (의존 역전 원칙) 와의 연결은?
답:
1. DIP:
역전:
고/저수준:
인터페이스 소유:
상속 → 합성:
Phase 4~5 (상속):
- 추상클래스 상속
- getConnection 오버라이드
Phase 6 (합성):
- ConnectionMaker 보유
- 주입
// 상속 (Phase 4)
abstract class ShipmentDao {
abstract Connection getConnection(); // 상속으로 구현
}
class MySqlShipmentDao extends ShipmentDao {
Connection getConnection() { return null; }
}
// 합성 (Phase 6)
class ShipmentDao {
private final ConnectionMaker connectionMaker; // 보유
ShipmentDao(ConnectionMaker cm) { this.connectionMaker = cm; }
}
// 상속 X, 주입
합성 이점:
- 단일 상속 제약 X
- 런타임 교체
- 느슨한 결합
- 테스트 (Mock)
- 여러 협력 객체
전략 패턴 예고:
ConnectionMaker (전략):
- 변하는 알고리즘 (연결)
ShipmentDao (컨텍스트):
- 전략 사용
→ Unit 6.3 전략 패턴
// 상속 → 합성 전환 완료
// 합성 기반 (Phase 6)
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(); // 위임
// ...
}
}
// 효과 (상속 한계 모두 극복):
// 1. ShipmentDao 가 다른 클래스 상속 가능
// 2. 런타임 ConnectionMaker 교체
// 3. 느슨한 결합
// 4. Mock 으로 테스트
// 5. 여러 협력 객체 (Logger, Metrics 등) 추가 가능
// → 전략 패턴 (Unit 6.3)
interface ConnectionMaker { Connection makeConnection() throws Exception; }
상속에서 합성으로의 전환은?
답:
1. 전환:
코드:
이점:
전략 패턴:
| Q | 핵심 답변 |
|---|---|
| 인터페이스 도입? | ConnectionMaker 추상화 |
| 인터페이스 의존? | 구체 모름 |
| 외부 주입? | 생성자, 조립자 결정 |
| 새 DB 불변? | 새 구현만, DAO 그대로 |
| 결합도? | 모듈 간 의존 (낮을수록 좋음) |
| 결정 책임? | 외부 (조립자) |
| DIP? | 추상에 의존 (역전) |
| 합성 이점? | 제약 X, 런타임 |
| 상속 → 합성? | 보유 + 주입 |
| 전략 패턴? | ConnectionMaker (전략) |
답:
답:
답:
답:
답:
1. 인터페이스 도입
2. 외부 주입 + 결합도
3. DIP와 합성
이번 Unit에서 인터페이스 결합도 낮추기를 봤다면, 다음은 OCP (★ 깊이 파기).
🌱 Phase 6 — OCP & 전략 패턴
✅ Unit 6.1 인터페이스로 결합도 낮추기 ← 여기
⏭ Unit 6.2 OCP (개방폐쇄원칙) ★깊이
⏭ Unit 6.3 전략 패턴
✅ Part A — 동시성 마무리 (7 Unit)
🌱 Part B — 토비의 스프링
✅ Phase 3~5 (9 Unit)
🌱 Phase 6 — OCP & 전략 패턴 (1/3 진행)
총: 17/26 Unit
🌱 Phase 6 시작 — 인터페이스 + 합성