Code/OOP

[오브젝트] 8. 의존성 관리하기

noahkim_ 2025. 4. 4. 02:07

조영호 님의 "오브젝트" 책을 정리한 글입니다.

 

1. 의존성 이해하기

변경과 의존성

의존성
  • 함께 협력하는 객체들 사이에 발생하는 관계
구분 설명
구현 시점 - 의존 대상 객체가 변경되면, 의존하는 객체도 함께 변경될 가능성이 있음
실행 시점 - 의존 객체가 정상 동작하려면, 의존 대상 객체가 반드시 존재해야 함
방향성
- 항상 단방향임 (의존 A → B: A는 B에 의존하지만, B는 A에 의존하지 않음)
변경 전파 - 의존 방향을 따라 변경이 전파됨 (즉, B가 바뀌면 A도 영향 받을 수 있음)

 

예시
더보기
public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    // constructor...

    public boolean isSatisfiedBy(Screening screening) {
        return this.dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                this.startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                this.endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }
}
  • PeriodCondition 인스턴스가 정상적으로 동작하기 위해서는 Screening 인스턴스가 존재해야 함

 

UML 표기법
관계 기호 의미 예시
인터페이스 구현 -----▷ implements 관계 class Dog implements Animal
상속 ──▷ extends 관계 class Dog extends Mammal
의존 (Dependency) -----▶ 메서드 인자 등 잠깐 사용 class Dog가 Toy를 메서드 인자로 받음
연관 (Association) ──▶ 멤버 변수로 참조 class Person has Address 필드
- 집합 (Aggregation) ◇── has-a (느슨한 포함) 학급 ◇── 학생
- 구성 (Composition) ◆── has-a (강한 포함, 생명주기 공유) 자동차 ◆── 엔진

 

의존성 전이

  • 의존하고 있는 대상이 변경되면, 그 대상에 의존하고 있는 다른 클라이언트 코드들도 연쇄적으로 영향을 받는 현상
  • 얼마나 영향이 전파될지는 변경의 방향과 캡슐화의 정도에 따라 달라짐
구분 정의 영향
직접 의존성 어떤 요소가 다른 요소를 직접 참조하거나 사용하는 경우 PaymentService가 변경되면 OrderService에 직접 영향
간접 의존성 A가 직접 의존하지 않지만, 중간 객체를 통해 간접적으로 의존하게 되는 경우 PaymentService가 변경되면 OrderController까지 전이적 영향 가능

 

런타임 의존성과 컴파일타임 의존성

구분 컴파일타임 의존성 런타임 의존성
정의 코드 작성 시점에 명시적으로 드러나는 의존성 프로그램 실행 시 실제로 연결되는 의존성
확정 시점 컴파일 시 실행 시
유연성 낮음 (의존 대상이 고정됨) 높음 (동적으로 결정 가능)
확장성/유지보수 낮음 (변경 시 코드 수정 많음) 높음 (구현 변경이 쉬움)
결합도 상대적으로 높음 상대적으로 낮음
특징 타입은 명확하지만, 변경에 취약 느슨한 결합, 테스트 용이

 

컨텍스트 독립성

항목 설명
정의
모듈이 특정 실행 환경에 강하게 결합되지 않고, 다양한 환경에서 재사용 가능하도록 설계되는 것
문제점 (컨텍스트 의존)
특정 컨텍스트에 강하게 의존하면, 다른 문맥에서 재사용/테스트하기 어려움
좋은 설계 방향
클래스가 최소한의 가정만으로 동작하도록 설계
(자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 함)
장점
- 테스트 용이
- 재사용성 향상
- 유연한 구조
- 유지보수성 증가

 

의존성 해결하기

  • 컴파일 타임 의존성을 실행 컨텍스트에 맞는 적절한 런타임 의존성으로 교체하기

 

해결 방법
구분 생성자 주입 Setter 주입 메서드 인자 주입
타이밍 객체 생성 시 주입 객체 생성 후 설정 메서드 호출 시점
안정성 ✅ 객체가 항상 유효한 상태로 생성 ❌ 설정 전까지는 불안정할 수 있음 ✅ 전체 상태와 무관
불변성 보장 ✅ 주입된 의존성 변경 불가 ❌ 주입된 의존성을 나중에 변경 가능 ✅ 일회성 사용 (상태 유지 X)
유연성 구조적으로 안정적인 설계에 사용 동적으로 설정 또는 변경이 필요한 경우 일시적인 의존성에 적합
사용 예시 필수 의존성이 필요한 서비스나 모듈 선택적 기능/외부 설정 등이 있는 경우 유틸성 메서드

 

2. 유연한 설계

의존성과 결합도

바람직한 의존성
  • 추상 계층에 의존하도록 하기
    • 협력할 객체의 클래스를 고정할 필요 없음
  • 장점
    • 재사용성: 컨텍스트에 독립적

 

결합도
  • 의존성의 바람직한 정도

 

지식이 결합을 낳는다

결합도의 정도
  • 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정됨
  • 결합도를 낮추기 위해서는 의존성의 내부 정보에 대해 적게 알고 있어야 하며 의존성은 자신의 정보를 최대한 숨겨야 함

 

추상화에 의존하라

  • 어떤 구조를 좀더 명확하게 알기 위해 자세한 내용을 감춤으로써 복잡도를 극복하는 방법
  • 대상에 대해 알아야 할 지식의 양을 줄일 수 있음
항목 추상 클래스 인터페이스
역할 부분 구현을 제공하는 추상적 틀 전적으로 동작 계약(Contract)을 정의
내부 구현 은닉 ✅ 가능 (구현된 메서드로 내부 동작 일부 숨김)
❌ 불가능 (구현 없음 → 동작 자체를 정의하지 않음)
자식 클래스 은닉 ❌ 자식 클래스 종류는 외부에서 알아야 할 수 있음 ✅ 자식 클래스(구현체)를 몰라도 사용 가능
상속 계층 필요성 🔍 필요 (상속 구조를 알아야 전체 구조 파악 가능) ❌ 불필요 (계약만 알면 사용 가능)
추상화 수준 중간 수준의 추상화 가장 높은 수준의 추상화


명시적인 의존성

항목 명시적인 의존성 숨겨진 의존성
정의 의존성이 외부에 드러나 있음 (생성자, setter, 메서드 인자 등)
내부에서 직접 생성 (new 등으로 직접 인스턴스화)
인터페이스 노출 ✅ 퍼블릭 인터페이스에 드러남
❌ 퍼블릭 인터페이스에 드러나지 않음
의존성 파악 용이성 ✅ 외부에서 쉽게 파악 가능
❌ 코드 내부를 들여다봐야 알 수 있음
재사용성 ✅ 다른 컨텍스트에서도 쉽게 재사용 가능
❌ 재사용하려면 내부 구현 수정 필요
유연성 ✅ 의존 객체 교체 용이 (DI 등 가능)
❌ 의존 객체 고정됨 (유연성 낮음)
예시 Service(UserRepository repo) ← 외부에서 주입
new UserRepository() ← 생성자 내부에서 직접 생성

 

 

new는 해롭다

단점
단점 설명
🔗 결합도 높음 구체 클래스에 직접 의존함
📦 클래스 내부 책임 과다 생성과 사용의 책임을 한 클래스가 모두 가짐
🤯 사용자가 모든 걸 알아야 함 어떤 클래스인지, 어떤 인자를 넣어야 하는지, 생성 순서까지 모든 걸 클라이언트 코드가 알아야 함.

 

해결 방법
전략 설명
🛠 생성과 사용의 분리
new는 클라이언트 코드가 하도록 위임하고, 클래스는 그저 받아서 사용만 함. 
🧪 생성자 주입 (Constructor Injection) 협력 객체를 생성자가 받음. 초기화도 깔끔하고, 필수 의존성 보장됨.
🧴 Setter 주입 선택적인 의존성 주입. 늦게 설정할 수 있음, 하지만 누락 위험 존재.
🧾 메서드 인자 주입 그때그때 일회용 의존성 주입. 긴밀한 관계가 아닌 경우 유용.

 

가끔은 생성해도 무방하다

  • 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있음
  • ex) 협력하는 기본 객체를 설정하고 싶을 경우

 

표준 클래스에 대한 의존은 해롭지 않다

private List<DiscountCondition> conditions = new ArrayList<>();
  • 변경될 확률이 거의 없는 표준 클래스는 의존해도 문제가 되지 않음

 

컨텍스트 확장하기

조합 가능한 행동

선언적으로 행동을 정의할 수 있음
  • 객체들의 조합을 통해 무엇을 하는지를 표현할 수 있음
  • 코드를 보는 것 만으로 어떤 일을 하는지 쉽게 파악할 수 있음