Repository: dev-4-team/eye-on-backend
Pull request #33: Test/performance
Author: login: smart-cau
Created: 2025-04-07T09:09:36Z | Merged: 2025-04-10T23:20:45Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Prometheus 기반 모니터링 기능이 도입되어 실시간 메트릭 수집이 향상되었습니다.
  - 웹소켓 메시지 전송 최적화로 더욱 빠른 실시간 통신을 지원합니다.
  
- **Infrastructure**
  - 컨테이너 재시작 정책과 서비스 구성이 개선되어 시스템 안정성이 강화되었습니다.
  - 서버 및 데이터베이스 성능 설정이 업데이트되고, 허용 도메인이 확대되었습니다.
  
- **Tests**
  - 성능 테스트 스크립트가 개선되어 요청 처리와 메트릭 추적 기능이 크게 강화되었습니다.
  
- **Refactor**
  - 내부 서비스 의존성 및 로깅 관련 코드가 정리되어 운영 효율성이 향상되었습니다.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/build.gradle b/build.gradle
index ce87f05..9b428b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -66,6 +66,9 @@ dependencies {
     // Log
     implementation 'org.springframework.boot:spring-boot-starter-log4j2'
     implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.10.0'
+
+    // Monitoring
+    implementation 'io.micrometer:micrometer-registry-prometheus'
 }
 
 test {
diff --git a/config/prometheus.yml b/config/prometheus.yml
index fbb1796..52e3940 100644
--- a/config/prometheus.yml
+++ b/config/prometheus.yml
@@ -1,14 +1,49 @@
 global:
-  scrape_interval: 15s
-  evaluation_interval: 15s
+  scrape_interval: 10s
+  evaluation_interval: 10s
+
+rule_files:
 
 scrape_configs:
+  # Prometheus 자체 모니터링
   - job_name: 'prometheus'
     static_configs:
-      - targets: ['localhost:9090']
+      - targets: [ 'prometheus:9090' ]  # localhost가 아닌 서비스 이름 사용
 
-remote_write:
-  - url: http://localhost:9090/api/v1/write
+  # Spring Boot 애플리케이션 메트릭 (Actuator 활성화 필요)
+  - job_name: 'spring-boot'
+    metrics_path: 'api/actuator/prometheus'
+    scrape_interval: 10s
+    static_configs:
+      - targets: [ 'app:8080' ]
+
+  # Docker 호스트 메트릭 (선택적) - node-exporter 추가 필요
+  - job_name: 'node-exporter'
+    static_configs:
+      - targets: [ 'node-exporter:9100' ]
 
-remote_read:
-  - url: http://localhost:9090/api/v1/read
\ No newline at end of file
+  # k6 부하 테스트 메트릭
+  - job_name: 'k6'
+    scrape_interval: 10s  # 성능 테스트 중 세밀한 데이터 수집
+    static_configs:
+      - targets: [ 'k6:6565' ]
+    metric_relabel_configs:
+      - source_labels: [ __name__ ]
+        regex: 'k6_.*'
+        action: 'keep'
+
+  # 컨테이너 메트릭 수집을 위한 cAdvisor
+  - job_name: 'cadvisor'
+    scrape_interval: 10s
+    metrics_path: /metrics  # 명시적으로 경로 지정
+    static_configs:
+      - targets: ['cadvisor:8080']
+
+# 원격 쓰기/읽기 설정 (k6 통합용)
+# k6에서 원격 쓰기를 위한 설정 - 중요: 수신 필터링 추가
+remote_write:
+  - url: http://prometheus:9090/api/v1/write
+    write_relabel_configs:
+      - source_labels: [ __name__ ]
+        regex: 'k6_.*'
+        action: keep
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 32509be..3e4839f 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -2,4 +2,5 @@ services:
   redis:
     image: redis
     ports:
-      - "6379:6379"
\ No newline at end of file
+      - "6379:6379"
+    restart: unless-stopped
\ No newline at end of file
diff --git a/docker-compose-performance-test.yml b/docker-compose-performance-test.yml
index ac2b17a..c936995 100644
--- a/docker-compose-performance-test.yml
+++ b/docker-compose-performance-test.yml
@@ -3,11 +3,14 @@ services:
   app:
     env_file:
       - .env
+    environment:
+      - DB_URL=jdbc:postgresql://host.docker.internal:5432/${DB_NAME}
+      - REDIS_HOST=host.docker.internal
     build: .
     container_name: ${CONTAINER_NAME}
     ports:
       - "8080:8080"
-    restart: always
+    restart: unless-stopped
     volumes:
       - /var/log/eye-on:/app/logs
     deploy:
@@ -28,6 +31,22 @@ services:
       - monitoring
       - default
 
+  cadvisor:
+    image: gcr.io/cadvisor/cadvisor
+    privileged: true
+    restart: unless-stopped
+    container_name: cadvisor
+    ports:
+      - "8085:8080"
+    volumes:
+      - /:/rootfs:ro
+      - /var/run:/var/run:rw
+      - /sys:/sys:ro
+      - /var/lib/docker/:/var/lib/docker:ro
+      - /dev/disk/:/dev/disk:ro
+    networks:
+      - monitoring
+
   prometheus:
     image: prom/prometheus:v2.43.0
     container_name: prometheus
@@ -43,7 +62,9 @@ services:
       - --web.console.libraries=/etc/prometheus/console_libraries
       - --web.console.templates=/etc/prometheus/consoles
       - --web.enable-lifecycle
-      - --enable-feature=remote-write-receiver
+      - --web.enable-remote-write-receiver
+    depends_on:
+      - cadvisor
     networks:
       - monitoring
 
@@ -68,7 +89,7 @@ services:
     container_name: k6
     ports:
       - "6565:6565"
-    command: ["sleep", "infinity"]
+    command: [ "sleep", "infinity" ]
     environment:
       - K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write
       - K6_OUT=experimental-prometheus-rw
@@ -76,11 +97,25 @@ services:
       - API_PORT=${API_PORT}
     volumes:
       - ./src/test/java/com/on/eye/api/performance/scripts:/scripts
+    extra_hosts:
+      - "host.docker.internal:host-gateway"
     depends_on:
       - prometheus
     networks:
       - monitoring
 
+  node-exporter:
+    image: prom/node-exporter
+    container_name: node-exporter
+    ports:
+      - "9100:9100"
+    depends_on:
+      - prometheus
+    networks:
+      - monitoring
+
+
+
 networks:
   monitoring:
 
diff --git a/src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java b/src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java
index fdb2230..f57147f 100644
--- a/src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java
+++ b/src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java
@@ -6,7 +6,7 @@
 import org.springframework.web.bind.annotation.*;
 
 import com.on.eye.api.cheer.dto.CheerStat;
-import com.on.eye.api.cheer.service.CheerDbService;
+import com.on.eye.api.cheer.service.CheerService;
 
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -19,7 +19,7 @@
 @RequiredArgsConstructor
 @Slf4j
 public class CheerRestController {
-    private final CheerDbService cheerDbService;
+    private final CheerService cheerDbService;
 
     /**
      * 특정 시위의 응원 수를 조회
diff --git a/src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java b/src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java
index 762bab3..2db69fb 100644
--- a/src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java
+++ b/src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java
@@ -35,7 +35,6 @@ public class CheerWebSocketController {
     public void processCheerRequest(
             @DestinationVariable Long protestId,
             @Payload(required = false) CheerRequest cheerRequest) {
-        log.debug("응원 요청 - 시위 ID: {}", protestId);
         cheerCacheService.cheerProtest(protestId);
     }
 
@@ -46,7 +45,6 @@ public void processCheerRequest(
      */
     @SubscribeMapping(CHEER_TOPIC)
     public List<CheerStat> getInitCheerCounts() {
-        log.debug("응원 토픽 구독");
         return cheerCacheService.getAllCheerStats();
     }
 }
diff --git a/src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java b/src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java
index 1422ea6..c4c2729 100644
--- a/src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java
+++ b/src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java
@@ -16,7 +16,8 @@
 @Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 public class ProtestCheerCount extends BaseTimeEntity {
-    @Id @Column(unique = true)
+    @Id
+    @Column(unique = true)
     private Long protestId;
 
     @Column(nullable = false)
diff --git a/src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java b/src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java
index 38b66b8..f3448af 100644
--- a/src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java
+++ b/src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java
@@ -18,9 +18,10 @@ public interface ProtestCheerCountRepository extends JpaRepository<ProtestCheerC
     List<ProtestCheerCount> findAllByProtestIds(@Param("protestIds") List<Long> protestIds);
 
     @Query(
-            "SELECT pc FROM ProtestCheerCount pc WHERE pc.protestId IN (SELECT p.id FROM Protest p WHERE p.startDateTime >= :startDateTime)")
-    List<ProtestCheerCount> findAllByProtestStartDateTime(
-            @Param("startDateTime") LocalDateTime startDateTime);
+            "SELECT pc FROM ProtestCheerCount pc WHERE pc.protestId IN (SELECT p.id FROM Protest p WHERE p.startDateTime >= :startDateTime and p.endDateTime < :endDateTime)")
+    List<ProtestCheerCount> findAllByProtestBetweenStartDateTimeAndEndDateTime(
+            @Param("startDateTime") LocalDateTime startDateTime,
+            @Param("endDateTime") LocalDateTime endDateTime);
 
     @Modifying
     @Query(
diff --git a/src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java b/src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java
index 32bf20b..dcd47ed 100644
--- a/src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java
+++ b/src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java
@@ -31,7 +31,7 @@ public Integer incrementCheerCount(Long protestId) {
 
     @Override
     public Integer getCheerCount(Long protestId) {
-        String key = generateCheerCountKeyWithNullCheck(protestId);
+        String key = generateCheerCountKey(protestId);
         String count = redisTemplate.opsForValue().get(key);
         Integer result = count == null ? 0 : Integer.parseInt(count);
         log.debug("시위 ID: {} 응원 수 조회 - 카운트: {}", protestId, result);
diff --git a/src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java b/src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java
index b6c51f2..507f228 100644
--- a/src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java
+++ b/src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java
@@ -34,7 +34,6 @@ public Integer cheerProtest(Long protestId) {
         // 시위 존재 확인
 
         Integer newCount = cheerCacheRepository.incrementCheerCount(protestId);
-        log.debug("시위 ID: {} 응원 성공 - 현재 응원 수: {}", protestId, newCount);
 
         CheerUpdateDto updateDto = CheerUpdateDto.of(protestId, newCount);
         // websocket을 통해 모든 구독자에게 업데이트 전송
@@ -73,7 +72,6 @@ public List<CheerStat> getAllCheerStats() {
     public void setCheerCount(Long protestId, Integer count) {
 
         cheerCacheRepository.setCheerCount(protestId, count);
-        log.info("시위 ID: {} 응원 수 설정 완료 - 설정값: {}", protestId, count);
 
         // 변경된 값을 실시간으로 발행
         publishCheerUpdate(CheerUpdateDto.of(protestId, count));
@@ -82,15 +80,10 @@ public void setCheerCount(Long protestId, Integer count) {
     public void publishCheerUpdate(CheerUpdateDto updateDto) {
         String destination = CHEER_TOPIC;
         simpMessagingTemplate.convertAndSend(destination, updateDto);
-        log.debug(
-                "시위 ID: {} 응원 업데이트 메시지 발행 완료 | destination: {}",
-                updateDto.protestId(),
-                destination);
     }
 
     public void clearAllOutdatedCheerCounts() {
         cheerCacheRepository.clearAllOutdatedCheerCounts();
-        log.info("모든 응원 Cache Clear");
     }
 
     public List<CheerStat> getCheerCountsForSync() {
diff --git a/src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java b/src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java
index f042d25..71adb6f 100644
--- a/src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java
+++ b/src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java
@@ -58,8 +58,8 @@ private void syncDBToCache(boolean isUpdate) {
         log.info("DB -> Cache 응원 데이터 초기화 시작");
 
         List<ProtestCheerCount> allCheers =
-                protestCheerCountRepository.findAllByProtestStartDateTime(
-                        LocalDateTimeUtils.todayStartTime());
+                protestCheerCountRepository.findAllByProtestBetweenStartDateTimeAndEndDateTime(
+                        LocalDateTimeUtils.todayStartTime(), LocalDateTimeUtils.todayEndTime());
         int loadedCount = 0;
 
         for (ProtestCheerCount cheer : allCheers) {
diff --git a/src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java b/src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java
new file mode 100644
index 0000000..f246efa
--- /dev/null
+++ b/src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java
@@ -0,0 +1,13 @@
+package com.on.eye.api.global.config.log;
+
+import org.springframework.boot.logging.LoggingSystem;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class Log4j2Config {
+    @Bean
+    public LoggingSystem loggingSystem() {
+        return LoggingSystem.get(this.getClass().getClassLoader());
+    }
+}
diff --git a/src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java b/src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java
index f94beab..d7937bb 100644
--- a/src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java
+++ b/src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java
@@ -1,18 +1,23 @@
 package com.on.eye.api.global.config.websocket;
 
-import static com.on.eye.api.global.constants.Constants.CORS_ALLOW_LIST;
-
 import org.springframework.context.annotation.Configuration;
 import org.springframework.messaging.simp.config.MessageBrokerRegistry;
 import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
 import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
 import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
+
+import static com.on.eye.api.global.constants.Constants.CORS_ALLOW_LIST;
 
-/** WebSocket 및 STOMP 메시지 브로커 설정을 담당하는 설정 클래스 */
+/**
+ * WebSocket 및 STOMP 메시지 브로커 설정을 담당하는 설정 클래스
+ */
 @Configuration
 @EnableWebSocketMessageBroker
 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
-    /** STOMP 메시지 브로커 설정. /topic: 구독 기반 메시지 전달을 위한 prefix /app: 클라이언트에서 서버로 메시지 전송 시 사용할 prefix */
+    /**
+     * STOMP 메시지 브로커 설정. /topic: 구독 기반 메시지 전달을 위한 prefix /app: 클라이언트에서 서버로 메시지 전송 시 사용할 prefix
+     */
     @Override
     public void configureMessageBroker(MessageBrokerRegistry registry) {
         // server -> client 메세지 브로드캐스트를 위한 prefix
@@ -22,9 +27,22 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
         registry.setApplicationDestinationPrefixes("/app");
     }
 
-    /** WebSocket 연결 엔드포인트 등록. SockJS를 통해 WebSocket을 지원하지 않는 브라우저에서도 동작 보장 */
+    /**
+     * WebSocket 연결 엔드포인트 등록. SockJS를 통해 WebSocket을 지원하지 않는 브라우저에서도 동작 보장
+     */
     @Override
     public void registerStompEndpoints(StompEndpointRegistry registry) {
         registry.addEndpoint("/ws").setAllowedOrigins(CORS_ALLOW_LIST).withSockJS();
     }
+
+    /**
+     * WebSocket 메시지 전송 설정 구성 메시지 크기 제한, 버퍼 크기 등을 설정
+     */
+    @Override
+    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
+        // 작은 메시지에 최적화된 버퍼 설정
+        registration.setSendBufferSizeLimit(128 * 1024); // 128KB (기본값보다 낮게)
+        registration.setMessageSizeLimit(8 * 1024); // 8KB (작은 메시지에 충분)
+        registration.setSendTimeLimit(5000); // 5초 (빠른 처리 유도)
+    }
 }
diff --git a/src/main/java/com/on/eye/api/global/constants/Constants.java b/src/main/java/com/on/eye/api/global/constants/Constants.java
index 315b7dd..29aa9a9 100644
--- a/src/main/java/com/on/eye/api/global/constants/Constants.java
+++ b/src/main/java/com/on/eye/api/global/constants/Constants.java
@@ -14,6 +14,7 @@ public final class Constants {
         "http://localhost:3000",
         "http://localhost:63342",
         "https://www.eye-on.kr",
-        "https://eye-on.kr"
+        "https://eye-on.kr",
+        "https://smartcow-test.ddnsking.com",
     };
 }
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9fa2b62..53b836a 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,4 +1,10 @@
 server:
+  tomcat:
+    mbeanregistry:
+      enabled: true
+    threads:
+      max: 10
+      min-spare: 5
   servlet:
     context-path: /api             # 컨텍스트 패스는 별도로 설정
 spring:
@@ -29,9 +35,11 @@ spring:
   datasource:
     hikari:
       driver-class-name: org.postgresql.Driver
-      connection-timeout: '20000'
       maximum-pool-size: '4'
-      minimum-idle: '1'
+      minimum-idle: '4'
+      idle-timeout: 600000    # 10분
+      max-lifetime: 1800000   # 30분
+      connection-timeout: 20000  # 20초
       connection-test-query: SELECT 1
     password: ${DB_PASSWORD}
     driver-class-name: org.postgresql.Driver
@@ -74,13 +82,20 @@ management:
   endpoints:
     web:
       exposure:
-        include: health,info
+        include: health,info,prometheus,metrics
+  prometheus:
+    metrics:
+      export:
+        enabled: true
+  info:
+    process:
+      enabled: true
 
 logging:
   config: classpath:log4j2-${spring.profiles.active}.xml
   level:
     web: DEBUG
-    com.zaxxer.hikari.HikariConfig: DEBUG
+#    com.zaxxer.hikari.HikariConfig: DEBUG
     org.springframework.orm.jpa: DEBUG
     org.springframework.transaction: DEBUG
     org.springframework.security: DEBUG
diff --git a/src/main/resources/log4j2-dev.xml b/src/main/resources/log4j2-dev.xml
index cfe7f5d..6913ff6 100644
--- a/src/main/resources/log4j2-dev.xml
+++ b/src/main/resources/log4j2-dev.xml
@@ -110,6 +110,8 @@
             <AppenderRef ref="ConsoleAppender"/>
         </Logger>
 
+        <Logger name="org.springframework.boot.actuate" level="INFO"/>
+
         <Root level="INFO">
             <AppenderRef ref="AsyncApplicationAppender"/>
             <AppenderRef ref="AsyncErrorAppender"/>
diff --git a/src/main/resources/log4j2-prod.xml b/src/main/resources/log4j2-prod.xml
index de10ea1..d3d1b8c 100644
--- a/src/main/resources/log4j2-prod.xml
+++ b/src/main/resources/log4j2-prod.xml
@@ -15,24 +15,24 @@
         <RollingFile name="ApplicationAppender"
                      fileName="${FILE_PATH}/current/application.log"
                      filePattern="${FILE_PATH}/archive/application/application-%d{yyyy-MM-dd}-%i.log">
-            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8" />
+            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
             <Policies>
                 <SizeBasedTriggeringPolicy size="20MB"/>
-                <TimeBasedTriggeringPolicy interval="1" modulate="true" />
+                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
             </Policies>
             <DefaultRolloverStrategy max="30">
                 <Delete basePath="${FILE_PATH}/archive/application" maxDepth="1">
                     <IfLastModified age="30d"/>
                 </Delete>
             </DefaultRolloverStrategy>
-            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY" />
+            <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
         </RollingFile>
 
         <!-- Error Log: WARN 레벨 이상 -->
         <RollingFile name="ErrorAppender"
                      fileName="${FILE_PATH}/current/error.log"
                      filePattern="${FILE_PATH}/archive/error/error-%d{yyyy-MM-dd}-%i.log">
-            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8" />
+            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
             <Policies>
                 <SizeBasedTriggeringPolicy size="20MB"/>
                 <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
@@ -42,14 +42,14 @@
                     <IfLastModified age="30d"/>
                 </Delete>
             </DefaultRolloverStrategy>
-            <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
+            <ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
         </RollingFile>
 
         <!-- Auth Log: 인증 과정에서 발생하는 로그만 -->
         <RollingFile name="AuthAppender"
                      fileName="${FILE_PATH}/auth/auth.log"
                      filePattern="${FILE_PATH}/auth/auth-%d{yyyy-MM-dd}-%i.log">
-            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8" />
+            <PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
             <Policies>
                 <SizeBasedTriggeringPolicy size="20MB"/>
                 <TimeBasedTriggeringPolicy interval="1" modulate="true"/>
@@ -81,9 +81,11 @@
             <AppenderRef ref="AsyncAuthAppender"/>
         </Logger>
 
+        <Logger name="org.springframework.boot.actuate" level="INFO"/>
+
         <Root level="INFO">
-            <AppenderRef ref="AsyncApplicationAppender" />
-            <AppenderRef ref="AsyncErrorAppender" />
+            <AppenderRef ref="AsyncApplicationAppender"/>
+            <AppenderRef ref="AsyncErrorAppender"/>
             <AppenderRef ref="ConsoleAppender" level="WARN"/>
         </Root>
     </Loggers>
diff --git a/src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js b/src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js
index 2a8ef8b..b50c418 100644
--- a/src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js
+++ b/src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js
@@ -1,25 +1,21 @@
 import http from 'k6/http';
-import { check } from 'k6';
-import { Counter, Trend } from 'k6/metrics';
+import {check, sleep} from 'k6';
+import {Counter, Rate, Trend} from 'k6/metrics';
 
 // 테스트 설정 변수
 const CONFIG = {
-    BASE_URL: `http://${__ENV.API_HOST}:${__ENV.API_PORT}`,
+    BASE_URL: `https://${__ENV.API_HOST}`,
     PROTEST_IDS: [1, 2, 3, 4],
     POLLING_INTERVAL: 3000,
+    CHEER_REQUEST_PER_SECOND: 3, // 1초에 3번 응원 요청
 
-// 테스트 단계 설정
-
+    // 테스트 단계 설정
     STAGES: [
-        { duration: '10s', target: 50 },    // 10초 동안 50명으로 증가
-        { duration: '10s', target: 100 },   // 10초 동안 100명으로 증가
-        { duration: '10s', target: 200 },   // 10초 동안 200명으로 증가
-        { duration: '10s', target: 300 },   // 10초 동안 300명으로 증가
-        { duration: '10s', target: 400 },   // 10초 동안 400명으로 증가
-        { duration: '10s', target: 500 },   // 10초 동안 500명으로 증가
-        { duration: '10s', target: 0 },     // 10초 동안 0명으로 감소
+        {duration: '1m', target: 300},
+        {duration: '2m', target: 2500},
+        {duration: '2m', target: 2500},
+        {duration: '2m', target: 0},
     ]
-
 }
 
 // 커스텀 메트릭 정의
@@ -28,6 +24,12 @@ const cheerResponseTime = new Trend('cheer_response_time');
 const pollCallCounter = new Counter('poll_calls');
 const pollResponseTime = new Trend('poll_response_time');
 
+// 성공/실패 카운트를 위한 Rate 메트릭 추가
+const cheerSuccessRate = new Rate('cheer_success_rate');
+const cheerFailRate = new Rate('cheer_fail_rate');
+const pollSuccessRate = new Rate('poll_success_rate');
+const pollFailRate = new Rate('poll_fail_rate');
+
 export const options = {
     scenarios: {
         rest_api_test: {
@@ -41,48 +43,90 @@ export const options = {
 
 let lastPollTime = 0;
 
-export default function() {
-    const protestId = CONFIG.PROTEST_IDS[Math.floor(Math.random() * CONFIG.PROTEST_IDS.length)];
-
-    // 1. POST 요청으로 응원하기
-    let cheerRes = http.post(`${CONFIG.BASE_URL}/api/cheer/protest/${protestId}`);
-
-    // POST 요청 메트릭 기록
-    cheerCallCounter.add(1);
-    cheerResponseTime.add(cheerRes.timings.duration);
-
-    // 응답 검증
-    check(cheerRes, {
-        'POST 응원 요청 성공': (r) => r.status === 200,
-        '응원 응답에 cheerCount 포함': (r) => {
-            try {
-                const body = JSON.parse(r.body);
-                return body.data.cheerCount !== undefined;
-            } catch (e) {
-                return false;
-            }
+export default function () {
+    // 1초에 3번의 POST 요청을 수행하기 위한 반복문
+    for (let i = 0; i < CONFIG.CHEER_REQUEST_PER_SECOND; i++) {
+        const protestId = CONFIG.PROTEST_IDS[Math.floor(Math.random() * CONFIG.PROTEST_IDS.length)];
+
+        // 응원 요청 파라미터 설정
+        const params = {
+            timeout: '5s', // 요청 타임아웃 설정
+            tags: {name: 'cheer-api'} // 요청 태깅
+        };
+
+        // 1. POST 요청으로 응원하기
+        try {
+            let cheerRes = http.post(`${CONFIG.BASE_URL}/api/cheer/protest/${protestId}`, null, params);
+
+            // POST 요청 메트릭 기록
+            cheerCallCounter.add(1);
+            cheerResponseTime.add(cheerRes.timings.duration);
+
+            // 응답 검증
+            const isSuccess = check(cheerRes, {
+                'POST 응원 요청 성공': (r) => r.status === 200,
+                '응원 응답에 cheerCount 포함': (r) => {
+                    try {
+                        const body = JSON.parse(r.body);
+                        return body.data && body.data.cheerCount !== undefined;
+                    } catch (e) {
+                        return false;
+                    }
+                },
+                '응답 시간 1000ms 이내': (r) => r.timings.duration < 1000
+            });
+
+            // 성공/실패 카운트 업데이트
+            cheerSuccessRate.add(isSuccess);
+            cheerFailRate.add(!isSuccess);
+        } catch (e) {
+            cheerFailRate.add(1);
         }
-    });
+
+        // 1초를 3등분하여 각 요청 사이에 간격을 둠 (마지막 요청 후에는 쉬지 않음)
+        if (i < CONFIG.CHEER_REQUEST_PER_SECOND - 1) {
+            sleep(1 / CONFIG.CHEER_REQUEST_PER_SECOND);
+        }
+    }
 
     // 2. polling - 3초 마다 모든 시위의 응원 통계 조회
     const currentTime = new Date().getTime();
     if (currentTime - lastPollTime > CONFIG.POLLING_INTERVAL) {
-        // 모든 시위의 응원 통계 조회
-        const pollRes = http.get(`${CONFIG.BASE_URL}/api/cheer/protest`);
-
-        // 폴링 호출 카운터 증가
-        pollCallCounter.add(1);
-
-        // 폴링 응답 시간 기록
-        pollResponseTime.add(pollRes.timings.duration);
-
-        // 폴링 성공 여부 확인
-        check(pollRes, {
-            'GET 모든 응원 통계 조회 성공': (r) => r.status === 200,
-        });
+        try {
+            // 모든 시위의 응원 통계 조회
+            const pollRes = http.get(`${CONFIG.BASE_URL}/api/cheer/protest`, {
+                timeout: '5s',
+                tags: {name: 'poll-api'}
+            });
+
+            // 폴링 호출 카운터 증가
+            pollCallCounter.add(1);
+
+            // 폴링 응답 시간 기록
+            pollResponseTime.add(pollRes.timings.duration);
+
+            // 폴링 성공 여부 확인
+            const isPollSuccess = check(pollRes, {
+                'GET 모든 응원 통계 조회 성공': (r) => r.status === 200,
+                '유효한 JSON 응답': (r) => {
+                    try {
+                        JSON.parse(r.body);
+                        return true;
+                    } catch (e) {
+                        return false;
+                    }
+                },
+                '응답 시간 1000ms 이내': (r) => r.timings.duration < 1000
+            });
+
+            // 성공/실패 카운트 업데이트
+            pollSuccessRate.add(isPollSuccess);
+            pollFailRate.add(!isPollSuccess);
+        } catch (e) {
+            pollFailRate.add(1);
+        }
 
         // 마지막 폴링 시간 업데이트
         lastPollTime = currentTime;
     }
-}
-
+}
\ No newline at end of file
diff --git a/src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js b/src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js
index bbd9358..277a373 100644
--- a/src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js
+++ b/src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js
@@ -1,22 +1,33 @@
-import { check } from 'k6';
+import {check, sleep} from 'k6';
 import ws from 'k6/ws';
-import { Counter, Trend } from 'k6/metrics';
+import {Counter, Rate, Trend} from 'k6/metrics';
+
+// 각 VU마다 독립적인 세션 ID와 연결 ID 생성
+function generateSessionId() {
+    return Math.floor(Math.random() * 1000).toString();
+}
+
+function generateConnectionId() {
+    return Math.random().toString(36).substring(2, 8);
+}
+
+const sessionId = generateSessionId();
+const connectionId = generateConnectionId();
 
 // 테스트 설정 변수
 const CONFIG = {
-    WS_URL: `ws://${__ENV.API_HOST}:${__ENV.API_PORT}/ws`,
+    WS_URL: `wss://${__ENV.API_HOST}/api/ws/${sessionId}/${connectionId}/websocket`,
     PROTEST_IDS: [1, 2, 3, 4],
-    CHEER_INTERVAL: 3000,  // 응원 요청 간격
+    CHEER_INTERVAL: 333,  // 응원 요청 간격. 1초에 3번
 
     // 테스트 단계 설정
     STAGES: [
-        { duration: '10s', target: 50 },    // 10초 동안 50명으로 증가
-        { duration: '10s', target: 100 },   // 10초 동안 100명으로 증가
-        { duration: '10s', target: 200 },   // 10초 동안 200명으로 증가
-        { duration: '10s', target: 300 },   // 10초 동안 300명으로 증가
-        { duration: '10s', target: 400 },   // 10초 동안 400명으로 증가
-        { duration: '10s', target: 500 },   // 10초 동안 500명으로 증가
-        { duration: '10s', target: 0 },     // 10초 동안 0명으로 감소
+        // {duration: '1m', target: 300},
+        // {duration: '2m', target: 2500},
+        // {duration: '2m', target: 2500},
+        {duration: '1m', target: 50},
+        {duration: '2m', target: 300},
+        {duration: '2m', target: 0},
     ]
 };
 
@@ -26,6 +37,12 @@ const cheerResponseTime = new Trend('ws_cheer_response_time');      // 응원 
 const connectionCounter = new Counter('ws_connections');            // 웹소켓 연결 수
 const messageCounter = new Counter('ws_messages_received');         // 수신한 메시지 수
 
+// 성공/실패 카운트를 위한 Rate 메트릭 추가
+const cheerSuccessRate = new Rate('cheer_success_rate');
+const cheerFailRate = new Rate('cheer_fail_rate');
+const pollSuccessRate = new Rate('poll_success_rate');
+const pollFailRate = new Rate('poll_fail_rate');
+
 export const options = {
     scenarios: {
         websocketTest: {
@@ -42,7 +59,12 @@ const RETRY_CONFIG = {
     RETRY_DELAY: 1000,
 };
 
-export default function() {
+// SockJS를 위한 STOMP 프레임 JSON 래핑 함수
+function wrapSockJsMessage(message) {
+    return JSON.stringify([message]);
+}
+
+export default function () {
     const protestId = CONFIG.PROTEST_IDS[Math.floor(Math.random() * CONFIG.PROTEST_IDS.length)];
 
     // 응답 시간 측정을 위한 상태 추적
@@ -62,114 +84,220 @@ export default function() {
             return;
         }
 
-        const res = ws.connect(CONFIG.WS_URL, null, function(socket) {
+        const res = ws.connect(CONFIG.WS_URL, null, function (socket) {
             connectionCounter.add(1);
+            connectSuccess = true;
 
             socket.on('open', () => {
-                // STOMP CONNECT 프레임 전송(서버에 연결 요청)
-                socket.send(buildConnectFrame());
+                // console.log('WebSocket 연결 열림');
+
+                // SockJS 초기화 메시지 수신 대기
+                // 이 단계는 실제 메시지 처리 이전에 필요함
             });
 
-            let connected = false;
+            let sockJsOpened = false;
+            let stompConnected = false;
             let lastCheerTime = 0;
 
             socket.on('message', (data) => {
+                // console.log('메시지 수신:', data);
                 messageCounter.add(1);
 
-                // STOMP CONNECTED 프레임 수신받음(연결 성립)
-                if (data.startsWith('CONNECTED') && !connected) {
-                    connected = true;
+                // SockJS 초기화 메시지 ('o') 처리
+                if (data === 'o' && !sockJsOpened) {
+                    sockJsOpened = true;
+                    // console.log('SockJS 연결 초기화됨');
+
+                    // STOMP CONNECT 프레임 전송(SockJS 형식으로 래핑)
+                    const connectFrame = buildConnectFrame();
+                    socket.send(wrapSockJsMessage(connectFrame));
+                    // console.log('STOMP CONNECT 프레임 전송');
+                }
+                // SockJS 메시지 처리 ('a[...]')
+                else if (data.startsWith('a') && data.length > 1) {
+                    try {
+                        // SockJS 메시지 형식: a["MESSAGE\n..."]
+                        const jsonStart = data.indexOf('[');
+                        const jsonEnd = data.lastIndexOf(']') + 1;
+
+                        if (jsonStart !== -1 && jsonEnd !== -1) {
+                            const jsonData = data.substring(jsonStart, jsonEnd);
+                            const messages = JSON.parse(jsonData);
+
+                            // 메시지 배열의 각 항목 처리
+                            for (const message of messages) {
+                                handleStompMessage(socket, message, messageState);
+                            }
+                        }
+                    } catch (e) {
+                        console.error('SockJS 메시지 파싱 오류:', e);
+                        // 메시지 파싱 실패시 Poll 실패로 간주
+                        pollFailRate.add(1);
+                    }
+                }
+                // SockJS 종료 메시지 ('c[...]')
+                else if (data.startsWith('c')) {
+                    // console.log('SockJS 연결 종료됨');
+                }
+                // 기타 메시지 처리
+                else {
+                    console.log('알 수 없는 메시지 형식:', data);
+                }
+            });
+
+            // 일정 시간마다 응원 요청 전송
+            socket.setInterval(() => {
+                if (sockJsOpened && stompConnected) {
+                    const currentTime = new Date().getTime();
+
+                    if (currentTime - lastCheerTime >= CONFIG.CHEER_INTERVAL) {
+                        try {
+                            const destination = `/app/cheer/protest/${protestId}`;
+                            const sendFrame = buildSendFrame(destination, {});
+                            socket.send(wrapSockJsMessage(sendFrame));
+
+                            cheerCallCounter.add(1);
+                            messageState.sendTime = currentTime;
+                            lastCheerTime = currentTime;
+                            messageState.responseReceived = false;
+                            // console.log(`응원 요청 전송: ${destination}`);
+                        } catch (e) {
+                            console.error('응원 요청 전송 실패:', e);
+                            cheerFailRate.add(1);
+                        }
+                    }
+                }
+            }, 100); // 100ms 간격으로 체크
+
+            // STOMP 메시지 처리 함수
+            function handleStompMessage(socket, message, messageState) {
+                // STOMP CONNECTED 프레임 처리
+                if (message.startsWith('CONNECTED') && !stompConnected) {
+                    stompConnected = true;
+                    // console.log('STOMP 연결 성공');
 
                     // 응원 정보 토픽 구독
                     const subscriptionId = 'sub-' + Math.random().toString(36).substring(2, 15);
-                    socket.send(buildSubscribeFrame('/topic/cheer', subscriptionId));
+                    const subscribeFrame = buildSubscribeFrame('/topic/cheer', subscriptionId);
+                    socket.send(wrapSockJsMessage(subscribeFrame));
+                    // console.log('토픽 구독: /topic/cheer');
+                    // 구독 성공으로 간주
+                    pollSuccessRate.add(1);
 
                     // 에러 토픽 구독
                     const errorSubscriptionId = 'err-' + Math.random().toString(36).substring(2, 15);
-                    socket.send(buildSubscribeFrame('/user/queue/errors', errorSubscriptionId));
+                    const errorSubscribeFrame = buildSubscribeFrame('/user/queue/errors', errorSubscriptionId);
+                    socket.send(wrapSockJsMessage(errorSubscribeFrame));
+                    // console.log('에러 토픽 구독: /user/queue/errors');
 
                     // 응원 요청 전송
                     const currentTime = new Date().getTime();
                     messageState.sendTime = currentTime;
                     lastCheerTime = currentTime;
 
-                    const destination = `/app/cheer/protest/${protestId}`;
-                    socket.send(buildSendFrame(destination, {}));
-                    cheerCallCounter.add(1);
+                    try {
+                        const destination = `/app/cheer/protest/${protestId}`;
+                        const sendFrame = buildSendFrame(destination, {});
+                        socket.send(wrapSockJsMessage(sendFrame));
+                        cheerCallCounter.add(1);
+                        // console.log(`첫 응원 요청 전송: ${destination}`);
+                    } catch (e) {
+                        console.error('첫 응원 요청 전송 실패:', e);
+                        cheerFailRate.add(1);
+                    }
                 }
-
-                // 메시지 수신 처리
-                else if (data.startsWith('MESSAGE')) {
+                // STOMP MESSAGE 프레임 처리
+                else if (message.startsWith('MESSAGE')) {
                     try {
                         // 메시지 헤더에서 destination 추출
-                        const destinationMatch = data.match(/destination:([^\n]*)/);
+                        const destinationMatch = message.match(/destination:([^\n]*)/);
                         const destination = destinationMatch ? destinationMatch[1].trim() : 'unknown';
 
                         // 메시지 본문 추출
-                        const bodyStart = data.indexOf('\n\n') + 2;
-                        const bodyEnd = data.lastIndexOf('\0');
+                        const bodyStart = message.indexOf('\n\n') + 2;
+                        const bodyEnd = message.lastIndexOf('\0');
                         if (bodyStart > 1 && bodyEnd > bodyStart) {
-                            const body = data.substring(bodyStart, bodyEnd);
-                            const response = JSON.parse(body);
-
-                            // 응원 토픽 메시지 처리
-                            if (destination === '/topic/cheer') {
-                                messageState.receiveTime = new Date().getTime();
-                                messageState.responseReceived = true;
-
-                                // 응답 시간 측정
-                                const responseTime = messageState.receiveTime - messageState.sendTime;
-                                cheerResponseTime.add(responseTime);
+                            const body = message.substring(bodyStart, bodyEnd);
+                            // console.log(`메시지 본문 (${destination}): ${body}`);
+
+                            try {
+                                const response = JSON.parse(body);
+
+                                // 응원 토픽 메시지 처리
+                                if (destination === '/topic/cheer') {
+                                    messageState.receiveTime = new Date().getTime();
+                                    messageState.responseReceived = true;
+
+                                    // 응답 시간 측정
+                                    const responseTime = messageState.receiveTime - messageState.sendTime;
+                                    cheerResponseTime.add(responseTime);
+                                    // console.log(`응답 시간: ${responseTime}ms`);
+
+                                    // 성공적인 응원 응답 처리
+                                    cheerSuccessRate.add(1);
+
+                                    // 토픽 메시지 성공적 수신
+                                    pollSuccessRate.add(1);
+                                }
+                            } catch (e) {
+                                console.error('JSON 파싱 오류:', e);
+                                cheerFailRate.add(1);
+                                pollFailRate.add(1);
                             }
                         }
-                    } catch(e) {
-                        // 메시지 파싱 오류
+                    } catch (e) {
+                        console.error('메시지 파싱 오류:', e);
+                        cheerFailRate.add(1);
+                        pollFailRate.add(1);
                     }
                 }
-            });
-
-            // 응원 요청 전송
-            socket.setInterval(() => {
-                if (connected) {
-                    const currentTime = new Date().getTime();
-
-                    if (currentTime - lastCheerTime >= CONFIG.CHEER_INTERVAL) {
-                        const destination = `/app/cheer/protest/${protestId}`;
-                        socket.send(buildSendFrame(destination, {}));
-
-                        cheerCallCounter.add(1);
-                        messageState.sendTime = currentTime;
-                        lastCheerTime = currentTime;
-                        messageState.responseReceived = false;
-                    }
+                // 에러 메시지 처리
+                else if (message.startsWith('ERROR')) {
+                    console.error('STOMP 에러 수신:', message);
+                    cheerFailRate.add(1);
+                    pollFailRate.add(1);
                 }
-            }, 100); // 100ms 간격으로 체크
+                // 기타 STOMP 프레임 처리
+                else {
+                    console.log('기타 STOMP 메시지:', message.substring(0, 50));
+                }
+            }
 
             // 연결 유지
-            socket.setTimeout(function() {
-                if (connected) {
+            socket.setTimeout(function () {
+                if (stompConnected) {
                     const receiptId = 'receipt-' + Math.random().toString(36).substring(2, 15);
-                    socket.send(buildDisconnectFrame(receiptId));
+                    const disconnectFrame = buildDisconnectFrame(receiptId);
+                    socket.send(wrapSockJsMessage(disconnectFrame));
+                    // console.log('STOMP 연결 종료 요청');
                 }
                 socket.close();
-            }, 140 * 1000); // 140초 유지
+                // console.log('WebSocket 연결 종료');
+            }, 60 * 1000 * 5); // 5분 유지
         });
 
         // 웹소켓 연결 성공 여부 체크
-        check(res, { '웹소켓 연결 성공': (r) => r && r.status === 101 });
+        const wsConnectCheck = check(res, {'웹소켓 연결 성공': (r) => r && r.status === 101});
+
+        // 웹소켓 연결 성공/실패 기록
+        if (wsConnectCheck) {
+            // 웹소켓 연결은 polling과 유사한 성격으로 poll 성공률에 반영
+            pollSuccessRate.add(1);
+        } else {
+            pollFailRate.add(1);
+        }
 
         if (!connectSuccess && retryCount < RETRY_CONFIG.MAX_RETRIES) {
             retryCount++;
             console.log(`연결 시도 실패, ${retryCount}번째 재시도`);
-            setTimeout(connectWithRetry, RETRY_CONFIG.RETRY_DELAY);
+            sleep(RETRY_CONFIG.RETRY_DELAY / 1000);
+            connectWithRetry();
         }
     }
 
     connectWithRetry();
 }
 
-
-
 // STOMP CONNECT 프레임 생성
 function buildConnectFrame() {
     return 'CONNECT\n' +


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>docker-compose-dev.yml (1)</summary><blockquote>

`6-6`: **파일 끝에 개행 문자 추가 필요**

YAML 파일 끝에는 개행 문자가 있어야 합니다. 이는 YAML 표준 형식을 준수하기 위한 것입니다.

```diff
 services:
   redis:
     image: redis
     ports:
       - "6379:6379"
-    restart: unless-stopped
+    restart: unless-stopped
+
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 YAMLlint (1.35.1)</summary>

[error] 6-6: no new line character at the end of file

(new-line-at-end-of-file)

</details>

</details>

</blockquote></details>
<details>
<summary>src/main/java/com/on/eye/api/global/constants/Constants.java (1)</summary><blockquote>

`17-18`: **CORS 허용 목록에 테스트 도메인 추가**

성능 테스트를 위한 도메인 `https://smartcow-test.ddnsking.com`를 CORS 허용 목록에 추가한 것은 적절합니다. 다만, 몇 가지 고려사항이 있습니다:

1. 테스트 환경과 프로덕션 환경의 CORS 설정을 분리하는 것을 고려해보세요. Spring 프로필을 활용하여 환경별 설정을 분리할 수 있습니다.
2. 테스트 도메인이 일시적으로만 필요한 경우, 코드 변경 없이 환경 변수로 관리하는 방법도 고려해보세요.



```java
// 환경별 설정을 분리하는 예시
public final class Constants {
    // 기존 상수들...
    
    // 공통 CORS 설정
    private static final String[] COMMON_CORS_ALLOW_LIST = {
        "http://localhost:3000",
        "http://localhost:63342",
        "https://www.eye-on.kr",
        "https://eye-on.kr",
    };
    
    // 개발 환경 추가 CORS 설정
    private static final String[] DEV_ADDITIONAL_CORS = {
        "https://smartcow-test.ddnsking.com",
    };
    
    // 환경에 따라 적절한 CORS 설정을 반환하는 메서드
    public static String[] getCorsAllowList(String profile) {
        if ("dev".equals(profile) || "test".equals(profile)) {
            String[] result = new String[COMMON_CORS_ALLOW_LIST.length + DEV_ADDITIONAL_CORS.length];
            System.arraycopy(COMMON_CORS_ALLOW_LIST, 0, result, 0, COMMON_CORS_ALLOW_LIST.length);
            System.arraycopy(DEV_ADDITIONAL_CORS, 0, result, COMMON_CORS_ALLOW_LIST.length, DEV_ADDITIONAL_CORS.length);
            return result;
        }
        return COMMON_CORS_ALLOW_LIST;
    }
}
```

</blockquote></details>
<details>
<summary>src/main/resources/log4j2-dev.xml (1)</summary><blockquote>

`113-114`: **액추에이터 로깅 추가**

Spring Boot Actuator의 로깅 레벨을 INFO로 설정한 것은 성능 테스트 모니터링을 위한 좋은 접근입니다. 이를 통해 헬스 체크, 메트릭 및 기타 운영 관련 정보를 적절히 로깅할 수 있습니다.

몇 가지 개선 제안:

1. 현재 설정은 appender를 명시하지 않아 루트 로거의 appender를 상속받습니다. 필요하다면 액추에이터 로그를 위한 별도의 appender를 구성하는 것을 고려해보세요.
2. Micrometer와 Prometheus를 활용한 성능 모니터링을 위해 추가적인 설정이 필요할 수 있습니다. 예를 들어, 특정 엔드포인트나 서비스에 대한 상세 메트릭을 수집하기 위한 로깅 설정을 추가할 수 있습니다.



```xml
<Logger name="org.springframework.boot.actuate" level="INFO" additivity="false">
    <AppenderRef ref="AsyncApplicationAppender"/>
    <AppenderRef ref="ConsoleAppender"/>
</Logger>
```

</blockquote></details>
<details>
<summary>src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java (1)</summary><blockquote>

`7-13`: **Log4j2 설정이 적절하게 구성되었습니다.**

LoggingSystem 빈을 명시적으로 제공하는 방식은 Spring Boot에서 로깅 시스템을 명확하게 제어하는 데 도움이 됩니다. 이 설정은 표준 방식을 따르고 있어 로깅 시스템을 적절히 초기화합니다.

다만, 이 구성이 필요한 이유에 대한 주석이 있으면 더 명확할 것 같습니다. 일반적으로 Spring Boot는 로깅 시스템을 자동으로 구성하므로, 이 빈이 필요한 특별한 이유가 있는지 설명하는 것이 좋습니다.

</blockquote></details>
<details>
<summary>src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java (1)</summary><blockquote>

`9-9`: **서비스 의존성 추상화 개선**

`CheerDbService`에서 `CheerService`로 인터페이스 의존성을 변경한 것은 좋은 리팩토링입니다. 이는 다음과 같은 장점을 제공합니다:

1. 단일 책임 원칙(SRP)과 의존성 역전 원칙(DIP)을 더 잘 준수
2. 인터페이스 기반 프로그래밍으로 테스트 용이성 향상
3. 구현체 교체 없이 다양한 데이터 소스 활용 가능성 확보

다만, 기존 필드명 `cheerDbService`를 새로운 타입에 맞게 `cheerService`로 변경하는 것이 더 일관성 있는 네이밍이 될 것입니다.


```diff
-private final CheerService cheerDbService;
+private final CheerService cheerService;
```

그리고 관련된 메서드 호출도 모두 수정해야 합니다:

```diff
-CheerStat stats = cheerDbService.getCheerStat(protestId);
+CheerStat stats = cheerService.getCheerStat(protestId);

-List<CheerStat> statsList = cheerDbService.getAllCheerStats();
+List<CheerStat> statsList = cheerService.getAllCheerStats();

-Integer cheerCount = cheerDbService.cheerProtest(protestId);
+Integer cheerCount = cheerService.cheerProtest(protestId);
```


Also applies to: 22-22

</blockquote></details>
<details>
<summary>src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java (1)</summary><blockquote>

`21-24`: **쿼리 필터링 조건 개선으로 성능 최적화**

시작 시간과 종료 시간을 모두 고려하는 쿼리로 개선했습니다. 이 변경사항은 다음과 같은 이점을 제공합니다:

1. 시위 데이터의 필터링 정확도 향상
2. 불필요한 데이터 조회 감소로 성능 개선
3. 시간 범위 기반의 정밀한 데이터 분석 가능

특히 성능 테스트에서 중요한 변경사항입니다. 다만, 이 쿼리가 대량의 데이터를 처리할 때 인덱스 최적화가 필요할 수 있습니다.



Protest 엔티티의 startDateTime과 endDateTime에 복합 인덱스를 추가하여 이 쿼리의 성능을 더욱 향상시킬 수 있습니다. 성능 테스트 시 이 쿼리의 실행 계획(execution plan)을 확인하고 필요시 인덱스를 추가하는 것을 권장합니다.

</blockquote></details>
<details>
<summary>config/prometheus.yml (1)</summary><blockquote>

`5-6`: **비어 있는 rule_files 섹션 추가됨**

rule_files 섹션이 추가되었지만 내용이 비어 있습니다. 필요한 경우 알림 규칙이나 기록 규칙을 정의하는 파일을 여기에 추가하세요.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: .coderabbit.yml**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3ee0deac4d383f0bc875f541bb6578c6b159d043 and b63e7543792912a2650bc0aaed17c2b82ea1f39c.

</details>

<details>
<summary>📒 Files selected for processing (19)</summary>

* `build.gradle` (1 hunks)
* `config/prometheus.yml` (1 hunks)
* `docker-compose-dev.yml` (1 hunks)
* `docker-compose-performance-test.yml` (4 hunks)
* `src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java` (2 hunks)
* `src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java` (0 hunks)
* `src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java` (1 hunks)
* `src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java` (1 hunks)
* `src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java` (1 hunks)
* `src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java` (0 hunks)
* `src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java` (1 hunks)
* `src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java` (1 hunks)
* `src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java` (2 hunks)
* `src/main/java/com/on/eye/api/global/constants/Constants.java` (1 hunks)
* `src/main/resources/application.yml` (3 hunks)
* `src/main/resources/log4j2-dev.xml` (1 hunks)
* `src/main/resources/log4j2-prod.xml` (3 hunks)
* `src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js` (3 hunks)
* `src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js` (4 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* src/main/java/com/on/eye/api/cheer/controller/CheerWebSocketController.java
* src/main/java/com/on/eye/api/cheer/service/CheerCacheService.java

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>📓 Path-based instructions (1)</summary>

<details>
<summary>`**/*.java`: Review on this guidelines.

You are a Senior Architect with over 20 years of experience in Java and Spring Framework. You have deep expertise particularly in Spring Bo...</summary>

> `**/*.java`: Review on this guidelines.
> 
> You are a Senior Architect with over 20 years of experience in Java and Spring Framework. You have deep expertise particularly in Spring Boot, object-oriented design principles, and design patterns. Always consider the code context and business requirements when reviewing. Remember that there is no "perfect" solution for all code, so acknowledge trade-offs and explain them. Please follow these guidelines when reviewing code:
> 
> ### Review Approach
> 1. **Focus on Incremental Improvement**:
>   * Suggest gradual improvements rather than presenting perfect solutions from the start
>   * Mention the strengths of the current code first, then suggest improvements
>   * Use a tone of "this could be improved like this" rather than "this is wrong"
> 2. **Adherence to Object-Oriented Principles**:
>   * Review compliance with SOLID principles:
>       * Single Responsibility Principle (SRP)
>       * Open-Closed Principle (OCP)
>       * Liskov Substitution Principle (LSP)
>       * Interface Segregation Principle (ISP)
>       * Dependency Inversion Principle (DIP)
>   * Encourage "Object-Oriented Programming Calisthenics" principles:
>       * Only one level of indentation per method
>       * Don't use the else keyword
>       * Wrap all primitives and strings
>       * Only one dot per line (Law of Demeter)
>       * Don't abbreviate
>       * Keep all entities small
>       * Don't use classes with more than three instance variables
>       * Use first-class collections
>       * Don't use getters and setters
> 3. **Suggesting Appropriate Design Patterns**:
>     * Specifically suggest design patterns that match the problem context
>       * Recommend patterns commonly used in Spring, contextually:
> 
> ## Key Review Areas
> 1. **Spring Architecture**:
>   * Effective use of Spring container and dependency injection
>   * Appropriate management of Bean lifecycles
>   * Handling cross-cutting concerns using AOP
> 2. **Test Quality**:
>   * Existence and quality of unit tests and integration tests
>   * Adequacy of test coverage
>   * Testable Design
>   * Effective use of the Spring Test framework
> 3. **Performance and Scalability**:
>   * Resolving N+1 query issues
>   * Appropriate caching strategies
>   * Proper transaction management
>   * Utilizing asynchronous and parallel processing
> 4. **Error Handling and Logging**:
>     * Consistency in exception handling strategies
>       * Appropriateness of logging strategies (log levels, context information)
>       * Implementation of global exception handlers
> 

- `src/main/java/com/on/eye/api/global/constants/Constants.java`
- `src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java`
- `src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java`
- `src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java`
- `src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java`
- `src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java`
- `src/main/java/com/on/eye/api/cheer/repository/ProtestCheerCountRepository.java`
- `src/main/java/com/on/eye/api/cheer/controller/CheerRestController.java`

</details>

</details><details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java (1)</summary><blockquote>

<details>
<summary>src/main/java/com/on/eye/api/global/common/util/LocalDateTimeUtils.java (1)</summary>

* `LocalDateTimeUtils` (5-23)

</details>

</blockquote></details>
<details>
<summary>src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js (1)</summary><blockquote>

<details>
<summary>src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js (9)</summary>

* `CONFIG` (18-32)
* `cheerSuccessRate` (41-41)
* `cheerFailRate` (42-42)
* `pollSuccessRate` (43-43)
* `pollFailRate` (44-44)
* `protestId` (68-68)
* `cheerCallCounter` (35-35)
* `cheerResponseTime` (36-36)
* `body` (321-321)

</details>

</blockquote></details>

</details><details>
<summary>🪛 YAMLlint (1.35.1)</summary>

<details>
<summary>docker-compose-dev.yml</summary>

[error] 6-6: no new line character at the end of file

(new-line-at-end-of-file)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (45)</summary><blockquote>

<details>
<summary>docker-compose-dev.yml (1)</summary>

`5-6`: **Redis 서비스에 적절한 재시작 정책 추가**

Redis 서비스에 `restart: unless-stopped` 정책을 추가한 것은 좋은 선택입니다. 이 설정은 컨테이너가 충돌하거나 Docker 데몬이 재시작될 때 Redis 서비스가 자동으로 재시작되도록 보장하면서도, 개발자가 명시적으로 중지했을 때는 재시작되지 않도록 합니다.

추가적인 개선사항으로 Redis 데이터 지속성을 위한 볼륨 마운트 설정도 고려해 보세요.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 YAMLlint (1.35.1)</summary>

[error] 6-6: no new line character at the end of file

(new-line-at-end-of-file)

</details>

</details>

</details>
<details>
<summary>src/main/java/com/on/eye/api/cheer/entity/ProtestCheerCount.java (1)</summary>

`19-20`: **어노테이션 배치 개선**

어노테이션을 각각 별도의 줄에 배치하여 코드 가독성이 향상되었습니다. 이는 Java 코드 스타일 가이드에 부합하는 좋은 변경입니다.

단, 관련된 `ProtestCheerCountRepository` 인터페이스에서 `findAllByProtestStartDateTime`이 `findAllByProtestBetweenStartDateTimeAndEndDateTime`으로 변경되었다고 하니, 이 엔티티와 관련된 테스트 코드도 함께 업데이트되었는지 확인하세요.

</details>
<details>
<summary>build.gradle (1)</summary>

`69-71`: **모니터링 의존성 추가에 대한 적절한 조치입니다.**

Micrometer Prometheus 레지스트리를 추가한 것은 성능 모니터링을 위한 좋은 선택입니다. 이 의존성을 통해 Spring Boot 애플리케이션의 메트릭을 Prometheus에 노출시킬 수 있으며, 이는 성능 테스트 및 모니터링에 필수적입니다.

</details>
<details>
<summary>src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java (1)</summary>

`60-62`: <details>
<summary>❓ Verification inconclusive</summary>

**쿼리 메서드 개선으로 날짜 범위 검색 정확도가 향상되었습니다.**

기존 `findAllByProtestStartDateTime` 대신 `findAllByProtestBetweenStartDateTimeAndEndDateTime`을 사용함으로써 당일 시작과 종료 시간 사이의 모든 항목을 정확하게 조회할 수 있게 되었습니다. 이는 데이터 정확성과 완전성을 높이는 좋은 변경입니다.

이 변경으로 인해 `ProtestCheerCountRepository` 인터페이스의 메서드 서명도 업데이트되었을 것입니다. 관련 테스트 코드도 함께 업데이트되었는지 확인하면 좋겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# 업데이트된 리포지토리 메서드가 있는 파일 검색
find src -name "ProtestCheerCountRepository.java" -type f -exec cat {} \;

# 관련 테스트 파일 검색
find src/test -name "*Test.java" -type f -exec grep -l "protestCheerCountRepository" {} \;
```

Length of output: 1525

---



</details>

**쿼리 메서드 개선 확인 및 테스트 검증 요청**

- `ProtestCheerCountRepository` 인터페이스 내 `findAllByProtestBetweenStartDateTimeAndEndDateTime` 메서드가 JPQL 쿼리를 통해 당일 시작 시각 이상, 종료 시각 미만 조건을 올바르게 처리하는 것으로 확인되었습니다.
- 서비스 코드(`src/main/java/com/on/eye/api/cheer/service/CheerSyncService.java`, 60-62번 라인)에서 해당 메서드를 사용하여 데이터 조회 정확성을 향상시킨 점은 긍정적입니다.
- 다만, grep 결과로는 protestCheerCountRepository를 직접 참조하는 테스트 파일이 검색되지 않았습니다. 테스트 코드가 간접적으로 업데이트되었는지 또는 추가 검증이 필요한지 재확인 부탁드립니다.

</details>
<details>
<summary>src/main/resources/log4j2-prod.xml (2)</summary>

`84-85`: **액츄에이터 로깅 설정 추가로 모니터링 개선**

Spring Boot Actuator 로깅을 INFO 레벨로 설정하여 애플리케이션의 상태, 메트릭, 건강 상태 등을 모니터링할 수 있도록 개선했습니다. 이는 성능 테스트 및 모니터링을 위한 좋은 변경사항입니다.

---

`87-88`: **루트 로거 설정 개선**

비동기 애플리케이션 애펜더와 비동기 에러 애펜더 설정으로 로깅 성능을 향상시켰습니다. 비동기 로깅은 애플리케이션의 주 스레드 성능에 영향을 미치지 않으므로 성능 테스트에 도움이 됩니다.

</details>
<details>
<summary>src/main/java/com/on/eye/api/cheer/repository/RedisCheerCacheRepository.java (1)</summary>

`34-39`: **Redis 키 조회 예외 처리 개선**

기존의 `generateCheerCountKeyWithNullCheck` 대신 `generateCheerCountKey`를 사용하여 불필요한 예외 발생을 방지했습니다. 이 변경으로 키가 존재하지 않을 때 예외를 던지는 대신 기본값 0을 반환하여 성능이 향상됩니다.

이는 다음과 같은 이점이 있습니다:
1. 예외 발생 및 처리에 따른 오버헤드 감소
2. 응원 수가 없는 시위에 대한 자연스러운 기본값 제공
3. 높은 부하 상황에서의 안정성 향상

</details>
<details>
<summary>config/prometheus.yml (7)</summary>

`2-3`: **스크랩 간격 최적화**

스크랩 및 평가 간격을 15초에서 10초로 줄였습니다. 이는 더 빈번한 메트릭 수집을 가능하게 하여 성능 테스트 모니터링에 적합합니다.

---

`11-11`: **서비스 이름 참조로 변경**

타겟을 'localhost:9090'에서 'prometheus:9090'으로 변경한 것은 Docker 환경에서 적절합니다. 서비스 간 네트워킹이 올바르게 작동할 것입니다.

---

`20-23`: **Node Exporter 모니터링 설정 추가**

Docker 호스트 메트릭을 수집하기 위한 Node Exporter 설정이 추가되었습니다. 이는 docker-compose-performance-test.yml에 추가된 node-exporter 서비스와 잘 일치합니다.

---

`25-34`: **k6 부하 테스트 메트릭 설정 추가**

k6 부하 테스트 메트릭 수집을 위한 설정이 추가되었습니다. 메트릭 필터링이 적절히 구성되어 있어 k6_ 접두사를 가진 메트릭만 수집됩니다.

---

`35-41`: **cAdvisor 컨테이너 메트릭 수집 설정 추가**

컨테이너 메트릭 수집을 위한 cAdvisor 설정이 추가되었습니다. 이는 docker-compose-performance-test.yml에 추가된 cadvisor 서비스와 잘 일치합니다.

---

`42-49`: **원격 쓰기 설정 추가**

k6 메트릭에 대한 원격 쓰기 설정이 추가되었습니다. 이는 k6에서 Prometheus로 메트릭을 직접 전송할 수 있게 해주며, k6_ 접두사를 가진 메트릭만 필터링하도록 설정되어 있습니다.

---

`13-19`: <details>
<summary>✅ Verification successful</summary>

**Spring Boot 애플리케이션 메트릭 모니터링 설정 추가**

Spring Boot 애플리케이션 메트릭을 수집하기 위한 설정이 잘 구성되었습니다. 설정된 경로(api/actuator/prometheus)가 application.yml의 context-path 설정과 일치하는지 확인하세요.


---

🏁 Script executed:

```shell
#!/bin/bash
# Spring Boot 애플리케이션에서 Actuator 엔드포인트가 올바르게 설정되었는지 확인
cat src/main/resources/application.yml | grep -A 10 "management:" | grep -A 10 "endpoints:"
```

Length of output: 259

---



</details>

**코드 검증 완료: Actuator 설정은 올바르게 적용되어 있습니다.**

- `src/main/resources/application.yml` 파일의 출력을 보면, Prometheus 메트릭(export)이 활성화되어 있고, `endpoints.web.exposure.include`에 `prometheus`가 포함되어 있습니다.
- 다만, Prometheus 스크랩 설정에 사용된 경로(`api/actuator/prometheus`)와 실제 어플리케이션의 컨텍스트 패스(예: `server.servlet.context-path` 또는 `management.endpoints.web.base-path` 설정)가 일치하는지 다시 한 번 확인해 주세요.

</details>
<details>
<summary>src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js (7)</summary>

`2-3`: **성능 측정을 위한 메트릭 개선**

sleep 함수와 Rate 메트릭을 추가로 임포트한 것은 좋은 변경입니다. 이를 통해 요청 간격 제어와 성공/실패율 측정이 가능해졌습니다.

---

`7-7`: **HTTPS 프로토콜 사용**

BASE_URL이 HTTP에서 HTTPS로 변경되었습니다. 이는 보안 측면에서 적절한 변경입니다.

---

`10-10`: **응원 요청 빈도 설정 변수 추가**

초당 응원 요청 수를 제어하는 설정 변수를 추가한 것은 테스트 유연성을 높이는 좋은 개선입니다.

---

`13-18`: **테스트 단계 조정**

테스트 단계 설정이 더 긴 기간과 높은 부하(최대 2500 사용자)로 변경되었습니다. 이는 시스템의 확장성과 내구성을 더 철저히 테스트하기 위한 적절한 접근입니다.

---

`27-31`: **성공/실패 측정을 위한 Rate 메트릭 추가**

성공 및 실패 비율을 측정하기 위한 Rate 메트릭을 추가한 것은 테스트 결과 분석에 매우 유용합니다. 이를 통해 시스템의 안정성을 더 정확히 평가할 수 있습니다.

---

`46-90`: **응원 요청 로직 개선**

응원 요청 로직이 크게 개선되었습니다:
1. 초당 여러 요청을 보내기 위한 반복문 추가
2. 요청 시간 초과 및 태깅 설정
3. try-catch 블록을 사용한 오류 처리
4. 성공/실패 메트릭 추적

이러한 변경은 테스트의 견고성과 정확성을 높입니다.

---

`95-126`: **폴링 로직 개선**

폴링 로직에도 비슷한 개선이 적용되었습니다:
1. 시간 초과 및 태깅 매개변수 추가
2. try-catch 블록을 사용한 오류 처리
3. 성공/실패 메트릭 추적

이러한 변경은 테스트의 견고성을 높이고 결과 분석에 더 많은 정보를 제공합니다.

</details>
<details>
<summary>src/main/resources/application.yml (4)</summary>

`39-42`: **Hikari 커넥션 풀 설정 개선**

최소 유휴 연결 수를 1에서 4로 증가시키고, 유휴 타임아웃 및 최대 수명 매개변수를 추가한 것은 연결 풀 안정성을 높이는 데 도움이 됩니다. 이러한 변경은 부하 테스트 중 데이터베이스 연결 관리를 개선할 것입니다.

---

`85-85`: **Prometheus 및 메트릭 엔드포인트 노출**

관리 엔드포인트 노출 목록에 prometheus와 metrics를 추가한 것은 성능 모니터링 인프라와의 통합을 위해 필요합니다. 이는 Prometheus 설정과 일치합니다.

---

`98-98`: **HikariConfig 로깅 비활성화**

HikariConfig 로깅이 비활성화되었습니다. 이는 불필요한 로그 출력을 줄여 로그 파일 크기를 관리하는 데 도움이 됩니다. 그러나 연결 풀 문제 디버깅이 필요한 경우 이 주석을 제거할 수 있습니다.

---

`86-92`: <details>
<summary>✅ Verification successful</summary>

**Prometheus 메트릭 내보내기 및 프로세스 정보 활성화**

Prometheus 메트릭 내보내기와 프로세스 정보를 활성화한 것은 모니터링 기능을 강화하는 좋은 변경입니다. 이는 build.gradle에 추가된 micrometer-registry-prometheus 의존성과 함께 작동합니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# build.gradle에 Prometheus 관련 의존성이 추가되었는지 확인
cat build.gradle | grep -A 3 -B 3 "micrometer"
```

Length of output: 220

---



</details>

**승인: Prometheus 메트릭 내보내기 및 프로세스 정보 활성화 설정**

- `src/main/resources/application.yml` 파일에 Prometheus 메트릭 내보내기와 프로세스 정보 활성화 설정이 올바르게 추가되었습니다.
- `build.gradle` 파일에서 `io.micrometer:micrometer-registry-prometheus` 의존성이 추가되어 해당 설정과 연동됩니다.

이 변경사항은 모니터링 기능을 강화하는 좋은 개선으로 보이며, 추가 수정 없이 승인할 수 있습니다.

</details>
<details>
<summary>docker-compose-performance-test.yml (8)</summary>

`6-8`: **앱 서비스 환경 변수 추가**

앱 서비스에 DB_URL과 REDIS_HOST 환경 변수를 추가한 것은 외부 리소스와의 연결을 구성하는 데 유용합니다. 이로써 Docker 환경 내에서 외부 데이터베이스와 Redis 인스턴스에 접근할 수 있습니다.

---

`13-13`: **restart 정책 변경**

앱 서비스의 restart 정책을 'always'에서 'unless-stopped'로 변경한 것은 테스트 환경에 적합합니다. 이를 통해 수동으로 중지한 컨테이너가 다시 시작되지 않도록 할 수 있습니다.

---

`34-49`: **cAdvisor 서비스 추가**

컨테이너 메트릭 수집을 위한 cAdvisor 서비스가 추가되었습니다. 이는 Prometheus 설정의 cadvisor 작업 구성과 일치하며, 컨테이너 성능 모니터링을 가능하게 합니다.

---

`65-65`: **Prometheus 커맨드 옵션 업데이트**

Prometheus의 원격 쓰기 수신기 활성화 옵션이 '--enable-feature=remote-write-receiver'에서 '--web.enable-remote-write-receiver'로 변경되었습니다. 이는 최신 Prometheus 버전과의 호환성을 위한 변경으로 보입니다.

---

`66-67`: **Prometheus 의존성 업데이트**

Prometheus 서비스가 cadvisor에 의존하도록 설정이 업데이트되었습니다. 이는 Prometheus가 시작되기 전에 cadvisor가 먼저 시작되도록 보장합니다.

---

`92-92`: **k6 명령 구문 변경**

k6 명령의 구문이 약간 변경되었지만 기능적으로는 동일합니다. 이는 코드 가독성을 높이기 위한 변경으로 보입니다.

---

`100-101`: **k6 extra_hosts 설정 추가**

k6 서비스에 host.docker.internal을 위한 extra_hosts 항목이 추가되었습니다. 이를 통해 컨테이너 내에서 호스트 시스템에 접근할 수 있습니다.

---

`107-115`: **node-exporter 서비스 추가**

호스트 시스템 메트릭 수집을 위한 node-exporter 서비스가 추가되었습니다. 이는 Prometheus 설정의 node-exporter 작업 구성과 일치하며, 호스트 성능 모니터링을 가능하게 합니다.

</details>
<details>
<summary>src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js (12)</summary>

`1-15`: **각 가상 사용자(VU)에 대한 고유 ID 생성 기능 추가가 효과적입니다.**

세션 ID와 연결 ID를 독립적으로 생성하는 함수를 추가한 것은 테스트 환경에서 각 가상 사용자를 구분하는 데 매우 유용합니다. 이렇게 하면 테스트 중 잠재적인 세션 충돌을 방지할 수 있습니다.

---

`18-32`: **WebSocket URL 업데이트와 응원 간격 조정이 적절합니다.**

WebSocket URL을 wss로 변경하고 동적 세션/연결 ID를 추가한 것은 보안성을 높이고 실제 환경에 더 가깝게 테스트할 수 있게 해줍니다. 응원 요청 간격을 333ms로 조정하여 더 높은 부하 테스트가 가능해졌습니다.

---

`40-44`: **성공/실패 비율 측정을 위한 메트릭 추가가 효과적입니다.**

Rate 메트릭을 추가하여 응원 요청과 폴링 작업의 성공/실패율을 추적하는 것은 성능 테스트의 품질을 높이는 좋은 접근 방식입니다. 이를 통해 시스템의 안정성을 더 정확하게 평가할 수 있습니다.

---

`62-65`: **SockJS 메시지 래핑 함수 추가가 적절합니다.**

SockJS 프로토콜에 맞게 메시지를 래핑하는 함수를 추가한 것은 웹소켓 통신의 정확성을 보장하는 좋은 방법입니다.

---

`87-96`: **WebSocket 연결 처리 방식의 개선이 효과적입니다.**

WebSocket 연결 성공 여부를 명시적으로 추적하고, 불필요한 로그를 제거하여 성능 테스트 중 로그 오버헤드를 줄인 점이 좋습니다.

---

`98-145`: **SockJS 메시지 처리 로직 구현이 체계적입니다.**

SockJS 초기화 메시지, 데이터 메시지, 종료 메시지 등 다양한 유형의 메시지를 체계적으로 처리하는 구조가 잘 구현되어 있습니다. JSON 파싱 오류 발생 시 적절한 예외 처리와 메트릭 기록이 이루어지고 있습니다.

---

`149-170`: **응원 요청 전송 로직이 효율적으로 개선되었습니다.**

setInterval을 사용하여 주기적으로 상태를 체크하고, 연결 상태와 마지막 요청 시간을 고려하여 응원 요청을 보내는 방식이 효율적입니다. 오류 처리와 메트릭 기록이 적절히 구현되어 있습니다.

---

`173-208`: **STOMP 메시지 처리 함수의 구현이 체계적입니다.**

STOMP CONNECTED 프레임 처리, 토픽 구독, 에러 토픽 구독, 응원 요청 전송 등의 로직이 체계적으로 구현되어 있습니다. 오류 발생 시 메트릭 기록을 통해 문제를 추적할 수 있는 점이 좋습니다.

---

`210-263`: **다양한 STOMP 메시지 유형에 대한 처리가 잘 구현되어 있습니다.**

MESSAGE, ERROR 등 다양한 STOMP 프레임 유형에 대한 처리 로직이 체계적으로 구현되어 있습니다. 오류 처리와 메트릭 기록이 적절히 이루어지고 있으며, 불필요한 로그를 제거하여 성능 테스트 효율성을 높인 점이 좋습니다.

---

`267-276`: **연결 유지 시간 확장이 적절합니다.**

WebSocket 연결 유지 시간을 5분으로 확장한 것은 장시간 테스트에 적합한 변경이며, 여러 단계의 테스트를 수행하는 데 유리합니다.

---

`280-288`: **WebSocket 연결 상태 확인과 메트릭 기록이 적절합니다.**

WebSocket 연결 성공/실패 여부를 체크하고, 이를 폴링 성공/실패 메트릭으로 기록하는 방식이 시스템 성능을 평가하는 데 유용합니다.

---

`293-294`: **재시도 메커니즘 개선이 적절합니다.**

setTimeout 대신 sleep 함수를 사용하여 재시도 지연을 제어하는 방식으로 변경한 것은 k6의 특성에 더 적합하며, 코드 가독성도 향상되었습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-04-07T09:14:24Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-04-09T00:24:44Z

Review comments:
diff_hunk: @@ -27,4 +28,16 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {
     public void registerStompEndpoints(StompEndpointRegistry registry) {
         registry.addEndpoint("/ws").setAllowedOrigins(CORS_ALLOW_LIST).withSockJS();
     }
+
+    /** WebSocket 메시지 전송 설정 구성 메시지 크기 제한, 버퍼 크기 등을 설정 */
+    @Override
+    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
+        // 작은 메시지에 최적화된 버퍼 설정
+        registration.setSendBufferSizeLimit(128 * 1024); // 128KB (기본값보다 낮게)
+        registration.setMessageSizeLimit(8 * 1024); // 8KB (작은 메시지에 충분)
+        registration.setSendTimeLimit(5000); // 5초 (빠른 처리 유도)
+
+        // 동시 처리 메시지 수 제한
+        registration.setSendBufferSizeLimit(512 * 1024); // 총 버퍼 크기
+    }
path: src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**WebSocket 전송 설정 개선, 그러나 버퍼 크기 설정에 중복이 있습니다.**

WebSocket 메시지 전송에 대한 세부 설정을 추가한 것은 성능 최적화를 위해 좋은 접근입니다. 하지만 코드에서 `setSendBufferSizeLimit`가 두 번 호출되고 있습니다:
- 라인 36: 128KB로 설정
- 라인 41: 512KB로 설정

마지막 호출이 이전 값을 덮어쓰므로, 첫 번째 설정은 무시됩니다. 이는 의도된 것인지 확인이 필요합니다.

두 가지 해결 방안을 제안합니다:

```diff
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        // 작은 메시지에 최적화된 버퍼 설정
-       registration.setSendBufferSizeLimit(128 * 1024); // 128KB (기본값보다 낮게)
        registration.setMessageSizeLimit(8 * 1024); // 8KB (작은 메시지에 충분)
        registration.setSendTimeLimit(5000); // 5초 (빠른 처리 유도)

        // 동시 처리 메시지 수 제한
        registration.setSendBufferSizeLimit(512 * 1024); // 총 버퍼 크기
    }
```

또는 다른 설정을 의도했다면 메서드 이름을 적절히 수정해야 합니다:

```diff
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        // 작은 메시지에 최적화된 버퍼 설정
        registration.setSendBufferSizeLimit(128 * 1024); // 128KB (기본값보다 낮게)
        registration.setMessageSizeLimit(8 * 1024); // 8KB (작은 메시지에 충분)
        registration.setSendTimeLimit(5000); // 5초 (빠른 처리 유도)

        // 동시 처리 메시지 수 제한
-       registration.setSendBufferSizeLimit(512 * 1024); // 총 버퍼 크기
+       registration.setTimeToFirstMessage(512 * 1024); // 적절한 다른 설정
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    /** WebSocket 메시지 전송 설정 구성 메시지 크기 제한, 버퍼 크기 등을 설정 */
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        // 작은 메시지에 최적화된 버퍼 설정
        // Removed: registration.setSendBufferSizeLimit(128 * 1024); // 128KB (기본값보다 낮게)
        registration.setMessageSizeLimit(8 * 1024); // 8KB (작은 메시지에 충분)
        registration.setSendTimeLimit(5000); // 5초 (빠른 처리 유도)

        // 동시 처리 메시지 수 제한
        registration.setSendBufferSizeLimit(512 * 1024); // 총 버퍼 크기
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T09:14:23Z
author_association: NONE
diff_hunk: @@ -1,4 +1,10 @@
 server:
+  tomcat:
+    mbeanregistry:
+      enabled: true
+    threads:
+      max: 10
+      min-spare: 5
path: src/main/resources/application.yml
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Tomcat 설정 추가**

MBean 레지스트리를 활성화하고 스레드 설정을 구성한 것은 모니터링 강화를 위한 좋은 변경입니다. 그러나 최대 스레드 수가 10개로 설정된 것은 부하 테스트 시 병목 현상을 일으킬 수 있습니다. 애플리케이션의 특성과 예상 부하에 따라 이 값을 조정하는 것을 고려하세요.

---

🏁 Script executed:

```shell
#!/bin/bash
# 부하 테스트 스크립트에서 사용되는 최대 동시 사용자 수 확인
grep -r "target:" src/test/java/com/on/eye/api/performance/scripts/
```

Length of output: 1181

---



</details>

**Tomcat 스레드 설정 개선 필요**

현재 `src/main/resources/application.yml` 파일에서 최대 스레드 수(`tomcat.threads.max`)가 10으로 설정되어 있습니다. 성능 테스트 스크립트(`src/test/java/com/on/eye/api/performance/scripts/`) 결과, 최대 동시 사용자가 2500까지 시뮬레이션되고 있음을 확인했습니다. 이로 인해 낮은 스레드 수가 실제 부하 환경에서 병목 현상을 유발할 가능성이 있으므로, 애플리케이션의 요구사항과 부하 패턴에 맞게 해당 값을 재조정할 필요가 있습니다.

- **파일:** `src/main/resources/application.yml` (라인 2-7)
- **이슈:** `tomcat.threads.max`가 10으로 설정되어 있어, 성능 테스트에서 요구되는 최대 동시 사용자 수(최대 2500)와 비교할 때 현저히 낮습니다.
- **권장:** 부하 테스트 및 실제 운영 환경에 맞도록 최대 스레드 수를 적절한 값으로 상향 조정하세요.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-07T09:14:23Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-07T09:10:06Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이번 PR은 모니터링 및 성능 관리 구성을 강화하기 위한 다양한 변경 사항을 포함합니다.  
- **build.gradle**에 Micrometer의 Prometheus 레지스트리 의존성이 추가되었습니다.  
- **config/prometheus.yml**에서는 스크랩 간격, 잡 구성, 원격 쓰기 구성 등이 업데이트되었습니다.  
- **docker-compose** 파일들에서는 재시작 정책 추가, 환경 변수 변경, 신규 서비스 추가 등이 이루어졌습니다.  
- Java 백엔드 코드에서는 서비스 의존성 변경, 쿼리 메서드 수정, 로깅 제거 등의 수정이 포함되었습니다.  
- 추가로 Log4j2 및 WebSocket 설정, 리소스 구성, 테스트 스크립트 개선이 진행되었습니다.

## Changes

| 파일(경로) | 변경 내용 |
| --- | --- |
| `build.gradle` <br> `config/prometheus.yml` | Micrometer Prometheus 의존성 추가 및 Prometheus 스크랩 간격, 잡 구성(스프링 부트, node-exporter, k6, cadvisor)·remote_write 업데이트 |
| `docker-compose-dev.yml` <br> `docker-compose-performance-test.yml` | redis, app 서비스 재시작 정책 추가/변경; app에 환경 변수 추가; cadvisor, node-exporter 신규 서비스 추가; k6 extra_hosts 및 커맨드 수정 |
| `src/main/java/com/on/eye/api/cheer/controller/…` <br> `src/main/java/com/on/eye/api/cheer/repository/…` <br> `src/main/java/com/on/eye/api/cheer/service/…` | CheerRestController에서 서비스 의존성 (CheerDbService → CheerService) 변경; CheerWebSocketController의 디버그 로그 제거; ProtestCheerCountRepository의 쿼리 메서드 변경; RedisCheerCacheRepository의 null 체크 제거; CheerCacheService의 로깅 삭제; CheerSyncService에서 수정된 메서드 호출 적용 |
| `src/main/java/com/on/eye/api/global/config/log/Log4j2Config.java` <br> `src/main/java/com/on/eye/api/global/config/websocket/WebSocketConfig.java` <br> `src/main/java/com/on/eye/api/global/constants/Constants.java` | Log4j2Config 신규 클래스 추가; WebSocket 전송 설정 메서드 추가; CORS_ALLOW_LIST에 신규 도메인 추가 |
| `src/main/resources/application.yml` <br> `src/main/resources/log4j2-dev.xml` <br> `src/main/resources/log4j2-prod.xml` | Tomcat 및 데이터소스, Prometheus 메트릭, 관리 엔드포인트 설정 추가/변경; actuator 로거 추가 및 XML 포맷 조정 |
| `src/test/java/com/on/eye/api/performance/scripts/rdb/simple-test.js` <br> `src/test/java/com/on/eye/api/performance/scripts/websocket/simple-test.js` | HTTPS 사용, 신규 변수·메트릭 도입, 테스트 단계 재구성 및 요청/연결 처리 로직 개선 (세션/연결 ID 생성, 재시도 로직, 타임아웃 연장) |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Sync as CheerSyncService
    participant Repo as ProtestCheerCountRepository
    participant Cache as CheerCacheService
    Sync->>Repo: findAllByProtestBetweenStartDateTimeAndEndDateTime(start, end)
    Repo-->>Sync: 반환된 cheer count 리스트
    Sync->>Cache: 캐시 업데이트 요청
    Cache-->>Sync: 업데이트 확인 응답
```

```mermaid
sequenceDiagram
    participant VU as Virtual User
    participant WS as WebSocket Server
    VU->>WS: wss://... (세션 및 연결 ID 포함)로 연결 요청
    WS-->>VU: 연결 수립 확인
    VU->>WS: cheer 메시지 전송
    WS-->>VU: 메시지 응답 전달
```

## Possibly related PRs
- **dev-4-team/eye-on-backend#29**: Prometheus 모니터링 의존성 추가와 Docker Compose 설정 개선을 위한 변경 사항이 연관되어 있어, 코드 차원에서 유사한 목적을 지니고 있음.

## Poem
> 나는 귀여운 토끼, 코드 숲 속을 뛰노네,  
> 새 의존성과 설정이 반짝이며 빛나네.  
> Docker와 자바, 설정의 조각들이 모여 춤추고,  
> 테스트 스크립트도 노래하며 진화를 맞이하네.  
> 앞으로도 계속 달릴 거야, 코드와 함께하는 즐거운 발자국! 🐰🌟

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpADK2VmgqyjcEj2wAbb5AokAI7Y0riQABS2AJSQ4rjedJAARNChAPTclABm+BTMmAwkSZAA7mjIDAHUiXKxsJ7YiJSQiPkUuGAMaNjoGPSo8EoY4lnwieXWdhhOAs0AzHMakACSYV1Ys+gMhYhN9NR1njm++CXwGESQAKo2ADJcANqwuLjciBxpaUTqsNgCGkxmGklBIwAAWMA0NBAkiyEhgfAYMACNAMADWZFoGR83jSCwAuuFnq93p9vrhfv9AcCSKCIVCYXCEUiUejMdjfHi5lElh5JpAcgxGtJ+FhMhQcnkCp4aIhxBdSj9zoceBR8CIxAAadDeCn4bBEWAtTIMeCjBiQJS4bTeZABIguWjnS4UzwMWCYUjIfJKfh8V38YSicRSZD4LIq8WS/IYQqxULIFyeDD4MK8fASQaJZUBxBONqyDTmSwAYRYbGGyEczlc7nqClY7DDWADthQse82F9aH8QRCcoFuV7mZIJQFauYKuFfFaLg6XR6ToCYnGYQDAlTkGnCiUVFU6m0S2WWACiG4iKa2qYu5UamtOg9yHTmaU+yw3QI+Ro9AC3G88nOJ0unlF0PTCHsAhHMdH0gWYyFiCh4CIUgAloXl6w3MIU1lOoDjLJQbFvdR+HFapE0TLBzkqEgK2tPxILGMdEFkOUaO1NhMGdFAwlofARWw3swAY0cG2YdRkApA4PSkHUqloeQ4JPWlGLodC3XLdgdUQfBIFo+AL1wtd62EsdARjfpE24P94C6AQElFf9Snqd88BYagbOHRjEwCHhuj2bU82Q0IuMkozKE8fIFM8M9RDNDymD6dR9IwZBsD6ZoSgQd0VRM1V8B2MNSNlJYjAAeSkKhfG1ANzhoKgxGS3ssgSMRExaa0+kdPLClobAfMldskvchUIOUkSzgpHN63wkhCP3MJmNY5htRo7hH3gAAvEL6w/Nzv0gDBqD6zxwxy6RzxSzxMHoAMiGwLNvHOEUBoDXL0wKosDH0YxwCgTF+AjXbiDIZR9rM9guF4QMNRDEVamvZR5q0HRvpMKA4AGCicAIYHyCoMGNOGLgqCY/MXAU+QEb3O9kd0MBDB+0wDAEe7vDQogqFoBIOAMJI+YMCxIAAQWWXHQcSPMa3kU73U9aQ608J0sgjWq1V6nZ0AOkSlEydLY3kAbmERdRci4iaEBbesAAMdcxMhTWkK3YO8fK0QBlUrZZ+A2Y0Dm0C5kgndGBIlnsE04q6XxZGq+tHvISArfgZg/xo9h3MRSAAHJ9I0MTKhYEg6o4POJ0LyghJIb45VcMB0zYV1Gkzp2YMU9BaFfWIdKyVFvYPGgVVqyv8ca06AFkbNLurM+QKxJ/qRpeyr3BXEHPgjYwE3EIVbg+vPJpEHQ1B/adcQM7ID1YxFAN0xh6fGDQbgVF78Q4fkNATjOBViIIBRfGDXp6AkAAB57wioXRCDAJI6VngXBuiBtSZRskaI+iolCOWFPQdem8uLXR1JQECnd8DeDROoJYAA5HSccRQlHCr2I2Uh6BDiNkrMY/RLaoCaA1RE2pAI2WGqBA4AZgFIAIbbPWDs7Q0W0JRYYqJwJ9AFGlLhh1vCfWLMLXUoNkpQJVEoBg3gXDpxSu7YB552iJCHDvOyHl2BJXll9SAZDtYkF1kMBgb9250C4InZOCRaJGKzjnEusDKDFwniEigFcl41zroXeeiAm7tgTl7H2fsA5W15vzL6TMEqjCIBkOeJBGgaFkMwbwPM+ZJAFpYEWYt8YSzJivGWF8vQKwUBgPJfUAnByOEOGB9d4mQBgmlXcRBETRVpMoPwzCI5GIPjAesRAXYoj8IgSoD8roKNpO/bAATB4UAkO/ZA0lPCt1QtgHq44WCQAAIwAFYWiiERLQHRNyAAMjyEovKWELLWTFgyNQGlbCgPgSAAH0emIGbhMVux86DanfnqA0RoUzGliuafwoLvKKxIKMcg9BzbKlIZAIwx4PZrKoJkMFuSkJQs+afDAMcZQuFIGEF69Z+lxKKcgIQgghkwpIPBWWFxEhZAnAnF2kdYD4DlBwAAnG8hVTsf5W1iXA+Viq3lWx4YlYCOCWgICyGy8VPZJXvyauFS+ndNZNAOTZZM0ISA/L+ZAXlAh2mdOHhnAaPYw5b0uAAIXwJuB+1k9UZxboKmRy9FAXPhWi00WRZD6vrhAp81AjSnStg/eAaQ5G7IIBQApsD4lO1wT2a0FBWXu2zVZDgAAON5jarY/PbklRE79/wItdXygaTA/4NQVKmmyyAxXXJ7AAEVds0aVA5GhcStimJQYBTG5DqtCmQUa26vmqiywuC6l3wlXeYig8r3las+gAMT6q6Cg3a3UeqQl0hlq9IBogAGyQBdv7eMcouLDsgfyzd8EqKdh3YqCkNr1mZHbHVQ5fhTrvM+c8ii+xYh7rCFmj9HB313Nwy2hZqAH1HK7iQI6p5NZWwA2CgIBjZjeGpYiPJdLOEvp/sHOqulwEjtguUCxlsSDwD4IdNg8zfnkDHA+vtQtaCZm0nwSN8E4U3R0v2lqaxET3jxlx5eI6ArhyTSm7jgHH6QazWkADdLcGVtZQurosmkC5AbU2i9RhL3nE7dHD2AQjY0DBSURCNAnY0rul6rAimsDYG4LQaoKmeD4Fqtaq2xJuAfGLQM7lGqFV5u4LmiQNy0gBfUIHBFCjiKga7CKK2RW/O0ZUCQBjIW6Xse9pxgMlnYKCYVDVmgGw+OMIE0Jg6jqD5GHUULTRYWdEBj0QYqbJiQFrosXwKxj0LS2JfogIwUAABSfKQvPsasp7xUA3VgpEyQLg09eDOmRMG3AiTlRWxC+lrlxTSneAybt/bTGn1he3V4hOp3BDncdVdw9K7Fsnse1gZ7v38lqviSUspX3IB7fdQd/7x2gc9oEKDtgV2P0w4Ti9xH3LkefYcejx9oWAnY6tsDvHF2rv2bk7kYncOOlIVe3AinqObA0VTJ4Gr6kue05fVFmL35vE+aF/5wLgckmc7yTzpHH2MmVO20zXibJImAlAWAEEFOKn80FrU0geNYstEadLCMwrWkGD5PbkUPoZQ6QDDbadeuWAG6N+rgU3tPDnAkEQmSAZj7tqwKdHswLQhzidueNbBshwe9QkgJ2trMyFFDgZvhUcmVfqegnU8lbOBbgwAkXYYA5T4CsnQDdnWlOeNoAgrKRoYqJrGBJMCKoBdOgqJp6RzREDSp8PsVyX48+ORL3OcvlfkDEQi5AUxa31CORr3XtCCzPBmLCPkKyXEBp99QD56RqVYwtNUmN6pGi6pzOtTN0Qc37+nWPftSxfw1vL+GHYrbDiYHii4AeKvgy5x7tBcBpTz7V4ECb5OzKiZ72rF50Dp5K464Yje7Jwyrwh+4o6ZJVLZJgBGBoHlz65YG1zZC5AxiFCQihDG54E37m4gz1L0CSwFjuzO5/5O6X7TbWzEEYEG5RiUHSg0Fyh84B72QVa+jh5tovqv4YCZhqgYC0SQCHKIQqCV6voe6hoZ6UBZ4kD6bopT7yBWwToBpgo3C3BloKJWw2AACiE6yw9gYKAAEqVPYNANqk5EgpaLikXlLioHxgApAMfgPhgOQMoj4feLaGpL2HKLPonjZMnv6NbNoY8nanGEvjMqMKKuKtmt4GUCxMqjpFbJAdIIgNAbXpkLQJ4YipQFxOJJ1tJPpHwL1P6u0lps0DPu0GJi6ggYUNqM9v7GzhQJ4UvqrLGj1K3t4ZISKA0acFgJjgEnmNlBMEnGgKQFeIPk9MJo6tqLvvAioUQk4NIKVvQOQLgCULkG7E0C8M6PMnyKqoUo0DoekZ4EvpLlbj/GIgNiqBJo5MpiTkMY5iMWkXoacSqGZLgrXi+gNGQOoVxLLv3CLkBo3gsZfvQGOpOAziuodHZPCFkGRrgMdAALyInwgi4VyFCSCUBFFA5gA0L/Bwl4kVy+bkkK6UmCYVQEYPEfovF6EomtwfH7Q/wzG9AJzALLxoBgqzq4B0rsArxAoykaB8EaD7IqI1FlaymiS4LMSyJAIClbqICPSGjxDyABDRjUDfitonzJSebdoSagmIGRwJD0CLqKBHpQ7roGlN4dzmzcRhglALHw6HbcLtj6Jdj1H5AbHtFD47FsB7FroHHfH2xd7gnnGXEUDXGFwgSjZjZm6Tb34/yP76KGIMphgRhv7LZeDWLrY/6bbbaOIiR9FXTN7eKs7AlOx+mLEMo8yQC6AJxrGkBcBECVCqn4BpDtnyYTlAnyYZJ9lQCc4dEUD46Xb3wOazkGDzkJz7FcBJCNr1p3LOaNpJBzn9k2wuKYiIBgqIiQxPFQqblnnplXHvC6TGyFrOgZIOJOL/KvEA60DeIQ6VkgldnBlha9lnmDmrl1xpCAWek0kPkLkJRLkrlcCwW77wVbmqqJm7lyrnpnpvJvInkIUJzfFXk3l5QZbPHEWLqFwZlogvlYLvkXCflQC2HyFCaIjKGqHwDqEigAkqqpHNngULmmHmF3DElCC0ACAMAcB7y4AczSCBDlKfBKkqlqnvwcB3JghzAABMaQAAJAAN6iVkJCyjy2EAC+p5C5dhDhThrh7hxJqlXuqpwwlA6pDZAucR7Q8WSeW40WVuQKgluh9qwlSB3lZe78BRdKgASYThWl4QEV5lEVGwENlljOAKLQmNRCmipDiPElrcp8mhXUUQBMkJBgAEnkYkCkmC40D0nsnLick0mQBxXYkMkaBlXwhkn1XFYcnUkjENm2FAKSlDIypan8XFG8mOmFBhVWwSlUDSljUvlJDOW66uV1QqIcAynEDVAFFEWa4EFGCIAUAMAWbSJpBCBoCHITksBpCIhpCwgkA5a5ruiCpFpIVqh/xFolj1CUBeW4BljDCfUJAUAaCXWHIm5VJm6iwW7iwsE27sHcFtIcFJIe4/VvX/WA0xpfXBYGK7Dtgh7eBh71hZF8KyERi5ghVxjJn6xXJYno2UAToCD2BU2K4qoM0UAs2vEEZhyGHOleY1SYE+VxE0DKFAoc1M1c16EN5nKC4MJBG/gGKXJ+mYAoBC0LTWii2aTi2/Wc2s0EYyY2kdr56RiISHL9x4rmrZFswk662S363emRYBXCk6TmmWpxhWwc1S32pwEpRQiMIRie123M362HwVCX7oDKzBjd5B5+2YDiABKnSU2/kYIQYWwQmabA3NAsw8R8TIACS1HJGeDCJ/pDpxKKBhhF1DZZBKIMrvzqCFhtIDKKD3y+DICEoCbtFZ13rW4rHIBWyspe2a2eED2FwTbeBD3UBQrgnPa60wKyi4CeF/q+BbjiCPQbQyjWyvWM0h3c0B4NYt5eHZQphjjni1Q6Kq3nBxFWpZpe2h3b78AVSdrtQgpiDHRBEuzfAWhJ31gfVEIg10LSLl7O6H1+moqk3hrGJFkk3l0sFISHREkUYp6/2Z1EICguwlBqIMEFllkP71izalnaILboWMIraf42J1ld4NkABqLgPFeJsQsgMGOV/525Zt1QAeKikAEtu9/J29FA9te9yoJZ+NQdGNoQWN3dvtCcx1p1+Q5wF1V1aAN1QI91j1z1E5utN1QN/9lAaQHNmNqDX1YNSjTsrVN25tRwHmfgd9v5/Dgj0tSSIj/dBjEjRjIN0jVssjZ1Cj4Nyj1IajcIGj/D2j2NIN+juthjOjxjfjGuWSEAhBBg3j8jGAij11ATqT6jOamjb1oT3dETb1AA6iQMzdOgDe45QCYxDfQdDXUlbqweTIjXLJwb/dwajdbBzcU6U2yOU9Ex44wHjQvvIaHpvZ4LLvBu7BcTpEoCzJcJ/d8AqCLanJWDnnzXaSqFQl+vgMhNtDvmqAvSsBOu054PlQVK48EKEE7M3fQEvmgmXIfRMDQivRMAGJs/M7s9bgIBSvANwGxu7q029Z3LlhaFNCc6yseOoBzWWGlLKVc7A2pE0K+SwpA8gA1knB5v3K89sws5cKdB3hHFsLg3Vi7YwLrb2Bc3KKhkCx5HmF85UD87g+bPqOBJNh80wP1LXbaY9EAX6F3Wg81KcJMzA3qN8tfvmXfrg9AziiWfNq/nBaQ9WV/htlQ1+apm01Kz4TKy/hWfK7y6thQ3EF3qULQq7u06gErFkEWAdQk0dSdT46k34yo3dZk0E9kyExtrIGkPPRI7rdC8MFU2gJDQwTDUwfUwjc0s08jRHcHiM21BA3sp3ZgNhN0kIbcQqOyic+mAvcsNUfvTbaCwnN63KFC/qMMLjeUPcdbAAAI5tWGulVtlidjMAYDhBpTwAXOQDEkIQhBRBWHJuMu0K8DKT6hGmUwsBqD4qig2rOj2SbO4InKomPKPzMGwbu6XGWgiJURhBUKVuoDO6Wh50HQhpR1iAqg12xh13cvSx8Cf0eQ/3qRKAIox29D9tEOn4FuICOpBFIXnAhDWqZ6jNDamwLPmo7wUCgJYPitaIGQasEOys6skN6vkO1mGv2JQAUI6gStEOwdP6EMGRyuIcf41nf6oft0mv+xOr0GHVJN2spNpP+O3WBNPVutaO/gyqbyetFsA2+ulu4AC57wccBtBu1Ow0rsNNNJ25I2O6tPNPHOFv7M+tvV+t8cuLseFqyC+11TdxxgxtE2jNIuzLk2awXMrzXO9gGIksEm4DugLpcclswtOzLi5AvK8Z7BTtZuhDtSz64L/RS4yhJzSAxHAfWM6bSqul4q0Dj0BqyBcf2Cl4TrVDQABcy1brjPxqILZRNDqaJBOcUAueZS0LXwKcDgRWWgcNlDIAKXVDJGq1Dh9jmo/w9j4vZGukRUJc0BJdsDckIDeiwOO29h/ioiJAq0upmdWwRdRcxfFe4ABq0VRpxdzjtckCdckBCx9BsW0BLcreeEZcoqCszFYwR6Xu+RUD1zNBzV9BbfJdBGk1Xz1gmfyAtaTa9jssucohucZyYRGi5il5BG+ccPiCibuweeUsxEOkPff55gUahQqhZfBiJAg9akLtNe/d+eQBVdtYXy8v1d+AqpteJfXflpYD/f9zz64Sw6YhXddefRcGyfnzSiw+56mg8v3tlfWi9i6bbJ+C1BUS5BmL8Jikk+i7VzSK4BMoBC1CF0fPXN3yIDwNVVBENEVv5Q8X7QQ+3sMCQc1I4PYf/PSvP64MEdLYKv6soe/4Nmjx9dpesNWy3AiLAB2c8cwt6CcORe+DRexfxcE9sDhBVtWCGLMDhBJD48dcBdJAxC3D5TvxU/RRe+h9sC9tJL7LacnOO9Ke8f8dqe5AadK7JPnWOsZMPWuu5Y5N6Nsdy/qdevTf2fDCZ8V/Z8Buo6W8it/neJ29ygO/V9O/DAu8Tfu9TdC5yizcXHzdx/LcBdre0Abcx++/++ndB8h/j9sDh+QCR+Rwx9eftAx/ah+8B9B+U/e9FAR9R/eAb8H/x8kCJ9EpuUShDfyeD/cfp8wt18ceeN5++NKNOtMfBOseqf1+uBV8H+NfFTgJ3U6N8qONrGjnI3z6f9C+WTEviE3L4cc0gIRKFqiHqAv8wBfjYTjUhDaW59o4nW3KS0jbSc3QEdW7s+zG6D1u+uAcIJH23jTcc2V/TujYWQKIA0Br1TAdn3La7AYimzGHkO0zAjtHIzpRIKPXwEkBgBAAaVhCFMfgZCHEOjXRDhBEeTAlLvBEVpDcCUPwTWM6SSyidqg0g2ECoMYG0Be2YdTDtByUj0I7uyYHEKS1EBuwM26PUNvtAxDyAO6HsagU/zLahdFASwQGk0AubDAu0GzbZh5AEEMRhB8gMgFD0SABh3Bd8eDIMAbqwRHusANUJ/EuAx47O6AkgBQlwCXpS2U/IBIUF+bJQ4CFNesO4NKATABIOQEZEkhCIoB86lCREChGXxAJRAeAbMOwl64isYitLIISEGGAc9EIXPIVmQMBZMAYWdNYImwMAZX1gG6JVvLQmIhKBu4PgLUj/A3pqgUAVQ9SLMNQDTBfAfA7FlxCWbKEYexwDBomFtCu0pESw2qHIhp49diBIqFoEnGsjZFn2NQgwS+nehlElc3gygMp08I51uI8WXrAnSjg6hbhr6JroUEOiIQdIBXHyD2DEi7AuINQ3iPxE3C6YgofAVWsAjKEMptQtGZNF/D9pkYfiuYP4MMM0gnC/Ar1dEJMIUAwstet+KwTwX154djERvcxCb2Q4kdzeqrSwfNhw5atDeCHY3kh2I7KtqE5HX0FNHNZmhLWEApmO/wdawDGOLrZjggK0bNkCmoIvId7WzzYCamuAupgQPDaScSBtPEVEMw9gcD6gZoxXM4wJojMzo9Cc1Hi0mSVQtmOzRZprWWbbCe8z4LMD4VmaK8OkqbLllxl2DrERQxLeIWq2mG8cqWOVeZLzU7z80C8Nw04FxB3bGsfIdzaXEYBuRHgWB/DLjnCxFYF4ZmBoQMV5y1qjCYebHQUbDwuQFQa6fgKiD5k0is9+G7I0YT6nR7UkxQ03Q5rUMkQ2C0IBgHSlWPJSFxgBdYsLgXnOA5BmxlwzSDDxpR5B4h9YG4gQiHFksZho4ocD2ER7TiKuWkB4XOKLCLAVgLA/ViPg5pXBnaiua5g2JKZNjP6LY0MYZHbBARPiKDXxPugzis9RMn7UgIqzJqApkGUw5oDlRnE+EEgVpAwGCCXEe59EZGCgOPVKh4A/OtAYAXSm/EDxYx24kMVcJ7y8JgIh4t0AkDoYKho8K9YcV0Fert0Jg1vV4dIBxToSiG1cMjGUiLxYsgxlwTwcnTBJQjbEnmXsOrA+YVQNwFfIgf+OZY7wtSrRLiMAm6EMpORE2LDjBz16asDeRDAUe/jIbyjKGaHRxDpELqFljJcHbVp0MI6WSlW1ksjj5FNYqjkAFrK1vE01G0cYB6TXUUX31EvVDRrNY0ZzVkCxg3RQnS0cLDwFw1rcUsJpo6Kbp9cvGsUhgKYWgD4ASweQ6RmjV1r2AcpbongccgFTwRbucWfdgGAKKQiAgnPUMGzx7CYkPYiPYAVwNcDdcToiEEDn4H4E95RB4XQCJN096LdD+6gpSIN2Vo6CeweglVH328Ae9puw/GhGQAW5b9D+k/aftNIsH7tC6z7Zdo6jqjppdgqYs6KAOz5+DD6J9MMmBi4hfdYev3HzgokB7PREJeUBelMRWI+RBAHRDErkTX7R9D+VwNegfAIAxZZA203ACt3CCJ9cEtvE/jHwhnewoZigNALIA24Ize25I86ACikD/gLB8bF9I9AxCORTE10Z9lQA+GnR/CYwsYPLQ6kzZqAARS8JHQJKDpLg0qUyGeIzHoAfIupd0IoU2g5FrkbM60O9zdwQk8h+knXkZN0S4d4OLk2UUR3cmkcGyGHeyZK0ckqznJlZIUVZNI4liIoFHM1r5LVH+T8CkArUfRy/56iNGSyQQO/G0Yq5P6aQSPkQDBBCAdKgNPJAlM1widXBDSNKRGwykGBxMIkbso1A9EXZXS3s32f7PhwzTYMasONGwkjCog0QSYknCwA0CIgOqcILQLll9jLJ34AIeHBoE/rdc92gzFoS+1TBW4/SVsBtqBSMSeE6JAvGHsRAjFKAqwN2BULHKLmQA5Bk0PoQMwrbdo5eFweyLMFVpjd3mFwMqUtERlwFfJvhfFH9KNDNS+oxiS+nHRvqB1vZOLVeaLT6kJxT5zoc+TRF9rX0dOYYAQEDK3CzzQIjEitlswo58BWeKMn2X7IDlIRKp/pBrJa0IzIAbiUWOEacFHTfTB4fsIzknL9mBicWadAtn6hwRWQ1sASJChKQVmGSoG+sqUWZJlGdiNZBrUUehxjkNz6cSClOWLjf5BSP+IU1Rk7OyYuyVk7s7nJ7LoWAKiA4AqhWODM705Teq/c4SvJYii0UFN8yRTRHXlOMG5f85OXwsYXQDmFDHVhWFOdkVzcQL2Hhds2UXVzYmGoxJvbIL6hT4BuaDhW7JewMltIPTNIF03sBlM+FQc03FaP+Fhy2CEch3A6K9DpyJiV0UbllMxwkAnFZTaAHTLPBrpip1scJT0xUVTzdgO8tWinFojPsrY8SjELgEt6JjSAAaNUOgT4XHQQSyfIbqTL64n5zoF4eAAwz7SgUuIWSwuAhEwDRLha2ZO4gYVzFyTBQ86FiVgGgl5y5eG9QvGJHGoKIWYUdGcOLMrZgIW+NxZ9k0AmXYAplnwkZY9DGXWobkOletFIIDQF5BlsE4ZZ4A2XfwdIuy/ZUEUpofSAuoys5ZAAeScIUM1pSPOs2IgLLNYBAOiLBBWUElpl6y9FphgjB3Jtley19AlCFAUAAgoww5Xs3yhlFnQCLUXJ6hfzlCxIIygMBfC5hcQ8W+QFerCqpb08rUghKUFalqD2x9QVAVBd3FYgrY1QBURFWK214EKeRJkvkeWTVlkK3JFC+sg4mb5hdW+25YUSHkGA05joTS+GVEt3zhAJVkS1pbvgFzRIAk9oERGFmYFJL+6EqxJU9jMU6jNFlir4Doq4X5I7FZTRxSU2cUJKjFpjExbazUXaiWFzrLRewqNUJRr6spfRheA6iyk3FUNDxaHPhrhy7RkcvxbHUJqtT2sigQziPAOEJwSwpUGwE4SFi3BbgpUQpmCluCOEPCQsqgB4KVAsDAh3qulM41WbdKTaQiIBJuwVDyljCK1F4G8DSyPVmQGgNEBQBPL9dmpQ+egBCOOGCsCxNCbtW/BdQ1qBidakkGllnDtAmAJQEQpoHbgpQSEFwKucwBPIWCjujUamX0GfaPQBwrEjBstiQhLCBoJYNUFXlKgDTlQXlSlXGHsAeg2i4QeNYmpiABAKWspcXiU3kDigxlBCF9f2C1Ksz6wfxYAokF4gpNeJj9KZKsmXgXJEGJ0WNZ7QTVJqU1aajNVmsc6PDjESiFYWnV+IiQa1nw3xG/DrzpQ4sFaqtXMxET4LuReDXkarKNlyjNZlC2yeKIcnKziF+HUhRZLglm9NsZs3SBRxtla5TFTC1JqeGvXSAcsYaIxHQWDn+qJBga7xcGt8UoMxcIZLAD0gTihpsFek/3MMnShVpxkjyJ+tMijXmg5kzqB0qxgQl8B8pzAYCGKv+xL5lMK0XEnHHfmQBR4s3VWiqurhvwFEzXSkaBDkiPI02RAdujoPyCVrmATgG7ucCTgxazwSYOoHJHmRkopZ5QcTQ5qMQHK4t0WycIMHsgJRwiuDBdq3H7FkY3OHUm5NajBBpkRIp0s7hQFgV8ACt/nNgMyxu5oAotMWx6FZ1uWlat04xdWPGmPG7NJwP8d9ARU1AEUPkYkXwEgCeRbqIgSGMSNMFlAxBcENyTUI2jeTTaCKukb2DusW0udwgcwWbb+3W2xETQsMEmQ/SK0AoM4n0jre+wPlhAdKu2mbQdvm1PKlt4Qd7chi3VRBtQPa/9am364gNrUPYFMEiGrgIkXEp4NOHpJJRRyZCXLUIe1kwBJjlCmIM+pWDVlxDC8A4JfJuo7gikOwlWNhgVWeJBEqMxmaekESh1NkHt6mvpHeR0xppOVYQJzc3hc3wkv4mgB+puJ0iAj8aMEEjIu06pb5bg5QU0t2mXmXAR5WATZkCkBAaANoXWroaDQQC5zEIGgZwvAB13wBElS+cGG5UYR4AdUIEghFeKFw/5zU+7ZUPLpUKUBlJqQ9lAcBLVMquREoohaZI42c6qyoihUX/kEVZaX02Oaip+GAi5wF5GAHzcvELCS6uA0GxXDqpE1pAxNfUHYJJu03JQ+ckelgNHopApbc4XWrgO8lUX2sM90gcTYgBz3wSi56uAvXZuoAaBi9NIg+KturzLtVydySvXR0z0nUJNWmhvRgHz2YUycxSSzB1U9IdVXNgOFPf3vOqD7s9I+yBuPogqxiNAIug+Enu7ap7Yc9slfcPqwWj6N9DiYpf9hYazV/C2kLPU6m110Nc4uWpwGAFa3l6l9CjY/XXrX3Sb/crVW/eJo0CP7ddq2+LcwDf0BwuAYIT/aJpr336f9p+9fU3swoxgsd7AOfbQFx0+r2qq6OIa5XDJKAuA9QRFLAE1BC7YD1eu/UPsQNSadNKOFqgnDQOkBaImB7AwfFwOLZ8DMxYg2Rl1BkGhdmoSffAksyUHv99e5A7gT7IX6O5L6a3rNUsCq71dQCTXcAYN1P79dhuvhVwAnS2EA0VwAAOJiH4DNBiQ3/twLWtAp9qqg7XrSCf1k5huWkBoCARlIcBSU60V4saY+L7E0cscPM2aCK7+uAJZwY/HRB5yrYuQfhWeH9RipHUdFDQBuFTBaA361QJeh0v53IAndCQYmUlmWBkJL0pUOuYmFR0ZwhtcaNqM1w8hO7ZgTRIcOylQChHc50ZTtIWP53Wougvzd+rgn/EEr0AYUFYPkdKhfpJk3gFJbeIEiCD9IjQEQaBV6FhDxJIWqLOBpLoEJq4MG9+qzyd2BGXt5+EBggh0EBgHS/hvgLMHqJuUEFvQjVlbGACnzKAiAPQDoQiKs8AAGqPFuBh7AUgeKjT7rY1+7+RnGwPcKOD0NlbjRI1sgnBuPYtmgF2YkkkEiMaBojzoWI2wHiOJHNA+aaoMUGyMNZYTeRgo0kDSAPHc+6e8Q/Yb9mOGJAzhiwwFOE3WHSTBi8k+mDQguHykiUxgvJtSmKb3hymsgbJwLZWwyTOlWuGrCpOfYPj3qQPKqDWKIRHIunGSBaVC1twhAjQXAOkquU7QSjUeQGC6mOOlrZk5a+sIgllChGroaoRoQGH0TscFQ1oMLe7CaDGbC2lpdytLtkDMs62CcOAKeGlRsx3Mk2J2A1lDFVTgM1gjMCNuXjCS44HzV4+8YVMEImWeAcUR83PbKJ66QBF5Ze1CGM6/DUJvgECnhOImLgyJ0cFcQSP3ZkjRJVI/1zKPzTIMpqcRXMxGMhbcjgxoo9ukjwuSaZ8x1BR0afjcsjWrPX/S+h57k6nQCoT+qOnFQBgMFCoINCGhSOFoUl6wPKC+Fjpy8TSQzDVoOeSh3wSD/Bm7nTvA1kyiGvEI9uBBPb9GVjRY+s75Rsipl/A/9UMzOElI0BvgJxXlgGEfPP01jb9AGbGq2OyG8942RWYQt+PsriG6s7lTxpVah7QTgq647BZhNwmq0CJoeUQCLOomyzGJmgFiZGO4nBjBJok2nrpMmHs9gp4U/4JZNxNbZVhtIAvQdlwDi+uaElVQSerfNfmdevLgIDSBy9wJs6sGu8DZPJSxOto7k/YlDVOiLT7pKU20G9gARhmenAzmZr1kZ1VN/2UbRcAOI/dX6sGtkQcziJehwSRKwoGqY1a/rPOWKtzfud0yAZJS6IRFQ/StgBohY9gWwmJUsL9cWG1qYUJAGcLQBoAVgewO2FYg/pTovl/y+CSzPimsA3FXinGucK2FbCNgMFHYQACKVwWwu4TBRWBErYKFy/GrIQTo051Zq6X/Vx6AaZgzQZpICzMuUtHkow8UADq3xtI9LNpkUANvghrmN4ZmkIW/EmzxpNBpoIdFsJ+b2QR8a6Q3Gpq87+K/SBJGhD3RdgipIkrRJYq1e7wHBTu3AfypDvQxVpmlp0HSncn23Th5kpKTuik0UQXtkoBeDXq+iWWjmFdZLGq0j2qlKRvzsGuLKKTrO14gJd170MNZTjWA3C0Acln+qfDD5jtT7HwnihqDGESw8VnK6lfSuZXsrSVvK6VAKsEZbC6AkG55wekHcTuZ0u45oQC4dbrM6xHFjEUoBqgFM10Sy2MWThqh5afpBPZ0GoDZQ7IrsbyMUJVBhWrAONyluCWWIFQ0g3cb2O/Q6x9pHrfYBMELOTCCtbLGIegH0suA2FKzEtocC9KFtlFexQRUWy6X5s+rkdhtV5QaZ3z/1/0ogC+EgEnDvFPxH1kc0Eqe0JmybCx5wQYdsLA2nrQE5qeMNanXiRam7SBIMO7FAjWeieSy09YXx3DtILStkNoMgxa38auCPW+/XqS5lyokGgvCjSMv6dkzl7VIbgjVAsw5Q5AfGmHYoKkq4wC9C4fS1+ZpDUljNuHa+v5R9A3NK0KFUwkx0sH2A4JDrArcZV5lmV1GyUX8Y5X0byFUFmybQzUIMN6ccNhK0lcRsZXoAWVnK2jYxvEnTqdF8xfqsYsZAK7LF7i7XY9WcXuLXw8qgvX4uo5p79DeyPTn4aOBtgZRQiEFk3u0X0gO9p1QauYvSgj7iEdixnqkpn3eLl9oQPeSgA33Yr993WpehtAv2D9MjO1tvb1Vf297P9y+H/YZYcWgHPFlOHxbAfX26GUD8E1hV8CP2Co8Dyvcg8dXf9sm6DwoJg4Aen3cHF92ggQ4cSQPZ7JD8O7A+9iUO371DjRag/Cn72JQQhDB2xZPs4Pz78IUB/eUsO0n37coeixYrQcH3f7kjuvaaocUsPZHbDgS7JvcOeKFNXhpTWJfwZqiAlw2iBf6PNRHmlZAYCVb+lWPH2lgMgymG01FLSEjaWp2IOuzzvaIBinilmliMRC1sadniwGsVuSi1tFzVQfuG2w7b7w5eGcXBPdoiLLAJ0zW5fNjYUIVm/A24cINQyuA8gH6Tjiwo3M8s/xvLTXboT5HScvpwg1WXYFbA21lZYwfPXIGRFw0lBHIxjx5KE/fAKIGnjUTJ7MocHNB9kEzKpcNuBnXIzt+2ubUdq+Q6IFgcwL7cs5QwIoP4h+JhLkCOBmXRhw4yO59G/Ls6eMSW4q4fUkJ2ZSpId3YJQ4GL8NeH3gR59uX/rkPn7qRmejw7gffOj633eqG7DpFP2k7CiFOz5DTuI1qr0t2q7gnDs4qio2iSm5WtLoK7EQ0TiNLTavNf1G5tU61KKRZH0VX0lqtEDtkCuwrGGmQA4pUf5ot3sVVIoaOvQCQEr2IZRPOTvudCGWO7a8dl16BiKUuLLOLjyKgBztxZl2iLUl+S4TEwS+KCiZg/pyWZsiRnGcI2D5FxTcybtjdUgWMPkBsBhU1t19HHmsQj5GlFqspu0kxfGJUAVTnSN5Y9xGlBU3AIOJy1KPUiQrgdG4itzdM8IGbGYOHf7F7MF3hnRjCDUXV1doTsZMRFVy2BJsJnRXw1O2HM8nA3IwQHyH7S5x/gPJVtPQg4i0ayGvoFrHQmNzwDuMiJ7YTqSABncqijGISbTHO2e1dcqJg3P4QQCqdLscrHH5rnps45rv/3NAXugycPd91gXzJgJk2UxuvSXXUnJDgJxnGCcIqwntAeRURaUe4AVHu9kR/Q9YvH2tHJTexdkuAd4O5HqOKdxEXpxzusAkTjF8zpzYrvD9SDj+yg9ocl9t3jDj1do8Pe6P8H4DjzcZjgtuq1gdz0F4gHgedsXU8D8IJnH4ZgpE7V5epJnCiAABuKh0+5odsLX36jiR7u8Kz7uzV37k93yv/f32vVEzigC87A9dsHSkH6D7rQhQ2gaM1QRDyh4EdoehHL7pi1h4YeaPcPXzfDzI5/dN9iPJDwD75W8CfOHnHDKjyJBo/h3YP9z+D0x+Q+oflHn9jj6I4tLYf+3e7vjzo4E+EeoAzfCBAB9I+/O+HUniD9UCg9ye9bjHmgMx5U/ru1PGHzj2I8rs7vtPvHg94XCPesPRC7DqAGe5fSdWFe1+8Up6ShvdBdQF15RIjMgCGVHPG74Rxozfc8fP3Pngj/o7Mbhfd8kXrYTF8acxAEvrH1T8+5c8afxH3HnD+l/XeZf/P8jgKQzDRjf4A62MQgP09N1l4SYnJxpvDHdLUxSE2gOmIYGa9mR1AYKQYPB7GgDrYPv3b6AYGa8CB30cwEgAAHYtKcwVb3Kh0q4UdKaAHSrhjeTSU3kaANAHQBuSreGAOlAQPWh0pkYbkWQOYHKgtDzfRv5Ycb5N5ozTe6AYKf6PoCAA== -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=dev-4-team/eye-on-backend&utm_content=33):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` or `@coderabbitai 요약` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: smart-cau
created_at: 2025-04-07T09:10:07Z
body: @coderabbit review
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-07T09:10:11Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE