업데이트:

디미터의 법칙


클린코드를 학습 도중 디미터의 법칙을 배웠고 이것에 대해 정리하고자 포스트를 작성하게되었습니다.

디미터의 법칙이란 객체 지향 프로그래밍에서 더 객체 지향 답게 프로그래밍 하는 기법의 일종인데, “어떤 객체는 연관(혹은 의존)된 객체의 메소드만을 호출해야 한다”는 법칙입니다.

디미터의 법칙 제약

만약 클래스 C에서 메소드 f를 호출한다면 다음과 같은 메소드만 호출해야 함을 제약하고 있습니다.

  1. 클래스 C
  2. f가 생성한 객체
  3. f로 넘어온 parameter 객체
  4. C 멤버변수의 객체

음… 저도 무슨 말인지 잘 이해가 안돼서 바로 예시를 보도록 하겠습니다.

예시 코드


Model 클래스

@Getter
public class Post {
    private Long id;
    private String title;
    private String content;
    private User user;
}
@Getter
public class User {
		private Long id;
		private String name;
		private Email email;
}
@Getter
public class Email {
		private String userId;
		private String address;
}

위와 같은 클래스가 있을 때 email 주소google만 사용하는 사용자의 게시물만 필터링하는 코드를 작성해보겠습니다.

💡보통은 Repository에서 조회 조건을 넘겨서 쿼리에서 필터링 하지만 예시 코드이니 너그럽게 봐주세요😅

Service 코드

Bad

public class PostService {

		private final PostRepository postRepository;
		
		public List<Post> getPostsByUserEmail() {
				return postRepository.findAll().stream()
						.filter(post -> {
                return post.getUser().getEmail().getAddress().equals("google");
            })
            .collect(Collectors.toList());
		}
}

위의 코드 중에 email 필터링 부분을 보시면 user를 호출하고, email, address를 연속으로 호출하여 검사를 진행하는 것을 알 수 있습니다.

Post에서 의존하지 않은 Email까지 호출한 것이므로 이는 디미터의 법칙을 어긴 것이고, 이를 “기차 충돌”이라 표현합니다.

이번엔 위의 코드를 디미터의 법칙을 준수한 코드로 변경해보겠습니다.

Good

PostUser에 호출할 메소드를 추가해줍니다.

public class Email {
		private String userId;
		private String address;

		public boolean isGoogleEmail() {
				return address.equals("google");
		}
}
public class User {
		private Long id;
		private String name;
		private Email email;

		public boolean vaildByEmail() {
				return email.isGoogleEmail();
		}
}
public class Post {
    private Long id;
    private String title;
    private String content;
    private User user;

		public boolean vaildByUser() {
				return user.vaildByUserEmail();
		}
}
public class PostService {

		private final PostRepository postRepository;
		
		public List<Post> getPostsByUserEmail() {
				return postRepository.findAll().stream()
						.filter(post -> {
                return post.vaildByUser();
            })
            .collect(Collectors.toList());
		}
}

User, Post, PostService 코드를 보시면 눈으로 보기에도 깔끔해지고 책임도 명확해지며 Post가 직접 Email를 참조하지 않기 때문에 결합도도 떨어진 것을 알 수 있습니다. 또한 UserPost에서 각각 연관된 객체만을 호출하기 때문에 캡슐화 역시 유지한 것을 알 수 있습니다.

장점


그럼 디미터의 법칙의 장점은 무엇일까요?

기차 충돌 상황처럼 결합도가 높다는 것은 하나의 수정이 발생했을 때 Side-Effect가 발생할 소지가 높다는 뜻이고, 수정할 코드도 많아 진다는 뜻입니다.

post.getUser().getEmail().getAddress().equals("google");

만약 google 이메일 주소를 검사하는 위와 같은 코드가 여러 곳에 분산되어 있고, naver로 변경 요구사항이 발생했다면 어떻게 될까요?

google검사 부분을 모두 naver로 변경해야 하는 일이 발생하게 됩니다.

post.vaildByUser();

하지만 위와 같이 디미터의 법칙을 유지시킨 코드에선 다음 메소드를

public boolean isGoogleEmail() {
		return address.equals("google");
}
public boolean isNaverEmail() {
		return address.equals("naver");
}

같은 메소드로 수정하거나 추가해줌으로써 해결할 수 있습니다.

PostEmail을 참조하지 않기 때문에 신경 쓸일 없이, 수정 없이 그대로 사용하면 됩니다.

주의점


post.vaildByUser()처럼 디미터의 법칙을 준수하면 도트(.) 하나만 찍기 때문에 “오직 하나의 도트만 사용하라”라는 말로 요약되기도 하는데 이는 반은 맞고 반은 틀렸다고 볼 수 있습니다.

디미터의 법칙은 도트 하나만 쓰라고 강제하는 것은 아닙니다.

대표적인 위의 예시 코드에서도 사용한 메소드 체이닝을 사용하는 Java 8Stream API가 있죠.

postRepository.findAll().stream()
		.filter()
		.collect();

그럼 Stream API는 디미터의 법칙을 위반한 것일까요?

아닙니다.

filter()collect() 모두 동일한 Stream(=this)를 반환할 뿐 내부의 구현을 외부로 노출하지도 않고, 직접적으로 연관되지 않은 객체를 참조한 것도 아니기 때문에 디미터의 법칙을 준수한 것입니다.

💡또한 DTO자료구조의 경우는 목적이 내부 데이터를 노출하는 것이기 때문에 디미터의 법칙을 지킬 필요가 없습니다.

마무리


오늘은 디미터의 법칙에 대해서 알아봤는데요.

제가 건방진 것인지 모르겠지만 처음에는 무슨 “~법칙”이라고 돼있어서 겁부터 먹었는데, 결국 객체 지향 프로그래밍을 준수하고 메시징을 보내는 방식으로 메소드를 호출 한다면 함께 지켜질 법칙이라고 생각합니다.

(나중에 더욱 더 공부하다가 후회하는 순간이 올지 모르겠네요.😅 하지만 겁부터 먹는 버릇을 고쳐야 겠다는 생각도 하게되는 학습이었습니다.)

오늘도 제 포스트를 봐주셔서 감사합니다. 아직 클린코드를 계속 학습 중이라 또 좋은 내용이 있으면 포스팅을 진행하겠습니다.

감사합니다~

📌참고 블로그


[OOP] 디미터의 법칙(Law of Demeter)

디미터 법칙 (The Law of Demeter)

댓글남기기