From 34c876ed0d71fec1be5c73b0944285c0b924430b Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:26:58 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[feat]=20=EC=B1=85=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=EC=A0=95=EC=9D=98=20(#1?= =?UTF-8?q?11)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/scheduler/BookDeleteScheduler.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/scheduler/BookDeleteScheduler.java diff --git a/src/main/java/konkuk/thip/common/scheduler/BookDeleteScheduler.java b/src/main/java/konkuk/thip/common/scheduler/BookDeleteScheduler.java new file mode 100644 index 000000000..367be3684 --- /dev/null +++ b/src/main/java/konkuk/thip/common/scheduler/BookDeleteScheduler.java @@ -0,0 +1,23 @@ +package konkuk.thip.common.scheduler; + +import konkuk.thip.book.application.port.in.BookCleanUpUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class BookDeleteScheduler { + + private final BookCleanUpUseCase bookCleanUpUseCase; + + // 매일 새벽 4시 실행 + @Scheduled(cron = "0 0 4 * * *", zone = "Asia/Seoul") + public void cleanUpUnusedBooks() { + log.info("[스케줄러] 사용되지 않는 Book 데이터 삭제 시작"); + bookCleanUpUseCase.deleteUnusedBooks(); + log.info("[스케줄러] 사용되지 않는 Book 데이터 삭제 완료"); + } +} \ No newline at end of file From 6dcadba11aa3109b52065778552acb7dda04b852 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:27:06 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[feat]=20=EC=B1=85=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/BookCleanUpUseCase.java | 5 ++++ .../service/BookCleanUpService.java | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/konkuk/thip/book/application/port/in/BookCleanUpUseCase.java create mode 100644 src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java diff --git a/src/main/java/konkuk/thip/book/application/port/in/BookCleanUpUseCase.java b/src/main/java/konkuk/thip/book/application/port/in/BookCleanUpUseCase.java new file mode 100644 index 000000000..446385ed3 --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/port/in/BookCleanUpUseCase.java @@ -0,0 +1,5 @@ +package konkuk.thip.book.application.port.in; + +public interface BookCleanUpUseCase { + void deleteUnusedBooks(); +} diff --git a/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java b/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java new file mode 100644 index 000000000..e80b3999b --- /dev/null +++ b/src/main/java/konkuk/thip/book/application/service/BookCleanUpService.java @@ -0,0 +1,30 @@ +package konkuk.thip.book.application.service; + +import konkuk.thip.book.application.port.in.BookCleanUpUseCase; +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.book.application.port.out.BookQueryPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BookCleanUpService implements BookCleanUpUseCase { + + private final BookCommandPort bookCommandPort; + private final BookQueryPort bookQueryPort; + + @Async + @Override + @Transactional + public void deleteUnusedBooks() { + Set unusedBookIds = bookQueryPort.findUnusedBookIds(); + log.info("삭제할 사용되지 않는 Book IDs: {}", unusedBookIds); + bookCommandPort.deleteAllByIdInBatch(unusedBookIds); + } +} From f5293ef7d2beb9c946e40c4b96a39000820cbdba Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:27:23 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[feat]=20=EC=82=AD=EC=A0=9C=EA=B0=80=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EC=B1=85=20id=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/BookQueryPersistenceAdapter.java | 6 ++++++ .../out/persistence/repository/BookJpaRepository.java | 10 ++++++++++ .../thip/book/application/port/out/BookQueryPort.java | 3 +++ 3 files changed, 19 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java index 1e77dc93a..e11deff83 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookQueryPersistenceAdapter.java @@ -12,6 +12,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; @@ -55,4 +56,9 @@ public List findJoiningRoomsBooksByUserId(Long userId) { .map(bookMapper::toDomainEntity) .collect(Collectors.toList()); } + + @Override + public Set findUnusedBookIds() { + return bookJpaRepository.findUnusedBookIds(); + } } diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java index 3ea27d835..b9aa02c9c 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Optional; +import java.util.Set; public interface BookJpaRepository extends JpaRepository { Optional findByIsbn(String isbn); @@ -28,4 +29,13 @@ public interface BookJpaRepository extends JpaRepository { List findJoiningRoomsBooksByUserId(Long userId); boolean existsByIsbn(String isbn); + + // Room, Feed, SavedBook에 모두 참조되지 않은 책 ID만 찾는 쿼리 + @Query(""" + SELECT DISTINCT b.bookId FROM BookJpaEntity b + WHERE b.bookId NOT IN (SELECT DISTINCT r.bookJpaEntity.bookId FROM RoomJpaEntity r) + AND b.bookId NOT IN (SELECT DISTINCT f.bookJpaEntity.bookId FROM FeedJpaEntity f) + AND b.bookId NOT IN (SELECT DISTINCT s.bookJpaEntity.bookId FROM SavedBookJpaEntity s) + """) + Set findUnusedBookIds(); } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java index e4f9c8224..830d23f01 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookQueryPort.java @@ -3,6 +3,7 @@ import konkuk.thip.book.domain.Book; import java.util.List; +import java.util.Set; public interface BookQueryPort { @@ -13,4 +14,6 @@ public interface BookQueryPort { List findSavedBooksByUserId(Long userId); List findJoiningRoomsBooksByUserId(Long userId); + + Set findUnusedBookIds(); } From 0fc3c885a535f14d16d0940814979487e699091e Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:27:30 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[feat]=20=EC=B1=85=20batch=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/BookCommandPersistenceAdapter.java | 6 ++++++ .../thip/book/application/port/out/BookCommandPort.java | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index aef360221..91e40f8a1 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -15,6 +15,7 @@ import org.springframework.stereotype.Repository; import java.util.Optional; +import java.util.Set; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -86,4 +87,9 @@ public void saveSavedBook(Long userId, Long bookId) { public void deleteSavedBook(Long userId, Long bookId) { savedBookJpaRepository.deleteByUserIdAndBookId(userId, bookId); } + + @Override + public void deleteAllByIdInBatch(Set unusedBookIds) { + bookJpaRepository.deleteAllByIdInBatch(unusedBookIds); + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index c3b85b1a8..5c1d019db 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -6,6 +6,7 @@ import konkuk.thip.common.exception.code.ErrorCode; import java.util.Optional; +import java.util.Set; public interface BookCommandPort { @@ -27,4 +28,5 @@ default Book getByIsbnOrThrow(String isbn){ void saveSavedBook(Long userId, Long bookId); void deleteSavedBook(Long userId, Long bookId); + void deleteAllByIdInBatch(Set unusedBookIds); } From cce78a9c2b387005de5c05e761c834eda785de88 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:27:43 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EC=97=90=EC=84=9C=EB=8A=94=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=EB=A5=BC=20=EB=8F=99=EA=B8=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/config/TestAsyncConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/test/java/konkuk/thip/config/TestAsyncConfig.java diff --git a/src/test/java/konkuk/thip/config/TestAsyncConfig.java b/src/test/java/konkuk/thip/config/TestAsyncConfig.java new file mode 100644 index 000000000..398f62fb9 --- /dev/null +++ b/src/test/java/konkuk/thip/config/TestAsyncConfig.java @@ -0,0 +1,18 @@ +package konkuk.thip.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.task.SyncTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@Profile("test") +public class TestAsyncConfig { + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + return new SyncTaskExecutor(); // 동기 실행 + } +} + From 2d592124cf73ff2b0d877925a9c8a61d8cb1a966 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 01:28:05 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[test]=20=EC=B1=85=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=A4=84=EB=9F=AC=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/BookDeleteSchedulerTest.java | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 src/test/java/konkuk/thip/common/scheduler/BookDeleteSchedulerTest.java diff --git a/src/test/java/konkuk/thip/common/scheduler/BookDeleteSchedulerTest.java b/src/test/java/konkuk/thip/common/scheduler/BookDeleteSchedulerTest.java new file mode 100644 index 000000000..ae1d0157b --- /dev/null +++ b/src/test/java/konkuk/thip/common/scheduler/BookDeleteSchedulerTest.java @@ -0,0 +1,80 @@ +package konkuk.thip.common.scheduler; + +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; +import konkuk.thip.book.application.port.in.BookCleanUpUseCase; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +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.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@DisplayName("[통합] Book 삭제 스케줄러 기능 테스트") +class BookDeleteSchedulerTest { + + @Autowired + private BookJpaRepository bookJpaRepository; + + @Autowired + private RoomJpaRepository roomJpaRepository; + + @Autowired + private FeedJpaRepository feedJpaRepository; + + @Autowired + private SavedBookJpaRepository savedBookJpaRepository; + + @Autowired + private UserJpaRepository userJpaRepository; + + @Autowired + private BookCleanUpUseCase bookCleanUpUseCase; + + @Test + @DisplayName("Room, Feed, SavedBook 어디에도 연결되지 않은 Book은 삭제된다") + void deleteUnusedBooks_success() { + // given + // 사용되지 않는 Book + BookJpaEntity unusedBook = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle("고아책")); + + // Room에 연결된 Book + BookJpaEntity roomBook = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle("방책")); + roomJpaRepository.save(TestEntityFactory.createRoom(roomBook, TestEntityFactory.createLiteratureCategory())); + + // Feed에 연결된 Book + BookJpaEntity feedBook = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle("피드책")); + UserJpaEntity feedUser = userJpaRepository.save(TestEntityFactory.createUser(TestEntityFactory.createLiteratureAlias())); + feedJpaRepository.save(TestEntityFactory.createFeed(feedUser, feedBook, true)); + + // SavedBook에 연결된 Book + BookJpaEntity savedBook = bookJpaRepository.save(TestEntityFactory.createBookWithBookTitle("저장책")); + UserJpaEntity savedUser = userJpaRepository.save(TestEntityFactory.createUser(TestEntityFactory.createLiteratureAlias(), "저장유저")); + savedBookJpaRepository.save(TestEntityFactory.createSavedBook(savedUser, savedBook)); + + // when + bookCleanUpUseCase.deleteUnusedBooks(); + + // then + List remainingBooks = bookJpaRepository.findAll(); + + // 삭제되지 않은 책의 제목만 수집 + List remainingTitles = remainingBooks.stream().map(BookJpaEntity::getTitle).toList(); + + assertThat(remainingTitles).contains("방책", "피드책", "저장책"); + assertThat(remainingTitles).doesNotContain("고아책"); + } +} \ No newline at end of file From 1bba783e77c38528fc3f14920504fed7a2a47b73 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Tue, 26 Aug 2025 23:11:21 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[refactor]=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=B1=85=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=88=98=EC=A0=95=20(#111)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/BookJpaRepository.java | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java index b9aa02c9c..60390d96e 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/BookJpaRepository.java @@ -31,11 +31,21 @@ public interface BookJpaRepository extends JpaRepository { boolean existsByIsbn(String isbn); // Room, Feed, SavedBook에 모두 참조되지 않은 책 ID만 찾는 쿼리 - @Query(""" - SELECT DISTINCT b.bookId FROM BookJpaEntity b - WHERE b.bookId NOT IN (SELECT DISTINCT r.bookJpaEntity.bookId FROM RoomJpaEntity r) - AND b.bookId NOT IN (SELECT DISTINCT f.bookJpaEntity.bookId FROM FeedJpaEntity f) - AND b.bookId NOT IN (SELECT DISTINCT s.bookJpaEntity.bookId FROM SavedBookJpaEntity s) - """) + @Query( + "SELECT b.bookId " + + "FROM BookJpaEntity b " + + "WHERE NOT EXISTS ( " + + " SELECT 1 FROM RoomJpaEntity r " + + " WHERE r.bookJpaEntity = b " + + ") " + + "AND NOT EXISTS ( " + + " SELECT 1 FROM FeedJpaEntity f " + + " WHERE f.bookJpaEntity = b " + + ") " + + "AND NOT EXISTS ( " + + " SELECT 1 FROM SavedBookJpaEntity s " + + " WHERE s.bookJpaEntity = b " + + ")" + ) Set findUnusedBookIds(); }