오브젝트 실전 강의 교재¶
8장 — 의존성 관리하기¶
원서: 조영호 『오브젝트』 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 예시 → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리)
0. 이 장을 시작하기 전에¶
0.1 학습 목표¶
- 의존성 의 정확한 정의 — "변경의 길".
- 컴파일타임·런타임 의존성 의 분리가 유연성의 비결.
new의 함정 — 직접 생성은 결합도 폭증.- 명시적 의존성 (생성자 주입) 의 중요성.
- Effective Java Item 5 (DI)·64 (인터페이스 참조) 의 깊이 있는 적용.
0.2 큰 그림 — 의존성이 변경의 통로¶
비유 — "도로망"
도로 (의존성) 가 많을수록 한 곳 공사 (변경) 가 다른 곳에 영향. 도로를 줄이거나·우회로 (인터페이스) 를 만들거나·우회 가능한 큰 도로 (추상) 를 만들면 영향 제한.
0.3 현업에서 왜 중요한가¶
- Spring DI 의 핵심 가치를 깊이 있게 이해.
- 테스트 가능성 = 의존성 명시 + 추상 의존.
- "왜 new 가 해롭다고 하는가" 의 진짜 이유.
1. 의존성 이해하기¶
1.1 변경과 의존성¶
A 가 B 에 의존한다 = B 의 변경이 A 에 영향을 줄 수 있다.
이 정의가 핵심. 단순히 "A 가 B 를 사용" 보다 더 정확.
1.2 의존성 종류¶
- 클래스 의존성: A 가 B 를 필드·매개변수·반환·지역 변수로 사용.
- 메서드 의존성: A 가 B 의 특정 메서드 호출.
- 모듈 의존성: A 가 B 와 같은 모듈에.
1.3 의존성 전이¶
A → B, B → C 면 A 는 C 에 간접 의존. C 변경이 B 거쳐 A 영향 가능.
1.4 런타임 의존성과 컴파일타임 의존성¶
public class Movie {
private DiscountPolicy policy; // 컴파일타임: 추상 DiscountPolicy
public Money fee(Screening s) {
return discount(policy); // 런타임: 실제 AmountDiscountPolicy 인스턴스
}
}
- 컴파일타임 의존성: 코드에 명시된 타입 (
DiscountPolicy). - 런타임 의존성: 실제 협력하는 객체 (
AmountDiscountPolicy).
두 의존성의 분리 = 유연성의 비결.
1.5 컨텍스트 독립성¶
객체가 자신이 사용될 컨텍스트를 모르고 자기 일에만 집중. 외부에서 결정되어 주입.
// ❌ — 자신의 컨텍스트를 안다
public class Movie {
public Money fee(Screening s) {
if (System.getProperty("env").equals("prod")) { // 외부 환경 알기
...
}
}
}
// ✅ — 컨텍스트 독립
public class Movie {
public Money fee(Screening s) {
return fee.minus(discountPolicy.calculateDiscountAmount(s));
// 정책은 외부에서 주입
}
}
1.6 의존성 해결하기¶
런타임 의존성 결정 방식:
1. 생성자 주입 — 권장.
2. 세터 주입 — 선택적 의존만.
3. 메서드 매개변수 — 임시 협력.
4. 직접 생성 (new) — 피해라.
2. 유연한 설계¶
2.1 의존성과 결합도¶
- 의존성 자체는 나쁘지 않음 — 협력이 곧 의존.
- 나쁜 결합도 = 변경의 길이 많고·구체에 의존·내부 구조에 의존.
- 좋은 결합도 = 의존성 최소 + 추상에 의존 + 명시.
2.2 지식이 결합을 낳는다¶
A 가 B 에 대해 알면 알수록 결합도 ↑.
- "B 가 있다" 만 알기 = 약한 결합.
- "B 의 인터페이스만 알기" = 적절한 결합.
- "B 의 내부 구조 알기" = 강한 결합.
2.3 추상화에 의존하라 (DIP)¶
구체 클래스가 아니라 추상에 의존.
// ❌ 구체 의존
public class OrderService {
private final JpaOrderRepository repo = new JpaOrderRepository(); // 구체
}
// ✅ 추상 의존
public class OrderService {
private final OrderRepository repo; // 인터페이스
public OrderService(OrderRepository repo) { this.repo = repo; }
}
→ 구현 교체 자유 + 테스트 mock 주입 자유.
2.4 명시적인 의존성¶
숨겨진 의존성 (전역 변수·싱글턴·정적 호출) 보다 생성자 매개변수로 명시.
// ❌ 숨김
public class OrderService {
public Order create() {
EmailSender.getInstance().send("주문"); // 정적 호출 — 의존성 숨김
}
}
// ✅ 명시
public class OrderService {
private final EmailSender emailSender;
public OrderService(EmailSender emailSender) { this.emailSender = emailSender; }
}
→ 생성자가 의존성을 선언 — 사용자가 한눈에 인지. 테스트 시 mock 주입 가능.
2.5 new 는 해롭다¶
public class OrderService {
public Order process(...) {
EmailSender sender = new EmailSender(host, port, user, password); // ❌
sender.send(...);
}
}
문제: 1. 구체 클래스에 결합 — EmailSender 가 인터페이스 아니라 구체. 2. 생성 매개변수 (host·port) 도 알아야 — 책임 폭증. 3. 테스트 어려움 — mock 주입 불가. 4. 변경 영향 큼 — EmailSender 생성자 변경이 모든 호출자 영향.
2.6 가끔은 생성해도 무방하다¶
- 단순 값 객체 (
new Money(...),new Address(...)) — 가볍고 불변. - 메서드 안 임시 객체 — 외부에 노출 안 되는 헬퍼.
→ 도메인 객체·서비스·외부 리소스 (DB·HTTP) 는 주입, 단순 값·임시 객체는 직접 생성 OK.
2.7 표준 클래스에 대한 의존은 해롭지 않다¶
String·Integer·List·Map 같은 표준 라이브러리는 변경 거의 없음 → 직접 사용 OK.
2.8 컨텍스트 확장하기¶
새 컨텍스트 (테스트·다른 환경) 에 객체 재사용 = 컨텍스트 독립성의 보상.
// 운영
new OrderService(new JpaOrderRepository(...));
// 테스트
new OrderService(new InMemoryOrderRepository());
// 다른 모듈에서 재사용
new OrderService(new RemoteOrderRepository(...));
2.9 조합 가능한 행동¶
작은 의존성 + 추상 의존 = 객체들을 레고처럼 조합 가능.
핵심 교훈¶
- 의존성 = 변경의 길. 의존성 관리가 좋은 설계의 핵심.
- 컴파일타임 vs 런타임 의존성 분리 = 유연성의 비결.
- 추상에 의존 (DIP) — 구체에 결합하지 마라.
- 명시적 의존성 (생성자 주입) — 숨김 의존 (싱글턴·정적) 회피.
new는 신중 — 도메인·서비스·외부 리소스는 주입, 값 객체는 직접 OK.- 컨텍스트 독립성 = 재사용·테스트 자유.
현업 예제 — Spring DI 의 깊이¶
안티패턴 (숨겨진 의존)¶
@Service
public class OrderService {
public void process(Order o) {
// 정적 호출 — 의존성 숨김
DBConnection.execute("INSERT ...");
EmailUtil.send(o.email(), "주문");
LoggerFactory.get(OrderService.class).info("...");
}
}
→ 테스트 시 mock 주입 불가. 의존성이 코드 안에 숨어 있음.
권장 (생성자 주입)¶
@Service
public class OrderService {
private final OrderRepository repo;
private final EmailSender emailSender;
public OrderService(OrderRepository repo, EmailSender emailSender) {
this.repo = repo;
this.emailSender = emailSender;
}
public void process(Order o) {
repo.save(o);
emailSender.send(o.email(), "주문");
}
}
// 테스트
new OrderService(new InMemoryOrderRepository(), new FakeEmailSender());
→ 생성자가 의존성 명시. 테스트 자유.
→ Effective Java Item 5 와 동일.
함정 / 주의¶
- 모든 것을 주입 = 과한 설계. 단순 값 객체·임시 헬퍼는 직접 생성 OK.
- 추상에 의존 도그마 — 구현이 1개뿐이면 인터페이스 도입은 추측성 일반화 (entity-refactoring 3.15).
- DI 컨테이너 의존 — 컨테이너 없이 도메인 객체를 만들지 못하면 컨텍스트 독립성 잃음.
- 세터 주입 남용 — 객체 생성 후 미설정 상태 가능, NPE 위험. 생성자 주입 우선.
체크리스트¶
- 도메인·서비스 클래스가 인터페이스에 의존하는가 (구체 X)
- 의존성이 생성자 매개변수로 명시되어 있는가 (정적 호출·싱글턴 X)
- 테스트 시 mock 주입이 자연스럽게 가능한가
-
new가 단순 값 객체·임시 헬퍼에 한정되어 있는가 - 단일 구현 인터페이스가 있는가 (추측성 일반화 의심)
퀴즈¶
- 의존성의 정의 를 한 문장으로?
- 컴파일타임과 런타임 의존성 분리 가 유연성의 비결인 이유?
new가 해로운 직접 이유 두 가지?- 명시적 의존성 (생성자 주입) 의 두 가지 이점?
- Effective Java Item 5 와 8장이 같은 메시지인 이유?
정답·해설¶
- A 가 B 에 의존한다 = B 의 변경이 A 에 영향을 줄 수 있다. 단순 사용보다 정확한 정의 — "변경 영향" 이 핵심.
- 코드에 명시된 타입 (추상) 과 실제 협력하는 객체 (구체) 가 분리 되어 있어 구체 교체 시 코드 무변경. Movie 코드가
DiscountPolicy만 알면 새 정책 (AmountDiscountPolicy → PercentDiscountPolicy) 으로 교체해도 Movie 무변경. - (1) 구체 클래스에 결합 — 새 구현 교체 불가. (2) 생성 매개변수까지 알아야 — 책임 폭증. 테스트 어려움도 추가 (mock 주입 불가).
- (1) 생성자가 의존성 선언 — 사용자가 한눈에 인지. (2) 테스트 시 mock 주입 자유 — 외부 시스템 없이 단위 테스트 가능.
- Item 5 = "자원을 직접 명시하지 말고 의존성 주입을 사용하라". 8장 = "
new는 해롭다·추상에 의존·명시적 의존성". 같은 OO 원칙 (DIP) 의 다른 표현. Item 5 가 Java 권고 형태, 8장이 OO 사고 원리 형태.
다음 장 예고 — 9장: 유연한 설계¶
8장의 의존성 관리 원칙을 SOLID 의 OCP·DIP 와 함께 체계화. 생성/사용 분리, 의존성 주입, 유연성에 대한 조언. "유연한 설계는 유연성이 필요할 때만" — 도그마 회피.