디미터 법칙은 “Object-Oriented Programming: An Objective Sense of Style”에서 처음 소개 되었다.
이 글의 저자들은 디미터라는 이름의 프로젝트를 진행하던 도중 다른 객체들과의 협력을 통해 프로그램을 완성해나가는 객체지향 프로그래밍에서 객체들의 협력 경로를 제한하면 결합도를 효과적으로 낮출 수 있다는 사실을 알게 되었다.
그리고 디미터 법칙을 만들었다.
프로젝트를 진행하던 개발자들은 어떤 객체가 다른 객체에 대해 지나치게 많이 알다보니, 결합도가 높아지고 좋지 못한 설계를 야기한다는 것을 발견했다. 그래서 이를 개선하고자 객체에게 자료를 숨기는 대신 함수를 공개하도록 하였다.
디미터의 법칙은 다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다는 것을 의미한다.
이런 이유로 Don't Talk to Strangers(낯선 이에게 말하지 마라) 또는 Principle of least Knowledge(최소 지식 원칙) 으로도 알려져 있다. 또는 직관적으로 이해하기 위해 여러 개의 .(도트)를 사용하지 말라는 법칙으로도 많이 알려져 있으며, 디미터의 법칙을 준수하면 앞서 말했듯이, 결합도를 낮출 수 있고, 캡슐화를 높혀 자율성과 응집도를 높일 수 있다.
객체지향에서 가장 중요한 것은 "객체가 어떤 데이터를 가지고 있는가 ?"가 아니라, "객체가 어떤 메세지를 주고 받고 있는가?"이다. 그래서 디미터 법칙은 객체 지향 프로그래밍에서 상당히 중요한 개념인데, 디미터의 법칙이 위배 된다는 것은 올바른 객체 지향 프로그래밍을 하지 못하고 있다는 증거이기도 하다.
우리는 올바른 객체지향 프로그래밍을 하지 못하는 코드를 살펴 볼 것이고, 그 곳에 디미터의 법칙을 적용해보도록 하자.
디미터 법칙의 핵심은 객체 주고의 경로를 따라 멀리 떨어져 있는 낯선 객체에 메세지를 보내는 설계는 피하라는 것이다.
바꿔 말하여 객체는 내부적으로 보유하고 있거나 메세지를 통해 확보한 정보만을 가지고 의사 결정을 내려야 하고 다른 객체를 탐색해 뭔가를 일어나게 해서는 안된다는 말이다.
public class Post {
private final List<Comment> comments;
public Post(List<Comment> comments) {
this.comments = comments;
}
public List<Comment> getComments() {
return comments;
}
}
public class Board {
private final List<Post> posts;
public Board(List<Post> posts) {
this.posts = posts;
}
public void addComment(int postId, String content) {
posts.get(postId).getComments().add(new Comment(content));
}
...
}
위 코드의 Board
객체, 그 안에 있는 addComment
메서드를 살펴보자.
Board
객체의 인스턴스 변수 posts
에서 getter를 거듭하여 멀리 떨어져 있는 낯선 객체 Comment
를 추가하는 코드이다.
이처럼 getter가 줄줄이 이어지는 코드 형태가 디미터 법칙을 위반한 전형적인 코드이다.
왜 낯선 객체에 메세지를 보내는 설계를 피해야하며, 그러 인해 발생하는 문제는 무엇일까 ?
우선, Post
객체의 인스턴스 변수 List<Comment> comments
를 Comments
라는 일급 컬렉션 객체로 수정해보자.
public class Post {
private final Comments comments;
public Post(Comments comments) {
this.comments = comments;
}
public Comments getComments() {
return comments;
}
}
getter를 통해 Post
객체의 List<Comment> comments
를 사용하던 Board
객체의 addComment
메서드가 깨진다.
public class Board {
private final List<Post> posts;
public Board(List<Post> posts) {
this.posts = posts;
}
//에러 발생
public void addComment(int postId, String content) {
posts.get(postId).getComments().add(new Comment(content));
}
...
}
이처럼 Board
객체의 addComment
메서드 내에서 Post
객체도 알고 Comment
객체도 알고 있다면 Board
객체는 Post
객체의 변화에 영향을 받고 Comment
객체의 변화에도 영향을 받게 된다.
이러한 설계가 프로젝트 내에 많다면, 하나의 변화에 수많은 클래스들이 깨질 수가 있다.
즉 객체 간 결합도가 높아지고 객체 구조의 변화에 쉽게 무너진다.
변화에 유연히 대처하지 못하게 되는 것이다.
예로 서울에 살고 있는 어떤 사용자에게 알림을 보내주는 함수를 구현하고자 한다.
이를 구현하려면 우리는 다음과 같은 User
객체와 Address
객체가 필요하고, User
객체는 Address
라는 주소 객체를 가지고 있을 것이다.
@Getter public class User {
private String email;
private String name;
private Address address;
}
@Getter public class Address {
private String region;
private String details;
}
그 다음 어떤 사용자가 서울에 살고 있으면 알림을 보내주는 메서드를 다음과 같이 구현했다고 하자.
@Service
public class NotificationService {
public void sendMessageForSeoulUser(final User user) {
if("서울".equals(user.getAddress().getRegion())) {
sendNotification(user);
}
}
}
위의 코드는 정말 흔하게 볼 수 있지만, 디미터 법칙을 위반하고 있다. 객체에게 메세지를 보내는 것이 아니라 객체가 가지고 있는 자료를 확인 하고 있으며, 다른 객체가 어떠한 자료를 갖고 있는지 지나치게 잘 알고 있기 때문이다.(우리는 getter
메서드를 통해 User
객체가 email
, name
, address
를 가지고 있음을 파악할 수 있다).
우리는 Address
객체의 데이터를 통해 사용자의 지역을 파악하는 것이 아니라, Address
의 객체에 메세지를 보내 서울 지역에 사는지 파악하도록 구현해야 한다.
public class Address {
private String region;
private String details;
public boolean isSeoulRegion() {
return "서울".equals(region);
}
}
public class User {
private String email;
private String name;
private Address address;
public boolean isSeoulUser() {
return address.isSeoulRegion();
}
}
위와 같이 객체에게 보내는 메세지를 구현하면 불필요한 @Getter들 역시 지울 수 잇고, User
객체와 Address
객체가 어떠한 데이터들을 지니고 있는지 모른 채 메세지를 보낼 수 있다.
그리하여 기존의 알림을 보내는 로직을 다음과 같이 수정할 수 있다.
@Service
public class NotificationService {
public void sendMessageForSeoulUser(final User user) {
if(user.isSeoulUser()) {
sendNotification(user);
}
}
}
새로 작성된 코드는 여러개의 .(도트)를 사용하여 참조하지 않고 있다. 디미터의 법칙을 잘 준수하고 있다.
하지만 여기서 주목해야 하는 것은 디미터 법칙이 하나의 .(도트)를 사용하도록 강제하는 것은 아니란는 점이다.
디미터 법칙은 "노출 범위를 제한하기 위해 객체의 모든 메서드가 다음에 해당하는 메서드만을 호출해야 한다"고 말한다.
이 또한 코드를 통해 살펴보자
class Demeter {
private Member member;
public myMethod(OtherObject other) {
// ...
}
public okLawOfDemeter(Paramemter param) {
myMethod(); // 1. 객체 자신의 메서드
param.paramMethod(); // 2. 메서드의 파라미터로 넘어온 객체들의 메서드
Local local = new Local();
local.localMethod(); // 3. 메서드 내부에서 생성, 초기화된 객체의 메서드
member.memberMethod(); // 4. 인스턴스 변수로 가지고 있는 객체가 소유한 메서드
}
}
위의 규칙을 지켜 최대한 노출범위를 제한하면 좀 더 에러가 적고, 변화에 유연히 대처할 수 있는 클래스를 만들 수 있다.
자료구조라면 디미터 법칙을 거론할 필요가 없으며, 디미터 법칙은 .(도트) 하나만을 강제하는 규칙이 아니다.
디미터의 법칙은 결합도와 관련된 것이며, 객체의 내부 구조가 외부로 노출되는지에 대한 것이다. Stream API 같은 경우에는 동일한 Stream으로 변환하여 반환 할 뿐, 캡슐화는 그대로 유지되므로 문제가 없다. 만약 여러 .(도트)가 사용되더라도 객체의 내부 구현이 노출되지 않는다면 그것은 디미터의 법칙을 준수하는 코드이다.
또한 DTO 나 컬렉션 객체와 같은 자료 구조의 경우에는 물을 수 밖에 없다. 만약 묻는 대상이 객체가 아닌 자료구조라면 당연히 내부를 노출해야 하므로 디미터의 법칙을 적용할 필요가 없다.
잘 정리하셨네요! 결국에는 디미터 법칙이라는거는 캡슐화를 중심으로 확장된 원칙이네요 ㅎㅎ 배우고 갑니다!