티스토리 뷰

728x90
반응형

서론


  • 책임에 초점을 맞춰서 설계할 때의 가장 큰 어려움은 어떤 객체에 어떤 책임을 주어야 하는가?입니다. 이러한 책임 할당 과정은 일련의 트레이드오프의 활동입니다.

 

책임 주도 설계를 향해


💡 데이터보다 행동을 먼저 결정하라.

  • 객체에게 중요한 것은 데이터가 아닌 협력하는 객체에게 제공하는 행동입니다. 클라이언트의 관점에서 객체가 수행하는 행동은 곧 책임을 의미합니다. 객체는 협력에 참여하기 위해 존재하며 협력안에서 적절한 행동을 수행할 때 비로소 객체는 존재합니다.
  • 데이터는 객체가 책임을 수행할 때 필요로 하는 재료일 뿐입니다.

 

행동의 관점에서 바라보는 방법

  • 데이터 중심의 설계는 "이 객체가 포함해야 하는 데이터는 무엇인가?"를 결정한 후에 "데이터를 처리하는데 필요한 작업은 무엇인가?"를 결정합니다.
  • 책임 중심의 설계는 "이 객체가 수행해야 할 책임은 무엇인가?"를 결정한 후에 "책임을 수행하기 위한 데이터는 무엇인가?"를 결정합니다. 이렇게 책임을 먼저 결정한 후에 데이터를 결정해야합니다.

 

💡 협력이라는 문맥 안에서 책임을 결정하라.

  • 객체에게 책임을 부여할 때 협력에 얼마나 적합한지를 기준으로 결정합니다. 만약 객체에게 할당된 책임이 협력에 어울리지 않는다면 그 책임은 나쁜 것입니다. 반면 책임이 조금 어색하지만 협력에 적합하다면 그 책임은 좋은 책임입니다.
    책임이란 객체의 입장이 아닌 협력의 입장에서 바라봐야 합니다.
  • 협력을 시작하는 주체는 메시지 전송자이므로 협력에 적합한 책임이란 메시지 수신자가 아닌 메시지 전송자에게 적합해야 합니다. 
    또한 협력에 적합한 책임을 찾기 위해서는 객체가 메시지를 선택하는 게 아닌 메시지가 객체를 선택하도록 해야 합니다. 메시지가 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것입니다.

 

책임 할당을 위한 GRASP 패턴


💡 정보 전문가에게 책임을 할당하라.(Information Expert 패턴)

  • 책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 책임으로 생각하는 것입니다. 이 책임을 메시지라 생각하고 이 메시지를 책임질 객체는 누구인가?부터 시작하게 됩니다.
  • 메시지는 메시지를 수신할 객체가 아닌 메시지 전송자의 의도를 반영해서 결정해야 합니다.
  • 객체의 책임과 책임을 수행하기 위해 필요한 데이터는 동일한 객체안에 존재해야 합니다. 이렇게 하는 이유는 정보를 알고 있는 객체만이 책임을 어떻게 수행할지 스스로 결정할 수 있으며 그 과정에서 캡슐화를 유지할 수 있습니다.
    이러한 원칙이 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것입니다.
  • 정보 != 데이터 입니다. 객체가 책임을 수행하기 위해 정보를 알고 있어야 하는데 이 정보는 데이터가 아닐 수도 있습니다. 여기서 정보란 책임을 수행하기 위해 다른 객체를 알고 있거나 본인이 데이터를 가지고 있거나로 해석할 수 있습니다.

 

💡 높은 응집도와 낮은 결합도

  • 설계는 트레이드오프의 활동입니다. 동일한 기능을 구현할 때 수많은 설계의 방법이 있습니다. 이때 하나의 방법을 선택해야 하는데
    이럴 경우 높은 응집도와 낮은 결합도의 측면에서 생각하면 더 나은 방법을 선택할 수 있습니다.

 

💡 창조자에게 객체 생성 책임을 할당하라.

  • 최종적으로는 어떠한 인스턴스를 생성하는 것입니다. 그렇기 때문에 생성될 인스턴스를 가장 잘 알고 있거나, 긴말하게 사용하거나,
    초기화에 필요한 정보를 가지고 있는 객체에 책임을 할당해야 합니다.

 

구현을 통한 검증


문제의 코드

  • 아래 DiscountCondition 클래스는 내부에서 2가지의 책임을 가지고 있습니다. type에 따라 기간조건, 순번조건을 반환하고 있습니다. 
  • 이 클래스의 가장 큰 문제는 변경에 취약합니다.
    • 첫 번째 이유 - isSatisfiedBy 메서드에서 새로운 조건이 생기면 if-else 구문을 추가해야 합니다.
    • 두 번째 이유 - 순번 또는 기간의 로직에 변경이 발생하는 경우입니다.
public class DiscountCondition {

    private DiscountConditionType type;
    private int sequence;
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;
    
    public boolean isSatisfiedBy(Screening screening) {
        if (type == DiscountConditionType.PERIOD)
            return isSatisfiedByPeriod(screening);
        
        return isSatisfiedBySequence(screening);
    }
    
    
    private boolean isSatisfiedByPeriod(Screening screening) {
        return dayOfWeek.equals(screening.getWhenScreened().getDayOfWeek()) &&
                startTime.compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
    }

    private boolean isSatisfiedBySequence(Screening screening) {
        return sequence == screening.getSequence();
    }
}

 

👍 변경의 이유를 파악할 수 있는 방법 

  • 인스턴스 변수가 초기화되는 시점을 파악합니다.
    • 응집도가 높은 클래스는 인스턴스가 생성될 때 모든 속성을 함께 초기화합니다. 반면 응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 그대로 남겨집니다. DiscountCondition의 경우 순번 조건인 경우 sequence 속성만 초기화하고 기간 조건인 경우 dayOfWeek, startTime, endTime만을 초기화합니다.
      이런 경우 함께 초기화되는 속성을 기준으로 코드를 분리해야 합니다.
  • 메서드들이 속성을 사용하는 방식을 파악합니다.
    • 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도가 높다고 할 수 있지만 반면에 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있습니다. 응집도를 높이기 위해서는 메서드에 접근하는 속성을 기준으로 코드를 분리해야 합니다.

👍 클래스 응집도 판단하기

  • 클래스가 하나 이상의 이유로 변경이 발생하게 된다면 응집도가 낮은 것입니다. 변경의 이유를 기준으로 클래스를 분리해야 합니다.
  • 클래스의 속성을 초기화하는 시점에 따라 서로 다른 속성을 초기화하고 있다면 응집도가 낮으니 속성의 그룹에 따라 클래스를 분리해야 합니다.
  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮으니 그룹을 기준으로 클래스를 분리해야 합니다.

 

💡 타입 분리하기

  • 아래는 순번 조건과 기간 조건을 분리한 다음 Movie 클래스에서는 각각의 속성을 가지고 있습니다. 하지만 여전히 Movie 클래스는 문제를 가지고 있습니다. 
    • 첫 번째 문제. SequenceCondition, PeriodCondition 양쪽에게 결합이 됩니다. 
    • 두 번째 문제. 새로운 조건이 추가되면 Movie 클래스에 또 다른 속성이 추가됩니다.
// 순번 조건
@AllArgsConstructor
public class SequenceCondition {

    private int sequence;

    public boolean isSatisfiedBy(Screening screening) {
        return sequence == screening.getSequence();
    }
}

// 기간 조건
@AllArgsConstructor
public class PeriodCondition {

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

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

@AllArgsConstructor
public class Movie {

    private String title;
    private Duration runningTime;
    private Money fee;
    private List<SequenceCondition> sequenceConditions; // 순번 조건 목록
    private List<PeriodCondition> periodConditions;     // 기간 조건 목록
}

 

💡 다형성을 통해 분리하기

  • SequenceCondition, PeriodCondition이 DiscountCondition을 구현하고 있고 Movie 클래스에서는 인터페이스만 가지고 있으면 문제를 해결할 수 있습니다.
  • 추상클래스, 인터페이스를 사용하여 문제를 해결할 수 있습니다. 역할을 대체할 클래스들 사이에서 공유해야 할 무엇인가가 있다면 추상 클래스를 사용하고 구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용하면 됩니다.
// 추상화
public interface DiscountCondition {
    boolean isSatisfiedBy(Screening screening);
}

@AllArgsConstructor
public abstract class Movie {

    private String title;
    private Duration runningTime;
    private Money fee;
    private List<DiscountCondition> discountConditions;
}

 

💡 변경으로부터 보호하기

  • 아래는 위의 예제를 그림으로 표현한 것입니다. SequenceCondition의 경우 순번 조건의 로직이 변경될 때만 변경이 이루어지고 
    PeriodCondition의 경우 기간 조건의 로직이 변경될 경우에만 변경이 발생하게 됩니다. 또한 새로운 할인 조건이 추가되더라도 문제는 발생하지 않습니다. 또한 Movie의 관점에서는 구체적인 타입을 캡슐화를 할 수 있으며 이를 변경 보호 패턴이라 합니다.

 

💡 변경과 유연성

  • 변경에 대비할 수 있는 두 가지 방법
    • 첫째. 코드를 이해하고 수정하기 쉽게 최대한 단순하게 설계하는 것
    • 둘째. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 유연하게 만드는 것

 

책임 주도 설계의 대안


  • 책임 주도 설계에 익숙해지기란 매우 어려우며 많은 시간과 노력이 필요합니다.
  • 책임과 객체 사이에서 방황하고 있을 때 해결책을 찾는 방법으로는 최대한 빠르게 기능을 수행하는 코드를 만들고 리펙토링을 하는 것입니다. 그 이유는 아무것도 없는 상태에서 책임과 협력에 관해 고민하는 것보다 일단 실행되는 코드를 얻고 난 후 책임들을 올바른
    위치로 이동시키는 것이 효율적입니다.

 

💡 메서드 응집도

  • 긴 메서드는 이해하기 어렵고 유지보수에 부정적인 영향을 미칩니다.
    • 어떤 일을 수행하는지 한눈에 파악하기 힘들며 전체의 코드를 이해하는데 많은 시간이 걸립니다.
    • 하나의 메서드 안에서 많은 작업을 처리하기 때문에 변경이 필요할 때 수정할 부분을 찾기 힘듭니다.
    • 메서드 내부의 일부 로직만 변경하더라도 버그의 발생 확률이 높아집니다.
    • 로직의 일부를 재사용하기 어렵습니다.
    • 코드를 재사용하는 유일한 방법은 복사 붙여넣기 뿐이므로 중복을 유발하기 쉽습니다.

 

💡 객체를 자율적으로 만들자

  • 객체는 자율적인 존재여야 한다는 사실을 떠올리면 쉽습니다. 자신이 소유하고 있는 데이터를 스스로 처리할 수 있도록 만드는 것이 자율적인 객체를 만드는데 지름길입니다.
  • 어떤 데이터를 사용하는지 가장 쉽게 알 수 있는 방법은 메서드 안에서 어떤 클래스의 접근자 메서드를 사용하는지 파악하는 것입니다.

 

✔️ 정리

  • 책임 주도 설계가 익숙하지 않다면 일단 코드를 먼저 구현한 후에 리펙토링을 진행하는 것입니다. 처음부터 책임 주도 설계 방법을 따르는 것보다 동작하는 코드를 작성한 후에 리펙토링하는 것이 더 훌륭한 결과물을 낳을 수 있습니다.
  • 캡슐화, 응집도, 결합도를 이해하고 훌륭한 객체지향 원칙을 적용하기 위해 노력한다면 책임 주도 설계 방법을 단계적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있습니다.

 

 

 

 

 

 

 

728x90
반응형