쉽게 말하는 DDD 서비스, 리포지토리

jay·2022년 6월 1일
4
post-thumbnail

글에 들어가기에 앞서서

지난 포스팅에서 엔티티와 값 객체 (value object)에 대해 알아보았습니다.

오늘은 어플리케이션에서 http 요청을 처리하기 위해 주요한 로직을 다루는도메인 서비스와 어플리케이션에 영속성을 제공하기 위한 리포지토리, 그리고 도메인 서비스와 리포지토리를 사용하는 유스케이스를 구현한 애플리케이션 서비스에 대해 알아보겠습니다.

오늘 알아볼 것은

  • 도메인 서비스
  • 리포지토리
  • 애플리케이션 서비스

도메인 서비스

소프트웨어 개발에서 말하는 서비스는 클라이언트를 위한 역할을 맡은 객체를 의마하나 행동이 매우 다양해 어떤 것인지 콕 찝어서 이야기하기 불분명한 부분이 있습니다.

도메인 주도 설계에서 말하는 서비스는 아래 두가지로 나누어집니다.

  • 도메인을 위한 서비스
  • 애플리케이션을 위한 서비스

도메인 서비스란

여기서 말하는 도메인 서비스는 도메인을 위한 서비스를 의미합니다.

도메인 객체에 나타내기 부자연스러운 로직이 있습니다.
회원가입 기능을 위해 기존에 존재하는 아이디를 확인하는 절차는 꼭 제공해야 하는 서비스입니다.

회원을 나타내는 User 엔티티가 있다고 가정할 때
이 엔티티는 도메인 객체로 서비스 내에서 회원이 어떤 정보를 가지고 있는지, 이름에 특수문자가 들어갈 수 있는지에 대한 로직은 담을 수 있으나

이 회원 객체가 중복 검사를 하는 과정은 엔티티가 맡아야 할 역할이라고 하기에는 부자연스러운 부분이 있습니다. 회원이 직접 디비를 조회하고 유효성 검사를 해야 한다는 생각으로 그 의식의 흐름이 전파되니까요.

class User {
  id;
  name;
  repository;
  constructor(id, name, repository){
    this.id = id;
    this.name = name;
    this.repository = repository;
  }
  
  isValidName(){
    return this.name && this.name.length > 5;
  }
  
  isDuplicate(){
    const user = this.repository.find(name,id);
   	return user.length;
  }
}

// 사용하는 곳
...
const { id, name } = request.body;
const repository = new Repository();
const user = new User(id, name, repository);

if(!user.isDuplicate()) {
	userService.register(user);
}
else {
  res.send(400);
}

회원 엔티티를 생성한 이후 회원한테 본인이 중복되었는지에 대해 확인을 하라는 역할을 맡게 했습니다. 이는 많은 경우 개발자에게 혼란을 줄 수 있는 도메인 표현입니다.

이렇게 특정 역할에 대해 도메인에 대한 부자연스러움을 해결하도록 도와주는 것이 도메인 서비스 입니다.

javascript 개발자 입장에서 확대 해석해보면 애플리케이션 내에서 도메인 객체와 연관된 요청을 처리하기 위한 유틸 함수라고 보시면 됩니다.

남용하면 안된다

다시 한번 중복 기능을 수도 코드로 정의해보겠습니다.

class UserService() {
  ...
  isDuplicate() {
    ...
    // return true or false
    ...
  }
  ...
}

보면 사용하기 쉽고 간편하고 역할 분리가 잘 이뤄진 것처럼 보입니다.

도메인 해석 기반으로 웹 사이트에 추가해야할 기능을 고려해보면 더욱 많은 도메인 서비스가 필요해질 수 있습니다. 회원명 수정, 프로필 이미지 수정과 같은 부분이 예시가 될 수 있습니다.

class UserService() {
  ...
  isDuplicate() {
    ...
    // return true or false
    ...
  }
    
  changeName(){
    ...
  }
    
  changeProfileImage() {
    ...
  }
  ...
}

이제 User 엔티티와 관련된 모든 역할은 도메인 서비스에 정의하는게 자연스럽게 보입니다. 하지만 이렇게 되면 엔티티는 오직 게터와 세터만 가지게 됩니다.

아, 엔티티에 관련된 모든 로직은 도메인 서비스에 넣었구나라고 생각할 수도 있겠지만

처음 코드를 접한 사람은 어떤 기능이 어떤 도메인 서비스에 들어갔는지 일일히 모든 도메인 서비스를 까봐야 합니다. 그리고 어떤 부분을 엔티티가 처리하는지, 엔티티는 이 어플리케이션에서 어떤 도메인 규칙을 담당하는지 파악하기 어려워집니다.

엔티티가 그저 속성을 가둔 껍데기가 되기 때문입니다.
이런 도메인 객체를 빈혈 도메인 모델이라고 합니다.
빈혈 도메인 모델은 한 객체에 객체와 연관된 데이터와 행위를 모아둔다는 객체 지향 설계의 기본 원칙을 위반하는 안티 패턴입니다.

유스케이스 수립하는 방법

도메인 서비스는 항상 도메인 객체와 함께 사용되어야 합니다. 그렇지 않으면 uuid를 만들어내는 역할만 가진 유틸 함수랑 다를 바가 없습니다.

유스케이스는 기능 시나리오라고 생각하시면 됩니다.
앞서 봤던 예시 코드도 회원 가입 유스케이스를 기반으로 만든 것입니다.

이번 예시코드에서 앞서 사용한 엔티티(User)는 그대로 사용하도록 하겠습니다.

유스케이스는 다시 한번 회원 가입 기능을 사용할 것이며
http 리퀘스트를 처리하는 도메인 서비스에서 어떻게 회원 가입을 처리하는지 확인해보시기 바랍니다.

// controller
const registerHandler = (req,res) => {
  const { id, name, password } = req.body;
  const user = new User(id,name,password);
  const userService = new UserService(user, userRepository);
  
  try {
    const response = userService.register(user);
    // response 객체로 매핑해주는 과정은 생략합니다.
    res.status(200).json(response);
  }
  catch(e) {
  	if(e instanceof ValidationError){
      res.send(400);
    }
    else {
      res.send(500);
    }
  }
}

class UserService {
  user;
  
  constructor(user){
    this.user = user;
  }
  
  isDuplicate() {
    db.query(`
		select * 
 			.... 
	`)
  }
  
  register() {
    db.query(`
		insert into ...
	`
  }
}

대략적으로 작성한 코드입니다.
보면 UserService에 mysql 관련 구문이 위치해 있는 것을 알 수 있는데요, repository를 따로 사용하지 않기 때문에 UserService에서 쿼리를 작성했습니다. 이 경우 추후 다른 NOSQL 프로그램을 사용할 때 관련 쿼리를 사용하는 모든 도메인 서비스를 수정해야 하는 치명적인 단점을 가지게 됩니다.

이번 단락을 벗어나며

앞서 봤던 유효성 검사를 도메인 객체가 아닌 도메인 서비스에서 처리하게 한 부분은 사실 도메인 객체의 역할을 보는 관점에 따라 논란의 소지가 있을 수 있다고 합니다.

도메인 서비스는 도메인 모델을 코드 상에 나타냈다는 부분에서 도메인 객체, 엔티티와 같다.

  • 도메인 주도 설계 철저 입문 p.73

어떤 처리가 도메인에 중요한 기능이라면 이는 도메인 서비스에 위치해야 합니다. 다시 말하면 사용자 중복 처리가 애플리케이션의 도메인 해석적으로 봤을 때 분명히 도메인 모델이 해야 할 역할이라면 이는 도메인 서비스에 위치해야 합니다. 사용자명 중복은 도메인에 기초한 개념입니다.

하지만 어떤 기능이 애플리케이션을 구동시키기 위한 기능이지 도메인에 중요한 기능이 아니라면 이는 도메인 서비스에 위치하면 안됩니다.
영속성을 위한 데이터스토어는 도메인에 입각한 지식이 아닙니다. 이는 어플리케이션을 구동시키기 위해 존재하는 것이지요.
따라서 이를 도메인 서비스에 위치시키면 안된다는 관점도 존재합니다.
이러한 기능은 애플리케이션 서비스에 위치시킵니다.

리포지토리

리포지토리란

웹사이트는 새로고침을 하면 기존 브라우저 메모리에 담고 있던 모든 값들이 날라가기 때문에 브라우저 스토리지나 서버의 데이터베이스와 같은 도구의 도움을 받습니다. 이 때 데이터를 보관하는 기능을 제공하는 도구를 리포지토리라고 합니다.
그리고 보통 프로젝트 내부에서는 repository 혹은 영속성을 의미하는 persistence와 같은 이름으로 따로 폴더 명을 지어 관리합니다.

리포지토리의 책임은 도메인 객체를 저장하고 복원하는 것입니다. 이 책임을 퍼시스턴시라고 합니다.

앞서 도메인 서비스에서 sql 쿼리를 다루는 부분을 보았듯이, 데이터베이스 기술마다 퍼시스턴시를 구현하는 방식에는 차이가 있어 도메인 코드에 노출시키기에는 적절하지 않습니다. 그 이유는 다음과 같습니다.

  • 코드만 봐서는 어떤 역할을 수행하는지 이해하기 어렵고
  • 다른 기술로 변경하기 어렵습니다.

그렇기 때문에 퍼시스턴시를 추상화 하여 따로 관리하는 것이 좋습니다.

아래의 UserService는 리포지토리를 의존성 주입받아 비즈니스 로직을 순수하게 유지하는 것을 보여줍니다.

class UserService() {
  ...
  userRepository;
  
  constructor(user,userRepository) {
    ...
    this.userRepository = userRepository 
  }
  
  
  isDuplicate() {
    ...
    this.userRepository.find(user.name)
    // return true or false
    ...
  }
  ...
}

리포지토리의 역할

앞서 리포지토리의 역할은 도메인 객체를 저장하고 복원하는 것이라고 했습니다.

타입스크립트와 같은 정적 언어를 사용하면 인터페이스를 만듬으로 이 책임에 대한 선언을 미리 할 수 있는데요

interface UserRepository {
   updateName(id: string, name: string): Promise<void>;
   save(id: string, name: string, password: string): Promise<void>
}  

위의 인터페이스를 보면 updateName과 같이 메서드를 정의하면 updatePassword, updateSomething과 같은 수많은 메소드가 생성될 수 있어 좋지 않습니다.

객체가 저장하고 있는 데이터의 수정은 객체에게 맡기는 것이 좋습니다.

글을 마치며

오늘은 도메인 서비스와 리포지토리에 대해 알아보았습니다.
nodejs로 객체지향 프로그래밍을 하시는 분들이라면 본인의 코드가 어떻게 이뤄져 있는지 한번 점검해보시고 더욱 가독성 있고 변경에 용이하게 만들어보시기 바랍니다.

감사합니다.

profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글