From e91460cf5f655471f514159530ec79a8593f5fac Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Mon, 4 May 2026 16:20:15 +0900 Subject: [PATCH 01/19] =?UTF-8?q?fix(room):=20=EB=AA=85=EC=8B=9C=EC=A0=81?= =?UTF-8?q?=20flush=20=EC=A0=9C=EA=B1=B0=20=ED=9B=84=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=20flush=EB=A1=9C=20DB=20=EB=B0=98=EC=98=81=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ChatMessageRepository.java | 2 +- .../usecase/DeleteRoomUseCase.java | 5 +- .../usecase/KickRoommateUseCase.java | 1 - .../room/domain/service/RoomService.java | 4 - .../FlushRemovalIntegrationTest.java | 215 ++++++++++++++++++ .../unit/usecase/DeleteRoomUseCaseTest.java | 6 +- .../unit/usecase/KickRoommateUseCaseTest.java | 4 - 7 files changed, 220 insertions(+), 17 deletions(-) create mode 100644 src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java index f5bb086..f143428 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java @@ -20,7 +20,7 @@ int decreaseUnreadCount(@Param("chatRoomNo") String chatRoomNo, @Param("fromTime") LocalDateTime fromTime, @Param("userNo") String userNo); - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM ChatMessage m WHERE m.chatRoom.chatRoomNo = :chatRoomNo") void deleteByChatRoomNo(@Param("chatRoomNo") String chatRoomNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/DeleteRoomUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/DeleteRoomUseCase.java index e77cae8..65fe1ca 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/DeleteRoomUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/DeleteRoomUseCase.java @@ -33,8 +33,8 @@ public class DeleteRoomUseCase { * 방 삭제 * - 방장 권한 검증 * - 방장 외 룸메이트 존재 시 삭제 불가 - * - 방 소프트 삭제 → 룸메이트 퇴실 처리 → flush(deletedAt DB 반영) - * - RoomRequest, RoomRule, RoomLike 연관 데이터 삭제 + * - 방 소프트 삭제 → 룸메이트 퇴실 처리 + * - RoomRequest, RoomRule, RoomLike 연관 데이터 삭제 (@Modifying flushAutomatically로 자동 flush) * - 채팅방 삭제 이벤트 발행 */ public void execute(String requesterNo, String roomNo) { @@ -54,7 +54,6 @@ public void execute(String requesterNo, String roomNo) { room.delete(); roommateService.leaveRoom(requesterNo, roomNo); - roomService.flush(); roomRequestService.deleteAllByRoom(room); roomRuleService.deleteByRoomNo(roomNo); diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/KickRoommateUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/KickRoommateUseCase.java index c56b398..70bab83 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/KickRoommateUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/KickRoommateUseCase.java @@ -41,7 +41,6 @@ public void execute(String requesterNo, String roomNo, String kickedUserNo) { roommateService.leaveRoom(kickedUserNo, roomNo); room.minusCurrentMate(); - roomService.flush(); eventPublisher.publishEvent(new RoommateKickedEvent(roomNo, kickedUserNo)); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java index e2877e2..da9c00c 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java @@ -61,10 +61,6 @@ public FindRoomsResponse findMyRoom(String userNo) { .orElseThrow(() -> new RestApiException(ROOM_NOT_FOUND)); } - public void flush() { - roomRepository.flush(); - } - public List findLikedRooms(String userNo) { return roomRepository.findLikedRooms(userNo); } diff --git a/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java new file mode 100644 index 0000000..fd83119 --- /dev/null +++ b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java @@ -0,0 +1,215 @@ +package com.project.dorumdorum.domain.room.integration; + +import com.google.firebase.FirebaseApp; +import com.google.firebase.messaging.FirebaseMessaging; +import com.project.dorumdorum.domain.chat.domain.entity.ChatRoom; +import com.project.dorumdorum.domain.chat.domain.entity.ChatRoomMember; +import com.project.dorumdorum.domain.chat.domain.repository.ChatMessageRepository; +import com.project.dorumdorum.domain.chat.domain.repository.ChatRoomMemberRepository; +import com.project.dorumdorum.domain.chat.domain.repository.ChatRoomRepository; +import com.project.dorumdorum.domain.chat.domain.service.ChatMessageService; +import com.project.dorumdorum.domain.chat.domain.service.ChatRoomMemberService; +import com.project.dorumdorum.domain.checklist.domain.repository.RoomRuleRepository; +import com.project.dorumdorum.domain.room.application.usecase.DeleteRoomUseCase; +import com.project.dorumdorum.domain.room.application.usecase.KickRoommateUseCase; +import com.project.dorumdorum.domain.room.domain.entity.ResidencePeriod; +import com.project.dorumdorum.domain.room.domain.entity.Room; +import com.project.dorumdorum.domain.room.domain.entity.RoomType; +import com.project.dorumdorum.domain.room.domain.repository.RoomLikeRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomRequestRepository; +import com.project.dorumdorum.domain.roommate.domain.entity.ConfirmStatus; +import com.project.dorumdorum.domain.roommate.domain.entity.RoomRole; +import com.project.dorumdorum.domain.roommate.domain.entity.Roommate; +import com.project.dorumdorum.domain.roommate.domain.repository.RoommateRepository; +import com.project.dorumdorum.domain.user.domain.entity.Gender; +import com.project.dorumdorum.domain.user.domain.entity.Role; +import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.service.UserService; +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.context.SpringBootTest; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 명시적 flush() 제거 후 실제 DB 반영 검증 통합 테스트. + * + * 검증 대상: + * 1. KickRoommateUseCase — room.minusCurrentMate()가 flushAutomatically(BEFORE_COMMIT 리스너 + * 내 decreaseUnreadCount)를 통해 DB에 반영되는지 + * 2. DeleteRoomUseCase — room.delete()가 deleteAllByRoom(flushAutomatically=true)를 통해 + * DB에 반영되고 연관 데이터가 정리되는지 + * + * @ActiveProfiles("local-db"): 로컬 Docker PostgreSQL(dorumdorum_test DB)에 직접 연결. + * Testcontainers 없이 실제 DB로 동작을 검증한다. + */ +@SpringBootTest(properties = { + "spring.datasource.driver-class-name=org.postgresql.Driver", + "spring.datasource.url=jdbc:postgresql://localhost:5432/dorumdorum_test", + "spring.datasource.username=dorumdorum", + "spring.datasource.password=test1234", + "spring.jpa.defer-datasource-initialization=true", + "spring.jpa.hibernate.ddl-auto=create-drop", + "spring.sql.init.mode=always", + "spring.sql.init.schema-locations=classpath:schema.sql" +}) +@ActiveProfiles("test") +@DisplayName("flush() 제거 후 실제 DB 반영 통합 테스트") +class FlushRemovalIntegrationTest { + + @MockitoBean private FirebaseApp firebaseApp; + @MockitoBean private FirebaseMessaging firebaseMessaging; + @MockitoBean private SimpMessagingTemplate messagingTemplate; + + @MockitoSpyBean private UserService userService; + @MockitoSpyBean private ChatMessageService chatMessageService; + @MockitoSpyBean private ChatRoomMemberService chatRoomMemberService; + + @Autowired private KickRoommateUseCase kickRoommateUseCase; + @Autowired private DeleteRoomUseCase deleteRoomUseCase; + + @Autowired private RoomRepository roomRepository; + @Autowired private RoommateRepository roommateRepository; + @Autowired private ChatRoomRepository chatRoomRepository; + @Autowired private ChatRoomMemberRepository chatRoomMemberRepository; + @Autowired private ChatMessageRepository chatMessageRepository; + @Autowired private RoomRequestRepository roomRequestRepository; + @Autowired private RoomRuleRepository roomRuleRepository; + @Autowired private RoomLikeRepository roomLikeRepository; + + private static final String KICK_HOST_NO = "flush_test_kick_host_001"; + private static final String KICK_MEMBER_NO = "flush_test_kick_member_001"; + private static final String DELETE_HOST_NO = "flush_test_del_host_001"; + + private Room kickRoom; + private ChatRoom kickChatRoom; + private Room deleteRoom; + + @BeforeEach + void setUp() { + // ── KickRoommateUseCase 픽스처 ──────────────────────────────────────── + kickRoom = roomRepository.save(Room.builder() + .roomType(RoomType.TYPE_1).capacity(2).title("kick-flush-test") + .hostUserNo(KICK_HOST_NO).residencePeriod(ResidencePeriod.SEMESTER) + .gender(Gender.MALE).build()); + kickRoom.plusCurrentMate(); + kickRoom = roomRepository.saveAndFlush(kickRoom); + + roommateRepository.save(Roommate.builder().room(kickRoom).userNo(KICK_HOST_NO) + .roomRole(RoomRole.HOST).confirmStatus(ConfirmStatus.ACCEPTED).build()); + roommateRepository.save(Roommate.builder().room(kickRoom).userNo(KICK_MEMBER_NO) + .roomRole(RoomRole.MEMBER).confirmStatus(ConfirmStatus.ACCEPTED).build()); + + kickChatRoom = chatRoomRepository.save(ChatRoom.builder().roomNo(kickRoom.getRoomNo()).build()); + chatRoomMemberRepository.save(ChatRoomMember.builder().chatRoom(kickChatRoom).userNo(KICK_HOST_NO).build()); + chatRoomMemberRepository.save(ChatRoomMember.builder().chatRoom(kickChatRoom).userNo(KICK_MEMBER_NO).build()); + + // ── DeleteRoomUseCase 픽스처 ───────────────────────────────────────── + deleteRoom = roomRepository.save(Room.builder() + .roomType(RoomType.TYPE_1).capacity(2).title("delete-flush-test") + .hostUserNo(DELETE_HOST_NO).residencePeriod(ResidencePeriod.SEMESTER) + .gender(Gender.MALE).build()); + + roommateRepository.save(Roommate.builder().room(deleteRoom).userNo(DELETE_HOST_NO) + .roomRole(RoomRole.HOST).confirmStatus(ConfirmStatus.ACCEPTED).build()); + + stubUserService(); + stubChatMessageService(); + } + + @AfterEach + void tearDown() { + chatMessageRepository.deleteAllInBatch(); + chatRoomMemberRepository.deleteAllInBatch(); + chatRoomRepository.deleteAllInBatch(); + roomRequestRepository.deleteAllInBatch(); + roomRuleRepository.deleteAllInBatch(); + roomLikeRepository.deleteAllInBatch(); + roommateRepository.deleteAllInBatch(); + roomRepository.deleteAllInBatch(); + } + + // ========================================================================= + // KickRoommateUseCase: room.minusCurrentMate() flush 검증 + // ========================================================================= + + @Test + @DisplayName("강퇴 후 room.currentMateCount 감소가 DB에 반영된다 (explicit flush 없이)") + void kick_CurrentMateCountDecrement_PersistedWithoutExplicitFlush() { + int beforeCount = roomRepository.findById(kickRoom.getRoomNo()).orElseThrow().getCurrentMateCount(); + + kickRoommateUseCase.execute(KICK_HOST_NO, kickRoom.getRoomNo(), KICK_MEMBER_NO); + + Room persisted = roomRepository.findById(kickRoom.getRoomNo()).orElseThrow(); + assertThat(persisted.getCurrentMateCount()) + .as("minusCurrentMate()가 flush 없이도 DB에 반영되어야 한다") + .isEqualTo(beforeCount - 1); + } + + @Test + @DisplayName("강퇴 후 Roommate 삭제가 DB에 반영된다 (explicit flush 없이)") + void kick_RoommateDelete_PersistedWithoutExplicitFlush() { + kickRoommateUseCase.execute(KICK_HOST_NO, kickRoom.getRoomNo(), KICK_MEMBER_NO); + + assertThat(roommateRepository.existsByUserNoAndRoomNo(KICK_MEMBER_NO, kickRoom.getRoomNo())) + .as("Roommate 삭제가 DB에 반영되어야 한다") + .isFalse(); + } + + // ========================================================================= + // DeleteRoomUseCase: room.delete() (deletedAt) flush 검증 + // ========================================================================= + + @Test + @DisplayName("방 삭제 후 room.deletedAt이 DB에 반영된다 (explicit flush 없이)") + void delete_SoftDeletedAt_PersistedWithoutExplicitFlush() { + deleteRoomUseCase.execute(DELETE_HOST_NO, deleteRoom.getRoomNo()); + + Room persisted = roomRepository.findById(deleteRoom.getRoomNo()).orElseThrow(); + assertThat(persisted.getDeletedAt()) + .as("room.delete()가 flush 없이도 deletedAt이 DB에 반영되어야 한다") + .isNotNull(); + } + + @Test + @DisplayName("방 삭제 후 Roommate가 DB에서 제거된다 (explicit flush 없이)") + void delete_RoommateCleanup_PersistedWithoutExplicitFlush() { + deleteRoomUseCase.execute(DELETE_HOST_NO, deleteRoom.getRoomNo()); + + assertThat(roommateRepository.findByUserNo(DELETE_HOST_NO)) + .as("방 삭제 시 Roommate가 DB에서 제거되어야 한다") + .isEmpty(); + } + + // ========================================================================= + // 헬퍼 + // ========================================================================= + + private void stubUserService() { + User fakeUser = User.builder() + .nickname("테스트멤버").name("홍길동").email("flush-test@test.com") + .password("pw").role(Role.USER).studentNo("202499999").build(); + doReturn(fakeUser).when(userService).findById(KICK_MEMBER_NO); + } + + private void stubChatMessageService() { + var fakeMessage = mock(com.project.dorumdorum.domain.chat.domain.entity.ChatMessage.class); + when(fakeMessage.getMessageNo()).thenReturn("fake-flush-msg-001"); + when(fakeMessage.getCreatedAt()).thenReturn(java.time.LocalDateTime.now()); + doReturn(fakeMessage).when(chatMessageService).save(any(), anyString(), anyString(), any(), anyInt()); + } +} diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/DeleteRoomUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/DeleteRoomUseCaseTest.java index 30cdcef..ec74f96 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/DeleteRoomUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/DeleteRoomUseCaseTest.java @@ -97,7 +97,6 @@ void execute_WhenHostAlone_DeletesAndPublishesEvent() { verify(room).delete(); verify(roommateService).leaveRoom("host", "r1"); - verify(roomService).flush(); verify(roomRequestService).deleteAllByRoom(room); verify(roomRuleService).deleteByRoomNo("r1"); verify(roomLikeRepository).deleteAllByRoom(room); @@ -105,7 +104,7 @@ void execute_WhenHostAlone_DeletesAndPublishesEvent() { } @Test - @DisplayName("cascade 삭제는 방 소프트 삭제 → 룸메이트 제거 → flush → 요청/규칙/좋아요 삭제 순서로 수행") + @DisplayName("cascade 삭제는 방 소프트 삭제 → 룸메이트 제거 → 요청/규칙/좋아요 삭제 순서로 수행") void execute_CascadeDeletesInOrder() { Room room = mock(Room.class); when(roomService.findByIdForUpdate("r1")).thenReturn(room); @@ -115,10 +114,9 @@ void execute_CascadeDeletesInOrder() { useCase.execute("host", "r1"); - InOrder inOrder = inOrder(room, roommateService, roomService, roomRequestService, roomRuleService, roomLikeRepository, eventPublisher); + InOrder inOrder = inOrder(room, roommateService, roomRequestService, roomRuleService, roomLikeRepository, eventPublisher); inOrder.verify(room).delete(); inOrder.verify(roommateService).leaveRoom("host", "r1"); - inOrder.verify(roomService).flush(); inOrder.verify(roomRequestService).deleteAllByRoom(room); inOrder.verify(roomRuleService).deleteByRoomNo("r1"); inOrder.verify(roomLikeRepository).deleteAllByRoom(room); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/KickRoommateUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/KickRoommateUseCaseTest.java index 8069eed..f439622 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/KickRoommateUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/KickRoommateUseCaseTest.java @@ -41,7 +41,6 @@ void execute_WhenHostKicksOther_Success() { verify(roommateService).leaveRoom("member1", "r1"); verify(room).minusCurrentMate(); - verify(roomService).flush(); verify(eventPublisher).publishEvent(new RoommateKickedEvent("r1", "member1")); } @@ -71,7 +70,6 @@ void execute_WhenKickedUserNotInRoom_ThrowsException() { .isInstanceOf(RestApiException.class); verify(room, never()).minusCurrentMate(); - verify(roomService, never()).flush(); verify(eventPublisher, never()).publishEvent(any()); } @@ -86,7 +84,6 @@ void execute_WhenNotHost_ThrowsNoPermission() { .isInstanceOf(RestApiException.class); verify(roommateService, never()).leaveRoom(any(), any()); - verify(roomService, never()).flush(); verify(eventPublisher, never()).publishEvent(any()); } @@ -101,7 +98,6 @@ void execute_WhenKickSelf_ThrowsCannotKickSelf() { .isInstanceOf(RestApiException.class); verify(roommateService, never()).leaveRoom(any(), any()); - verify(roomService, never()).flush(); verify(eventPublisher, never()).publishEvent(any()); } } From 57c47026b79cf912e031c79dd90cc4976f080fcd Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Mon, 4 May 2026 17:39:31 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20hard=20delete=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=9D=84=20soft=20delete=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/domain/entity/ChatMessage.java | 2 ++ .../domain/chat/domain/entity/ChatRoom.java | 2 ++ .../chat/domain/entity/ChatRoomMember.java | 8 ++---- .../repository/ChatMessageRepository.java | 2 +- .../repository/ChatRoomMemberRepository.java | 2 +- .../domain/repository/ChatRoomRepository.java | 2 +- .../domain/service/ChatRoomMemberService.java | 3 +- .../chat/domain/service/ChatRoomService.java | 3 +- .../domain/entity/ChecklistBase.java | 11 ++++++++ .../checklist/domain/entity/RoomRule.java | 2 ++ .../domain/repository/RoomRuleRepository.java | 2 +- .../domain/room/domain/entity/RoomLike.java | 5 ++-- .../room/domain/entity/RoomRequest.java | 5 ++-- .../domain/repository/RoomLikeRepository.java | 8 ++++-- .../repository/RoomRequestRepository.java | 3 +- .../domain/service/RoomRequestService.java | 6 ++-- .../roommate/domain/entity/Roommate.java | 5 ++-- .../domain/service/RoommateService.java | 4 +-- src/main/resources/schema.sql | 23 +++++++++++---- .../service/ChatRoomMemberServiceTest.java | 7 +++-- .../unit/service/ChatRoomServiceTest.java | 7 +++-- .../FlushRemovalIntegrationTest.java | 28 +++++++++++++------ .../unit/service/RoomRequestServiceTest.java | 14 ++++++---- 23 files changed, 100 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatMessage.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatMessage.java index fd60f8f..a7b13b0 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatMessage.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatMessage.java @@ -4,12 +4,14 @@ import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@SQLRestriction("deleted_at is null") @Table( indexes = @Index(name = "idx_chat_message_room_created_message", columnList = "chat_room_no, created_at, message_no") ) diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoom.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoom.java index 8b3da9f..8ce081e 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoom.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoom.java @@ -10,10 +10,12 @@ import jakarta.persistence.Index; import jakarta.persistence.Table; import lombok.*; +import org.hibernate.annotations.SQLRestriction; import java.time.LocalDateTime; @Entity +@SQLRestriction("deleted_at is null") @Table( indexes = { @Index(name = "idx_chat_room_last_message_at", columnList = "last_message_at"), diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoomMember.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoomMember.java index 52c4631..5d4d018 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoomMember.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoomMember.java @@ -4,6 +4,7 @@ import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; import java.time.LocalDateTime; @@ -12,14 +13,11 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@SQLRestriction("deleted_at is null") @Table( indexes = { @Index(name = "idx_chat_room_member_user_room", columnList = "user_no, chat_room_no") - }, - uniqueConstraints = @UniqueConstraint( - name = "uk_chat_room_member_room_user", - columnNames = {"chat_room_no", "user_no"} - ) + } ) public class ChatRoomMember extends BaseEntity { diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java index f143428..f496966 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatMessageRepository.java @@ -21,6 +21,6 @@ int decreaseUnreadCount(@Param("chatRoomNo") String chatRoomNo, @Param("userNo") String userNo); @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM ChatMessage m WHERE m.chatRoom.chatRoomNo = :chatRoomNo") + @Query("UPDATE ChatMessage m SET m.deletedAt = CURRENT_TIMESTAMP WHERE m.chatRoom.chatRoomNo = :chatRoomNo AND m.deletedAt IS NULL") void deleteByChatRoomNo(@Param("chatRoomNo") String chatRoomNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomMemberRepository.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomMemberRepository.java index 8dbda33..40821ad 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomMemberRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomMemberRepository.java @@ -37,6 +37,6 @@ Optional findByChatRoomNoAndUserNoForUpdate( boolean existsByChatRoom_ChatRoomNoAndUserNo(String chatRoomNo, String userNo); @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM ChatRoomMember m WHERE m.chatRoom = :chatRoom") + @Query("UPDATE ChatRoomMember m SET m.deletedAt = CURRENT_TIMESTAMP WHERE m.chatRoom = :chatRoom AND m.deletedAt IS NULL") void deleteAllByChatRoom(@Param("chatRoom") ChatRoom chatRoom); } diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomRepository.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomRepository.java index 0a2ad5a..bd20de3 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomRepository.java @@ -24,6 +24,6 @@ Optional findByRoomNoAndChatRoomTypeAndApplicantUserNo( String roomNo, ChatRoomType chatRoomType, String applicantUserNo); @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM ChatRoom c WHERE c.chatRoomNo = :chatRoomNo") + @Query("UPDATE ChatRoom c SET c.deletedAt = CURRENT_TIMESTAMP WHERE c.chatRoomNo = :chatRoomNo AND c.deletedAt IS NULL") void deleteByChatRoomNo(@Param("chatRoomNo") String chatRoomNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomMemberService.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomMemberService.java index bf18bea..8280a61 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomMemberService.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomMemberService.java @@ -67,7 +67,8 @@ public boolean isMemberByChatRoomNo(String chatRoomNo, String userNo) { } public void leave(ChatRoomMember member) { - chatRoomMemberRepository.delete(member); + member.delete(); + chatRoomMemberRepository.save(member); } public void deleteAllByChatRoom(ChatRoom chatRoom) { diff --git a/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomService.java b/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomService.java index d2573ad..ac401ae 100644 --- a/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomService.java +++ b/src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomService.java @@ -64,7 +64,8 @@ public void updateLastMessage(ChatRoom chatRoom, String content, String senderNo } public void delete(ChatRoom chatRoom) { - chatRoomRepository.delete(chatRoom); + chatRoom.delete(); + chatRoomRepository.save(chatRoom); } public void deleteByChatRoomNo(String chatRoomNo) { diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/ChecklistBase.java b/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/ChecklistBase.java index 6aeb6f5..becea0d 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/ChecklistBase.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/ChecklistBase.java @@ -123,6 +123,17 @@ public abstract class ChecklistBase { @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; + @Column(name = "deleted_at") + private LocalDateTime deletedAt; + + public boolean isDeleted() { + return deletedAt != null; + } + + public void delete() { + deletedAt = LocalDateTime.now(); + } + public void updateChecklist( String bedtime, String wakeUp, diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/RoomRule.java b/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/RoomRule.java index c14534a..2f98971 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/RoomRule.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/domain/entity/RoomRule.java @@ -6,8 +6,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.SQLRestriction; @Entity +@SQLRestriction("deleted_at is null") @Table( name = "room_rule", indexes = { diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java index 4a09be0..4b7eeab 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java @@ -14,6 +14,6 @@ public interface RoomRuleRepository extends JpaRepository { Optional findByRoomNo(@Param("roomNo") String roomNo); @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM RoomRule rr WHERE rr.room.roomNo = :roomNo") + @Query("UPDATE RoomRule rr SET rr.deletedAt = CURRENT_TIMESTAMP WHERE rr.room.roomNo = :roomNo AND rr.deletedAt IS NULL") void deleteByRoomNo(@Param("roomNo") String roomNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomLike.java b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomLike.java index bb492fb..0b4437a 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomLike.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomLike.java @@ -4,16 +4,15 @@ import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@SQLRestriction("deleted_at is null") @Table( - uniqueConstraints = { - @UniqueConstraint(name = "uk_room_like_user_room", columnNames = {"user_no", "room_no"}) - }, indexes = { } ) diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomRequest.java b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomRequest.java index 0f2745c..0ae886d 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomRequest.java @@ -4,16 +4,15 @@ import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@SQLRestriction("deleted_at is null") @Table( - uniqueConstraints = { - @UniqueConstraint(name = "uk_room_request_user_room_direction", columnNames = {"user_no", "room_no", "direction"}) - }, indexes = { @Index(name = "idx_room_request_room_created", columnList = "room_no, created_at") } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java index 93dc535..3cbc3e7 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java @@ -10,11 +10,13 @@ public interface RoomLikeRepository extends JpaRepository { boolean existsByUserNoAndRoom(String userNo, Room room); - void deleteByUserNoAndRoom(String userNo, Room room); - // N+1 DELETE 문제 — @Modifying JPQL 벌크 DELETE로 교체 @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM RoomLike l WHERE l.room = :room") + @Query("UPDATE RoomLike l SET l.deletedAt = CURRENT_TIMESTAMP WHERE l.userNo = :userNo AND l.room = :room AND l.deletedAt IS NULL") + void deleteByUserNoAndRoom(@Param("userNo") String userNo, @Param("room") Room room); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RoomLike l SET l.deletedAt = CURRENT_TIMESTAMP WHERE l.room = :room AND l.deletedAt IS NULL") void deleteAllByRoom(@Param("room") Room room); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java index 137d831..8c28d78 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java @@ -15,9 +15,8 @@ public interface RoomRequestRepository extends JpaRepository findByUserNoAndRoomAndDirection(String userNo, Room room, Direction direction); - // N+1 DELETE 문제 — @Modifying JPQL 벌크 DELETE로 교체 @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("DELETE FROM RoomRequest r WHERE r.room = :room") + @Query("UPDATE RoomRequest r SET r.deletedAt = CURRENT_TIMESTAMP WHERE r.room = :room AND r.deletedAt IS NULL") void deleteAllByRoom(@Param("room") Room room); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomRequestService.java b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomRequestService.java index fcf0532..42b05ef 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomRequestService.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomRequestService.java @@ -42,7 +42,8 @@ public RoomRequest findById(String requestNo) { } public void delete(RoomRequest entity) { - roomRequestRepository.delete(entity); + entity.delete(); + roomRequestRepository.save(entity); } public List findApplicationsByRoom(Room room) { @@ -54,7 +55,8 @@ public void cancelJoinRequest(String userNo, Room room) { .findByUserNoAndRoomAndDirection(userNo, room, Direction.USER_TO_ROOM) .orElseThrow(() -> new RestApiException(ROOM_REQUEST_NOT_FOUND)); - roomRequestRepository.delete(request); + request.delete(); + roomRequestRepository.save(request); } public void deleteAllByRoom(Room room) { diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/domain/entity/Roommate.java b/src/main/java/com/project/dorumdorum/domain/roommate/domain/entity/Roommate.java index 2f5c485..e00ff01 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/domain/entity/Roommate.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/domain/entity/Roommate.java @@ -5,16 +5,15 @@ import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.*; import lombok.*; +import org.hibernate.annotations.SQLRestriction; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PROTECTED) @Builder +@SQLRestriction("deleted_at is null") @Table( - uniqueConstraints = { - @UniqueConstraint(name = "uk_roommate_user_no", columnNames = "user_no") - }, indexes = { @Index(name = "idx_roommate_room_no", columnList = "room_no") } diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java b/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java index b503f1d..54c135c 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java @@ -66,8 +66,8 @@ public boolean isHostOfRoom(String userNo, String roomNo) { public void leaveRoom(String userNo, String roomNo) { Roommate roommate = roommateRepository.findByUserNoAndRoomNo(userNo, roomNo) .orElseThrow(() -> new RestApiException(_NOT_FOUND)); - // uk_roommate_user_no 유니크 제약으로 인해 소프트 삭제 불가 — 하드 삭제 유지 - roommateRepository.delete(roommate); + roommate.delete(); + roommateRepository.save(roommate); } public List findMyRoommates(String userNo) { diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8f90ec5..36faf01 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -4,17 +4,28 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_user_email CREATE UNIQUE INDEX IF NOT EXISTS uk_user_student_no ON users (student_no); +DROP INDEX IF EXISTS uk_roommate_user_no; CREATE UNIQUE INDEX IF NOT EXISTS uk_roommate_user_no - ON roommate (user_no); + ON roommate (user_no) + WHERE deleted_at IS NULL; +DROP INDEX IF EXISTS uk_room_like_user_room; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_like_user_room - ON room_like (user_no, room_no); + ON room_like (user_no, room_no) + WHERE deleted_at IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS uk_device_user_device ON devices (user_no, device_id); +DROP INDEX IF EXISTS uk_room_request_user_room_direction; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_request_user_room_direction - ON room_request (user_no, room_no, direction); + ON room_request (user_no, room_no, direction) + WHERE deleted_at IS NULL; + +DROP INDEX IF EXISTS uk_chat_room_member_room_user; +CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_member_room_user + ON chat_room_member (chat_room_no, user_no) + WHERE deleted_at IS NULL; ALTER TABLE IF EXISTS users ALTER COLUMN gender SET NOT NULL; @@ -30,13 +41,15 @@ CREATE INDEX IF NOT EXISTS idx_room_status_gender_remaining_created ALTER TABLE IF EXISTS chat_room DROP CONSTRAINT IF EXISTS uk_chat_room_direct; +DROP INDEX IF EXISTS uk_chat_room_group; CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_group ON chat_room (room_no) - WHERE chat_room_type = 'GROUP'; + WHERE chat_room_type = 'GROUP' AND deleted_at IS NULL; +DROP INDEX IF EXISTS uk_chat_room_direct; CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_direct ON chat_room (room_no, applicant_user_no) - WHERE chat_room_type = 'DIRECT'; + WHERE chat_room_type = 'DIRECT' AND deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_chat_room_room_no ON chat_room (room_no); diff --git a/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomMemberServiceTest.java b/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomMemberServiceTest.java index b3ad6ad..5d62930 100644 --- a/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomMemberServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomMemberServiceTest.java @@ -121,13 +121,14 @@ void countByChatRoom_DelegatesToRepository() { } @Test - @DisplayName("leave: repository.delete에 위임") - void leave_DelegatesToRepository() { + @DisplayName("leave: 멤버를 소프트 삭제하고 저장") + void leave_SoftDeletesAndSaves() { ChatRoomMember member = ChatRoomMember.builder().chatRoom(chatRoom).userNo("user-1").build(); service.leave(member); - verify(chatRoomMemberRepository).delete(member); + assertThat(member.getDeletedAt()).isNotNull(); + verify(chatRoomMemberRepository).save(member); } @Test diff --git a/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomServiceTest.java b/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomServiceTest.java index 757773f..24461a3 100644 --- a/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatRoomServiceTest.java @@ -95,13 +95,14 @@ void updateLastMessage_UpdatesFields() { } @Test - @DisplayName("delete: repository.delete에 위임") - void delete_DelegatesToRepository() { + @DisplayName("delete: 채팅방을 소프트 삭제하고 저장") + void delete_SoftDeletesAndSaves() { ChatRoom chatRoom = ChatRoom.builder().roomNo("room-1").build(); service.delete(chatRoom); - verify(chatRoomRepository).delete(chatRoom); + assertThat(chatRoom.getDeletedAt()).isNotNull(); + verify(chatRoomRepository).save(chatRoom); } @Test diff --git a/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java index fd83119..a813226 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -90,6 +91,7 @@ class FlushRemovalIntegrationTest { @Autowired private RoomRequestRepository roomRequestRepository; @Autowired private RoomRuleRepository roomRuleRepository; @Autowired private RoomLikeRepository roomLikeRepository; + @Autowired private JdbcTemplate jdbcTemplate; private static final String KICK_HOST_NO = "flush_test_kick_host_001"; private static final String KICK_MEMBER_NO = "flush_test_kick_member_001"; @@ -101,6 +103,8 @@ class FlushRemovalIntegrationTest { @BeforeEach void setUp() { + truncateTables(); + // ── KickRoommateUseCase 픽스처 ──────────────────────────────────────── kickRoom = roomRepository.save(Room.builder() .roomType(RoomType.TYPE_1).capacity(2).title("kick-flush-test") @@ -133,14 +137,7 @@ void setUp() { @AfterEach void tearDown() { - chatMessageRepository.deleteAllInBatch(); - chatRoomMemberRepository.deleteAllInBatch(); - chatRoomRepository.deleteAllInBatch(); - roomRequestRepository.deleteAllInBatch(); - roomRuleRepository.deleteAllInBatch(); - roomLikeRepository.deleteAllInBatch(); - roommateRepository.deleteAllInBatch(); - roomRepository.deleteAllInBatch(); + truncateTables(); } // ========================================================================= @@ -212,4 +209,19 @@ private void stubChatMessageService() { when(fakeMessage.getCreatedAt()).thenReturn(java.time.LocalDateTime.now()); doReturn(fakeMessage).when(chatMessageService).save(any(), anyString(), anyString(), any(), anyInt()); } + + private void truncateTables() { + jdbcTemplate.execute(""" + TRUNCATE TABLE + chat_message, + chat_room_member, + chat_room, + room_request, + room_rule, + room_like, + roommate, + room + RESTART IDENTITY CASCADE + """); + } } diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomRequestServiceTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomRequestServiceTest.java index 9db86ae..1dedba7 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomRequestServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomRequestServiceTest.java @@ -62,11 +62,12 @@ void findById_WhenMissing_Throws() { } @Test - @DisplayName("Should delete provided request entity") - void delete_DeletesEntity() { + @DisplayName("Should soft delete provided request entity") + void delete_SoftDeletesEntity() { RoomRequest request = RoomRequest.builder().roomRequestNo("rq1").build(); service.delete(request); - verify(roomRequestRepository).delete(request); + assertThat(request.getDeletedAt()).isNotNull(); + verify(roomRequestRepository).save(request); } @Test @@ -87,8 +88,8 @@ void findApplicationsByRoom_ReturnsRepositoryResult() { } @Test - @DisplayName("Should cancel join request by user and room") - void cancelJoinRequest_FindsAndDeletesRequest() { + @DisplayName("Should cancel join request by soft deleting request") + void cancelJoinRequest_FindsAndSoftDeletesRequest() { Room room = Room.builder().roomNo("r1").build(); RoomRequest request = RoomRequest.builder().roomRequestNo("rq1").build(); when(roomRequestRepository.findByUserNoAndRoomAndDirection("u1", room, Direction.USER_TO_ROOM)) @@ -96,7 +97,8 @@ void cancelJoinRequest_FindsAndDeletesRequest() { service.cancelJoinRequest("u1", room); - verify(roomRequestRepository).delete(request); + assertThat(request.getDeletedAt()).isNotNull(); + verify(roomRequestRepository).save(request); } @Test From b3a03b8d75827bf6af6730c551543de31a816e54 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Mon, 4 May 2026 19:23:29 +0900 Subject: [PATCH 03/19] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd.yml | 4 +-- build.gradle | 4 +++ ...atTransactionAtomicityIntegrationTest.java | 25 +++++++++++++---- .../DeleteRoomPersistenceIntegrationTest.java | 27 ++++++++++++++----- .../FlushRemovalIntegrationTest.java | 25 ++++++++--------- ...ickRoommatePersistenceIntegrationTest.java | 25 +++++++++++++---- 6 files changed, 79 insertions(+), 31 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 620ddb4..0406ed5 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -40,8 +40,8 @@ jobs: - name: Run tests run: | - echo ">>> [CI] Running tests..." - ./gradlew test --no-daemon + echo ">>> [CI] Running tests without integration tests..." + ./gradlew test -PskipIntegrationTests --no-daemon echo ">>> [CI] Tests passed" docker-build-and-push: diff --git a/build.gradle b/build.gradle index d4ff20b..574ce83 100644 --- a/build.gradle +++ b/build.gradle @@ -94,6 +94,10 @@ dependencies { tasks.named('test') { useJUnitPlatform() + + if (project.hasProperty('skipIntegrationTests')) { + exclude '**/*IntegrationTest.class' + } } def generated = "$buildDir/generated/sources/annotationProcessor/java/main" diff --git a/src/test/java/com/project/dorumdorum/domain/chat/integration/ChatTransactionAtomicityIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/chat/integration/ChatTransactionAtomicityIntegrationTest.java index eb13a2f..f59c82d 100644 --- a/src/test/java/com/project/dorumdorum/domain/chat/integration/ChatTransactionAtomicityIntegrationTest.java +++ b/src/test/java/com/project/dorumdorum/domain/chat/integration/ChatTransactionAtomicityIntegrationTest.java @@ -33,6 +33,7 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; import org.springframework.test.context.ActiveProfiles; @@ -88,6 +89,7 @@ static void requireDocker() { @Autowired private RoommateRepository roommateRepository; @Autowired private ChatRoomRepository chatRoomRepository; @Autowired private ChatRoomMemberRepository chatRoomMemberRepository; + @Autowired private JdbcTemplate jdbcTemplate; // ─── External Services (Mocking to isolate behavior) ───────────────────── @MockitoBean private SimpMessagingTemplate messagingTemplate; @@ -110,6 +112,8 @@ static void requireDocker() { @BeforeEach void setUp() { + truncateTables(); + // 1. Room 저장 (@PrePersist → currentMateCount=1, roomStatus=CONFIRM_PENDING 자동 설정) room = roomRepository.save( Room.builder() @@ -162,11 +166,7 @@ void setUp() { @AfterEach void tearDown() { - // FK 순서: ChatRoomMember → ChatRoom → Roommate → Room - chatRoomMemberRepository.deleteAll(); - chatRoomRepository.deleteAll(); - roommateRepository.deleteAll(); - roomRepository.deleteAll(); + truncateTables(); } // ========================================================================= @@ -345,4 +345,19 @@ private void stubChatMessageService() { .when(chatMessageService) .save(any(), anyString(), anyString(), any(), anyInt()); } + + private void truncateTables() { + jdbcTemplate.execute(""" + TRUNCATE TABLE + chat_message, + chat_room_member, + chat_room, + room_request, + room_rule, + room_like, + roommate, + room + RESTART IDENTITY CASCADE + """); + } } diff --git a/src/test/java/com/project/dorumdorum/domain/room/integration/DeleteRoomPersistenceIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/room/integration/DeleteRoomPersistenceIntegrationTest.java index 43d568a..c577171 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/integration/DeleteRoomPersistenceIntegrationTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/integration/DeleteRoomPersistenceIntegrationTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -74,6 +75,7 @@ static void requireDocker() { @Autowired private ChatRoomRepository chatRoomRepository; @Autowired private ChatRoomMemberRepository chatRoomMemberRepository; @Autowired private ChatMessageRepository chatMessageRepository; + @Autowired private JdbcTemplate jdbcTemplate; private static final String HOST_NO = "test_delete_room_host_001"; private static final String APPLICANT_NO = "test_delete_room_applicant_001"; @@ -82,6 +84,8 @@ static void requireDocker() { @BeforeEach void setUp() { + truncateTables(); + room = roomRepository.save(Room.builder() .roomType(RoomType.TYPE_1) .capacity(2) @@ -165,13 +169,7 @@ void setUp() { @AfterEach void tearDown() { - chatMessageRepository.deleteAllInBatch(); - chatRoomMemberRepository.deleteAllInBatch(); - chatRoomRepository.deleteAllInBatch(); - roomRequestRepository.deleteAllInBatch(); - roomRuleRepository.deleteAllInBatch(); - roommateRepository.deleteAllInBatch(); - roomRepository.deleteAllInBatch(); + truncateTables(); } @Test @@ -193,4 +191,19 @@ void execute_PersistsSoftDeleteAndCleanup() { verify(messagingTemplate, times(3)) .convertAndSendToUser(any(), eq("/queue/notification"), any())); } + + private void truncateTables() { + jdbcTemplate.execute(""" + TRUNCATE TABLE + chat_message, + chat_room_member, + chat_room, + room_request, + room_rule, + room_like, + roommate, + room + RESTART IDENTITY CASCADE + """); + } } diff --git a/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java index a813226..10fc2b6 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/integration/FlushRemovalIntegrationTest.java @@ -26,7 +26,10 @@ import com.project.dorumdorum.domain.user.domain.entity.Role; import com.project.dorumdorum.domain.user.domain.entity.User; import com.project.dorumdorum.domain.user.domain.service.UserService; +import com.project.dorumdorum.testsupport.TestcontainersSupport; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -55,23 +58,21 @@ * 2. DeleteRoomUseCase — room.delete()가 deleteAllByRoom(flushAutomatically=true)를 통해 * DB에 반영되고 연관 데이터가 정리되는지 * - * @ActiveProfiles("local-db"): 로컬 Docker PostgreSQL(dorumdorum_test DB)에 직접 연결. - * Testcontainers 없이 실제 DB로 동작을 검증한다. + * @ActiveProfiles("test"): Testcontainers PostgreSQL로 실제 DB 동작을 검증한다. */ -@SpringBootTest(properties = { - "spring.datasource.driver-class-name=org.postgresql.Driver", - "spring.datasource.url=jdbc:postgresql://localhost:5432/dorumdorum_test", - "spring.datasource.username=dorumdorum", - "spring.datasource.password=test1234", - "spring.jpa.defer-datasource-initialization=true", - "spring.jpa.hibernate.ddl-auto=create-drop", - "spring.sql.init.mode=always", - "spring.sql.init.schema-locations=classpath:schema.sql" -}) +@SpringBootTest @ActiveProfiles("test") @DisplayName("flush() 제거 후 실제 DB 반영 통합 테스트") class FlushRemovalIntegrationTest { + @BeforeAll + static void requireDocker() { + Assumptions.assumeTrue( + TestcontainersSupport.requireDockerOrSkip("FlushRemovalIntegrationTest"), + "Docker is required for FlushRemovalIntegrationTest" + ); + } + @MockitoBean private FirebaseApp firebaseApp; @MockitoBean private FirebaseMessaging firebaseMessaging; @MockitoBean private SimpMessagingTemplate messagingTemplate; diff --git a/src/test/java/com/project/dorumdorum/domain/room/integration/KickRoommatePersistenceIntegrationTest.java b/src/test/java/com/project/dorumdorum/domain/room/integration/KickRoommatePersistenceIntegrationTest.java index 4f3451b..6ee5557 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/integration/KickRoommatePersistenceIntegrationTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/integration/KickRoommatePersistenceIntegrationTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -75,6 +76,7 @@ static void requireDocker() { @Autowired private ChatRoomRepository chatRoomRepository; @Autowired private ChatRoomMemberRepository chatRoomMemberRepository; @Autowired private ChatMessageRepository chatMessageRepository; + @Autowired private JdbcTemplate jdbcTemplate; private static final String HOST_NO = "test_kick_roommate_host_001"; private static final String MEMBER_NO = "test_kick_roommate_member_001"; @@ -84,6 +86,8 @@ static void requireDocker() { @BeforeEach void setUp() { + truncateTables(); + User fakeUser = User.builder() .nickname("MemberNick") .name("Member") @@ -147,11 +151,7 @@ void setUp() { @AfterEach void tearDown() { - chatMessageRepository.deleteAllInBatch(); - chatRoomMemberRepository.deleteAllInBatch(); - chatRoomRepository.deleteAllInBatch(); - roommateRepository.deleteAllInBatch(); - roomRepository.deleteAllInBatch(); + truncateTables(); } @Test @@ -172,4 +172,19 @@ void execute_PersistsKickChangesAcrossRoomAndChat() { verify(messagingTemplate).convertAndSendToUser(eq(MEMBER_NO), eq("/queue/notification"), any()); }); } + + private void truncateTables() { + jdbcTemplate.execute(""" + TRUNCATE TABLE + chat_message, + chat_room_member, + chat_room, + room_request, + room_rule, + room_like, + roommate, + room + RESTART IDENTITY CASCADE + """); + } } From cc5edc3bc900575caa8502f2ac536a3462d35dc1 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Tue, 5 May 2026 12:04:17 +0900 Subject: [PATCH 04/19] =?UTF-8?q?feat:=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20JFR/JMC=20=EA=B8=B0=EB=A1=9D=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 4 ++++ .gitignore | 8 +++++++- docker-compose.yml | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 10321c4..dad317c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,10 @@ .gradle build out +logs +*.jfr +*.hprof +gc.log* *.iml .idea **/.DS_Store diff --git a/.gitignore b/.gitignore index a311993..12b265a 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,13 @@ pinpoint-agent/tools/ .env /analysis/ +### JVM profiling artifacts ### +logs/ +*.jfr +*.hprof +gc.log* + # macOS artefacts .DS_Store -docs/ \ No newline at end of file +docs/ diff --git a/docker-compose.yml b/docker-compose.yml index bf88b1f..1dbec3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,8 +46,11 @@ services: - PINPOINT_APPLICATION_NAME=dorumdorum - PINPOINT_AGENT_ID=${PINPOINT_AGENT_ID:-dorumdorum-backend-${HOSTNAME}} - PINPOINT_AGENT_NAME=${PINPOINT_AGENT_NAME:-dorumdorum-be} + - JFR_RUN_ID=${JFR_RUN_ID:-cloud-load-test} + - JAVA_OPTS=${JAVA_OPTS:--XX:StartFlightRecording=name=dorumdorum,settings=profile,disk=true,dumponexit=true,filename=/app/logs/jfr/dorumdorum-${JFR_RUN_ID:-cloud-load-test}.jfr,maxage=30m,maxsize=512m} volumes: - ./firebase-service-account.json:/app/firebase-service-account.json:ro + - ./logs/jfr:/app/logs/jfr depends_on: postgres: condition: service_healthy From 6cfefb100942a360b45687d09f75e67d26caeeed Mon Sep 17 00:00:00 2001 From: KoungQ Date: Tue, 5 May 2026 12:41:02 +0900 Subject: [PATCH 05/19] =?UTF-8?q?chore:=20=EC=8B=9C=ED=81=AC=EB=A6=BF=20?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=EB=AA=A8=EB=93=88=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 2 ++ .gitmodules | 3 +++ docker-compose.yml | 4 ++-- secrets | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 secrets diff --git a/.dockerignore b/.dockerignore index dad317c..a536446 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ .git .github .gradle +.env +secrets build out logs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..392a0f4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "secrets"] + path = secrets + url = https://github.com/DorumDorum/secrets diff --git a/docker-compose.yml b/docker-compose.yml index 1dbec3d..f6e8d00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,7 +38,7 @@ services: - "8080:8080" stop_grace_period: 45s env_file: - - .env + - ./secrets/.env environment: - REDIS_HOST=redis - REDIS_PORT=6379 @@ -49,7 +49,7 @@ services: - JFR_RUN_ID=${JFR_RUN_ID:-cloud-load-test} - JAVA_OPTS=${JAVA_OPTS:--XX:StartFlightRecording=name=dorumdorum,settings=profile,disk=true,dumponexit=true,filename=/app/logs/jfr/dorumdorum-${JFR_RUN_ID:-cloud-load-test}.jfr,maxage=30m,maxsize=512m} volumes: - - ./firebase-service-account.json:/app/firebase-service-account.json:ro + - ./secrets/firebase-service-account.json:/app/firebase-service-account.json:ro - ./logs/jfr:/app/logs/jfr depends_on: postgres: diff --git a/secrets b/secrets new file mode 160000 index 0000000..5d9f67f --- /dev/null +++ b/secrets @@ -0,0 +1 @@ +Subproject commit 5d9f67fa14d9368c18ca68365c765fc7314ba555 From 672b1bb992c3003b28c578fbaea8551c3e1611e1 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Tue, 5 May 2026 13:11:42 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20=EC=BB=A8=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=84=88=20JFR/JMC=20=EA=B8=B0=EB=A1=9D=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 2 +- Dockerfile.pinpoint | 4 ++-- docker-compose.yml | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 83d51c1..0b6e546 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,4 +17,4 @@ COPY --from=builder /app/build/libs/*.jar app.jar EXPOSE 8080 -ENTRYPOINT ["sh", "-c", "exec java ${JAVA_OPTS} -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} -jar /app/app.jar"] +ENTRYPOINT ["sh", "-c", "DEFAULT_JAVA_OPTS=\"-XX:StartFlightRecording=name=dorumdorum,settings=${JFR_SETTINGS:-profile},disk=true,dumponexit=true,filename=/app/logs/jfr/dorumdorum-${JFR_RUN_ID:-cloud-load-test}.jfr,maxage=${JFR_MAX_AGE:-30m},maxsize=${JFR_MAX_SIZE:-512m}\"; EFFECTIVE_JAVA_OPTS=\"${JAVA_OPTS:-$DEFAULT_JAVA_OPTS}\"; exec java ${EFFECTIVE_JAVA_OPTS} -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} -jar /app/app.jar"] diff --git a/Dockerfile.pinpoint b/Dockerfile.pinpoint index ccbffc8..9bcca92 100644 --- a/Dockerfile.pinpoint +++ b/Dockerfile.pinpoint @@ -38,12 +38,12 @@ EXPOSE 8080 # pinpoint-root.config에서 Collector IP 오버라이드 (profiler.transport.grpc.collector.ip) # -D 옵션으로 전달 -ENTRYPOINT ["sh", "-c", "java \ +ENTRYPOINT ["sh", "-c", "DEFAULT_JAVA_OPTS=\"-XX:StartFlightRecording=name=dorumdorum,settings=${JFR_SETTINGS:-profile},disk=true,dumponexit=true,filename=/app/logs/jfr/dorumdorum-${JFR_RUN_ID:-cloud-load-test}.jfr,maxage=${JFR_MAX_AGE:-30m},maxsize=${JFR_MAX_SIZE:-512m}\"; EFFECTIVE_JAVA_OPTS=\"${JAVA_OPTS:-$DEFAULT_JAVA_OPTS}\"; java \ -javaagent:/app/pinpoint-agent/pinpoint-bootstrap-${PINPOINT_VERSION}.jar \ -Dpinpoint.agentId=${PINPOINT_AGENT_ID} \ -Dpinpoint.applicationName=${PINPOINT_APPLICATION_NAME} \ -Dprofiler.transport.grpc.collector.ip=${PINPOINT_COLLECTOR_IP} \ -Dpinpoint.container=true \ - ${JAVA_OPTS} \ + ${EFFECTIVE_JAVA_OPTS} \ -Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod} \ -jar /app/app.jar"] diff --git a/docker-compose.yml b/docker-compose.yml index f6e8d00..7ca60a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,8 +46,6 @@ services: - PINPOINT_APPLICATION_NAME=dorumdorum - PINPOINT_AGENT_ID=${PINPOINT_AGENT_ID:-dorumdorum-backend-${HOSTNAME}} - PINPOINT_AGENT_NAME=${PINPOINT_AGENT_NAME:-dorumdorum-be} - - JFR_RUN_ID=${JFR_RUN_ID:-cloud-load-test} - - JAVA_OPTS=${JAVA_OPTS:--XX:StartFlightRecording=name=dorumdorum,settings=profile,disk=true,dumponexit=true,filename=/app/logs/jfr/dorumdorum-${JFR_RUN_ID:-cloud-load-test}.jfr,maxage=30m,maxsize=512m} volumes: - ./secrets/firebase-service-account.json:/app/firebase-service-account.json:ro - ./logs/jfr:/app/logs/jfr @@ -125,7 +123,7 @@ services: image: pinpointdocker/pinpoint-web:2.5.4 container_name: pinpoint-web env_file: - - .env + - ./secrets/.env depends_on: - pinpoint-hbase networks: From 88d39d2267fd802c63f0747238cf1997373255d1 Mon Sep 17 00:00:00 2001 From: KoungQ Date: Tue, 5 May 2026 13:12:12 +0900 Subject: [PATCH 07/19] =?UTF-8?q?refactor:=20CD=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cicd.yml | 116 +++++++++++++------------------------ 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 620ddb4..229eaf7 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -1,4 +1,7 @@ # CI/CD에 필요한 GitHub Secrets (Repo Settings > Secrets and variables > Actions) +# - Docker Hub: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN +# - Server: SERVER_HOST, SERVER_USER, SERVER_SSH_KEY, SERVER_SSH_PORT +# - GitHub repo/submodule access: GITHUB_TOKEN name: cicd @@ -118,18 +121,6 @@ jobs: IMAGE: ${{ secrets.DOCKERHUB_USERNAME }}/dorumdorum-be:pinpoint DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} - FIREBASE_SERVICE_ACCOUNT_B64: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_B64 }} - RDB_USERNAME: ${{ secrets.RDB_USERNAME }} - RDB_URL: ${{ secrets.RDB_URL }} - RDB_PASSWORD: ${{ secrets.RDB_PASSWORD }} - MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }} - MYSQL_DATABASE: ${{ secrets.MYSQL_DATABASE }} - JWT_KEY: ${{ secrets.JWT_KEY }} - JWT_ACCESS_EXPIRATION: ${{ secrets.JWT_ACCESS_EXPIRATION }} - JWT_REFRESH_EXPIRATION: ${{ secrets.JWT_REFRESH_EXPIRATION }} - SMTP_USERNAME: ${{ secrets.SMTP_USERNAME }} - SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} CONTAINER_NAME: dorumdorum-be GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO_FULL_NAME: ${{ github.repository }} @@ -138,56 +129,11 @@ jobs: username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} port: ${{ secrets.SERVER_SSH_PORT }} - envs: IMAGE,DOCKERHUB_USERNAME,DOCKERHUB_TOKEN,FIREBASE_SERVICE_ACCOUNT_B64,RDB_USERNAME,RDB_URL,RDB_PASSWORD,MYSQL_ROOT_PASSWORD,MYSQL_DATABASE,JWT_KEY,JWT_ACCESS_EXPIRATION,JWT_REFRESH_EXPIRATION,SMTP_USERNAME,SMTP_PASSWORD,CONTAINER_NAME,GITHUB_TOKEN,REPO_FULL_NAME,DISCORD_WEBHOOK + envs: IMAGE,DOCKERHUB_USERNAME,DOCKERHUB_TOKEN,CONTAINER_NAME,GITHUB_TOKEN,REPO_FULL_NAME script_stop: true script: | set -e - echo ">>> [Deploy] SSH 연결됨, 환경 변수 정규화 중..." - - RDB_USERNAME="$(printf '%s' "$RDB_USERNAME" | tr -d '\r\n')" - RDB_URL="$(printf '%s' "$RDB_URL" | tr -d '\r\n')" - RDB_PASSWORD="$(printf '%s' "$RDB_PASSWORD" | tr -d '\r\n')" - MYSQL_ROOT_PASSWORD="$(printf '%s' "$MYSQL_ROOT_PASSWORD" | tr -d '\r\n')" - MYSQL_DATABASE="$(printf '%s' "$MYSQL_DATABASE" | tr -d '\r\n')" - JWT_KEY="$(printf '%s' "$JWT_KEY" | tr -d '\r\n')" - JWT_ACCESS_EXPIRATION="$(printf '%s' "$JWT_ACCESS_EXPIRATION" | tr -d '\r\n')" - JWT_REFRESH_EXPIRATION="$(printf '%s' "$JWT_REFRESH_EXPIRATION" | tr -d '\r\n')" - SMTP_USERNAME="$(printf '%s' "$SMTP_USERNAME" | tr -d '\r\n')" - SMTP_PASSWORD="$(printf '%s' "$SMTP_PASSWORD" | tr -d '\r\n')" - - DEPLOY_PATH="${HOME}/dorumdorum" - mkdir -p "$DEPLOY_PATH" - cd "$DEPLOY_PATH" - echo ">>> [Deploy] DEPLOY_PATH=$DEPLOY_PATH" - - # 1) Firebase 서비스 계정 파일 생성 (VM에 저장) - printf '%s' "$FIREBASE_SERVICE_ACCOUNT_B64" | base64 -d > firebase-service-account.json - chmod 600 firebase-service-account.json - - # 2) 컨테이너에서 참조할 경로를 env로 고정 - FIREBASE_SERVICE_ACCOUNT_PATH="/app/firebase-service-account.json" - - # 3) .env 생성 - # Pinpoint Web reads Spring datasource keys directly. - printf '%s\n' \ - "SPRING_PROFILES_ACTIVE=prod" \ - "RDB_USERNAME=$RDB_USERNAME" \ - "RDB_URL=$RDB_URL" \ - "RDB_PASSWORD=$RDB_PASSWORD" \ - "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD" \ - "MYSQL_DATABASE=$MYSQL_DATABASE" \ - "SPRING_DATASOURCE_URL=$RDB_URL" \ - "SPRING_DATASOURCE_USERNAME=$RDB_USERNAME" \ - "SPRING_DATASOURCE_PASSWORD=$RDB_PASSWORD" \ - "JWT_KEY=$JWT_KEY" \ - "JWT_ACCESS_EXPIRATION=$JWT_ACCESS_EXPIRATION" \ - "JWT_REFRESH_EXPIRATION=$JWT_REFRESH_EXPIRATION" \ - "SMTP_USERNAME=$SMTP_USERNAME" \ - "SMTP_PASSWORD=$SMTP_PASSWORD" \ - "DISCORD_WEBHOOK=$DISCORD_WEBHOOK" \ - "FIREBASE_SERVICE_ACCOUNT_PATH=$FIREBASE_SERVICE_ACCOUNT_PATH" > .env - chmod 600 .env - echo ">>> [Deploy] .env, firebase-service-account.json 생성 완료" + echo ">>> [Deploy] SSH 연결됨" docker info >/dev/null 2>&1 || { echo "Docker socket permission denied for current deploy user." @@ -198,8 +144,8 @@ jobs: echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin echo ">>> [Deploy] Docker Hub 로그인 완료" - echo ">>> [Deploy] MySQL 이미지 pull 중..." - docker pull mysql:8.0 + echo ">>> [Deploy] PostgreSQL 이미지 pull 중..." + docker pull postgres:16-alpine echo ">>> [Deploy] Redis 이미지 pull 중..." docker pull redis:7-alpine echo ">>> [Deploy] Backend 이미지 pull 중..." @@ -215,9 +161,14 @@ jobs: echo ">>> [Deploy] COMPOSE_PATH=$COMPOSE_PATH, Git clone 중..." rm -rf temp_repo - # Git에서 production 브랜치로 docker-compose.yml, monitoring 가져오기 + # Git에서 production 브랜치로 docker-compose.yml, monitoring, secrets submodule 가져오기 git clone --depth 1 -b production "https://x-access-token:${GITHUB_TOKEN}@github.com/${REPO_FULL_NAME}.git" temp_repo - echo ">>> [Deploy] Clone 완료, docker-compose.yml · monitoring 복사 중..." + ( + cd temp_repo + git -c url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf="https://github.com/" \ + submodule update --init --recursive --depth 1 + ) + echo ">>> [Deploy] Clone 완료, docker-compose.yml · monitoring · secrets 복사 중..." if [ -f "temp_repo/BE/docker-compose.yml" ]; then cp temp_repo/BE/docker-compose.yml . @@ -248,6 +199,19 @@ jobs: mkdir -p monitoring/prometheus monitoring/grafana/provisioning/dashboards/json monitoring/grafana/provisioning/datasources fi + rm -rf secrets + if [ -d "temp_repo/BE/secrets" ]; then + cp -R temp_repo/BE/secrets . + echo ">>> [Deploy] BE/secrets 복사 완료" + elif [ -d "temp_repo/secrets" ]; then + cp -R temp_repo/secrets . + echo ">>> [Deploy] secrets 복사 완료" + else + echo "ERROR: secrets submodule directory not found in temp_repo/" + exit 1 + fi + chmod 600 secrets/.env secrets/firebase-service-account.json + rm -rf temp_repo echo ">>> [Deploy] temp_repo 삭제, compose 파일 확인 중..." @@ -257,21 +221,23 @@ jobs: ls -la exit 1 fi - - # .env 파일과 firebase-service-account.json 복사 - cp "$DEPLOY_PATH/.env" . - cp "$DEPLOY_PATH/firebase-service-account.json" . - echo ">>> [Deploy] .env, firebase-service-account.json 복사 완료" + + if [ ! -f "secrets/.env" ] || [ ! -f "secrets/firebase-service-account.json" ]; then + echo "ERROR: required secret files are missing" + ls -la secrets || true + exit 1 + fi # docker-compose로 모든 서비스 실행 export BACKEND_IMAGE="$IMAGE" export CONTAINER_NAME="$CONTAINER_NAME" + COMPOSE="docker compose --env-file secrets/.env -f docker-compose.yml" echo ">>> [Deploy] 기존 컨테이너 정리 (down + 이름으로 강제 제거)..." - docker compose -f docker-compose.yml down --remove-orphans >/dev/null 2>&1 || true + $COMPOSE down --remove-orphans >/dev/null 2>&1 || true docker rm -f dorumdorum-redis dorumdorum-be dorumdorum-prometheus dorumdorum-grafana zoo1 pinpoint-hbase pinpoint-collector pinpoint-web 2>/dev/null || true echo ">>> [Deploy] [1/6] ZooKeeper 기동..." - docker compose -f docker-compose.yml up -d zoo1 + $COMPOSE up -d zoo1 for i in $(seq 1 30); do if docker ps --format '{{.Names}}' | grep -q '^zoo1$'; then echo ">>> [Deploy] ZooKeeper up" @@ -281,7 +247,7 @@ jobs: done echo ">>> [Deploy] [2/6] HBase 기동..." - docker compose -f docker-compose.yml up -d pinpoint-hbase + $COMPOSE up -d pinpoint-hbase echo ">>> [Deploy] HBase 초기화 대기..." HBASE_READY=0 for i in $(seq 1 300); do @@ -305,7 +271,7 @@ jobs: fi echo ">>> [Deploy] [3/6] Pinpoint Collector 기동..." - docker compose -f docker-compose.yml up -d pinpoint-collector + $COMPOSE up -d pinpoint-collector COLLECTOR_READY=0 for i in $(seq 1 60); do if docker logs pinpoint-collector 2>&1 | grep -Eq "Started .*CollectorApp|Started CollectorApp"; then @@ -322,7 +288,7 @@ jobs: fi echo ">>> [Deploy] [4/6] Pinpoint Web 기동..." - docker compose -f docker-compose.yml up -d pinpoint-web + $COMPOSE up -d pinpoint-web WEB_READY=0 for i in $(seq 1 60); do if docker logs pinpoint-web 2>&1 | grep -Eq "Started .*WebApp|Started WebApp|Started .*PinpointWebApplication"; then @@ -339,10 +305,10 @@ jobs: fi echo ">>> [Deploy] [5/6] Backend 기동..." - docker compose -f docker-compose.yml up -d backend + $COMPOSE up -d backend echo ">>> [Deploy] [6/6] Redis/Monitoring 기동..." - docker compose -f docker-compose.yml up -d redis prometheus grafana + $COMPOSE up -d redis prometheus grafana echo ">>> [Deploy] 오래된 이미지 정리 중..." docker image prune -af --filter "until=168h" From 6203a3ac3847c13bdc763d0b38bea4c26b3321f2 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Tue, 5 May 2026 14:51:14 +0900 Subject: [PATCH 08/19] fix schema.sql and test code --- src/main/resources/schema.sql | 10 ++++--- .../unit/service/RoommateServiceTest.java | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 36faf01..531f2e6 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -4,12 +4,14 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_user_email CREATE UNIQUE INDEX IF NOT EXISTS uk_user_student_no ON users (student_no); -DROP INDEX IF EXISTS uk_roommate_user_no; +ALTER TABLE IF EXISTS room_rule ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP; + +ALTER TABLE IF EXISTS roommate DROP CONSTRAINT IF EXISTS uk_roommate_user_no; CREATE UNIQUE INDEX IF NOT EXISTS uk_roommate_user_no ON roommate (user_no) WHERE deleted_at IS NULL; -DROP INDEX IF EXISTS uk_room_like_user_room; +ALTER TABLE IF EXISTS room_like DROP CONSTRAINT IF EXISTS uk_room_like_user_room; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_like_user_room ON room_like (user_no, room_no) WHERE deleted_at IS NULL; @@ -17,12 +19,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_room_like_user_room CREATE UNIQUE INDEX IF NOT EXISTS uk_device_user_device ON devices (user_no, device_id); -DROP INDEX IF EXISTS uk_room_request_user_room_direction; +ALTER TABLE IF EXISTS room_request DROP CONSTRAINT IF EXISTS uk_room_request_user_room_direction; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_request_user_room_direction ON room_request (user_no, room_no, direction) WHERE deleted_at IS NULL; -DROP INDEX IF EXISTS uk_chat_room_member_room_user; +ALTER TABLE IF EXISTS chat_room_member DROP CONSTRAINT IF EXISTS uk_chat_room_member_room_user; CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_member_room_user ON chat_room_member (chat_room_no, user_no) WHERE deleted_at IS NULL; diff --git a/src/test/java/com/project/dorumdorum/domain/roommate/unit/service/RoommateServiceTest.java b/src/test/java/com/project/dorumdorum/domain/roommate/unit/service/RoommateServiceTest.java index 60d4d8e..fd3d0b1 100644 --- a/src/test/java/com/project/dorumdorum/domain/roommate/unit/service/RoommateServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/roommate/unit/service/RoommateServiceTest.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.Optional; +import org.mockito.ArgumentCaptor; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -198,4 +200,31 @@ void isHostOfRoom_WhenNotFound_ReturnsFalse() { assertThat(result).isFalse(); } + + @Test + @DisplayName("leaveRoom: 룸메이트를 소프트 삭제하고 저장한다") + void leaveRoom_SoftDeletesRoommate() { + Roommate roommate = Roommate.builder() + .roommateNo("rm1") + .userNo("u1") + .confirmStatus(ConfirmStatus.ACCEPTED) + .roomRole(RoomRole.MEMBER) + .build(); + when(roommateRepository.findByUserNoAndRoomNo("u1", "room1")).thenReturn(Optional.of(roommate)); + + roommateService.leaveRoom("u1", "room1"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Roommate.class); + verify(roommateRepository).save(captor.capture()); + assertThat(captor.getValue().isDeleted()).isTrue(); + } + + @Test + @DisplayName("leaveRoom: 룸메이트를 찾지 못하면 예외를 던진다") + void leaveRoom_WhenNotFound_Throws() { + when(roommateRepository.findByUserNoAndRoomNo("u1", "room1")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> roommateService.leaveRoom("u1", "room1")) + .isInstanceOf(RestApiException.class); + } } From b0f8694b4e150b6a99cd20c47a2de132da1f0757 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Tue, 5 May 2026 15:23:58 +0900 Subject: [PATCH 09/19] fix: sql schema changed --- src/main/resources/schema.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 531f2e6..a08997a 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -7,11 +7,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_user_student_no ALTER TABLE IF EXISTS room_rule ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP; ALTER TABLE IF EXISTS roommate DROP CONSTRAINT IF EXISTS uk_roommate_user_no; +DROP INDEX IF EXISTS uk_roommate_user_no; CREATE UNIQUE INDEX IF NOT EXISTS uk_roommate_user_no ON roommate (user_no) WHERE deleted_at IS NULL; ALTER TABLE IF EXISTS room_like DROP CONSTRAINT IF EXISTS uk_room_like_user_room; +DROP INDEX IF EXISTS uk_room_like_user_room; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_like_user_room ON room_like (user_no, room_no) WHERE deleted_at IS NULL; From e013bfb7fd0c564f88fe4c5b12c2fe8f05ae02c2 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Tue, 5 May 2026 19:22:13 +0900 Subject: [PATCH 10/19] fix: add sql DROP INDEX --- src/main/resources/schema.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index a08997a..bd343de 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -22,11 +22,13 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_device_user_device ON devices (user_no, device_id); ALTER TABLE IF EXISTS room_request DROP CONSTRAINT IF EXISTS uk_room_request_user_room_direction; +DROP INDEX IF EXISTS uk_room_request_user_room_direction; CREATE UNIQUE INDEX IF NOT EXISTS uk_room_request_user_room_direction ON room_request (user_no, room_no, direction) WHERE deleted_at IS NULL; ALTER TABLE IF EXISTS chat_room_member DROP CONSTRAINT IF EXISTS uk_chat_room_member_room_user; +DROP INDEX IF EXISTS uk_chat_room_member_room_user; CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_member_room_user ON chat_room_member (chat_room_no, user_no) WHERE deleted_at IS NULL; From 58a3b96eea913f1d437d98ce79b3803338350b58 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Wed, 6 May 2026 16:56:44 +0900 Subject: [PATCH 11/19] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B2=84=20?= =?UTF-8?q?=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=8A=A4=ED=8E=99=20=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20=EB=B0=8F=20JVM/=EC=BB=A4=EB=84=A5?= =?UTF-8?q?=EC=85=98=ED=92=80=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 5 +++-- src/main/resources/application-prod.yml | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bf88b1f..9180ea8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,6 +46,7 @@ services: - PINPOINT_APPLICATION_NAME=dorumdorum - PINPOINT_AGENT_ID=${PINPOINT_AGENT_ID:-dorumdorum-backend-${HOSTNAME}} - PINPOINT_AGENT_NAME=${PINPOINT_AGENT_NAME:-dorumdorum-be} + - JAVA_OPTS=-Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=20m volumes: - ./firebase-service-account.json:/app/firebase-service-account.json:ro depends_on: @@ -58,8 +59,8 @@ services: deploy: resources: limits: - cpus: "4.0" - memory: 16G + cpus: "2.0" + memory: 8G restart: unless-stopped networks: - dorumdorum-net diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 94c2503..f204d39 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,13 +1,17 @@ server: shutdown: graceful + tomcat: + threads: + max: 100 + min-spare: 10 spring: lifecycle: timeout-per-shutdown-phase: 30s datasource: hikari: - maximum-pool-size: 16 - minimum-idle: 4 + maximum-pool-size: 8 + minimum-idle: 2 connection-timeout: 5000 validation-timeout: 1000 idle-timeout: 600000 From 76cbd5cf793a4332fe3993c7534418b65c8de485 Mon Sep 17 00:00:00 2001 From: ydking0911 Date: Fri, 8 May 2026 12:27:26 +0900 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20github=20PR=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=20-=20deploy=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=9D=B4=EC=8A=A4=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=ED=94=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 9180ea8..89b0fa9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,9 +46,10 @@ services: - PINPOINT_APPLICATION_NAME=dorumdorum - PINPOINT_AGENT_ID=${PINPOINT_AGENT_ID:-dorumdorum-backend-${HOSTNAME}} - PINPOINT_AGENT_NAME=${PINPOINT_AGENT_NAME:-dorumdorum-be} - - JAVA_OPTS=-Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=20m + - JAVA_OPTS=-Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -Xlog:gc\*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=20m volumes: - ./firebase-service-account.json:/app/firebase-service-account.json:ro + - gc_logs:/var/log depends_on: postgres: condition: service_healthy @@ -56,11 +57,8 @@ services: condition: service_started pinpoint-collector: condition: service_started - deploy: - resources: - limits: - cpus: "2.0" - memory: 8G + mem_limit: 8G + cpus: 2.0 restart: unless-stopped networks: - dorumdorum-net @@ -186,6 +184,7 @@ volumes: grafana_data: hbase_data: zookeeper_data: + gc_logs: networks: dorumdorum-net: From 4ca6234fa95bcf417d0554c70614b5a050a391af Mon Sep 17 00:00:00 2001 From: KoungQ Date: Sun, 10 May 2026 13:29:54 +0900 Subject: [PATCH 13/19] =?UTF-8?q?chore:=20JVM=20GC=20=ED=8A=9C=EB=8B=9D?= =?UTF-8?q?=EC=9A=A9=20k6=20=EB=B6=80=ED=95=98=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=99=98=EA=B2=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + docker-compose.yml | 44 +- load-testing/README.md | 16 + load-testing/k6/.gitkeep | 1 + .../dashboards/json/k6-jvm-gc-load-test.json | 776 ++++++++++++++++++ 5 files changed, 834 insertions(+), 6 deletions(-) create mode 100644 load-testing/README.md create mode 100644 load-testing/k6/.gitkeep create mode 100644 monitoring/grafana/provisioning/dashboards/json/k6-jvm-gc-load-test.json diff --git a/.gitignore b/.gitignore index 12b265a..7534d64 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,9 @@ logs/ *.hprof gc.log* +### k6 load test artifacts ### +load-testing/results/ + # macOS artefacts .DS_Store diff --git a/docker-compose.yml b/docker-compose.yml index 7ca60a9..4e4c978 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -40,15 +40,19 @@ services: env_file: - ./secrets/.env environment: - - REDIS_HOST=redis - - REDIS_PORT=6379 - - PINPOINT_COLLECTOR_IP=pinpoint-collector - - PINPOINT_APPLICATION_NAME=dorumdorum - - PINPOINT_AGENT_ID=${PINPOINT_AGENT_ID:-dorumdorum-backend-${HOSTNAME}} - - PINPOINT_AGENT_NAME=${PINPOINT_AGENT_NAME:-dorumdorum-be} + REDIS_HOST: redis + REDIS_PORT: 6379 + PINPOINT_COLLECTOR_IP: pinpoint-collector + PINPOINT_APPLICATION_NAME: dorumdorum + PINPOINT_AGENT_ID: ${PINPOINT_AGENT_ID:-dorumdorum-backend-local} + PINPOINT_AGENT_NAME: ${PINPOINT_AGENT_NAME:-dorumdorum-be} + JAVA_TOOL_OPTIONS: >- + ${JAVA_TOOL_OPTIONS:--XX:+UseG1GC -XX:InitialRAMPercentage=50.0 -XX:MaxRAMPercentage=75.0 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/heapdump -XX:+ExitOnOutOfMemoryError -Xlog:gc*,safepoint:file=/app/logs/gc/gc.log:time,uptime,level,tags:filecount=10,filesize=50m} volumes: - ./secrets/firebase-service-account.json:/app/firebase-service-account.json:ro - ./logs/jfr:/app/logs/jfr + - ./logs/gc:/app/logs/gc + - ./logs/heapdump:/app/logs/heapdump depends_on: postgres: condition: service_healthy @@ -56,6 +60,8 @@ services: condition: service_started pinpoint-collector: condition: service_started + cpus: ${BACKEND_CPU_LIMIT:-4.0} + mem_limit: ${BACKEND_MEMORY_LIMIT:-16g} deploy: resources: limits: @@ -153,12 +159,38 @@ services: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--web.enable-lifecycle" + - "--web.enable-remote-write-receiver" extra_hosts: - "host.docker.internal:host-gateway" restart: unless-stopped networks: - dorumdorum-net + k6: + image: ${K6_IMAGE:-grafana/k6:latest} + container_name: dorumdorum-k6 + profiles: + - loadtest + environment: + BASE_URL: ${K6_BASE_URL:-http://backend:8080} + K6_OUT: ${K6_OUT:-experimental-prometheus-rw} + K6_PROMETHEUS_RW_SERVER_URL: ${K6_PROMETHEUS_RW_SERVER_URL:-http://prometheus:9090/api/v1/write} + K6_PROMETHEUS_RW_PUSH_INTERVAL: ${K6_PROMETHEUS_RW_PUSH_INTERVAL:-5s} + K6_PROMETHEUS_RW_TREND_STATS: ${K6_PROMETHEUS_RW_TREND_STATS:-p(90),p(95),p(99),min,max,avg} + K6_PROMETHEUS_RW_STALE_MARKERS: ${K6_PROMETHEUS_RW_STALE_MARKERS:-true} + K6_WEB_DASHBOARD: ${K6_WEB_DASHBOARD:-true} + K6_WEB_DASHBOARD_EXPORT: ${K6_WEB_DASHBOARD_EXPORT:-/results/k6-report.html} + volumes: + - ./load-testing/k6:/scripts:ro + - ./load-testing/results:/results + working_dir: /scripts + entrypoint: ["k6"] + depends_on: + - backend + - prometheus + networks: + - dorumdorum-net + grafana: image: grafana/grafana:11.2.0 container_name: dorumdorum-grafana diff --git a/load-testing/README.md b/load-testing/README.md new file mode 100644 index 0000000..026fd45 --- /dev/null +++ b/load-testing/README.md @@ -0,0 +1,16 @@ +# k6 Load Testing + +This directory is reserved for k6 load-test assets. Test scripts are intentionally not included. + +Place scripts under `load-testing/k6/` when needed, then run: + +```sh +docker compose --profile loadtest run --rm k6 run --tag testid=gc-baseline /scripts/