3.동시성 제어에서 겪은 문제 해결 과정
개요
진행하고 있는 팀 프로젝트에서 레디스(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번 주문)
테스트 코드 실행 결과
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개로 되었다.
동시성이 제어가 안됐다.
해결 과정
처음에는 코드를 잘못 작성한게 아닌가 해서 코드를 수정하면서 테스트를 진행해봤다.(실패)
분산 락 적용 시 락 획득 대기 시간과 락 유지 시간을 조절하여 경쟁 상태를 피할 수 있을 거라 생각했다. 하지만 단순히 시간을 조절하는 것으로는 근본적인 해결이 되지 않았고, 오히려 불필요한 지연만 발생시켰다.(실패)
@Transactional의 isolation 레벨을 높여 동시성 문제를 해결하려 했다. 하지만 isolation 레벨을 높이면 동시성 문제는 어느 정도 해결되지만, 동시에 성능 저하와 데드락 가능성이 높아지는 단점이 있다고 해서 관련 정보를 찾으며 코드를 작성하다가 중단했다.(실패)
4. 새벽 6시가 됐다 (수면실패)
해결책
private final EntityManager entityManager;
//영속성 컨텍스트를 초기화
entityManager.clear();
해결된 테스트 결과
잘못된 결과가 나오는 이유
영속성 컨텍스트에 저장된 오래된 데이터가 동시성 이슈를 일으킴
clear()는 저장된 데이터를 초기화하고 DB에서 새 데이터를 가져옴
clear()의 동시성 문제 해결 방법
각 트랜잭션이 독립적인 영속성 컨텍스트를 가지게 해 트랜잭션 간 격리성 향상
매 작업마다 DB에서 최신 데이터를 가져와 동시성 문제 해결
단점
매번 DB에서 데이터를 가져와야 해 성능 이슈 발생 가능
상황에 따라 적절한 사용 필요 (하지만 영속성을 초기화 시킬 경우가 별로 없다고 함)
느낀점
일단 작성한 코드를 기반으로 문제를 해결해서 매우 뿌듯하다.
다음에는 레디스의 특성을 이용한 동시성 제어를 사용해서 이러한 문제를 해결해봐야겠다.








