반응형

💡 Oreilly의 Implementing Domain-Driven Design, 조영호님의 객체지향의 사실과 오해, 로버트 C. 마틴의 Clean Code을 인용하여 두 원칙을 설명하고 있습니다. 자세한 출처는 포스팅 하단에 적어두겠습니다.
이 글은 킹갓제너럴 동료 조셉님이 인싸이트를 제공해주어 작성하게 되었습니다 (🙌 감사)

디미터 법칙(Law of Demeter [Appleton, LoD])묻지 말고 시켜라(Tell, Don’t ASK [PragProg, TDA]Aggregates 을 구현할 때 사용할 수 있는 설계 원칙이며, 두 가지 원칙 모두 메시지를 먼저 결정하고 객체가 메시지를 따르게 하도록 설계하여 정보 은닉을 강조한다.

송신자는 수신자가 어떤 객체인지 모르지만 자신이 전송한 메시지를 잘 처리할 것이라는 것을 믿고 메시지를 전송할 수 밖에 없다. 이런 스타일의 협력 패턴은 '묻지 말고 시켜라' 라는 이름으로 널리 알려져 있다.
결과적으로 이 스타일은 객체를 자율적으로 만들고 캡슐화를 보장하며 결합도를 낮게 유지시켜 주기 때문에 설계를 유연하게 만든다.
샌디 메츠는 '묻지 말고 시켜라' 스타일이란 "메시지가 '어떻게' 해야 하는 지를 지시하지 않고 '무엇'을 해야 하는지를 요청"하는 것이라고 설명한다. 결과적으로 메시지 송신자와 수신자의 결합도가 낮아지기 때문에 설계를 좀 더 유연하게 만들 여지가 많아지고 의도 역시 명확해진다.

둘 다 객체 간의 결합도를 낮추고, 자율성을 높여 서로 협력할 수 있도록 하는 것이 목적이다.
각 원칙에 대해 간단히 설명하고 이 원칙들을 따르도록 리팩토링하는 예제를 살펴보도록 한다.


디미터의 법칙(Law of Demeter)

디미터의 법칙 혹은 데메테르의 법칙으로 불리고 있다.

이 가이드 라인은 최소한의 지식 원칙(The Principle of Least Knowledge) 을 강조한며 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다. 객체는 자료를 숨기고 함수를 공개한다. 즉, 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미다. 그러면 내부 구조를 숨기지 않고 노출하는 셈이니까.

클라이언트 객체와 이 객체가 책임을 수행하기 위해 사용하는 어떤 객체를 생각해보자. (이 두번째 객체를 서버라고 가정해도 된다.)
클라이언트 개체가 서버 개체를 사용할 때 서버 구조에 대해 가능한 적게 알아야 한다.
서버의 속성(attributes)과 모양(properties)은 클라이언트에 의해 완전히 알려지지 않은 상태로 유지되어야 한다.
클라이언트는 서버의 표면 인터페이스(surface interface)에 선언된 명령을 수행하도록 서버에 요청할 수 있다.
클라이언트는 서버에 접근하여 내부를 묻고 해당 내부에게 명령을 내려서는 안된다.
클라이언트가 서버의 내부에 의해 렌더링되는 서비스가 필요한 경우, 클라이언트는 반드시 서버 내부의 접근 권한을 부여받으면 안된다. 대신 서버 객체는 오직 표면 인터페이스(surface interface)를 제공해야한다.

디미터 법칙의 기본적인 요약은 다음과 같다.
"클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다"

  • 클래스 C
  • f가 생성한 객체
  • f 메서드에게 전달되는 모든 매개변수
  • C 인스턴스 변수에 저장된 객체

묻지 말고 시켜라(Tell, Don’t Ask)

이 가이드라인은 단순히 객체에게 무엇을 해야 하는지 알려줘야 한다고 주장한다.
이 원칙의 묻지마 부분은 다음과 같이 고객에게도 적용할 수 있다.

  • 클라이언트 객체는 반드시 서버 객체가 갖고 있는 부분을 물어보지 말아야한다.
  • 객체 스스로 상태를 보고 결정을 내리게 해야한다.
  • 그리고 서버 객체에게 무언가 하라고 지시해야 한다.
  • 대신 클라이언트는 서버의 공용 인터페이스에서 제공하는 명령을 사용하여 서버에게 내릴 명령을 “지시"해야 한다.

이 가이드라인은 디미터 법칙과 동기(motivations)가 매우 유사하지만, 더 폭넓게 적용하기가 더 쉬울 수 있다.


예제를 살펴보자

deletePostPostService 내에 있는 메소드로, 게시글을 삭제하는 기능을 수행한다.
이 기능을 수행하기 위해서는 삭제 요청된 게시글ID가 존재해야하고, 게시글 작성자와 삭제 요청자가 일치해야한다.

Before

@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository messageRepository;

    public Mono<Void> deletePost(final DeletePostCommand command) {
        return postRepository.findByPostId(command.getPostId())
                .switchIfEmpty(Mono.error(new PostNotFoundException()))
                .flatMap(post -> {
                                        // 작성자와 삭제 요청자가 일치하는 지 확인하기 위해 여러 객체들에게 정보를 묻고 있음
                    if (!post.getUser().getId().equals(command.getDeleter().getUserId())) {
                        return Mono.error(new AccessDeniedException());
                    }
                    return postRepository.save(post.deleteAndCopy());
                })
                .then();
    }
}

다음 코드는 디미터 법칙을 어기고 있다.
if (!post.getUser().getId().equals(command.getDeleter().getUserId()))

  • 메소드 하나가 아는 지식이 굉장히 많다. 위 코드를 사용하는 메소드는 많은 객체를 탐색할 줄 안다는 말이다.
  • 위 예제가 디미터 법칙을 위반하는 지 여부는 post, user, id가 객체인지 아니면 자료 구조 인지에 달렸다. 객체라면 내부 구조를 숨겨야 하므로 확실히 디미터 법칙을 위반한다.
  • 반면 자료구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다. 위 예제 코드가 자료 구조이고 조회 함수를 사용하지 않고 다음과 같이 구현했더라면 논란의 여지가 없었을 것이다.
    if (!post.user.id.equals(command.deleter.id))
  • 자료 구조는 무조건 함수 없이 공개 변수만 포함하고, 객체는 비공개 변수와 공개 함수를 포함한다면 문제는 훨씬 간단할 것이다. 하지만 단순한 자료 구조에도 조회 함수와 설정 함수를 정의하라 요구하는 프레임워크와 표준(ex: 빈 Bean)이 존재한다.
  • 이런 혼란으로 때때로 절반은 객체, 절반은 자료 구조인 잡종 구조가 나오기도 한다. 양쪽 세상에서 단점만 모아놓은 구조다. 개발자가 함수나 타입을 보호할지 공개할지 확신하지 못해 어중간하게 내놓은 설계에 불과하다.

참고로 위 설명글은 클린 코드 책 6장에 있는 내용이며, 이 포스팅 샘플코드에 맞게 설명을 수정했다.
어쨌든 이 예제의 경우는 객체가 객체를 탐색하므로 결합이 강하게 생겨있다.
다음 예제에서는 디미터 법칙을 적용해서 결합도를 낮춰본다.


Refactoring 1

public class PostService {
    private final PostRepository messageRepository;

    public Mono<Void> deletePost(final DeletePostCommand command) {
        return postRepository.findByPostId(command.getPostId())
                .switchIfEmpty(Mono.error(new PostNotFoundException()))
                .flatMap(post -> postRepository.save(post.deleteAndCopy(command.getDeleter())))
                .then();
    }
}

이전과 달리 deletePost 메소드 내에서 여러 객체를 탐색하지 않는다. 게시글을 삭제하기 위해서는 작성자와 수정자가 일치해야한다는 지식을 Post 에게로 옮겼기 때문이다. 이 과정에서 PostService 는 더 이상 권한을 체크하도록 묻거나 시키지 않는다. 대신 PostdeleteAndCopy() 를 수행하면서 필수 선행조건이므로 외부에서 이를 시키는게 아니라, Post 가 삭제를 수행 때 삭제 권한 여부를 체크하는 것이 더 적절하다고 생각했다. 추후에 요청자가 Manager일 경우에는 수정/삭제가 가능하게 해달라는 요청이 온다면 Post에서 요청자에 대한 권한 검증이 이루어질 것이다.
물론 deleteAndCopy()를 호출하기 전에 명시적으로 권한을 체크해서 예외를 반환하도록 시켜도 디미터의 법칙을 지킬 수 있다.
이렇게 디미터의 법칙을 따른다고 하더라도, 실제 구현은 개발자마다 조금씩 다를 수 있다. 즉, 구현은 개발자 나름이란 소리!

public class Post {
    private final String id;
  private final PostWriter user;
  private final Boolean deleted;
    ...

    public Post deleteAndCopy(final PostWriter deleter) {
        verifyDeletePermission(deleter); // 권한 체크
        return copyFromThis()
                .deleted(true)
                .build();
    }

    // 작성자와 삭제 요청자가 일치하지 않을 경우 예외 반환
    private void verifyDeletePermission(final PostWriter deleter) {
        if (!this.user.equals(deleter)) { // id 기준 equals and hashcode 생성되어 있음
            throw new AccessDeniedException();
        }
    }
}

Post 에서는 삭제 전에 작성자와 삭제 요청자가 일치하는 지 권한을 체크하는 책임을 위임받아서 verifyDeletePermission() 메서드를 새로 추가했다. 만약 여기서 디미터 법칙을 적용하지 않았다면 PostWriter 객체를 아래처럼 탐색하는 코드가 되었을 수도 있다.
if (!this.user.getId().equals(deleter.getId()))


Refactoring 2

결합도를 낮추기 위해 동일한 원칙들을 따르더라도 개발자마다 다르게 구현할 수도 있다.
나같은 경우 작성자와 삭제 요청자가 일치하지 않으면 예외를 반환해야한다는 지식은 Post 가 갖고 있는게 더 적절하다고 판단했기 때문에 Refactoring 1의 예제처럼 구현했다. 하지만 정답은 없고 이를 다르게 구현할 수도 있다! Refactoring 2 예제에서는 어떻게 구현하였는 지 살펴보자!
동료분이 동일한 로직을 어떻게 구현했는지 두번째 예제와 이 분의 생각도 함께 공유하겠다.

public class Post {
    private final String id;
  private final PostWriter user;
  private final Boolean deleted;
    ...

    public Post deleteAndCopy(final PostWriter deleter) {
        this.user.checkDeletePermission(deleter); // 권한 체크
        return copyFromThis()
                .deleted(true)
                .build();
    }
}

이전 리팩토링 예제에서는 PostPostWriter에게 동일한 사용자인지 묻고 에러를 던질지 말지를 판단하고 있다.
동료분은 Post 에서 권한을 체크하기 위해 boolean으로 묻던 이전과 다르게, PostWriter 에게 책임을 부여하고 권한 검사라는 기능으로 추상화시켰다.
아래 코드를 살펴보자.

public class PostWriter  {
  private final Long id;
    ...

    public void checkDeletingPermission(final PostWriter performer) {
        if (getId().equals(performer.getId())) {
            return;
        }
        throw new AccessDeniedException();
    }
}

동료분께서는 왜 이렇게 작성하셨을까? (가공하다가 혹시 전달이 잘못될까봐 그대로 옮겨옴)

코드를 가만히 보면 위의 2가지 패턴 (boolean 으로 답변, 예외로 답변) 모두 게시글의 수정자, 삭제자가 권한이 있는지에 대해 판단하는 것은 결국 PostWriter 입니다.
모두 정보 전문가 패턴에 의해 PostWriter 가 자신의 데이터를 관리하고 있는데요, 따라서 저는 거절의 의사를 boolean false 로 밝히지 않고 처음부터 PostWriter 가 예외를 던지게 한 것입니다! (PostWriter 가 여러 곳에서 사용되며 권한 검사가 동일한 요구 사항으로 진행된다고 가정했을 때 boolean 으로 거절의사를 밝힐 경우 매번 if ~ 코드가 들어가야합니다. => 코드의 중복으로 이어짐 => 해당 코드를 한 곳으로 응집시킬 수 있음을 암시)
즉 이로 인해 저의 경우 수정, 삭제에 대한 사용자 권한 검사의 책임은 PostWriter 에게 온전히 쥐어준 것 입니다.
현실세계를 예로 들면 누군가 작성한 글을 인용하고 싶을 때 “A 님 제가 A 님의 글중 일부를 인용해도 괜찮을까요?” “이 부분만 살짝 수정해서 인용해도 괜찮을까요?” 라고 묻고 이에 대한 허락은 해당 글의 작성자가 하는 것과 비슷할 것 같습니다.
다음으로 이렇게 게시글 수정, 삭제에 대한 사용자 권한 검사 후 Post 의 content에 대한 실제 수정 여부는 온전히 게시글 자신의 책임일 것입니다.
예를 들면 Post 가 삭제된 글이라면(deleted 가 true 라면) 해당 글은 수정되어서는 안된다라는 요구 사항이 있었다면 PostWriter 가 OK 했어도 Post 는 자신의 deleted 상태를 보고 수정할지 안할지 결정할 것입니다.

위 말을 요약하자면, PostWriter 가 수정/삭제에 대한 사용자 권한 검사를 책임을 갖고, Post 는 실제 게시글의 내용이 수정되었는지 혹은 이미 삭제된 게시글인지 등의 책임에 집중하도록 한다. 
동료분은 객체는 자신의 상태를 스스로 관리하는 거고, 어떤 역할의 책임을 누구에게 줄 지 결정할 때의 기준은 가장 많은 정보를 알고 있는 객체에게 주는 것이 맞다고 생각한다고 했다. 종종 구현을 어느 방향으로 해도 문제가 없을 때 (이 지식은 얘가 가져도 될 것 같고, 또는 쟤가 가져도 될 것 같을 때) 기준점을 잡고 싶으면 정보 전문가 패턴 을 생각해볼 수 있다. 결국 목적은 객체 간의 결합도를 낮추는 것이다.

  • 정보 전문가 패턴 : 역할을 수행할 수 있는 정보를 가지고 있는 객체에 역할을 부여하자. 단순해 보이는 이 원칙은 객체지향의 기본 원리 중에 하나이다. 객체는 데이터와 처리로직이 함께 묶여 있는 것이고, 자신의 데이터를 감추고자 하면 오직 자기 자신의 처리 로직에서만 데이터를 처리하고, 외부에는 그 기능(역할)만을 제공해야 하기 때문이다.
  • GRASP : General Responsibility Assignment Software Patterns의 축약어이다. Object-Oriented 디자인의 핵심 문제는 각 객체에 역할(또는 책임)을 부여하는 것이다. GRASP Pattern은 역할 부여의 원칙들을 말하고 있다. 일반적으로 디자인 패턴이라고 불리우는 것들처럼 구체적인 구조는 없지만, 각 디자인 패턴들은 GRASP 패턴이 제시하는 철학를 각 상황에서 구체적으로 구현할 것이라 볼 수 있다. GRASP 패턴을 설명하면, Creational 패턴들이 왜 앞에서 설명한 두 가지를 목표로 하고 있는지를 이해하는데 도움이 되리라 생각된다. GRASP 패턴은 아홉 가지로 구성되어 있고, 그 중 하나가 정보 전문가 패턴이다.

중요한 것

내 생각과 동료분께서 한 말 중 인상적이었던 것을 메모한다.

  • 객체 간의 결합도를 낮추고, 자율성을 높여 서로 협력할 수 있도록 하는 것
  • 이렇게 A 객체가 어떤 책임을 수행하다가 B 객체의 도움이 필요하면 B 객체에게 일을 시키고, 이때 또 C 객체의 도움이 필요하면 B 객체가 C 객체에게 도움을 요청하면서 객체의 협력이 이루어진다.
  • 원칙에 매몰되지 않고 최선을 선택하는 게 가장 좋다

출처

반응형