프로젝트/팀 개발일지

[Spring Boot/Toy Project] Quostomize - Refactor/카드혜택변경: 변경하기

Se On 2024. 11. 15. 20:59

들어가며

✍️ 카드혜택변경 1차 리팩토링
카드 혜택 변경 시 불필요한 중복 쿼리를 제거하여 한 번의 select, insert, update로 처리되도록 개선하는 것을 목표로 리팩토링을 진행했습니다.
  • 결과: 중복 쿼리를 제거하고, 로직을 단순화하였습니다.
  • 배운 점: 지속적으로 최적화 방법을 탐색하며 개선 작업 진행이 필요함을 느꼈습니다.

Refactor/카드혜택변경: 변경하기

💭 초기 목표

  • 카드혜택 변경 시, 한 번의 select, insert, update로 처리하도록 개선
  • 불필요한 중복 작업을 제거하여 쿼리 성능 최적화

 

🖼️ 현실

  • 문제점: 카드혜택 변경 대상이 5건일 경우, 아래와 같은 쿼리가 발생
    1. is_active를 false로 update (필요)
    2. 대상 카드채번id select (필요)
    3. 신규 혜택 insert (필요)
    4. 불필요한 오래된 혜택 비활성화 update 반복 (중복 발생)
    5. 다시 신규 혜택 insert (필요)
    6. 중복된 비활성화 update 반복 (불필요)
  • 문제 요약: 혜택 비활성화 쿼리가 1번만 실행되어야 하나, 각 insert 시마다 반복 실행됨

🛠️ 1차 해결: deactivateCardBenefitsByCardSequenceId 변경

변경 작업

1. deactivateCardBenefitsByCardSequenceId 호출 최소화 

  • 초기 update 로직을 1번만 실행하도록 수정
  • 중복 호출 원인 파악 후, Repository와 Service 계층에서 변경
int existingBenefit = cardBenefitRepository.deactivateCardBenefitsByCardSequenceId(cardSequenceId, recentTime);

 

2. 혜택변경 쿼리문 및 메서드 수정

  • Repository
    : recentTime을 비교하지 않아도 비활성화 대상 카드를 선정할 수 있기 때문에 아래와 같이 개선하였습니다.
// 기존
@Query("UPDATE CardBenefit cb SET cb.isActive = false WHERE cb.cardDetail.cardSequenceId = :cardSequenceId AND cb.isActive = true AND cb.createdAt < :recentTime")
int deactivateCardBenefitsByCardSequenceId(@Param("cardSequenceId") long cardSequenceId, @Param("recentTime") LocalDateTime recentTime);

// 변경
@Query("update CardBenefit cb set cb.isActive = false where cb.cardDetail.cardSequenceId = :cardSequenceId")
void deactivateCardBenefitsByCardSequenceId(@Param("cardSequenceId") long cardSequenceId);
  • Service
    1) 한 번의 Request에서 cardSequenceId는 동일한 값을 가지므로 첫 번째 요청으로부터 cardSequenceId를 가져옵니다.
    : long cardSequenceId = cardBenefitRequests.get(0).cardSequenceId();

    2) 동일한 cardSequenceid를 가진 모든 CardBenefit의 isActive를 false로 설정합니다. (최초 1번 호출)
    : cardBenefitRepository.deactivateCardBenefitsByCardSequenceId(cardSequenceId);

    3) 새로운 혜택 정보를 일괄 추가합니다.
    : List<CardBenefit> cardBenefits = cardBenefitRequests.stream() ~ .collect(Collectors.toList());
// 기존
    public void updateCardBenefits(List<CardBenefitRequest> cardBenefitRequests) {
        for (CardBenefitRequest request : cardBenefitRequests) {
            long cardSequenceId = request.cardSequenceId();
            int existingBenefit = cardBenefitRepository.deactivateCardBenefitsByCardSequenceId(cardSequenceId, recentTime);
                CardDetail cardDetail = cardDetailRepository.findById(request.cardSequenceId())
                        .orElseThrow(() -> new AppException(ErrorCode.CARD_DETAIL_NOT_FOUND));
                cardBenefitRepository.save(
                        CardBenefit.builder()
                                .cardDetail(cardDetail)
                                .benefitEffectiveDate(request.benefitEffectiveDate())
                                .benefitRate(request.benefitRate())
                                .isActive(true)
                                .upperCategory(BenefitCommonCode.builder().benefitCommonId(request.upperCategoryId()).build())
                                .lowerCategory(request.lowerCategoryId() != null ?
                                        BenefitCommonCode.builder().benefitCommonId(request.lowerCategoryId()).build() : null)
                                .build()
                );
        }
    }

// 변경
    @Transactional
    public void updateCardBenefits(List<CardBenefitRequest> cardBenefitRequests) {
        long cardSequenceId = cardBenefitRequests.get(0).cardSequenceId();
            cardBenefitRepository.deactivateCardBenefitsByCardSequenceId(cardSequenceId);
                CardDetail cardDetail = cardDetailRepository.findById(cardSequenceId)
                        .orElseThrow(() -> new AppException(ErrorCode.CARD_DETAIL_NOT_FOUND));
                List<CardBenefit> cardBenefits = cardBenefitRequests.stream()
                        .map(request -> CardBenefit.builder()
                                        .cardDetail(cardDetail)
                                        .benefitEffectiveDate(request.benefitEffectiveDate())
                                        .benefitRate(request.benefitRate())
                                        .isActive(true)
                                        .upperCategory(BenefitCommonCode.builder().benefitCommonId(request.upperCategoryId()).build())
                                        .lowerCategory(request.lowerCategoryId() != null ?
                                                BenefitCommonCode.builder().benefitCommonId(request.lowerCategoryId()).build() : null)
                                        .build())
                        .collect(Collectors.toList());
                cardBenefitRepository.saveAll(cardBenefits);

 

 

결과

  • 중복된 비활성화 update 제거로 성능 개선 완료
  • 트랜잭션 흐름
    1) is_active를 false로 update (필요)
    2) 대상 카드채번id select (필요)
    3) 신규 혜택 insert (혜택 개수별로 insert, 필요)

 

🛠️ 2차 개선: Bulk Insert 적용 검토

상황

  • 현재는 한 테이블에 insert문이 여러 번 발생
  • Bulk Insert로 최적화 가능성 존재

결정

  • 개발 일정 상 학습 및 구현 시간이 부족해 추후 개선 대상으로 보류

✏️ 배운 점

  1. 계획과 실제의 차이
    • 기존 설계에서 특정 메서드가 반복 호출되며 예상보다 쿼리 실행 횟수가 증가
    • 각 단계의 필요성과 중복을 점검하며 문제를 명확히 정의하는 과정의 중요성
  2. 중요한 개선 포인트
    • 작은 변경으로도 쿼리 효율성을 대폭 개선 가능
    • 반복 호출을 줄이는 단순화 작업이 성능 최적화의 핵심

 

🎯 목적

  1. 지속
    • 현재 수정된 로직 유지
    • 동일한 개선 원칙을 다른 유사 로직에 적용
  2. 추후 개선
    • Bulk Insert와 같은 추가 최적화 작업을 학습 후 적용
    • 반복 로직 분리
  3. 포기
    • 당장 해결할 수 없는 영역은 우선순위를 낮추고, 지속적인 개선 대상으로 분류

정리하면

  • 카드 혜택 변경 시 발생하던 중복된 쿼리를 제거하고 로직을 단순화하여 select, insert, update 쿼리 트랜잭션을 최적화하였습니다.
  • Bulk Insert를 적용한 추가 최적화 가능성을 확인하였으나, 학습 및 구현 시간 등의 이유로 우선순위를 고려하여 추후 개선 대상으로 보류하였습니다.
  • 작은 변경만으로도 효율성을 높일 수 있음을 배웠고, 유사 로직에도 동일한 개선 원칙을 적용할 계획입니다. (예시: 혜택변경 예약하기)

 

GitHub - woorifisa-projects-3rd/Quostomize-BE

Contribute to woorifisa-projects-3rd/Quostomize-BE development by creating an account on GitHub.

github.com

반응형