ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OOP] DTO, Entity와 객체지향적 사고
    Software Design 2022. 1. 10. 20:47
    반응형

    express, mongoose 두 스택을 사용할 때는 DTO와 Entity를 사용하는 법은커녕 개념조차 깊게 이해하고 있지 못했다.

    그러다가 Nest.js, TypeORM 스택을 이용해서 개발을 하다보니 DTO와 Entity에 대해 알게 되고, 객체지향이란 무엇일까를 생각하며 코드를 작성하는 방식이 바뀌고 있다.

    계층형 아키텍처에서 Controller는 DTO로 변환된 요청 body를 매개변수로 적절한 Service의 메소드를 호출하게 되고, Service는 DTO를 비즈니스 로직을 통해 Entity로 변환된 객체를 Repository를 이용해서 저장하게 된다.

     

     

    이때 DTO를 Entity로 변환해보면서 객체지향적인 방법에 대해서 고민했던 점들을 정리해 보았다.

    스키마

    사용자가 회원가입 form을 작성해서 서버에 요청을 보내고, 서버에서는 사용자를 데이터베이스에 저장하는 서비스를 다음과 같은 Entity, DTO, Service로 구현해보려고 한다.

    @Entity()
    class User {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      email: string;
    
      @Column()
      password: string;
    
      @Column()
      name: string;
    
      @Column({
        name: 'created_at',
        type: 'timestamp with time zone',
      })
      createdAt: Date;
    }
    class CreateUserDto {
      email: string;
      password: string;
      name: string;
    }
    @Injectable()
    class UserService {
      constructor(
        private userRepository: UserRepository,
      ) {}
    
      create(createUserDto: CreateUserDto) {
        // ...
      }
    }

    DTO에서 Entity로

    가장 단순하게 구현하는 방법은 다음과 같이 서비스의 메소드에 DTO를 Entity로 변환하는 로직을 작성하는 것이다.

    // UserService
    async create(createUserDto: CreateUserDto) {
      const user = new User();
      user.email = createUserDto.email;
      user.password = createUserDto.password; // 패스워드 hash 생략
      user.name = createUserDto.name;
      user.createdAt = new Date();
    
      const { id } = await this.userRepositoiry.save(user);
      return id;
    }

    이 방식은 다음과 같은 문제들이 있다.

    1. Entity 객체를 생성할때 필요한 로직이 서비스의 메소드에 구현되어있다.
    2. 여러 메소드들에서 Entity를 생성해야 한다면 중복되는 코드를 작성하게 된다.
    3. Entity를 생성하는 로직이 변경되는 경우, Entity를 생성하는 모든 코드를 찾아서 수정해야 한다.

    Entity 객체를 생성하는 책임은 해당 Entity에 있다. 그렇기 때문에 해당 Entity에 인스턴스를 생성하는 메소드를 구현해야 한다.

    User Entity에 static 메소드로 해당 로직을 구현해주자.(생성자를 사용하지 않는 이유)

    @Entity()
    class User {
      // ...
    
      static from(createUserDto: CreateUserDto) {
        const user = new User();
        user.email = createUserDto.email;
        user.password = createUserDto.password;
        user.name = createUserDto.name;
        user.createdAt = new Date();
        return user;
      }
    }
    // UserService
    async create(createUserDto: CreateUserDto) {
      const user = User.from(createUserDto);
    
      const { id } = await this.userRepositoiry.save(user);
      return id;
    }

    Entity를 생성하는 로직이 해당 Entity에 구현되었기 때문에 필요한 곳에서 Entity의 메소드를 통해 Entity를 생성할 수 있다.


    하지만 이 방식은 큰 문제가 있는데, 바로 Entity가 DTO에 의존하게 되는 문제가 있다.

    createUserDto는 Presentation layer에서 전달되는 DTO이므로, 클라이언트의 요청 인터페이스에서의 변경이 Entity를 생성하는 로직에 직접적인 변화를 준다는 것이다.

    Entity는 비즈니스 로직의 중심이 되어야 하기 때문에, 특정 DTO에 의존하지 않게 구현해주자.

    @Entity()
    class User {
      // ...
    
      static from(
        email: string,
        password: string,
        name: string,
      ) {
        const user = new User();
        user.email = email;
        user.password = password;
        user.name = name;
        user.createdAt = new Date();
        return user;
      }
    }
    // UserService
    async create(createUserDto: CreateUserDto) {
      const user = User.from(
        createUserDto.email,
        createUserDto.password,
        createUserDto.name,
      );
    
      const { id } = await this.userRepositoiry.save(user);
      return id;
    }

    User Entity에서 CreateUserDto에 의존하는 부분을 제거하고, 매개변수로 필요한 값들을 전달받아 객체를 생성하게 수정하였다.


    하지만 아직 문제가 다 해결된 것은 아니다.

    현재 DTO의 모든 필드는 public으로 공개되어있다. 그렇기 때문에 Service의 메소드에서 DTO의 모든 프로퍼티에 직접 접근해서 User Entity를 생성하고 있다.

    객체의 필드가 public으로 공개되거나 무분별한 getter, setter를 통해 접근성과 불변성이 보장되지 않는 것은 잠재적인 문제를 발생시킬 수 있다. 객체지향의 원칙 중 하나인 캡슐화가 전혀 되어있지 않기 때문이다.

    다음과 같이 DTO의 필드들을 private으로 선언해주자.

    class CreateUserDto {
      private email: string;
      private password: string;
      private name: string;
    }
    // UserService
    async create(createUserDto: CreateUserDto) {
      const user = User.from(
        // createUserDto.email,
        // createUserDto.password,
        // createUserDto.name,
      );
    
      const { id } = await this.userRepositoiry.save(user);
      return id;
    }

    더 이상 Service에서 해당 DTO의 필드에 접근할 수가 없게 되었다. 그렇기 때문에 User Entity를 생성하는 것도 불가능해졌다.

    어떻게 DTO를 Entity로 변환할 수 있을까?

    DTO에 자신을 Entity로 변환해주는 메소드를 작성해주면 된다. 해당 DTO에 Entity의 from 메소드를 통해 자기의 프로퍼티를 이용해 User Entity를 만드는 메소드를 구현해주자.

    class CreateUserDto {
      // ...
    
      toUserEntity() {
        return User.from(
          this.email,
          this.password,
          this.name,
        );
      }
    }

    최종적인 Service 메소드는 다음과 같다.

    // UserService
    async create(createUserDto: CreateUserDto) {
      const user = createUserDto.toUserEntity();
    
      const { id } = await this.userRepositoiry.save(user);
      return id;
    }

    각각의 객체가 책임과 역할을 가지게 되고, 서비스에 구현되는 로직은 해당 객체의 메소드를 이용하게 바뀌었다.

    (Github 링크)

    References

    반응형

    'Software Design' 카테고리의 다른 글

    [OOP] Tell, Don't Ask(TDA) 원칙과 캡슐화  (1) 2022.01.18

    댓글

Designed by Tistory.