[디자인 패턴] 도메인 모델 패턴과 트랜잭션 스크립트 패턴

이동엽·2023년 2월 19일
3

디자인패턴

목록 보기
1/1

개요

글쓴이는 Java + Spring 조합으로 취업을 하고 싶은 백엔드 개발자이다.

스프링은 배울 내용이 워낙 많기에, 우선 MVC 패턴에 대해 공부하고자 Nest.js를 가볍게 훑어본 경험이 있다.
그 당시에는 강의 영상에서 자연스럽게 트랜잭션 스크립트 패턴을 적용했기 때문에, 그게 답인줄만 알았다.


그러다 인프런에서 김영한님의 JPA 활용 1편을 수강하게되었고, 머릿 속에서 가지고 있던 지식과 새로 들어온 지식의 충돌이 생겼다! → 강의에서는 도메인 모델 패턴을 사용했기 때문!

비즈니스 로직은 서비스 계층에서 담당하여 처리하는 줄로만 알았던 나인데, 도메인에 비즈니스 로직을 담는 방식이라니.. ?


게다가 내용도 모른 체 들어만 본 TDD처럼 DDD라는 용어도 나오니 혼란스럽기 짝이 없다.
이렇게 무엇이 정답인지 모르기에 자료를 찾아보고, 내가 따라쳐본 코드를 통해 차이를 알아보고자 글을 작성한다.


🔥 트랜잭션 스크립트 패턴이란?

엔티티에 비지니스로직이 거의 없고, 서비스 계층에서 비즈니스 로직을 처리하는 방법을 말한다.
따라서 엔티티는 단순하게 데이터를 전달하는 역할이 되면서 서비스 로직이 커지게 된다.
→ 생각해보니 당시 Nest.js를 공부할때 Entity와 DTO를 헷갈려하곤 했다.


내가 사용했던 트랜잭션 스크립트 패턴 예시

사용자 관련 로직을 처리하는 Entity와 DTO의 생김새는 아래와 같다.

  • Entity
@Entity()
@Unique(['username'])
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;

  @OneToMany((type) => Inventory, (inventory) => inventory.user, {
    eager: true,
  })
  inventorys: Inventory[];
}

  • DTO
export class UserUpdateDto {
  username: string;
  password: string;
  updateUsername: string;

  getUpdateUsername(): object {
    return {
      username: this.updateUsername,
    };
  }
}

우선 전체적인 흐름은 아래와 같다.

  • 사용자의 요청은 web 계층의 Controller로 전달된다.
  • Controller는 해당 경로에 맞는 기능을 호출하기 위해 application 계층의 Service에게 전달한다.
  • 데이터베이스에 접근해야 할 로직이라면 persistence 계층의 Repository에게 위임한다.

  • 컨트롤러 - web/user.controller.ts
    • return 문을 보면 요청을 Service에게 위임하는 것을 볼 수 있다.
@Controller('users')
export class UserController {
  constructor(private userService: UserService) {}

  @Patch('/:id/username')
  @UseGuards(AuthGuard('jwt'))
  updateUserName(
    @Param('id', ParseIntPipe) id: number,
    @Body(ValidationPipe) userUpdateDto: UserUpdateDto,
    @GetUser() user: User,
  ) {
    return this.userService.updateUserName(id, userUpdateDto, user);
  }
}

  • 서비스 - application/user.service.ts
    • 생산자에서 의존성 주입을 통해 UserRepository를 이용하여 사용자 이름을 수정하는 비즈니스 로직을 가진다.
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: UserRepository,
  ) {}

  async updateUserName(
    id: number,
    userUpdateDto: UserUpdateDto,
    user: User,
  ): Promise<void> {
    const { username, password } = userUpdateDto;

    if (
      user &&
      user.username === username &&
      bcrypt.compare(password, user.password)
    ) {
      await this.userRepository
        .createQueryBuilder()
        .update(User)
        .set(userUpdateDto.getUpdateUsername())
        .where('id = :id', { id })
        .execute();
    } else {
      throw new NotFoundException(`해당 Id를 가진 회원은 존재하지 않습니다.`);
    }
  }
}

🔥 도메인 모델 패턴이란?

대부분의 비즈니스 로직이 엔티티 안에 구성되어 있고, 서비스 계층은 엔티티에 필요한 역할을 위임한다.
엔티티 안에 비즈니스 로직을 가지고 객체지향을 활용하는 기법이며, DDD를 접목시킬 경우 이 방법을 사용한다.
→ 엔티티에 비즈니스 로직이 있으니, 비즈니스 로직의 유무를 통해 DTO와의 차이점을 더욱 쉽게 이해할 수 있었다.


내가 사용했던 도메인 모델 패턴 예시

주문 도메인에 해당하는 Entity의 생김새는 아래와 같다.

  • 주문 엔티티 - domain/Order.java
    • 엔티티에 주문 취소와 같은 비즈니스 로직들이 담겨있어, 조금 더 객체지향 설계에 가깝다.
    • (이해를 위해 최소한의 메소드만 추출하였다.)
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = LAZY) //XXXToOne은 기본 FetchType이 EAGER이므로 직접 바꿔주기
    @JoinColumn(name = "member_id")
    private Member member;

    //XXXToMany는 기본 FetchType이 Lazy
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> orderItems = new ArrayList<>();

    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)
    @JoinColumn(name = "delivery_id") //주인
    private Delivery delivery;

    private LocalDateTime orderDate; //주문시간

    @Enumerated(EnumType.STRING) //EnumType.ORDINARY가 디폴트인데 절대 쓰지 말 것!! 중간에 다른거 끼면 골치아픔
    private OrderStatus status; //주문상태 [ORDER, CANCEL]을 뜻하는 enum

    //==비즈니스 로직==//
    /**
     * 주문 취소
     */
    public void cancel() {
        if (delivery.getStatus() == DeliveryStatus.COMP) { //이미 배송 완료라면 취소 불가
            throw new IllegalStateException("이미 배송완료된 상품은 취소가 불가능합니다.");
        }

        this.setStatus(OrderStatus.CANCEL);
        for (OrderItem orderItem: orderItems) {
            orderItem.cancel();
        }
    }
}

  • 컨트롤러 - controller/OrderController.java
    • 주문을 취소하는 요청이 들어오면 Service에게 위임한다.
@Controller
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;
    private final MemberService memberService;
    private final ItemService itemService;

    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId") Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}

  • 서비스 - service/OrderService.java
    • Service는 Repository로부터 취소 요청에 해당되는 Order를 얻어온다.
    • 이후, 취소 로직은 Order 객체에 작성되어 있으므로 객체 내의 cancel()을 호출한다.
    • 이전에 설명한 바와 같이, 서비스 계층은 요청을 전달하는 역할만 한다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final MemberRepository memberRepository;
    private final ItemRepository itemRepository;
    
    /**
     * 주문 취소
     */
    @Transactional
    public void cancelOrder(Long orderId) {
        //주문 엔티티 조회
        Order order = orderRepository.findOne(orderId);

        //주문 취소
        order.cancel();
    }
}

아래 사진은 나와 같은 주제로 헷갈려서 질문한 사람에 대한 영한님의 답변이다.

요약

  • DDD : 도메인 주도 설계로, 도메인(엔티티)이 비즈니스 로직의 주도권을 가진다.
    이로 인해 서비스는 엔티티를 호출하는 정도의 얇은 로직만을 가진다. (= 도메인 모델 패턴)
  • 엔티티는 단순히 필드와 getter, setter만을 가지고, 비즈니스 로직은 서비스에서 처리한다.
    이 방식의 경우 엔티티를 객체보단 자료구조(데이터를 전달하는 DTO)로 사용하는 방식이다.


트랜잭션 스크립트 패턴의 장단점

장점

  • 구현이 매우 쉽다.
    • 그 이유는 트랜잭션 스크립트 방식의 구현 방법의 단순함 때문이다.
    • MVC 패턴과 커맨드 패턴을 함께 사용하더라도 이러한 단순함은 그대로 유지되기 때문에, 스트러츠 코드에 조금만 익숙해지면 손쉽게 스트러츠로도 트랜잭션 스크립트 패턴을 구현할 수 있게 된다.

단점

  • 트랜잭션 스크립트로 구성된 어플리케이션은 비즈니스 로직이 복잡해질수록 난잡한 코드를 만들게 된다.
    • 애초에 도메인에 대한 분석/설계 개념이 약하기 때문에 코드의 중복 발생을 막기 어려워진다.
    • 또한, 쉬운 개발에 익숙해지기 때문에 공통된 코드를 공통 모듈로 분리하지 않고 복사&붙이기 방식으로 중복된 코드를 만드는 유혹에 빠지기 쉽다.

도메인 모델 패턴의 장단점

장점

  • 객체 지향에 기반한 재사용성, 확장성, 그리고 유지 보수의 편리함에 있다.
    • 일단 도메인 모델을 구축하고나면 약간의 수정은 필요하겠지만 언제든지 재사용할 수 있다.
    • 또한, 인터페이스에서 더 나아가 컴포넌트 개념을 바탕으로 도메인 모델을 개발하게 되면 무한한 확장성을 갖게 된다.

단점

  • 하나의 도메인 모델을 구축하는 데 많은 노력이 필요하다.
    • 객체를 판별해내야 하고 객체들 간의 관계를 정리하고 설계하며, 객체와 데이터베이스 사이의 매핑이 필요하기 때문이다.
    • 이는 이론과 경험을 함께 겸비하지 않으면 쉽게 풀수 없는 문제이기 때문에, 도메인 모델에 능숙한 개발자가 팀에 없을 경우 도메인 모델을 구축하는 것 자체가 힘들어질 수도 있다.


참고자료

profile
백엔드 개발자로 등 따숩고 배 부르게 되는 그 날까지

1개의 댓글

comment-user-thumbnail
2023년 2월 19일

오늘도 잘 보고 갑니다.

답글 달기