캡슐화

0

데이터와 관련된 기능을 묶고 객체가 기능을 어떻게 구현했는지 외부에 감추는 것, Imfomation Hiding(정보 은닉)의 의미를 포함한다.

캡슐화를 하는 이유는 객체가 사용되는 부분에서 영향 없이 내부에서 변경하여 연쇄적인 변경을 감소시킬 수 있다.

캡슐화는 기능을 제공하고 구현을 숨긴다.

삭제 정책 요구사항
1. TODO 목록에 대한 삭제 기능을 수행한다
2. 권한은 소유자(작성자)만 할 수 있다.

if(user.getUserId() == todo.getOwnerId()) {
    deleteById(todo.getTodoId());
}

삭제 정책 요구사항 추가
1. TODO 목록에 대한 삭제 기능을 수행한다.
2. 삭제 권한은 소유자(작성자)만 할 수 있다.
3. 부적절한 콘텐츠는 관리자가 수정 및 삭제가 가능하다.
다만 물리적인 삭제가 아닌 숨김 처리로 변경하고 이력을 관리.. 어쩌고 저쩌고 블라블라

요구사항의 변화는 시스템의 데이터 구조 등 변화를 발생시킨다.

데이터를 사용하는 코드의 수정이 발생된다.

if(user.getUserId() == todo.getOwnerId() || user.gerRole() == UserRole.ADMIN) {
// ... 작업 명세
	if(user.getRole() == UserRole.ADMIN) {
    	disableById(todo.getTodoId());
    } else {
    	deleteById(todo.getTodoId());
    }
}

분기에 대한 캡슐화를 해보자

if(todo.isOwner(userId) || user.isAdmin()) {
	if(user.isAdmin()) {
    	repository.disableById(todo.getTodoId());
    } else {
    	repository.deleteById(todo.getTodoId());
    }
}
if(todo.hasDeletePermissions(user)) {
	if(user.isAdmin()) {
    	repository.disableById(todo.getTodoId());
    } else {
    	repository.deleteById(todo.getTodoId());
    }
}

뭐 별거인가? 금방 뚝딱이다.

하지만 권한 체크라는 것이 이 곳 한군데만 쓰일까?

그냥 개인이 쓰려고 만든 코드를 사용자들이 좋다고 막 해서 이것 저것 기능이 추가되기 시작한다면?

목록보기 조회 요구사항 추가
1. 열람 권한
.. 열람 권한 명세
2. 구독
.. 구독 기능 명세
3. 차단
.. 차단 기능 명세

관리자는 전체를 다 볼 수 있고 소유자는 본인 것만 볼 수 있다.
공개글, 비공개 글을 통해 열람 권한을 설정한다.
나를 구독 하고 있는 사용자는 공개글을 볼 수 있다.
내가 차단한 유저는 나의 게시글을 열람할 권한이 없다.
내가 보기 싫은 게시글은 숨김처리해야된다

이런 명세들이 늘어나면서 코드는 계속 변경된다.
소프트웨어는 변경(사용)되지 않으면 죽은, 망한 시스템이기 때문이다.
변경되는 사항들을 사람들은 어떻게 효과적으로 변경할 수 있을까? 고민해온 것이다.

캡슐화 하라는건 알겠는데 어떻게 해야하는데?

Tell, Don't Ask

  1. 데이터를 받아서 처리하지 말고 처리 해달라고 하기
if(user.getUserId() == todo.getOwnerId()) {}

직접 비교하지 않고

if(todo.hasPermission(user) {}

해당 객체에 권한이 있는 사용자인지 처리하는 메서드로 위임하기

Demeter's Law

  1. 메서드에서 생성한 객체의 메서드만 호출하기
  2. 파라미터로 받은 객체의 메서드만 호출하기
  3. 필드로 참조하는 객체의 메서드만 호출하기
if (user.getExpiredAt().isAfter(now))

or

LocalDateTime expiredAt = user.getExpiredAt();
if (expiredAt.isAfter(now))

만료 시점을 판별하는 코드가 있다고 했을 때, 데미테르의 법칙을 적용해보자.

if (user.isExpired())

User 객체의 메서드에서 호출하기

public class User {

    private LocalDateTime expiredAt;

	// expiredAt 변수가 null이거나 empty인 경우 isExpired() 메서드가 오류를 발생시킬 수 있음
	// expiredAt 변수는 LocalDateTime 타입이기  때문에 empty에 대한 상태를 가질수 없다.
	// 즉, NPE에 대한 에러만 예외처리를 하면 된다.
    public boolean isExpired() {
        if (this.expiredAt == null) {
            return false;
        }
        return this.expiredAt.isAfter(LocalDateTime.now());
    }
}

오 그럴싸하다, 근데 만료되지 않은 사용자가 아니라 만료되기 하루 전, 1주일 전, 한달 전
에 무언가 해야한다면? 조금만 더 고쳐보자

public class User {

    private LocalDateTime expiredAt;


    public boolean isExpired() {
        return isExpired(LocalDateTime.now());
    }

    public boolean isExpired(LocalDateTime expiredAt) {
        return this.expiredAt != null && this.expiredAt.isAfter(expiredAt);
    }

}

위의 수정된 코드에서 isExpired() 메서드는 현재 시간을 사용하여 isExpired(LocalDateTime expiredAt) 메서드를 호출하도록 수정했고 isExpired(LocalDateTime expiredAt) 메서드에서는 this.expiredAt 필드가 null이 아니면서, expiredAt보다 이후인 경우에만 true를 반환하도록 변경했다.

오 더 그럴싸해졌다

근데 사용하는 입장에서 알아야할까?

응 몰라도, 된다. 모르게 하려고 이렇게 하는거다

나 : 여러분 회원 만료일자 검증할 때 갖다 쓰세요~ 하고 말이다.

if (user.isExpired()) {} // 만료 시점 확인

// 만료 시점 1주일 전 확인
 LocalDateTime oneWeekAgo = LocalDateTime.now().minusWeeks(1);
 if (user.isExpired(oneWeekAgo)) {}

데미테르의 법칙을 따르다 보면 캡슐화에 가까워질 수 있다.

캡슐화는 기능의 구현을 외부에 감추고,
캡슐화를 한 메서드(기능)를 사용하는 블럭에서 영향을 최소화 할수 있고 캡슐화된 메서드,
즉 내부 구현의 변경을 통해 유연함을 가질 수 있다.

0개의 댓글