From 3ae75af71073091343f35b0385faf1ca865d85e3 Mon Sep 17 00:00:00 2001 From: Junkov0 Date: Fri, 31 Jul 2026 15:29:04 +0900 Subject: [PATCH 1/2] feat: add Redis/Kafka boilerplate for distributed environment migration --- docker-compose.yml | 33 +++ k6/problem-ai/config.js | 8 +- k6/problem-ai/load-result.json | 220 +++++++-------- k6/problem-ai/soak-result.json | 262 +++++++++--------- k6/problem-ai/spike-result.json | 260 ++++++++--------- k6/problem-ai/stress-result.json | 226 +++++++-------- .../api/auth/config/SecurityConfig.java | 4 +- .../src/main/resources/application.yaml | 15 + momogo-batch/build.gradle | 4 + .../momogo/batch/config/ShedLockConfig.java | 22 ++ .../batch/scheduler/BatchScheduler.java | 3 + .../ExpiredUserCleanupScheduler.java | 2 + .../src/main/resources/application.yaml | 15 + momogo-core/build.gradle | 5 + .../core/common/config/KafkaConfig.java | 18 ++ .../core/common/config/KafkaTopics.java | 13 + .../core/common/config/RedisConfig.java | 22 ++ .../src/main/resources/application.yaml | 15 + 18 files changed, 658 insertions(+), 489 deletions(-) create mode 100644 docker-compose.yml create mode 100644 momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java create mode 100644 momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4dcd8ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + redis: + image: redis:7-alpine + container_name: momogo-redis + ports: + - "6379:6379" + + kafka: + image: apache/kafka:3.8.0 + container_name: momogo-kafka + ports: + - "9092:9092" + environment: + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + + kafka-ui: + image: provectuslabs/kafka-ui:latest + container_name: momogo-kafka-ui + ports: + - "8090:8080" + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092 + depends_on: + - kafka diff --git a/k6/problem-ai/config.js b/k6/problem-ai/config.js index 7e3bb9a..b88b126 100644 --- a/k6/problem-ai/config.js +++ b/k6/problem-ai/config.js @@ -13,8 +13,8 @@ import { check } from 'k6'; // 응답 결과(200 OK 등) 성공 여부 검증 * ============================================================================ */ -// 1. [서버 주소] 현재 배포된 AWS ALB(로드밸런서) 퍼블릭 DNS 주소 -export const BASE_URL = 'http://momogo-alb-1906718718.ap-northeast-2.elb.amazonaws.com'; +// 1. [서버 주소] EC2 + Nginx (momogo.kro.kr) +export const BASE_URL = 'http://momogo.kro.kr'; // 2. [테스트 계정] ADMIN 권한 + 공간(Space) 소유 계정 export const ADMIN_USER = { @@ -23,8 +23,8 @@ export const ADMIN_USER = { }; // 3. [테스트 대상] ADMIN_USER 소유 공간/카테고리 UUID -export const SPACE_ID = '9d683a9a-9e5d-4a7c-9230-b40f7c62d31e'; -export const CATEGORY_ID = 'effa0724-42c5-4335-964c-1e2556007e1a'; +export const SPACE_ID = '07ddf318-4762-45b4-84e6-ec6be69d4f14'; +export const CATEGORY_ID = 'e52067a1-8e66-4ec9-a343-927405c3b69d'; /** * [JWT 인증 토큰 발급 setup 함수] diff --git a/k6/problem-ai/load-result.json b/k6/problem-ai/load-result.json index ce944ba..66ad2e9 100644 --- a/k6/problem-ai/load-result.json +++ b/k6/problem-ai/load-result.json @@ -1,166 +1,166 @@ { "root_group": { + "name": "", + "path": "", "id": "d41d8cd98f00b204e9800998ecf8427e", "groups": { "setup": { + "name": "setup", + "path": "::setup", + "id": "5c0f8025f7e0b6654089e5b00e950f1a", "groups": {}, "checks": { "Login Successful (HTTP 200)": { + "passes": 1, + "fails": 0, "name": "Login Successful (HTTP 200)", "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1, - "fails": 0 + "id": "bedba4f0acf48db4a1e0b99e768818d4" } - }, - "name": "setup", - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a" + } } }, "checks": { "AI Problem Generation Success (201)": { - "name": "AI Problem Generation Success (201)", "path": "::AI Problem Generation Success (201)", "id": "2641500d9aae22ca0aeb735974979037", - "passes": 74, - "fails": 4 + "passes": 67, + "fails": 8, + "name": "AI Problem Generation Success (201)" } - }, - "name": "", - "path": "" + } }, "metrics": { - "error_rate": { - "passes": 4, - "fails": 74, - "thresholds": { - "rate<0.05": true - }, - "value": 0.05128205128205128 - }, - "http_req_sending": { + "http_req_blocked": { "min": 0, "med": 0, - "max": 0.5059, + "max": 51.8237, "p(90)": 0, - "p(95)": 0, - "avg": 0.006403797468354431 - }, - "checks": { - "passes": 75, - "fails": 4, - "value": 0.9493670886075949 + "p(95)": 2.283725, + "avg": 1.0608315789473686 }, - "http_req_failed": { - "passes": 4, - "fails": 75, - "value": 0.05063291139240506 - }, - "http_req_blocked": { - "p(95)": 0.8032699999999544, - "avg": 0.9820379746835444, + "http_req_tls_handshaking": { + "p(95)": 0, + "avg": 0, "min": 0, "med": 0, - "max": 41.5508, + "max": 0, "p(90)": 0 }, - "data_sent": { - "count": 126051, - "rate": 505.8668829872479 - }, - "http_req_connecting": { + "http_req_sending": { "p(90)": 0, - "p(95)": 0.8032699999999544, - "avg": 0.6721151898734177, + "p(95)": 0, + "avg": 0.005593421052631578, "min": 0, "med": 0, - "max": 17.0669 - }, - "vus": { - "value": 1, - "min": 1, - "max": 3 - }, - "iteration_duration": { - "avg": 8400.927180769231, - "min": 4431.3608, - "med": 4720.69545, - "max": 50535.0224, - "p(90)": 15569.640969999991, - "p(95)": 30778.169084999983 + "max": 0.4251 }, "vus_max": { + "max": 3, "value": 3, - "min": 3, - "max": 3 + "min": 3 }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0 + "iteration_duration": { + "avg": 8913.08115333333, + "min": 5388.5987, + "med": 5782.1383, + "max": 48739.6799, + "p(90)": 17266.536480000013, + "p(95)": 25330.858449999992 }, - "http_req_waiting": { - "avg": 5332.521148101266, - "min": 103.6248, - "med": 1720.0426, - "max": 47533.7525, - "p(90)": 12265.669140000009, - "p(95)": 27687.98786999999 + "error_rate": { + "passes": 8, + "fails": 67, + "thresholds": { + "rate<0.05": true + }, + "value": 0.10666666666666667 }, - "http_req_duration{expected_response:true}": { - "med": 1717.5031, - "max": 29299.3488, - "p(90)": 9299.28142, - "p(95)": 12569.229029999991, - "avg": 3606.2454613333334, - "min": 103.6248 + "http_req_failed": { + "passes": 8, + "fails": 68, + "value": 0.10526315789473684 }, - "data_received": { - "count": 181631, - "rate": 728.9201023701265 + "iterations": { + "count": 75, + "rate": 0.30204230617178024 }, - "ai_generation_latency": { - "min": 1431, - "med": 1720.5, - "max": 47534, - "p(90)": 12569.199999999992, - "p(95)": 27777.649999999983, - "avg": 5400.384615384615 + "data_received": { + "count": 170301, + "rate": 685.841423778138 }, - "iterations": { - "count": 78, - "rate": 0.3130289872591676 + "vus": { + "value": 1, + "min": 1, + "max": 3 }, "http_req_duration": { - "max": 47534.4313, - "p(90)": 12265.857620000008, - "p(95)": 27688.06979999999, - "avg": 5332.896022784809, - "min": 103.6248, - "med": 1720.0426, + "avg": 5839.168152631579, + "min": 369.2119, + "med": 2774.7471, + "max": 45739.3552, + "p(90)": 14135.55285, + "p(95)": 22216.819475, "thresholds": { "p(95)<15000": true } }, + "data_sent": { + "count": 118696, + "rate": 478.0161809782084 + }, + "ai_generation_latency": { + "p(95)": 22330.399999999994, + "avg": 5912.68, + "min": 2388, + "med": 2782, + "max": 45740, + "p(90)": 14266.200000000012 + }, "http_reqs": { - "count": 79, - "rate": 0.31704217940351587 + "count": 76, + "rate": 0.30606953692073735 + }, + "http_req_waiting": { + "p(90)": 14132.735349999999, + "p(95)": 22216.819475, + "avg": 5838.56733026316, + "min": 368.8114, + "med": 2773.65715, + "max": 45736.7462 }, "http_req_receiving": { - "p(95)": 1.50718, - "avg": 0.36847088607594924, "min": 0, "med": 0, - "max": 1.8783, - "p(90)": 0.94332 + "max": 10.785, + "p(90)": 1.00765, + "p(95)": 2.27265, + "avg": 0.595228947368421 + }, + "http_req_connecting": { + "med": 0, + "max": 10.274, + "p(90)": 0, + "p(95)": 2.283725, + "avg": 0.5076039473684211, + "min": 0 + }, + "http_req_duration{expected_response:true}": { + "p(90)": 3831.9011100000075, + "p(95)": 8887.069804999994, + "avg": 3544.213716176471, + "min": 369.2119, + "med": 2739.2021, + "max": 23924.7887 + }, + "checks": { + "passes": 68, + "fails": 8, + "value": 0.8947368421052632 } }, "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIxMTczLCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMTkzNzMsImp0aSI6IjIzODMwZDg0LWFkZmQtNDg2Ni1iNmZmLTIyNGM1OTYzOTU0MSJ9.AI070Smmkh9iEhirwSCgCyHs7vY6I6i6FKezWwE5RA8", - "csrfToken": "bb86b3a1-9911-4131-99f3-64c75d44f20b" + "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk1NzIyLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTM5MjIsImp0aSI6IjVhMGVjNjA2LTU0MTEtNDY4MS1iZTllLThhNGQ4ZDVhMTk3NyJ9.bIokWJI1Yo6F5RvEf0eMM6xl3V7KmVFCmIMf8ArwSSE", + "csrfToken": "6319c265-b924-4f9e-8866-c4294bd6391a" } } \ No newline at end of file diff --git a/k6/problem-ai/soak-result.json b/k6/problem-ai/soak-result.json index 2eba3c4..a45a3a5 100644 --- a/k6/problem-ai/soak-result.json +++ b/k6/problem-ai/soak-result.json @@ -1,166 +1,166 @@ { - "root_group": { - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e", - "groups": { - "setup": { - "name": "setup", - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a", - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "fails": 0, - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1 - } - } - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 114, - "fails": 4, - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)" - } - } - }, "metrics": { - "http_req_duration": { - "avg": 6431.187930252105, - "min": 92.5452, - "med": 1865.5074, - "max": 60000.4346, - "p(90)": 13880.99628000001, - "p(95)": 38808.42451999996, - "thresholds": { - "p(95)<15000": true - } - }, - "http_req_receiving": { - "max": 1.5068, - "p(90)": 0.9184, - "p(95)": 1.0269199999999992, - "avg": 0.2940235294117648, - "min": 0, - "med": 0 - }, - "checks": { - "passes": 115, - "fails": 4, - "value": 0.9663865546218487 - }, - "http_req_duration{expected_response:true}": { - "avg": 4568.101827826085, - "min": 92.5452, - "med": 1804.9854, - "max": 49466.0998, - "p(90)": 6804.930180000001, - "p(95)": 27549.091109999994 + "http_req_failed": { + "passes": 0, + "fails": 164, + "value": 0 }, - "data_sent": { - "count": 190571, - "rate": 263.68869497938397 + "data_received": { + "rate": 542.7608833367937, + "count": 393261 }, - "vus": { - "value": 1, - "min": 1, - "max": 2 + "http_req_connecting": { + "avg": 0.24038414634146343, + "min": 0, + "med": 0, + "max": 15.4775, + "p(90)": 0, + "p(95)": 0 }, - "ai_generation_latency": { - "p(95)": 39217.09999999993, - "avg": 6485.4915254237285, - "min": 1482, - "med": 1867.5, - "max": 60001, - "p(90)": 14263.19999999999 + "iteration_duration": { + "avg": 8327.549047852763, + "min": 7400.9275, + "med": 7757.7225, + "max": 20045.3774, + "p(90)": 9240.39226, + "p(95)": 12493.147110000004 }, - "http_req_blocked": { + "http_req_tls_handshaking": { + "avg": 0, + "min": 0, "med": 0, - "max": 31.4853, + "max": 0, "p(90)": 0, - "p(95)": 8.950599999999998, - "avg": 0.8108773109243698, - "min": 0 + "p(95)": 0 }, - "iteration_duration": { - "med": 6867.6497500000005, - "max": 65001.2099, - "p(90)": 19263.86304999999, - "p(95)": 44217.79187499993, - "avg": 11485.922727118656, - "min": 6482.2853 + "data_sent": { + "count": 257736, + "rate": 355.7154638463815 }, "vus_max": { "value": 2, "min": 2, "max": 2 }, - "http_req_connecting": { + "http_req_receiving": { + "med": 0.158, + "max": 10.7239, + "p(90)": 0.9932900000000002, + "p(95)": 1.0894999999999997, + "avg": 0.45652682926829263, + "min": 0 + }, + "iterations": { + "count": 163, + "rate": 0.22496516050128887 + }, + "error_rate": { + "passes": 0, + "fails": 163, + "thresholds": { + "rate<0.05": false + }, + "value": 0 + }, + "http_req_blocked": { + "avg": 0.6593865853658536, "min": 0, "med": 0, - "max": 13.133, + "max": 84.1939, "p(90)": 0, - "p(95)": 8.108739999999994, - "avg": 0.6090352941176471 + "p(95)": 0 + }, + "http_reqs": { + "count": 164, + "rate": 0.2263453148601925 }, "http_req_waiting": { - "p(95)": 38807.87416999995, - "avg": 6430.889257983194, - "min": 92.5452, - "med": 1865.5074, - "max": 60000.4346, - "p(90)": 13879.967000000011 + "avg": 3308.328158536584, + "min": 368.1503, + "med": 2756.7853999999998, + "max": 15044.6296, + "p(90)": 4238.19987, + "p(95)": 7466.804339999997 }, "http_req_sending": { - "max": 0.5532, + "min": 0, + "med": 0, + "max": 1.505, "p(90)": 0, "p(95)": 0, - "avg": 0.00464873949579832, - "min": 0, - "med": 0 - }, - "http_req_failed": { - "passes": 4, - "fails": 115, - "value": 0.03361344537815126 + "avg": 0.023870121951219514 }, - "data_received": { - "count": 273211, - "rate": 378.0357559335496 + "checks": { + "passes": 164, + "fails": 0, + "value": 1 }, - "iterations": { - "count": 118, - "rate": 0.1632738769674678 + "vus": { + "value": 1, + "min": 1, + "max": 2 }, - "error_rate": { - "passes": 4, - "fails": 114, + "http_req_duration": { + "avg": 3308.808555487803, + "min": 369.6553, + "med": 2757.2959, + "max": 15044.6296, + "p(90)": 4238.50197, + "p(95)": 7466.804339999997, "thresholds": { - "rate<0.05": false - }, - "value": 0.03389830508474576 + "p(95)<15000": false + } }, - "http_reqs": { - "count": 119, - "rate": 0.16465755389092093 + "ai_generation_latency": { + "avg": 3327.0490797546013, + "min": 2399, + "med": 2758, + "max": 15045, + "p(90)": 4239.6, + "p(95)": 7492.000000000003 }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0 + "http_req_duration{expected_response:true}": { + "med": 2757.2959, + "max": 15044.6296, + "p(90)": 4238.50197, + "p(95)": 7466.804339999997, + "avg": 3308.808555487803, + "min": 369.6553 } }, "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIyMjI2LCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMjA0MjYsImp0aSI6ImVkMmI0OGFkLTJhMWEtNGRhOC05MzA4LWU1ZGQ4ZjFlNGUyNCJ9.ZOAJ6NwRMCJjxx_YALM3riXUqSWDA_EVtc5H2OXAsog", - "csrfToken": "5f29b32c-6bc9-46ac-9524-37d2cb81e734" + "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk3MjkxLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTU0OTEsImp0aSI6IjUwZDJjYzU1LTMzOWYtNDRhNy1hN2Q2LTAwMzNhNzM1YzhkMyJ9.pUT-u04j5_PbtX3g2zrOGDHF_mk5HeAHdu_gKYKsn_M", + "csrfToken": "b2235495-97a4-4b8c-876d-3d6d205a4d50" + }, + "root_group": { + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": { + "setup": { + "groups": {}, + "checks": { + "Login Successful (HTTP 200)": { + "name": "Login Successful (HTTP 200)", + "path": "::setup::Login Successful (HTTP 200)", + "id": "bedba4f0acf48db4a1e0b99e768818d4", + "passes": 1, + "fails": 0 + } + }, + "name": "setup", + "path": "::setup", + "id": "5c0f8025f7e0b6654089e5b00e950f1a" + } + }, + "checks": { + "AI Problem Generation Success (201)": { + "fails": 0, + "name": "AI Problem Generation Success (201)", + "path": "::AI Problem Generation Success (201)", + "id": "2641500d9aae22ca0aeb735974979037", + "passes": 163 + } + } } } \ No newline at end of file diff --git a/k6/problem-ai/spike-result.json b/k6/problem-ai/spike-result.json index e44bf7d..c5c430b 100644 --- a/k6/problem-ai/spike-result.json +++ b/k6/problem-ai/spike-result.json @@ -1,163 +1,163 @@ { - "metrics": { - "ai_generation_latency": { - "avg": 18422.666666666668, - "min": 1669, - "med": 2645, - "max": 60001, - "p(90)": 55194.00000000001, - "p(95)": 60000.55 - }, - "iterations": { - "count": 30, - "rate": 0.2773789835922574 - }, - "data_sent": { - "count": 51853, - "rate": 479.4310812069774 + "root_group": { + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "groups": { + "setup": { + "name": "setup", + "path": "::setup", + "id": "5c0f8025f7e0b6654089e5b00e950f1a", + "groups": {}, + "checks": { + "Login Successful (HTTP 200)": { + "passes": 1, + "fails": 0, + "name": "Login Successful (HTTP 200)", + "path": "::setup::Login Successful (HTTP 200)", + "id": "bedba4f0acf48db4a1e0b99e768818d4" + } + } + } }, - "http_req_failed": { - "passes": 3, - "fails": 28, - "value": 0.0967741935483871 + "checks": { + "AI Problem Generation Success (201)": { + "fails": 11, + "name": "AI Problem Generation Success (201)", + "path": "::AI Problem Generation Success (201)", + "id": "2641500d9aae22ca0aeb735974979037", + "passes": 31 + } + } + }, + "metrics": { + "checks": { + "passes": 32, + "fails": 11, + "value": 0.7441860465116279 }, - "http_reqs": { - "count": 31, - "rate": 0.2866249497119993 + "http_req_duration{expected_response:true}": { + "p(90)": 6572.015050000005, + "p(95)": 15709.697434999995, + "avg": 4325.288781249999, + "min": 363.3182, + "med": 2770.71015, + "max": 22350.138 }, - "http_req_duration": { - "avg": 17826.777058064516, - "min": 96.9715, - "med": 2260.2906, - "max": 60000.5223, - "p(90)": 54652.1156, - "p(95)": 59993.476899999994 + "iteration_duration": { + "med": 3340.94485, + "max": 56644.8232, + "p(90)": 33358.3253, + "p(95)": 42532.62645999996, + "avg": 12423.49761190476, + "min": 2917.3243 }, - "http_req_tls_handshaking": { - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0, - "avg": 0 + "http_req_waiting": { + "p(95)": 41590.229429999985, + "avg": 11651.152530232559, + "min": 361.9995, + "med": 2828.5592, + "max": 56144.5384, + "p(90)": 32692.998920000005 }, "http_req_sending": { - "max": 0.6761, + "max": 0.5083, "p(90)": 0, "p(95)": 0, - "avg": 0.02180967741935484, + "avg": 0.023565116279069768, "min": 0, "med": 0 }, - "checks": { - "passes": 28, - "fails": 3, - "value": 0.9032258064516129 + "ai_generation_latency": { + "p(95)": 42030.899999999965, + "avg": 11922.92857142857, + "min": 2417, + "med": 2840, + "max": 56144, + "p(90)": 32857.899999999994 + }, + "http_req_failed": { + "passes": 11, + "fails": 32, + "value": 0.2558139534883721 }, "http_req_connecting": { + "p(90)": 10.16382, + "p(95)": 10.82964, + "avg": 2.7151930232558135, + "min": 0, "med": 0, - "max": 17.7754, - "p(90)": 14.3874, - "p(95)": 15.778649999999999, - "avg": 5.172193548387097, - "min": 0 + "max": 17.7431 }, - "http_req_waiting": { - "max": 60000.5223, - "p(90)": 54651.2913, - "p(95)": 59993.476899999994, - "avg": 17826.482629032256, - "min": 95.8237, - "med": 2260.2906 + "http_req_receiving": { + "avg": 0.35709069767441864, + "min": 0, + "med": 0, + "max": 1.9357, + "p(90)": 0.9794000000000004, + "p(95)": 1.4850499999999995 }, - "vus_max": { - "value": 10, - "min": 10, - "max": 10 + "http_req_duration": { + "avg": 11651.533186046512, + "min": 363.3182, + "med": 2828.5592, + "max": 56144.5384, + "p(90)": 32692.998920000005, + "p(95)": 41590.31056999999 }, - "http_req_duration{expected_response:true}": { - "avg": 13308.664739285716, - "min": 96.9715, - "med": 2196.2102, - "max": 54652.1156, - "p(90)": 41791.651280000005, - "p(95)": 45761.562809999996 + "http_reqs": { + "count": 43, + "rate": 0.3421694236941456 }, - "iteration_duration": { - "min": 2169.5639, - "med": 3144.9058, - "max": 60501.0432, - "p(90)": 55695.08303000001, - "p(95)": 60500.954195, - "avg": 18923.123756666668 + "error_rate": { + "passes": 11, + "fails": 31, + "thresholds": { + "rate<0.3": false + }, + "value": 0.2619047619047619 + }, + "http_req_blocked": { + "p(95)": 13.352739999999997, + "avg": 3.609232558139535, + "min": 0, + "med": 0, + "max": 48.4996, + "p(90)": 10.20392 + }, + "data_sent": { + "count": 66556, + "rate": 529.6146084508733 }, "data_received": { - "count": 65957, - "rate": 609.8361873598174 + "count": 86887, + "rate": 691.3970864305402 }, "vus": { - "value": 1, "min": 1, - "max": 10 + "max": 10, + "value": 1 }, - "http_req_blocked": { - "avg": 5.97144193548387, - "min": 0, - "med": 0, - "max": 37.4544, - "p(90)": 14.7688, - "p(95)": 17.281950000000002 + "vus_max": { + "value": 10, + "min": 10, + "max": 10 }, - "error_rate": { - "passes": 3, - "fails": 27, - "thresholds": { - "rate<0.3": false - }, - "value": 0.1 + "iterations": { + "count": 42, + "rate": 0.3342119952361422 }, - "http_req_receiving": { - "avg": 0.2726193548387097, + "http_req_tls_handshaking": { + "max": 0, + "p(90)": 0, + "p(95)": 0, + "avg": 0, "min": 0, - "med": 0, - "max": 1.2992, - "p(90)": 0.9413, - "p(95)": 1.05395 + "med": 0 } }, "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIyMDk2LCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMjAyOTYsImp0aSI6ImE1N2Q5YTA4LTE3Y2MtNDUyMS04ZTQ3LTRlZjNjNGQwOGEwNSJ9.Hv0GAyJtzZOVQv9SgqqivorJh7kGH_WItSvLMYeoak4", - "csrfToken": "c6cc9aa4-f248-4547-8b6a-12beeb3f6eb8" - }, - "root_group": { - "groups": { - "setup": { - "id": "5c0f8025f7e0b6654089e5b00e950f1a", - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "fails": 0, - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1 - } - }, - "name": "setup", - "path": "::setup" - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "fails": 3, - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 27 - } - }, - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e" + "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk3MTQ1LCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTUzNDUsImp0aSI6IjViYjQyY2ZiLTIwYzItNDcyMS04NWY5LTliMDI1YzFmNTQwYyJ9.7sKzr8huN-qbRg-nvsDDiljgkT0dL3pCkzEBYcFAwEM", + "csrfToken": "7c74b19f-7184-46d3-bbaa-c4a666a02c3b" } } \ No newline at end of file diff --git a/k6/problem-ai/stress-result.json b/k6/problem-ai/stress-result.json index 2b622c4..a07e4a6 100644 --- a/k6/problem-ai/stress-result.json +++ b/k6/problem-ai/stress-result.json @@ -1,170 +1,170 @@ { "root_group": { - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e", "groups": { "setup": { - "name": "setup", "path": "::setup", "id": "5c0f8025f7e0b6654089e5b00e950f1a", "groups": {}, "checks": { "Login Successful (HTTP 200)": { - "fails": 0, "name": "Login Successful (HTTP 200)", "path": "::setup::Login Successful (HTTP 200)", "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1 + "passes": 1, + "fails": 0 } - } + }, + "name": "setup" } }, "checks": { "AI Problem Generation Success (201)": { + "fails": 55, "name": "AI Problem Generation Success (201)", "path": "::AI Problem Generation Success (201)", "id": "2641500d9aae22ca0aeb735974979037", - "passes": 119, - "fails": 44 + "passes": 103 }, "Server Alive (Not 500 Error)": { - "name": "Server Alive (Not 500 Error)", "path": "::Server Alive (Not 500 Error)", "id": "0fa9e611fe9555269e171bceaccf51ad", - "passes": 163, - "fails": 0 + "passes": 158, + "fails": 0, + "name": "Server Alive (Not 500 Error)" } - } + }, + "name": "", + "path": "", + "id": "d41d8cd98f00b204e9800998ecf8427e" }, "metrics": { - "iteration_duration": { - "avg": 13484.380167484665, - "min": 2540.5021, - "med": 3843.9342, - "max": 54857.456, - "p(90)": 39002.59976, - "p(95)": 43650.4985 - }, - "iterations": { - "count": 163, - "rate": 0.3902673321672176 - }, - "http_req_duration{expected_response:true}": { - "med": 1846.7712000000001, - "max": 46699.4283, - "p(90)": 11141.320990000002, - "p(95)": 19373.144819999998, - "avg": 4829.905158333333, - "min": 92.521 - }, - "http_req_failed": { - "passes": 44, - "fails": 120, - "value": 0.2682926829268293 - }, - "error_rate": { - "passes": 44, - "fails": 119, - "thresholds": { - "rate<0.3": false - }, - "value": 0.26993865030674846 + "vus": { + "max": 12, + "value": 1, + "min": 1 }, - "checks": { - "fails": 44, - "passes": 283, - "value": 0.8654434250764526 + "http_reqs": { + "count": 159, + "rate": 0.3826088853307642 }, - "data_sent": { - "count": 274447, - "rate": 657.102444854579 + "http_req_duration": { + "p(90)": 39935.80350000001, + "p(95)": 43403.75668, + "avg": 13937.80584968553, + "min": 362.8992, + "med": 2851.3875, + "max": 57101.6146 }, - "http_req_connecting": { - "p(95)": 11.766774999999987, - "avg": 0.9353335365853659, + "http_req_receiving": { + "avg": 5.659659748427673, "min": 0, "med": 0, - "max": 14.8501, - "p(90)": 0 + "max": 439.0698, + "p(90)": 1.5107200000000003, + "p(95)": 1.8669099999999985 }, - "http_req_sending": { - "med": 0, - "max": 1.6093, - "p(90)": 0, - "p(95)": 0, - "avg": 0.01871036585365854, - "min": 0 - }, - "data_received": { - "count": 326210, - "rate": 781.0374627378408 - }, - "http_reqs": { - "count": 164, - "rate": 0.3926616102786729 + "http_req_duration{expected_response:true}": { + "max": 22399.5935, + "p(90)": 3272.06782, + "p(95)": 11668.679964999992, + "avg": 3597.5725653846166, + "min": 362.8992, + "med": 2689.1234999999997 }, "http_req_blocked": { - "avg": 1.2446920731707318, "min": 0, "med": 0, - "max": 38.9052, + "max": 135.3158, "p(90)": 0, - "p(95)": 13.67919 + "p(95)": 29.662049999999947, + "avg": 4.099650314465409 + }, + "http_req_waiting": { + "p(90)": 39935.80350000001, + "p(95)": 43403.23712, + "avg": 13932.115055974846, + "min": 362.8992, + "med": 2850.9977, + "max": 57100.5352 + }, + "error_rate": { + "passes": 55, + "fails": 103, + "thresholds": { + "rate<0.3": true + }, + "value": 0.34810126582278483 }, "http_req_tls_handshaking": { + "med": 0, + "max": 0, + "p(90)": 0, + "p(95)": 0, "avg": 0, + "min": 0 + }, + "http_req_sending": { + "avg": 0.031133962264150945, "min": 0, "med": 0, - "max": 0, + "max": 1.188, "p(90)": 0, "p(95)": 0 }, - "http_req_waiting": { - "min": 92.521, - "med": 2738.5003500000003, - "max": 53857.1452, - "p(90)": 37998.92066, - "p(95)": 42648.744865, - "avg": 12407.050471341467 - }, - "http_req_duration": { - "p(90)": 37998.92066, - "p(95)": 42648.86194, - "avg": 12407.386081707322, - "min": 92.521, - "med": 2738.9315500000002, - "max": 53857.1452 - }, - "http_req_receiving": { - "med": 0, - "max": 2.2376, - "p(90)": 0.98165, - "p(95)": 1.1827949999999998, - "avg": 0.31690000000000007, - "min": 0 + "checks": { + "passes": 262, + "fails": 55, + "value": 0.8264984227129337 }, - "vus": { - "max": 12, - "value": 1, - "min": 1 + "data_received": { + "count": 301376, + "rate": 725.2146882103422 }, "vus_max": { + "value": 12, "min": 12, - "max": 12, - "value": 12 + "max": 12 + }, + "iterations": { + "count": 158, + "rate": 0.3802025401400047 + }, + "data_sent": { + "rate": 604.9936824799963, + "count": 251416 + }, + "http_req_failed": { + "passes": 55, + "fails": 104, + "value": 0.34591194968553457 + }, + "http_req_connecting": { + "avg": 3.211336477987421, + "min": 0, + "med": 0, + "max": 86.2648, + "p(90)": 0, + "p(95)": 24.25951999999997 }, "ai_generation_latency": { - "p(95)": 42649.2, - "avg": 12483.957055214723, - "min": 1540, - "med": 2844, - "max": 53857, - "p(90)": 38001 + "med": 2858.5, + "max": 57101, + "p(90)": 40009.600000000006, + "p(95)": 43416.55, + "avg": 14027.474683544304, + "min": 2414 + }, + "iteration_duration": { + "avg": 15028.209652531645, + "min": 3415.231, + "med": 3859.25255, + "max": 58101.7709, + "p(90)": 41009.84429, + "p(95)": 44416.697995 } }, "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIxNjQ1LCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMTk4NDUsImp0aSI6Ijc0YzViYzhkLTA4MGEtNDgwZi04NTY3LWUyMWRlYjFlODU4MiJ9.EdZ4qima4jeIJlsygg2nDSdgX4OEKpt8yPhD0_7YKpI", - "csrfToken": "d9105869-2b52-426e-ad7a-74ccb4937e1b" + "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk2MjIxLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTQ0MjEsImp0aSI6ImIxMDk5YzY3LTc3NjYtNDYxMy1hZmQ4LTM4OGY4YjU0NTY0MyJ9.6LAWyJxbNWAFcF-g5XjTFeRouVoDwiqfyKxAd0W8gEA", + "csrfToken": "9f275164-9398-4223-b744-751abab7dbc2" } } \ No newline at end of file diff --git a/momogo-api/src/main/java/com/momogo/api/auth/config/SecurityConfig.java b/momogo-api/src/main/java/com/momogo/api/auth/config/SecurityConfig.java index d8380b6..327878e 100644 --- a/momogo-api/src/main/java/com/momogo/api/auth/config/SecurityConfig.java +++ b/momogo-api/src/main/java/com/momogo/api/auth/config/SecurityConfig.java @@ -194,7 +194,9 @@ public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of( - "http://localhost:5173" + "http://localhost:5173", + "http://momogo.kro.kr", + "https://momogo.kro.kr" )); configuration.setAllowedMethods(List.of( diff --git a/momogo-api/src/main/resources/application.yaml b/momogo-api/src/main/resources/application.yaml index aea3cc5..d2097b7 100644 --- a/momogo-api/src/main/resources/application.yaml +++ b/momogo-api/src/main/resources/application.yaml @@ -4,6 +4,21 @@ spring: enabled: true application: name: momogo-api + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + producer: + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: momogo-api + auto-offset-reset: earliest + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer + spring.json.trusted.packages: com.momogo.core.domain.* servlet: multipart: max-file-size: 10MB diff --git a/momogo-batch/build.gradle b/momogo-batch/build.gradle index 7700bc6..8e2dcad 100644 --- a/momogo-batch/build.gradle +++ b/momogo-batch/build.gradle @@ -10,6 +10,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-batch' + // 스케줄러 분산 락 (다중 인스턴스 중복 실행 방지, Redis 기반) + implementation 'net.javacrumbs.shedlock:shedlock-spring:5.16.0' + implementation 'net.javacrumbs.shedlock:shedlock-provider-redis-spring:5.16.0' + // 배치 전용 테스트 라이브러리 testImplementation 'org.springframework.batch:spring-batch-test' } diff --git a/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java b/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java new file mode 100644 index 0000000..76ab4a3 --- /dev/null +++ b/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java @@ -0,0 +1,22 @@ +package com.momogo.batch.config; + +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.redis.spring.RedisLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * 배치 인스턴스가 여러 대로 늘어나도 @Scheduled 작업이 한 번만 실행되도록 하는 분산 락 설정. + * 스케줄러 메서드에 @SchedulerLock(name = "...")을 붙이면 이 락이 적용된다. + */ +@Configuration +@EnableSchedulerLock(defaultLockAtMostFor = "PT30M") +public class ShedLockConfig { + + @Bean + public LockProvider lockProvider(RedisConnectionFactory connectionFactory) { + return new RedisLockProvider(connectionFactory, "momogo-batch"); + } +} diff --git a/momogo-batch/src/main/java/com/momogo/batch/scheduler/BatchScheduler.java b/momogo-batch/src/main/java/com/momogo/batch/scheduler/BatchScheduler.java index 1a25c75..292c6a7 100644 --- a/momogo-batch/src/main/java/com/momogo/batch/scheduler/BatchScheduler.java +++ b/momogo-batch/src/main/java/com/momogo/batch/scheduler/BatchScheduler.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.launch.JobLauncher; @@ -33,6 +34,7 @@ public class BatchScheduler { // 매일 00:10 - 어제 하루치 개인 일간 리포트 생성 @Scheduled(cron = "0 10 0 * * *") + @SchedulerLock(name = "dailyReportJob", lockAtMostFor = "PT30M") public void runDailyReportJob() { try { jobLauncher.run(dailyReportJob, new JobParametersBuilder() @@ -46,6 +48,7 @@ public void runDailyReportJob() { // 매주 월요일 00:20 - 지난주 개인 주간 리포트 생성 @Scheduled(cron = "0 20 0 * * MON") + @SchedulerLock(name = "weeklyReportJob", lockAtMostFor = "PT30M") public void runWeeklyReportJob() { try { jobLauncher.run(weeklyReportJob, new JobParametersBuilder() diff --git a/momogo-batch/src/main/java/com/momogo/batch/scheduler/ExpiredUserCleanupScheduler.java b/momogo-batch/src/main/java/com/momogo/batch/scheduler/ExpiredUserCleanupScheduler.java index ef5c443..669e778 100644 --- a/momogo-batch/src/main/java/com/momogo/batch/scheduler/ExpiredUserCleanupScheduler.java +++ b/momogo-batch/src/main/java/com/momogo/batch/scheduler/ExpiredUserCleanupScheduler.java @@ -3,6 +3,7 @@ import com.momogo.core.domain.user.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -20,6 +21,7 @@ public class ExpiredUserCleanupScheduler { * 크론식 구성: 초(0) 분(0) 시(4) 일(*) 월(*) 요일(*) */ @Scheduled(cron = "0 0 4 * * *") + @SchedulerLock(name = "expiredUserCleanupJob", lockAtMostFor = "PT30M") public void runExpiredUserCleanupJob() { log.info("[ExpiredUserCleanupScheduler] 30일 경과 회원 물리 삭제 스케줄 작업 시작"); try { diff --git a/momogo-batch/src/main/resources/application.yaml b/momogo-batch/src/main/resources/application.yaml index e58e1c1..36e3375 100644 --- a/momogo-batch/src/main/resources/application.yaml +++ b/momogo-batch/src/main/resources/application.yaml @@ -6,6 +6,21 @@ spring: enabled: true application: name: momogo-batch + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + producer: + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: momogo-batch + auto-offset-reset: earliest + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer + spring.json.trusted.packages: com.momogo.core.domain.* profiles: active: dev group: diff --git a/momogo-core/build.gradle b/momogo-core/build.gradle index eb8815d..789e90a 100644 --- a/momogo-core/build.gradle +++ b/momogo-core/build.gradle @@ -22,6 +22,7 @@ dependencies { // HTTP & JSON Utilities (Tomcat이 포함된 spring-boot-starter-web 대신 경량 의존성 사용) api 'org.springframework:spring-web' api 'com.fasterxml.jackson.core:jackson-annotations' + api 'com.fasterxml.jackson.core:jackson-databind' // PDF 라이브러리 implementation 'com.github.librepdf:openpdf:2.2.2' @@ -30,6 +31,10 @@ dependencies { implementation platform('software.amazon.awssdk:bom:2.29.52') implementation 'software.amazon.awssdk:s3' + // 분산환경 공용 인프라 (Redis, Kafka) - api/realtime/batch가 공통으로 사용 + api 'org.springframework.boot:spring-boot-starter-data-redis' + api 'org.springframework.kafka:spring-kafka' + // 인프라 테스트용 Testcontainers testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.testcontainers:junit-jupiter' diff --git a/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java index 2a88363..5c2a019 100644 --- a/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java +++ b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java @@ -1,7 +1,25 @@ package com.momogo.core.common.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.retrytopic.RetryTopicConfiguration; +import org.springframework.kafka.retrytopic.RetryTopicConfigurationBuilder; +/** + * @KafkaListener에 공통으로 적용되는 기본 재시도/DLT 정책. + * 개별 리스너가 @RetryableTopic으로 별도 정책을 지정하지 않으면 이 설정을 따른다. + * (재시도: 1초 -> 2초 -> 4초 -> 8초, 최대 4회 시도 후 "{topic}.DLT" 토픽으로 격리) + */ @Configuration public class KafkaConfig { + + @Bean + public RetryTopicConfiguration defaultRetryTopicConfiguration(KafkaTemplate kafkaTemplate) { + return RetryTopicConfigurationBuilder.newInstance() + .exponentialBackoff(1000, 2.0, 30000) + .maxAttempts(4) + .autoCreateTopics(true, 3, (short) 1) + .create(kafkaTemplate); + } } diff --git a/momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java new file mode 100644 index 0000000..66302e1 --- /dev/null +++ b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaTopics.java @@ -0,0 +1,13 @@ +package com.momogo.core.common.config; + +/** + * Kafka 토픽 이름 상수. 각자 문자열로 하드코딩하면 오타/불일치로 발행-구독이 + * 어긋날 수 있으므로, 새 토픽이 필요하면 이 클래스에 추가해서 공용으로 참조한다. + */ +public final class KafkaTopics { + + public static final String NOTIFICATION_EVENTS = "notification-events"; + + private KafkaTopics() { + } +} diff --git a/momogo-core/src/main/java/com/momogo/core/common/config/RedisConfig.java b/momogo-core/src/main/java/com/momogo/core/common/config/RedisConfig.java index 08abf38..803ae24 100644 --- a/momogo-core/src/main/java/com/momogo/core/common/config/RedisConfig.java +++ b/momogo-core/src/main/java/com/momogo/core/common/config/RedisConfig.java @@ -1,7 +1,29 @@ package com.momogo.core.common.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +/** + * 인스턴스 간 공유해야 하는 상태(JWT 세션, 분산 락 등)를 Redis에 저장할 때 + * 공통으로 사용하는 RedisTemplate 설정. + * RedisConnectionFactory는 spring-boot-starter-data-redis가 spring.data.redis.* + * 설정값을 읽어 자동 구성하므로 여기서 별도로 정의하지 않는다. + */ @Configuration public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + return template; + } } diff --git a/momogo-realtime/src/main/resources/application.yaml b/momogo-realtime/src/main/resources/application.yaml index 0f426ee..39e96d8 100644 --- a/momogo-realtime/src/main/resources/application.yaml +++ b/momogo-realtime/src/main/resources/application.yaml @@ -6,6 +6,21 @@ spring: enabled: true application: name: momogo-realtime + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + producer: + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: momogo-realtime + auto-offset-reset: earliest + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer + spring.json.trusted.packages: com.momogo.core.domain.* profiles: active: dev group: From eefef43601a18865506762928cd8e6cc346c3c50 Mon Sep 17 00:00:00 2001 From: Junkov0 Date: Fri, 31 Jul 2026 16:04:26 +0900 Subject: [PATCH 2/2] fix: correct Kafka retry/DLT config and ShedLock doc to match actual behavior --- k6/problem-ai/load-result.json | 166 ----------------- k6/problem-ai/soak-result.json | 166 ----------------- k6/problem-ai/spike-result.json | 163 ----------------- k6/problem-ai/stress-result.json | 170 ------------------ .../momogo/batch/config/ShedLockConfig.java | 3 +- .../core/common/config/KafkaConfig.java | 5 +- 6 files changed, 5 insertions(+), 668 deletions(-) delete mode 100644 k6/problem-ai/load-result.json delete mode 100644 k6/problem-ai/soak-result.json delete mode 100644 k6/problem-ai/spike-result.json delete mode 100644 k6/problem-ai/stress-result.json diff --git a/k6/problem-ai/load-result.json b/k6/problem-ai/load-result.json deleted file mode 100644 index 66ad2e9..0000000 --- a/k6/problem-ai/load-result.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "root_group": { - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e", - "groups": { - "setup": { - "name": "setup", - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a", - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "passes": 1, - "fails": 0, - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4" - } - } - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 67, - "fails": 8, - "name": "AI Problem Generation Success (201)" - } - } - }, - "metrics": { - "http_req_blocked": { - "min": 0, - "med": 0, - "max": 51.8237, - "p(90)": 0, - "p(95)": 2.283725, - "avg": 1.0608315789473686 - }, - "http_req_tls_handshaking": { - "p(95)": 0, - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0 - }, - "http_req_sending": { - "p(90)": 0, - "p(95)": 0, - "avg": 0.005593421052631578, - "min": 0, - "med": 0, - "max": 0.4251 - }, - "vus_max": { - "max": 3, - "value": 3, - "min": 3 - }, - "iteration_duration": { - "avg": 8913.08115333333, - "min": 5388.5987, - "med": 5782.1383, - "max": 48739.6799, - "p(90)": 17266.536480000013, - "p(95)": 25330.858449999992 - }, - "error_rate": { - "passes": 8, - "fails": 67, - "thresholds": { - "rate<0.05": true - }, - "value": 0.10666666666666667 - }, - "http_req_failed": { - "passes": 8, - "fails": 68, - "value": 0.10526315789473684 - }, - "iterations": { - "count": 75, - "rate": 0.30204230617178024 - }, - "data_received": { - "count": 170301, - "rate": 685.841423778138 - }, - "vus": { - "value": 1, - "min": 1, - "max": 3 - }, - "http_req_duration": { - "avg": 5839.168152631579, - "min": 369.2119, - "med": 2774.7471, - "max": 45739.3552, - "p(90)": 14135.55285, - "p(95)": 22216.819475, - "thresholds": { - "p(95)<15000": true - } - }, - "data_sent": { - "count": 118696, - "rate": 478.0161809782084 - }, - "ai_generation_latency": { - "p(95)": 22330.399999999994, - "avg": 5912.68, - "min": 2388, - "med": 2782, - "max": 45740, - "p(90)": 14266.200000000012 - }, - "http_reqs": { - "count": 76, - "rate": 0.30606953692073735 - }, - "http_req_waiting": { - "p(90)": 14132.735349999999, - "p(95)": 22216.819475, - "avg": 5838.56733026316, - "min": 368.8114, - "med": 2773.65715, - "max": 45736.7462 - }, - "http_req_receiving": { - "min": 0, - "med": 0, - "max": 10.785, - "p(90)": 1.00765, - "p(95)": 2.27265, - "avg": 0.595228947368421 - }, - "http_req_connecting": { - "med": 0, - "max": 10.274, - "p(90)": 0, - "p(95)": 2.283725, - "avg": 0.5076039473684211, - "min": 0 - }, - "http_req_duration{expected_response:true}": { - "p(90)": 3831.9011100000075, - "p(95)": 8887.069804999994, - "avg": 3544.213716176471, - "min": 369.2119, - "med": 2739.2021, - "max": 23924.7887 - }, - "checks": { - "passes": 68, - "fails": 8, - "value": 0.8947368421052632 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk1NzIyLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTM5MjIsImp0aSI6IjVhMGVjNjA2LTU0MTEtNDY4MS1iZTllLThhNGQ4ZDVhMTk3NyJ9.bIokWJI1Yo6F5RvEf0eMM6xl3V7KmVFCmIMf8ArwSSE", - "csrfToken": "6319c265-b924-4f9e-8866-c4294bd6391a" - } -} \ No newline at end of file diff --git a/k6/problem-ai/soak-result.json b/k6/problem-ai/soak-result.json deleted file mode 100644 index a45a3a5..0000000 --- a/k6/problem-ai/soak-result.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "metrics": { - "http_req_failed": { - "passes": 0, - "fails": 164, - "value": 0 - }, - "data_received": { - "rate": 542.7608833367937, - "count": 393261 - }, - "http_req_connecting": { - "avg": 0.24038414634146343, - "min": 0, - "med": 0, - "max": 15.4775, - "p(90)": 0, - "p(95)": 0 - }, - "iteration_duration": { - "avg": 8327.549047852763, - "min": 7400.9275, - "med": 7757.7225, - "max": 20045.3774, - "p(90)": 9240.39226, - "p(95)": 12493.147110000004 - }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0 - }, - "data_sent": { - "count": 257736, - "rate": 355.7154638463815 - }, - "vus_max": { - "value": 2, - "min": 2, - "max": 2 - }, - "http_req_receiving": { - "med": 0.158, - "max": 10.7239, - "p(90)": 0.9932900000000002, - "p(95)": 1.0894999999999997, - "avg": 0.45652682926829263, - "min": 0 - }, - "iterations": { - "count": 163, - "rate": 0.22496516050128887 - }, - "error_rate": { - "passes": 0, - "fails": 163, - "thresholds": { - "rate<0.05": false - }, - "value": 0 - }, - "http_req_blocked": { - "avg": 0.6593865853658536, - "min": 0, - "med": 0, - "max": 84.1939, - "p(90)": 0, - "p(95)": 0 - }, - "http_reqs": { - "count": 164, - "rate": 0.2263453148601925 - }, - "http_req_waiting": { - "avg": 3308.328158536584, - "min": 368.1503, - "med": 2756.7853999999998, - "max": 15044.6296, - "p(90)": 4238.19987, - "p(95)": 7466.804339999997 - }, - "http_req_sending": { - "min": 0, - "med": 0, - "max": 1.505, - "p(90)": 0, - "p(95)": 0, - "avg": 0.023870121951219514 - }, - "checks": { - "passes": 164, - "fails": 0, - "value": 1 - }, - "vus": { - "value": 1, - "min": 1, - "max": 2 - }, - "http_req_duration": { - "avg": 3308.808555487803, - "min": 369.6553, - "med": 2757.2959, - "max": 15044.6296, - "p(90)": 4238.50197, - "p(95)": 7466.804339999997, - "thresholds": { - "p(95)<15000": false - } - }, - "ai_generation_latency": { - "avg": 3327.0490797546013, - "min": 2399, - "med": 2758, - "max": 15045, - "p(90)": 4239.6, - "p(95)": 7492.000000000003 - }, - "http_req_duration{expected_response:true}": { - "med": 2757.2959, - "max": 15044.6296, - "p(90)": 4238.50197, - "p(95)": 7466.804339999997, - "avg": 3308.808555487803, - "min": 369.6553 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk3MjkxLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTU0OTEsImp0aSI6IjUwZDJjYzU1LTMzOWYtNDRhNy1hN2Q2LTAwMzNhNzM1YzhkMyJ9.pUT-u04j5_PbtX3g2zrOGDHF_mk5HeAHdu_gKYKsn_M", - "csrfToken": "b2235495-97a4-4b8c-876d-3d6d205a4d50" - }, - "root_group": { - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e", - "groups": { - "setup": { - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1, - "fails": 0 - } - }, - "name": "setup", - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a" - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "fails": 0, - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 163 - } - } - } -} \ No newline at end of file diff --git a/k6/problem-ai/spike-result.json b/k6/problem-ai/spike-result.json deleted file mode 100644 index c5c430b..0000000 --- a/k6/problem-ai/spike-result.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "root_group": { - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e", - "groups": { - "setup": { - "name": "setup", - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a", - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "passes": 1, - "fails": 0, - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4" - } - } - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "fails": 11, - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 31 - } - } - }, - "metrics": { - "checks": { - "passes": 32, - "fails": 11, - "value": 0.7441860465116279 - }, - "http_req_duration{expected_response:true}": { - "p(90)": 6572.015050000005, - "p(95)": 15709.697434999995, - "avg": 4325.288781249999, - "min": 363.3182, - "med": 2770.71015, - "max": 22350.138 - }, - "iteration_duration": { - "med": 3340.94485, - "max": 56644.8232, - "p(90)": 33358.3253, - "p(95)": 42532.62645999996, - "avg": 12423.49761190476, - "min": 2917.3243 - }, - "http_req_waiting": { - "p(95)": 41590.229429999985, - "avg": 11651.152530232559, - "min": 361.9995, - "med": 2828.5592, - "max": 56144.5384, - "p(90)": 32692.998920000005 - }, - "http_req_sending": { - "max": 0.5083, - "p(90)": 0, - "p(95)": 0, - "avg": 0.023565116279069768, - "min": 0, - "med": 0 - }, - "ai_generation_latency": { - "p(95)": 42030.899999999965, - "avg": 11922.92857142857, - "min": 2417, - "med": 2840, - "max": 56144, - "p(90)": 32857.899999999994 - }, - "http_req_failed": { - "passes": 11, - "fails": 32, - "value": 0.2558139534883721 - }, - "http_req_connecting": { - "p(90)": 10.16382, - "p(95)": 10.82964, - "avg": 2.7151930232558135, - "min": 0, - "med": 0, - "max": 17.7431 - }, - "http_req_receiving": { - "avg": 0.35709069767441864, - "min": 0, - "med": 0, - "max": 1.9357, - "p(90)": 0.9794000000000004, - "p(95)": 1.4850499999999995 - }, - "http_req_duration": { - "avg": 11651.533186046512, - "min": 363.3182, - "med": 2828.5592, - "max": 56144.5384, - "p(90)": 32692.998920000005, - "p(95)": 41590.31056999999 - }, - "http_reqs": { - "count": 43, - "rate": 0.3421694236941456 - }, - "error_rate": { - "passes": 11, - "fails": 31, - "thresholds": { - "rate<0.3": false - }, - "value": 0.2619047619047619 - }, - "http_req_blocked": { - "p(95)": 13.352739999999997, - "avg": 3.609232558139535, - "min": 0, - "med": 0, - "max": 48.4996, - "p(90)": 10.20392 - }, - "data_sent": { - "count": 66556, - "rate": 529.6146084508733 - }, - "data_received": { - "count": 86887, - "rate": 691.3970864305402 - }, - "vus": { - "min": 1, - "max": 10, - "value": 1 - }, - "vus_max": { - "value": 10, - "min": 10, - "max": 10 - }, - "iterations": { - "count": 42, - "rate": 0.3342119952361422 - }, - "http_req_tls_handshaking": { - "max": 0, - "p(90)": 0, - "p(95)": 0, - "avg": 0, - "min": 0, - "med": 0 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk3MTQ1LCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTUzNDUsImp0aSI6IjViYjQyY2ZiLTIwYzItNDcyMS04NWY5LTliMDI1YzFmNTQwYyJ9.7sKzr8huN-qbRg-nvsDDiljgkT0dL3pCkzEBYcFAwEM", - "csrfToken": "7c74b19f-7184-46d3-bbaa-c4a666a02c3b" - } -} \ No newline at end of file diff --git a/k6/problem-ai/stress-result.json b/k6/problem-ai/stress-result.json deleted file mode 100644 index a07e4a6..0000000 --- a/k6/problem-ai/stress-result.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "root_group": { - "groups": { - "setup": { - "path": "::setup", - "id": "5c0f8025f7e0b6654089e5b00e950f1a", - "groups": {}, - "checks": { - "Login Successful (HTTP 200)": { - "name": "Login Successful (HTTP 200)", - "path": "::setup::Login Successful (HTTP 200)", - "id": "bedba4f0acf48db4a1e0b99e768818d4", - "passes": 1, - "fails": 0 - } - }, - "name": "setup" - } - }, - "checks": { - "AI Problem Generation Success (201)": { - "fails": 55, - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 103 - }, - "Server Alive (Not 500 Error)": { - "path": "::Server Alive (Not 500 Error)", - "id": "0fa9e611fe9555269e171bceaccf51ad", - "passes": 158, - "fails": 0, - "name": "Server Alive (Not 500 Error)" - } - }, - "name": "", - "path": "", - "id": "d41d8cd98f00b204e9800998ecf8427e" - }, - "metrics": { - "vus": { - "max": 12, - "value": 1, - "min": 1 - }, - "http_reqs": { - "count": 159, - "rate": 0.3826088853307642 - }, - "http_req_duration": { - "p(90)": 39935.80350000001, - "p(95)": 43403.75668, - "avg": 13937.80584968553, - "min": 362.8992, - "med": 2851.3875, - "max": 57101.6146 - }, - "http_req_receiving": { - "avg": 5.659659748427673, - "min": 0, - "med": 0, - "max": 439.0698, - "p(90)": 1.5107200000000003, - "p(95)": 1.8669099999999985 - }, - "http_req_duration{expected_response:true}": { - "max": 22399.5935, - "p(90)": 3272.06782, - "p(95)": 11668.679964999992, - "avg": 3597.5725653846166, - "min": 362.8992, - "med": 2689.1234999999997 - }, - "http_req_blocked": { - "min": 0, - "med": 0, - "max": 135.3158, - "p(90)": 0, - "p(95)": 29.662049999999947, - "avg": 4.099650314465409 - }, - "http_req_waiting": { - "p(90)": 39935.80350000001, - "p(95)": 43403.23712, - "avg": 13932.115055974846, - "min": 362.8992, - "med": 2850.9977, - "max": 57100.5352 - }, - "error_rate": { - "passes": 55, - "fails": 103, - "thresholds": { - "rate<0.3": true - }, - "value": 0.34810126582278483 - }, - "http_req_tls_handshaking": { - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0, - "avg": 0, - "min": 0 - }, - "http_req_sending": { - "avg": 0.031133962264150945, - "min": 0, - "med": 0, - "max": 1.188, - "p(90)": 0, - "p(95)": 0 - }, - "checks": { - "passes": 262, - "fails": 55, - "value": 0.8264984227129337 - }, - "data_received": { - "count": 301376, - "rate": 725.2146882103422 - }, - "vus_max": { - "value": 12, - "min": 12, - "max": 12 - }, - "iterations": { - "count": 158, - "rate": 0.3802025401400047 - }, - "data_sent": { - "rate": 604.9936824799963, - "count": 251416 - }, - "http_req_failed": { - "passes": 55, - "fails": 104, - "value": 0.34591194968553457 - }, - "http_req_connecting": { - "avg": 3.211336477987421, - "min": 0, - "med": 0, - "max": 86.2648, - "p(90)": 0, - "p(95)": 24.25951999999997 - }, - "ai_generation_latency": { - "med": 2858.5, - "max": 57101, - "p(90)": 40009.600000000006, - "p(95)": 43416.55, - "avg": 14027.474683544304, - "min": 2414 - }, - "iteration_duration": { - "avg": 15028.209652531645, - "min": 3415.231, - "med": 3859.25255, - "max": 58101.7709, - "p(90)": 41009.84429, - "p(95)": 44416.697995 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiLstZzspIDsmIEiLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1Mzk2MjIxLCJ1c2VySWQiOiJiZWYyZTA4MS1lODY5LTRhODEtODFiMC1kZTJlZjY4NzQ5ZWEiLCJpYXQiOjE3ODUzOTQ0MjEsImp0aSI6ImIxMDk5YzY3LTc3NjYtNDYxMy1hZmQ4LTM4OGY4YjU0NTY0MyJ9.6LAWyJxbNWAFcF-g5XjTFeRouVoDwiqfyKxAd0W8gEA", - "csrfToken": "9f275164-9398-4223-b744-751abab7dbc2" - } -} \ No newline at end of file diff --git a/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java b/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java index 76ab4a3..6e0a08e 100644 --- a/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java +++ b/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java @@ -8,7 +8,8 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; /** - * 배치 인스턴스가 여러 대로 늘어나도 @Scheduled 작업이 한 번만 실행되도록 하는 분산 락 설정. + * 동일한 잠금 이름의 @Scheduled 작업이 여러 인스턴스에서 동시에 실행되지 않도록 조정하는 분산 락 설정. + * 잠금 만료 또는 Redis 장애 시 재실행될 수 있으므로 작업은 멱등해야 한다. * 스케줄러 메서드에 @SchedulerLock(name = "...")을 붙이면 이 락이 적용된다. */ @Configuration diff --git a/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java index 5c2a019..30f43ea 100644 --- a/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java +++ b/momogo-core/src/main/java/com/momogo/core/common/config/KafkaConfig.java @@ -9,7 +9,8 @@ /** * @KafkaListener에 공통으로 적용되는 기본 재시도/DLT 정책. * 개별 리스너가 @RetryableTopic으로 별도 정책을 지정하지 않으면 이 설정을 따른다. - * (재시도: 1초 -> 2초 -> 4초 -> 8초, 최대 4회 시도 후 "{topic}.DLT" 토픽으로 격리) + * (최초 처리 포함 총 5회 시도 - 재시도 4회 간격은 1초 -> 2초 -> 4초 -> 8초 - + * 후 실패하면 "{topic}-dlt" 토픽으로 격리) */ @Configuration public class KafkaConfig { @@ -18,7 +19,7 @@ public class KafkaConfig { public RetryTopicConfiguration defaultRetryTopicConfiguration(KafkaTemplate kafkaTemplate) { return RetryTopicConfigurationBuilder.newInstance() .exponentialBackoff(1000, 2.0, 30000) - .maxAttempts(4) + .maxAttempts(5) .autoCreateTopics(true, 3, (short) 1) .create(kafkaTemplate); }