벌크연산을 통한 쿼리 최적화

2025. 4. 4. 12:05·프로젝트 일기/한편의 수학 학원

🙈 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
'프로젝트 일기/한편의 수학 학원' 카테고리의 다른 글
  • 데코레이터 패턴을 통한 비동기 처리의 안정적 도입
  • 이미지 업로드 API에서의 트랜잭션 병목과 비동기 처리 전략
  • [Refactor] Bean Validation Duplicated
  • 로그인 시도 횟수 제한 기능 구현하기
윤희종
윤희종
호기심을 잃지 말자 지적, 질문은 언제나 환영합니다 ;)
  • 윤희종
    서버견문록
    윤희종
  • 전체
    오늘
    어제
    • 분류 전체보기 (36)
      • 데일리 플랜 (1)
      • 이것저것 (4)
      • Java (6)
      • Spring (12)
        • SpringBoot (10)
        • Spring MVC (0)
      • Computer Science (4)
        • Network (1)
        • Operating System (0)
        • Data Structure (0)
        • Algorithm (2)
        • Database (0)
      • IOS (0)
      • 프로그래머스 문제풀이 (2)
      • 프로젝트 일기 (7)
        • 한편의 수학 학원 (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    성능 개선
    스프링
    인증 테스트
    인증 우회 테스트
    SecurityFilterChain 구성
    스프링 부트
    비동기 처리 유의점
    캐시
    SecurityFilterChain
    springboot
    Spring
    제네릭
    read through
    servlet
    mysql 쿼리 최적화
    알고리즘
    cache write back
    스프링 시큐리티 구성
    스프링 부트 인증 우회
    스프링 시큐리티 사용법
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
윤희종
벌크연산을 통한 쿼리 최적화
상단으로

티스토리툴바