반응형

두 법칙은 객체 간의 결합도를 효과적으로 낮추기 위한 방법 중 하나이다.
의존하는 객체에 단 한 번의 메소드 참조만 하는 것이다.
조금 더 풀어보면, 데이터 중심이 아닌 기능 중심으로 코드를 작성하는 것이다.

즉, 캡슐화를 해야한다는 것인데 이는 데이터에 의존하는 절자지향 방식을 하는 사람에게는
생각보다 아주 많이 어려운 방법이다. 그렇기 때문에 두 가지의 규칙이 있다.


Tell, Don't Ask

데이터를 물어보지 않고, 기능을 실행해 달라고 말하라는 규칙이다. 즉, 데이터를 나는 모르니까
너가 알아서 처리해줘!! 라는 것이다.

아래는 만료일자에 따라 어떠한 로직을 처리하는 코드이다.
먼저 절차 지향 방식의 코드를 살펴 보자.

내가 너의 데이터를 만져서 컨트롤 할게!!

나는 SI 재직 시절에 100% 이와 같은 코드로 되어 있었다. 코드 복잡할수록 한군데 고치면 다 망가지는 전형적인 코드이다.
결국 내부의 데이터를 알아야 하고, 내부의 구조가 바뀌면 영향이 가는 코드가 된다.

// member.getExpiryDate() -> 만료 일자 데이터를 가져옴
if (member.getExpiryDate() ! null &&
    member.getExpiryDate().getDate < System.currentTimeMillis()) {
  // 만료 되었을 때 처리
}

나는 너의 사정을 알고 싶지 않아! 너가 알아서 결과만 줘!

이번에는 위의 코드를 규칙을 지켜 리팩토링 해보겠다.
끝이다. 아주 간단하고, member의 내부 구조가 어떻게 바뀌든 전혀 상관이 없게 된다.
즉, 내부 사정을 전혀 모르고 요청만 한 것이다.

if (member.isExpired()) {
  // 만료 되었을 때 처리
}

이렇게 요청만 하도록 작성을 하면 구현 내용은 자연스레 감춰지게 되어 캡슐화가 되어 결합도가 낮아지게 된다.


디미터 법칙(Law of Demeter)

낯선 사람은 경계하고 친구랑만 놀라는 의미이다.

디미터 법칙은 Tell, Don't Ask 규칙을 따를 수 있도록 만들어 주는 또 다른 규칙이다.
규칙을 살펴 보자.

  • 메서드에서 생성한 객체의 메소드만 호출
  • 파라미터로 받은 객체의 메소드만 호출
  • 필드로 참조하는 객체의 메소드만 호출

파라미터로 받은 객체의 메소드만 호출에 대하여 예를 하나 들어보겠다.

파라미터로 전달 받은 member의 메소드인 getDate()를 호출 후 다시 getDate()가 리턴한 getTime()을 호출한다.
즉, member이외 객체의 메소드를 호출한 것이다. 이렇게 되면 내부에서 getDate()의 타입이 바뀌어 getTime()을 호출할 수 없다면?
연쇄 반응이 일어나게 되는 것이다. 이런한 코드를 기차 충돌이라고 한다. 여러 객차가 한 줄로 이어진 기차 같기 때문이다.

public void processSome(Member member) {
  if (member.getDate().getTime() < ...) { // 디미터 법칙 위반
    ....
  }
}

위 코드를 디미터 법칙을 따르도록 바꾸려면 member 객체에 대한 한 번의 메소드 호출로 변경을 해야 한다.
결국 데이터 중심이 아닌 기능 중심으로 코드 작성을 유도 하므로 캡슐화가 향상된다.
아래와 같이 바꿔 볼 수 있다.

public void processSome(Member member) {
  if (member.someMethod() < ...) { // 
    ....
  }
}

public class Member {

  public Date someMethod() {
      return getDate().getTime();  
  }
}

신문 배달부와 지갑

디미터 법칙과 Tell, Don't Ask 법칙에 유명한 예제가 신문 배달부와 지갑이라는 예제이다.
어떻게 코드가 리팩토링 되는지 살펴보자.

// 고객
public class Customer {
  private Wallet wallet;
  public Wallet getWallet() {
    return wallet;
  }
}

// 지갑
public class Wallet {
  private int money;
  private int getTotalMoney() {
    return money;
  }

  public void substractMoney(int debit) {
    money -= debit;
  }
}

이제 신문 배달부는 고객에게 요금을 받기 위해 아래와 같이 코드를 작성한다.
이 코드는 아주 이상하다. 고객의 지갑에서 배달부가 돈을 확인하고 꺼내간다.
이건 강도다! 배달부는 돈만 받으면 되고 돈이 있는지 없는지, 어디에 있는지 전혀 알 필요가 없다.
혹시 돈이 없다면 고객과 합의를 하면 된다. 오로지 배달부는 돈을 받는 것만 하면 되는 것이다.

만약 고객의 돈이 주머니에 있으면 해당 코드도 같이 바뀌게 된다. 캡슐화가 안됐기 때문이다.

// Paperboy 클래스
int payment = 10000; // 신문 가격
Wallet wallet = customer.getWallet(); // 고객의 지갑

if (wallet.getTotalMoney() >= payment) { // 지갑에 신문 가격 보다 돈이 많으면 돈 가져가기
  wallet.substractMoney(payment);
} else {
  // 돈이 없으니 다음에 가져오기
}

// 코드 정리
- 고객님 지갑 주세요 : customer.getWallet();
- 지갑에 돈 확인할게요 : wallet.getTotalMoeny >= payment
- 돈 뺄게요 : wallet.substractMoeny(payment)

이번에는 정상적으로 고객이 돈을 주도록 코드를 바꿔 보겠다.
Customer 클래스에서 돈을 빼는 처리를 하고, 배달부는 돈을 받기만 하고, 돈이 없을 경우 다음에 가져오는 처리를 한다.
즉, 고객은 돈을 확인하고 빼는 역할이고, 배달부는 돈을 받고, 돈이 없으면 다음에 가져오는 역할을 하는 것이다.
이렇게 되면 고객의 돈이 지갑에 있든, 집에 있든 배달부와는 전혀 상관이 없고 배달부의 코드도 변경이 필요없다.
이것이 바로 디미터 법칙과 Tell, Don't Ask 법칙을 지켜 캡슐화를 향상 시켰을 때의 장점이 된다.

// 고객
public class Customer {
  private Wallet wallet;

  public int getPayment(int payment) {
    if (wallet == null) {
      throw new NotEnoughMoneyException();
    }
    if (wallet.getTotalMoney() >= paymnet) {
      wallet.substractMoney(payment);
      return payment;
    }

    throw new NotEnoughMoneyException();
  }
}

// 배달부 : Paperboy
int payment = 10000;
try {
  int paidAmount = customer.getPayment(payment);
} catch (NotEnoughMoneyException ex) {
  // 돈이 없으니 다음에 가져오기
}
반응형

'기타 IT' 카테고리의 다른 글

템플릿 메서드 패턴  (0) 2021.12.29
전략(Strategy) 패턴  (0) 2021.12.23
POSIX의 EOF(End Of File) 규칙  (0) 2021.12.03
ec2에서 톰캣 서버 포트 포워딩 하기  (0) 2021.10.02
구글 oauth2 리다이렉션 400오류  (0) 2021.09.30
복사했습니다!