함수형 프로그래밍을 적용한 분산락 성능 개선 (2)

2025. 2. 21. 01:34·Project

이전 글과 이어지는 게시글입니다

 

1. 개요

이전글에서는 분산락에 개선점을 언급하였고, 이를 해결하기 위해 함수형 프로그래밍을 도입한 과정을 소개하려고 한다.

 

분산락의 개선점

  1. 적용 여부 확인의 어려움
  2. 락 장시간 점유로 인한 성능 저하

 

이를 개선하기 위해선 먼저 key 관리 방법을 바꾸고, 락이 필요한 영역에만 락을 적용해야 한다.

기존 코드를 함수형 프로그래밍으로 개선하여 해결할 수 있다.

함수형 프로그래밍은 간단히 파라미터로 함수를 넘기는 것을 말한다.

 

 

2. 구현

LockManager

@Slf4j
@RequiredArgsConstructor
@Component
public class LockManager {
    private final RedissonClient redissonClient;
    private final SupplierForTransaction supplierForTransaction;
    private final String PREFIX = "RedissonLock-";

    public <T> T lock(String lockKey, Supplier<T> supplier) {
        RLock lock = redissonClient.getLock(PREFIX + lockKey);
        try {
            boolean lockable = lock.tryLock(50000, 20000, TimeUnit.MILLISECONDS);
            if (!lockable) {
                log.info("Lock 획득 실패={}", lockKey);
                throw new RuntimeException("Lock 획득 실패");
            }

            log.info("락 획득 및 로직 수행");
            return supplierForTransaction.get(supplier);
        } catch (InterruptedException e) {
            log.error("락을 획득하는 중 에러 발생", e);
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                log.info("락 해제");
                lock.unlock();
            }
        }

        throw new RuntimeException("Lock 획득 실패");
    }
}

 

SupplierForTransaction

@Component
public class SupplierForTransaction {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T proceed(Supplier<T> supplier) {
        return supplier.get();
    }
    
}

 

위 코드를 통해 락이 필요한 부분만 Supplier로 넘겨주면 된다.

기존 코드는 Aspect를 통해 JoinPoint를 받아 이를 트랜잭션으로 감싸줬다면, 이젠 함수를 받아서 트랜잭션을 감싸야 하기 때문에 AopForTransaction 대신 SupplierForTransaction을 통해 처리하였다.

 

DistributedTicketServiceV2

@Service
@RequiredArgsConstructor
public class DistributedTicketServiceV2 {

    private final LockManager lockManager;
    private final DistributedTicketRepository distributedTicketRepository;

    public void ticketingWithRedisson(Long ticketId, Long quantity) {
        lockManager.lock(
                String.valueOf(ticketId),
                () -> {
                    ticketing(ticketId, quantity);
                    return null;
                });
    }

    private void ticketing(Long ticketId, Long quantity) {
        DistributedTicket distributedTicket = distributedTicketRepository.findById(ticketId).orElseThrow();
        distributedTicket.decrease(quantity);
        distributedTicketRepository.saveAndFlush(distributedTicket);
    }

}

 

Supplier는 반환값이 필요하지만 ticketing은 void형태로 반환값을 null로 하여 파라미터에 넣어줘야 했다.

 

3. 테스트 

테스트 환경

  • Ticket의 quantity(수량)은 1000개 할당하여 저장
  • 32개의 스레드를 통해 100개의 티켓팅 요청(수량 하나 감소)을 처리
  • 이 요청을 처리하는 로직에 락 불필요 영역(sleep(500))을 추가하여 테스트 진행

테스트를 진행하기 위해 개선 전/후 코드에 sleep(500)을 추가하였다

public class DistributedTicketServiceV1 {
    ...
    @RedissonLock(value = "#ticketId")
    public void ticketingWithRedisson(Long ticketId, Long quantity) {
        sleep(500);      // 추가

        DistributedTicket distributedTicket = distributedTicketRepository.findById(ticketId).orElseThrow();
        distributedTicket.decrease(quantity);
        distributedTicketRepository.saveAndFlush(distributedTicket);
    }

}

public class DistributedTicketServiceV2 {
    ...
    public void ticketingWithRedisson(Long ticketId, Long quantity) {
        sleep(500);      // 추가
        lockManager.lock(
                String.valueOf(ticketId),
                () -> {
                    ticketing(ticketId, quantity);
                    return null;
                });
    }
    ...
}

 

 

 

DistributedTicketServiceV2Test

@Slf4j
@SpringBootTest
class DistributedTicketServiceV2Test {

    @Autowired
    private DistributedTicketServiceV2 distributedTicketServiceV2;
    @Autowired
    private DistributedTicketRepository distributedTicketRepository;

    private final static Integer CONCURRENT_COUNT = 100;
    private static Long TICKET_ID = null;
    private final static StopWatch stopwatch = new StopWatch();

    @BeforeEach
    public void before() {
        log.info("1000개의 티켓 생성");
        DistributedTicket distributedTicket = new DistributedTicket(1000L);
        DistributedTicket saved = distributedTicketRepository.saveAndFlush(distributedTicket);
        TICKET_ID = saved.getId();
    }

    @AfterEach
    public void after() {
        distributedTicketRepository.deleteAll();
    }

    private void ticketingTest(Consumer<Void> action) throws InterruptedException {
        Long originQuantity = distributedTicketRepository.findById(TICKET_ID).orElseThrow().getQuantity();

        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(CONCURRENT_COUNT);

        for (int i = 0; i < CONCURRENT_COUNT; i++) {
            executorService.submit(() -> {
                try {
                    action.accept(null);
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        DistributedTicket distributedTicket = distributedTicketRepository.findById(TICKET_ID).orElseThrow();
        assertEquals(originQuantity - CONCURRENT_COUNT, distributedTicket.getQuantity());
    }

    @Test
    @DisplayName("동시에 100명의 티켓팅 : 분산락 - 함수형 프로그래밍")
    public void ticketingWithDistributedLock() throws Exception {
        stopwatch.start("동시에 100명의 티켓팅 : 분산락 - 함수형 프로그래밍");
        ticketingTest((_no) -> distributedTicketServiceV2.ticketingWithRedisson(TICKET_ID, 1L));
        stopwatch.stop();

        System.out.println(stopwatch.prettyPrint());
    }

}

 

4. 결과

AOP 분산락(개선 전) 테스트 결과

 

함수형 분산락(개선 후) 테스트 결과

테스트 결과 분석

개선 전 코드는 52.8초 vs 개선 후 코드는 2.8초라는 결과에 집중하자

개선된 코드는 락이 필요한 영역만 락을 할당해줬기 때문에 약 100개의 요청마다 0.5초를 아꼈다.

즉, 개선 전 후의 차이인 50초는 100 * 0.5 = 50의 결과이다.

 

이를 통해 락이 필요한 영역에만 락을 할당해주었기 때문에 락이 불필요한 영역을 로직은 락을 기다리지 않고 먼저 실행될 수 있었고 50초라는 시간의 이점을 얻을 수 있었다.

 

개선 전후의 성능 이점을 얻을 수 있었던 이유를 그림으로 표한하면 다음과 같다.

'Project' 카테고리의 다른 글

낙관적 락 vs 비관적 락 vs 분산락 (with Redisson)  (0) 2025.02.20
Lambda + CDN을 통한 스트리밍 영상 제공  (1) 2025.02.19
GitHub Actions + ECR + ECS + ALB를 통한 CICD  (1) 2024.12.28
'Project' 카테고리의 다른 글
  • 낙관적 락 vs 비관적 락 vs 분산락 (with Redisson)
  • Lambda + CDN을 통한 스트리밍 영상 제공
  • GitHub Actions + ECR + ECS + ALB를 통한 CICD
lsh2613
lsh2613
웹 백엔드 개발자 준비생의 공부일기
  • lsh2613
    Heon's Note
    lsh2613
  • 전체
    오늘
    어제
    • 분류 전체보기 (185)
      • Study (35)
        • Java (0)
        • Spring (14)
        • OOP (4)
        • JPA (12)
        • Design Pattern (3)
        • DB (0)
        • Http & Network (0)
        • Maven (0)
        • Gradle (0)
        • Jenkins (2)
      • DevOps (13)
      • Book Review (0)
        • 자바의 정석 (0)
      • Coding Test (117)
        • 이코테 (5)
        • 백준 (70)
        • 프로그래머스 (37)
        • SW Expert Academy (4)
      • Project (12)
        • WebSocket을 적용한 1:1 채팅 (0)
        • RabbitMQ(STOMP)를 적용한 1:1 채팅 (4)
        • MySQL Spatial Index를 적용한 성능.. (1)
        • Elasticsearch의 전문 검색 인덱스 성능.. (3)
      • Error Solution (6)
      • Review (0)
      • ETC (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    STOMP
    rabbitmq
    채팅
    AMQP
    apic
  • 최근 댓글

  • hELLO· Designed By정상우.v4.10.0
lsh2613
함수형 프로그래밍을 적용한 분산락 성능 개선 (2)
상단으로

티스토리툴바