외부활동/우아한테크코스 [프리코스]

책임과 무한 리팩토링(정보 전문가 vs 단일책임원칙)

softmoca__ 2025. 10. 31. 21:57
목차

3주차 미션을 진행하며 기존에 알고 있던 설계 원칙들간의 모순(?) 충돌(?)과 트레이트 오프에 대해 고민하며 엄청난 리팩토링을 하게 되었다.

대부분의 나의 고민은 '책임'에 대한 시선이었다 !

결론적으로 책임을 바라보는 시선을 다르게 가지니 초반에 가진 모순에 대해 해결이 되는것 같다 !

하지만 이 또한 나의 편협한 시선일 뿐 이후 코드리뷰나 미션을 진행하며 언제든 바뀔 수 있다고 생각한다.

하지만 현재로서 나의 고민과 고민과정에서 어떻게 지금의 책임을 바라보는 시선을 하게 되었는지 흐름을 정리하고 자한다.

 

 

 

🤔 첫 리팩토링 후 만든 LottoResult

기본적인 기능 구현을 마친 뒤 리팩토링을 위해 다시금 코드를 하나씩 살펴 보았다. 그 과정에서 WinningStatistics에서 통계가 아닌 '결과'라는 개념 자체를 명시적으로 표현하며 수익률 계산의 책임을 분리할 수 있겠다고 생각해서 아래 와같은 LottoResult를 설계하게 되었다.

// 초기 설계
public class LottoResult {
    private final WinningStatistics statistics;
    private final PurchaseAmount purchaseAmount;
    
    public ProfitRate calculateProfitRate() {
        long totalPrize = statistics.calculateTotalPrize();
        return ProfitRate.of(totalPrize, purchaseAmount);
    }
    
    public WinningStatistics getStatistics() {
        return statistics;
    }
}

하지만 코드를 작성하고 보니 뭔가 어색했다.

// Controller
private void printResult(WinningNumbers winningNumbers, PurchaseAmount purchaseAmount) {
    WinningStatistics statistics = machine.check(winningNumbers);
    LottoResult result = new LottoResult(statistics, purchaseAmount);  // 🤔
    
    outputView.printStatistics(result.getStatistics());  // getter만 호출
    ProfitRate rate = result.calculateProfitRate();      // 단순 위임
    outputView.printProfitRate(rate.getValue());
}

 

리팩토링을 막상하고 나니 LottoResult가 실제로 하는 일이 거의 없었다 !getStatistics는 그냥 반환만 하고  record를 적용해보니 그저calculateProfitRate만 남아 유틸성 클래스처럼 느껴졌다.

또한  statistics와 purchaseAmount를 이미 가지고 있는데  다시 묶어서 새 객체를 만든다? 불필요한 객체를 억지로 만들었나? 라는 생각을 했다. 또한 다시 생각해보니 개념을 명확하게 하고자 만들었지만 의미가 모호하게 느껴졌다 "결과"가 정확히 뭘 의미하지 통계도 결과, 수익률도 결과 인데..??

 

"이 클래스 필요한가 ?"라는 질문으로 다시금 무한 리팩토링 여정을 떠났나며 각각 비교해 보았다.

 1: LottoResult 유지

  • ❌ 실제로 하는 일이 거의 없음
  • ❌ 단순 래퍼 클래스
  • ❌ 불필요한 객체 생성
  • ❌ "Result"의 의미가 모호

2: Controller가 직접 계산

  • ❌ Controller가 계산 로직을 가짐
  • ❌ Tell, Don't Ask 위반
  • ❌ 도메인 로직이 Controller로 누수

3: WinningStatistics가 계산 (최종 선택)

  • ✅ Information Expert 원칙 준수
  • ✅ Tell, Don't Ask 원칙 준수
  • ✅ 불필요한 중간 계층 제거
  • ✅ 도메인 로직이 도메인에 위치

LottoResult 제거 후 WinningStatistics에게 계산을 위임하기로 결정한 이유

1. Information Expert 원칙

"정보를 가장 많이 가진 객체가 책임을 져야 한다"

WinningStatistics는  각 등수별 당첨 개수, 총 상금 계산 방법,  통계 관련 모든 정보를 이미 알고 있다 !

수익률 계산에 필요한 총 상금또한 이미 알고 있고 구입 금액은 파라미터로 간단하게 받을 수 있다 !

2. Tell, Don't Ask 원칙

❌ 잘못된 방식 (Ask)

// Controller가 "물어보고" 직접 계산
long totalPrize = statistics.calculateTotalPrize();  // Ask
int amount = purchaseAmount.getAmount();             // Ask
ProfitRate rate = ProfitRate.of(totalPrize, purchaseAmount);  // 직접 계산

Controller가 내부 데이터를 꺼내서 직접 계산해서 캡슐화를 위반하며, Controller에 로직 누수가 발생한다 !

✅ 올바른 방식 (Tell)

// Controller가 "시킴"
ProfitRate rate = statistics.calculateProfitRate(purchaseAmount);  // Tell

WinningStatistics에게 계산을 위임해서 캡슐화를 유지하며 Controller는 흐름만 제어한다 ! 

3. 단일 책임 원칙은 어떻게?

public class WinningStatistics {
    // 책임 1: 통계 데이터 관리
    private final Map<Rank, Integer> rankCounts;
    
    // 책임 2: 통계 기반 계산
    public long calculateTotalPrize() { ... }          // 통계로 계산
    public ProfitRate calculateProfitRate(...) { ... }  // 통계로 계산
}

처음에는 수익률 계산 자체가 다른 책임이 아닌가 라는 생각이었다. 하지만 "통계 기반 계산"은 하나의 책임으로 볼수 있다는 걸 깨달았다 ! 결국에는 "통계를 활용한 계산"이라는 점에서 같은 범주이다 !

둘 다 "통계를 활용한 계산"이라는 같은 범주!

4. 응집도 분석

public class WinningStatistics {
    // 핵심 데이터
    private final Map<Rank, Integer> rankCounts;
    
    // 이 데이터로 할 수 있는 모든 계산
    public int getCountByRank(Rank rank)                      // 조회
    public long calculateTotalPrize()                         // 계산
    public ProfitRate calculateProfitRate(PurchaseAmount a)   // 계산
}

응집도 관점에서도 결국에는 모두 rankCounts 데이터를 활용하므로 높은 응집도 또한 유지 된다 !

5. 실제 도메인 개념과의 일치

1. 로또 구매
2. 당첨 번호 발표
3. 당첨 확인 (통계 작성)
4. 수익률 계산 ← "통계 보고서"의 일부!
"수익률"은 "통계"의 한 부분이다!

통계 보고서:
- 1등: 0개
- 2등: 0개
- 3등: 1개
- ...
- 총 상금: 1,500,000원
- 수익률: 62.5%

실제로 로또 판매점에서도 "당첨 통계와 수익률을 함께 출력"한다  !

 

 

 

단일 책임 원칙 다시 깊게 파고 들기

단일 책임 원칙  : "클래스를 변경하는 이유는 단 하나여야 한다"

WinningStatistics의 변경 이유는?

1. 통계 수집 방식 변경
   (예: Map 대신 다른 자료구조 사용)

2. 통계 계산 로직 변경
   (예: 총 상금 계산 방식 변경)

3. 수익률 계산 로직 변경
   (예: 수익률 공식 변경)

처음 3번은 다른 이유가 아닌가 ?
싶었지만 아니다 ! 결국 모두 "통계 관련 로직 변경" 이다 !

// ❌ 나쁜 예
public class WinningStatistics {
    // 통계 관련
    public long calculateTotalPrize() { ... }
    
    // 파일 I/O (완전히 다른 책임!)
    public void saveToFile(String filename) { ... }
    
    // 이메일 발송 (완전히 다른 책임!)
    public void sendEmail(String to) { ... }
}

진짜 위반한 사례는 극단적으로 위와 같다 !

 

 

 

 

 

핵심

"책임"의 정의

"메서드 하나가 곧 책임 하나"라는 이해는 잘못된 것 !
여러 개의 메서드가 있더라도, 그 메서드들이 서로 관련된 하나의 목적이나 데이터를 중심으로 동작한다면 그것은 여전히 하나의 책임이라 생각한다 !

예를 들어, Calculator 클래스의 add, subtract, multiply, divide 메서드는 각각 다른 연산을 하지만 모두 “계산”이라는 하나의 책임을 수행한다 !
마찬가지로 WinningStatistics 클래스의 getCountByRank, calculateTotalPrize, calculateProfitRate 메서드도 각각 다른 계산을 하지만 모두 “통계 관련 계산”이라는 하나의 책임을 수행하므로, 이는 하나의 책임으로 본다 !!

 

 

 

1. Information Expert는 강력한 원칙이다

"정보를 가진 객체가 책임을 진다"

WinningStatistics가:
- 통계 데이터를 가짐
- 총 상금을 계산할 수 있음
→ 수익률도 계산하는 것이 자연스러움

2. 단일 책임은 "메서드 개수"가 아니다

❌ 잘못된 이해: 메서드 하나 = 책임 하나
✅ 올바른 이해: 관련된 메서드들 = 하나의 책임

"통계 기반 계산" = 하나의 책임
- calculateTotalPrize()
- calculateProfitRate()

3. 도메인 개념과의 일치

"수익률"은 "통계 보고서"의 일부

통계와 수익률은 함께 제공되는 정보
→ WinningStatistics가 가지는 것이 자연스러움

 

 

 

 

마치며

처음에는 "결과"라는 개념을 명시적으로 표현하고 싶었다.

통계와 구입 금액을 하나로 묶어서 관리하면 책임이 분리될 것이라 생각했다.

하지만 막상 코드를 작성하고 보니 LottoResult는 실제로 하는 일이 거의 없는 단순한 래퍼 클래스에 불과했다 !

 

여러 방향에 대해 고민하며 각각의 장단점을 분석하고, 원칙들을 하나씩 적용해보며 나만의 답을 찾아보았다.

결국 WinningStatistics에게 수익률 계산 책임을 주는 것으로 결정했다! Information Expert 원칙에 따라 정보를 가진 객체가 책임을 지도록 했다 ! 그 결과 코드는 더 간결하고 명확해졌으며, 도메인 로직이 도메인 계층에 제대로 위치하게 되었다 !

 

원칙을 단순히 아는 것과 실제로 적용하는 것은 다르다는 것을 깨달았다!

불필요한 추상화는 과감히 제거해야 하며, 항상 실제 도메인 개념과 일치하는지 확인하는 과정이 너무 중요하다는 걸 깨달았다 !

 

 

 

돌이켜보면 무언가를 제거하는 것이 무언가를 추가하는 것보다 훨씬 더더 어려웠다.

3주차의 제거하는 리팩토링은  1,2주 미션 동안 새로운 걸 추가하는건 배운걸 바로 써먹으며 리팩토링 하는 것과 너무 많이 달랐다 !

기존 내가 알고 배웠던 것들이 얼마나 잘못되어있었던 것인가에 대한 두려움, 왜 그당시에는 이런 부분을 미처 생각하지 못하고 추가했을 까에 대한 자책.. 상당히 힘든 시간이었다..! 하지만 이런 과정에서 리팩토링의 본질에 한발 더 가까워진것 같다 !

더하는 것이 아는 제거할줄 아는 용기 ! 그런 용기를 이번 3주차를 통해 모든 시간을 갈아넣어 얻어 마무리하는 지금에서는 너무 뿌듯하다 !

 

역시나 객체지향 설계는 정답이 없는 것 같다.

하지만 원칙들을 이해하고 적용하다 보면, 점점 더 나은 방향으로 나아가고 있다는 확신이 든다 !

이번 리팩토링도 그런 여정의 한 부분이었다 !!