티스토리 뷰

728x90
반응형

서론


  • 잘 설계된 객체지향 애플리케이션은 작고 응집도 높은 객체들로 구성되는데 이러한 작고 응집도 높은 객체는 책임의 초점이 명확하고 한 가지 일만 잘하는 객체를 의미합니다.
  • 이런 작은 객체들은 혼자서 수행할 수 있는 일이 거의 없기 때문에 애플리케이션의 기능을 구현하기 위해서는 다른 객체들과 협력을 통해 공통의 목적을 이룹니다.
  • 협력은 객체가 다른 객체에 대해 알것을 강요합니다. 다른 객체와 협력하기 위해서는 그 객체가 존재한다는 사실을 알고 있어야 합니다.
  • 객체지향 설계의 핵심을 협력을 위해 필요한 의존성은 유지하면서도 변경을 방해하는 의존성은 제거하는데 있습니다. 이런 관점에서 객체지향 설계란 의존성을 관리하는 것이고, 객체가 변화를 받아들일 수 있게 의존성을 정리하는 기술이라고 할 수 있습니다.

 

의존성 이해하기


💡 변경과 의존성

  • 어떤 객체가 협력하기 위해서는 다른 객체를 필요로 하는데 이 때 두 객체 사이에 의존성이 존재하게 됩니다. 또한 의존성은 실행 시점과 구현 시점에서 서로 다른 의미를 가집니다.

실행 시점

  • 의존하는 객체가 정상적으로 동작하기 위해서는 실행 시점에 의존 대상 객체가 반드시 존재해야 합니다.

구현 시점

  • 의존 대상 객체가 변경될 경우 의존하는 객체도 함께 변경됩니다.

예제 코드

  • PeriodCondition 클래스의 isSatisfiedBy 메서드를 보면 Screening을 의존하고 있습니다. 만약 실행 시점에 해당 Screening이 존재하지 않으면 예상치 못한 예외가 발생하게 됩니다.
  • 어떤 객체가 어떠한 작업을 정상적으로 하기 위해 다른 객체를 필요로 하는 경우 두 객체 사이에 의존성이 존재하게 되고, 이 의존성은 방향을 가지며 항상 단방향입니다.
  • 두 요소 사이의 의존성은 의존되는 요소가 변경될 때 의존하는 요소도 함께 변경될 수 있다는 것을 의미하며 그렇기 때문에 의존성은 변경에 의한 영향의 전파 가능성을 암시합니다.
@AllArgsConstructor
public class PeriodCondition implements DiscountCondition {

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    @Override
    public boolean isSatisfiedBy(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }
}

 

💡 의존성 전이

  • 의존성은 전이 될 수 있습니다. 아래 예제를 보면 Screening 클래스는 Movie, int, LocalDateTime에 의존하고 있는데 PeriodCondition 클래스도 Screening 클래스에 의존하고 있으므로 간접적인 영향을 받게 됩니다. 
  • 하지만! 의존성은 함께 변경될 수 있는 가능성을 의미할 뿐 무조건적으로 의존성이 전이되는것은 아닙니다. 의존성이 실제로 전이될지 여부는 변경의 방향과 캡슐화의 정도에 따라 달라집니다.
    Screening 클래스에서 내부 구현을 효과적으로 캡슐화했다면PeriodCondition에는 영향을 미치지 않을 것입니다. 
  • 의존성은 직접 의존성과 간접 의존성으로 나뉩니다.
@Getter
public class Screening {

    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
}

@AllArgsConstructor
public class PeriodCondition implements DiscountCondition {

    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    @Override
    public boolean isSatisfiedBy(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }
}

 

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

  • 런타임 의존성은 실행 시점을 의미합니다. 
  • 컴파일타임 의존성은 코드의 작성 시점과 코드가 컴파일 되는 시점으로 구분할 수 있습니다. 이렇게 구분되는 이유는 컴파일러가 없는 언어에서는 코드의 작성 시점이 컴파일타임 시점이 되는거 아닌가 싶습니다.
  • 객체지향 애플리케이션에서 런타임 시점의 주인공은 객체입니다. 따라서 런타임 의존성이 다루는 주제는 객체 사이의 의존성입니다. 
    반면 컴파일타임 시점의 주인공은 클래스입니다. 따라서 컴파일타임 의존성이 다르는 주제는 클래스 사이의 의존성입니다.
  • 여기서 중요한 것은 런타임 시점의 의존성과 컴파일타임 시점의 의존성은 다를 수 있으며, 유연하고 재사용 가능한 코드를 설계하기 위해서는 두 종류의 의존성을 다르게 해야합니다.

예제 코드

  • 아래 예제를 보면 MovieA 클래스는 PeriodCondition 클래스에 대해서만 의존하고 있습니다. 이렇게 된다면 새로운 할인 정책을 추가하기 어렵게 됩니다. 
  • 반면 MovieB 클래스는 추상적인 DiscountCondition에 의존하고 있기 때문에 DiscountCondition을 상속 받고 있는 클래스라면 유동적으로 재사용 및 교체가 가능하게 됩니다.
  • 이렇듯 어떤 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안됩니다. 실제로 협력할 객체가 어떤 것인지는 런타임 시점에 해결해야 하고, 클래스가 협력할 객체의 클래스를 명시적으로 드러내고 있다면 다른 클래스의 인스턴스와 협력할 가능성 자체가 없어지게 됩니다. 따라서 컴파일 구조와 런타임 구조가 다를수록 설계가 유연해지고 재사용 가능하게 됩니다.
// 구체적인 타입을 알고 있는 Movie 클래스
public class MovieA {

    private String title;
    private Duration runningTime;
    private Money fee;
    private PeriodCondition periodCondition; // 구체적인 타입
}

// 추상적인 타입을 알고 있는 Movie 클래스
public class MovieB {

    private String title;
    private Duration runningTime;
    private Money fee;
    private DiscountCondition discountCondition; // 추상적인 타입
}

 

💡 컨텍스트 독립성

  • 우리는 방금 위에서 컴파일타임 의존성과 런타임 의존성이 달라야 한다는 사실을 확인했습니다. 클래스는 자신과 협력할 객체의 구체적인 클래스에 대해 알아서는 안됩니다. 구체적인 클래스를 알면 알수록 그 클래스가 사용되는 특정한 문맥에 강하게 결합되어 설계가 유연해지지 못하고 재사용 가능성도 없어지게 됩니다.
  • 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄져 있다면 다른 문맥에서 재사용하기 더 쉬워집니다. 이를 컨텍스트 독립성이라 합니다.
  • 설계가 유연해지기 위해서는 가능한 자신이 실행될 컨텍스트에 대한 구체적인 정보를 최대한 적게 알아야 합니다. 컨텍스트에 대한 정보가 적으면 적을수록 더 다양한 컨텍스트에서 재사용될 수 있기 때문입니다.

 

💡 의존성 해결하기

  • 컴파일타임 의존성은 런타임 시점에 구체적인 객체를 주입해줘야 하는데 어떻게 해결할 수 있을까요? 

 

객체를 생성하는 시점에 생성자를 통해 의존성 해결

public class Movie {

    private String title;
    private DiscountCondition discountCondition;

    public Movie(String title, DiscountCondition discountCondition) {
        this.title = title;
        this.discountCondition = discountCondition;
    }
}

// 생성자를 통해 주입
Movie movie = new Movie("공조2", new PeriodCondition(...));

 

객체를 생성후 setter 메서드를 통해 의존성 해결

  • setter 메서드를 사용하는 방법은 실행 시점에 의존 대상을 유연하게 변경할 수 있으나 객체가 생성된(new) 후 협력에 필요한 의존 대상을 설정하기 때문에 객체를 생성하고 의존 대상을 설정하기 전까지는 객체의 상태가 불완전할 수 있습니다. 따라서 더 좋은 방법은 생성자 방식과 setter 방식을 혼합해 사용하는 것입니다. 
Movie movie = new Movie();
movie.setTitle("공조2");
movie.setDiscountCondition(new PeriodCondition(...));

 

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

  • 메서드의 인자로 사용하는 방식은 협력 대상에 대해 지속적으로 의존 관계를 맺을 필요없이 메서드가 실행되는 동안만 일시적으로 의존 관계가 존재해도 무방하거나, 메서드가 실행될때마다 의존 대상이 매번 달라져야 하는 경우에 유용합니다.
public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
    return fee.minus(discountPolicy.getDiscountAmount(screening));
}

 

유연한 설계


💡 의존성과 결합도

  • 객체지향 패러다임의 근간은 협력입니다. 객체들은 협력을 통해 애플리케이션에 생명력을 불어 넣으며 객체들이 협력하기 위해서는 서로의 존재와 수행 가능한 책임을 알아야 합니다. 이런 지식들이 객체 사이에 의존성을 낳게 되며 이러한 의존성이 나쁜 것만은 아니고 과하면 문제가 될 수 있습니다.
  • 바람직한 의존성과 바람직하지 못한 의존성을 가리키는 용어가 존재하는데 결합도가 그것입니다. 두 요소 사이에 의존성이 바람직할 경우 느슨한 결합도, 약할 결합도라 하며 그렇지 않은 경우는 단단한 결합도, 강한 결합도라 합니다.
  • 바람직한 의존성은 설계를 재사용하기 쉽게 만드는 의존성입니다. 바람직하지 못한 의존성이란 설계를 재사용하기 어렵게 만드는 의존성입니다. 

예제 코드

  • MovieA 클래스는 구체적인 클래스에 의존하고 있습니다. 구체적인 클래스에 의존하고 있다고해서 무조건 나쁘다고 생각해야 할까요?
    그렇지는 않습니다. 우리는 3장 역할, 책임, 협력  에서 읽은 것처럼 단 한 종류의 객체만 협력에 참여할 경우 후보는 객체가 된다는 것을 상기할 필요가 있습니다. 그렇기 때문에 단 한 종류일 경우는 문제가 되지 않지만 다른 조건이 추가될 경우에는 문제가 발생합니다. 
    그러한 상황에서는 추상화 기법을 사용하여 문제를 해결해야 합니다.
public class MovieA {

    private String title;
    private Duration runningTime;
    private Money fee;
    private PeriodCondition periodCondition; // 구체적인 타입
}

 

의존성과 결합도

  • 일반적으로 의존성과 결합도를 동의어로 사용하지만 사실 두 용어는 서로 다른 관점에서 관계의 특성을 설명하는 용어라고 합니다.
    의존성은 두 요소 사이의 관계 유무를 설명합니다. 따라서 의존성의 관점에서는 두 요소 사이에 의존성이 존재한다라고 표현하며 
    결합도의 관점에서는 두 요소 사이에 존재하는 의존성의 정도를 상대적으로 표현합니다. 따라서 느슨하거나 강하거나 라고 표현합니다.

 

💡 지식이 결합을 낳는다

  • 결합도의 정도는 한 요소가 자신이 의존하고 있는 다른 요소에 대해 알고 있는 정보의 양으로 결정됩니다. 한 요소가 다른 요소에 대해 많은 정보를 알수록 강하게 결합되고, 반대로 한 요소가 다른 요소에 대해 적게 알수록 약하게 결합됩니다. 이렇게 서로에 대해 알고 있는 지식의 양이 결합도를 결정하게 됩니다.
  • 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미합니다. 기존 지식에 어울리지 않는 컨텍스트에서 클래스의 인스턴스를 재사용하기 위해서 할 수 있는 유일한 방법은 클래스를 수정하는 것입니다. 결합도를 느슨하게 유지하려면 협력하는 대상에 대해 더 적게 알아야하며, 협력하는 대상에 대해 필요한 정보 외에는 최대한 캡슐화하는 것이 중요합니다.

 

💡 추상화에 의존하라

  • 추상화를 사용하면 불필요한 정보를 감출 수 있습니다. 불필요한 정보를 감춘다는 것은 의존시 알아야하는 지식의 양을 줄일 수 있기 때문에 결합도를 느슨하게 유지할 수 있습니다.

결합도를 느슨하게 가지는 방법

  • 구체 클래스에 비해 추상 클래스는 메서드의 내부 구현과 자식 클래스의 종류에 대한 지식을 클라이언트에게 숨길 수 있습니다. 따라서 클라이언트가 알아야 하는 지식의 양이 적기 때문에 구체 클래스보다 추상 클래스에 의존하는 것이 결합도를 느슨하게 가질 수 있습니다. 또한 추상 클래스보다 인터페이스에 의존하게 되면 더욱 더 느슨한 결합도를 가질 수 있는데 그 이유는 인터페이스에 의존하면 상속 계층을 모르더라도 협력이 가능하기 때문입니다. 인터페이스 의존성은 협력하는 객체가 어떤 메시지를 수신할 수 있는지에 대한 지식만을 남기기 때문에 추상 클래스보다 느슨한 결합도를 유지할 수 있습니다.
    • 구체 클래스 의존성
    • 추상 클래스 의존성
    • 인터페이스 의존성

 

💡 명시적인 의존성

  • 아래 예제는 인스턴스 변수를 추상 클래스로 선언하더라도 생성자에서 구체적인 클래스를 주입받고 있는 것을 알 수 있습니다. 이는 추가적인 의존성을 발생시키게 됩니다.
  • 의존성의 대상을 생성자의 인자로 전달 받는 방식과 생성자 안에서 직접 생성하는 방식의 가장 큰 차이점은 퍼블릭 인터페이스(생성자)를 통해 설정을 유동적으로 할 수 있는지 방법을 제공하는 여부입니다.
  • 퍼블릭 인터페이스를 제공하는 방식을 명시적인 의존성이라 하며, 생성자 내부에서 직접 생성하는 방식을 숨겨진 의존성이라 합니다.
  • 의존성은 명시적으로 표시돼야 하며, 내부 구현에 숨겨둔 다면 코드를 파악하기 힘들 수 있습니다. 유연하고 재사용 가능한 설계란 퍼블릭 인터페이스를 통해 의존성이 명시적으로 드러나는 설계입니다.
public class Movie {

    private String title;
    private DiscountCondition discountCondition;

    public Movie(String title) {
        this.title = title;
        this.discountCondition = new PeriodCondition(...);
    }
}

 

💡 new는 해롭다

  • 인스턴스를 생성할 수 있는 new 연산자는 안타깝게도 new를 잘못 사용하면 클래스 사이의 결합도가 극단적으로 높아질 수 있습니다.

결합도 측면에서 new가 해로운 이유

  • 첫째. new 연산자를 사용하기 위해서는 구체 클래스의 이름을 직접 기술해야 합니다. 따라서 new를 사용하는 클라이언트는 추상화가 아닌 구체 클래스에 의존하기 때문에 결합도가 높아집니다.
  • 둘째. new 연산자는 생성하려는 구체 클래스뿐만 아니라 어떤 인자를 사용해야 하는지 혹은 어떤 순서인지 클라이언트가 알아야 하기 때문에 결합도가 높아질 수 있습니다.

 

💡 가끔은 생성해도 무방하다

  • 클래스 안에서 객체의 인스턴스를 직접 생성하는 방식이 유용한 경우도 있습니다. 주로 협력하는 기본 객체를 설정하고 싶은 경우가 여기에 속합니다.
public class Movie {

    private String title;
    private DiscountCondition discountCondition;

    public Movie(String title) {
        this(title, new PeriodCondition(...)); // 주로 사용될 기본 객체 설정
    }

    public Movie(String title, DiscountCondition discountCondition) {
        this.title = title;
        this.discountCondition = discountCondition;
    }
}

 

💡 조합 가능한 행동

  • 어떤 객체와 협력하느냐에 따라 객체의 행동이 달라지는 것은 유연하고 재사용 가능한 설계가 가진 특징입니다. 유연하고 재사용 가능한 설계는 응집도 높은 책임들을 가진 작은 객체들을 다양한 방식으로 연결함으로써 애플리케이션의 기능을 쉽게 확장할 수 있습니다.
  • 유연하고 재사용 가능한 설계는 객체가 어떻게 하는지를 나열하지 않고 객체들의 조합을 통해 무엇을 하는지를 표현하는 클래스들로 구성됩니다.

 

 

✔️ 정리

  • 유연하고 재사용 가능한 설계는 작은 객체들의 행동을 조합함으로써 새로운 행동을 이끌어낼 수 있는 설계입니다. 훌륭한 객체지향 설계란 어떻게 하는지 표현하는게 아닌 객체들의 조합을 선언적으로 표현함으로써 객체들이 무엇을 하는지 표현하는 설계입니다. 
    이런 설계를 창조하는데 있어서 핵심은 의존성을 관리하는 것입니다.

 

 

 

 

 

 

 

 

 

 

728x90
반응형