본문 바로가기
OOP

2장) 캡슐화

by Tom_dev 2023. 5. 5.

앞서 객체와 절차 지향, 객체 지향을 보고 오셨다면 객체를 통해서 객체 지향을 활용한 첫 번째, 캡슐화에 대해 설명하겠습니다.

 

캡슐화

  • 데이터 + 관련 기능 묶기
  • 객체가 기능을 어떻게 구현했는지 외부에 감추는 것
    • 구현에 사용된 데이터의 상세 내용은 외부에 감춤
    • 실제 구현에 사용된 데이터의 타입, 값을 어떻게 사용하는지 구현과 관련된 상세 내용을 감춘다.

캡슐화하지않은 예시

// 초창기 서비스 
// REGULAR 상태이며 만료일이 지나지 않았다면 
if(acc.getMembership() == REGULAR && acc.getExpDate().isAfter(now())) {
	.. 정회원 기능 
} 

// 서비스 런칭 이후 (5년 이상 장수했음)
// 5년 이상 사용자 일부 기능 정회원 혜택 1개월 무상 제공
if(acc.getMembership() == REGULAR && (
	(acc.getServiceDate().isAfter(fiveYearAgo) && acc.getExpDate().isAfter(now())) ||
    (acc.getServiceDate().isBefore(fiveYearAgo) && addMonth(acc.getExpDate()).isAfter(now()))
	)) {
	.. 정회원 기능 
}
  • 요구사항의 변화가 데이터 구조/사용에 변화를 발생시켰다
    (초창기 : 멤버십 상태 + 만료일자 → 멤버십 + 만료 일자 + 회원가입 날짜)로 요구사항이 추가 된다.
    • 절차 지향의 데이터를 공유하는 방식의 문제점 → 요구사항 변화에 따라 데이터의 사용이 변화한다. → 수정해야 하는 코드가 연쇄적으로 발생
    • 초창기 Account는 오직 멤버십의 상태 그리고 만료일만 갖고 판단하였다.
  • 요구 사항 변경 예시
    • 장기 사용자에게 특정 기능 실행 권한을 연장(단 유효 일자는 그대로 유지)
    • 계정을 차단하면 모든 실행 권한 없음
    • 자바 7의 Date를 자바 8의 LocalDateTime으로 변경

유저의 회원가입 날짜(getServiceDate)를 사용해야 하는 요구사항의 변화가 데이터를 사용하는 코드의 구조에 변경을 발생시켰다. 

이는 다시 말해 요구사항의 변화가 데이터 구조/사용에 변화를 발생시켰고 수정을 위해서는 기존 데이터를 사용하는 코드를 위와 같이 전부 수정해야 한다 

캡슐화를 했다면

  • 캡슐화란? 데이터 + 기능
  • 기능은 그대로 제공하고 구현 상세를 감춘다.

아래 초창기 코드와 요구사항에 따라 변경된 코드를 외부/ 내부에서 볼 때에 맞춰 작성했다. 다음과 같이 확인해 보자.

초창기 코드

<초창기 코드> 

// 외부에서 볼 때의 모습 
if(acc.hasRegularPermission()) {..정회원 기능 } 
 
// 내부에서 볼 때 
// 필드에 맴버 변수들의 데이터와 hasRegularPermission() 기능 제공
public class Account{ 
	private Membership membership; // Data
	private Date expDate;          // Data

	public boolean hasRegularPermission() { // 기능 
		return membership == REGULAR && expDate.isAfter(now()); 
	}
}

 

위 코드는 초창기 유저 상태를 "멤버십 상태 + 만료일자" 정보를 사용하여 확인하는 코드이다. 

기존 절차지향적인 코드와는 다르게 Account 객체 내부에서 hasRegularPermission 메서드를 사용했고 메소드 내부(메서드의 블랙박스)에서 "멤버십 상태 + 만료일자" 요구사항을 정의했고 그 결괏값을 리턴한다. 

 

요구사항이 변경된 코드 

<요구사항이 적용된 코드> 

// 외부에서 볼 때의 모습(변경 없음)
if(acc.hasRegularPermission()) {..정회원 기능 } 

// 내부에서 볼 때 
// Account 클래스의 내부 기능 구현만 변경되고 그 기능을 사용하는 다른 코드는 변경하지 않는다.(위와 같이)

public boolean hasRegularPermission() {
  return membership == REGULAR && 
		(expDate.isAfter(now()) || 
			( 
			 serviceDate.isBefore(fiveYearAgo()) && 
			 addMonth(expDate).isAfter(now())
			)
	  );
}

놀랍게도 5년이 지나 요구사항의 변경이 생겼지만 외부에서 보는 acc.hasRegularPermission()은 변경되지 않았다.
오직 Account 클래스의 hasRegularPermission 메서드 내부의 요구사항만이 변경되었다.

 

위와 같이 캡슐화는 연쇄적인 변경 전파를 최소화할 수 있다.

 

객체에서 제공하는 메서드의 내부만 변경하기만 한다면 외부에서의 변화는 최소화 될 것이다.

 

캡슐화와 기능

  • 캡슐화 시도 → 기능에 대한 (의도) 이해를 높일 수 있다.
if(acc.getMembership() == REGULAR) { } // 이 사람이 왜 멤버십이 REGULAR와 같은지 검사하는 실제 이유는 뭘까 

-> 캡슐화 이후 

if(acc.hasRegularPermission()) {..}  // Account(계정)클래스에서 계정이 REGULAR 권한을 가졌는지 확인하기 위함

public class Account {
	public boolean hasRegularPermission() {..}
}

 

캡슐화를 위한 규칙

  • Tell, Don’t Ask
    • 데이터를 요청하지 말고 객체 내부에서 처리해서 리턴 받기
    • 사용자가 유효한지 검사하는 조건이 여러 서비스에 구현되어 있다면, 해당 조건이 변경될 때마다 사용자를 검사하는 모든 서비스의 코드를 수정해야 할 것이다. 따라서 이런 조건은 객체 내부 메서드에서 직접 작성한다.
if(acc.getMembership() == REGULAR) {..정회원 기능} // 데이터를 가져와서 판단하는 것이 아니라
-> if(acc.hasRegularPermission()) {..정회원 기능}  // 객체에서 기능으로 지원하기
  • Demeter’s Law
    • 메서드에서 생성한 객체의 메서드 하나만 호출
    • 파라미터로 받은 객체의 메서드 하나만 호출
    • 필드로 참조하는 객체의 메서드 하나만 호출
acc.getExpDate().isAfter(now()); Account에서 연속적으로 부르는 방식(X) -> acc.isExpired();
Date date = acc.getExpDate(); Account에서 연속적으로 부르는 방식(X) -> acc.isValid(now());
date.isAfter(now()); Good!

정리

  • 기능명세를 외부에 감춘다. (메서드의 블랙박스)
  • 캡슐화를 잘할수록 연쇄적인 변경 전파가 줄어들어 수정 비용이 낮아진다.
  • 캡슐화를 통해 기능을 사용하는 코드에 영향을 주지 않고 (또는 최소화) 내부 구현을 변경할 수 있는 유연함

캡슐화 예제

 

예제 1번

다음 중 캡슐화를 통해 리팩토링이 가능한 라인은?

public AuthResult authenticate(String id, String pw) {

	Member mem = findOne(id);
	if(mem == null) return AuthResult.NO_MATCH;  // 매칭 되지 않음
	
	if(mem.getVerificationEmailStatus() != 2) {  // 상태코드가 2일 경우 이메일 인증 되지 않음
		return AuthResult.NO_EMAIL_VERIFIED;
	}
	
	if(passwordEncoder.isPasswordValid(mem.getPassword(), pw, mem.getId())) {
		return AuthResult.SUCCESS; // 성공 
	}

	return AuthResult.NO_MATCH; // 매칭되지 않음
}
if(mem.getVerificationEmailStatus()!= 2) {
   return AuthResult.NO_EMAIL_VERIFIED;
}

정답 : 위 코드는 TDA (Tell, Don’t Ask)의 적용을 통해 리팩토링을 할 수 있는 것으로 보인다. 데이터를 요청받고 요청받은 Status가 ≠ 2인지 판단하는 행위는 추후 조건(status의 상태코드가 3으로 변경 등)의 변경이 발생할 경우 모든 조건을 변경해야 할 수도  있기 때문에 판단 자체를 메서드 내부의 기능으로 구현하여 요청받는 객체 내부에서 요청과 동시에 기능으로 처리한다. 

'OOP' 카테고리의 다른 글

1장) 객체, 그리고 객체 지향  (0) 2023.05.05
intro) 왜 객체지향을 배워야 할까?  (0) 2023.05.05