From 54d64a12383d05c35b976de2cd9d7ae941e91c4e Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 11:14:00 +0900 Subject: [PATCH 01/31] =?UTF-8?q?chore=20:=20RabbitMQ=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 15 +++++++++++++++ src/main/resources/application.yml | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..6079d8a6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +# Docker Compose 파일 형식 버전 지정 +version: '3.8' + +# 실행할 서비스 목록 +services: + # RabbitMQ 서비스 정의 + rabbitmq: + image: rabbitmq:3-management # 관리자 UI가 포함된 공식 RabbitMQ 이미지 + container_name: local-rabbitmq # 컨테이너에 고정된 이름을 부여 + ports: + - "5672:5672" # AMQP 포트: Spring Boot 앱이 연결할 포트 + - "15672:15672" # 관리자 UI 포트: 웹 브라우저로 접속할 포트 + environment: + - RABBITMQ_DEFAULT_USER=guest # 기본 사용자 ID + - RABBITMQ_DEFAULT_PASS=guest # 기본 비밀번호 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c13af761..2eebe2a5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,6 +46,11 @@ spring: time-to-live: 300000 cache-null-values: false key-prefix: + rabbitmq: # RabbitMQ 연결 설정 + host: ${SPRING_RABBITMQ_HOST:localhost} # 환경 변수(SPRING_RABBITMQ_HOST)가 있으면 그 값을 쓰고, 없으면 기본값(localhost)을 사용 + port: 5672 + username: guest + password: guest springdoc: default-produces-media-type: application/json;charset=UTF-8 From feaa21d95fb6674242829adf75546d8c6f91e082 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 11:35:32 +0900 Subject: [PATCH 02/31] =?UTF-8?q?chore=20:=20CI=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=EC=97=90=EC=84=9C=20RabbitMQ=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=EB=A5=BC=20=EB=9D=84?= =?UTF-8?q?=EC=9B=8C=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-server-ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/test-server-ci.yml b/.github/workflows/test-server-ci.yml index ed2b5349..14655ca7 100644 --- a/.github/workflows/test-server-ci.yml +++ b/.github/workflows/test-server-ci.yml @@ -23,6 +23,20 @@ jobs: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + # CI 작업이 실행되는 동안 RabbitMQ 서비스 컨테이너를 함께 실행 + services: + rabbitmq: + image: rabbitmq:3-management + ports: + - 5672:5672 + # RabbitMQ가 완전히 준비될 때까지 기다리는 상태 확인 옵션 + options: >- + --health-cmd "rabbitmq-diagnostics check_running" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: # 1. 소스 코드 체크아웃 - name: Checkout source code @@ -74,6 +88,12 @@ jobs: # 7. Gradle 테스트 실행 - name: Test with Gradle + # 테스트 단계에서 RabbitMQ 연결을 위한 환경 변수 설정 + env: + SPRING_RABBITMQ_HOST: localhost + SPRING_RABBITMQ_PORT: 5672 + SPRING_RABBITMQ_USERNAME: guest + SPRING_RABBITMQ_PASSWORD: guest run: ./gradlew test # 8. 테스트 결과 요약 출력 From b8a65c3245f827c3629f0ce8debcb6bfe4c0a5c3 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:05:33 +0900 Subject: [PATCH 03/31] =?UTF-8?q?new=20:=20RabbitMQ=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4,=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++ .../dashboard/dto/GraphUpdateMessage.java | 7 +++ .../backend/global/mq/RabbitMQConfig.java | 47 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java create mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java diff --git a/build.gradle b/build.gradle index 1c8bd883..8a276fa1 100644 --- a/build.gradle +++ b/build.gradle @@ -115,6 +115,10 @@ dependencies { // Redis (Spring starter) implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // RabbitMQ (Spring starter) + implementation 'org.springframework.boot:spring-boot-starter-amqp' + testImplementation 'org.springframework.amqp:spring-rabbit-test' } dependencyManagement { diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java new file mode 100644 index 00000000..6ea10ede --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java @@ -0,0 +1,7 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.dto; + +public record GraphUpdateMessage( + Integer dashboardId, + String requestBody +){ +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java new file mode 100644 index 00000000..d4e6a10a --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java @@ -0,0 +1,47 @@ +package org.tuna.zoopzoop.backend.global.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + private static final String EXCHANGE_NAME = "zoopzoop.exchange"; + private static final String QUEUE_NAME = "graph.update.queue"; + private static final String ROUTING_KEY = "graph.update.#"; + + @Bean + public TopicExchange exchange() { + return new TopicExchange(EXCHANGE_NAME); + } + + @Bean + public Queue queue() { + return new Queue(QUEUE_NAME); + } + + @Bean + public Binding binding(Queue queue, TopicExchange exchange) { + return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); + } + + @Bean + public MessageConverter messageConverter() { + // 메시지를 JSON 형식으로 직렬화/역직렬화하는 컨버터 + return new Jackson2JsonMessageConverter(); + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(messageConverter); + return rabbitTemplate; + } +} From feb8f390d94711f0e6675f0be32988a8954d6f22 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:12:46 +0900 Subject: [PATCH 04/31] =?UTF-8?q?feat=20:=20producer=20method=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/service/DashboardService.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java index d1d8636d..0cb7ab69 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java @@ -4,10 +4,12 @@ import jakarta.persistence.NoResultException; import lombok.RequiredArgsConstructor; import org.apache.commons.codec.binary.Hex; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; @@ -29,8 +31,9 @@ public class DashboardService { private final DashboardRepository dashboardRepository; private final MembershipService membershipService; - private final ObjectMapper objectMapper; private final SignatureService signatureService; + private final ObjectMapper objectMapper; + private final RabbitTemplate rabbitTemplate; @@ -109,6 +112,33 @@ public void verifyAccessPermission(Member member, Integer dashboardId) throws Ac } } + // =========================== message 관리 메서드 =========================== + + /** + * Graph 업데이트 요청을 RabbitMQ 큐에 비동기적으로 발행하는 메서드 + * @param dashboardId 대시보드 ID + * @param requestBody 요청 바디 + * @param signatureHeader 서명 헤더 + */ + public void queueGraphUdate(Integer dashboardId, String requestBody, String signatureHeader){ + // 서명 검증은 동기적으로 즉시 처리 + if (!signatureService.isValidSignature(requestBody, signatureHeader)) { + throw new SecurityException("Invalid webhook signature."); + } + + // 대시보드 존재 여부 확인 + if (!dashboardRepository.existsById(dashboardId)) { + throw new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다."); + } + + // 큐에 보낼 메시지 생성 + GraphUpdateMessage message = new GraphUpdateMessage(dashboardId, requestBody); + + // RabbitMQ에 메시지 발행 + rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); + } + + From a41beea87d23db0b49b43966fcff3e9aacabeea1 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:19:13 +0900 Subject: [PATCH 05/31] =?UTF-8?q?feat=20:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=90=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApiV1DashboardController.java | 6 ++-- .../extraComponent/GraphUpdateConsumer.java | 32 +++++++++++++++++++ .../dashboard/service/DashboardService.java | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java index 9e12f49c..6b04aa68 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java @@ -33,18 +33,18 @@ public class ApiV1DashboardController { */ @PutMapping("/{dashboardId}/graph") @Operation(summary = "React-flow 데이터 저장(갱신)") - public ResponseEntity> updateGraph( + public ResponseEntity> queueGraphUpdate( @PathVariable Integer dashboardId, @RequestBody String requestBody, @RequestHeader("Liveblocks-Signature") String signature ) { - dashboardService.verifyAndUpdateGraph(dashboardId, requestBody, signature); + dashboardService.queueGraphUpdate(dashboardId, requestBody, signature); return ResponseEntity .status(HttpStatus.OK) .body(new RsData<>( "200", - "React-flow 데이터를 저장했습니다.", + "데이터 업데이트 요청이 성공적으로 접수되었습니다.", null )); } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java new file mode 100644 index 00000000..d8f663f1 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java @@ -0,0 +1,32 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; +import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GraphUpdateConsumer { + private final DashboardService dashboardService; + private final ObjectMapper objectMapper; + + @RabbitListener(queues = "graph.update.queue") + public void handleGraphUpdate(GraphUpdateMessage message) { + log.info("Received graph update message for dashboardId: {}", message.dashboardId()); + try { + BodyForReactFlow dto = objectMapper.readValue(message.requestBody(), BodyForReactFlow.class); + dashboardService.updateGraph(message.dashboardId(), dto); + log.info("Successfully updated graph for dashboardId: {}", message.dashboardId()); + } catch (Exception e) { + // 실제 운영에서는 메시지를 재시도하거나, 실패 큐(Dead Letter Queue)로 보내는 등의 + // 정교한 에러 처리 로직이 필요합니다. + log.error("Failed to process graph update for dashboardId: {}", message.dashboardId(), e); + } + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java index 0cb7ab69..c1f18e7d 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java @@ -120,7 +120,7 @@ public void verifyAccessPermission(Member member, Integer dashboardId) throws Ac * @param requestBody 요청 바디 * @param signatureHeader 서명 헤더 */ - public void queueGraphUdate(Integer dashboardId, String requestBody, String signatureHeader){ + public void queueGraphUpdate(Integer dashboardId, String requestBody, String signatureHeader){ // 서명 검증은 동기적으로 즉시 처리 if (!signatureService.isValidSignature(requestBody, signatureHeader)) { throw new SecurityException("Invalid webhook signature."); From afdfdb0c34a45abfbcd707eb0b7d6481282d701b Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:38:39 +0900 Subject: [PATCH 06/31] =?UTF-8?q?feat=20:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=9A=94=EC=B2=AD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../controller/ApiV1DashboardController.java | 4 +-- .../controller/DashboardControllerTest.java | 35 ++++++++++--------- .../testSupport/ControllerTestSupport.java | 12 +++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 8a276fa1..b135ff17 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,9 @@ dependencies { // RabbitMQ (Spring starter) implementation 'org.springframework.boot:spring-boot-starter-amqp' testImplementation 'org.springframework.amqp:spring-rabbit-test' + + // Awaitility (비동기 테스트 지원) + testImplementation 'org.awaitility:awaitility:4.2.0' } dependencyManagement { diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java index 6b04aa68..45e59df1 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java @@ -41,9 +41,9 @@ public ResponseEntity> queueGraphUpdate( dashboardService.queueGraphUpdate(dashboardId, requestBody, signature); return ResponseEntity - .status(HttpStatus.OK) + .status(HttpStatus.ACCEPTED) .body(new RsData<>( - "200", + "202", "데이터 업데이트 요청이 성공적으로 접수되었습니다.", null )); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index 5263a2e0..dd357d08 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -15,6 +15,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; +import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; @@ -29,7 +30,10 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -52,6 +56,9 @@ class DashboardControllerTest extends ControllerTestSupport { @Autowired private MembershipService membershipService; + @Autowired + private DashboardRepository dashboardRepository; + private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; @@ -149,35 +156,29 @@ void getGraph_Fail_NotFound() throws Exception { @Test @WithUserDetails(value = "KAKAO:dc1111", setupBefore = TestExecutionEvent.TEST_METHOD) - @DisplayName("대시보드 그래프 데이터 저장 - 성공") + @DisplayName("대시보드 그래프 데이터 저장 요청 - 성공") void updateGraph_Success() throws Exception { // Given String url = String.format("/api/v1/dashboard/%d/graph", authorizedDashboardId); String requestBody = createReactFlowJsonBody(); String validSignature = generateLiveblocksSignature(requestBody); - // When: 데이터 저장 + // When: 데이터 저장 요청 (메세지 발행) ResultActions updateResult = mvc.perform(put(url) .contentType(MediaType.APPLICATION_JSON) .header("Liveblocks-Signature", validSignature) // ★ 서명 헤더 추가 .content(requestBody)); - // Then: 저장 성공 응답 확인 - expectOk( - updateResult, - "React-flow 데이터를 저장했습니다." - ); - - // When: 데이터 재조회하여 검증 - ResultActions getResult = performGet(url); + // Then: 요청 접수 성공 응답 확인 + expectAccepted(updateResult, "데이터 업데이트 요청이 성공적으로 접수되었습니다."); - // Then: 재조회 결과가 수정한 데이터와 일치하는지 확인 - getResult - .andExpect(jsonPath("$.data.nodes", hasSize(2))) - .andExpect(jsonPath("$.data.edges", hasSize(1))) - .andExpect(jsonPath("$.data.nodes[0].id").value("1")) - .andExpect(jsonPath("$.data.nodes[0].data.title").value("노드1")) - .andExpect(jsonPath("$.data.edges[0].id").value("e1-2")); + // Then: (비동기 검증) 최종적으로 DB에 데이터가 반영될 때까지 최대 5초간 기다립니다. + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + }); } @Test diff --git a/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java b/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java index f0d64e8a..d2e74e9f 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java +++ b/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java @@ -152,6 +152,18 @@ protected void expectCreated(ResultActions resultActions, String msg) throws Exc .andExpect(jsonPath("$.msg").value(msg)); } + /** + * 202 Accepted 응답을 기대하는 헬퍼 메서드 + * @param resultActions - MockMvc의 ResultActions 객체 + * @param msg - 기대하는 메시지 + * @throws Exception - 예외 발생 시 던짐 + */ + protected void expectAccepted(ResultActions resultActions, String msg) throws Exception { + resultActions.andExpect(status().isAccepted()) + .andExpect(jsonPath("$.status").value("202")) + .andExpect(jsonPath("$.msg").value(msg)); + } + /** * 400 Bad Request 응답을 기대하는 헬퍼 메서드 * @param resultActions - MockMvc의 ResultActions 객체 From f76bf0c838624b1621babab9a85879afcefe2bdc Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:48:17 +0900 Subject: [PATCH 07/31] =?UTF-8?q?feat=20:=20ConsumerTest=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GraphUpdateConsumerTest.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java new file mode 100644 index 00000000..31978f73 --- /dev/null +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -0,0 +1,95 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; +import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; +import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.awaitility.Awaitility.await; + +@SpringBootTest +@Transactional +class GraphUpdateConsumerTest extends ControllerTestSupport { + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private DashboardRepository dashboardRepository; + + @Autowired + SpaceService spaceService; + + // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) + private Integer existingDashboardId; + + @BeforeEach + void setUp(){ + spaceService.createSpace("TestSpace1_forGraphUpdateConsumerTest", "thumb1"); + } + + @Test + @DisplayName("큐에 업데이트 메시지가 들어오면 컨슈머가 DB를 성공적으로 업데이트한다") + void handleGraphUpdate_Success() throws Exception { + // Given + // ControllerTestSupport의 setUp에서 생성된 대시보드 ID를 가져옵니다. + existingDashboardId = spaceService.findByName("TestSpace1_forGraphUpdateConsumerTest").getDashboard().getId(); + + String requestBody = createReactFlowJsonBody(); // 테스트용 JSON 데이터 + GraphUpdateMessage message = new GraphUpdateMessage(existingDashboardId, requestBody); + + // When: 테스트에서 직접 RabbitMQ에 메시지를 발행합니다. + rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); + + // Then: 컨슈머가 메시지를 처리하여 DB가 변경될 때까지 최대 5초간 기다립니다. + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + Graph updatedGraph = dashboardRepository.findById(existingDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + }); + } + + private String createReactFlowJsonBody() { + return """ + { + "nodes": [ + { + "id": "1", + "type": "CUSTOM", + "data": { "title": "노드1", "description": "설명1" }, + "position": { "x": 100, "y": 200 } + }, + { + "id": "2", + "type": "CUSTOM", + "data": { "title": "노드2" }, + "position": { "x": 300, "y": 400 } + } + ], + "edges": [ + { + "id": "e1-2", + "source": "1", + "target": "2", + "type": "SMOOTHSTEP", + "animated": true, + "style": { "stroke": "#999", "strokeWidth": 2.0 } + } + ] + } + """; + } + +} \ No newline at end of file From 45505854062042f8f4fb6b42b523719b752f716c Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 14:11:24 +0900 Subject: [PATCH 08/31] =?UTF-8?q?fix=20:=20=EC=A0=80=EC=9E=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/entity/Dashboard.java | 2 +- .../controller/DashboardControllerTest.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java index b13006c4..74da4291 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java @@ -21,7 +21,7 @@ public class Dashboard extends BaseEntity { // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "graph_id") private Graph graph; diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index dd357d08..a7b8215f 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -14,6 +14,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; @@ -59,6 +60,9 @@ class DashboardControllerTest extends ControllerTestSupport { @Autowired private DashboardRepository dashboardRepository; + @Autowired + private TransactionTemplate transactionTemplate; + private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; @@ -174,10 +178,14 @@ void updateGraph_Success() throws Exception { // Then: (비동기 검증) 최종적으로 DB에 데이터가 반영될 때까지 최대 5초간 기다립니다. await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { - Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); - assertThat(updatedGraph.getNodes()).hasSize(2); - assertThat(updatedGraph.getEdges()).hasSize(1); - assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + // [수정] transactionTemplate을 사용하여 트랜잭션 내에서 검증 로직을 실행 + transactionTemplate.execute(status -> { + Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + return null; // execute 메서드는 반환값이 필요 + }); }); } From b0c8baa3d94814ba5e0966d7bb93b90737a8ed36 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 17:30:15 +0900 Subject: [PATCH 09/31] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DashboardControllerTest.java | 50 +++++++++++-------- .../GraphUpdateConsumerTest.java | 42 +++++++++------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index a7b8215f..dbc3e07e 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -10,19 +10,21 @@ import org.springframework.http.MediaType; import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; +import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; +import org.tuna.zoopzoop.backend.domain.space.membership.repository.MembershipRepository; import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.repository.SpaceRepository; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; @@ -35,11 +37,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @@ -48,23 +47,21 @@ @Transactional @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DashboardControllerTest extends ControllerTestSupport { - @Autowired - private SpaceService spaceService; + @Autowired private SpaceService spaceService; + @Autowired private MemberService memberService; + @Autowired private MembershipService membershipService; - @Autowired - private MemberService memberService; + @Autowired SpaceRepository spaceRepository; + @Autowired MemberRepository memberRepository; + @Autowired MembershipRepository membershipRepository; - @Autowired - private MembershipService membershipService; + @Autowired private TransactionTemplate transactionTemplate; - @Autowired - private DashboardRepository dashboardRepository; - - @Autowired - private TransactionTemplate transactionTemplate; - - private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; + private Integer authorizedDashboardId; + + private String authorizedSpaceName = "TestSpace1_forDashboardControllerTest"; + private String unauthorizedSpaceName = "TestSpace2_forDashboardControllerTest"; @Value("${liveblocks.secret-key}") private String testSecretKey; @@ -76,8 +73,8 @@ void setUp() { memberService.createMember("tester2_forDashboardControllerTest", "url", "dc2222", Provider.KAKAO); // 2. 스페이스 생성 (생성과 동시에 대시보드도 생성됨) - Space space1 = spaceService.createSpace("TestSpace1_forDashboardControllerTest", "thumb1"); - Space space2 = spaceService.createSpace("TestSpace2_forDashboardControllerTest", "thumb2"); + Space space1 = spaceService.createSpace(authorizedSpaceName, "thumb1"); + Space space2 = spaceService.createSpace(unauthorizedSpaceName, "thumb2"); // 테스트에서 사용할 대시보드 ID 저장 this.authorizedDashboardId = space1.getDashboard().getId(); @@ -98,6 +95,14 @@ void setUp() { ); } + @AfterAll + void tearDown() { + // 멤버십, 스페이스, 멤버 모두 삭제 + membershipRepository.deleteAll(); + spaceRepository.deleteAll(); + memberRepository.deleteAll(); + } + // ============================= GET GRAPH ============================= // @Test @@ -180,7 +185,8 @@ void updateGraph_Success() throws Exception { await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { // [수정] transactionTemplate을 사용하여 트랜잭션 내에서 검증 로직을 실행 transactionTemplate.execute(status -> { - Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + Space space = spaceService.findByName(authorizedSpaceName); + Graph updatedGraph = space.getDashboard().getGraph(); assertThat(updatedGraph.getNodes()).hasSize(2); assertThat(updatedGraph.getEdges()).hasSize(1); assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 31978f73..407b53fc 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -1,16 +1,16 @@ package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; @@ -22,30 +22,29 @@ @SpringBootTest @Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class GraphUpdateConsumerTest extends ControllerTestSupport { - @Autowired - private RabbitTemplate rabbitTemplate; - - @Autowired - private DashboardRepository dashboardRepository; - - @Autowired - SpaceService spaceService; + @Autowired private RabbitTemplate rabbitTemplate; + @Autowired private TransactionTemplate transactionTemplate; + @Autowired SpaceService spaceService; // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) private Integer existingDashboardId; - @BeforeEach + private String existingSpaceName = "TestSpace1_forGraphUpdateConsumerTest"; + + @BeforeAll void setUp(){ - spaceService.createSpace("TestSpace1_forGraphUpdateConsumerTest", "thumb1"); + spaceService.createSpace(existingSpaceName, "thumb1"); } + @Test @DisplayName("큐에 업데이트 메시지가 들어오면 컨슈머가 DB를 성공적으로 업데이트한다") void handleGraphUpdate_Success() throws Exception { // Given // ControllerTestSupport의 setUp에서 생성된 대시보드 ID를 가져옵니다. - existingDashboardId = spaceService.findByName("TestSpace1_forGraphUpdateConsumerTest").getDashboard().getId(); + existingDashboardId = spaceService.findByName(existingSpaceName).getDashboard().getId(); String requestBody = createReactFlowJsonBody(); // 테스트용 JSON 데이터 GraphUpdateMessage message = new GraphUpdateMessage(existingDashboardId, requestBody); @@ -55,9 +54,14 @@ void handleGraphUpdate_Success() throws Exception { // Then: 컨슈머가 메시지를 처리하여 DB가 변경될 때까지 최대 5초간 기다립니다. await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { - Graph updatedGraph = dashboardRepository.findById(existingDashboardId).get().getGraph(); - assertThat(updatedGraph.getNodes()).hasSize(2); - assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + transactionTemplate.execute(status -> { + Space space = spaceService.findByName(existingSpaceName); + Graph updatedGraph = space.getDashboard().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + return null; + }); }); } From 89acf55e46eaea8c045bb4beb0f2b14729bab7b4 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 09:29:44 +0900 Subject: [PATCH 10/31] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/controller/DashboardControllerTest.java | 1 - .../backend/domain/dashboard/service/GraphServiceTest.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index dbc3e07e..cd9f2ec9 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -43,7 +43,6 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) @Transactional @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DashboardControllerTest extends ControllerTestSupport { diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java index 3a2cbe62..7e9af41c 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; From befdbd8ee2536027d20daa1f0db4ac2194f16983 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 12:36:30 +0900 Subject: [PATCH 11/31] =?UTF-8?q?feat=20:=20dlq=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/mq/RabbitMQConfig.java | 32 ++++++++++++++++--- src/main/resources/application.yml | 8 ++++- .../GraphUpdateConsumerTest.java | 4 +-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java index d4e6a10a..080efd70 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java +++ b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java @@ -1,9 +1,6 @@ package org.tuna.zoopzoop.backend.global.mq; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; @@ -17,6 +14,10 @@ public class RabbitMQConfig { private static final String QUEUE_NAME = "graph.update.queue"; private static final String ROUTING_KEY = "graph.update.#"; + private static final String DLQ_EXCHANGE_NAME = EXCHANGE_NAME + ".dlx"; + private static final String DLQ_QUEUE_NAME = QUEUE_NAME + ".dlq"; + private static final String DLQ_ROUTING_KEY = "graph.update.dlq"; + @Bean public TopicExchange exchange() { return new TopicExchange(EXCHANGE_NAME); @@ -24,7 +25,10 @@ public TopicExchange exchange() { @Bean public Queue queue() { - return new Queue(QUEUE_NAME); + return QueueBuilder.durable(QUEUE_NAME) + .withArgument("x-dead-letter-exchange", DLQ_EXCHANGE_NAME) // 실패 시 메시지를 보낼 Exchange + .withArgument("x-dead-letter-routing-key", DLQ_ROUTING_KEY) // 실패 시 사용할 라우팅 키 + .build(); } @Bean @@ -32,6 +36,24 @@ public Binding binding(Queue queue, TopicExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); } + // ================= DLQ 인프라 구성 추가 ================= // + + @Bean + public TopicExchange dlqExchange() { + return new TopicExchange(DLQ_EXCHANGE_NAME); + } + + @Bean + public Queue dlqQueue() { + return new Queue(DLQ_QUEUE_NAME); + } + + @Bean + public Binding dlqBinding(Queue dlqQueue, TopicExchange dlqExchange) { + return BindingBuilder.bind(dlqQueue).to(dlqExchange).with(DLQ_ROUTING_KEY); + } + + // ================= DLQ 인프라 구성 추가 ================= // @Bean public MessageConverter messageConverter() { // 메시지를 JSON 형식으로 직렬화/역직렬화하는 컨버터 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2eebe2a5..227d53b1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -51,7 +51,12 @@ spring: port: 5672 username: guest password: guest - + listener: + simple: + retry: + enabled: true + initial-interval: 2000 + max-attempts: 3 springdoc: default-produces-media-type: application/json;charset=UTF-8 logging: @@ -60,6 +65,7 @@ logging: org.hibernate.orm.jdbc.extract: TRACE org.springframework.transaction.interceptor: TRACE com.back: DEBUG + org.springframework.retry: DEBUG server: port: 8080 diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 407b53fc..868b33f7 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -1,12 +1,10 @@ package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; -import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; From 6e0d280f0df779d050351a775c3a7a1d0fdbf016 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 14:01:48 +0900 Subject: [PATCH 12/31] =?UTF-8?q?feat=20:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EB=B3=B4=EC=9E=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20version=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoopzoop/backend/domain/dashboard/entity/Graph.java | 5 +++++ .../dashboard/extraComponent/GraphUpdateConsumer.java | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java index 7f70b105..7715d2d6 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java @@ -3,6 +3,7 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; import lombok.Getter; import lombok.Setter; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; @@ -14,6 +15,10 @@ @Setter @Entity public class Graph extends BaseEntity { + + @Version + private Long version; + @OneToMany(mappedBy = "graph", cascade = CascadeType.ALL, orphanRemoval = true) private List nodes = new ArrayList<>(); diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java index d8f663f1..fffdb834 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; @@ -23,6 +24,12 @@ public void handleGraphUpdate(GraphUpdateMessage message) { BodyForReactFlow dto = objectMapper.readValue(message.requestBody(), BodyForReactFlow.class); dashboardService.updateGraph(message.dashboardId(), dto); log.info("Successfully updated graph for dashboardId: {}", message.dashboardId()); + } catch (ObjectOptimisticLockingFailureException e) { + // Optimistic Lock 충돌 발생! + // 내가 처리하려던 메시지는 이미 구버전 데이터에 대한 요청이었음. + // 따라서 이 메시지는 무시하고 정상 처리된 것으로 간주. + log.warn("Stale update attempt for dashboardId: {}. A newer version already exists. Discarding message.", message.dashboardId()); + // 예외를 다시 던지지 않으므로, 메시지는 큐에서 정상적으로 제거(ACK)됩니다. } catch (Exception e) { // 실제 운영에서는 메시지를 재시도하거나, 실패 큐(Dead Letter Queue)로 보내는 등의 // 정교한 에러 처리 로직이 필요합니다. From d29ed3d4ddc8cc4576ad967b276d05d4ec570b2b Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 14:51:37 +0900 Subject: [PATCH 13/31] =?UTF-8?q?refactor=20:=20MQConfig=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/dashboard/service/SignatureService.java | 5 +++++ .../backend/global/{ => config}/mq/RabbitMQConfig.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) rename src/main/java/org/tuna/zoopzoop/backend/global/{ => config}/mq/RabbitMQConfig.java (97%) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java index 0c06021e..242e925b 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java @@ -26,6 +26,11 @@ public class SignatureService { * @return 서명이 유효하면 true, 그렇지 않으면 false */ public boolean isValidSignature(String requestBody, String signatureHeader) { + // [임시 코드] 로컬 테스트를 위해 무조건 true 반환 +// if ("true".equals(System.getProperty("local.test.skip.signature"))) { +// return true; +// } + try { // 1. 헤더 파싱 String[] parts = signatureHeader.split(","); diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java similarity index 97% rename from src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java rename to src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java index 080efd70..9c2ff21c 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java +++ b/src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java @@ -1,4 +1,4 @@ -package org.tuna.zoopzoop.backend.global.mq; +package org.tuna.zoopzoop.backend.global.config.mq; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.ConnectionFactory; From a9d1960435f42bbf6f585967457a12cb1703e166 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 11:14:00 +0900 Subject: [PATCH 14/31] =?UTF-8?q?chore=20:=20RabbitMQ=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Conflicts: # docker-compose.yml # src/main/resources/application.yml --- src/main/resources/application.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7b594c9f..59250437 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,6 +40,17 @@ spring: port: 5672 username: ${SPRING_RABBITMQ_USERNAME:guest} password: ${SPRING_RABBITMQ_PASSWORD:guest} + data: #RedisTemplate 등을 사용하기 위한 직접 연결용 + redis: + host: localhost + port: 6379 + timeout: 6000 + cache: #Spring Cache를 사용하기 위한 Redis + type: redis + redis: + time-to-live: 300000 + cache-null-values: false + key-prefix: springdoc: default-produces-media-type: application/json;charset=UTF-8 From 8158ad570006530f10d7a4d7efe5f76a55347a6c Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 11:35:32 +0900 Subject: [PATCH 15/31] =?UTF-8?q?chore=20:=20CI=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=EC=97=90=EC=84=9C=20RabbitMQ=20?= =?UTF-8?q?=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=EB=A5=BC=20=EB=9D=84?= =?UTF-8?q?=EC=9B=8C=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test-server-ci.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/test-server-ci.yml b/.github/workflows/test-server-ci.yml index ed2b5349..14655ca7 100644 --- a/.github/workflows/test-server-ci.yml +++ b/.github/workflows/test-server-ci.yml @@ -23,6 +23,20 @@ jobs: env: SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + + # CI 작업이 실행되는 동안 RabbitMQ 서비스 컨테이너를 함께 실행 + services: + rabbitmq: + image: rabbitmq:3-management + ports: + - 5672:5672 + # RabbitMQ가 완전히 준비될 때까지 기다리는 상태 확인 옵션 + options: >- + --health-cmd "rabbitmq-diagnostics check_running" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: # 1. 소스 코드 체크아웃 - name: Checkout source code @@ -74,6 +88,12 @@ jobs: # 7. Gradle 테스트 실행 - name: Test with Gradle + # 테스트 단계에서 RabbitMQ 연결을 위한 환경 변수 설정 + env: + SPRING_RABBITMQ_HOST: localhost + SPRING_RABBITMQ_PORT: 5672 + SPRING_RABBITMQ_USERNAME: guest + SPRING_RABBITMQ_PASSWORD: guest run: ./gradlew test # 8. 테스트 결과 요약 출력 From 7dae0366e7e10365f4afd0030a6f700a85081259 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:05:33 +0900 Subject: [PATCH 16/31] =?UTF-8?q?new=20:=20RabbitMQ=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=81=B4=EB=9E=98=EC=8A=A4,=20dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +- .../dashboard/dto/GraphUpdateMessage.java | 7 +++ .../backend/global/mq/RabbitMQConfig.java | 47 +++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java create mode 100644 src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java diff --git a/build.gradle b/build.gradle index 8ec30a42..8a276fa1 100644 --- a/build.gradle +++ b/build.gradle @@ -116,8 +116,9 @@ dependencies { // Redis (Spring starter) implementation 'org.springframework.boot:spring-boot-starter-data-redis' - // RabbitMQ + // RabbitMQ (Spring starter) implementation 'org.springframework.boot:spring-boot-starter-amqp' + testImplementation 'org.springframework.amqp:spring-rabbit-test' } dependencyManagement { diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java new file mode 100644 index 00000000..6ea10ede --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/dto/GraphUpdateMessage.java @@ -0,0 +1,7 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.dto; + +public record GraphUpdateMessage( + Integer dashboardId, + String requestBody +){ +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java new file mode 100644 index 00000000..d4e6a10a --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java @@ -0,0 +1,47 @@ +package org.tuna.zoopzoop.backend.global.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + private static final String EXCHANGE_NAME = "zoopzoop.exchange"; + private static final String QUEUE_NAME = "graph.update.queue"; + private static final String ROUTING_KEY = "graph.update.#"; + + @Bean + public TopicExchange exchange() { + return new TopicExchange(EXCHANGE_NAME); + } + + @Bean + public Queue queue() { + return new Queue(QUEUE_NAME); + } + + @Bean + public Binding binding(Queue queue, TopicExchange exchange) { + return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); + } + + @Bean + public MessageConverter messageConverter() { + // 메시지를 JSON 형식으로 직렬화/역직렬화하는 컨버터 + return new Jackson2JsonMessageConverter(); + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); + rabbitTemplate.setMessageConverter(messageConverter); + return rabbitTemplate; + } +} From 307a1cafe9588cacdab7a6649211f7356833f986 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:12:46 +0900 Subject: [PATCH 17/31] =?UTF-8?q?feat=20:=20producer=20method=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/service/DashboardService.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java index b6ab567c..67f212f6 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java @@ -3,9 +3,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.NoResultException; import lombok.RequiredArgsConstructor; +import org.apache.commons.codec.binary.Hex; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Dashboard; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; @@ -25,6 +29,7 @@ public class DashboardService { private final MembershipService membershipService; private final ObjectMapper objectMapper; private final SignatureService signatureService; + private final RabbitTemplate rabbitTemplate; @@ -103,6 +108,33 @@ public void verifyAccessPermission(Member member, Integer dashboardId) throws Ac } } + // =========================== message 관리 메서드 =========================== + + /** + * Graph 업데이트 요청을 RabbitMQ 큐에 비동기적으로 발행하는 메서드 + * @param dashboardId 대시보드 ID + * @param requestBody 요청 바디 + * @param signatureHeader 서명 헤더 + */ + public void queueGraphUdate(Integer dashboardId, String requestBody, String signatureHeader){ + // 서명 검증은 동기적으로 즉시 처리 + if (!signatureService.isValidSignature(requestBody, signatureHeader)) { + throw new SecurityException("Invalid webhook signature."); + } + + // 대시보드 존재 여부 확인 + if (!dashboardRepository.existsById(dashboardId)) { + throw new NoResultException(dashboardId + " ID를 가진 대시보드를 찾을 수 없습니다."); + } + + // 큐에 보낼 메시지 생성 + GraphUpdateMessage message = new GraphUpdateMessage(dashboardId, requestBody); + + // RabbitMQ에 메시지 발행 + rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); + } + + From f44cbc006e8ed4c89aa53b984e5f54e12ad5edbe Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:19:13 +0900 Subject: [PATCH 18/31] =?UTF-8?q?feat=20:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=ED=81=90=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApiV1DashboardController.java | 6 ++-- .../extraComponent/GraphUpdateConsumer.java | 32 +++++++++++++++++++ .../dashboard/service/DashboardService.java | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java index 9e12f49c..6b04aa68 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java @@ -33,18 +33,18 @@ public class ApiV1DashboardController { */ @PutMapping("/{dashboardId}/graph") @Operation(summary = "React-flow 데이터 저장(갱신)") - public ResponseEntity> updateGraph( + public ResponseEntity> queueGraphUpdate( @PathVariable Integer dashboardId, @RequestBody String requestBody, @RequestHeader("Liveblocks-Signature") String signature ) { - dashboardService.verifyAndUpdateGraph(dashboardId, requestBody, signature); + dashboardService.queueGraphUpdate(dashboardId, requestBody, signature); return ResponseEntity .status(HttpStatus.OK) .body(new RsData<>( "200", - "React-flow 데이터를 저장했습니다.", + "데이터 업데이트 요청이 성공적으로 접수되었습니다.", null )); } diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java new file mode 100644 index 00000000..d8f663f1 --- /dev/null +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java @@ -0,0 +1,32 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; +import org.tuna.zoopzoop.backend.domain.dashboard.service.DashboardService; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GraphUpdateConsumer { + private final DashboardService dashboardService; + private final ObjectMapper objectMapper; + + @RabbitListener(queues = "graph.update.queue") + public void handleGraphUpdate(GraphUpdateMessage message) { + log.info("Received graph update message for dashboardId: {}", message.dashboardId()); + try { + BodyForReactFlow dto = objectMapper.readValue(message.requestBody(), BodyForReactFlow.class); + dashboardService.updateGraph(message.dashboardId(), dto); + log.info("Successfully updated graph for dashboardId: {}", message.dashboardId()); + } catch (Exception e) { + // 실제 운영에서는 메시지를 재시도하거나, 실패 큐(Dead Letter Queue)로 보내는 등의 + // 정교한 에러 처리 로직이 필요합니다. + log.error("Failed to process graph update for dashboardId: {}", message.dashboardId(), e); + } + } +} diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java index 67f212f6..4771b6f4 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/DashboardService.java @@ -116,7 +116,7 @@ public void verifyAccessPermission(Member member, Integer dashboardId) throws Ac * @param requestBody 요청 바디 * @param signatureHeader 서명 헤더 */ - public void queueGraphUdate(Integer dashboardId, String requestBody, String signatureHeader){ + public void queueGraphUpdate(Integer dashboardId, String requestBody, String signatureHeader){ // 서명 검증은 동기적으로 즉시 처리 if (!signatureService.isValidSignature(requestBody, signatureHeader)) { throw new SecurityException("Invalid webhook signature."); From 15b22e5bb073c61f82c5b48afd31660d5c8ae4a5 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:38:39 +0900 Subject: [PATCH 19/31] =?UTF-8?q?feat=20:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EC=9A=94=EC=B2=AD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../controller/ApiV1DashboardController.java | 4 +-- .../controller/DashboardControllerTest.java | 35 ++++++++++--------- .../testSupport/ControllerTestSupport.java | 12 +++++++ 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 8a276fa1..b135ff17 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,9 @@ dependencies { // RabbitMQ (Spring starter) implementation 'org.springframework.boot:spring-boot-starter-amqp' testImplementation 'org.springframework.amqp:spring-rabbit-test' + + // Awaitility (비동기 테스트 지원) + testImplementation 'org.awaitility:awaitility:4.2.0' } dependencyManagement { diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java index 6b04aa68..45e59df1 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/ApiV1DashboardController.java @@ -41,9 +41,9 @@ public ResponseEntity> queueGraphUpdate( dashboardService.queueGraphUpdate(dashboardId, requestBody, signature); return ResponseEntity - .status(HttpStatus.OK) + .status(HttpStatus.ACCEPTED) .body(new RsData<>( - "200", + "202", "데이터 업데이트 요청이 성공적으로 접수되었습니다.", null )); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index 5263a2e0..dd357d08 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -15,6 +15,7 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; +import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; @@ -29,7 +30,10 @@ import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.awaitility.Awaitility.await; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; @@ -52,6 +56,9 @@ class DashboardControllerTest extends ControllerTestSupport { @Autowired private MembershipService membershipService; + @Autowired + private DashboardRepository dashboardRepository; + private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; @@ -149,35 +156,29 @@ void getGraph_Fail_NotFound() throws Exception { @Test @WithUserDetails(value = "KAKAO:dc1111", setupBefore = TestExecutionEvent.TEST_METHOD) - @DisplayName("대시보드 그래프 데이터 저장 - 성공") + @DisplayName("대시보드 그래프 데이터 저장 요청 - 성공") void updateGraph_Success() throws Exception { // Given String url = String.format("/api/v1/dashboard/%d/graph", authorizedDashboardId); String requestBody = createReactFlowJsonBody(); String validSignature = generateLiveblocksSignature(requestBody); - // When: 데이터 저장 + // When: 데이터 저장 요청 (메세지 발행) ResultActions updateResult = mvc.perform(put(url) .contentType(MediaType.APPLICATION_JSON) .header("Liveblocks-Signature", validSignature) // ★ 서명 헤더 추가 .content(requestBody)); - // Then: 저장 성공 응답 확인 - expectOk( - updateResult, - "React-flow 데이터를 저장했습니다." - ); - - // When: 데이터 재조회하여 검증 - ResultActions getResult = performGet(url); + // Then: 요청 접수 성공 응답 확인 + expectAccepted(updateResult, "데이터 업데이트 요청이 성공적으로 접수되었습니다."); - // Then: 재조회 결과가 수정한 데이터와 일치하는지 확인 - getResult - .andExpect(jsonPath("$.data.nodes", hasSize(2))) - .andExpect(jsonPath("$.data.edges", hasSize(1))) - .andExpect(jsonPath("$.data.nodes[0].id").value("1")) - .andExpect(jsonPath("$.data.nodes[0].data.title").value("노드1")) - .andExpect(jsonPath("$.data.edges[0].id").value("e1-2")); + // Then: (비동기 검증) 최종적으로 DB에 데이터가 반영될 때까지 최대 5초간 기다립니다. + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + }); } @Test diff --git a/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java b/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java index f0d64e8a..d2e74e9f 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java +++ b/src/test/java/org/tuna/zoopzoop/backend/testSupport/ControllerTestSupport.java @@ -152,6 +152,18 @@ protected void expectCreated(ResultActions resultActions, String msg) throws Exc .andExpect(jsonPath("$.msg").value(msg)); } + /** + * 202 Accepted 응답을 기대하는 헬퍼 메서드 + * @param resultActions - MockMvc의 ResultActions 객체 + * @param msg - 기대하는 메시지 + * @throws Exception - 예외 발생 시 던짐 + */ + protected void expectAccepted(ResultActions resultActions, String msg) throws Exception { + resultActions.andExpect(status().isAccepted()) + .andExpect(jsonPath("$.status").value("202")) + .andExpect(jsonPath("$.msg").value(msg)); + } + /** * 400 Bad Request 응답을 기대하는 헬퍼 메서드 * @param resultActions - MockMvc의 ResultActions 객체 From 40ce2b4f3ba8f2d49424e6788f610aa4d0451660 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 12:48:17 +0900 Subject: [PATCH 20/31] =?UTF-8?q?feat=20:=20ConsumerTest=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GraphUpdateConsumerTest.java | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java new file mode 100644 index 00000000..31978f73 --- /dev/null +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -0,0 +1,95 @@ +package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; +import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; +import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; +import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; +import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; +import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.awaitility.Awaitility.await; + +@SpringBootTest +@Transactional +class GraphUpdateConsumerTest extends ControllerTestSupport { + @Autowired + private RabbitTemplate rabbitTemplate; + + @Autowired + private DashboardRepository dashboardRepository; + + @Autowired + SpaceService spaceService; + + // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) + private Integer existingDashboardId; + + @BeforeEach + void setUp(){ + spaceService.createSpace("TestSpace1_forGraphUpdateConsumerTest", "thumb1"); + } + + @Test + @DisplayName("큐에 업데이트 메시지가 들어오면 컨슈머가 DB를 성공적으로 업데이트한다") + void handleGraphUpdate_Success() throws Exception { + // Given + // ControllerTestSupport의 setUp에서 생성된 대시보드 ID를 가져옵니다. + existingDashboardId = spaceService.findByName("TestSpace1_forGraphUpdateConsumerTest").getDashboard().getId(); + + String requestBody = createReactFlowJsonBody(); // 테스트용 JSON 데이터 + GraphUpdateMessage message = new GraphUpdateMessage(existingDashboardId, requestBody); + + // When: 테스트에서 직접 RabbitMQ에 메시지를 발행합니다. + rabbitTemplate.convertAndSend("zoopzoop.exchange", "graph.update.rk", message); + + // Then: 컨슈머가 메시지를 처리하여 DB가 변경될 때까지 최대 5초간 기다립니다. + await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { + Graph updatedGraph = dashboardRepository.findById(existingDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + }); + } + + private String createReactFlowJsonBody() { + return """ + { + "nodes": [ + { + "id": "1", + "type": "CUSTOM", + "data": { "title": "노드1", "description": "설명1" }, + "position": { "x": 100, "y": 200 } + }, + { + "id": "2", + "type": "CUSTOM", + "data": { "title": "노드2" }, + "position": { "x": 300, "y": 400 } + } + ], + "edges": [ + { + "id": "e1-2", + "source": "1", + "target": "2", + "type": "SMOOTHSTEP", + "animated": true, + "style": { "stroke": "#999", "strokeWidth": 2.0 } + } + ] + } + """; + } + +} \ No newline at end of file From 65a63e783c77db5b1e8242a5d5480ecc7c38a1ac Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 14:11:24 +0900 Subject: [PATCH 21/31] =?UTF-8?q?fix=20:=20=EC=A0=80=EC=9E=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=84=B1?= =?UTF-8?q?=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/entity/Dashboard.java | 2 +- .../controller/DashboardControllerTest.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java index b13006c4..74da4291 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java @@ -21,7 +21,7 @@ public class Dashboard extends BaseEntity { // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "graph_id") private Graph graph; diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index dd357d08..a7b8215f 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -14,6 +14,7 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; @@ -59,6 +60,9 @@ class DashboardControllerTest extends ControllerTestSupport { @Autowired private DashboardRepository dashboardRepository; + @Autowired + private TransactionTemplate transactionTemplate; + private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; @@ -174,10 +178,14 @@ void updateGraph_Success() throws Exception { // Then: (비동기 검증) 최종적으로 DB에 데이터가 반영될 때까지 최대 5초간 기다립니다. await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { - Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); - assertThat(updatedGraph.getNodes()).hasSize(2); - assertThat(updatedGraph.getEdges()).hasSize(1); - assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + // [수정] transactionTemplate을 사용하여 트랜잭션 내에서 검증 로직을 실행 + transactionTemplate.execute(status -> { + Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + return null; // execute 메서드는 반환값이 필요 + }); }); } From 05c4f78e5803fd885cbdaf10113c8599a6e2d597 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Tue, 30 Sep 2025 17:30:15 +0900 Subject: [PATCH 22/31] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DashboardControllerTest.java | 50 +++++++++++-------- .../GraphUpdateConsumerTest.java | 42 +++++++++------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index a7b8215f..dbc3e07e 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -10,19 +10,21 @@ import org.springframework.http.MediaType; import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.GraphRepository; import org.tuna.zoopzoop.backend.domain.member.enums.Provider; +import org.tuna.zoopzoop.backend.domain.member.repository.MemberRepository; import org.tuna.zoopzoop.backend.domain.member.service.MemberService; import org.tuna.zoopzoop.backend.domain.space.membership.enums.Authority; +import org.tuna.zoopzoop.backend.domain.space.membership.repository.MembershipRepository; import org.tuna.zoopzoop.backend.domain.space.membership.service.MembershipService; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; +import org.tuna.zoopzoop.backend.domain.space.space.repository.SpaceRepository; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; @@ -35,11 +37,8 @@ import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.awaitility.Awaitility.await; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest @AutoConfigureMockMvc @@ -48,23 +47,21 @@ @Transactional @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DashboardControllerTest extends ControllerTestSupport { - @Autowired - private SpaceService spaceService; + @Autowired private SpaceService spaceService; + @Autowired private MemberService memberService; + @Autowired private MembershipService membershipService; - @Autowired - private MemberService memberService; + @Autowired SpaceRepository spaceRepository; + @Autowired MemberRepository memberRepository; + @Autowired MembershipRepository membershipRepository; - @Autowired - private MembershipService membershipService; + @Autowired private TransactionTemplate transactionTemplate; - @Autowired - private DashboardRepository dashboardRepository; - - @Autowired - private TransactionTemplate transactionTemplate; - - private Integer authorizedDashboardId; private Integer unauthorizedDashboardId; + private Integer authorizedDashboardId; + + private String authorizedSpaceName = "TestSpace1_forDashboardControllerTest"; + private String unauthorizedSpaceName = "TestSpace2_forDashboardControllerTest"; @Value("${liveblocks.secret-key}") private String testSecretKey; @@ -76,8 +73,8 @@ void setUp() { memberService.createMember("tester2_forDashboardControllerTest", "url", "dc2222", Provider.KAKAO); // 2. 스페이스 생성 (생성과 동시에 대시보드도 생성됨) - Space space1 = spaceService.createSpace("TestSpace1_forDashboardControllerTest", "thumb1"); - Space space2 = spaceService.createSpace("TestSpace2_forDashboardControllerTest", "thumb2"); + Space space1 = spaceService.createSpace(authorizedSpaceName, "thumb1"); + Space space2 = spaceService.createSpace(unauthorizedSpaceName, "thumb2"); // 테스트에서 사용할 대시보드 ID 저장 this.authorizedDashboardId = space1.getDashboard().getId(); @@ -98,6 +95,14 @@ void setUp() { ); } + @AfterAll + void tearDown() { + // 멤버십, 스페이스, 멤버 모두 삭제 + membershipRepository.deleteAll(); + spaceRepository.deleteAll(); + memberRepository.deleteAll(); + } + // ============================= GET GRAPH ============================= // @Test @@ -180,7 +185,8 @@ void updateGraph_Success() throws Exception { await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { // [수정] transactionTemplate을 사용하여 트랜잭션 내에서 검증 로직을 실행 transactionTemplate.execute(status -> { - Graph updatedGraph = dashboardRepository.findById(authorizedDashboardId).get().getGraph(); + Space space = spaceService.findByName(authorizedSpaceName); + Graph updatedGraph = space.getDashboard().getGraph(); assertThat(updatedGraph.getNodes()).hasSize(2); assertThat(updatedGraph.getEdges()).hasSize(1); assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 31978f73..407b53fc 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -1,16 +1,16 @@ package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.assertj.core.api.AbstractStringAssert; +import org.junit.jupiter.api.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Graph; -import org.tuna.zoopzoop.backend.domain.dashboard.repository.DashboardRepository; import org.tuna.zoopzoop.backend.domain.space.space.entity.Space; import org.tuna.zoopzoop.backend.domain.space.space.service.SpaceService; import org.tuna.zoopzoop.backend.testSupport.ControllerTestSupport; @@ -22,30 +22,29 @@ @SpringBootTest @Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class GraphUpdateConsumerTest extends ControllerTestSupport { - @Autowired - private RabbitTemplate rabbitTemplate; - - @Autowired - private DashboardRepository dashboardRepository; - - @Autowired - SpaceService spaceService; + @Autowired private RabbitTemplate rabbitTemplate; + @Autowired private TransactionTemplate transactionTemplate; + @Autowired SpaceService spaceService; // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) private Integer existingDashboardId; - @BeforeEach + private String existingSpaceName = "TestSpace1_forGraphUpdateConsumerTest"; + + @BeforeAll void setUp(){ - spaceService.createSpace("TestSpace1_forGraphUpdateConsumerTest", "thumb1"); + spaceService.createSpace(existingSpaceName, "thumb1"); } + @Test @DisplayName("큐에 업데이트 메시지가 들어오면 컨슈머가 DB를 성공적으로 업데이트한다") void handleGraphUpdate_Success() throws Exception { // Given // ControllerTestSupport의 setUp에서 생성된 대시보드 ID를 가져옵니다. - existingDashboardId = spaceService.findByName("TestSpace1_forGraphUpdateConsumerTest").getDashboard().getId(); + existingDashboardId = spaceService.findByName(existingSpaceName).getDashboard().getId(); String requestBody = createReactFlowJsonBody(); // 테스트용 JSON 데이터 GraphUpdateMessage message = new GraphUpdateMessage(existingDashboardId, requestBody); @@ -55,9 +54,14 @@ void handleGraphUpdate_Success() throws Exception { // Then: 컨슈머가 메시지를 처리하여 DB가 변경될 때까지 최대 5초간 기다립니다. await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> { - Graph updatedGraph = dashboardRepository.findById(existingDashboardId).get().getGraph(); - assertThat(updatedGraph.getNodes()).hasSize(2); - assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + transactionTemplate.execute(status -> { + Space space = spaceService.findByName(existingSpaceName); + Graph updatedGraph = space.getDashboard().getGraph(); + assertThat(updatedGraph.getNodes()).hasSize(2); + assertThat(updatedGraph.getEdges()).hasSize(1); + assertThat(updatedGraph.getNodes().get(0).getData().get("title")).isEqualTo("노드1"); + return null; + }); }); } From db9ef67cfebcd0a5aeacc3f780ae44403ee3d711 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 09:29:44 +0900 Subject: [PATCH 23/31] =?UTF-8?q?fix=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/controller/DashboardControllerTest.java | 1 - .../backend/domain/dashboard/service/GraphServiceTest.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index dbc3e07e..cd9f2ec9 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -43,7 +43,6 @@ @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) @Transactional @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DashboardControllerTest extends ControllerTestSupport { diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java index 3a2cbe62..7e9af41c 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; From a50b5b7a6509faa8bd7f0bdbaec0a1a7f5ce64b4 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 12:36:30 +0900 Subject: [PATCH 24/31] =?UTF-8?q?feat=20:=20dlq=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/global/mq/RabbitMQConfig.java | 32 ++++++++++++++++--- src/main/resources/application.yml | 8 ++++- .../GraphUpdateConsumerTest.java | 4 +-- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java index d4e6a10a..080efd70 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java +++ b/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java @@ -1,9 +1,6 @@ package org.tuna.zoopzoop.backend.global.mq; -import org.springframework.amqp.core.Binding; -import org.springframework.amqp.core.BindingBuilder; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; @@ -17,6 +14,10 @@ public class RabbitMQConfig { private static final String QUEUE_NAME = "graph.update.queue"; private static final String ROUTING_KEY = "graph.update.#"; + private static final String DLQ_EXCHANGE_NAME = EXCHANGE_NAME + ".dlx"; + private static final String DLQ_QUEUE_NAME = QUEUE_NAME + ".dlq"; + private static final String DLQ_ROUTING_KEY = "graph.update.dlq"; + @Bean public TopicExchange exchange() { return new TopicExchange(EXCHANGE_NAME); @@ -24,7 +25,10 @@ public TopicExchange exchange() { @Bean public Queue queue() { - return new Queue(QUEUE_NAME); + return QueueBuilder.durable(QUEUE_NAME) + .withArgument("x-dead-letter-exchange", DLQ_EXCHANGE_NAME) // 실패 시 메시지를 보낼 Exchange + .withArgument("x-dead-letter-routing-key", DLQ_ROUTING_KEY) // 실패 시 사용할 라우팅 키 + .build(); } @Bean @@ -32,6 +36,24 @@ public Binding binding(Queue queue, TopicExchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY); } + // ================= DLQ 인프라 구성 추가 ================= // + + @Bean + public TopicExchange dlqExchange() { + return new TopicExchange(DLQ_EXCHANGE_NAME); + } + + @Bean + public Queue dlqQueue() { + return new Queue(DLQ_QUEUE_NAME); + } + + @Bean + public Binding dlqBinding(Queue dlqQueue, TopicExchange dlqExchange) { + return BindingBuilder.bind(dlqQueue).to(dlqExchange).with(DLQ_ROUTING_KEY); + } + + // ================= DLQ 인프라 구성 추가 ================= // @Bean public MessageConverter messageConverter() { // 메시지를 JSON 형식으로 직렬화/역직렬화하는 컨버터 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 59250437..e51ec045 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -40,6 +40,12 @@ spring: port: 5672 username: ${SPRING_RABBITMQ_USERNAME:guest} password: ${SPRING_RABBITMQ_PASSWORD:guest} + listener: + simple: + retry: + enabled: true + initial-interval: 2000 + max-attempts: 3 data: #RedisTemplate 등을 사용하기 위한 직접 연결용 redis: host: localhost @@ -51,7 +57,6 @@ spring: time-to-live: 300000 cache-null-values: false key-prefix: - springdoc: default-produces-media-type: application/json;charset=UTF-8 logging: @@ -60,6 +65,7 @@ logging: org.hibernate.orm.jdbc.extract: TRACE org.springframework.transaction.interceptor: TRACE com.back: DEBUG + org.springframework.retry: DEBUG server: port: 8080 diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 407b53fc..868b33f7 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -1,12 +1,10 @@ package org.tuna.zoopzoop.backend.domain.dashboard.extraComponent; -import org.assertj.core.api.AbstractStringAssert; import org.junit.jupiter.api.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; -import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; From 974473e37ded8db0507194524d14532c1790c1fc Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 14:01:48 +0900 Subject: [PATCH 25/31] =?UTF-8?q?feat=20:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EB=B3=B4=EC=9E=A5=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=B4=20version=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoopzoop/backend/domain/dashboard/entity/Graph.java | 5 +++++ .../dashboard/extraComponent/GraphUpdateConsumer.java | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java index 7f70b105..7715d2d6 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Graph.java @@ -3,6 +3,7 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; import lombok.Getter; import lombok.Setter; import org.tuna.zoopzoop.backend.global.jpa.entity.BaseEntity; @@ -14,6 +15,10 @@ @Setter @Entity public class Graph extends BaseEntity { + + @Version + private Long version; + @OneToMany(mappedBy = "graph", cascade = CascadeType.ALL, orphanRemoval = true) private List nodes = new ArrayList<>(); diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java index d8f663f1..fffdb834 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumer.java @@ -4,6 +4,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Component; import org.tuna.zoopzoop.backend.domain.dashboard.dto.BodyForReactFlow; import org.tuna.zoopzoop.backend.domain.dashboard.dto.GraphUpdateMessage; @@ -23,6 +24,12 @@ public void handleGraphUpdate(GraphUpdateMessage message) { BodyForReactFlow dto = objectMapper.readValue(message.requestBody(), BodyForReactFlow.class); dashboardService.updateGraph(message.dashboardId(), dto); log.info("Successfully updated graph for dashboardId: {}", message.dashboardId()); + } catch (ObjectOptimisticLockingFailureException e) { + // Optimistic Lock 충돌 발생! + // 내가 처리하려던 메시지는 이미 구버전 데이터에 대한 요청이었음. + // 따라서 이 메시지는 무시하고 정상 처리된 것으로 간주. + log.warn("Stale update attempt for dashboardId: {}. A newer version already exists. Discarding message.", message.dashboardId()); + // 예외를 다시 던지지 않으므로, 메시지는 큐에서 정상적으로 제거(ACK)됩니다. } catch (Exception e) { // 실제 운영에서는 메시지를 재시도하거나, 실패 큐(Dead Letter Queue)로 보내는 등의 // 정교한 에러 처리 로직이 필요합니다. From fcaefd598d98c4fb7abb05b385e1e01d81227ee5 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 14:51:37 +0900 Subject: [PATCH 26/31] =?UTF-8?q?refactor=20:=20MQConfig=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9C=84=EC=B9=98=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/dashboard/service/SignatureService.java | 5 +++++ .../backend/global/{ => config}/mq/RabbitMQConfig.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) rename src/main/java/org/tuna/zoopzoop/backend/global/{ => config}/mq/RabbitMQConfig.java (97%) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java index 0c06021e..242e925b 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/service/SignatureService.java @@ -26,6 +26,11 @@ public class SignatureService { * @return 서명이 유효하면 true, 그렇지 않으면 false */ public boolean isValidSignature(String requestBody, String signatureHeader) { + // [임시 코드] 로컬 테스트를 위해 무조건 true 반환 +// if ("true".equals(System.getProperty("local.test.skip.signature"))) { +// return true; +// } + try { // 1. 헤더 파싱 String[] parts = signatureHeader.split(","); diff --git a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java b/src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java similarity index 97% rename from src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java rename to src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java index 080efd70..9c2ff21c 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/global/mq/RabbitMQConfig.java +++ b/src/main/java/org/tuna/zoopzoop/backend/global/config/mq/RabbitMQConfig.java @@ -1,4 +1,4 @@ -package org.tuna.zoopzoop.backend.global.mq; +package org.tuna.zoopzoop.backend.global.config.mq; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.ConnectionFactory; From 5d54358713043086d0804aaeba5ac0c270cf1f5b Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 15:34:23 +0900 Subject: [PATCH 27/31] =?UTF-8?q?fix=20:=20=EC=B5=9C=EC=8B=A0=20=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/dashboard/controller/DashboardControllerTest.java | 3 --- .../dashboard/extraComponent/GraphUpdateConsumerTest.java | 1 - .../backend/domain/dashboard/service/GraphServiceTest.java | 1 - .../domain/space/membership/service/MembershipServiceTest.java | 3 --- 4 files changed, 8 deletions(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java index cd9f2ec9..2e51822a 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/controller/DashboardControllerTest.java @@ -4,15 +4,12 @@ import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.security.test.context.support.TestExecutionEvent; import org.springframework.security.test.context.support.WithUserDetails; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.jdbc.Sql; import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 868b33f7..4cb3b335 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.*; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java index 7e9af41c..3a2cbe62 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/service/GraphServiceTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.dashboard.entity.Edge; diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java index 01eac5e1..8d74ea52 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/space/membership/service/MembershipServiceTest.java @@ -5,9 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.security.test.context.support.TestExecutionEvent; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import org.tuna.zoopzoop.backend.domain.member.entity.Member; From e27ebe296743848c84df025768d9dc40d4238068 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 16:01:52 +0900 Subject: [PATCH 28/31] =?UTF-8?q?fix=20:=20copilot=20review=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/extraComponent/GraphUpdateConsumerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java index 4cb3b335..4774022c 100644 --- a/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java +++ b/src/test/java/org/tuna/zoopzoop/backend/domain/dashboard/extraComponent/GraphUpdateConsumerTest.java @@ -23,7 +23,7 @@ class GraphUpdateConsumerTest extends ControllerTestSupport { @Autowired private RabbitTemplate rabbitTemplate; @Autowired private TransactionTemplate transactionTemplate; - @Autowired SpaceService spaceService; + @Autowired private SpaceService spaceService; // 테스트에 사용할 dashboardId (실제 DB에 존재하는 ID) private Integer existingDashboardId; From 29136fbf4002dcc0b7cffd70717ae8c3893c211c Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 16:03:50 +0900 Subject: [PATCH 29/31] =?UTF-8?q?feat=20:=20Dashboard=EC=97=90=EC=84=9C=20?= =?UTF-8?q?graph=20=EC=B0=B8=EC=A1=B0=20=EB=B0=A9=EC=8B=9D=20EAGER=20->=20?= =?UTF-8?q?LAZY=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoopzoop/backend/domain/dashboard/entity/Dashboard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java index 74da4291..b13006c4 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java @@ -21,7 +21,7 @@ public class Dashboard extends BaseEntity { // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "graph_id") private Graph graph; From 6022f40357ab0e1c4f460f9e8e0a80d66ea98679 Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 16:09:38 +0900 Subject: [PATCH 30/31] =?UTF-8?q?CI=20=EC=8B=A4=ED=8C=A8=ED=95=B4=EC=84=9C?= =?UTF-8?q?=20=EB=8B=A4=EC=8B=9C=20EAGER=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoopzoop/backend/domain/dashboard/entity/Dashboard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java index b13006c4..74da4291 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java @@ -21,7 +21,7 @@ public class Dashboard extends BaseEntity { // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "graph_id") private Graph graph; From 2be799bf9cc13072647a6fbfdea8b4d7aaa370ff Mon Sep 17 00:00:00 2001 From: EpicFn Date: Wed, 1 Oct 2025 16:15:13 +0900 Subject: [PATCH 31/31] =?UTF-8?q?fix=20:=20=EB=8B=A4=EC=8B=9C=20LAZY?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../zoopzoop/backend/domain/dashboard/entity/Dashboard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java index 74da4291..b13006c4 100644 --- a/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java +++ b/src/main/java/org/tuna/zoopzoop/backend/domain/dashboard/entity/Dashboard.java @@ -21,7 +21,7 @@ public class Dashboard extends BaseEntity { // 이 대시보드가 담고 있는 그래프 콘텐츠 (1:1 관계) // Cascade 설정을 통해 Dashboard 저장 시 Graph도 함께 저장되도록 함 - @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "graph_id") private Graph graph;