240814 TIL

송형근·2024년 8월 14일
0

TIL

목록 보기
13/43
post-thumbnail

스파르타 Java 단기 심화 과정


코드카타


프로그래머스 131128 숫자 짝꿍

https://school.programmers.co.kr/learn/courses/30/lessons/131128

— 문제 설명

두 정수 XY의 임의의 자리에서 공통으로 나타나는 정수 k(0 ≤ k ≤ 9)들을 이용하여 만들 수 있는 가장 큰 정수를 두 수의 짝꿍이라 합니다(단, 공통으로 나타나는 정수 중 서로 짝지을 수 있는 숫자만 사용합니다). XY의 짝꿍이 존재하지 않으면, 짝꿍은 -1입니다. XY의 짝꿍이 0으로만 구성되어 있다면, 짝꿍은 0입니다.

예를 들어, X = 3403이고 Y = 13203이라면, X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 3, 0, 3으로 만들 수 있는 가장 큰 정수인 330입니다. 다른 예시로 X = 5525이고 Y = 1255이면 X와 Y의 짝꿍은 X와 Y에서 공통으로 나타나는 2, 5, 5로 만들 수 있는 가장 큰 정수인 552입니다(X에는 5가 3개, Y에는 5가 2개 나타나므로 남는 5 한 개는 짝 지을 수 없습니다.)

두 정수 XY가 주어졌을 때, XY의 짝꿍을 return하는 solution 함수를 완성해주세요.

— 제한 조건

  • 3 ≤ XY의 길이(자릿수) ≤ 3,000,000입니다.
  • XY는 0으로 시작하지 않습니다.
  • XY의 짝꿍은 상당히 큰 정수일 수 있으므로, 문자열로 반환합니다.

— 입출력 예

XYresult
"100""2345""-1"
"100""203045""0"
"100""123450""10"
"12321""42531""321"
"5525""1255""552"

입출력 예 #1

  • XY의 짝꿍은 존재하지 않습니다. 따라서 "-1"을 return합니다.

입출력 예 #2

  • XY의 공통된 숫자는 0으로만 구성되어 있기 때문에, 두 수의 짝꿍은 정수 0입니다. 따라서 "0"을 return합니다.

입출력 예 #3

  • XY의 짝꿍은 10이므로, "10"을 return합니다.

입출력 예 #4

  • XY의 짝꿍은 321입니다. 따라서 "321"을 return합니다.

입출력 예 #5

  • 지문에 설명된 예시와 같습니다.

— 문제 풀이

class Solution {
    public String solution(String X, String Y) {
        int[] numX = new int[10];
        int[] numY = new int[10];
        for(int i=0;i<X.length();i++){
            numX[Integer.parseInt(X.charAt(i)+"")]++;
        }
        for(int i=0;i<Y.length();i++){
            numY[Integer.parseInt(Y.charAt(i)+"")]++;
        }
        StringBuilder sb = new StringBuilder();
        boolean flag = false; // 0이 아닌 공통된 수가 있는지 체크
        for(int i=9;i>=0;i--){
            if(numX[i]==0||numY[i]==0)continue;
            if(i>0&&!flag) flag = true;
            while(numX[i]>0&&numY[i]>0){
                sb.append(i);
                numX[i]--;
                numY[i]--;
            }
        }
        if(sb.toString().equals("")) return "-1";
        else if(!flag) return "0";
        return sb.toString();
    }
}

프로그래머스 42862 체육복

https://school.programmers.co.kr/learn/courses/30/lessons/42862

— 문제 설명

점심시간에 도둑이 들어, 일부 학생이 체육복을 도난당했습니다. 다행히 여벌 체육복이 있는 학생이 이들에게 체육복을 빌려주려 합니다. 학생들의 번호는 체격 순으로 매겨져 있어, 바로 앞번호의 학생이나 바로 뒷번호의 학생에게만 체육복을 빌려줄 수 있습니다. 예를 들어, 4번 학생은 3번 학생이나 5번 학생에게만 체육복을 빌려줄 수 있습니다. 체육복이 없으면 수업을 들을 수 없기 때문에 체육복을 적절히 빌려 최대한 많은 학생이 체육수업을 들어야 합니다.

전체 학생의 수 n, 체육복을 도난당한 학생들의 번호가 담긴 배열 lost, 여벌의 체육복을 가져온 학생들의 번호가 담긴 배열 reserve가 매개변수로 주어질 때, 체육수업을 들을 수 있는 학생의 최댓값을 return 하도록 solution 함수를 작성해주세요.

— 제한 조건

  • 전체 학생의 수는 2명 이상 30명 이하입니다.
  • 체육복을 도난당한 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
  • 여벌의 체육복을 가져온 학생의 수는 1명 이상 n명 이하이고 중복되는 번호는 없습니다.
  • 여벌 체육복이 있는 학생만 다른 학생에게 체육복을 빌려줄 수 있습니다.
  • 여벌 체육복을 가져온 학생이 체육복을 도난당했을 수 있습니다. 이때 이 학생은 체육복을 하나만 도난당했다고 가정하며, 남은 체육복이 하나이기에 다른 학생에게는 체육복을 빌려줄 수 없습니다.

— 입출력 예

nlostreservereturn
5[2, 4][1, 3, 5]5
5[2, 4][3]4
3[3][1]2

예제 #1
1번 학생이 2번 학생에게 체육복을 빌려주고, 3번 학생이나 5번 학생이 4번 학생에게 체육복을 빌려주면 학생 5명이 체육수업을 들을 수 있습니다.

예제 #2
3번 학생이 2번 학생이나 4번 학생에게 체육복을 빌려주면 학생 4명이 체육수업을 들을 수 있습니다.

— 문제 풀이

class Solution {
    public int solution(int n, int[] lost, int[] reserve) {
        int[] students = new int[n];
        for(int i=0;i<n;i++){
            students[i] = 1;
        }
        for(int i=0;i<lost.length;i++){
            students[lost[i]-1]--;
        }
        for(int i=0;i<reserve.length;i++){
            students[reserve[i]-1]++;
        }
        
        for(int i=0;i<n;i++){
            if(students[i]==0){
                if(i>0&&students[i-1]==2){
                    students[i-1]--;
                    students[i]++;
                }
                else if(i<n-1&&students[i+1]==2){
                    students[i+1]--;
                    students[i]++;
                }
            }
        }
        
        int answer = 0;
        for(int i=0;i<n;i++){
            if(students[i]>0) answer++;
        }
        
        return answer;
    }
}

Kafka

Kafka란

  • 분산 스트리밍 플랫폼. 주로 실시간 데이터 피드의 빅 데이터 처리를 목적으로 사용됨
  • 메시지 큐와 유사하지만, 대용량 데이터 스트림을 저장하고 실시간으로 분석하거나 처리하는 데 중점을 둠

Kafka의 주요 기능

  • 실시간 데이터 처리 : 대용량 데이터를 실시간으로 처리하고 분석함
  • 데이터 통합 : 다양한 소스에서 데이터를 수집하고 이를 통합하여 분석
  • 내결함성 : 데이터 손실 없이 안정적으로 데이터를 저장하고 전송

Kafka의 장단점

  • 장점
    • 신뢰성
      • 데이터 복제 : 데이터를 여러 브로커에 복제하여 저장. 이를 통해 단일 브로커 장애 시에도 데이터 손실을 방지할 수 있음
      • 확인 메커니즘 : 데이터가 소비자에게 성공적으로 전달되었는지 확인하는 기능을 제공
    • 유연성
      • 다양한 소비자 패턴 : 여러 소비자가 동시에 데이터를 구독할 수 있음
      • 프로토콜 지원 : 기본적으로 Kafka의 프로토콜을 사용하지만, 다양한 클라이언트를 통해 다른 언어에서도 사용할 수 있음
    • 확장성
      • 분산 시스템 : 클러스터링을 통해 여러 노드에서 데이터를 분산 처리할 수 있음
      • 수평 확장 : 브로커와 파티션을 추가하여 쉽게 확장할 수 있음
    • 성능
      • 높은 처리량 : 대용량 데이터를 실시간으로 빠르게 처리할 수 있음
      • 저지연 : 데이터 전송의 지연을 최소화하여 실시간 처리 가능
    • 관리 및 모니터링
      • 관리 도구 : 다양한 관리 도구를 통해 클러스터를 모니터링하고 관리할 수 있음
      • 플러그인 시스템 : 다양한 플러그인을 통해 기능 확장 가능
  • 단점
    • 설정 및 운영 복잡성
      • 복잡한 설정 : 초기 설정이 다소 복잡할 수 있으며, 클러스터링 및 분산 환경에서는 더 많은 설정이 필요
      • 운영 관리 : 대규모 환경에서 운영하고 관리하는 데 많은 노력이 필요
    • 성능 문제
      • 브로커 오버헤드 : 높은 트래픽 상황에서는 브로커의 오버헤드가 발생할 수 있음
      • 대규모 메시지 처리 : 대규모의 메시지를 처리할 때 성능 저하가 발생할 수 있으며, 이러한 경우 적절한 클러스터링 및 최적화가 필요
    • 운영 비용
      • 리소스 소비 : 메모리와 CPU 자원을 많이 소비할 수 있어, 충분한 리소스를 제공해야 원활하게 운영될 수 있음
      • 모니터링 및 유지보수 : 지속적인 모니터링과 유지보수가 필요하여, 추가적인 인력과 비용 발생
    • 러닝 커브
      • 학습 필요성 : 개념과 설정을 이해하는 데 시간이 걸릴 수 있고, 난이도가 있는 편

Kafka의 기본 구성 요소

  • 메시지
    • Kafka를 통해 전달되는 데이터 단위
    • 키(key), 값(value), 타임스탬프(timestamp), 그리고 몇가지 메타데이터로 구성됨
  • 토픽
    • 메시지를 저장하는 장소. 메시지는 토픽에 저장되었다가 소비자에게 전달됨
    • 토픽은 여러 파티션으로 나누어질 수 있으며, 파티션은 메시지를 순서대로 저장함. 파티션을 통해 병렬 처리가 가능
  • 파티션
    • 토픽을 물리적으로 나눈 단위, 각 파티션은 독립적으로 메시지를 저장하고 관리함
    • 각 파티션은 메시지를 순서대로 저장하며, 파티션 내의 메시지는 고유한 오프셋으로 식별됨
    • 파티션을 통해 데이터를 병렬 처리할 수 있으며, 클러스터 내의 여러 브로커에 분산시켜 저장할 수 있음
    • 메시지를 특정 파티션에 할당하는 데 사용되는 값
    • 동일한 키를 가진 메시지는 항상 동일한 파티션에 저장됨
  • 프로듀서
    • 메시지를 생성하고 Kafka에 보내는 역할
    • 프로듀서는 특정 토픽에 메시지를 보냄
  • 컨슈머
    • 토픽에서 메시지를 가져와 처리하는 역할
    • 특정 컨슈머 그룹에 속하며, 같은 그룹에 속한 컨슈머들은 토픽의 파티션을 분산 처리함
    • 기본적으로 스티키 파티셔닝(Sticky Partitioning)을 사용. 특정 컨슈머가 특정 파티션에 붙어서 계속해서 데이터를 처리하는 방식, 데이터 지역성을 높여 캐시 히트율을 증가시키고 전반적인 처리 성능을 향상시킴
  • 브로커
    • 클러스터의 각 서버를 의미, 메시지를 저장하고 전송하는 역할
    • 하나의 Kafka 클러스터는 여러 브로커로 구성될 수 있으며, 각 브로커는 하나 이상의 토픽 파티션을 관리
  • 주키퍼
    • 클러스터를 관리하고 조정하는 데 사용되는 분산 코디네이션 서비스
    • 주키퍼는 브로커의 메타데이터를 저장하고, 브로커 간의 상호작용을 조정함

Kafka와 RabbitMQ의 차이점

  • 설계 철학
    • RabbitMQ : 전통적인 메시지 브로커, 안정적인 전달과 큐잉에 중점
    • Kafka : 분산 스트리밍 플랫폼, 대규모 실시간 데이터 스트림의 저장과 분석에 중점을 둠
  • 메시지 모델
    • RabbitMQ : 큐를 중심으로 메시지를 전달. 메시지는 큐에 저장하고, 큐에서 하나 이상의 컨슈머에게 전달
    • Kafka : 토픽을 중심으로 메시지를 저장. 메시지는 토픽의 파티션에 저장되고, 컨슈머는 파티션에서 메시지를 읽음
  • 메시지 지속성
    • RabbitMQ : 메시지를 메모리나 디스크에 저장할 수 있으며, 일반적으로 단기 저장을 목표로 함
    • Kafka : 메시지를 디스크에 저장하며, 장기 저장을 목표로 함. 데이터 로그는 설정된 기간 동안 보존됨
  • 사용
    • RabbitMQ : 작업 큐, 요청/응답 패턴, 비동기 작업 처리 등 전통적인 메시지 큐 사용 사례에 적합
    • Kafka : 실시간 데이터 스트리밍, 로그 수집 및 분석, 이벤트 소싱 등 대규모 데이터 스트림 처리에 적합

Kafka 실습

  • Kafka 설치

    • docker-compose.yml
      version: '3.8'
      services:
        zookeeper:
          image: wurstmeister/zookeeper:3.4.6
          platform: linux/amd64
          ports:
            - "2181:2181"
          environment:
            ZOOKEEPER_CLIENT_PORT: 2181
            ZOOKEEPER_TICK_TIME: 2000
      
        kafka:
          image: wurstmeister/kafka:latest
          platform: linux/amd64
          ports:
            - "9092:9092"
          environment:
            KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092
            KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
            KAFKA_LISTENERS: INSIDE://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092
            KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
            KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
          volumes:
            - /var/run/docker.sock:/var/run/docker.sock
      
        kafka-ui:
          image: provectuslabs/kafka-ui:latest
          platform: linux/amd64
          ports:
            - "8080:8080"
          environment:
            KAFKA_CLUSTERS_0_NAME: local
            KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092
            KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181
            KAFKA_CLUSTERS_0_READONLY: "false"
      
  • Producer Project

    • 의존성
      • Spring Web
      • Lombok
      • Spring for Apache Kafka
    • application.yml
      spring:
        application:
          name: producer
        kafka:
          bootstrap-servers: localhost:9092
          producer:
            key-serializer: org.apache.kafka.common.serialization.StringSerializer
            value-serializer: org.apache.kafka.common.serialization.StringSerializer
      server:
        port: 8090
      
    • ProducerApplicationKafkaConfig
      @Configuration
      public class ProducerApplicationKafkaConfig {
          @Bean
          public ProducerFactory<String, String> producerFactory() {
              Map<String, Object> configProps = new HashMap<>();
              configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
              configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
              configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
              return new DefaultKafkaProducerFactory<>(configProps);
          }
      
          @Bean
          public KafkaTemplate<String, String> kafkaTemplate() {
              return new KafkaTemplate<>(producerFactory());
          }
      }
    • ProducerController
      @RestController
      @RequiredArgsConstructor
      public class ProducerController {
      
          private final ProducerService producerService;
      
          @GetMapping("/send")
          public String sendMessage(@RequestParam("topic") String topic,
                                    @RequestParam("key") String key,
                                    @RequestParam("message") String message) {
              producerService.sendMessage(topic, key, message);
              return "Message sent to Kafka topic";
          }
      }
    • ProducerService
      @Service
      @RequiredArgsConstructor
      public class ProducerService {
      
          private final KafkaTemplate<String, String> kafkaTemplate;
      
          public void sendMessage(String topic , String key, String message) {
              for (int i = 0; i < 10; i++) {
      
                  kafkaTemplate.send(topic, key, message + " " + i);
              }
      
          }
      }
  • Consumer Project

    • 의존성
      • Spring Web
      • Lombok
      • Spring for Apache Kafka
    • application.yml
      spring:
        application:
          name: consumer
        kafka:
          bootstrap-servers: localhost:9092
          producer:
            key-serializer: org.apache.kafka.common.serialization.StringSerializer
            value-serializer: org.apache.kafka.common.serialization.StringSerializer
      server:
        port: 8091
      
    • ConsumerApplicationKafkaConfig
      // 이 클래스는 Kafka 컨슈머 설정을 위한 Spring 설정 클래스입니다.
      @EnableKafka // Kafka 리스너를 활성화하는 어노테이션입니다.
      @Configuration // Spring 설정 클래스로 선언하는 어노테이션입니다.
      public class ConsumerApplicationKafkaConfig {
      
          // Kafka 컨슈머 팩토리를 생성하는 빈을 정의합니다.
          // ConsumerFactory는 Kafka 컨슈머 인스턴스를 생성하는 데 사용됩니다.
          // 각 컨슈머는 이 팩토리를 통해 생성된 설정을 기반으로 작동합니다.
          @Bean
          public ConsumerFactory<String, String> consumerFactory() {
              // 컨슈머 팩토리 설정을 위한 맵을 생성합니다.
              Map<String, Object> configProps = new HashMap<>();
              // Kafka 브로커의 주소를 설정합니다.
              configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
              // 메시지 키의 디시리얼라이저 클래스를 설정합니다.
              configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
              // 메시지 값의 디시리얼라이저 클래스를 설정합니다.
              configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
              // 설정된 프로퍼티로 DefaultKafkaConsumerFactory를 생성하여 반환합니다.
              return new DefaultKafkaConsumerFactory<>(configProps);
          }
      
          // Kafka 리스너 컨테이너 팩토리를 생성하는 빈을 정의합니다.
          // ConcurrentKafkaListenerContainerFactory는 Kafka 메시지를 비동기적으로 수신하는 리스너 컨테이너를 생성하는 데 사용됩니다.
          // 이 팩토리는 @KafkaListener 어노테이션이 붙은 메서드들을 실행할 컨테이너를 제공합니다.
          @Bean
          public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
              // ConcurrentKafkaListenerContainerFactory를 생성합니다.
              ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
              // 컨슈머 팩토리를 리스너 컨테이너 팩토리에 설정합니다.
              factory.setConsumerFactory(consumerFactory());
              // 설정된 리스너 컨테이너 팩토리를 반환합니다.
              return factory;
          }
      }
    • ConsumerEndpoint
      @Slf4j
      @Service
      public class ConsumerEndpoint {
      
          // 이 메서드는 Kafka에서 메시지를 소비하는 리스너 메서드입니다.
          // @KafkaListener 어노테이션은 이 메서드를 Kafka 리스너로 설정합니다.
          @KafkaListener(groupId = "group_a", topics = "topic1")
          // Kafka 토픽 "test-topic"에서 메시지를 수신하면 이 메서드가 호출됩니다.
          // groupId는 컨슈머 그룹을 지정하여 동일한 그룹에 속한 다른 컨슈머와 메시지를 분배받습니다.
          public void consumeFromGroupA(String message) {
              log.info("Group A consumed message from topic1: " + message);
          }
      
          // 동일한 토픽을 다른 그룹 ID로 소비하는 또 다른 리스너 메서드입니다.
          @KafkaListener(groupId = "group_b", topics = "topic1")
          public void consumeFromGroupB(String message) {
              log.info("Group B consumed message from topic1: " + message);
          }
      
          // 다른 토픽을 다른 그룹 ID로 소비하는 리스너 메서드입니다.
          @KafkaListener(groupId = "group_c", topics = "topic2")
          public void consumeFromTopicC(String message) {
              log.info("Group C consumed message from topic2: " + message);
          }
      
          // 다른 토픽을 다른 그룹 ID로 소비하는 리스너 메서드입니다.
          @KafkaListener(groupId = "group_c", topics = "topic3")
          public void consumeFromTopicD(String message) {
              log.info("Group C consumed message from topic3: " + message);
          }
      
          @KafkaListener(groupId = "group_d", topics = "topic4")
          public void consumeFromPartition0(String message) {
              log.info("Group D consumed message from topic4: " + message);
          }
      }
  • 실행 후 확인

    • localhost:8090/send?topic=topic1&key=key-1&message=hihi
    • 결과 - 로그
    • 결과 - Kafka UI

SAGA Pattern 실습

RabbitMQ를 활용해 보상 트랜잭션이 구현된 상품 주문 시스템 구현
Project : Order, Product, Payment
Queue : market.product, market.payment, market.err.product, market.err.order

OrderApplication

  • build.gradle 의존성
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-amqp'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'org.springframework.amqp:spring-rabbit-test'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
  • application.yml
    spring:
      application:
        name: order
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    
    message:
      exchange: market
      err:
        exchange : market.err
      queue:
        product: market.product
        payment: market.payment
        err:
          order : market.err.order
          product : market.err.product
  • OrderApplicationQueueConfig
    @Configuration
    public class OrderApplicationQueueConfig {
    
        @Bean
        public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    
        @Value("${message.exchange}")
        private String exchange;
    
        @Value("${message.queue.product}")
        private String queueProduct;
    
        @Value("${message.queue.payment}")
        private String queuePayment;
    
        @Value("${message.err.exchange}")
        private String exchangeErr;
    
        @Value("${message.queue.err.order}")
        private String queueErrOrder;
    
        @Value("${message.queue.err.product}")
        private String queueErrProduct;
    
        @Bean public TopicExchange exchange() { return new TopicExchange(exchange); }
    
        @Bean public Queue queueProduct() { return new Queue(queueProduct); }
        @Bean public Queue queuePayment() { return new Queue(queuePayment); }
    
        @Bean public Binding bindingProduct() { return BindingBuilder.bind(queueProduct()).to(exchange()).with(queueProduct); }
        @Bean public Binding bindingPayment() { return BindingBuilder.bind(queuePayment()).to(exchange()).with(queuePayment); }
    
        @Bean public TopicExchange exchangeErr() { return new TopicExchange(exchangeErr); }
    
        @Bean public Queue queueErrOrder() { return new Queue(queueErrOrder); }
        @Bean public Queue queueErrProduct() { return new Queue(queueErrProduct); }
    
        @Bean public Binding bindingErrOrder() { return BindingBuilder.bind(queueErrOrder()).to(exchangeErr()).with(queueErrOrder); }
        @Bean public Binding bindingErrProduct() { return BindingBuilder.bind(queueErrProduct()).to(exchangeErr()).with(queueErrProduct); }
    }
    
  • Order
    @Builder
    @Data
    @ToString
    public class Order {
        private UUID orderId;
        private String userId;
        private String orderStatus;
        private String errorType;
    
        public void cancelOrder(String receiveErrorType) {
            orderStatus = "CANCEL";
            errorType = receiveErrorType;
        }
    }
    
  • DeliveryMessage
    @Data
    @Builder
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public class DeliveryMessage {
        private UUID orderId;
        private UUID paymentId;
    
        private String userId;
    
        private Integer productId;
        private Integer productQuantity;
    
        private Integer payAmount;
    
        private String errorType;
    }
  • OrderEndpoint
    @Slf4j
    @RestController
    @RequiredArgsConstructor
    public class OrderEndpoint {
    
        private final OrderService orderService;
    
        private final RabbitTemplate rabbitTemplate;
    
        @GetMapping("order/{orderId}")
        public ResponseEntity<Order> getOrder(@PathVariable UUID orderId) {
            Order order = orderService.getOrder(orderId);
            return ResponseEntity.ok(order);
        }
    
        @PostMapping("/order")
        public ResponseEntity<Order> order(@RequestBody OrderRequestDto orderRequestDto) {
            Order order = orderService.createOrder(orderRequestDto);
            return ResponseEntity.ok(order);
        }
    
        @RabbitListener(queues = "${message.queue.err.order}")
        public void errOrder(DeliveryMessage message) {
            log.info("ERROR RECEIVE !!!");
            orderService.rollbackOrder(message);
        }
    
        @Data
        public static class OrderRequestDto {
            private String userId;
            private Integer productId;
            private Integer productQuantity;
            private Integer payAmount;
    
            public Order toOrder (){
                return Order.builder()
                        .orderId(UUID.randomUUID())
                        .userId(userId)
                        .orderStatus("RECEIPT")
                        .build();
    
            }
    
            public DeliveryMessage toDeliveryMessage(UUID orderId){
                return DeliveryMessage.builder()
                        .orderId(orderId)
                        .userId(userId)
                        .productId(productId)
                        .productQuantity(productQuantity)
                        .payAmount(payAmount)
                        .build();
            }
        }
    
    }
    
  • OrderService
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class OrderService {
    
        @Value("${message.queue.product}")
        private String productQueue;
    
        private final RabbitTemplate rabbitTemplate;
    
        private Map<UUID, Order> orderStore = new HashMap<>();
    
        public Order createOrder(OrderEndpoint.OrderRequestDto orderRequestDto) {
            Order order = orderRequestDto.toOrder();
            DeliveryMessage deliveryMessage = orderRequestDto.toDeliveryMessage(order.getOrderId());
    
            orderStore.put(order.getOrderId(), order);
    
            log.info("send Message : {}",deliveryMessage.toString());
    
            rabbitTemplate.convertAndSend(productQueue, deliveryMessage);
    
            return order;
    
        }
    
        public void rollbackOrder(DeliveryMessage message) {
            Order order = orderStore.get(message.getOrderId());
            order.cancelOrder(message.getErrorType());
            log.info(order.toString());
    
        }
    
        public Order getOrder(UUID orderId) {
            return orderStore.get(orderId);
        }
    }

ProductApplication

  • build.gradle 의존성
    dependencies {
    	// Jackson 의존성
    	implementation 'com.fasterxml.jackson.core:jackson-databind'
    	implementation 'com.fasterxml.jackson.core:jackson-core'
    	implementation 'com.fasterxml.jackson.core:jackson-annotations'
    
    	implementation 'org.springframework.boot:spring-boot-starter-amqp'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'org.springframework.amqp:spring-rabbit-test'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
  • application.yml
    spring:
      application:
        name: product
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    
    message:
      exchange: market
      err:
        exchange : market.err
      queue:
        product: market.product
        payment: market.payment
        err:
          order : market.err.order
          product : market.err.product
  • ProductApplicationQueueConfig
    @Configuration
    public class ProductApplicationQueueConfig {
        @Bean
        public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    }
  • DeliveryMessage
    @Data
    @Builder
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public class DeliveryMessage {
        private UUID orderId;
        private UUID paymentId;
    
        private String userId;
    
        private Integer productId;
        private Integer productQuantity;
    
        private Integer payAmount;
    
        private String errorType;
    }
  • ProductEndpoint
    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class ProductEndpoint {
    
        private final ProductService productService;
    
        @RabbitListener(queues = "${message.queue.product}")
        public void receiveMessage(DeliveryMessage deliveryMessage) {
            productService.reduceProductAmount(deliveryMessage);
    
            log.info("PRODUCT RECEIVE:{}", deliveryMessage.toString());
        }
    
        @RabbitListener(queues="${message.queue.err.product}")
        public void receiveErrorMessage(DeliveryMessage deliveryMessage) {
            log.info("ERROR RECEIVE !!!");
            productService.rollbackProduct(deliveryMessage);
        }
    }
  • ProductService
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ProductService {
    
        private final RabbitTemplate rabbitTemplate;
    
        @Value("${message.queue.payment}")
        private String paymentQueue;
    
        @Value("${message.queue.err.order}")
        private String orderErrorQueue;
    
        public void reduceProductAmount(DeliveryMessage deliveryMessage) {
    
            Integer productId = deliveryMessage.getProductId();
            Integer productQuantity = deliveryMessage.getProductQuantity();
    
            if (productId != 1 || productQuantity > 1) {
                this.rollbackProduct(deliveryMessage);
                return;
            }
    
            rabbitTemplate.convertAndSend(paymentQueue,deliveryMessage);
        }
    
        public void rollbackProduct(DeliveryMessage deliveryMessage){
            log.info("PRODUCT ROLLBACK!!!");
            if(!StringUtils.hasText(deliveryMessage.getErrorType())){
                deliveryMessage.setErrorType("PRODUCT ERROR");
            }
            rabbitTemplate.convertAndSend(orderErrorQueue, deliveryMessage);
        }
    }
    

PaymentApplication

  • build.gradle 의존성
    dependencies {
    	// Jackson 의존성
    	implementation 'com.fasterxml.jackson.core:jackson-databind'
    	implementation 'com.fasterxml.jackson.core:jackson-core'
    	implementation 'com.fasterxml.jackson.core:jackson-annotations'
    
    	implementation 'org.springframework.boot:spring-boot-starter-amqp'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'org.springframework.amqp:spring-rabbit-test'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
  • application.yml
    spring:
      application:
        name: payment
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    
    message:
      exchange: market
      err:
        exchange : market.err
      queue:
        product: market.product
        payment: market.payment
        err:
          order : market.err.order
          product : market.err.product
  • DeliveryMessage
    @Data
    @Builder
    @ToString
    @AllArgsConstructor
    @NoArgsConstructor
    public class DeliveryMessage {
        private UUID orderId;
        private UUID paymentId;
    
        private String userId;
    
        private Integer productId;
        private Integer productQuantity;
    
        private Integer payAmount;
    
        private String errorType;
    }
  • Payment
    @Data
    @Builder
    public class Payment {
        private UUID paymentId;
        private String userId;
    
        private String payAmount;
    
        private String payStatus;
    }
  • PaymentApplicationQueueConfig
    @Configuration
    public class PaymentApplicationQueueConfig {
        @Bean
        public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
            return new Jackson2JsonMessageConverter();
        }
    }
    
  • PaymentEndpoint
    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class PaymentEndpoint {
    
        private final PaymentService paymentService;
    
        @RabbitListener(queues = "${message.queue.payment}")
        public void receiveMessage(DeliveryMessage deliveryMessage) {
            log.info("PAYMENT RECEIVE : {}", deliveryMessage.toString());
            paymentService.createPayment(deliveryMessage);
    
        }
    }
  • PaymentService
    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class PaymentService {
    
        private final RabbitTemplate rabbitTemplate;
    
        @Value("${message.queue.err.product}")
        private String productErrorQueue;
    
        public void createPayment(DeliveryMessage deliveryMessage) {
            Payment payment = Payment.builder()
                    .paymentId(UUID.randomUUID())
                    .userId(deliveryMessage.getUserId())
                    .payStatus("SUCCESS").build();
    
            Integer payAmount = deliveryMessage.getPayAmount();
    
            if (payAmount >= 10000) {
                log.error("Payment amount exceeds limit: {}", payAmount);
                payment.setPayStatus("CANCEL");
                deliveryMessage.setErrorType("PAYMENT_LIMIT_EXCEEDED");
                this.rollbackPayment(deliveryMessage);
            }
        }
    
        public void rollbackPayment(DeliveryMessage deliveryMessage) {
            log.info("PAYMENT ROLLBACK !!!");
            rabbitTemplate.convertAndSend(productErrorQueue, deliveryMessage);
        }
    }

실행 결과

  • 정상 동작
    • localhost:8080/order ( POST )
      {
          "userId" : "user1",
          "productId" : 1,
          "productQuantity" : 1,
          "payAmount" : 1000
      }
    • 결과
  • 에러 확인
    • localhost:8080/order ( POST )
      {
          "userId" : "user1",
          "productId" : 1,
          "productQuantity" : 1,
          "payAmount" : 10000
      }
    • 등록 된 order ID 복사
    • localhost:8080/order/{orderId} (GET)
    • 결과

JMeter

Apache에서 만든 Java Application Open Source 부하 테스트 도구

부하 테스트 시나리오

위에서 만든 주문 서비스를 활용한 부하 테스트

  1. Thread Group 생성 ( Test Plan - Add - Threads ( Users ) - Thread Group

    • Number of Threads : 사용자 수
    • Ramp-up period : 시작 하는 데 걸리는 시간
    • Loop Count : 반복 횟수
  2. HTTP Request 추가 ( Thread Group - Add - Sampler - HTTP Request )

  3. HTTP Request 정보 입력 - Protocol, Host, Port, HTTP Method, Path

    • Body Data
      {
          "userId" : "user1",
          "productId" : 1,
          "productQuantity" : 1,
          "payAmount" : 1000
      }
  4. HTTP Header Manager 추가 ( HTTP Request - Add - Config Element - HTTP Header Manager )

  5. Header에 Content-Type 추가

    • Content-Type : application/json
  6. Report 추가 ( Thread Group - Add - Listener - View Results Tree, Summary Report )

  7. 실행 후 Report 확인

profile
기록을 남겨보자

0개의 댓글