๐Ÿ‘ท๐Ÿป Architecture

7ํŽธ. ์ƒ๋‹ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ํ•˜๋Š” MSA (with Spring Boot)

DevPoong 2024. 4. 21. 23:42

์ด์ „์— ์ง„ํ–‰ํ–ˆ์—ˆ๋˜ ๋ฐ๋ธŒํ†ก์ด๋ผ๋Š” ์ƒ๋‹ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ Micro Service ๋‚ด๋ถ€ ๊ตฌ์„ฑ์— ๋Œ€ํ•ด ๊ฐ„๋žตํ•˜๊ฒŒ ์• ๊ธฐํ•ด๋ณด๊ฒ ๋‹ค.

 

1. ์ƒ๋‹ด Aggregate

์˜ˆ์‹œ๋กœ ๊ฐ€์ ธ์˜จ ์ƒ๋‹ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค.

์•กํ„ฐ๋Š” ์ƒ๋‹ด์‚ฌ, ๋‚ด๋‹ด์ž๋กœ ๊ตฌ์„ฑ๋˜๋ฉฐ ํ˜„์žฌ ๊ฐ€์ ธ์˜จ ์˜ˆ์‹œ๋Š” ์ƒ๋‹ด ๋งค์นญ Micro Service๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•œ๋‹ค.

Consultation์ด๋ผ๋Š” ๋„๋ฉ”์ธ Entity๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค. ์ƒ๋‹ด์ด๋ผ๋Š” Aggregate๋Š” ์ƒ๋‹ด ๋“ฑ๋ก, ์ˆ˜์ •, ์ทจ์†Œ, ๋ฆฌ๋ทฐ ์ž‘์„ฑ์— ๋Œ€ํ•œ ์ฑ…์ž„์„ ๊ฐ–๊ณ  ์žˆ์œผ๋ฉฐ, ์—ฌ๋Ÿฌ Entity์™€ ๊ด€๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜๋ฅผ ์—„๊ฒฉํ•˜๊ฒŒ ๋”ฐ๋ฅด์ž๋ฉด JPA Entity์™€ POJO Entity๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š”๊ฒŒ ๋งž์ง€๋งŒ, JPA๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ทธ๋ ‡๊ฒŒ ๋„๋ฉ”์ธ์— ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ๋ฐฉํ•ดํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ด ์˜ˆ์ œ์—์„œ๋Š” JPA Entity ํ•˜๋‚˜๋งŒ์„ ์ •์˜ํ–ˆ๋‹ค.
@Entity
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Consultation extends BaseTime {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "consultation_id")
    private Long id;

    @Column(nullable = false)
    private Long productId;

    @Column(nullable = false)
    private Long consulterId;

    @Column(nullable = false)
    private String consulterName;

    @Column(nullable = false)
    private Long consultantId;

    @Column(nullable = false)
    private String consultantName;

    @Column(unique = true)
    private Long paymentId;

    @Embedded
    private ConsultationDetails consultationDetails;

    @Column(nullable = false, length = 20)
    private ProcessStatus status;

    @Column(nullable = false)
    private Money cost;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "review_id")
    private Review review;

    @OneToOne(mappedBy = "consultation", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private ConsultationCancellation consultationCancellation;

    @Column(nullable = false)
    private boolean canceled = false;
    
    ...
    
    public static Consultation createConsultation(Long consulterId, String consulterName,
                                                  Long consultantId, String consultantName,
                                                  Long productId, ConsultationDetails consultationDetails,
                                                  Integer cost) {
        Consultation newConsultation = Consultation.builder()
                .consulterId(consulterId)
                .consulterName(consulterName)
                .consultantId(consultantId)
                .consultantName(consultantName)
                .productId(productId)
                .consultationDetails(consultationDetails)
                .cost(cost)
                .build();
        newConsultation.setStatus(ACCEPT_WAIT);
        return newConsultation;
    }
    
    public void writeReview(Integer score, String content,
                            String photoUrl, String photoOriginName, String photoStoredName, LocalDate reviewAt) {
        if (!this.status.equals(PAID)
                && this.consultationDetails.getReservationDate().isAfter(reviewAt)
                && this.consultationDetails.getReservationDate().plusDays(7).isBefore(reviewAt)
                && this.review != null) {
            throw new BusinessRuleException(REVIEW_IMPOSSIBLE_STATUS);
        }
        this.review = Review.createReview(this.consultantId, this.consulterName, this.consultantId, this.consultantName, score, content);
    }

    private ConsultationCancellation cancel(ProcessStatus status, String canceledReason) {
        this.status = status;
        this.canceled = true;

        ConsultationCancellation consultationCancellation = createConsultationCancellation(this.productId, canceledReason);

        changeConsultationCancellation(consultationCancellation);

        return consultationCancellation;
    }
    ......
}

 

 

2. ์ƒ๋‹ด ์Šน์ธ Application Service

์ƒ๋‹ด ์Šน์ธ ๊ณผ์ •์— ๋Œ€ํ•ด์„œ ๊ฐ„๋žตํ•˜๊ฒŒ ๋งํ•˜์ž๋ฉด, ๋‚ด๋‹ด์ž๊ฐ€ ์ƒ๋‹ด์‚ฌ์˜ ์ƒ๋‹ด ์ƒํ’ˆ์„ ๊ฒฐ์ œํ•˜๊ณ  ์ƒ๋‹ด์‚ฌ์—๊ฒŒ ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด, ์ƒ๋‹ด์‚ฌ๊ฐ€ ์Šน์ธ ํ˜น์€ ๊ฑฐ์ ˆ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋•Œ, ์ƒ๋‹ด ์š”์ฒญ์„ ์Šน์ธ ํ•˜๋Š” Use case์— ๋Œ€ํ•ด์„œ ์‚ดํŽด๋ณด๊ฒ ๋‹ค.

@Service
@RequiredArgsConstructor
public class AcceptConsultationService implements AcceptConsultationUseCase {

    private final ConsultationQueryableRepo consultationQueryableRepo;
    private final ProductProducer productProducer;
    private final PaymentProducer paymentProducer;
    
    @Transactional
    @Override
    public void acceptConsultation(Long consultantId, Long consultationId) {
        Consultation findConsultation = consultationQueryableRepo.findByIdWithConsultantId(consultationId, consultantId)
                .orElseThrow(() -> new NotFoundException(NOT_FOUND_CONSULTATION));

        findConsultation.accept();
        productProducer.sendConsultationInfoProduct("consultation-topic", findConsultation);
        paymentProducer.sendConsultationInfoPayment("approved-consultation", findConsultation);
    }
}

 

๋กœ์ง์„ ์‚ดํŽด๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ํฌ๊ฒŒ 4๋‹จ๊ณ„๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋‹ค.

  1. ๋‚ด๋‹ด์ž๊ฐ€ ์ƒ๋‹ด์„ ์š”์ฒญํ•  ๋•Œ ์ƒ์„ฑํ•œ ์ƒ๋‹ด Entity๋ฅผ ์กฐํšŒํ•ด (CQRSํŒจํ„ด์œผ๋กœ ์กฐํšŒ์šฉ QueryableRepo ๋ถ„๋ฆฌ)
  2. ์ƒ๋‹ด Entity์˜ ์ƒํƒœ๋ฅผ "์Šน์ธ๋จ"์œผ๋กœ ๋ณ€๊ฒฝ
  3. ์ƒ๋‹ด ์ƒํ’ˆ ์ด๋ฒคํŠธ ๋ฐœํ–‰ Producer๋ฅผ ํ†ตํ•ด ์ƒ๋‹ด ์ƒํ’ˆ Micro Service์— ์ƒํ’ˆ "๊ตฌ๋งค๋ถˆ๊ฐ€" ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ Event ๋ฐœํ–‰ (๋น„๋™๊ธฐ)
  4. ๊ฒฐ์ œ ์ด๋ฒคํŠธ ๋ฐœํ–‰ Producer๋ฅผ ํ†ตํ•ด ๊ฒฐ์ œ Micro Service์— ์ƒ๋‹ด "๊ฒฐ์ œ ๋Œ€๊ธฐ" ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ Event ๋ฐœํ–‰ (๋น„๋™๊ธฐ)

 

 

Event ๋ฐœ์ƒ Producer์˜ ๊ฒฝ์šฐ DIP๋ฅผ ์ ์šฉํ•˜์—ฌ, ์™ธ๋ถ€ adapter ํŒจํ‚ค์ง€์— ๊ตฌํ˜„์ฒด๊ฐ€ ์กด์žฌํ•˜๊ณ , ๋‚ด๋ถ€ application ํŒจํ‚ค์ง€์— interface๊ฐ€ ์กด์žฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋‚ด๋ถ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค์—์„œ๋Š” Producer Interface๋ฅผ ์ฐธ์กฐํ•˜๊ฒŒ ๋œ๋‹ค.

 

Event Publish, Subscribe ๋งค๋‹ˆ์ปค๋‹ˆ์ฆ˜์„ ์œ„ํ•ด์„œ๋Š” Kafka๋ฅผ ์ด์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ๋Œ€ํ‘œ์ ์ด๊ณ , ํ•ด๋‹น ๊ธฐ์ˆ ์„ Producer๋‚˜ Consumer ๊ตฌํ˜„์ฒด์—์„œ ์ ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค. 

 

 

3. ์ƒํ’ˆ ์ƒํƒœ ๋ณ€๊ฒฝ Event Producer

์ด๋ฒˆ์—๋Š”, ์ƒํ’ˆ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ProductProducer Interface์˜ ๊ตฌํ˜„์ฒด์ธ ProductKafkaProducer์— ๋Œ€ํ•ด์„œ ์–˜๊ธฐํ•ด๋ณด๊ฒ ๋‹ค.

์ด๋ฏธ ์ด๋ฆ„์—์„œ ์œ ์ถ”๊ฐ€๋Šฅํ•˜๋“ฏ์ด, Kafka๋ผ๋Š” Event Driven Architecture ๊ธฐ์ˆ ์„ ์ ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ–ˆ๋‹ค.

@Service
@Slf4j
@RequiredArgsConstructor
public class ProductKafkaProducer implements ProductProducer {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public void sendConsultationInfoProduct(String topic, Consultation consultation) {
        String jsonInString = "";
        try{
            jsonInString = serializeMapper().writeValueAsString(consultation);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }

        kafkaTemplate.send(topic, jsonInString);
        log.info("์นดํ”„์นด ๋ฉ”์‹œ์ง€ ์ „์†ก ์„ฑ๊ณต : " + consultation);
    }

    public ObjectMapper serializeMapper() {
		....
    }
}

์ƒ๋‹ด Micro Service์—์„œ "์ƒํ’ˆ ์Šน์ธ ์ƒํƒœ ๋ณ€๊ฒฝ ํ† ํ”ฝ"์— ๋Œ€ํ•ด ProductKafkaProducer๋ฅผ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•˜๊ณ , ํ•ด๋‹น ํ† ํ”ฝ์„ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ์ƒํ’ˆ Micro Service์—์„œ ProdcutKafkaConsumer๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ์ฝ์–ด ์ƒํ’ˆ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ๋กœ์ง๊นŒ์ง€ ์‹คํ–‰๋  ๊ฒƒ์ด๋‹ค.

 

 

4. ์ƒํ’ˆ ์ƒํƒœ ๋ณ€๊ฒฝ Event Consumer

์ด๋ฒˆ์—๋Š” ์ƒํ’ˆ Micro Service์˜ ์ƒํ’ˆ ์ƒํƒœ ๋ณ€๊ฒฝ Event Consumer์— ๋Œ€ํ•ด์„œ ์„ค๋ช…ํ•ด๋ณด๊ฒ ๋‹ค.

 

์ƒ๋‹ด Micro Service๋กœ๋ถ€ํ„ฐ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœํ–‰๋œ ๊ฒƒ์ด ๊ฐ์ง€๊ฐ€ ๋˜๋ฉด, ํ•ด๋‹น ์ด๋ฒคํŠธ๋ฅผ ์ฝ์–ด ์ ์ ˆํ•œ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ๋œ๋‹ค. ์ด๋ฒˆ ์ผ€์ด์Šค๋Š” ์ƒ๋‹ด ์ƒํ’ˆ ์Šน์ธ ์ƒํƒœ ๋ณ€๊ฒฝ ๋กœ์ง์ด ์‹คํ–‰๋  ๊ฒƒ์ด๋‹ค. 

@Service
@Slf4j
@RequiredArgsConstructor
public class MatchingKafkaConsumer {
    private final CancleUseCase cancleUseCase;
    private final ReserveUseCase reserveUseCase;

    @KafkaListener(topics = "consultation-topic")
    public void receiveConsultationInfo(String kafkaMessage) {
        log.info("Kafka Message: " + kafkaMessage);

        ProductReservedReq productReservedReq = null;
        try{
            productReservedReq = deserializeMapper().readValue(kafkaMessage, ProductReservedReq.class);
        } catch (JsonProcessingException ex){
            ex.printStackTrace();
        }
      
        dataSynchronization(productReservedReq);
    }

    private void dataSynchronization(ProductReservedReq productReservedReq) {
        if (productReservedReq.getStatus() == ProcessStatus.ACCEPTED) {
            reserveUseCase.reserveProduct(productReservedReq);
        }
        if (productReservedReq.getStatus() == ProcessStatus.CONSULTANT_REFUSED
                ||productReservedReq.getStatus() == ProcessStatus.CONSULTANT_CANCELED
                || productReservedReq.getStatus() == ProcessStatus.CONSULTER_CANCELED){
            cancleUseCase.cancleConsultation(productReservedReq);
        }
    }

  	.....
}

 

์—ฌ๊ธฐ๊นŒ์ง€, ์ƒ๋‹ด Micro Service ์˜ˆ์ œ๋ฅผ ์ด์šฉํ•œ ์„ค๋ช…์ด์—ˆ๋‹ค. ์—ญ์‹œ๐Ÿค”, ๊ฐœ๋ฐœ์ž๋Š” ์ฝ”๋“œ๋กœ ์„ค๋ช…ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ํŽธํ•˜๊ณ  ์ง๊ด€์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•œ๋‹ค.