그리미
파일 공유 링크 삭제 로직 리팩토링 본문
https://dev.mysql.com/doc/refman/8.4/en/delete.html
MySQL :: MySQL 8.4 Reference Manual :: 15.2.2 DELETE Statement
MySQL 8.4 Reference Manual / ... / SQL Statements / Data Manipulation Statements / DELETE Statement DELETE is a DML statement that removes rows from a table. A DELETE statement can start with a WITH clause to define common table expressions
dev.mysql.com
The DELETE statement deletes rows from tbl_name and returns the number of deleted rows.
.
즉, delete 를 사용하면 삭제 행의 개수를 알 수 있다.
이를 이용해 파일 공유 링크 삭제 로직 리팩토링 해보자.
// 기존
@Component
@RequiredArgsConstructor
public class DeleteLinkScheduler {
private static final int DELETE_SIZE = 100;
private final FileShareRepository fileShareRepository;
@Scheduled(cron = "0 0 3 * * ?")
public void deleteExpiredLinks() {
ZonedDateTime expirationTime = ZonedDateTime.now(ZoneOffset.UTC).minusHours(3);
// 삭제 대상의 id 를 담는 리스트
List<Long> expirationLinkIds;
do {
// 삭제 대상의 id를 조회한다 (100 개 단위로 조회)
expirationLinkIds = fileShareRepository.findExpirations(expirationTime, DELETE_SIZE);
// 삭제 대상을 지운다
if (!expirationLinkIds.isEmpty()) {
fileShareRepository.deleteByIds(expirationLinkIds);
}
} while (expirationLinkIds.size() == DELETE_SIZE);
}
}
// delete returns the number of deleted rows 를 바탕으로 리팩터링
@Scheduled(cron = "*/5 * * * * ?")
public void deleteExpiredLinks() {
ZonedDateTime expirationTime = ZonedDateTime.now(ZoneOffset.UTC).minusHours(3);
Integer expirationLinkCnt;
do {
expirationLinkCnt = fileShareRepository.deleteByExpirations(expirationTime, DELETE_SIZE);
} while (expirationLinkCnt == DELETE_SIZE);
}
// 테스트 코드
@ExtendWith(MockitoExtension.class)
@DisplayName("파일공유 링크삭제 스케줄러 테스트")
class DeleteLinkSchedulerTest {
@Mock
private FileShareRepository fileShareRepository;
@InjectMocks
private DeleteLinkScheduler deleteLinkScheduler;
@Test
@DisplayName("파일공유 링크 삭제 성공 테스트")
void deleteExpiredLinksSuccess() {
Integer expiredLinkIds = 99;
when(fileShareRepository.deleteByExpirations(any(ZonedDateTime.class), anyInt()))
.thenReturn(expiredLinkIds);
deleteLinkScheduler.deleteExpiredLinks();
verify(fileShareRepository, times(1)).deleteByExpirations(any(ZonedDateTime.class), anyInt());
}
@Test
@DisplayName("파일공유 링크 삭제 성공 테스트 - 삭제 대상이 딱 100개 일 때 두 번 호출 확인")
void deleteExpiredLinksSuccessTwoIn100() {
when(fileShareRepository.deleteByExpirations(any(ZonedDateTime.class), anyInt()))
.thenReturn(100, 0);
deleteLinkScheduler.deleteExpiredLinks();
verify(fileShareRepository, times(2)).deleteByExpirations(any(ZonedDateTime.class), anyInt());
}
@Test
@DisplayName("파일공유 링크 삭제 성공 테스트 - 두 번 호출 확인")
void deleteExpiredLinksSuccessTwo() {
when(fileShareRepository.deleteByExpirations(any(ZonedDateTime.class), anyInt()))
.thenReturn(100, 10);
deleteLinkScheduler.deleteExpiredLinks();
verify(fileShareRepository, times(2)).deleteByExpirations(any(ZonedDateTime.class), anyInt());
}
}

보다시피 삭제 대상이 딱 100개 남았을 떄는 쿼리가 한 번더 호출된다.
이를 해결하기 위해 101개를 객체를 조회하고 100개를 삭제하는 방식을 고려해 보았으나
삭제 행 뿐만아니라 삭제 대상 객체들도 메모리에 올라와야하는 데서 발생하는 추가적인 메모리와 조회 쿼리가 더 나간 다는 단점이 있어 현재 방식을 고수하기로 했습니다.