스터디

[스터디: 오브젝트] 의존성 관리

GYUD 2024. 2. 22. 20:27

 

이번 장에서는 의존성, 의존성 전이, 의존성 해결에 관해 이야기 한다.

 

객체간의 협력을 위해서는 의존은 필수적이다.

하지만 과도한 의존은 나쁜 설계를 만든다.

 

시스템의 특징과 트레이드오프 관계, 예상되는 변경사항을 고민하여 의존성을 관리함으로써 최적의 설계를 만들자.

설계를 잘하는 것 만큼, 설계를 활용하여 변경에 잘 대처하는 능력을 기르는 것도 중요하다.

 


의존성

A라는 객체가 B라는 객체를 알고 있을 때, 'A가 B를 의존한다' 라고 한다.

 

PeriodCondition 클래스를 예로 들어보자.

public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    ...

    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
            startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
            emdTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

 

PeriodCondition은 DiscountCondition, DayOfWeek, LocalTime, Screening 객체에 의존한다.

 

DayOfWeek 와 LocalTIme 객체는 인스턴스 변수로, Screening 객체는 메서드의 인자로, DiscountCondition은 객체의 서브타입으로써 의존하고 있다.

 

의존성이 있는건 똑같지만 이들을 구분짓는다면 아래와 같이 의존관계를 표현할 수 있을 것이다.

 

의존성 전이

위 예시에서 Screening 객체가 Movie 객체를 의존하고 있다고 가정하자.

 

Movie 객체가 캡슐화가 잘 되어 있다면, 의존성 전이가 일어나지 않는다.

하지만 캡슐화가 잘 되어 있지 않다면, PeriodCondition 객체는 Screening 객체를 통해 Movie 객체를 의존할 수 있다.

 

이렇게 한 단계를 거치는 의존관계를 간접 의존성이라고 한다.

간접 의존성은 직접 의존성과 다르게 코드 안에서 의존 관계가 드러나지 않기 떄문에 오류가 생기면 해결하기가 어렵다.

 

 

의존성 해결

컴파일 타임에 추상화에 의존하던 의존 관계를 런타임에 구체적인 구현체에 의존하는 것으로 바꾸어 주는 것을 의미한다.

이해를 위해 런타임 의존성과 컴파일 타임 의존성에 대해 이야기해보자.

 

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

런타임 의존성은 말 그대로 실행 시점의 의존성이다.
컴파일 타임 의존성은 컴파일 시점의 의존성으로 코드 자체의 의존성이다.

쉽게 자바로 예를 들자면 런타임 의존성 객체들간의 의존성이고, 컴파일 타임 의존성클래스 간의 의존성을 의미한다.

이 둘은 다를 수 있으며, 달라야만 다형성을 활용해 객체를 쉽게 바꿀 수 있다.

 

이때, 컴파일 타임 의존성을 런타임 의존성으로 바꾸는 것의존성 해결이라고 한다.

 

의존성 해결의 방법에는 3가지가 있다.

 

1. 생성자를 통한 의존성 해결

Movie avatar = new Movie("아바타", ... , new AmoountDiscountPolicy());
Movie starWars = new Movie("스타워즈", ... , new PercenDiscountPolicy());

생성자를 활용해서 의존성을 주입해주는 방식이다.

생성자만을 이용해서 의존성을 주입한다면, 이미 생성된 객체에 대해서는 의존성을 변경할 수 없다는 단점이 존재한다.

 

 

2. setter 메서드를 활용한 의존성 해결

public class Movie {
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}


avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

setter 메서드를 통해 의존성을 주입해주는 방식이다.

setter 메서드를 사용하면 중간에 의존성을 변경할 수 있다는 장점이 있긴 하지만, 메서드를 호출하기 전까지 객체가 의존성을 가지지 않아 불완전한 상태가 된다.

 

이 때문에 실제로는 생성자를 통해 객체 생성 시 의존성을 주입하고, 의존성 변경이 필요할 때 setter 메서드를 통해 의존성을 변경하는 방법을 주로 사용한다.

 

 

3. 메서드 실행 시 인자를 이용한 의존성 해결

public class Movie {
    public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

앞서 DiscountCondition이 Screening 객체를 메서드 인자를 통해서 의존하는 것과 같이 위 코드에서 Movie 객체는 DiscountPolicy 객체를 인스턴스 변수로 가지지 않고 외부에서 주입받아서 사용한다.

 

Movie 객체가 항상 DiscountPolicy 객체를 의존할 필요가 없거나, 의존하는 객체가 너무 자주 바뀐다면 위 방식을 사용하면 효율적으로 의존관계를 지정해 줄 수 있다.

 

이렇게 의존성을 지정하는 방법은 다양하고, 이를 시스템에 맞게 잘 활용해야 한다.

모든 설계는 트레이드오프 관계이기 때문에 시스템의 특징을 고려하여 적절한 설계 방법을 선택해야 할 것이다.

 

 

책에서는 좋은 의존성 설계를 위해 활용할 수 있는 몇가지 팁을 소개한다.

 

서로를 모르도록 의존성을 설계하라.

필요한 최소한의 의존관계를 설정하여 결합도를 낮출 수 있다.

 

 

명시적인 의존성을 사용하라

의존성이 있다면 이를 숨기지 말고 드러내라는 의미이다.

public class Movie {
    
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, Money fee) {
        ...
        this.discountPolicy = new AmountDiscountPolicy();
    }
}

위 코드의 Movie 객체는 AmountDiscountPolicy를 의존하고 있지만, 명시적으로 드러나지 않고 메서드 내부에 드러난다.

이렇게 내부로 의존관계를 숨기는 코드는 오히려 의존관계를 찾게 어렵게 만들 뿐이다.

 

의존관계가 있는것은 협력을 위해서 당연히 필요한 것이기 때문에 이를 숨기지 말고, 당당하게 드러내야 좋은 설계를 할 수 있다.

 

 

new는 해롭다.

new는 구현체를 생성하는 키워드이기 때문에 new를 사용하면 구현체 자체에 의존할 수 밖에 없다.

이를 해결하기 위해서는 new가 아닌 외부로 부터 의존관계를 주입받도록 해야한다.

 

하지만 항상 new를 사용하지 않아야 하는 것은 아니다.

Movie 객체의 DiscountPolicy가 대부분 AmountDiscountPolicy 구현체라면 아래와 같이 생성자를 통해 의존성을 주입하고, AmountDiscountPolicy가 아닌 특별한 경우에만 외부에서 주입받도록 할 수 있다.

public class Movie {
    
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runnintTime) {
        this(title, runnintTime, new AmountDiscountPolicy(...));
    }

    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy){
        ...
        this.discountPolicy = discountPolicy;
    }
}

이렇게 하면 결합도가 높아졌기 때문에 변경에는 용이하지 않은 설계가 탄생한다.

하지만 Movie 객체 생성시 대부분의 경우 DiscountPolicy를 주입해줄 필요가 없기 때문에 사용성이 좋아질 것이다.

 

많은 경우에는 변경에 용이한 방법을 택하겠지만, 시스템의 사용성이 더 중요한 가치라고 여긴다면, 생성자를 통해 의존성을 주입하는 것이 더 적절한 설계가 될 것이다.

 


설계를 활용하기

설계를 잘하는 것이 중요하지만, 설계를 활용할 줄도 알아야 한다.

 

만약 AmountDiscountPolicy와 PeriodDiscountPolicy를 중복으로 적용해야하는 영화를 추가해야한다고 가정하자.

어떻게 효율적으로 추가할 수 있을지 방법을 고민해보자.

 

나 역시 어떻게 변경할지 고민해봤는데, Movie 객체의 discountPolicy 인스턴스 변수를 List<DiscountPolicy> discountPolicies 로 변경해 주었을 것 같다.

 

하지만 이러한 변경은 올바르지 않은 변경이다.

변경의 목적은 할인 정책 변경하기 위함인데, Movie 객체의 인스턴스 변수를 변경했기 때문이다.

 

NoneDiscountPolicy의 경우 새로운 할인 정책이기 때문에 새로운 정책을 추가하면 된다고 생각했지만, 중복 정책의 경우에는 이미 존재하는 정책을 중복하여 적용하는 것이기 때문에 위와 같이 변경해야 겠다고 생각했었다.

 

하지만 중복 정책도 NoneDiscountPolicy와 같이 새로운 정책으로 보고, 추가해 줄 수 있다.

public class OverlappedDiscountPolicy extends DiscountPolicy {
    private List<DiscountPolicy> discountPolicies = new ArrayList<>();

    public OverlappedDiscountPolicy(List<DiscountPolicy> discountPolicies) {
        this.discountPolicies = discountPolicies;
    }
    
    ...
}

이렇게 하면 Movie 객체를 변경할 필요 없이, 기존의 협력관계를 유지하면서 새로운 정책만 추가할 수 있다.

확장에는 개방적이고, 수정에는 폐쇄적인 설계를 잘 만들고, 이를 실제로 활용한 것이다.

 


마지막 부분을 읽고 아직 많이 부족하다는 것을 느꼈다.

 

1달 넘게 객체지향에 대해 공부하면서, 추상화를 활용한 설계 방법에는 적응되었다.

하지만 이런 설계를 제대로 활용하는 능력은 아직 부족한 것 같다.

 

좋은 설계를 하는 것도 중요하지만, 결국 변경에 잘 대처하기 위해서 좋은 설계를 한다는 목적을 까먹지 않아야한다.

변경사항이 있다면, 현재의 설계에서 어떻게 효율적으로 변경할 수 있는지를 고민하고 몸에 익혀야 할 것 같다.