-
Notifications
You must be signed in to change notification settings - Fork 0
feat: E-Commerce 시스템 확장 및 PG 전략 패턴 적용 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e01d163
bcac04a
7e2fa08
f47003f
00ceb0b
c679811
86079a4
0f3178b
2c8187f
023bdfe
cc5acb6
4f018ae
f407fee
b290d07
f177dfc
0bc9f0b
c45b76e
46a5cb7
14ced57
ebb2467
7ccd2b0
c59435b
1b46f81
d4f410d
e83d771
83855d7
216da31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.example.common.config; | ||
|
|
||
| import org.apache.kafka.clients.consumer.ConsumerConfig; | ||
| import org.apache.kafka.common.serialization.StringDeserializer; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.kafka.annotation.EnableKafka; | ||
| import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; | ||
| import org.springframework.kafka.core.ConsumerFactory; | ||
| import org.springframework.kafka.core.DefaultKafkaConsumerFactory; | ||
| import org.springframework.kafka.support.serializer.JsonDeserializer; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * 공통 Kafka Consumer 설정 | ||
| * 모든 마이크로서비스에서 재사용 가능 | ||
| */ | ||
| @Configuration | ||
| @EnableKafka | ||
| public class KafkaConsumerConfig { | ||
|
|
||
| @Value("${spring.kafka.bootstrap-servers:localhost:9092}") | ||
| private String bootstrapServers; | ||
|
|
||
| @Value("${spring.kafka.consumer.group-id:default-group}") | ||
| private String groupId; | ||
|
|
||
| @Bean | ||
| public ConsumerFactory<String, Object> consumerFactory() { | ||
| Map<String, Object> props = new HashMap<>(); | ||
| props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); | ||
| props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); | ||
| props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); | ||
| props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class); | ||
| props.put(JsonDeserializer.TRUSTED_PACKAGES, "*"); | ||
| props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); | ||
| props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true); | ||
| props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000); | ||
|
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Saga 보상 트랜잭션에서 자동 커밋은 위험 현재 자동 커밋 활성화 시 메시지 처리 중 예외 발생 시에도 오프셋이 커밋되어 보상 트랜잭션이 누락될 수 있습니다. 문제 시나리오:
수동 커밋 또는 컨테이너 ACK 모드로 변경하세요: -props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true);
-props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 1000);
+props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);그리고 리스너 컨테이너 팩토리에서 ACK 모드를 명시: @Bean
public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Object> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD); // 레코드 단위 ACK
return factory;
}🤖 Prompt for AI Agents |
||
|
|
||
| return new DefaultKafkaConsumerFactory<>(props); | ||
| } | ||
|
|
||
| @Bean | ||
| public ConcurrentKafkaListenerContainerFactory<String, Object> kafkaListenerContainerFactory() { | ||
| ConcurrentKafkaListenerContainerFactory<String, Object> factory = | ||
| new ConcurrentKafkaListenerContainerFactory<>(); | ||
| factory.setConsumerFactory(consumerFactory()); | ||
|
|
||
| // 에러 핸들러 설정 - 재시도 가능한 예외와 불가능한 예외 구분 | ||
| factory.setCommonErrorHandler(new org.springframework.kafka.listener.DefaultErrorHandler( | ||
| new org.springframework.util.backoff.FixedBackOff(1000L, 3L) // 1초 간격으로 3회 재시도 | ||
| )); | ||
|
|
||
| return factory; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package com.example.common.config; | ||
|
|
||
| import org.apache.kafka.clients.producer.ProducerConfig; | ||
| import org.apache.kafka.common.serialization.StringSerializer; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.kafka.core.DefaultKafkaProducerFactory; | ||
| import org.springframework.kafka.core.KafkaTemplate; | ||
| import org.springframework.kafka.core.ProducerFactory; | ||
| import org.springframework.kafka.support.serializer.JsonSerializer; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * 공통 Kafka Producer 설정 | ||
| * 모든 마이크로서비스에서 재사용 가능 | ||
| */ | ||
| @Configuration | ||
| public class KafkaProducerConfig { | ||
|
|
||
| @Value("${spring.kafka.bootstrap-servers:localhost:9092}") | ||
| private String bootstrapServers; | ||
|
|
||
| @Bean | ||
| public ProducerFactory<String, Object> producerFactory() { | ||
| Map<String, Object> config = new HashMap<>(); | ||
| config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); | ||
| config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); | ||
| config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); | ||
|
|
||
| // Producer 성능 및 신뢰성 설정 | ||
| config.put(ProducerConfig.ACKS_CONFIG, "1"); // 리더 파티션 ACK (성능과 신뢰성 균형) | ||
| config.put(ProducerConfig.RETRIES_CONFIG, 3); // 전송 실패 시 재시도 | ||
| config.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy"); // 압축 | ||
| config.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 배치 대기 시간 | ||
| config.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 배치 크기 | ||
|
|
||
| return new DefaultKafkaProducerFactory<>(config); | ||
| } | ||
|
|
||
| @Bean | ||
| public KafkaTemplate<String, Object> kafkaTemplate() { | ||
| return new KafkaTemplate<>(producerFactory()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.example.common.event; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 배송 완료 이벤트 | ||
| * - Delivery Service에서 배송 완료 시 발행 | ||
| * - Order Service가 구독하여 주문 상태를 DELIVERED로 업데이트 | ||
| */ | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class DeliveryCompletedEvent implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| /** | ||
| * 주문 ID | ||
| */ | ||
| private Long orderId; | ||
|
|
||
| /** | ||
| * 배송 ID | ||
| */ | ||
| private String deliveryId; | ||
|
|
||
| /** | ||
| * 이벤트 발생 시각 | ||
| */ | ||
| private LocalDateTime completedAt; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package com.example.common.event; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 배송 실패 이벤트 | ||
| * - Delivery Service에서 배송 실패 시 발행 (수령 거부, 주소 오류 등) | ||
| * - Order Service가 구독하여 고객센터 처리 | ||
| */ | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class DeliveryFailedEvent implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| /** | ||
| * 주문 ID | ||
| */ | ||
| private Long orderId; | ||
|
|
||
| /** | ||
| * 배송 ID | ||
| */ | ||
| private String deliveryId; | ||
|
|
||
| /** | ||
| * 실패 사유 | ||
| */ | ||
| private String reason; | ||
|
|
||
| /** | ||
| * 이벤트 발생 시각 | ||
| */ | ||
| private LocalDateTime failedAt; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package com.example.common.event; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 배송 시작 이벤트 | ||
| * - Delivery Service에서 배송 시작 시 발행 | ||
| * - Order Service가 구독하여 주문 상태 업데이트 | ||
| */ | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class DeliveryStartedEvent implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| /** | ||
| * 주문 ID | ||
| */ | ||
| private Long orderId; | ||
|
|
||
| /** | ||
| * 배송 ID (추적 번호) | ||
| */ | ||
| private String deliveryId; | ||
|
|
||
| /** | ||
| * 배송지 주소 | ||
| */ | ||
| private String address; | ||
|
|
||
|
Comment on lines
+34
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주소 PII 최소화 필요 공용 이벤트에 전체 |
||
| /** | ||
| * 택배사 | ||
| */ | ||
| private String carrier; | ||
|
|
||
| /** | ||
| * 이벤트 발생 시각 | ||
| */ | ||
| private LocalDateTime startedAt; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package com.example.common.event; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 재고 확보 실패 이벤트 | ||
| * - Inventory Service에서 재고 부족 시 발행 | ||
| * - Order Service가 구독하여 주문 취소 처리 | ||
| */ | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class InventoryReservationFailedEvent implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| /** | ||
| * 주문 ID | ||
| */ | ||
| private Long orderId; | ||
|
|
||
| /** | ||
| * 상품 ID | ||
| */ | ||
| private Long productId; | ||
|
|
||
| /** | ||
| * 요청 수량 | ||
| */ | ||
| private Integer requestedQuantity; | ||
|
|
||
| /** | ||
| * 현재 재고 수량 | ||
| */ | ||
| private Integer availableQuantity; | ||
|
|
||
|
Comment on lines
+35
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 수량 유효성 보장
🤖 Prompt for AI Agents |
||
| /** | ||
| * 실패 사유 | ||
| */ | ||
| private String reason; | ||
|
|
||
| /** | ||
| * 이벤트 발생 시각 | ||
| */ | ||
| private LocalDateTime failedAt; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package com.example.common.event; | ||
|
|
||
| import lombok.AllArgsConstructor; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.math.BigDecimal; | ||
| import java.time.LocalDateTime; | ||
|
|
||
| /** | ||
| * 재고 확보 성공 이벤트 | ||
| * - Inventory Service에서 재고 차감 성공 시 발행 | ||
| * - Payment Service가 구독하여 결제 처리 시작 | ||
| */ | ||
| @Getter | ||
| @Builder | ||
| @NoArgsConstructor | ||
| @AllArgsConstructor | ||
| public class InventoryReservedEvent implements Serializable { | ||
|
|
||
| private static final long serialVersionUID = 1L; | ||
|
|
||
| /** | ||
| * 주문 ID | ||
| */ | ||
| private Long orderId; | ||
|
|
||
| /** | ||
| * 상품 ID | ||
| */ | ||
| private Long productId; | ||
|
|
||
| /** | ||
| * 상품명 (스냅샷) | ||
| */ | ||
| private String productName; | ||
|
|
||
| /** | ||
| * 확보된 수량 | ||
| */ | ||
| private Integer quantity; | ||
|
|
||
| /** | ||
| * 결제할 금액 | ||
| */ | ||
| private BigDecimal totalPrice; | ||
|
|
||
| /** | ||
| * 이벤트 발생 시각 | ||
| */ | ||
| private LocalDateTime reservedAt; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
보안 위험: JsonDeserializer TRUSTED_PACKAGES="*" 사용 금지
와일드카드 신뢰 패키지는 폴리모픽 역직렬화 공격 표면을 넓힙니다. 공용 이벤트 패키지로 제한하세요(예: com.example.common.event) 및 타입 매핑을 명시하세요.
코딩 가이드라인 기준.
🤖 Prompt for AI Agents