TIL

[Book] 객체지향과 디자인패턴_캡슐화(encapsulation)

뱅타 2022. 6. 2. 09:06

[객체지향과 디자인패턴] 캡슐화(encapsulation)

요즘 출근길에 개발자가 반드시 정복해야할 객체지향과 디자인패턴을 읽고 있습니다. 아직 초반부를 읽는 중인데 책을 읽다 보니 캡슐화에 대해서 설명이 되어 있더군요. 곰곰히 생각해보니 저는 캡슐화에 대해서 상세하게 알고 있지 않은 상태였습니다! 캡슐화 == '메소드로 빼기' 정도로만 인지하고 있더군요. 그래서 책에 나와 있는 내용을 바탕으로 캡슐화에 대해 정리해 보았습니다.

캡슐화

객체 지향을 처음 접하는 사람들은 무심결에 데이터 중심적인 절차 지향 방식 습관으로 코드를 짜는 습관을 가지고 있습니다. 이러한 습관을 고치는데 도움이 되는 두 개의 규칙이 존재합니다.

캡슐화를 위한 두 개의 규칙

  1. Tell, Don't Ask
  2. 데미테르의 법칙(Law of Demeter)

"Tell, Don't Ask"

"Tell, Don't Ask"는 데이터를 물어보지 않고 기능을 실행해 달라고 말하는 규칙입니다. 회원 만료 여부를 확인하는 코드로 예를 들어 보겠습니다.

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

데이터를 읽는 것은 데이터를 중심으로 코드를 작성하게 만드는 원인입니다. 따라서 데이터를 읽는 방식으로 코드를 작성하게 되면 절차 지향적인 코드를 유도하게 됩니다.

따라서 데이터 대신 기능을 실행해 달라고 명령을 내려야 합니다. 이 기능을 실행하기 위해 만료 일자 데이터를 가진 객체에게 만료 여부를 확인해 달라고 해야 합니다.

if(member.isExpired()){
    // 만료에 따른 처리
}

위와 같이 기능 실행을 요청하는 방식으로 코드를 작성하다 보면, 자연스럽게 해당 기능을 어떻게 구현했는지 여부가 감춰집니다. 즉, 기능 구현이 캡슐화 되는 것입니다.

데이테르의 법칙(Law of Demeter)

데미테르의 법칙은 Tell, Don't Ask 규칙을 따를 수 있도록 만들어 주는 다른 규칙입니다.

  • 메서드에서 생성한 객체의 메서드만 호출
  • 파라미터로 받은 객체의 메서드만 호출
  • 필드로 참조하는 객체의 메서드만 호출
public void processSome(Member member){
    if(member.getDate().getTime() < ...){ // 데미테르 법칙 위반
        ...
    }
}

위 코드는 데미테르법칙의 파라미터로 받은 객체의 메서드만 호출을 어긴 코드입니다.

파라미터로 전달받은 member의 getDate() 메서드를 호출한 뒤, 다시 getDate()가 리턴한 Date 객체의 getTime() 메서드를 호출하기 때문입니다.

따라서 데미테르의 법칙을 따르려면, 위 코드를 member 객체에 대한 한 번의 메서드 호출(member.getSomeThing())으로 변경해 주어야 합니다.

// member.getDate().getTime()
// 데미테르의 법칙을 지키기 위해 한 번의 member 객체 메서드 호출로 변경하는게 좋습니다.
member.someMethod()

예제

신문 배달부와 지갑

데미테르의 법칙을 설명할 때 사용되는 유명한 예제라고 합니다.

아래의 코드(신문배달부가 고객에게 요금을 받아 가는 상황)를 통해 캡슐화에 대해 조금 더 알아보겠습니다.

// 고객
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;
    }
    ...
}

고객에게 요금을 받기 위한 신문 배달부 클래스

// Paperboy 클래스의 코드
int payment = 10000;
Wallet wallet = customer.getWallet();
if(wallet.getTotalMoney() >= payment){
    wallet.substractMoney(payment);
}else{
    // 다음에 요금 받으러 오는 처리
}

위의 코드는 프로그램을 실행시킬 때 아무런 문제가 없지만 개념적으로 조금 이상한 부분이 있습니다. 위의 프로세스를 정리해 보겠습니다.

  1. 고객님 지갑 주세요 : customer.getWallet()
  2. 고객님의 지갑에 돈이 있는지 확인해 보겠습니다 : wallet.getTotalMoney() >= payment
  3. 지갑에서 돈을 빼 가겠습니다 : wallet.substractMoney(payment)

이상한 점이 눈에 들어오시나요? 바로 신문배달부 입장에서는 고객이 지갑을 가졌는지 또는 돈을 주머니에 보관하는지 여부는 중여하지 않습니다. 고객에게서 요금만 받아가면 되니까요.

그럼 조금 더 현실적으로 코드를 바꿔 보겠습니다.

// 신문 배달부가 직접 지갑을 뒤지지 않고 고객이 돈을 지불하는 방식
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();
    }
}

Customer 클래스의 getPayment() 메서드는 지갑에 요청한 금액의 돈이 있으면 그 만큼의 돈을 지불하고 없으면 NotEnoughMoneyException을 발생합니다.

// Paperboy 클래스는 아래와 같이 변경됩니다.
// Paperboy 클래스
int payment = 10000;
try{
    int paidAmount = customer.getPayment(payment);
    ...
} catch(NotEnoughMoneyException ex){
    // 다음에 요금 받으러 오는 처리
}

위의 코드에서 중요한 점은 이제 신문 배달부가 더이상 고객의 지갑을 뒤지지 않는다는 점입니다.

앞에서 작성했던 코드와 비교해 보겠습니다.

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

// 변경된 코드
int paidAmount = customer.getPayment(payment);

이전 코드는 데미테르의 법칙 중 메서드에서 생성한 객체의 메서드만 호출를 어기고 있습니다.

Paperboy 클래스 입장에서 customer 객체의 메서드만 사용해야 하는데, customer 객체를 통해서 가져온 wallet 객체의 메서드를 호출하고 있습니다. 반면 변경된 코드는 데미테르의 법칙에 따라 customer 객체의 메서드만 호출하고 있습니다.

만약 지갑이 null이 될 수 있거나 지갑이 아니라 주머니에서 돈을 꺼내서 준다거나 할 때, 이전 방식의 코드는 영향을 받습니다만 변경된 코드는 영향을 받지 않습니다. 변경된 코드는 데미테르의 법칙을 지키기 위해 자연스럽게 Customer 객체의 getPayment() 메서드를 호출하는 것으로 바뀌었고, 이것이 비용 지불 기능에 대한 캡슐화를 향상시켜 변경에 영향을 받지 않게 된 겁니다.

데미테르의 법칙을 지키지 않는 전형적인 두 가지 증상.

  • 연속된 get 메서드 호출
  • 임시 변수의 get 호출이 많음.

연속된 get 메서드 호출

value = someObject.getA().getB().getValue();

임수변수에 할당된 객체의 get을 호출하는 코드가 많은 경우

A a = someObject.getA();
B b = a.getB();
value = b.getValue();

두 가지 증상이 보인다면 데미테르의 법칙을 어기고 있을 가능성이 높으며 이는 곧 캡슐화를 약화시키는 원인이 될 수 있습니다. 따라서 위의 두 가지 증상이 보인다면 데미테르의 법칙을 어기고 있는지 잘 확인해 보고 관련 기능을 적극적으로 캡슐화하도록 노력해야 합니다.

긴 글 읽느라 고생하셨습니다. 해당 글은 개발자가 반드시 정복해야할 객체지향과 디자인패턴의 Chapter 02 객체 지향 50~55p를 주관적인 견해를 첨부해서 정리한 글입니다.

감사합니다.

References

https://en.wikipedia.org/wiki/Law_of_Demeter

728x90
반응형