본문 바로가기
객체지향

🫥 디미터 법칙의 오해

by 순원이 2024. 11. 12.

❓고민: 데이터 활용 책임을 누구에게 맡길 것인가

방법 1. 내부 데이터 전달

private Rank calculateRankForLotto(Lotto lotto, WinningBalls winningBalls, BonusBall bonusBall) {
    // WinningBalls의 내부 데이터를 꺼내서 전달
    int sameWinningCount = lotto.matchWith(winningBalls.getLottoNumbers());
...
}

방법 2. 객체 자체 전달

private Rank calculateRankForLotto(Lotto lotto, WinningBalls winningBalls,      BonusBall bonusBall) {
    // 객체 자체를 전달
    int sameWinningCount = lotto.matchWith(winningBalls);
    ...
}

❗️결론

객체 자체를 전달하는 두 번째 방식이 더 좋습니다. 데이터 활용 책임을 누구에게 맡길 것인가? 이에 대한 대답은 당연히 그 데이터를 가지고 있는 객체입니다. 저도 알고 있었지만, 위 고민의 시작은 객체자체를 전달하면 Lotto가 WinningBalls와 BonusBall을 알게 되고 Lotto가 이 객체들과 결합이 생기는 거 아닌가? 라는 생각에서 출발했습니다. 그러나 이 생각은 디미터 법칙(Don't Talk to Strangers)을 완전 잘못 이해하고 있는 거였습니다.

  1. 디미터 법칙에 대한 오해
  • 객체 간 최소한의 지식만 공유해야 한다는 원칙을 '객체 의존성 최소화'로 오해
  • 메소드가 특정 객체에 종속되면 재사용성이 떨어진다고 잘못 이해
  • 객체를 전달하면 결합도가 높아진다는 잘못된 이해
  1. 디미터 법칙의 진정한 의미
  • 최소한만 알아야 한다는 건 "낯선 자에게 말하지 말라"는 원칙을 위한 말임
  • 객체의 내부 구조 노출 방지가 핵심
  • 객체가 자신의 데이터를 책임지고 관리하도록 하는 것이 목적
// 방법 1: 데이터를 받는 경우
class Lotto {
    public int matchWith(List<Integer> numbers) {
        // 단순 데이터 비교 로직
    }
}

// 방법 2: 객체를 받는 경우
class Lotto {
    public int matchWith(WinningBalls winningBalls) {
        // 객체 간 협력 로직
    }
}

만약 match 메소드가 WinngBalls에만 쓰고 싶지 않다면, 아래와 같이 인터페이스로 해결하면 됩니다.

interface Matchable {
    int matchWith(List<Integer> numbers);
}

class Lotto {
    public int matchWith(Matchable winner) {
        return winner.matchWith(this.numbers);
    }
}

class WinningBalls implements Matchable {
    @Override
    public int matchWith(List<Integer> numbers) {
        // 구현
    }
}

Tell, Don’t Ask

위 단계를 통해 이전보다 낫게 만들어줬지만, 객체를 넘겨주는 것만으로는 좋은 코드가 되지 않습니다. 그 객체가 직접 실행하게 만들어야 합니다.

// 안좋은 예시 - 데이터를 직접 꺼내서 처리
public class Lotto {
    private List<Integer> numbers;

    // 내부 데이터를 외부로 노출하는 getter
    public List<Integer> getNumbers() {  // 캡슐화 위반
        return numbers;
    }

    public boolean matchWith(WinningNumbers winningNumbers) {
        List<Integer> winningNumbersList = winningNumbers.getNumbers();  // 데이터를 꺼내서
        return this.numbers.containsAll(winningNumbersList);  // 직접 처리


// 좋은 예시 - 객체에게 메시지를 보내 협력
public class Lotto {
    private List<Integer> numbers;

    public boolean matchWith(WinningNumbers winningNumbers) {
        return winningNumbers.contains(this.numbers); // 메시지를 통한 협력
    }
}

public class WinningNumbers {
    private List<Integer> numbers;

    public boolean contains(List<Integer> numbersToCheck) {
        // 자신의 데이터를 스스로 처리
        return this.numbers.containsAll(numbersToCheck);
    }
}

+) 디미터 법칙과 Tell, Don’t Ask는 다르다

// 디미터 법칙 위반
public class Order {
   private Customer customer;

   public void processOrder() {
       // customer -> account -> billingInfo 순으로 객체 탐색 (기차 충돌)
       if (customer.getAccount().getBillingInfo().isValidPaymentMethod()) {
           // 주문 처리
       }
   }
}

// 디미터 법칙 준수
public class Order {
   private Customer customer;

   public void processOrder() {
       // customer와만 직접 대화
       if (customer.canProcessPayment()) {
           // 주문 처리
       }
   }
}

디미터 법칙을 공부하기 위해 여러 블로그들을 찾아봤는데 두 법칙을 혼용하는 경우가 많았습니다. 물론 둘 다 객체 지향적으로 만들기 위한 좋은 법칙이라는 건 변함이 없기 때문에 문제가 되진 않습니다. 그래도 두 법칙이 뭘 말하고자 하는지, 명확한 의도는 알아야 한다고 생각합니다.

  • Tell, Don’t Ask: 다른 객체의 데이터를 물어보지말고 기능을 실행해달라고 말하는 것입니다.
    • 다른 객체(B)의 속성을 꺼내지 말고 B의 메소드를 호출하라는 뜻입니다.
  • 디미터 법칙: 다른 객체(B)의 상태에 포함되어 있는 또 다른 객체(C)의 메소드를 직접 이용하지 말라는 것입니다.
    • Tell, Don’t Ask 법칙을 지킨다면 애초에 디미터 법칙이 지켜지는 것입니다!

객체 지향에 대해 공부하다 보면, 기계적으로 학습할 때가 많았습니다. 이번 기회를 통해 기계적 학습이 아닌 객체지향의 본질을 알고 공부해야겠다는 생각을 할 수 있었습니다.

+) 객체지향의 본질은 객체 분리가 아닌 메시징을 주고받는 객체들의 협력입니다. 이 말이 객체지향을 체험하는데 영향을 줄거라 믿습니다.

'객체지향' 카테고리의 다른 글

🚫 else 금지 이유와 개선 법  (0) 2024.11.12
🤔 일급 컬렉션을 써야하는 이유  (0) 2024.11.12
Google Java Style Guide 번역  (1) 2024.10.15
[클린코드] 5 ~ 10장  (0) 2024.03.04
[클린코드] 4. 주석  (0) 2024.03.04