응집도란?
모듈 내부에 있는 데이터와 로직 사이의 관계가 얼마나 강한지 나타내는 지표입니다.(모듈을 클래스라고 생각하기)
응집도가 높은 구조는 변경하기 쉬우며, 바람직한 구조입니다. 반대로 응집도가 낮은 구조는 변경 시 문제가 발생하기 쉽습니다.
응집도를 낮추는 경우
1. static 메서드 오용
static 메서드를 사용하는 이유는 객체 지향 언어를 사용할 때, C 언어 같은 절차 지향 언어의 접근 방법을 사용하려 하기 때문입니다. 절차 지향 언어에서는 데이터와 로직이 따로 존재하도록 설계합니다. 이러한 접근 방법을 객체 지향 언어에 적용하여 설계하면, 데이터와 로직을 별도의 클래스에 배치하게 됩니다. 그래서 클래스의 인스턴스를 생성하지 않고도 사용할 수 있는 static 메서드를 활용하는 것입니다.
static 메서드를 사용하면 좋을 때 (응집도의 영향을 받지 않는 경우)
- 로그 출력 전용 메서드
- 포멧 변환 전용 메서드
하지만, 응집도가 낮아지는 문제를 일으키므로, 남용하지 않는 것이 좋습니다.
2. 초기화 로직이 분산 되어있는 경우
표준 회원으로 신규 가입했을 경우 3000포인트, 프리미엄 회원으로 신규 가입을 했을 경우 10000포인트 부여하는 코드 입니다.
//표준 회원
GiftPoint standardMemberPoint = new GiftPoint(3000);
//프리미엄 회원
GiftPoint premiumMemberPoint = new GiftPoint(10000);
생성자를 public으로 만들면 회원가입 포인트를 바꾸고 싶을 경우 소스 코드 전체를 확인해야 합니다.
[해결]
private 생성자와 팩토리 메서드를 사용해 목적에 따라 초기화할 수 있도록 코드를 만듭니다.
class GiftPoint{
private static final int MIN_POINT = 0;
private static final int STANDARD_MEMBER_POINT = 3000;
private static final int PREMIUM_MEMBER_POINT = 10000;
final int value;
//private로 외부에서는 인스턴스를 생성할 수 없습니다.
//클래스 내부에서만 생성할 수 있습니다.
private GiftPoint(final int point){
if(point < MIN_POINT){
throw new IllegalArgumentException("포인트는 0 이상이어야 합니다.");
}
value = point;
}
//일반 가입
static GiftPoint forStandardMembership(){
return new GiftPoint(STANDARD_MEMBER_POINT);
}
//프리미엄 가입
static GiftPoint forPremiumMembership(){
return new GiftPoint(PREMIUM_MEMBER_POINT);
}
}
생성자를 private로 만들면, 클래스 내부에서만 인스턴스를 생성할 수 있습니다. 인스턴스를 생성하기 위한 static 팩토리 메서드에서 생성자를 호출합니다. 팩토리 메서드는 목적에 따라 만들어 두는 것이 일반적입니다.
(forStandardMembership, forPremiumMembership 메서드를 팩토리 메서드라고 합니다)
이렇게 구성하면 신규 가입 포인트 사양이 변경 됬을 때, GiftPoint 클래스만 변경하면 됩니다.
생성 로직이 많아질 경우는 팩토리 클래스를 만들어 분리하는 방법을 고려하는 것이 좋습니다.
3.범용 처리 클래스(Common/Util)
똑같은 일을 수행하는 코드가 많아지면 코드를 재사용하기 위해 범용 클래스를 만들곤 합니다. 이때 static 메서드로 구현되는 경우가 많습니다. 꼭 필요한 경우가 아니면 범용 처리 클래스를 만들지 않는 것이 좋습니다.
[개선 전]
//범용 처리 클래스
class Common {
//생략
//세금 포함 금액 계산하기
static BigDecimal calcAmountIncudingTax (BigDecimal amountExcludingTax, BigDecimal taxRate){
return amountExcludingTax.multiply(taxRate);
}
}
[개선 후]
세금 포함 금액을 계산하는 클래스를 별도로 만들어서 사용한다.
class AmountIncludingTax{
final BigDecimal value;
AmountIncludingTax(final AmountExcludingTax amountExcludingTax, final TaxRate taxRate){
value = amountExcludingTax.value.multiply(taxRate.value);
}
}
횡단 관심사(cross-cutting concern) 란?
로그 출력과 오류 확인 처럼 다양한 상황에서 넓게 활용되는 기능을 뜻합니다.
- 로그 출력
- 오류 확인
- 디버깅
- 예외 처리
- 캐시
- 동기화
- 분산 처리
횡단 관심사에 해당하는 기능이라면 범용 코드로 만들어도 괜찮습니다.
4. 결과를 리턴하는 데 매개변수 사용하지 않기
[개선 전]
class ActorManager{
//게임 캐릭터 위치를 이동
void shift(Location location, int shiftX, int shiftY){
location.x += shiftX;
location.y += shiftY;
}
}
전달한 매개변수 location의 값을 변경하고 있습니다. 이처럼 출력으로 사용해 버리면, 매개변수가 입력인지 출력인지 메서드 내부의 로직을 확인해야 합니다.
메서드의 내용을 하나하나 확인하게 만드는 구조는 로직을 일고 이해하는 데 시간이 오래 걸려, 가독성이 좋지 않습니다.
[개선 후]
Location shift(final int shiftX, final int shiftY){
final int nextX = x + shiftX;
final int nextY = y + shiftY;
retrun new Location(nextX, nextY);
}
5. 매개변수가 너무 많은 경우
너무 많은 매개변수를 받는 메서드는 실수로 잘못된 값을 대입할 가능성이 높습니다.
메서드에 매개변수를 전달한다는 것은 해당 매개변수를 사용해서 어떤 기능을 수행하고 싶다는 의미입니다. 그래서 매개변수가 많다는 것은 많은 기능을 처리하고 싶다는 의미가 됩니다.
하지만 처리할 게 많아지면 로직이 복잡해지거나, 중복 코드가 생길 가능성이 높아집니다.
1) 기본 자료형에 대한 집착
boolean, int, float, double, String처럼 프로그래밍 언어가 표준적으로 제공하는 자료형을 기본 자료형(primitive type)이라고 합니다.
int recoveryMagicPoint(int currentMagicPoint, int originalMaxMagicPoint
, List<Integer> maxMagicPointIncrements, int recoveryAmount){
int currentMaxMagicPoint = originalMaxMagicPoint;
for(int each : maxMagicPointIncrements){
currentMaxMagicPoint += each;
}
return Math.min(currentMagicPoint + recoveryAmount, currentMaxMagicPoint);
}
기본 자료형에 집착하면, 코드 중복이 쉽게 발생합니다. 물론, 기본 자료형만으로도 '동작하는 코드'를 작성할 수 있습니다. 하지만 그렇게 구현하면, 관련 있는 데이터와 로직을 집약하기 힘듭니다. 따라서 버그가 생기기 쉽고, 가독성이 떨어집니다.
class MagicPoint{
private int currentAmount;
private int originalMaxAmount;
private final List<Integer> maxIncrements;
//생략
//매직 포인트 최댓값
int max(){
int amount = originalMaxAmount;
for(int each : maxIncrements){
amount+= each;
}
return amount;
}
//매직포인트 회복하기
void recover(final int recoveryAmount){
currentAmount = Math.min(currentAmount + recoveryAmount, max());
}
//매직포인트 소비하기
void consume(final int consumeAmount){...}
}
이렇게 하면 관련 있는 로직을 각각의 클래스에 응집할 수 있습니다. 매개변수가 많으면 데이터 하나하나를 매개변수로 다루지 말고, 그 데이터를 인스턴스 변수로 갖는 클래스를 만들고 활용하는 설계로 변경해 보세요.
6. 메서드 체인
void equipArmor(int memberId, Armor newArmor){
if(party.members[memberId].equipments.canChange){
party.members[memberId].equipments.armor = newArmor;
}
}
.(점)으로 여러 메서드를 연결해서 리턴 값의 요소에 차례차례 접근하는 방법을 메서드 체인이라고 부릅니다.
위 코드에서 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){
armor = newArmor;
}
}
//전체 장비 해제하기
void deactiveateAll(){
head = Equipment.EMPTY;
armor = Equipment.EMPTY;
arm = Equipment.EMPTY;
}
}
상세한 로직은 호출하는 쪽이 아니라, 호출되는 쪽에 구현합니다.
'일상 > 레벨업 독서' 카테고리의 다른 글
[내 코드가 그렇게 이상한가요?] 7장-컬렉션:중첩을 제거하는 구조화 테크닉 (0) | 2024.08.28 |
---|---|
[내 코드가 그렇게 이상한가요?] 6장-조건 분기 : 미궁처럼 복잡한 분기 처리를 무너뜨리는 방법 (0) | 2024.07.16 |
[내 코드가 그렇게 이상한가요?] 4장 불변 활용하기 : 안정적으로 동작하게 만들기 (0) | 2024.07.04 |
[내 코드가 그렇게 이상한가요?] 3장 모든 것과 연결되는 설계 기반 (0) | 2024.07.02 |
[내 코드가 그렇게 이상한가요?] 2장 설계 첫걸음 (0) | 2024.07.01 |