CH05 : 응집도
CHAPTER 5 : 응집도
범위 : 60p ~ 84p
단계 : 실전
제목 : 응집도, 흩어져 있는 것들 내용 : 코드가 여러 곳에 분산되는 문제를 해결하는 방법을 다룹니다.
INTRO
이 장에서는 응집도에 대해서 집중적으로 다룹니다.
응집도(cohension)
란 모듈 내부에 있는 데이터와 로직 사이의 관계가 얼마나 강한지 나타내는 지표이다.
- 모듈은 클래스, 패키지, 레이어 등을 모두 포함할 수 있는 용어이다.
- 이 책에서는 쉽게 모듈을 클래스라고 생각
따라서 응집도
를 클래스 내부
에 있는 데이터와 로직 사이의 관계가 얼마나 강한지 나타내는 지표
로 설명
- 응집도가 높은 주고는 변경하기 쉬우며, 바람직한 구조이다. 반대로 응집도가 낮은 구조는 변경 시 문제가 발생하기 쉽다.
응집도가 낮은 구조의 대표적인 예
- 데이터 클래스 및 데이터 클래스 외에도 응집도를 낮추는 악마들이 존재한다.
5.1 static 메서드 오용
5.1.1 static 메서드는 인스턴스 변수를 사용할 수 없음
static 메서드는 인스턴스 변수를 사용할 수 없다.
- 따라서 어떤 메서드를 static 메서드로 만든 시점에 이미 데이터와 데이터를 조작하는 로직 사이에 괴리가 생갑니다. (당연히 응집도가 낮아짐)
클래스가 스스로를 보호할 수 있게, 데이터와 로직을 모아 응집도가 높은 구조로 설계하는 것이 객체 지향 설계의 기본
5.1.2 인스턴스 변수를 사용한 구조로 변경하기
응집도는 ‘클래스 내부에서 데이터와 로직의 관계가 얼마나 강한지 나타내는 지표’
- 따라서 ‘인스턴스 변수’와 ‘인스턴스 변수’를 사용하는 로직을 같은 클래스에 만드는 것이 응집도를 높이는 방법이다.
응집도가 높은 구조가 될 수 있도록 인스턴스 변수를 사용해서 계산하는 구조로 설계를 변경하는 것이 좋다.
5.1.3 인스턴스 메서드인 척하는 static 메서드 주의하기
static 키워드가 붙어있지 않을 뿐, static 메서드와 같은 문제를 갖고 있는 인스턴스 메서드도 자주 볼 수 있다.
class PaymentManager{
private int discountRage; // 할인율
int add(int moneyAmount1, int moneyAmount2) {
return moneyAmount1 + moneyAmount2;
}
}
위 코드는 인스턴스 변수 discountRate를 전혀 사용하지 않음, static 키워드를 붙여도 아무 문제 없이 동작할 것
- 이처럼 인스턴스 메서드인 척 하는 static 메서드도 응집도를 낮춘다.
5.1.4 왜 static 메서드를 사용할까?
static 메서드를 사용하는 이유
- 객체 지향 언어를 사용할 때, C 언어 같은 절차 지향 언어의 접근 방법을 사용하려 하기 때문이다.
static 메서드는 클래스의 인스턴스를 만들지 않아도 되므로, 간단하게 사용할 수 있다. 하지만 응집도가 낮아지는 문제를 일으키므로, 남용하지 않는 것이 좋다.
5.1.5 어떠한 상황에서 static 메서드를 사용해야 좋을까 ?
static 메서드를 사용하면 좋은 상황
- 응집도의 영향을 받지 않는 경우, static 메서드를 사용해도 괜찮다.
- 예를 들어 로그 출력 전용 메서드, 포맷 변환 전용 메서드처럼 응집도와 관계없는 기능은 static 메서드로 설계하는 것이 좋다.
5.2 초기화 로직 분산
클래스를 잘 설계해도, 초기화 로직이 분산되어 응집도가 낮은 구조가 되어 버리는 경우가 있다.
// 코드 5.4 기프트 포인트를 나타내는 클래스
class GiftPoint {
private static final int MIN_POINT = 0;
final int value = 0;
public GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException("포인트를 0 이상 입력해야 됩니다.");
}
}
GiftPoint add(final GiftPoint other) {
return new GiftPoint(value + other.value);
}
boolean isEnough(final ConsumptionPoint point) {
return point.value <= value;
}
GiftPoint consume(final ConsumptionPoint point) {
if (!isEnough(point)) {
throw new IllegalArgumentException("포인트가 부족합니다");
}
return new GiftPoint(value - point.value);
}
}
GiftPoint 클래스는 기프트 포인트 관련된 로직이 데이터와 응집되어있는 것처럼 보이나 실상 그렇지 않다.
// 표준 회원 가입 포인트
GiftPoint standardMemberPoint = new GitfPoint(3000);
// 프리미엄 회원 가입 포인트
GiftPoint premiumMembershipPoint = new GitfPoint(10000);
생성자를 public으로 만들면
의도하지 않은 용도로 사용될 수 있다. 결과적으로 관련된 로직이 분산되기 때문에 유지보수하기 힘들어진다.
- 예를 들어 회원 가입 포인트를 변경하고 싶을 떄, 소스 코드 전체를 확인해야한다.
5.2.1 private 생성자 + 팩토리 메서드
로 목적에 따라 초기화하기
이러한 초기화 로직의 분산을 막으려면 생성자를 private으로 만들고, 대신 목적에 따라 팩토리 메서드를 만든다.
class GiftPoint {
private static final int MIN_POINT = 0;
private static final int STANDARD_MEMBERSHIP_POINT = 3000;
private static final int PREMIUM_MEMBERSHIP_POINT = 10000;
int value = 0;
private GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException("포인트를 0 이상 입력해야 됩니다.");
}
this.value = point;
}
public static GiftPoint forStandardMembership(){
return new GiftPoint(STANDARD_MEMBERSHIP_POINT);
}
public static GiftPoint forPremiumMembership(){
return new GiftPoint(PREMIUM_MEMBERSHIP_POINT);
}
...
}
생성자를 private으로 만들면, 클래스 내부에서만 인스턴스를 생성할 수 있습니다.
- 인스턴스를 생성하기 위한 static 팩토리 메서드에서 생성자를 호출한다.
- 팩토리 메서드는 목적에 따라 만들어 두는 것이 일반적이다.
GiftPoint standardMembershipPoint = GiftPoint.forStandardMembership(); GiftPoint premiumMembershipPoint = GiftPoint.forPremiumMembership();
5.2.2 생성 로직이 너무 많아지면 팩토리 클래스를 고려하기.
상황에 따라 생성 로직이 너무 많아질 수 있다. 그러면 해당 클래스가 무엇을 하는 클래스인지 알기 어렵다.
- 많은 생성 로직으로 인해 해당 클래스가 하는 일이 불분명해지기 떄문이다.
생성 로직이 너무 많아지는 것 같다면,생성 전용 팩토리 클래스를 분리하는 방법
을 고려하는 것이 좋다.
5.3 범용 처리 클래스 (Common/Util)
static 메서드를 빈번하게 볼 수 있는 클래스로, 범용 처리를 위한 클래스가 있습니다.
- 일반적으로 이러한 클래스에는 Common, Util이라는 이름이 붙어있다. 문제는 static 메서드와 마찬가지로 응집도가 낮은 구조가 만들어질 수 있다는 것이다.
똑같은 일을 수행하는 코드가 많아지면 코드를 재사용하기 위해 범용 클래스를 만들곤 한다. 이 때 static 메서드로 구현되는 경우가 많다.
자주 사용되는 메서드일 테니, 범용 처리 클래스에 구현해 두면 코드가 중복되는 일을 줄일 수 있을 것이다. 하지만 결국 static 메서드이므로 응집도가 낮은 구조라는 문제를 가지고 있다.
- 참고 static 메서드가 응집도를 낮추는 문제만 가져오지는 않는다.
5.3.1 너무 많은 로직이 한 클래스에 모이는 문제
Common 클래스 안에 여러 로직이 모여있고 심지어 모두 static 메서드이다. 이는 응집도가 낮은 구조이며 안타깝게도 이런 코드는 실제 프로덕션 코드에서도 굉장히 많이 볼 수 있다.
어째서 이런 일이 일어나는 걸까?
- 원인은 Common과 Util 이라는 이름 자체가 ‘범용’이라는 뜻이기 때문이다.
- 이 이름은 읽는 사람에게 ‘범용적으로 사용하고 싶은 로직은 Common 클래스에 모아 두면 되겠구나’라고 생각하게 만든다.
근본적인 원인은
- 범용의 의미와 재사용성을 잘못 이해하고 있기 때문이다.
- 재사용성은 설계의 응집도를 높이면 저절로 높아진다.
5.3.2 객체 지향 설계의 기본으로 돌아가기
꼭 필요한 경우가 아니면, 범용처리 클래스를 만들지 않는 것이 좋다. 객체 지향 설계의 기본으로 돌아가서 설계하도록 하자.
5.3.3 횡단 관심사
로그 출력과 오류 확인은 애플리케이션의 모든 동작에 필요한 기능이다.
이처럼 다양한 상황에서 넓게 활용되는 기능을 횡단 관심사(cross-cutting concern)라고 부른다. 대표적으로 다음과 같은 기능들이 예시다.
- 로그 출력
- 오류 확인
- 디버깅
- 예외 처리
- 캐시
- 동기화
- 분산 처리
횡단 관심사에 해당하는 기능이라면 범용 코드로 만들어도 괜찮다.
5.4 결과를 리턴하는데 매개변수 사용하지 않기
출력으로 사용되는 매개변수를 출력 매개변수라고 부른다.
매개 변수는 입력으로 전달하는 것이 일반적이다. 출력으로 사용해버리면 매개변수가 입력인지 출력인지 메서드 내부의 로직을 확인해야한다. 메서드의 내용을 하나하나 확인하게 만드는 구조는 로직을 읽고 이해하는데 시간이 오래 걸려, 가독성이 좋지 않다.
출력 매개변수로 설계하지 말고, 객체지향 설계의 기본으로 돌아가서 “데이터”와 “데이터를 조작하는 논리”를 같은 클래스에 배치하자.
5.5 매개변수가 너무 많은 경우
매개변수가 너무 많은 메서드는 응집도가 낮아지기 쉽다.
너무 많은 매개변수를 받는 메서드는 실수로 잘못된 값을 대입할 가능성이 높다. (P76)
메서드에 너무 많은 매개변수를 받는 문제는 왜 생기는 걸까?
- 메서드에 매개변수를 전달한다는 것은 해당 매개변수를 사용해서 어떤 기능을 수행하고 싶다는 의미이다.
- 그래서 매개변수가 많다는 것은 많은 기능을 처리하고 싶다는 의미가 된다.
- 하지만 처리할 게 많아지면 로직이 복잡해지거나, 중복 코드가 생길 가능성이 높아진다.
5.5.1 기본 자료형에 대한 집착
기본 자료형 집착(primitive obsession)
- 기본 자료형을 남용하는 현상을 기본 자료형 집착 이라고 한다.
기본 자료형만 써 온 개발자는 클래스 설계를 고려하지 않는 경우가 많다. 그래서 기본 자료형 집착에 빠지기 쉽다.
- “이 정도를 집착이라고 할 수 있나? 일반적인 구현 스타일인 것 같은데?” 또는 “클래스를 많이 만드는 것이 오히려 이상해보이는데?”라고 생각하는 독자도 있을 수 있다. 하지만 잘못된 생각이다.
기본 자료형으로만 구현하면
중복 코드가 많이 생기며 계산 로직이 이곳저곳에 분산되기 쉽다.
- 데이터는 단순히 존재하기만 할 순 없다.
- 데이터를 사용해 계산하거나 데이터를 판단해서 제어 흐름을 전환할 때 사용한다. 기본 자료형으로만 구현하려고 하면, 데이터를 사용한 계산과 제어 로직이 모두 분산된다. 응집도가 낮은 구조가 되는 것이다. (P78)
5.5.2 의미있는 단위는 모두 클래스로 만들기
매개변수가 너무 많아지는 문제를 피하려면, 개념적으로 의미 있는 클래스를 만들어야 한다.
매개변수가 많으면 데이터 하나하나를 매개변수로 다루지 말고, 그 데이터를 인스턴스 변수로 갖는 클래스를 만들고 활용하는 설계로 변경해보자.
5.6 메서드 체인
.(점)으로 여러 메서드를 연결해서 리턴 값의 요소에 차례차례 접근하는 방법을 메서드 체인이라고 부른다.
- 이 방법도 응집도를 낮출 수 있어 좋지 않은 작성 방법이다.
데메테르의 법칙
- 사용하는 객체 내부를 알아서는 안된다는 법칙이다.
- 메서드 체인 내부 구조를 돌아다닐 수 있는 설계는 데메테르의 법칙을 위반한다고 볼 수 있다.
5.6.1 묻지 말고 명령하기
소프트웨어 설계에는 ‘묻지 말고, 명령하기(Tell, Don’t Ask)’라는 유명한 격언이 있다.
- 이는 다른 객체의 내부 상태(변수)를 기반으로 판단하거나 제어하려고 하지 말고, 메서드로 명령해서 객체가 알아서 판단하고 제어하도록 설계하라는 의미이다.
인스턴스 변수를 private으로 변경해서, 외부에서 접근할 수 없게 합니다. 그리고 인스턴스 변수에 대한 제어는 외부에서 메서드로 명령하는 형태로 만듭니다. 상세한 판단과 제어는 명령을 받는 쪽에서 담당하게 된다.
Leave a comment