Law of Demeter (디미터 법칙)
사내에서 디미터 법칙을 주제로 작은 세미나를 준비했었는데, 그 내용을 정리해 보았습니다.
Table Of Contents
- 디미터 법칙(Law Of Demeter)
- 결합도 통제(Coupling Control)
- 정보 은닉(Information Hiding)
- 메시지와 인터페이스
- 참고 자료(Reference)
디미터 법칙
위키피디아에서는 디미터 법칙에 대해 다음과 같이 정의하고 있다.
💡 The Law of Demeter (LoD) or principle of least knowledge is a design guideline for developing software, particularly object-oriented programs. In its general form, the LoD is a specific case of loose coupling.
디미터 법칙 또는 최소 정보의 원칙은 소프트웨어 개발을 위한, 특히 객체 지향 프로그래밍을 위한 디자인 가이드이다. 일반적으로 디미터 법칙은 느슨한 커플링의 구체적인 예시이다.
위키피디아에서 디미터 법칙 세줄 요약을 해 주었다.
- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit. 각각의 개체는 다른 개체에 대한 제한된 정보만들 가져야 한다. 그리고 현재 개체에서 가까이 연관이 되어있는 개체에 대한 정보만 가지고 있어야 한다.
- Each unit should only talk to its friends; don't talk to strangers. 각각의 개체는 오직 그들의 친구랑만 대화할 수 있다. 낮선 이와 대화하면 안된다.
- Only talk to your immediate friends. 오직 당신의 인접한 친구랑만 대화해야 한다.
예를 들어, 디미터 법칙에 따르면 클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다.
- 클래스 C
- f가 생성한 객체
- f 인수로 넘어온 객체
- C 인스턴스 변수에 저장된 객체
그리고 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다.
해당 논문의 저자인 I.Holland 는 디미터 법칙의 목적을 다음과 같이 이야기 하고 있다.
This Law has two primary purposes:
- Simplifies modifications.(변경의 단순화.) It simplifies the updating of a program when the class dictionary is changed.
- Simplifies complexity of programming.(프로그래밍 복잡도의 단순화.) It restricts the number of types the programmer has to be aware of when writing a method.
결합도 통제(Coupling Control)
Q. 왜 느슨한 결합도(coupling)를 가져야 할까?
A. 하나의 코드 변경이 일어났을 때 다른 많은 코드들을 수정해 주어야 하는 걱정을 하지 않아도 되기 때문. 디미터 법칙을 잘 지키면, 오직 가까운 친구들하고만 대화를 하기 때문에 나와 상관없는 사람을 신경쓰지 않아도 된다.
예시 코드
class PostalCode {
constructor(postalCode) {
this.postalCode = postalCode;
}
setPostalCode(postalCode) {
this.postalCode = postalCode;
}
}
class Address {
constructor(streetName) {
this.streetName = streetName;
}
getPostalCode() {
return this.postalCode;
}
setPostalCode(postalCode) {
this.postalCode = new PostalCode(postalCode);
}
}
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getAddress() {
return this.address;
}
setAddress(address) {
this.address = new Address(address);
}
}
Address 클래스는 PostalCode를 참조하고, Person 클래스는 Address를 참조한다.
어떤 한 사람의 postal code를 설정하려면 다음과 같이 해야 한다.
Person person = new Person('Owen', 29);
person.getAddress().getPostalCode().setPostalCode('12345');
이렇게 했을 때 발생할 수 있는 문제점
→ 다양한 클래스(Address, PostalCode)의 인스턴스를 포함한 chaining이 엮여 있어서, 만약 이러한 클래스의 변경사항들이 생기면 모든 체인이 재작성 되어야 한다.
→ Person의 친구는 Address인데, 지금 PostalCode와 대화를 하고 있다. Person 입장에서 PostalCode는 낮선 사람. LoD를 위반.
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getAddress() {
return this.address;
}
setAddress(address) {
this.address = new Address(address);
}
getPostalCode() {
return this.postalCode;
}
setPostalCode(postalCode) {
this.postalCode = new PostalCode(postalCode);
}
}
정보 은닉(Information Hiding)
💡 캡슐화(Encapsulation) vs 정보 은닉(Information Hiding)
캡슐화(Encapsulation) : 관련 되어 있는 요소들을 묶어서 캡슐 외부와 내부를 구분. 객체들 간에서 서로 상대방 객체의 인터페이스(공용함수)만 볼 수 있음.
정보 은닉(Information Hiding) : 캡슐화가 되어 있는 데이터, 함수에 대해 외부에서 해당 함수의 구현과 데이터의 값을 알 수 없게 숨기는 것.
디미터 법칙은 캡슐화를 다른 관점에서 표현한 것.
캡슐화 원칙이 클래스 내부의 구현을 감춰야 한다는 사실을 강조한다면, 디미터 법칙은 협력하는 클래스의 캡슐화를 지키기 위해 접근 요소를 제한.
아래 예제 코드는 User, Payment, Credentials 총 3개의 클래스가 정의되어 있다. Payment 클래스에서 유저의 이메일 주소를 받아서 결제를 하는 로직을 작성해 보았다.
class User {
payment = new Payment()
}
class Payment {
private credentials = new Credentials()
email = "xyz@gmail.com"
}
class Credentials {
defaultMethod: String = "VISA"
cvv: String = "***"
}
class Utility {
user = new User()
showEmailAddress(){
console.log(this.user.payment.email)
}
}
var utility = new Utility();
utility.showEmailAddress();
위 코드의 문제점은 세 클래스 User, Payment, Credentials 간에 강하게 결합이 되어 있다는 점이다. 예를 들어 payment 인스턴스는 user에 노출이 되어 있고 다른 user가 노출되는 곳에 전부 노출된다.
그리고 새로운 모듈에서 email이 바뀔 수도 있다. 예를 들어 Profile이라는 클래스에서 값을 가져온다고 한다면, 이 변경은 user.payment 함수 체인을 변경하게 만든다.
정보 은닉이 이루어지고, LoD를 잘 따른 코드로 수정하면 다음과 같다. user 인스턴스에서 payment의 디테일한 구현 부분을 캡슐화 했다.
class User {
getEmailAddress(payment: Payment): String {
return payment.email;
}
}
class Payment {
private credentials = new Credentials();
email = "xyz@gmail.com";
}
class Credentials {
defaultMethod: String = "VISA";
cvv: String = "***";
}
class Utility {
user = new User();
showEmailAddress(){
console.log(this.user.getEmailAddress(new Payment()));
}
}
var utility = new Utility();
utility.showEmailAddress();
이렇게 수정을 하면 Payment 클래스의 구현에 변화가 생기더라도 User 클래스에 영향을 미치지 않는다. 왜냐하면 제한된 정보만을 가지고 있기 때문이다.
메시지와 인터페이스
훌륭한 객체지향 코드를 얻기 위해서는 클래스가 아니라 객체를 지향해야 한다. 좀 더 정확하게 말해서 협력 안에서 객체가 수행하는 책임에 초점을 맞춰야 한다. 여기서 중요한 것은 책임이 객체가 수신할 수 있는 메시지의 기반이 된다는 것이다. … 객체지향 어플리케이션의 가장 중요한 재료는 클래스가 아니라 객체들이 주고받는 메시지다.
condition.isSatisfiedBy(screening);
- condition : 메시지 수신자
- isSatisfiedBy : 오퍼레이션
- screening : 인자
- .isSatisfiedBy(screening) : 메시지
- condition.isSatisfiedBy(screening) : 메시지 전송
외부의 객체는 오직 메시지(.isSatisfiedBy(screening))를 통해서만 객체와 상호작용 할 수 있다. 이와 같이 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스(public interface)라고 한다.
퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation)이라고 부른다. 오퍼레이션은 수행 가능한 어떤 행동에 대한 추상화다.
메시지를 수신했을 때 실행되는 코드를 메서드(method)라고 부른다. 여기서 조심해야 할 점은 동일한 변수에 동일한 메시지를 전송하더라도 객체의 타입에 따라 실행되는 메서드가 달라질 수 있다는 점이다.
좋은 인터페이스는 최소한의 인터페이스와 추상적인 인터페이스라는 조건을 만족해야 한다. 추상적인 인터페이스는 어떻게 수행하는지가 아니라 무엇을 하는지를 표현한다.
최소주의를 따르면서 추상적인 인터페이스를 설계할 수 있는 가장 좋은 방법은 책임 주도 설계 방법을 따르는 것이다. 책임 주도 설계 방법은 메시지를 먼저 선택함으로서 협력과는 무관한 오퍼레이션이 인터페이스에 스며드는 것을 방지한다.
객체가 메시지를 선택하는 것이 아닌, 메시지가 객체를 선택하게 하라 → 클라이언트의 의도를 메시지에 표현
묻지 말고 시켜라 - Tell, Don’t Ask
하지만 개발을 하다보면 시키지 못하고 물어야 할 때도 있다. → 명령-쿼리 분리 원칙(CQRS)