글쓴이는 Java + Spring 조합으로 취업을 하고 싶은 백엔드 개발자이다.
스프링은 배울 내용이 워낙 많기에, 우선 MVC 패턴에 대해 공부하고자 Nest.js를 가볍게 훑어본 경험이 있다.
그 당시에는 강의 영상에서 자연스럽게 트랜잭션 스크립트 패턴을 적용했기 때문에, 그게 답인줄만 알았다.
그러다 인프런에서 김영한님의 JPA 활용 1편을 수강하게되었고, 머릿 속에서 가지고 있던 지식과 새로 들어온 지식의 충돌이 생겼다! → 강의에서는 도메인 모델 패턴을 사용했기 때문!
비즈니스 로직은 서비스 계층에서 담당하여 처리하는 줄로만 알았던 나인데, 도메인에 비즈니스 로직을 담는 방식이라니.. ?
게다가 내용도 모른 체 들어만 본 TDD처럼 DDD라는 용어도 나오니 혼란스럽기 짝이 없다.
이렇게 무엇이 정답인지 모르기에 자료를 찾아보고, 내가 따라쳐본 코드를 통해 차이를 알아보고자 글을 작성한다.
엔티티에 비지니스로직이 거의 없고, 서비스 계층에서 비즈니스 로직을 처리하는 방법을 말한다.
따라서 엔티티는 단순하게 데이터를 전달하는 역할이 되면서 서비스 로직이 커지게 된다.
→ 생각해보니 당시 Nest.js를 공부할때 Entity와 DTO를 헷갈려하곤 했다.
사용자 관련 로직을 처리하는 Entity와 DTO의 생김새는 아래와 같다.
@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[];
}
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
@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
@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
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();
}
}
아래 사진은 나와 같은 주제로 헷갈려서 질문한 사람에 대한 영한님의 답변이다.
요약
참고자료
오늘도 잘 보고 갑니다.