응집도란?
응집도가 높은 구조?
이 책에서 다음과 같은 응집도가 낮은 구조에 대한 예시를 보여준다
지금부터 예시에 대한 문제점과 함께 이를 해결하는 방법에 대해 알아보자.
class OrderManager {
static int add(int money1, int money2) {
return money1 + money2;
}
}
// moneyData1, moneyData2는 데이터 클래스
moneyData1.amount = OrderManager.add(moneyData1.amount, moneyData2.amount);
static 메소드는 클래스의 인스턴스를 생성하지 않고도 호출 가능하다.
위 처럼 데이터 클래스와 함께 사용하는 경우가 흔하다.
static 메소드는 구조적으로 인스턴스 변수를 사용할 수 없다. (내부 인스턴스를 사용한다면 컴파일 에러가 발생한다.)
다시 말해 데이터(인스턴스 변수)와 이를 조작하는 로직(static 메소드)이 분리될 수 밖에 없어서 -> 응집도를 낮춘다.
class MoneyData {
private final int amount;
// 생성자 생략
public MoneyData add(MoneyData other) {
return new MoneyData(this.amount + other.amount);
}
}
MoneyData addedMoneyData = moneyData1.add(moneyData2);
위와 같이 인스턴스 변수와 인스턴스 변수를 사용하는 로직을 같은 클래스에 만드는게 응집도를 높이는 방법이다.
class PaymentManager {
private int discountRate; // unused instance variable
int add(int moneyAmount1, int moneyAmount2) {
return moneyAmount1 + moneyAmount2;
}
}
PaymentManager#add 메소드는 인스턴스 변수를 사용하지 않는다. 다시 말하면 static 메소드와 다를바가 없다.
이와 같은 메소드를 인스턴스 메소드 인척하는 static 메소드라 한다.
절차 지향적 언어의 접근 방식을 객체 지향 언어에 억지로 적용하려 하기 때문이다.
static 메소드의 사용은 응집도를 매우 낮추기 때문에 남용하지 않도록 주의해야 한다.
응집도에 영향을 받지 않는 경우 사용해도 좋다
class GiftPoint {
private static final int MIN_PONT = 0;
final int value;
GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException();
}
value = point;
}
}
// 표준 회원 가입 포인트
GiftPoint standardMembershipPoint = new GiftPoint(3000);
// 프리미엄 회원 가입 포인트
GiftPoint premiumMembershipPoint = new GiftPoint(10000);
위 처럼 생성자를 노출시키면 자연스럽게 초기화 로직이 분산될 수 밖에 없다.
초기화 로직 또한 로직이다.
로직이 분산되면 유지 보수가 힘들어진다.
private 접근제한자로 생성자를 은닉하여 클래스 내부에서만 인스턴스를 생성하도록 하는게 바람직하다
대신 외부에서 목적에 맞게 인스턴스를 생성할 수 있도록 팩토리 메소드를 제공하도록 하자.
회원 가입 포인트
가 각각 얼마인지 알 필요 없다. 상황에 맞게 제공된 팩토리 메소드를 호출하면 된다.class GiftPoint {
private static final int MIN_PONT = 0;
final int value;
private GiftPoint(final int point) {
if (point < MIN_POINT) {
throw new IllegalArgumentException();
}
value = point;
}
static forStandardMembershipPoint() {
return new GiftPoint(3000);
}
static forPremiumMembershipPoint() {
return new GiftPoint(10000);
}
}
// 표준 회원 가입 포인트
GiftPoint standardMembershipPoint = GiftPoint.forStandardMembershipPoint();
// 프리미엄 회원 가입 포인트
GiftPoint premiumMembershipPoint = GiftPoint.forPremiumMembershipPoint();
추후에 생성 로직이 너무 많아지면 클래스가 하는 일이 무엇인지 파악하기 어려워진다.
이럴 경우 생성 전용 팩토리 클래스
를 분리하는 방법을 고려해볼 수 있다.
똑같은 일을 수행하는 코드가 많아지면 코드를 재사용하기 위해서 범용 클래스를 만든다. 이때 static 메소드로 구현되는 경우가 많다.
같은 일을 수행하는 코드가 여러 군데 분산되어 있다
는 말은해당 코드가 한 곳에서만 수행할 수 있도록 응집도를 높이는 리팩토링이 필요하다
는 의미라 생각된다.
- 코드 중복 -> 범용 클래스를 생성하기? 응집도가 높도록 다시 설계하기!
다만 횡단 관심사에 해당하는 기능이라면 범용 코드로 만들어도 괜찮다
Logger.trace("traceId = " + generateId()); // 트레이스 아이디 찍기 (횡단 관심사1)
try {
doSomething(); // 비지니스 로직
} catch (IllegalArgumentException e) {
Logger.report("error occurs"); // 에러 로그 찍기 (횡단 관심사2)
}
class ActorManager {
void shift(Location location, int shiftX, int shiftY) {
location.x += shiftX;
location.y += shiftY;
}
}
class DiscountManager {
void set(MoneyData money) {
money.amount -= 200;
if (money.amount < 0) {
money.amount = 0;
}
}
}
메소드가 매개변수의 값을 변경하고 있다.
출력 매개변수
라 한다class Location {
final int x;
final int y;
Location(final int x, final int y) {
this.x = x;
this.y = y;
}
Location shift(final int shiftX, final int shiftY) {
return new Location(this.x + shiftX, this.y + shiftY);
}
}
출력 매개변수로 설계하지 말고 -> 데이터와 데이터 조작 로직을 같은 클래스에 배치하도록 하자.
int recoverMagicPoint(int currentMaxMagicPoint,
int originalMaxMagicPoint,
List<Integer> maxMagicPointIncrements,
int recoveryAmount) {
// do something
}
위 처럼 메소드에 너무 많은 매개변수를 전달 받으면, 로직 내부에서 잘못된 값을 대입할 가능성이 높다.
boolean, int, float, double, String처럼 프로그래밍 언어가 표준적으로 제공하는 자료형을 기본 자료형이라 한다.
recoverMagicPoint 메소드에서 매개변수와 리턴 값에 모두 기본 자료형을 사용하고 있다. 이렇게 기본 자료형을 남용하는 현상을 기본 자료형 집착(primitive obsession)
이라 한다.
기본 자료형은 중복 코드를 야기한다.
int regularPrice;
// regularPrice라는 기본 자료형에 대한 유효성 검사 로직이 여기저기 생기기 쉽다
class PriceUtil {
boolean isFairPrice(int regularPrice) {
if (regularPrice < 0) { // regularPrice에 대한 유효성 검사
throw new IllegalArgumentException();
}
}
}
class Common {
int discountPrice(int regularPrice, float discountRate) {
if (regularPrice < 0) { // regularPrice에 대한 유효성 검사
throw new IllegalArgumentException();
}
if (discountRate < 0.0f) { // discountRate에 대한 유효성 검사
throw new IllegalArgumentException();
}
return (int) regularPrice * discountRate;
}
}
Common#discountPrice 메소드
이처럼 기본 자료형으로만 구현하면 중복 코드가 많아진다.
새로운 자료형을 설계해라
기본 자료형에 대한 로직
과 값
을 하나의 클래스에 모을 수 있도록 새로운 자료형
을 설계해보자.
class RegularPrice { // 정가를 의미하는 새로운 자료형
final int amount;
RegularPrice(final int amount) {
if (amount < 0) {
throw new IllegalArgumentException();
}
this.amount = amount;
}
}
class DiscountedRate { // 할인율을 의미하는 새로운 자료형
final float rate;
DiscountedRate(final float rate) {
if (rate < 0.0f) {
throw new IllegalArgumentException();
}
this.rate = rate;
}
}
class DiscountedPrice {
final int amount;
DiscountedPrice(final RegularPrice regularPrice, final DiscountedRate discountedRate) {
this.amount = (int) regularPrice.amount * discountedRate.rate;
}
}
DiscountedPrice 생성자
int recoverMagicPoint(int currentMagicPoint,
int originalMaxMagicPoint,
List<Integer> maxMagicPointIncrements,
int recoveryAmount) {
// 1. 매직 포인트 최댓값 구하기
int currentMaxMagicPoint = originalMaxMagicPoint;
for (int each : maxMagicPointIncrements) {
currentMaxMagicPoint += each;
}
// 2. 매직 포인트 회복하기
final int recoverdMagicPoint = Math.min(currentMagicPoint + recoveryAmount, currentMaxMagicPoint);
// 3. 매직 포인트 결과 반환
return recoverdMagicPoint;
}
recoverMagicPoint 메소드의 파라미터들을 의미있는 단위의 클래스의 인스턴스 변수로 구성해보자.
class MagicPoint {
private int currentAmount;
private int originalMaxAmount;
private final List<Integer> maxIncrements;
int max() {
// 매직 포인트 최댓값 구하기
int amount = this.originalMaxAmount;
for (int each : this.maxIncrements) {
amount += each;
}
return amount;
}
void recover(final int recoveryAmount) {
// 매직 포인트 회복하기
currentAmount = Math.min(this.currentAmount + recoveryAmount, max());
}
int current() {
// 매직 포인트 결과 반환
return this.currentAmount;
}
}
void equipArmor(int memberId, Armor newArmor) {
if(party.members[memberId].equipments.canChange) {
party.members[memberId].equipments.armor = newArmor;
}
}
.
으로 여러 메소드를 연결하여 리턴 값의 요소에 차례차례 접근하는 방법을 메소드 체인
이라고 부른다. 이 방법도 응집도를 낮출 수 있어 좋지 않다.
party를 통해 members, equipments, canChange, armor에 접근 또는 값을 할당할 수 있다.
할당하는 코드가 여러 곳에 중복 작성될 가능성이 있다.
사용하는 객체 내부를 알아서는 안된다는
데메테르의 법칙
이 있다. 메소드 체인은 이 법칙을 위반한다고 할 수 있다.
묻지 말고, 명령하기(Tell, Don't Ask)
class Equipments {
private boolean canChange;
private Equipment head;
private Equipment armor;
private Equipment arm;
void equipArmor(final Equipment newArmor) {
if (canChange) {
this.armor = newArmor;
}
}
}
이를 가능케 하기 위해서 인스턴스 변수를 private
으로 변경하여 외부에서 접근하지 못하게 하자.
자연스럽게 인스턴스 변수를 조회하거나 조작하는 로직은 클래스 내부에 존재하게 된다.