DDD Layered Architecture의 application service와 domain service에 어떤 코드를 넣어야 하는 걸까?🤔
이것은 일주일째 유저 코드의 생성, 수정을 몇 번이나 작성하게 만들고 있다.
DDD에 익숙하지 않은 나에게 코드를 작성하고 멘토에게 수정을 받을때마다 나는
도메인? 도메인 서비스에는 도대체 뭐를 작성해야하는거지? 라는 근원으로 질문을 던지게 된다.
처음 개발한 코드는 도메인 서비스에 기능을 몰빵해서 만들었다.(전 mono가 익숙하거든요 하하하)
1차 멘토링 후
1) 멘토의 말: 도메인 서비스가 아닌 어플리케이션 서비스에 구현해야한다.
2) 책 읽음: 최범균님의 '도메인 주도 개발 시작하기' 6.2 응용서비스의 역할을 읽고서는
(하나만 아는 놈이 신념을 가지면 안된다, 책 한권 읽은 사람이 제일 무섭다)
6.2 응용 서비스의 역할
응용 서비스는 클라이언트가 요청한 기능을 실행한다.
응용 서비스는 도메인 객체를 사용해서 사용자의 요청을 처리하는 것.
도메인 영역과 표현 영역을 연결해주는 창구 역할.
6.2.1 도메인 로직 넣지 않기
도메인 로직은 도메인 영역에 위치하고, 응용 서비스는 도메인 로직을 구현하지 않는다.
암호 변경 기능을 예로 들면
1) 비밀번호 변경 시, 기존 비번이 일치하는지 체크한다
기존 비번이 일치하는지 체크하는 것이 도메인의 핵심 로직이다!
따라서 응용 서비스에 이 로직을 구현하면 안된다!
--> 다시 읽어보니
2) 계정 정지 기능, 기존 비번이 일치하는지 체크한다
계정정지Service에서도 비번 일치 로직이 필요함.
따라서 코드 중복을 막기 위해
1) 응용 서비스 영역에서 보조 클래스를 만들거나
2) 도메인 영역에 암호 확인 기능을 구현하거나
책은 2번을 추천하고 있다
Member member = memberRepository.findById(memberId)
member.matchPassword(currentPassword); // 이런 식으로 도메인 영역에 암호 확인 기능을 넣어라
근데 이건 도메인 service가 아니라 MemberEntity에 넣어야 하는거 아닌가?-_-?
2차 DDD layered Architecture를 고려한 controller, application.service, domain.service의 모습을 가지게 되었다.
나의 현재 코드는 아래와 같다.
2차 controller
package com.coopang.user.presentation.controller;
import com.coopang.user.application.enums.UserRoleEnum;
import com.coopang.user.application.request.UserDto;
import com.coopang.user.application.response.UserResponseDto;
import com.coopang.user.application.service.UserService;
import com.coopang.user.presentation.request.ChangePasswordRequestDto;
import com.coopang.user.presentation.request.SearchRequestDto;
import com.coopang.user.presentation.request.SignupRequestDto;
import com.coopang.user.presentation.request.UpdateRequestDto;
import com.coopang.user.presentation.request.UserSearchCondition;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@Tag(name = "UserController API", description = "UserController API")
@Slf4j
@RestController
@RequestMapping("/users/v1")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// 회원 가입
@PostMapping("/join")
public ResponseEntity<String> signupUser(@Valid @RequestBody SignupRequestDto signupRequestDto) {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = mapper.map(signupRequestDto, UserDto.class);
final UserResponseDto user = userService.join(userDto);
return new ResponseEntity<>(user.getEmail(), HttpStatus.OK);
}
// 회원 정보 조회
@GetMapping("/user/{userId}")
public ResponseEntity<UserResponseDto> getUserInfo(@PathVariable("userId") UUID userId) {
final UserResponseDto userInfo = userService.getUserInfoById(userId);
return new ResponseEntity<>(userInfo, HttpStatus.OK);
}
// 회원 정보 수정
@Secured(UserRoleEnum.Authority.MASTER)
@PutMapping("/user/{userId}")
public ResponseEntity<UserResponseDto> updateUserInfo(@PathVariable("userId") UUID userId, @RequestBody UpdateRequestDto updateRequestDto) {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserDto userDto = mapper.map(updateRequestDto, UserDto.class);
userService.updateUser(userId, userDto);
final UserResponseDto userInfo = userService.getUserInfoById(userId);
return new ResponseEntity<>(userInfo, HttpStatus.OK);
}
// 회원 삭제
@Secured(UserRoleEnum.Authority.MASTER)
@DeleteMapping("/user/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable("userId") UUID deleteUserId) {
userService.deleteUser(deleteUserId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
// 회원 목록 조회 엔드포인트
@Secured(UserRoleEnum.Authority.MASTER)
@GetMapping("/user")
public ResponseEntity<Page<UserResponseDto>> getAllUsers(SearchRequestDto searchRequestDto) {
Page<UserResponseDto> users = userService.getAllUsers(searchRequestDto);
return new ResponseEntity<>(users, HttpStatus.OK);
}
// 회원 검색 엔드포인트
@Secured(UserRoleEnum.Authority.MASTER)
@GetMapping("/user/search")
public ResponseEntity<Page<UserResponseDto>> searchUsers(UserSearchCondition userSearchCondition, SearchRequestDto searchRequestDto) {
Page<UserResponseDto> users = userService.searchUsers(userSearchCondition, searchRequestDto);
return new ResponseEntity<>(users, HttpStatus.OK);
}
@PatchMapping("/user/change-password")
public ResponseEntity<String> changePassword(@RequestBody @Valid ChangePasswordRequestDto changePasswordRequestDto) {
userService.changePassword(changePasswordRequestDto);
return new ResponseEntity<>("비밀번호가 성공적으로 변경되었습니다.", HttpStatus.OK);
}
@Secured(UserRoleEnum.Authority.MASTER)
@PatchMapping("/user/block/{userId}")
public ResponseEntity<String> block(@PathVariable("userId") UUID userId) {
userService.blockUser(userId);
return new ResponseEntity<>(userId + " 블록 처리가 완료 되었습니다.", HttpStatus.OK);
}
}
2차 application.service
package com.coopang.user.application.service;
import com.coopang.user.application.enums.UserRoleEnum;
import com.coopang.user.application.error.UserNotFoundException;
import com.coopang.user.application.request.UserDto;
import com.coopang.user.application.response.UserResponseDto;
import com.coopang.user.domain.entity.user.UserEntity;
import com.coopang.user.domain.repository.UserRepository;
import com.coopang.user.domain.service.UserDomainService;
import com.coopang.user.presentation.request.ChangePasswordRequestDto;
import com.coopang.user.presentation.request.SearchRequestDto;
import com.coopang.user.presentation.request.UserSearchCondition;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
@Slf4j(topic = "UserService")
@Transactional
@Service
public class UserService {
private final UserRepository userRepository;
private final UserDomainService userDomainService;
public UserService(UserRepository userRepository, UserDomainService userDomainService) {
this.userRepository = userRepository;
this.userDomainService = userDomainService;
}
public UserResponseDto join(UserDto userDto) {
// email 중복 확인
if (userRepository.existsByEmail(userDto.getEmail())) {
throw new IllegalArgumentException("Email already exists");
}
return UserResponseDto.fromUser(userDomainService.createUser(userDto));
}
public UserResponseDto loginByEmail(String email, String password) {
UserEntity user = getUserInfoByEmail(email);
checkPassword(user, password);
return UserResponseDto.fromUser(user);
}
// 조회
@Cacheable(value = "users", key = "#userId")
public UserResponseDto getUserInfoById(UUID userId) {
UserEntity user = findById(userId);
return UserResponseDto.fromUser(user);
}
public UserEntity getUserInfoByEmail(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundException(email));
}
@Cacheable(value = "allUsers", key = "#condition")
public Page<UserResponseDto> searchUsers(UserSearchCondition condition, SearchRequestDto searchRequestDto) {
PageRequest pageRequest = PageRequest.of(
searchRequestDto.getValidatedPage(),
searchRequestDto.getValidatedSize(),
Sort.by(searchRequestDto.getValidatedSortBy())
);
Page<UserEntity> users = userRepository.search(condition, pageRequest);
return users.map(UserResponseDto::fromUser);
}
// 수정
@CacheEvict(value = "users", key = "#userId")
public void updateUser(UUID userId, UserDto dto) {
UserEntity user = findById(userId);
userDomainService.updateUser(user, dto);
log.debug("updateUser userId:{}", userId);
}
public void changePassword(ChangePasswordRequestDto request) {
UUID userId = request.getUserId();
UserEntity user = findById(userId);
// 비밀번호 확인
checkPassword(user, request.getCurrentPassword());
userDomainService.changePassword(user, request);
log.debug("changePassword userId:{}", userId);
}
public void blockUser(UUID userId) {
UserEntity user = findById(userId);
userDomainService.blockUser(user);
log.debug("blockUser userId:{}", userId);
}
// 삭제
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(UUID userId) {
UserEntity user = findById(userId);
userDomainService.deleteUser(user);
log.debug("deleteUser userId:{}", userId);
}
@Cacheable(value = "allUsers", key = "#searchRequestDto")
public Page<UserResponseDto> getAllUsers(SearchRequestDto searchRequestDto) {
UserSearchCondition condition = new UserSearchCondition();
condition.setUserName(searchRequestDto.getKeyword());
Pageable pageable = PageRequest.of(searchRequestDto.getValidatedPage(), searchRequestDto.getValidatedSize(), Sort.by(searchRequestDto.getValidatedSortBy()));
Page<UserEntity> users = userRepository.search(condition, pageable);
return users.map(UserResponseDto::fromUser);
}
@Cacheable(value = "users", key = "#userId")
public UserEntity findById(UUID userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException(userId.toString()));
}
public void checkPassword(UserEntity user, String currentPassword) {
try {
userDomainService.checkPassword(user, currentPassword);
} catch (Exception e) {
throw new IllegalArgumentException("현재 비밀번호가 일치하지 않습니다.", e);
}
}
}
2차 domain.service
package com.coopang.user.domain.service;
import com.coopang.user.application.enums.UserRoleEnum;
import com.coopang.user.application.request.UserDto;
import com.coopang.user.domain.entity.user.UserEntity;
import com.coopang.user.infrastructure.repository.UserJpaRepository;
import com.coopang.user.presentation.request.ChangePasswordRequestDto;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@Service
public class UserDomainService {
private final UserJpaRepository userJpaRepository;
private final PasswordEncoder passwordEncoder;
public UserDomainService(UserJpaRepository userJpaRepository, PasswordEncoder passwordEncoder) {
this.userJpaRepository = userJpaRepository;
this.passwordEncoder = passwordEncoder;
}
// 생성
public UserEntity createUser(UserDto userDto) {
// 비밀번호 암호화
final String encodedPassword = passwordEncoder.encode(userDto.getPassword());
// 회원 등록
UserEntity newUser = UserEntity.create(userDto, encodedPassword);
return userJpaRepository.save(newUser);
}
// 수정
public void updateUser(UserEntity user, UserDto dto) {
user.updateUserInfo(dto);
userJpaRepository.save(user);
}
public void changePassword(UserEntity user, ChangePasswordRequestDto dto) {
final String encodedPassword = passwordEncoder.encode(dto.getNewPassword());
user.changePassword(encodedPassword);
userJpaRepository.save(user);
}
public void checkPassword(UserEntity user, String currentPassword) {
// 현재 비밀번호 확인
if (!passwordEncoder.matches(currentPassword, user.getPassword())) {
throw new IllegalArgumentException();
}
}
public void blockUser(UserEntity user) {
user.setBlocked();
userJpaRepository.save(user);
}
// 삭제
public void deleteUser(UserEntity user) {
user.setDeleted(true);
userJpaRepository.save(user);
}
}
이렇게 작성했으나
멘토는 나에게 다시 피드백을 해주었다.
* 단순히 엔티티의 상태를 수정하는 로직은 도메인 서비스를 따로 구성하실 필요가 없습니다.
예를들어, 아래와 같은 도메인 서비스 로직이 있다면, 도메인 서비스가 행하는 것은 서비스의 요청을 그대로 엔티티에 전달하는 것 밖에 없습니다.
응용 서비스에서 user.updateUserInfo 함수로 바로 호출하면 아래의 로직은 필요가 없어 질 것 같습니다.
public void updateUser(UserEntity user, UpdateRequestDto dto) {
user.updateUserInfo(dto);
userJpaRepository.save(user);
}
* DDD의 관점에서 엔티티에서 DTO 를 사용하는 로직은 모두 제거해야 합니다.
DDD 는 3 Layer (도메인 -> 응용 -> 표현 계층의 순서로 고수준 모듈임) 를 따르기 때문에 도메인 영역의 엔티티가 응용 계층인 DTO 를 의존하면 안됩니다.
현재 구조에서는 만약 클라이언트의 요청으로 인해 DTO 에서 name 의 이름이 nickName 으로 변경 될 경우 엔티티까지 수정해야 하는 문제가 있습니다.
* 영속성 컨텍스트를 활용해보세요
userDomainService 에서 아래와 같은 코드를 확인했습니다.
// 수정
public void updateUser(UserEntity user, UpdateRequestDto dto) {
user.updateUserInfo(dto);
userJpaRepository.save(user);
}
아래와 같이 수정이 가능합니다.
// 수정
@Transactional // 영속성 컨텍스트를 사용하기 위함
public void updateUser(UserEntity user, UpdateRequestDto dto) {
user.updateUserInfo(dto);
// 영속성 컨텍스의 변경 감지(Dirty Checking)로 인해 repository.save 없이 DB 에 자동으로 업데이트 됩니다.
}
도메인 서비스에서 단순한 생성, 수정을 없애라.
추가로 지적해주신 것처럼 presantation 영역에서 service로 넘길때 dto를 변경해서 넘기는 것으로 수정했다.
이것도 물론 최범균님의 책과 예시 git 프로젝트를 보고 이해한 뒤 적용했다.
requestDto -> userDto 로 controller에서 변환해서 application과 domain 부분을 분리하였다.
그래서 위에 코드의 controller의 생성, 수정을 보면 UserDto mapper가 반영되어있음
그래서 3차 domain service에서는 비밀번호 관련 도메인 핵심기능인 비밀번호 관련만 남기고 생성, 수정은 없애야겠다.
아직 주문, 배달은 하지도 않았는데 거긴 application service와 domain service를 나눌 때 더 생각을 많이하고, 더 복잡하겠지?
DDD Layerd Architecture 구조는 사람마다 달랐다
나의 DDD layered Architecture 구조
나의 구조
어플리케이션은 서비스를 중심으로 작성
도메인은 jpa Entity 중심으로 작성했지만, UserRepository interface를 넣어놔서 infra에 의존하지 않게 했다.
인프라에는
QueryDSL 인터페이스, JPA 인터페이스, 즉 언제든 DB를 다른 것으로 갈아 끼울 수 있게 분리함.
프레젠테이션에는
컨트롤러와 외부에서 보내는 requestDto를 중심으로 작성하였다.
최범균님의 DDD layered Architecture 구조
최범균님의 구조
멤버 서버는 어플리케이션, 도메인, 인프라 3개
좀더 자세히 알고 싶어서 order 서버를 보면
command, infra, query, ui로 나뉜다.
command는 수정을 담당하는 영역이고 query는 조회를 담당하는 영역이다.
이 두개를 나는 합쳐서 사용했는데 조회는 빠르게 불러와야할때 기존 db가 아니라 nosql로 적용해서 불러오는 방법도 있기에 저렇게 분리하셨다고 책에 써져있다.
그러면 결국 application, domain, infra, ui 4개의 구조로 나뉜다.
최범균님이 강조한 도메인 패키지에는 과연 뭐가 들었을까?
서비스가 없다.. 두둥
그렇다 그는 domain service를 Order 안에 함수로 넣어놨다.
이런 domain service 파일이 없잖아? orderEntity.java에 도메인 기능을 넣어버렸군요!
package com.myshop.order.command.domain;
import com.myshop.common.event.Events;
import com.myshop.common.model.Money;
import javax.persistence.*;
import java.util.Date;
import java.util.List;
@Entity
@Table(name = "purchase_order")
@Access(AccessType.FIELD)
public class Order {
@EmbeddedId
private OrderNo number;
@Version
private long version;
@Embedded
private Orderer orderer;
@ElementCollection
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
@OrderColumn(name = "line_idx")
private List<OrderLine> orderLines;
@Column(name = "total_amounts")
private Money totalAmounts;
@Embedded
private ShippingInfo shippingInfo;
@Column(name = "state")
@Enumerated(EnumType.STRING)
private OrderState state;
@Temporal(TemporalType.TIMESTAMP)
private Date orderDate;
protected Order() {
}
public Order(OrderNo number, Orderer orderer, List<OrderLine> orderLines,
ShippingInfo shippingInfo, OrderState state) {
setNumber(number);
setOrderer(orderer);
setOrderLines(orderLines);
setShippingInfo(shippingInfo);
this.state = state;
this.orderDate = new Date();
Events.raise(new OrderPlacedEvent(number.getNumber(), orderer, orderLines, orderDate));
}
private void setNumber(OrderNo number) {
if (number == null) throw new IllegalArgumentException("no number");
this.number = number;
}
private void setOrderer(Orderer orderer) {
if (orderer == null) throw new IllegalArgumentException("no orderer");
this.orderer = orderer;
}
private void setOrderLines(List<OrderLine> orderLines) {
verifyAtLeastOneOrMoreOrderLines(orderLines);
this.orderLines = orderLines;
calculateTotalAmounts();
}
private void verifyAtLeastOneOrMoreOrderLines(List<OrderLine> orderLines) {
if (orderLines == null || orderLines.isEmpty()) {
throw new IllegalArgumentException("no OrderLine");
}
}
private void calculateTotalAmounts() {
this.totalAmounts = new Money(orderLines.stream()
.mapToInt(x -> x.getAmounts().getValue()).sum());
}
private void setShippingInfo(ShippingInfo shippingInfo) {
if (shippingInfo == null) throw new IllegalArgumentException("no shipping info");
this.shippingInfo = shippingInfo;
}
public OrderNo getNumber() {
return number;
}
public long getVersion() {
return version;
}
public Orderer getOrderer() {
return orderer;
}
public Money getTotalAmounts() {
return totalAmounts;
}
public ShippingInfo getShippingInfo() {
return shippingInfo;
}
public OrderState getState() {
return state;
}
public void changeShippingInfo(ShippingInfo newShippingInfo) {
verifyNotYetShipped();
setShippingInfo(newShippingInfo);
Events.raise(new ShippingInfoChangedEvent(number, newShippingInfo));
}
public void cancel() {
verifyNotYetShipped();
this.state = OrderState.CANCELED;
Events.raise(new OrderCanceledEvent(number.getNumber()));
}
private void verifyNotYetShipped() {
if (!isNotYetShipped())
throw new AlreadyShippedException();
}
public boolean isNotYetShipped() {
return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING;
}
public List<OrderLine> getOrderLines() {
return orderLines;
}
public boolean matchVersion(long version) {
return this.version == version;
}
public void startShipping() {
verifyShippableState();
this.state = OrderState.SHIPPED;
Events.raise(new ShippingStartedEvent(number.getNumber()));
}
private void verifyShippableState() {
verifyNotYetShipped();
verifyNotCanceled();
}
private void verifyNotCanceled() {
if (state == OrderState.CANCELED) {
throw new OrderAlreadyCanceledException();
}
}
}
그러면 application service는 어떻게 작성했을까?
조회용 application. service
package com.myshop.order.query.application;
import com.myshop.catalog.application.ProductService;
import com.myshop.catalog.domain.product.Product;
import com.myshop.order.command.domain.Order;
import com.myshop.order.command.domain.OrderNo;
import com.myshop.order.command.domain.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
public class OrderDetailService {
private OrderRepository orderRepository;
private ProductService productService;
@Transactional
public Optional<OrderDetail> getOrderDetail(String orderNumber) {
Order order = orderRepository.findById(new OrderNo(orderNumber));
if (order == null) return Optional.empty();
List<OrderLineDetail> orderLines = order.getOrderLines().stream()
.map(orderLine -> {
Optional<Product> productOpt = productService.getProduct(orderLine.getProductId().getId());
return new OrderLineDetail(orderLine, productOpt.get());
}).collect(Collectors.toList());
return Optional.of(new OrderDetail(order, orderLines));
}
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired
public void setProductService(ProductService productService) {
this.productService = productService;
}
}
수정용 application service
package com.myshop.order.command.application;
import com.myshop.common.event.Events;
import com.myshop.order.NoOrderException;
import com.myshop.order.command.domain.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class CancelOrderService {
private OrderRepository orderRepository;
private RefundService refundService;
private CancelPolicy cancelPolicy;
@Transactional
public void cancel(OrderNo orderNo, Canceller canceller) {
Events.handle((OrderCanceledEvent evt) -> refundService.refund(evt.getOrderNumber()));
Order order = findOrder(orderNo);
if (!cancelPolicy.hasCancellationPermission(order, canceller)) {
throw new NoCancellablePermission();
}
order.cancel();
//Events.reset();
}
private Order findOrder(OrderNo orderNo) {
Order order = orderRepository.findById(orderNo);
if (order == null) throw new NoOrderException();
return order;
}
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired
public void setRefundService(RefundService refundService) {
this.refundService = refundService;
}
@Autowired
public void setCancelPolicy(CancelPolicy cancelPolicy) {
this.cancelPolicy = cancelPolicy;
}
}
package com.myshop.order.command.application;
import com.myshop.order.command.domain.Order;
import com.myshop.order.command.domain.OrderNo;
import com.myshop.order.command.domain.OrderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static com.myshop.order.command.application.CheckOrder.checkNoOrder;
@Service
public class StartShippingService {
private OrderRepository orderRepository;
@Transactional
public void startShipping(StartShippingRequest req) {
Order order = orderRepository.findById(new OrderNo(req.getOrderNumber()));
checkNoOrder(order);
if (!order.matchVersion(req.getVersion())) {
// throw new VersionConflictException();
throw new OptimisticLockingFailureException("version conflict");
}
order.startShipping();
}
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
인프런 강사의 DDD layered Architecture 구조
크게 controller, dto, jpa, service, vo, messageQueue로 나뉜다.
controller
package com.example.orderservice.controller;
import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;
import com.example.orderservice.messagequeue.KafkaProducer;
import com.example.orderservice.messagequeue.OrderProducer;
import com.example.orderservice.service.OrderService;
import com.example.orderservice.vo.RequestOrder;
import com.example.orderservice.vo.ResponseOrder;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/order-service")
@Slf4j
public class OrderController {
Environment env;
OrderService orderService;
KafkaProducer kafkaProducer;
OrderProducer orderProducer;
@Autowired
public OrderController(Environment env, OrderService orderService,
KafkaProducer kafkaProducer, OrderProducer orderProducer) {
this.env = env;
this.orderService = orderService;
this.kafkaProducer = kafkaProducer;
this.orderProducer = orderProducer;
}
@GetMapping("/health_check")
public String status() {
return String.format("It's Working in Order Service on PORT %s",
env.getProperty("local.server.port"));
}
@PostMapping("/{userId}/orders")
public ResponseEntity<ResponseOrder> createOrder(@PathVariable("userId") String userId,
@RequestBody RequestOrder orderDetails) {
log.info("Before add orders data");
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
OrderDto orderDto = mapper.map(orderDetails, OrderDto.class);
orderDto.setUserId(userId);
/* jpa */
OrderDto createdOrder = orderService.createOrder(orderDto);
ResponseOrder responseOrder = mapper.map(createdOrder, ResponseOrder.class);
/* kafka */
// orderDto.setOrderId(UUID.randomUUID().toString());
// orderDto.setTotalPrice(orderDetails.getQty() * orderDetails.getUnitPrice());
/* send this order to the kafka */
// kafkaProducer.send("example-catalog-topic", orderDto);
// orderProducer.send("orders", orderDto);
// ResponseOrder responseOrder = mapper.map(orderDto, ResponseOrder.class);
log.info("After added orders data");
return ResponseEntity.status(HttpStatus.CREATED).body(responseOrder);
}
@GetMapping("/{userId}/orders")
public ResponseEntity<List<ResponseOrder>> getOrder(@PathVariable("userId") String userId) throws Exception {
log.info("Before retrieve orders data");
Iterable<OrderEntity> orderList = orderService.getOrdersByUserId(userId);
List<ResponseOrder> result = new ArrayList<>();
orderList.forEach(v -> {
result.add(new ModelMapper().map(v, ResponseOrder.class));
});
try {
Thread.sleep(1000);
throw new Exception("장애 발생");
} catch(InterruptedException ex) {
log.warn(ex.getMessage());
}
log.info("Add retrieved orders data");
return ResponseEntity.status(HttpStatus.OK).body(result);
}
}
application service 코드
package com.example.orderservice.service;
import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;
import com.example.orderservice.jpa.OrderRepository;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class OrderServiceImpl implements OrderService {
OrderRepository orderRepository;
@Autowired
public OrderServiceImpl(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public OrderDto createOrder(OrderDto orderDto) {
orderDto.setOrderId(UUID.randomUUID().toString());
orderDto.setTotalPrice(orderDto.getQty() * orderDto.getUnitPrice());
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
OrderEntity orderEntity = mapper.map(orderDto, OrderEntity.class);
orderRepository.save(orderEntity);
OrderDto returnValue = mapper.map(orderEntity, OrderDto.class);
return returnValue;
}
@Override
public OrderDto getOrderByOrderId(String orderId) {
OrderEntity orderEntity = orderRepository.findByOrderId(orderId);
OrderDto orderDto = new ModelMapper().map(orderEntity, OrderDto.class);
return orderDto;
}
@Override
public Iterable<OrderEntity> getOrdersByUserId(String userId) {
return orderRepository.findByUserId(userId);
}
}
결론 DDD Layerd Architecture 는 강사마다, Git을 뒤져보니 사람마다 다르게 생겨서
정답에 가깝게 만들고 싶었으나... 정답이 없는 것 같다.
그래서 나는 나의 방식으로 가야할 듯 하다.. + 최범균 아저씨처럼 application service에서 불러오고 domain service는 entity에 적는걸로 대체할 예정.
정해져 있는게 오히려 속도는 빠를텐데, 정답이 없으니, 원하는 모델 코드가 없다보니, 뭔가 계속해서 시행착오를 겪게 된다.
'Today I Learned' 카테고리의 다른 글
@Component, @Configuration + @ConfigurationProperties (0) | 2024.10.02 |
---|---|
페이징 처리 (0) | 2024.09.23 |
DDD Architecture (0) | 2024.09.14 |
grafana에 loki 추가 (0) | 2024.09.14 |
grafana slack 연동 (0) | 2024.09.14 |