본문 바로가기

관심사/독후감

<나루세 마사노부> 도메인 주도 설계 철저입문

밑줄 그으며 책 읽기 📝

애플리케이션 서비스는 유스케이스를 구현하는 객체이다. 도메인 객체는 도메인을 코드로 옮긴 것이다. 도메인을 코드로 나타냈다고 해도 그것만으로는 이용자가 당면한 문제나 필요가 해결되지 않는다. 이용자의 필요를 만족시키거나 문제를 해결하려면 도메인 객체의 힘을 하나로 엮어 올바른 방향으로 이끌어야 한다. 

...

유효성 검증 로직에서 에러를 반환하게 하는 경우, 결과 객체를 반환한다. 결과 객체는 개발자에게 강제력을 미치지 못한다. 즉, 처리 실패 핸들링의 역할을 클라이언트에 전적으로 위임하게 되는데, 이는 자칫 의도치 않게 실패를 그냥 지나쳐버리는 결과를 낳는다. 반대로 예외를 발새하는 쪽을 택하면 반환 값을 반환하지 않는다. 예외를 발생시키고 아무것도 하지 않으면 프로그램이 종료되고, 프로그램이 종료되지 않게 하려면 발생한 예외를 catch 문을 통해 해결해야하기 때문에 개발자에게 에러 핸들링을 강제할 수 있다. 그 결과로 실패를 그냥 지나쳐 후속 처리로 넘어가는 상황을 방지할 수 있다.

...

애플리케이션 서비스가 아닌 객체가 도메인 객체의 직접적인 클라이언트가 되어 도메인 객체를 자유롭게 조작하게 되면 문제가 발생하게 된다. 도메인 객체의 행동을 호출하는 것은 애플리케이션 서비스의 책임이다. 이 구조가 지켜지면, 도메인 객체의 행동을 호출하는 코드가 모두 애플리케이션 서비스에 모여있게 된다. 그렇지 않다면 여러 곳에 코드가 흩어질 수 있다.

이 외에 도메인 객체를 외부에 공개한다는 선택은 각 처리의 코드를 단순하게 만들 수 있지만, 그 대가로 많은 위험성을 안게 된다. 이를 방지하기 위한 수단으로 접근제어 수정자를 이용해 메서드의 호출을 제한하는 방법이 있지만, 클라이언트와 애플리케이션 서비스, 도메인 객체가 모두 같은 패키지에 있다면 이 방법도 사용하기 어렵다. 또 개발팀 내의 합의를 통해 도메인 객체의 메서드 호출에 대한 제약을 두는 방법도 있으나 이 방법은 강제력이 없다. 

그래서 필자는 도메인 객체를 직접 공개하지 않는 쪽을 권한다. 도메인 객체는 비공개로 남겨두고 클라이언트에 데이터 전송을 위한 객체인 DTO (data transfer object)를 만들어 여기에 데이터를 옮겨 넣어 반환하는 방법이다.

DTO를 적용하면 DTO를 정의하는 데 필요한 수고와 데이터를 옮겨 담는 데서 오는 약간의 성능저하가 있다. 그러나 어지간히 많은 양의 데이터를 옮기는 것이 아니면 성능 저하는 미미한 수준이다. 오히려 불필요한 의존을 줄이고 도메인 객체의 변경을 방해받지 않는 편익이 더 크다.

...

프로그램에는 응집도라는 개념이 있다. 응집도는 모듈의 책임 범위가 얼마나 집중되어 있는지 나타내는 척도다. 응집도가 높으면 모듈이 하나의 관심사에 집중하고 있다는 의미이므로 모듈의 견고성, 신뢰성, 재사용성, 가독성의 측면에서 바람직하다. 응집도를 측정하는 방법에는 LCOM (Lack of Cohesion in Methods)이라는 방식이 있다. 모든 인스턴스 변수가 모든 메서드에 사용돼야한다는 관점에서 인스턴스 변수의 개수와 메서드의 수를 통해 응집도를 계산하는 것이다.

...

도메인의 규칙을 지키는 "애그리게이트"애그리게이트는 경계와 루트를 가진다.

외부에서 애그리게이트를 다루는 조작은 모두 루트를 거쳐야 한다. 외부에서는 애그리게이트 내부에 있는 객체를 조작할 수 없다. 객체를 조작할 수 있는 인터페이스가 되는 객체는 애그리게이트 루트이다.

...

데메테르의 법칙은   어떤 컨텍스트에서 다음 객체의 메서드만을 호출할 수 있게 제한한다.
객체 자신, 인자로 전달받은 객체, 인스턴스 변수, 해당 컨텍스트에서 직접 생성한 객체.

...

Getter 함수는 만들지 말아야 한다. 게터를 통해 필드를 공개하면 객체에 구현돼야 할 규칙이 다른 곳에서 중복으로 구현될 수 있다. 

...

애그리게이트에 대한 변경은 해당 에그리게이트 자신에게만 맡기고, 퍼시스턴시 요청도 애그리게이트 단위로 해야 한다. 리포지토리는 애그리게이트마다 하나씩 만든다. 

코드 노트 ✏️

// 애플리케이션 서비스가 아닌 객체가 도메인 객체를 자유롭게 수정하는 것은 안티패턴이다. 
// 이 경우, userService에서 User Entity가 아니라 UserData DTO를 리턴하게 해야한다.
// 그래야 어플리케이션 (UserService)에서 이름 변경 행동을 담당하도록 강제할 수 있다.
public class Client{
    public UserService userService;
    
    public void changeName(String id, String name){
    	User user = userService.get(id);
        UserName name = new UserName(name);
        user.changeName(name)
    }
}
// X 다른 애그리거트의 속성을 getter으로 참조하지 말아야함 
var circle = new Circle(user.id, new CircleName("축구"));

// 위의 예시 코드처럼 객체 내부의 데이터를 이용해 인스턴스를 생성할 필요가 있을 때에는 
// 팩토리 역할을 하는 메서드를 사용하는 경우가 있다. 
public class User{
    private readonly UserId id;
    ...
    public Cicle createCircle(CircleName circleName){
    	return new Circle(id, new CircleName("축구"));
    }
}
// 코드 응집도가 낮은 클래스의 예시이다. 
// 이 경우, value1/2만 다루는 클래스, value3/4만 다루는 클래스를 각각 나눠야 응집도가 높아진다.
public class LowCohesion{
    private int value1;
    private int value2;
    private int value3;
    private int value4;
    
    public int methodA{
    	return value1 + value2;
    }
    
    public int methodB{
    	return value3 + value4;
    }
}