프로젝트를 수도 없이 리팩토링했다.
보다 더 객체지향적인 코드를 작성하기 위한,
유지 보수가 쉬운 코드를 작성하기 위한,
더 작은 객체를 위한 코드를 계속해서 고민하고 구조를 변경했다.
지난 달부터 해서 소프트 웨어 아키텍처에 관해서 관심이 생겼다.
클린 코드를 추구하다 보니 자연스럽게 설계적 고민으로 귀결되었다.
원티드 백엔드 챌린지를 하며 알게된 클린 아키텍처,
도메인 주도 설계 철저 입문,
도메인 주도 설계로 시작하는 마이크로 서비스를 읽어가며,
내가 구성해오던 소프트웨어 설계의 큰 전환점을 맞이하게 되었다.
단순히 예제 프로젝트만을 만드는게 아닌 본 프로젝트에 이를 적용시켜보기로 결정했다.
MSA 는 오버 엔지니어링이라 판단했고, 모노리스 구조이지만 최대한 도메인 별 분리가 된 상위 바운디드 컨텍스트로 나누어 리팩토링했다.
기존 코드 구성
바운디드 컨텍스트를 나누기
기존 구조에는 domain 이란 최상위 구조 아래 도메인 컨텍스들이 존재함.
이를 바깥으로 빼서 하나의 도메인 바운디드 컨텍스트로 만들었다.
- global 패키지는 각 컨텍스트간 전역적 공유
- 나머지 패키지는 도메인 중심 바운디드 컨텍스트로 설계
차상위 구조 - User
- user 패키지
- 어댑터 in out 구성
- 애플리케이션 - usecase와 구현체 port service 구성
- 도메인 엔티티는 jpa 엔티티로 구성
- dto 모델은 전역적이므로 각 계층 간 공유
세부 구조 - User
- 대략적 세부 구조 예시
도메인 엔티티 모델 - User
@Table(name = "users")
@Entity
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class User extends BaseTimeEntity<User> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String loginId;
private String password;
private String salt;
private String name;
@Column(unique = true)
private String email;
private String address;
private String zipcode;
@Enumerated(EnumType.STRING)
private Role role;
// <비즈니스 로직 메서드> //
public void updateUserInfo(final User modifiableEntity, final String updatePassword) {
this.name = modifiableEntity.getName();
this.email = modifiableEntity.getEmail();
this.address = modifiableEntity.getAddress();
this.zipcode = modifiableEntity.getZipcode();
this.password = updatePassword;
}
public void remove() {
this.role = Role.ROLE_WITHDRAWAL;
this.delete();
}
}
- Jpa entitiy로 도메인 엔티티 모델을 사용
- 기존 연관 관계 삭제 (order와 연관 관계 였다.)
이렇게 User domain bounded context를 간단히 살펴보았다.
고민해 볼 부분
- 도메인이 분리 되어 있어도 트랜잭션을 연결 지어야 한다.
첫번 째 방안
- Kafka 와 같은 메시지 브로커를 사용하는 것은 오버 엔지니어링이라 판단.
- Spring이 제공하는 동기 호출 클라이언트 Feign을 사용
- MSA 로 전환할 경우 좋은 선택지가 될 것이라 생각했었다.
두번 째 방안
- Application Event Publisher를 사용
- 모노리스 구성에서 메시지 브로커를 간단히 구성하는 방법이라 판단했다.
나는 후자를 택했다.
Feign 을 사용하는 것 조차 오버 엔지니어링이라 생각했다.
무엇보다 MSA로 전환할 생각이 없었기에, 굳이 API 동기 호출을 할 필요가 없다.
Application Event
다른 도메인 컨텍스트이지만 같은 트랜잭션 단위로 묶여야 하는 프로세스
이 것을 동기 이벤트로 해결함.
eventPublisher.publishEvent(
eventBuilder.createEvent(new OrderVariation(
savedPayment.getOrderId(),
paymentId,
false))
);
eventPublisher.publishEvent(
eventBuilder.createEvent(ItemsVariation.builder()
.numbers(itemNumbers.stream().map(Long::valueOf).toList())
.quantities(itemQuantities.stream().map(Integer::valueOf).toList())
.totalAmount(response.getAmount().getTotal())
.status(false)
.build())
);
- 결제 취소를 했을 경우이다.
- Order 도메인, Product 도메인은 별도의 컨텍스트이다.
- 주문 상태 변경과 상품 수량 변경에 대한 이벤트를 발행했다.
- 당연히 해당 메서드는 @Transactional을 붙여 하나의 트랜잭션으로 감싸진다.
- event 는 밖으로 나가는 구성이므로 out 패키지에 위치
- order 도메인에서 이벤트 구독자는 밖에서 안으로 들어오는 구성으로 in 에 위치
@EventListener
public void doOrderEvent(EventModel eventModel) {
String eventAction = eventModel.getEventAction()
.orElseThrow(EventNotExistsException::new);
if (eventAction.equals(ORDER_EVENT)) {
String payload = eventModel.getPayload();
.....
=> 이런 식으로 동기방식으로 이벤트 객체를 읽어 실행
=> 동기 실행
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "payment_id")
private Long paymentId;
- Order 엔티티 모델
- 기존 연관관계는 직접 jpa entity 매핑
- 지금은 pk만 컬럼으로 가지는 구성
- 이 pk를 가지고 다른 도메인 컨텍스트의 api 요청 가능
이런식으로 리팩토링을 해보았다.
나중에 MSA를 설계할 구성의 프로젝트를 진행할 경우
메시지 브로커나 트랜잭션 단위를 지키기 위한 다양한 패턴들을 적용할 초석을 마련한 것 같다.
어느정도 감을 잡았다는 말이다!
이렇게 잡아나가면 그 만큼 내 실력이 된다고 믿습니다.
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
OutBox Pattern 을 활용한 메일 전송 서비스 개발 [At Least Once] (0) | 2023.04.20 |
---|---|
[이슈] Pageable test 관련 에러 (0) | 2022.11.23 |
[Refactor] 패키지 구조와 의존성 (2) | 2022.10.14 |
[Redisson] 트랜잭션 문제 발생 및 해결 (0) | 2022.10.01 |
[Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결 (2) | 2022.09.27 |