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 deleted file mode 100644 index ce944ba..0000000 --- a/k6/problem-ai/load-result.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "root_group": { - "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)": { - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 74, - "fails": 4 - } - }, - "name": "", - "path": "" - }, - "metrics": { - "error_rate": { - "passes": 4, - "fails": 74, - "thresholds": { - "rate<0.05": true - }, - "value": 0.05128205128205128 - }, - "http_req_sending": { - "min": 0, - "med": 0, - "max": 0.5059, - "p(90)": 0, - "p(95)": 0, - "avg": 0.006403797468354431 - }, - "checks": { - "passes": 75, - "fails": 4, - "value": 0.9493670886075949 - }, - "http_req_failed": { - "passes": 4, - "fails": 75, - "value": 0.05063291139240506 - }, - "http_req_blocked": { - "p(95)": 0.8032699999999544, - "avg": 0.9820379746835444, - "min": 0, - "med": 0, - "max": 41.5508, - "p(90)": 0 - }, - "data_sent": { - "count": 126051, - "rate": 505.8668829872479 - }, - "http_req_connecting": { - "p(90)": 0, - "p(95)": 0.8032699999999544, - "avg": 0.6721151898734177, - "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 - }, - "vus_max": { - "value": 3, - "min": 3, - "max": 3 - }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0 - }, - "http_req_waiting": { - "avg": 5332.521148101266, - "min": 103.6248, - "med": 1720.0426, - "max": 47533.7525, - "p(90)": 12265.669140000009, - "p(95)": 27687.98786999999 - }, - "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 - }, - "data_received": { - "count": 181631, - "rate": 728.9201023701265 - }, - "ai_generation_latency": { - "min": 1431, - "med": 1720.5, - "max": 47534, - "p(90)": 12569.199999999992, - "p(95)": 27777.649999999983, - "avg": 5400.384615384615 - }, - "iterations": { - "count": 78, - "rate": 0.3130289872591676 - }, - "http_req_duration": { - "max": 47534.4313, - "p(90)": 12265.857620000008, - "p(95)": 27688.06979999999, - "avg": 5332.896022784809, - "min": 103.6248, - "med": 1720.0426, - "thresholds": { - "p(95)<15000": true - } - }, - "http_reqs": { - "count": 79, - "rate": 0.31704217940351587 - }, - "http_req_receiving": { - "p(95)": 1.50718, - "avg": 0.36847088607594924, - "min": 0, - "med": 0, - "max": 1.8783, - "p(90)": 0.94332 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIxMTczLCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMTkzNzMsImp0aSI6IjIzODMwZDg0LWFkZmQtNDg2Ni1iNmZmLTIyNGM1OTYzOTU0MSJ9.AI070Smmkh9iEhirwSCgCyHs7vY6I6i6FKezWwE5RA8", - "csrfToken": "bb86b3a1-9911-4131-99f3-64c75d44f20b" - } -} \ 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 2eba3c4..0000000 --- a/k6/problem-ai/soak-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)": { - "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 - }, - "data_sent": { - "count": 190571, - "rate": 263.68869497938397 - }, - "vus": { - "value": 1, - "min": 1, - "max": 2 - }, - "ai_generation_latency": { - "p(95)": 39217.09999999993, - "avg": 6485.4915254237285, - "min": 1482, - "med": 1867.5, - "max": 60001, - "p(90)": 14263.19999999999 - }, - "http_req_blocked": { - "med": 0, - "max": 31.4853, - "p(90)": 0, - "p(95)": 8.950599999999998, - "avg": 0.8108773109243698, - "min": 0 - }, - "iteration_duration": { - "med": 6867.6497500000005, - "max": 65001.2099, - "p(90)": 19263.86304999999, - "p(95)": 44217.79187499993, - "avg": 11485.922727118656, - "min": 6482.2853 - }, - "vus_max": { - "value": 2, - "min": 2, - "max": 2 - }, - "http_req_connecting": { - "min": 0, - "med": 0, - "max": 13.133, - "p(90)": 0, - "p(95)": 8.108739999999994, - "avg": 0.6090352941176471 - }, - "http_req_waiting": { - "p(95)": 38807.87416999995, - "avg": 6430.889257983194, - "min": 92.5452, - "med": 1865.5074, - "max": 60000.4346, - "p(90)": 13879.967000000011 - }, - "http_req_sending": { - "max": 0.5532, - "p(90)": 0, - "p(95)": 0, - "avg": 0.00464873949579832, - "min": 0, - "med": 0 - }, - "http_req_failed": { - "passes": 4, - "fails": 115, - "value": 0.03361344537815126 - }, - "data_received": { - "count": 273211, - "rate": 378.0357559335496 - }, - "iterations": { - "count": 118, - "rate": 0.1632738769674678 - }, - "error_rate": { - "passes": 4, - "fails": 114, - "thresholds": { - "rate<0.05": false - }, - "value": 0.03389830508474576 - }, - "http_reqs": { - "count": 119, - "rate": 0.16465755389092093 - }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIyMjI2LCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMjA0MjYsImp0aSI6ImVkMmI0OGFkLTJhMWEtNGRhOC05MzA4LWU1ZGQ4ZjFlNGUyNCJ9.ZOAJ6NwRMCJjxx_YALM3riXUqSWDA_EVtc5H2OXAsog", - "csrfToken": "5f29b32c-6bc9-46ac-9524-37d2cb81e734" - } -} \ 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 e44bf7d..0000000 --- a/k6/problem-ai/spike-result.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "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 - }, - "http_req_failed": { - "passes": 3, - "fails": 28, - "value": 0.0967741935483871 - }, - "http_reqs": { - "count": 31, - "rate": 0.2866249497119993 - }, - "http_req_duration": { - "avg": 17826.777058064516, - "min": 96.9715, - "med": 2260.2906, - "max": 60000.5223, - "p(90)": 54652.1156, - "p(95)": 59993.476899999994 - }, - "http_req_tls_handshaking": { - "min": 0, - "med": 0, - "max": 0, - "p(90)": 0, - "p(95)": 0, - "avg": 0 - }, - "http_req_sending": { - "max": 0.6761, - "p(90)": 0, - "p(95)": 0, - "avg": 0.02180967741935484, - "min": 0, - "med": 0 - }, - "checks": { - "passes": 28, - "fails": 3, - "value": 0.9032258064516129 - }, - "http_req_connecting": { - "med": 0, - "max": 17.7754, - "p(90)": 14.3874, - "p(95)": 15.778649999999999, - "avg": 5.172193548387097, - "min": 0 - }, - "http_req_waiting": { - "max": 60000.5223, - "p(90)": 54651.2913, - "p(95)": 59993.476899999994, - "avg": 17826.482629032256, - "min": 95.8237, - "med": 2260.2906 - }, - "vus_max": { - "value": 10, - "min": 10, - "max": 10 - }, - "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 - }, - "iteration_duration": { - "min": 2169.5639, - "med": 3144.9058, - "max": 60501.0432, - "p(90)": 55695.08303000001, - "p(95)": 60500.954195, - "avg": 18923.123756666668 - }, - "data_received": { - "count": 65957, - "rate": 609.8361873598174 - }, - "vus": { - "value": 1, - "min": 1, - "max": 10 - }, - "http_req_blocked": { - "avg": 5.97144193548387, - "min": 0, - "med": 0, - "max": 37.4544, - "p(90)": 14.7688, - "p(95)": 17.281950000000002 - }, - "error_rate": { - "passes": 3, - "fails": 27, - "thresholds": { - "rate<0.3": false - }, - "value": 0.1 - }, - "http_req_receiving": { - "avg": 0.2726193548387097, - "min": 0, - "med": 0, - "max": 1.2992, - "p(90)": 0.9413, - "p(95)": 1.05395 - } - }, - "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" - } -} \ 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 2b622c4..0000000 --- a/k6/problem-ai/stress-result.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "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)": { - "name": "AI Problem Generation Success (201)", - "path": "::AI Problem Generation Success (201)", - "id": "2641500d9aae22ca0aeb735974979037", - "passes": 119, - "fails": 44 - }, - "Server Alive (Not 500 Error)": { - "name": "Server Alive (Not 500 Error)", - "path": "::Server Alive (Not 500 Error)", - "id": "0fa9e611fe9555269e171bceaccf51ad", - "passes": 163, - "fails": 0 - } - } - }, - "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 - }, - "checks": { - "fails": 44, - "passes": 283, - "value": 0.8654434250764526 - }, - "data_sent": { - "count": 274447, - "rate": 657.102444854579 - }, - "http_req_connecting": { - "p(95)": 11.766774999999987, - "avg": 0.9353335365853659, - "min": 0, - "med": 0, - "max": 14.8501, - "p(90)": 0 - }, - "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_blocked": { - "avg": 1.2446920731707318, - "min": 0, - "med": 0, - "max": 38.9052, - "p(90)": 0, - "p(95)": 13.67919 - }, - "http_req_tls_handshaking": { - "avg": 0, - "min": 0, - "med": 0, - "max": 0, - "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 - }, - "vus": { - "max": 12, - "value": 1, - "min": 1 - }, - "vus_max": { - "min": 12, - "max": 12, - "value": 12 - }, - "ai_generation_latency": { - "p(95)": 42649.2, - "avg": 12483.957055214723, - "min": 1540, - "med": 2844, - "max": 53857, - "p(90)": 38001 - } - }, - "setup_data": { - "authToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqdW5AdGVzdC5jb20iLCJyb2xlcyI6WyJST0xFX0FETUlOIl0sIm5hbWUiOiJqdW4iLCJ1c2VyRW1haWwiOiJqdW5AdGVzdC5jb20iLCJ0eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzg1MjIxNjQ1LCJ1c2VySWQiOiJjYmE4YTFkZi1hZTYzLTRlYmMtYWI5Ni1kNjc1ZGQxMjljNjMiLCJpYXQiOjE3ODUyMTk4NDUsImp0aSI6Ijc0YzViYzhkLTA4MGEtNDgwZi04NTY3LWUyMWRlYjFlODU4MiJ9.EdZ4qima4jeIJlsygg2nDSdgX4OEKpt8yPhD0_7YKpI", - "csrfToken": "d9105869-2b52-426e-ad7a-74ccb4937e1b" - } -} \ 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..6e0a08e --- /dev/null +++ b/momogo-batch/src/main/java/com/momogo/batch/config/ShedLockConfig.java @@ -0,0 +1,23 @@ +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 작업이 여러 인스턴스에서 동시에 실행되지 않도록 조정하는 분산 락 설정. + * 잠금 만료 또는 Redis 장애 시 재실행될 수 있으므로 작업은 멱등해야 한다. + * 스케줄러 메서드에 @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..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 @@ -1,7 +1,26 @@ 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으로 별도 정책을 지정하지 않으면 이 설정을 따른다. + * (최초 처리 포함 총 5회 시도 - 재시도 4회 간격은 1초 -> 2초 -> 4초 -> 8초 - + * 후 실패하면 "{topic}-dlt" 토픽으로 격리) + */ @Configuration public class KafkaConfig { + + @Bean + public RetryTopicConfiguration defaultRetryTopicConfiguration(KafkaTemplate kafkaTemplate) { + return RetryTopicConfigurationBuilder.newInstance() + .exponentialBackoff(1000, 2.0, 30000) + .maxAttempts(5) + .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: