-
Notifications
You must be signed in to change notification settings - Fork 0
[fix] 모임방 참여 api 5xx 에러 발생 해결 #331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
6d2ddea
0567ce9
d331f9f
b3e3197
b5ff13e
ab03c5c
9fd4169
f6d805e
30c61d3
a182cc8
87e4d12
bf5ad79
51835da
37e4a4b
31f79f8
7bfe838
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package konkuk.thip.config; | ||
|
|
||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.retry.annotation.EnableRetry; | ||
|
|
||
| @Configuration | ||
| @EnableRetry(proxyTargetClass = true) | ||
| public class RetryConfig { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,8 @@ | ||
| package konkuk.thip.room.application.service; | ||
|
|
||
| import jakarta.persistence.LockTimeoutException; | ||
| import konkuk.thip.common.exception.BusinessException; | ||
| import konkuk.thip.common.exception.InvalidStateException; | ||
| import konkuk.thip.common.exception.code.ErrorCode; | ||
| import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; | ||
| import konkuk.thip.room.application.port.in.RoomJoinUseCase; | ||
|
|
@@ -14,13 +16,19 @@ | |
| import konkuk.thip.user.application.port.out.UserCommandPort; | ||
| import konkuk.thip.user.domain.User; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.retry.annotation.Backoff; | ||
| import org.springframework.retry.annotation.Recover; | ||
| import org.springframework.retry.annotation.Retryable; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Propagation; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| public class RoomJoinService implements RoomJoinUseCase { | ||
|
|
||
| private final RoomCommandPort roomCommandPort; | ||
|
|
@@ -30,13 +38,26 @@ public class RoomJoinService implements RoomJoinUseCase { | |
| private final RoomNotificationOrchestrator roomNotificationOrchestrator; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| @Retryable( | ||
| retryFor = { | ||
| LockTimeoutException.class | ||
| }, // 재시도 대상 예외 | ||
| noRetryFor = { | ||
| InvalidStateException.class, BusinessException.class | ||
| }, // 제시도 제외 예외 | ||
| maxAttempts = 2, | ||
| backoff = @Backoff(delay = 100, multiplier = 2) | ||
| ) | ||
| @Transactional(propagation = Propagation.REQUIRES_NEW) // 재시도마다 새로운 트랜잭션 | ||
| public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { | ||
| RoomJoinType type = roomJoinCommand.type(); | ||
|
|
||
| // 방이 존재하지 않거나 모집기간이 만료된 경우 예외 처리 | ||
| Room room = roomCommandPort.findById(roomJoinCommand.roomId()) | ||
| .orElseThrow(() -> new BusinessException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL)); | ||
| // Room room = roomCommandPort.findById(roomJoinCommand.roomId()) | ||
| // .orElseThrow(() -> new BusinessException(ErrorCode.USER_CANNOT_JOIN_OR_CANCEL)); | ||
|
|
||
| /** x-lock 획득하여 room 조회 **/ | ||
| Room room = roomCommandPort.getByIdForUpdate(roomJoinCommand.roomId()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p3: 존재하지 않는 방에대해 방참여/취소 요청에대한 예외를 기존에는 USER_CANNOT_JOIN_OR_CANCEL로 처리했었는데 수정된 코드에서는 ROOM_NOT_FOUND로 처리하고있는데 이렇게 수정하신 이유가 따로있나용??
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Room 엔티티 조회 시 x-lock을 걸고 조회하게끔 수정하는데 집중하다보니, 기존 코드처럼 조회가 되지 않을 경우에 대한 고려를 깊게 하지는 않았습니다! 현재 작업 중인 브랜치에서 이 부분 또한 고려해보도록 하겠습니다! |
||
|
|
||
| room.validateRoomRecruitExpired(); | ||
|
|
||
|
|
@@ -59,6 +80,21 @@ public RoomJoinResult changeJoinState(RoomJoinCommand roomJoinCommand) { | |
| return RoomJoinResult.of(room.getId(), type.getType()); | ||
| } | ||
|
|
||
| @Recover | ||
| public RoomJoinResult recoverLockTimeout(LockTimeoutException e, RoomJoinCommand roomJoinCommand) { | ||
| throw new BusinessException(ErrorCode.RESOURCE_LOCKED); | ||
| } | ||
|
|
||
| @Recover | ||
| public RoomJoinResult recoverInvalidStateException(InvalidStateException e, RoomJoinCommand roomJoinCommand) { | ||
| throw e; | ||
| } | ||
|
|
||
| @Recover | ||
| public RoomJoinResult recoverBusinessException(BusinessException e, RoomJoinCommand roomJoinCommand) { | ||
| throw e; | ||
| } | ||
|
Comment on lines
+88
to
+96
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @buzz0331 @hd0rable 이전에 방 참여 API 실행 중 도메인 로직으로 인해 발생한 InvalidStateException, BusinessException 이 @recover 메서드로 흡수되어 모두 423 error로 응답되었던 이슈를 해결했습니다 spring @retryable 로 정의한 메서드내에서 발생하는 exception을 재시도 여부와 관계없이 각 exception에 대한 @recover 메서드로 exception handling 이 필요하다고 하네요 |
||
|
|
||
| private void sendNotifications(RoomJoinCommand roomJoinCommand, Room room) { | ||
| RoomParticipant targetUser = roomParticipantCommandPort.findHostByRoomId(room.getId()); | ||
| User actorUser = userCommandPort.findById(roomJoinCommand.userId()); | ||
|
|
@@ -99,6 +135,4 @@ private void validateCancelable(RoomParticipant roomParticipant) { | |
| throw new BusinessException(ErrorCode.HOST_CANNOT_CANCEL); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| -- (user_id, room_id) 조합 유니크 제약 추가 | ||
| ALTER TABLE room_participants | ||
| ADD CONSTRAINT uk_room_participant_user_room | ||
| UNIQUE (user_id, room_id); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; | ||
| import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; | ||
| import konkuk.thip.common.util.TestEntityFactory; | ||
| import konkuk.thip.notification.application.port.in.RoomNotificationOrchestrator; | ||
| import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; | ||
| import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; | ||
| import konkuk.thip.room.domain.value.RoomParticipantRole; | ||
|
|
@@ -15,16 +16,18 @@ | |
| import konkuk.thip.user.domain.value.UserRole; | ||
| import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; | ||
| import konkuk.thip.user.domain.value.Alias; | ||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.test.context.ActiveProfiles; | ||
| import org.springframework.test.context.bean.override.mockito.MockitoBean; | ||
| import org.springframework.test.web.servlet.MockMvc; | ||
| import org.springframework.test.web.servlet.ResultActions; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.HashMap; | ||
|
|
@@ -34,17 +37,21 @@ | |
| import static konkuk.thip.room.application.port.in.dto.RoomJoinType.CANCEL; | ||
| import static konkuk.thip.room.application.port.in.dto.RoomJoinType.JOIN; | ||
| import static org.assertj.core.api.AssertionsForClassTypes.assertThat; | ||
| import static org.mockito.ArgumentMatchers.anyLong; | ||
| import static org.mockito.ArgumentMatchers.anyString; | ||
| import static org.mockito.Mockito.doNothing; | ||
| import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
| import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
|
|
||
| @SpringBootTest | ||
| @ActiveProfiles("test") | ||
| @Transactional | ||
| //@Transactional | ||
| @AutoConfigureMockMvc(addFilters = false) | ||
| @DisplayName("[통합] 방 참여/취소 API 통합 테스트") | ||
| class RoomJoinApiTest { | ||
|
|
||
| @Autowired private MockMvc mockMvc; | ||
| @MockitoBean private RoomNotificationOrchestrator roomNotificationOrchestrator; | ||
| @Autowired private ObjectMapper objectMapper; | ||
| @Autowired private RoomJpaRepository roomJpaRepository; | ||
| @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; | ||
|
|
@@ -57,6 +64,20 @@ class RoomJoinApiTest { | |
| private UserJpaEntity participant; | ||
| private RoomParticipantJpaEntity memberParticipation; | ||
|
|
||
| @BeforeEach | ||
| void mockNotification() { // 알림 서비스 모킹 | ||
| doNothing().when(roomNotificationOrchestrator) | ||
| .notifyRoomJoinToHost(anyLong(), anyLong(), anyString(), anyLong(), anyString()); | ||
| } | ||
|
|
||
| @AfterEach | ||
| void tearDown() { | ||
| roomParticipantJpaRepository.deleteAllInBatch(); | ||
| roomJpaRepository.deleteAllInBatch(); | ||
| bookJpaRepository.deleteAllInBatch(); | ||
| userJpaRepository.deleteAllInBatch(); | ||
| } | ||
|
|
||
| private void setUpWithOnlyHost() { | ||
| Alias alias = TestEntityFactory.createLiteratureAlias(); | ||
| createUsers(alias); | ||
|
|
@@ -134,7 +155,6 @@ void joinRoom_success() throws Exception { | |
| assertThat(room.getMemberCount()).isEqualTo(2); // 방 생성 시 1명 + 참여 1명 | ||
| } | ||
|
|
||
|
|
||
| @Test | ||
| @DisplayName("방 중복 참여 실패") | ||
| void joinRoom_alreadyParticipated() throws Exception { | ||
|
|
@@ -167,8 +187,8 @@ void cancelJoin_success() throws Exception { | |
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| em.flush(); | ||
| em.clear(); | ||
| // em.flush(); | ||
| // em.clear(); | ||
|
Comment on lines
+190
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 주석 처리된 영속성 컨텍스트 초기화로 인한 검증 누락 가능성이 있습니다.
🤖 Prompt for AI Agents |
||
|
|
||
| // 참여자 삭제 확인 | ||
| RoomParticipantJpaEntity member = roomParticipantJpaRepository.findById(memberParticipation.getRoomParticipantId()).orElse(null); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
소프트 딜리트와 유니크 제약 조건 조합의 데이터 정합성 문제를 해결해야 합니다.
TODO 주석에서 언급한 것처럼, 현재 구현은 중대한 제약사항이 있습니다:
status = 'INACTIVE'로 소프트 딜리트된 참여자도 유니크 제약 조건에 포함됩니다해결 방안:
WHERE status = 'ACTIVE'unique_key = CASE WHEN status='ACTIVE' THEN CONCAT(user_id,'_',room_id) ELSE NULL END)이 문제에 대한 구체적인 해결 방안을 제시해 드릴까요?
🤖 Prompt for AI Agents