Java

객체지향적 설계란 무엇일까?

한 객체에 기능이 많아지면 절차 지향적인 구조를 갖게 된다. 

(절차 지향적인 구조를 갖게 되면 기능과 관련된 데이터를 공유하기 때문에 유연하지 못한 구조를 갖게 된다.)

따라서 객체가 갖는 책임의 크기는 작아질수록 유연함을 얻을 수 있다. (관련 원칙 : SRP)

 

의존이 순환해서 발생할 경우 꼬리에 꼬리를 문 것 처럼 영향이 전파된다.

(변경은 의존 관계를 따라 전이된다.)
내가 변경되면 나에게 의존하고 있는 코드에 영향을 준다.

나의 요구가 변경되면 내가 의존하고 있는 타입에 영향을 준다. 그러므로! 캡슐화가 필요하다.

기능 구현을 캡슐화하면 내부 구현이 변경되더라도, 기능을 사용하는 곳의 영향을 최소화할 수 있다.

 

캡슐화를 위한 두 개의 규칙

  • Tell, Don’t Ask

데이터를 물어보지 않고, 기능을 실행해달라고 말하라는 규칙. 

데이터를 직접 가져오지 않고 (ex. boolean isExpired = member.getExpiredDate();) 데이터 대신에 기능을 실행해달라고 명령을 내리기 위해 만료 일자 데이터를 가진 객체에게 만료 여부를 확인해달라고 요청한다. (ex. if(member.isExpired())
즉, 데이터를 가진 객체에게 물어보자!!! (왤케 잘 안지켜질까? ㅠㅠ)

 

  • 데미테르의 법칙

  1. 메서드에서 생성한 객체의 메서드만 호출

  2. 파라미터로 받은 객체의 메서드만 호출

  3. 필드로 참조하는 객체의 메서드만 호출

if(member.getDate().getTime()) 는 데미테르 법칙을 위반했다. Member의 getDate() 메서드를 호출한 뒤, 다시 getDate()가 리턴한 Date객체의 getTime 메서드를 호출했기 때문이다. 이를 개선하기 위해서는 한 번의 member 객체 메서드 호출로 변경해야한다.

 

데미테르 법칙을 잘 이해할 수 있는 유명한 예제라는 PaperBoy 예제를 가져왔다. (유명하다는데 왜 나는 몰랐을까? ㅎㅎ)

 

Paperboy 예제로 알아보는 데미테르 법칙

신문 배달부가 고객에게 요금을 받아가는 상황을 코드로 만들어볼 것이다. 이전에, 고객과 지갑 클래스를 다음과 같이 만들 수 있다.

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

// 지갑
public class Wallet {
    private int money;
    public int getTotalMoney() { return money; }
    public void substractMoney(int debit) { money -= debit; }
...
}

신문 배달부 클래스는 고객에게 요금을 받기 위해 다음과 같은 코드를 작성할 것이다.

int payment = 10000;
Wallet wallet = customer.getWallet();
if (wallet.getTotalMoney() >= payment) {
    wallet.substractMoney(payment);
} else {
    // 다음에 받으러 올게요.
}

이 코드를 읽어보자면, 다음과 같다.

먼저 신문 배달부는 고객의 지갑을 가져온다. 가져온 고객의 지갑에 돈이 있는지 확인한다. 돈이 지불할 금액만큼 존재한다면 돈을 빼간다. 없다면 다음에 받으러 온다. 🤣 무언가 이상하지 않은가? 현실에서 이런 일이 일어난다면, 신문 배달부는 강도 취급을 받을 것이다.

 

신문 배달부 입장에서는 고객이 지갑을 가졌는지, 돈을 손에 쥐고 있는지 여부는 중요치 않다. 단지 고객에게 돈을 받아가기만 하면 된다.

따라서 코드는 다음과 같이 변경할 수 있다. 

// 변경된 고객 클래스
public class Customer {
    private Wallet wallet;
    
    public int getPayment(int payment) {
        if (wallet == null) throw new NotEnoughMoneyException();
        if (wallet.getTotalMoney() >= payment) {
            wallet.substractMoney(payment);
            return payment;
        }
        throw new NotEnoughMoneyException();
    }
}

// 변경된 신문 배달부 클래스
int payment = 10000;
try {
    int paidAmount = customer.getPayment(payment);
} catch (NotEnoughMoneyException e) {
    // 다음에 받으러 올게요.
}

이전에 작성했던 코드와 비교해본다면, 이전 코드는 데미테르의 법칙을 위반하고 있다. 

// 이전 코드
Wallet wallet = customer.getWallet();
if (wallet.getTotalMoney() >= payment) {
    wallet.substractMoney(payment);
}

Paperboy 클래스 내부에서 작성된 코드이기에, customer 객체의 메서드만 사용해야하지만 customer를 통해 불러온 지갑 객체도 사용하고 있다. (삐--)

데미테르의 법칙을 지키지 않는 전형적인 증상에는 1. 연속된 get 메서드 호출 (찔린다.) 2. 임시 변수의 get 호출이 많음. 이다.
만약 당신의 코드가 value = someObj.getA().getValue(); 와 같은 형태를 띄고 있다면.. 데미테르 법칙을 지키고 캡슐화를 할 수 있도록 수정해보자.