์ด์ ์ ์งํํ์๋ ๋ฐ๋ธํก์ด๋ผ๋ ์๋ด ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ํจ๊ป 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๋จ๊ณ๋ก ๊ตฌ์ฑ๋์ด ์๋ค.
- ๋ด๋ด์๊ฐ ์๋ด์ ์์ฒญํ ๋ ์์ฑํ ์๋ด Entity๋ฅผ ์กฐํํด (CQRSํจํด์ผ๋ก ์กฐํ์ฉ QueryableRepo ๋ถ๋ฆฌ)
- ์๋ด Entity์ ์ํ๋ฅผ "์น์ธ๋จ"์ผ๋ก ๋ณ๊ฒฝ
- ์๋ด ์ํ ์ด๋ฒคํธ ๋ฐํ Producer๋ฅผ ํตํด ์๋ด ์ํ Micro Service์ ์ํ "๊ตฌ๋งค๋ถ๊ฐ" ์ํ๋ก ๋ณ๊ฒฝ Event ๋ฐํ (๋น๋๊ธฐ)
- ๊ฒฐ์ ์ด๋ฒคํธ ๋ฐํ 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 ์์ ๋ฅผ ์ด์ฉํ ์ค๋ช ์ด์๋ค. ์ญ์๐ค, ๊ฐ๋ฐ์๋ ์ฝ๋๋ก ์ค๋ช ํ๋ ๊ฒ์ด ๊ฐ์ฅ ํธํ๊ณ ์ง๊ด์ ์ด๋ผ๊ณ ์๊ฐํ๋ค.