2장 객체 지향 5 캡슐화

inhalin·2022년 5월 11일
0

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴, 최범균

캡슐화란

캡슐화는 객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것이다.
구체적인 구현부를 감추면 좋은 점이 나중에 구현 방법이 수정되어도 기능을 쓰고 있는 코드는 고칠 필요가 없다.

5.1 절차 지향 방식 코드

회원의 서비스 만료 날짜 여부를 확인하는 코드가 있다. 만료 여부에 따라 제공되는 서비스나 보여지는 안내 페이지가 달라져야 한다.
회원 정보를 담고 있는 Member 클래스에는 만료일과 나이에 대한 정보가 들어있다.

public class Member {
	private Date expireDate;
    private int age;
    
    public Date getExpireDate() {
    	return expireDate;
    }
    
    public int getAge() {
    	return age;
    }
}

그리고 Member 객체를 가지고 만료 여부를 확인하는 코드가 있다. 현재 시간과 만료일을 비교해서 만료 여부를 확인한다.

if (member.getExpireDate() != null && 
	member.getExpireDate().getDate() < System.currentTimeMillis()) {
	//만료시 처리 내용
}

그런데 서비스를 잘 운영하다가 중간에 만료 조건에 대한 정책이 변경됐다. 이제는 회원의 나이에 따라 처리를 다르게 해주어야 한다. 18세 미만 청소년은 만료일 이후로 30일동안은 서비스를 더 이용할 수 있다.

long day30 = 1000 * 60 * 60 * 24 * 30; // 30일

if ((
		member.getAge() >= 18 
		&& member.getExpireDate() != null
		&& member.getExpireDate().getDate() < System.currentTimeMillis()
    ) 
    || 
    (
		member.getAge() < 18 
		&& member.getExpireDate() != null
		&& member.getExpireDate().getDate() < System.currentTimeMillis() - day30
	)) 
{
	// 만료시 처리 내용
}

만료 조건을 확인하는 코드는 여러 군데에서 사용중이기 때문에 모두 찾아서 변경해줘야 한다. 사용하는 곳이 많을수록 버그가 생길 가능성이 높아지고, 실수를 놓치고 지나친 후에 코드 수정 시기와 버그 발견 시기의 차이가 커질수록 버그 원인을 찾기가 힘들어진다.

다 찾아서 잘 바꿨더라도, 나중에 또 정책이 바뀌면? 기존에 있던 개발자가 퇴사했다면? 환장하는거다.

이게 데이터를 중심으로 프로그래밍했을 때의 문제점이다. 데이터를 직접적으로 사용하는 코드는 데이터 변화에 직접적으로 영향을 받는다. 데이터 구조나 쓰임이 조금만 바뀌어도 연관된 코드를 전부 다 수정해야 한다.

5.2 캡슐화된 기능 구현

위에서 절차 지향적으로 짠 코드를 객체 지향적 코드로 재구성해보자.
캡슐화는 뭐다? 기능의 구현을 숨기는거다. 여기서는 만료여부를 확인하는 부분을 숨긴다. 즉, 캡슐화한다. 만료 여부는 현재 시간과 만료일자를 비교해서 구한다.

public class Member {
	private Date expireDate;
    private int age;
    
    public boolean isExpired() {
    	return expireDate != null 
        		&& expireDate.getDate() < System.currentTimeMillis();
    }
}

이제 회원의 만료 여부를 확인할때 Member 클래스에 있는 isExpired() 메서드를 쓰면 된다. 어떤 조건으로 만료 여부를 결정하는지는 알 필요 없다.

if (member.isExpired()) {
	// 만료시 처리 내용
}

근데 어라? 만료 여부에 대한 정책이 변했다. 18세 미만은 만료일 이후 30일동안은 서비스를 더 이용할 수 있다. Member 클래스의 isExpired() 메서드를 수정해준다.

public class Member {
	// 생략... 
	private static final long DAY30 = 1000 * 60 * 60 * 24 * 30; // 30일
    
    public boolean isExpired() {
    	if (age < 18) {
        	return expireDate != null 
        			&& expireDate.getDate() < System.currentTimeMillis() - DAY30;
        }
        
    	return expireDate != null 
        		&& expireDate.getDate() < System.currentTimeMillis();
    }
}

만료 여부를 확인하기 위해 다른데서 isExpired()를 사용한 코드들은 수정할 필요가 없다. 앞으로 만료 여부를 확인하는 규칙이 어떻게 변하더라도 그냥 isExpired() 안에서만 수정해주면 된다.

5.3 캡슐화의 결과

캡슐화를 잘 할수록 더 쉽게 내부 기능 구현을 변경할 수 있다.

5.4 캡슐화의 규칙 두가지

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

Tell, Don't Ask.

데이터를 물어보는게 아니라(don't Ask), 기능을 실행해달라(tell)고 한다. 기능 실행을 요청하는 방식으로 코드를 작성하다 보면 자연스럽게 구현부가 감춰지고 캡슐화가 된다.

직접 데이터를 가져다가 뭘 하려고 하지 말고, 뭘 해서 얻으려고 하는 그걸 달라고 요청하는 방식을 쓰자.

Law of Demeter

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

이게 데미테르의 법칙을 구성하는 규칙들이다. 코드로 보면 좀 더 이해하기 쉽다.

데미테르 법칙 위반

if (member.getDate().getTime()) { 
    // ... 
}

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

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

신문 배달부 예제

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

코드를 설명하면, 신문배달부가 고객의 지갑을 직접 가져다가 돈이 있나없나 확인하고 있으면 돈을 꺼내간다.

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

public class Wallet {
	private int money;
    
    public int getTotal() { 
        return money; 
    }
    
    public int subtract(int debit) { 
        money -= debit; 
    } 
}

// 요금 받아가는 부분
int payment = 1000;

Wallet wallet = customer.getWallet();

if (wallet.getTotal() >= payment) {
	wallet.subtract(payment);
} else {
	// 돈이 부족함
}

이렇게 짜도 돌아가는데는 문제가 없지만 개념적으로 이상해진다. 지갑이 없으면 주머니를 뒤져서 가져가야 된다. 신문배달부는 고객이 돈을 어디에 어떻게 얼마나 보관하는지 알 필요가 없다. 고객이 직접 돈을 어디서든 가져다가 지불하면 된다.

public class Customer {
	private Wallet wallet;
    public int getPayment(int payment) {
    	if (wallet == null) throw new NotEnoughMoneyException();
        if (wallet.getTotal() >= payment) {
        	wallet.subtract(payment);
            return payment;
        }
	    throw new NotEnoughMoneyException();
    }
}

// Wallet 클래스는 위와 동일

//요금 받아가는 부분
int payment = 1000;
try {
	int paidAmount = customer.getPayment(payment);
} catch(NotEnoughMoneyException e) {
	// 돈이 부족함
}

마무리

데이터를 여기저기서 직접 처리하면서 생기는 버그는 다 잡아내기 진짜 힘들다. 여기서 고치고 저기서 고쳐서 오케이 하고 넘어갔는데 또 다른데서도 잘못된 게 계속 쓰이고 있어서 또 고치고 또 고치고...

캡슐화 그냥 이론적으로 공부할 땐 몰랐는데 실제로 회사에서 고생해보니 캡슐화의 중요성을 너무 잘 알겠다. 객체 지향 원칙을 잘 지키면서 개발하기가 쉽지 않다. 그러니까 보일 때마다 리팩토링 해주자.

0개의 댓글