티스토리 뷰

이번 글은 같이 스터디를 진행한 친구의 글을 옮긴 것이다. 원글은 여기에서 볼 수 있다.

객체 지향 설계


객체 지향 설계의 핵심은 역할, 책임, 협력이다. 협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용이다. 책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동이고, 역할은 대체 가능한 책임의 집합이다.

객체 지향 설계올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다.

설계 품질 척도


캡슐화란?

객체 내부 구현을 외부로부터 감추는 것, 이렇게 하면 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않는다. 이것은 객체 지향이 강력한 이유 중 하나 이기도 하다.

변경될 가능성이 높은 부분을 구현, 상대적으로 안정적인 부분을 인터페이스라고 한다.

캡슐화가 중요한 이유는 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있기 때문이다.

응집도란?

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.

객체 지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

결합도란?

결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다.

객체 지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.

높은 결합과 낮은 응집을 테스트 하는 방법


높은 응집과 낮은 결합은 모두 설계를 변경하기 쉽게 만들기 위해 필요하다. 즉, 변경 사항이 미치는 영향을 통해 응집도와 결합도를 테스트 할 수 있다.

응집도 테스트

응집도는 한모듈이 변경되기 위해서 모듈의 내부에서 발생하는 변경의 정도를 통해 측정할 수 있다.

  • 하나의 요구사항을 반영하기 위해 오직 하나의 모듈만 수정한다면 높은 응집
  • 하나의 요구사항을 반영하기 위해 다수의 모듈을 수정해야 한다면 낮은 응집

 

결합도 테스트

결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도를 통해 측정할 수 있다.

내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 할 수 있다.

  • 하나의 요구사항을 반영하기 위해 다수의 모듈을 수정해야 한다면 높은 결합
  • 하나의 요구사항을 반영하기 위해 오직 하나의 모듈만 수정한다면 낮은 결합

즉, 응집도와 결합도는 모두 관점의 차이일 뿐 캡슐화를 통해 해결이 가능 하다.

결합도가 높아도 상관 없는 경우

  • 일반적으로 변경될 확률이 매우 적은 안정적인 모듈에는 의존해도 문제 없다.
  • 표준 라이브러리에 포함된 모듈, 성숙 단계에 접어든 프레임워크

ex) 자바의 String, ArrayList 같은 경우

캡슐화 위반


public class Movie {

	private Money fee;

	private MovieType movieType;

	public Money getFee() {
		return fee;
	}

	public void setFee(Money fee) {
		this.fee = fee;
	}

	public MovieType getMovieType() {
		return movieType;
	}

	public void setMovieType(MovieType movieType) {
		this.movieType = movieType;
	}
}

위 코드는 직접 객체의 내부에 접근할 수 없기에 캡슐화의 원칙을 지키고 있는 것처럼 보인다. 그러나 getFee 메서드와 setFee 메서드를 통해 데이터에 접근할 수 있기 때문에 캡슐화가 지켜 졌다고 말하기 어렵다. 이렇게 된 이유는 Movie객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다. 항상 기억하자 객체에게 중요한 것은 책임이다.

높은 결합도


public class ReservationAgency {
	public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
		...
		Money fee;
		if(discountable) {
			...
			fee = movie.getFee().minus(discountedAmount).times(audienceCount);
		} else {
				fee = movie.getFee();
			}
		...
	}
}

ReservationAgency는 한명의 예매 요금을 계산하기 위해 Movie의 getFee메서드를 호출하며 계산된 결과를 Money 타입의 fee에 저장한다. 이때 fee의 타입을 변경한다고 가정해 보자. 이를 위해서는 getFee 메서드의 반환타입도 함께 수정해야 할 것이다. 그리고 getFee 메서드를 호출하는 reservationAgency의 구현도 변경된 타입에 맞게 함께 수정해야 할 것이다.

fee의 타입 변경으로 인해 협력하는 클래스가 변경되기 때문에 Money와 ReservationAgency 는 강하게 결합되어 있다고 할 수 있다.

낮은 응집도


서로 다른 이유로 변경되는 코드가 하나의 모듈안에 공존할 때 모듈의 응집도가 낮다고 말한다. 따라서 각 모듈의 응집도를 살펴보기 위해서는 코드를 수정하는 이유가 무엇인지 살펴봐야 한다.

ReservationAgency에 다음과 같은 변경사항이 생겼다고 생각해보자.

  • 할인 정책이 추가될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 예매 요금을 계산하는 방법이 변경될 경우

낮은 응집도는 두 가지 측면에서 설계에 문제를 일으킨다.

  1. 변경과 아무 상관이 없는 코드들이 영향을 받게 된다.
    • 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.
  2. 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다.
    • 응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.

단일 책임 원칙(Single Responsibility Principle, SRP)


모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 제시된 원칙. 클래스는 단 한가지의 변경 이유만 가져야 한다는 것, 응집도를 높일 수 있는 설계 원칙이다.

**한 가지 주의할 점은 단일 책임 원칙이라는 맥락에서 ‘책임’이라는 말이 ‘변경의 이유’라는 의미로 사용된다는 점이다. 단일 책임 원칙에서의 책임은 지금까지 살펴본 역할, 책임, 협력에서 이야기 하는 책임과는 다르며 변경과 관련된 더 큰 개념을 가리킨다.

캡슐화를 지켜라!


스스로 자신의 데이터를 책임지는 객체

우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 기억하자 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

객체를 설계하기 전 다음의 두 가지 질문을 꼭 기억하자.

Movie 개선

public class Movie {
	private Money fee;
	private MovieType movieType;

	public MovieType getMovieType() {
		return movieType;
	}

	public boolean isDiscountable(LocalDateTime whenScreened, int sequence) {
		for (DiscountCondition condition : discountConditions) {
			if (condition.getType() == DiscountConditionType.PERIOD) {
				if (condition.isDiscountable(whenScreened.getDayOfWeek(), whenScreened.toLocalTime())) {
					return true;
				}
			} else {
				if (condition.isDiscountable(sequence)) {
					return true;
					}
				}
			}
		
		return false;
		}
	}

	public Money calculateAmountDiscountedFee() {
		if (movieType != MovieType.AMONT_DISCOUNT() {
			throw new IllegalArgumentException();
		}
		return fee.minus(discountAmount);
	}

	public Monney calculatePercentDiscountedFee() {
		if (movieType != MovieType.PERCENT_DISCOUNT) {
			throw new IllegalArgumentException();
		}
		return fee.minus(fee.times(discountPercent));
	}

	public Money calculateNoneDiscountedFee() {
		if (movieType != MovieType.NONE_DISCOUNT) {
			throw new IllegalArgumentException();
		}
		return fee;
	}
}

(어떤 데이터를 포함하는가?) Movie는 MovieType를 가지고 있다.

.(객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?) 그렇기 때문에 단순히 getter, setter를 통해 데이터를 제공하는 객체가 아닌, 할인 여부를 판단하고 가격을 계산하는 오퍼레이션을 포함 시켰다

캡슐화가 지켜졌을까?

이렇게 Movie객체 스스로가 어떤 데이터를 가지고 있는지 판단하고 스스로 처리하도록 변경하였다. 하지만 여전히 캡슐화의 관점에서 부족하다. 할인 정책의 종류를 인터페이스를 통해 노출시키고 있기 때문이다. 만약 새로운 할인 정책이 추가되거나 제거된다면 어떻게 될 것인가? 아마 이 메서드들에 의존하는 모든 클라이언트가 영향을 받을 것이다.(높은 결합) 따라서 Movie는 세 가지 할인 정책을 포함하고 있다는 내부 구현을 성공적으로 캡슐화하지 못한다. 또한 할인 조건이 변경되었다고 가정해보자. 할인 조건의 종류를 변경하기 위해서는 할인 조건을 판단하는 DiscountCondition, Movie, 그리고 Movie를 사용하는 객체까지 함께 수정해야 한다.(낮은 응집) 하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다는 것은 설계의 응집도가 낮다는 증거다.

정리


캡슐화란

한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않게 하기 위해 객체 내부 구현을 외부로 감추는 것, 올바른 캡슐화를 통해 높은 응집과 낮은 결합을 유지한다.

캡슐화를 지키기 위한 방법

객체란 상태와(데이터) 행동을(오퍼레이션) 하나의 단위로 묶은 것이다.

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

항상 가정하자

객체를 설계 후 설계된 객체가 캡슐화가 잘 이루어 졌는지 가상의 변경사항을 통해 테스트하는 습관을 가지자.

  • 반환 타입 변경해 보기
  • 파라미터 추가해 보기
  • getter, setter 최대한 없애 보기
댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday