🙈 1. 문제 파악
현재 서비스에서는, 일일 문자인증 시도 횟수를 제한하고 있다.
이에 따라, 테이블에는 문자 인증 전송 횟수, 인증 중 상태, 인증 코드, 인증 시작 시간 에 관련한 칼럼이 있다.
일별 문자 인증 전송 횟수 초기화를 위해 아래와 같은 로직이 섞여있었다.
@Service
@RequiredArgsConstructor
public class AccountScheduler {
private final MemberRepository memberRepository;
@Scheduled(cron = "0 0 0 * * *")
@Transactional
public void resetVerificationStatus() {
memberRepository.stream().forEach(member -> member.resetVerifyInfo());
}
}
위와 같은 상태에서 코드 실행 시, 모든 member를 조회하는 쿼리 1번, 각 엔티티를 수정하는 쿼리 N번이 발생한다.
💡 2. 해결 방안
이를 JPA 벌크연산을 통해 해결해보도록 한다.
✍️ 3. 해결 과정
@Modifying
@Query("UPDATE Member m SET m.verifyDateTime = null, m.isVerifying = false, m.verifyMessageSendCount = 0, m.verificationCode = null")
void resetVerificationInfos();
를 통해 벌크연산으로 실행되도록 구성했다.
@Service
@RequiredArgsConstructor
public class AccountScheduler {
private final MemberRepository memberRepository;
@Scheduled(cron = "0 0 0 * * *")
@Transactional
public void resetVerificationStatus() {
memberRepository.resetVerificationInfos();
}
}
이전과 비교해보도록 하겠다.
약 1만개의 데이터가 있을 때,
이전의 방식으론, 테스트 환경에서 5분이상 지속되며, 서비스가 멈추는 상황이 발생했다.
이와 반면에, 벌크연산으로 수행했을 때 0.6 초만에 안정적으로 완료됐다.
---
아래는 전체 코드다. 테스트 코드에서 dirtyCheck를 위해 verificationCode
필드를 특정 값으로 변경했다.
package com.hanpyeon.academyapi.account;
import static org.junit.jupiter.api.Assertions.*;
import com.hanpyeon.academyapi.account.entity.Member;
import com.hanpyeon.academyapi.security.Role;
import jakarta.persistence.EntityManager;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StopWatch;
@SpringBootTest
class AccountSchedulerTest {
private static final Logger log = LoggerFactory.getLogger(AccountSchedulerTest.class);
StopWatch stopWatch = new StopWatch();
@Autowired
AccountScheduler accountScheduler;
@Autowired
EntityManager entityManager;
@Test
@Transactional
void measureExecutionTime() {
for (int i = 0 ; i < 100000 ; i ++) {
final Member member = Member.builder()
.name("name" + i)
.role(Role.STUDENT)
.encryptedPassword("test")
.build();
member.setVerificationCode("test");
entityManager.persist(member);
}
entityManager.flush();
stopWatch.start();
accountScheduler.resetVerificationStatus();
entityManager.flush();
stopWatch.stop();
log.info("이전 쿼리 : {}ms", stopWatch.getTotalTimeMillis());
}
@Test
@Transactional
void measureExecutionTime2() {
for (int i = 0 ; i < 100000 ; i ++) {
final Member member = Member.builder()
.name("name" + i)
.role(Role.STUDENT)
.encryptedPassword("test")
.build();
member.setVerificationCode("test");
entityManager.persist(member);
}
entityManager.flush();
stopWatch.start();
accountScheduler.resetVerificationStatus2();
stopWatch.stop();
log.info("개선된 쿼리 : {}ms", stopWatch.getTotalTimeMillis());
}
}
⭐️ 4. 깨달은 점
위와 같은 쿼리 작성으로 서비스의 동작 부분에 있어 성능적 개선이 이뤄질 수 있었다.
반면에, 이와 같은 이점이 있음에도 비즈니스로직이 레포지토리에 자체에 남게 되는 문제도 같이 생겨났다.
TradeOff는 어디서나 존재한다.
문제의 경중을 비교해봤을때, 레포지토리에 비즈니스로직이 존재하게 되는 문제보다, 향상된 속도로 인한 서비스 정상화가 더 큰 이득이라 판단하여
이와 같이 노선을 결정했다.
'프로젝트 일기 > 한편의 수학 학원' 카테고리의 다른 글
API 테스트 하는데... 인증까지 매번..? (0) | 2025.04.22 |
---|---|
데코레이터 패턴을 통한 비동기 처리의 안정적 도입 (0) | 2025.04.14 |
이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략 (0) | 2025.04.13 |
[Refactor] Bean Validation Duplicated (0) | 2025.04.09 |
로그인 시도 횟수 제한 기능 구현하기 (0) | 2025.04.03 |