설계는 변경을 위해 존재하여 변경은 어떻게든 비용이 발생한다. 훌륭한 설계란 합리적인 비용으로 변경을 수용할 수 있는 구조를 만드는 것이다.
적절한 비용안에서 변경 가능한 구조가 되기 위해선 객체끼리 느슨히 연결되있어야 하며 응집도가 높은, 흔히 말하는 유연한 코드가 되어야 한다.
내가 생각하는 유연한 코드는 다음 3가지를 모두 만족하는 코드다.
설계 시 협력이라는 문맥 안에서 객체마다 적당한 책임을 갖게 하는게 중요한 이유는 위에서 언급한 유연한 코드를 작성할 수 있게 하는 방법이기 때문이라 볼 수 있다.
유연하지 않은 코드가 되면 어떤 문제점이 발생할까? 이를 고려하지 않은 예제 코드들을 통해 중요성을 자세히 알아보자.
모듈에 포함된 내부 요소들이 연관되있는 정도를 응집도라 한다. 이는 객체마다 얼마나 책임이 분산(SCP)되었는가? 를 나타내는 지표라 봐도 무방하다.
하나의 로직을 변경 시 여러 객체를 수정해야 한다면 번거로울 것이다. 응집도가 높다면 변경 시 대상의 범위가 명확해져 소수의 객체만 수정할 수 있게 된다.
서로 다른 이유로 변경되는 코드가 하나의 모듈안에서 공존한다면 변경 포인트가 많아져 응집도가 낮다고 얘기한다.
ReservationAgency
는 모든 클래스와 의존위 다이어그램에서 변경의 이유가 다른 코드들이 서로 공존하고 있다. 이는 추후 확장/유지 보수시 문제가 발생한다.
만약, 할인 조건이 추가된다면 이를 담당하는 객체(DiscountCondition
)만 수정되야 하는데 뜬금없이 상영 객체(Screening
)를 수정해야 할 수 있다.
이는 객체지향 5가지 원칙 중 단일 책임 원칙(Single Responsibility Principle)을 위반한다.
설계 시 특정 로직에 대한 책임을 여러 곳에 전파하지 말고 최대한 소수로 하여 분산시키자. 그렇다면 해당 로직 수정 시 변경의 전파가 최소화될 것이다.
다른 모듈에 대해 얼마나 알고 있는지 나타내는 척도이자 객체 마다 의존성의 정도를 의미한다. 다른 객체를 얼마나 알고 있는가?를 나타낸다.
한 객체가 다른 객체에 필요한 정보만 안다면 결합도가 낮다는 것을 의미한다. 필요한 부분이 변경되는 경우 다른 객체의 수정은 필연적이다.
객체간 결합도가 높다면 다른 객체를 필요 이상으로 많이 안다는 것이다. 이를 결합도가 높다고 표현한다. 필요하지 않은 부분이 변경으로 인해 객체를 수정하는 상황이 발생한다.
할인 금액 계산 로직을 확장했는데 상영 관련 로직이 동작하지 않는다 생각해보자. 이는 예측하기도 어려우며 변경을 두렵게 한다.
이처럼 결합도가 높은 코드는 유연하지 않다고 볼 수 있다.
객체 지향의 3가지 특징(캡슐화, 상속, 다형성) 중 하나이다. 나는 캡슐화 개념보단 이에 대한 오해와 중요한 이유를 서술해보고자 한다.
Movie.java
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
// getter, setter
...
}
캡슐화를 지키기 위해 필드를 private
로 선언하고 관련된 비지니스 로직을 수행하기 위해 getter, setter를 제공했다. 덕분에 상태에 대해 외부에서 접근이 불가능하게 되었다.
우리는 일반적으로 캡슐화를 지키기 위해 위와 같이 코드를 작성한다. 이게 과연 캡슐화를 만족한걸까?
이는 단순히 객체 내부의 데이터를 감추는 것일 뿐이다. 캡슐화는 여기서 더 나아가 변할 수 있는 모든 것을 감추는 것이다.
참고
위에서 적용한 캡슐화는 데이터 캡슐화로 캡슐화의 종류 중 하나일 뿐이다.
결론부터 말하자면 이는 캡슐화가 지켜지지 않은 코드이다. 이는 필드를 public
으로 두는 것과 별반 차이도 없을 뿐더러 캡슐화가 지향하는 궁극적 목표를 달성하지 못한다.
나는 캡슐화가 지켜졌는지 여부를 판단하기 위해선 캡슐화가 정확히 어떤 목표를 이루기 위해 사용하는지를 알아야 된다 생각한다.
캡슐화를 통해 우리는 변경의 파급 효과가 적은 코드를 얻을 수 있다. 예를 들어 하나의 객체 내부가 변경되었을 때 다른 객체의 변경을 최소화한다는 것이다. 이는 자연스럽게 낮은 결합도와 높은 응집도를 유발한다.
즉, 캡슐화는 객체간 높은 응집도와 낮은 결합도를 목표로 하므로 이를 만족한다면 캡슐화를 만족하는 것으로 볼 수 있다.
DiscountCondition.java (높은 결합도)
public class DiscountCondition {
private DiscountConditionType type;
private int sequence;
private DayOfWeek dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
public DiscountConditionType getType() {
...
}
public boolean isDiscountable(DayOfWeek dayOfWeek, LocalTime time) {
...
}
public boolean isDiscountable(int sequence) {
...
}
}
isDicountable()
메서드 파라미터는 현 객체(DiscountCondition
)의 필드를 외부에 간접적으로 노출한다.결합도가 높아졌기에 해당 객체의 내부(필드)를 변경하면 외부에도 이것이 전파된다. 이러한 파급효과는 캡슐화가 부족하다는 증거다.
Screening.java (낮은 응집도)
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
public Money calculateFee(int audienceCount) {
switch (movie.getMovieType()) {
case AMOUNT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculateAmountDiscountedFee().times(audienceCount);
}
break;
case PERCENT_DISCOUNT:
if (movie.isDiscountable(whenScreened, sequence)) {
return movie.calculatePercentDiscountedFee().times(audienceCount);
}
case NONE_DISCOUNT:
movie.calculateNoneDiscountedFee().times(audienceCount);
}
return movie.calculateNoneDiscountedFee().times(audienceCount);
}
}
Screening
, Movie
, DiscountCondtiion
)하나의 로직을 여러 곳에서 처리하므로 응집도가 낮아 변경 포인트가 많아졌다.
내가 지금까지 생각했던 캡슐화는 반쪽짜리 캡슐화였다. 데이터를 중심으로 객체를 설계했기에 캡슐화를 위반하여 낮은 응집도와 높은 결합도를 가진 코드가 탄생했다.
훌륭한 협력이 훌륭한 책임을 낳고 훌륭한 객체를 낳는다라는 말이 어떤 의미인지 알게된 것 같다. 협력이라는 문맥을 고려하지 않는다면 이처럼 객체끼리 독립적인 존재가 되어버려 유연하지 못한 코드가 된다.
객체는 단순한 데이터 제공자가 아니다. 필드를 여러 개 두고 getter/setter를 통해 비지니스 로직을 수행하는건 C언어의 구조체와 다를게 없다.
객체 설계 시 어떤 데이터를 포함하는가? 를 고민 후 데이터에 대해 어떤 동작을 해야하는지?를 반드시 고려하자.
또한, 객체 내부가 아닌 외부에 초점을 둬야 한다. 내부에 의존하도록 설계한다면 내부 구현 변경 시 협력하는 모든 객체가 영향을 받게 된다.