Skip to main content

Command Palette

Search for a command to run...

3.동시성 제어에서 겪은 문제 해결 과정

Published
3 min readView as Markdown
T

안녕하세요! Web개발을 공부하고 있는 윤종일 입니다. 현재는 Java 백엔드 개발을 깊게 파고들고 있어요! 제 궁극적인 목표는 풀스택 개발자가 되는 것입니다. 프론트엔드와 백엔드 모두를 자유롭게 넘나들며, 사용자에게 가치를 전달할 수 있는 완성도 높은 애플리케이션을 만드는 것이 목표입니다.

GitHub 링크

개요

진행하고 있는 팀 프로젝트에서 레디스(Redis)를 사용하여 동시성 제어를 적용했다.
배포 환경에서의 동작 여부를 확인하기 위해 JMeter를 사용하여 테스트를 진행했다.
그 과정에서 레디스 관련 심각한 이슈를 만났지만, 다행히도 문제를 해결할 수 있었다.
이번 경험을 통해 레디스를 활용한 동시성 제어 구현에 대해 많은 것을 배울 수 있었다.

코드

    //상품 주문 코드
    public void createOrder(Map<Long, Long> basket, UserDetailsImpl userDetails, Long addressId)
        throws Exception {
        //상품 수량 검증 코드
        checkBasket(basket);
        //주문 객체 생성
        Order order = new Order(userDetails.getUser().getId(), OrderState.NOTPAYED, addressId);
        //주문 객체 저장
        orderRepository.save(order);
        //장바구니를 순회하며
        //주문한 상품과 상품 개수를 상품 상세정보 테이블에 업데이트
        //상품 테이블의 상품 수량 업데이트
        for (Long key : basket.keySet()) {
            //레디슨 클라이언트를 사용한 분산 락 적용
            String lockKey = "product_lock:" + key;
            RLock lock = redissonClient.getLock(lockKey);
            try {
                boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
                if (!isLocked) {
                    throw new RuntimeException("락 획득에 실패했습니다.");
                }
                //상태를 업데이트하는 메서드
                updateStockAndCreateOrderDetail(key, basket.get(key), order, basket);
            } catch (InterruptedException e) {
                throw new RuntimeException("락 획득 중 오류가 발생했습니다.", e);
            } finally {
                if (lock.isLocked()) {
                    lock.unlock();
                }
            }
        }
    }
    //상태를 업데이트하는 메서드
    @Transactional
    public void updateStockAndCreateOrderDetail(Long productId, Long quantity, Order order, Map<Long, Long> basket) {
        //영속성 컨텍스트를 초기화
        entityManager.clear();
        //상품 객체 생성
        Product product = productService.getProduct(productId);
        product.updateStockAfterOrder(quantity);
        //수정된 상품 저장
        productService.save(product);
        //상품 상세정보 객체 저장
        OrderDetail orderDetail = new OrderDetail(order, productId, quantity,
            product.getPrice(),
            product.getName());
        orderDetailRepository.save(orderDetail);
    }

테스트 코드

@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
class OrderServiceTest {
@MockBean
private ProductRepository productRepository;
@Autowired
private OrderService orderService;
UserDetailsImpl userDetails;
User user;
Product product;
Address address;
Map<Long, Long> basket = new HashMap<>();

    @BeforeEach
    //public void createOrder(Map<Long, Long> basket, UserDetailsImpl userDetails, Long addressId)
    void set() {
    user = new User(1L, "tester", "test@test.com", UserRoleEnum.USER);
    userDetails = new UserDetailsImpl(user);
    product = new Product(1L,"test",1000L,"test",1000L,"photo",user.getId());
    productRepository.save(product);
    address = new Address(1L, "test", "test", "test", user.getId());
    basket.put(1L,2L);
    Mockito.when(productRepository.findById(1L)).thenReturn(Optional.of(product));
    // productRepository.getReferenceById가 호출될 때 product 객체를 반환하도록 설정
    Mockito.when(productRepository.getReferenceById(1L)).thenReturn(product);
    }

    @Test
    @DisplayName("병렬로 100번 실행하여 재고 업데이트 테스트")
    void test(){
        IntStream.range(0, 100).parallel().forEach(i -> {
            try {
                orderService.createOrder(basket,userDetails,address.getId());
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }
}

동시성 제어를 구현하다가 겪은 문제

  • 팀원분이 작성한 코드를 기반으로 레디슨 클라이언트를 사용해서 분산 락을 구현했다.

  • 병렬로 실행한 테스트 코드가 정상 동작 했다.
    (수량 2개씩 100번 주문)

테스트 코드 실행 결과

테스트코드1

테스트코드2

JMeter를 사용한 테스트

  • 배포를 해야하기 때문에 확실한 검증이 필요해서 JMeter로 테스트를 해봤는데 결과가 이상했다.

JMeter 설정
Number of Threads (users) : 100 으로 설정
100명의 유저 csv 파일을 업로드
1초에 100명의 유저가 동시에 1번 주문을 실행되도록 설정
상품 개수는 1000개로 설정
인증/인가 문제는 HTTP Cookie Manager와 Regular Expression Extractor 설정

JMeterr 테스트 결과

  • 예상대로라면 상품은 1000개에서 900개로 수량이 줄어야 한다.

    잘못된 테스트 결과

    잘못된 테스트 수량

  • 하지만 숫자가 올라갔다 내려갔다 이상한 값이 출력 되었다.

  • 수량이 900이 되어야하는데 982개로 되었다.

  • 동시성이 제어가 안됐다.

해결 과정

  1. 처음에는 코드를 잘못 작성한게 아닌가 해서 코드를 수정하면서 테스트를 진행해봤다.(실패)

  2. 분산 락 적용 시 락 획득 대기 시간과 락 유지 시간을 조절하여 경쟁 상태를 피할 수 있을 거라 생각했다. 하지만 단순히 시간을 조절하는 것으로는 근본적인 해결이 되지 않았고, 오히려 불필요한 지연만 발생시켰다.(실패)

  3. @Transactional의 isolation 레벨을 높여 동시성 문제를 해결하려 했다. 하지만 isolation 레벨을 높이면 동시성 문제는 어느 정도 해결되지만, 동시에 성능 저하와 데드락 가능성이 높아지는 단점이 있다고 해서 관련 정보를 찾으며 코드를 작성하다가 중단했다.(실패)

4. 새벽 6시가 됐다 (수면실패)

해결책

private final EntityManager entityManager;
//영속성 컨텍스트를 초기화
entityManager.clear();

해결된 테스트 결과

1

2

수량

잘못된 결과가 나오는 이유

  • 영속성 컨텍스트에 저장된 오래된 데이터가 동시성 이슈를 일으킴

  • clear()는 저장된 데이터를 초기화하고 DB에서 새 데이터를 가져옴

clear()의 동시성 문제 해결 방법

  • 각 트랜잭션이 독립적인 영속성 컨텍스트를 가지게 해 트랜잭션 간 격리성 향상

  • 매 작업마다 DB에서 최신 데이터를 가져와 동시성 문제 해결

단점

  • 매번 DB에서 데이터를 가져와야 해 성능 이슈 발생 가능

  • 상황에 따라 적절한 사용 필요 (하지만 영속성을 초기화 시킬 경우가 별로 없다고 함)

느낀점

  1. 일단 작성한 코드를 기반으로 문제를 해결해서 매우 뿌듯하다.

  2. 다음에는 레디스의 특성을 이용한 동시성 제어를 사용해서 이러한 문제를 해결해봐야겠다.

1 views

More from this blog

서브넷 이해하기

서브넷 IPv4 IPv4로 보자면 데이터 전송 방식은 유니캐스트 멀티캐스트 브로드 캐스트다. 32bit 숫자로 구성되어 있다. 일반적으로 4개의 10진수로 표현된다. 데이터 전송 방식 데이터 전송 방식설명 유니캐스트한 대의 컴퓨터가 다른 한 대의 컴퓨터에게 데이터를 보내는 방식 멀티캐스트한 대의 컴ㅂ퓨터가 특정 그룹에 속한 여러 컴퓨터에게 ㄷ이터를 보내는 방식 브로드캐스트한 대의 컴퓨터가 같은 네트워크에 있는 모든 컴퓨터...

Aug 5, 20242 min read3
서브넷 이해하기

네트워크 계층

네트워크 계층 네트워크는 원하는 정보를 원하는 수신자 또는 기기에 정확하게 전송하기 위한 기반 인프라이다. OSI 7계층 (지겹지만 반복) OSI 7계층은 국제 표준화 기구인 ISO(International Standardization Organization)에서 개발한 컴퓨터 네트워크 프로토콜 디자인과 통신을 계층으로 나누어 설명한 개방형 시스템 상호 연결 모델이다. 각 계층은 서로 독립적으로 구성되어 있고, 각 계층은 하위 계층의 기...

Jul 29, 20243 min read3
네트워크 계층

메모리 단편화

메모리 단편화 메모리 단편화란 분할된 주기억장치에 프로세스를 할당, 반납 과정에서 사용되지 못하고 낭비되는 기억장치가 발생하는 현상이다. 유형으로는 내부 단편화와 외부 단편화가 있다. 내부 단편화 내부 단편화 개념 내부 단편화는 분할된 공간에 프로세스를 적재한 후 남은 공간이다. 내부 단편화는 고정 분할 할당 방식 또는 페이징 기법 사용 시 발생하는 메모리 단편화다. 해결 방안설명 슬랩 할당자 (Slab Allocator)페...

Jul 20, 20241 min read

2024.07.13 애플리케이션 테스트 수행

애플리케이션 테스트 수행 IDE 도구 개념 IDE(Integrated Development Environment) 도구는 코딩, 디버그, 컴파일, 배포 등 프로그램 개발과 관련된 모든 작업을 하나의 프로그램 안에서 처리하는 환경을 제공하는 소프트웨어이다. 기존에는 컴파일러, 텍스트 편집기, 디버거를 따로 사용했으나 통합 개발환경을 통해 하나로 묶은 대화형 인터페이스를 제공한다. IDE 도구의 기능 IDE 도구는 다양한 기능을 통합한 형태...

Jul 12, 20242 min read2
2024.07.13 애플리케이션 테스트 수행
J

Java 백엔드 및 풀스택 개발

111 posts

안녕하세요! Web개발을 공부하고 있는 윤종일 입니다. 현재는 Java 백엔드 개발을 깊게 파고들고 있어요! 제 궁극적인 목표는 풀스택 개발자가 되는 것입니다. 프론트엔드와 백엔드 모두를 자유롭게 넘나들며, 사용자에게 가치를 전달할 수 있는 완성도 높은 애플리케이션을 만드는 것이 목표입니다.

E-Mail : ji4971@naver.com