오브젝트 실전 강의 교재¶
9장 — 유연한 설계¶
원서: 조영호 『오브젝트』 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 예시 → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리)
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 8장의 의존성 관리를 SOLID 의 OCP·DIP 로 체계화.
- 생성과 사용 분리 — 객체 생성을 별도 책임으로.
- Factory 패턴 + DI 의 조합.
- "유연성은 필요할 때만" — 추측성 일반화 회피.
0.2 큰 그림 — 유연성의 도구 사다리¶
[ 1단 ] [ 2단 ] [ 3단 ]
OCP (개방-폐쇄) DIP (의존성 역전) 생성/사용 분리
추상 의존 추상 ↔ 구체 의존 뒤집기 Factory + DI
새 구현 추가에 무변경
비유 — "콘센트 표준"
집집마다 콘센트 모양이 제각각이라고 상상해 봅시다. 새 가전을 살 때마다 거기에 맞는 전용 플러그를 따로 깎아야 하니 여간 번거로운 게 아닙니다. 다행히 콘센트는 표준 규격으로 정해져 있어서, 어떤 가전을 사 와도 그대로 벽에 꽂으면 작동합니다. 새 제품이 나와도 벽을 뜯어고칠 필요 없이 같은 콘센트에 꽂기만 하면 됩니다.
객체지향의 OCP·DIP 가 바로 이 정신입니다. 콘센트의 표준 규격이 인터페이스에 해당하고, 거기에 꽂히는 가전이 구현에 해당합니다. 표준에만 맞추면 새 구현을 얼마든지 추가할 수 있고, 그때마다 기존 코드는 손대지 않아도 됩니다.
0.3 현업에서 왜 중요한가¶
- SOLID 5원칙 중 OCP·DIP 의 깊이.
- Spring
@Configuration+@Bean이 정확히 9장의 구현. - 추측성 일반화 (악취 entity-refactoring 3.15) 의 정확한 경계.
1. 개방-폐쇄 원칙 (OCP)¶
1.1 정의¶
확장에는 열려 있고, 변경에는 닫혀 있어야 (Bertrand Meyer, 1988).
- 확장에 열림 = 새 기능 (구현·정책) 추가 가능.
- 변경에 닫힘 = 기존 코드는 수정 안 함.
1.2 컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라¶
OCP 의 실천: - 코드 (컴파일타임) 는 추상 에 의존 → 고정. - 실제 객체 (런타임) 는 새 구현으로 교체 → 변경.
public class Movie {
private DiscountPolicy policy; // 컴파일타임 — 추상
}
// 새 정책 추가 — Movie 무변경
public class MembershipPolicy extends DiscountPolicy { ... }
1.3 추상화가 핵심이다¶
OCP 의 도구 = 추상화 (인터페이스·추상 클래스). 추상에 의존해야 새 구현 추가가 OCP 충족.
2. 생성과 사용 분리¶
2.1 객체 생성은 별도 책임¶
// ❌ 생성 + 사용 섞임
public class OrderService {
private final OrderRepository repo = new JpaOrderRepository(...); // 생성
public void process(Order o) {
repo.save(o); // 사용
}
}
// ✅ 분리
public class OrderService {
private final OrderRepository repo;
public OrderService(OrderRepository repo) { this.repo = repo; } // 받음
public void process(Order o) { repo.save(o); } // 사용
}
// 생성 책임은 외부
new OrderService(new JpaOrderRepository(...)); // 또는 Spring @Configuration
2.2 Factory 추가하기¶
생성 책임이 복잡하면 Factory 클래스:
public class MovieFactory {
public Movie createMovie(MovieType type, ...) {
return switch (type) {
case AMOUNT -> new Movie(..., new AmountDiscountPolicy(...));
case PERCENT -> new Movie(..., new PercentDiscountPolicy(...));
case NONE -> new Movie(..., new NoneDiscountPolicy());
};
}
}
→ Movie 사용자는 Factory 만 호출. 정책 종류 모름.
2.3 순수한 가공물에게 책임 할당하기¶
Factory 는 도메인 단어 아닌 순수 가공물 (GRASP 패턴). 도메인에 없어도 책임 분리·재사용 가치로 정당.
3. 의존성 주입 (DI)¶
3.1 세 가지 형태¶
| 형태 | 예 | 권장도 |
|---|---|---|
| 생성자 주입 | new Service(repo) |
★★★ 권장 |
| 세터 주입 | service.setRepo(repo) |
★ 선택적 의존만 |
| 메서드 매개변수 | service.process(order, repo) |
★★ 임시 협력 |
3.2 숨겨진 의존성은 나쁘다¶
// ❌ 숨김 — 정적 호출
public class OrderService {
public void process(Order o) {
SecurityContext.getCurrentUser(); // 정적 — 의존성 코드에 안 보임
}
}
// ✅ 명시
public class OrderService {
private final SecurityContext context;
public OrderService(SecurityContext context) { this.context = context; }
}
→ 생성자에 모든 의존이 명시. 사용자가 한눈에 인지. 테스트 자유.
4. 의존성 역전 원칙 (DIP)¶
4.1 정의¶
상위 모듈이 하위 모듈에 의존하면 안 된다. 둘 다 추상에 의존해야. 추상이 구체에 의존하면 안 된다. 구체가 추상에 의존해야.
4.2 추상화와 의존성 역전¶
[ 전통 ] [ DIP ]
OrderService → JpaRepository OrderService → OrderRepository
(상위) (하위 구체) (상위) (추상)
↑
JpaRepository implements OrderRepository
(하위 구체)
→ 화살표 방향이 뒤집힘 (역전).
4.3 의존성 역전 원칙과 패키지¶
같은 패키지에 추상 (인터페이스) + 구체 둘 다 두면 의존성 역전이 의미 없음. 추상은 상위 패키지에, 구체는 하위 패키지에:
domain/ # OrderService, OrderRepository (interface)
infrastructure/jpa/ # JpaOrderRepository (구체)
infrastructure/redis/ # RedisOrderRepository (구체)
→ domain 패키지는 infrastructure 를 모름. 의존성 화살표 = infrastructure → domain.
5. 유연성에 대한 조언¶
5.1 유연한 설계는 유연성이 필요할 때만 옳다¶
사용 사례 1개로 인터페이스·추상 클래스 도입 = 추측성 일반화 (악취 entity-refactoring 3.15).
YAGNI 정신. 변경이 잦다는 증거 가 있을 때 추상화.
5.2 협력과 책임이 중요하다¶
추상화·OCP·DIP 는 도구. 진짜 목표는 좋은 협력 + 책임 할당. 도구가 목표를 가리지 않도록.
핵심 교훈¶
- OCP — 확장에 열림, 변경에 닫힘. 추상 의존이 도구.
- DIP — 추상이 구체에 의존 X, 구체가 추상에 의존.
- 생성과 사용 분리 — Factory + DI 의 조합.
- 명시적 의존성 (생성자 주입) — 숨김 의존 회피.
- 유연성은 필요할 때만 — 사용 사례 1개로 추상화 X.
현업 예제 — Spring @Configuration¶
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository(DataSource ds) {
return new JpaOrderRepository(ds); // 구체 생성
}
@Bean
public OrderService orderService(OrderRepository repo) {
return new OrderService(repo); // 추상 주입
}
}
→ Spring 컨테이너가 생성 책임 (Factory) + DI 책임 을 자동화. OrderService 는 추상에만 의존, 생성 모름.
함정 / 주의¶
- 인터페이스 첫 도입 도그마 — 구현이 1개뿐이면 의미 없음. Effective Java Item 20 (인터페이스 우선) 도 다중 구현 가능성 있을 때.
- DI 컨테이너 (Spring) 없이 테스트 불가 = 너무 의존. 도메인 객체는 Spring 없이도 테스트 가능해야.
- 모든 곳에 Factory = 과한 설계. 단순 생성은 직접
newOK. - DIP 의 패키지 분리 가 모놀리스에는 과할 수 있음 — 충분히 큰 시스템에 도입.
체크리스트¶
- 새 구현 추가 시 기존 코드 무변경인가 (OCP)
- 도메인 객체가 인프라 (DB·HTTP) 에 직접 의존하는가 (DIP 위배)
- 생성 코드와 사용 코드가 분리되어 있는가
- 단일 구현 인터페이스를 도입하지 않았는가 (추측성 일반화)
- Spring 없이 도메인 객체를 테스트 가능한가
퀴즈¶
- OCP 를 한 문장으로?
- DIP 가 의존성 화살표를 뒤집는다는 의미?
- 생성과 사용 분리 의 이점?
- "유연성은 필요할 때만" 의 실천 기준?
- Spring
@Configuration이 9장의 어떤 원리?
정답·해설¶
- 확장에 열림, 변경에 닫힘. 새 기능 추가는 새 클래스로 (확장), 기존 코드는 수정 안 함 (닫힘). 도구는 추상화.
- 전통은 상위 → 하위 구체. DIP 는 상위 → 추상 ← 구체 (역전). 구체가 추상을 구현 (의존). 결과: 상위 모듈이 하위 구체에 결합 X.
- (1) 사용 코드가 단순 — 생성 책임 X. (2) 생성 정책 교체 자유 — 운영/테스트/다른 환경. (3) 테스트 mock 주입 가능.
- 사용 사례 2개 이상 + 변경 잦다는 증거. 1개로 인터페이스 도입 = 추측성 일반화. 리팩터링 "3의 법칙" — 세 번째에 추상화.
- 생성과 사용 분리 + DI + Factory.
@Configuration+@Bean이 Factory, 컨테이너가 DI 자동. 사용자 (Service) 는 추상에만 의존.
다음 장 예고 — 10장: 상속과 코드 재사용¶
상속을 통한 재사용의 매력 — 그러나 함정. 취약한 기반 클래스 문제, DRY 의 함정, 차이에 의한 프로그래밍. 11장 "합성과 유연한 설계" 의 전제.