군만두의 IT 개발 일지

5. 동시성 문제 해결: 비관적 락, 낙관적 락, 원자적 연산 본문

학습일지/CS

5. 동시성 문제 해결: 비관적 락, 낙관적 락, 원자적 연산

mandus 2025. 9. 7. 08:38

목차

     

    책 '주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 6장을 읽고, 재고 관리 서비스에서 마이너스 재고 문제가 발생했을 때를 생각하면서 동시성 제어 방법들을 정리했다.

    1. 동시성 문제란?

    여러 사용자가 동시에 같은 데이터를 수정할 때 발생하는 예측할 수 없는 결과다. 가장 흔한 예시는 재고 관리다.

    • 상황: 재고 10개인 상품에 동시에 15개 주문이 들어옴
    • 예상 결과: 10개만 주문 성공, 5개는 재고 부족으로 실패
    • 실제 결과: 15개 모두 주문 성공, 재고 -5개 (데이터 손상)
    // 문제 코드 - Race Condition 발생
    @Service
    @Transactional
    public class ProductService {
        
        public void purchaseProduct(Long productId, int quantity) {
            // 1. 재고 조회
            Product product = productRepository.findById(productId).orElseThrow();
            
            // 2. 재고 확인
            if (product.getStock() < quantity) {
                throw new OutOfStockException("재고 부족");
            }
            
            // 3. 재고 차감 (여기서 동시성 문제 발생)
            product.decreaseStock(quantity);
            productRepository.save(product);
        }
    }

    2. 해결책 1: 비관적 락(Pessimistic Lock)

    데이터를 읽는 순간부터 락을 걸어서 다른 트랜잭션이 접근하지 못하게 하는 방식이다.

    @Repository
    public interface ProductRepository extends JpaRepository<Product, Long> {
        
        @Lock(LockModeType.PESSIMISTIC_WRITE)
        @Query("SELECT p FROM Product p WHERE p.id = :id")
        Optional<Product> findByIdWithLock(@Param("id") Long id);
    }
    
    @Service
    @Transactional
    public class SafeProductService {
        
        public void purchaseProduct(Long productId, int quantity) {
            // 락으로 동시 접근 차단
            Product product = productRepository.findByIdWithLock(productId)
                .orElseThrow();
            
            if (product.getStock() < quantity) {
                throw new OutOfStockException("재고 부족");
            }
            
            // 한 번에 하나의 트랜잭션만 실행됨
            product.decreaseStock(quantity);
            productRepository.save(product);
        }
    }

    특징

    • 장점: 확실한 동시성 제어, 데이터 정합성 보장
    • 단점: 성능 저하, 데드락 위험
    • 사용: 충돌이 빈번하거나 데이터 정합성이 매우 중요한 경우

    3. 해결책 2: 낙관적 락(Optimistic Lock)

    버전 관리를 통해 업데이트할 때만 충돌을 확인하는 방식이다.

    // Entity에 버전 필드 추가
    @Entity
    public class Product {
        private Long id;
        private String name;
        private int stock;
        
        @Version // JPA 낙관적 락을 위한 버전 필드
        private Long version;
        
        public void decreaseStock(int quantity) {
            if (this.stock < quantity) {
                throw new OutOfStockException("재고 부족");
            }
            this.stock -= quantity;
        }
    }
    
    @Service
    @Transactional
    public class OptimisticProductService {
        
        public void purchaseProduct(Long productId, int quantity) {
            try {
                Product product = productRepository.findById(productId).orElseThrow();
                product.decreaseStock(quantity);
                productRepository.save(product);
                
            } catch (OptimisticLockingFailureException e) {
                // 버전 충돌 시 재시도 또는 실패 처리
                throw new ConcurrentModificationException("다시 시도해주세요");
            }
        }
    }

    특징

    • 장점: 좋은 성능, 데드락 없음
    • 단점: 충돌 시 재시도 필요, 복잡한 예외 처리
    • 사용: 충돌이 적고 읽기가 많은 경우

    4. 해결책 3: 원자적 연산(Atomic Operation)

    가장 간단하고 효과적인 방법으로, SQL 쿼리 한 번으로 조건 확인과 업데이트를 동시에 처리한다.

    @Repository
    public interface ProductRepository extends JpaRepository<Product, Long> {
        
        @Modifying
        @Query("UPDATE Product p SET p.stock = p.stock - :quantity " +
               "WHERE p.id = :id AND p.stock >= :quantity")
        int decreaseStockIfAvailable(@Param("id") Long id, @Param("quantity") int quantity);
    }
    
    @Service
    @Transactional
    public class AtomicProductService {
        
        public void purchaseProduct(Long productId, int quantity) {
            // 원자적 연산으로 재고 차감
            int updatedRows = productRepository.decreaseStockIfAvailable(productId, quantity);
            
            if (updatedRows == 0) {
                throw new OutOfStockException("재고 부족");
            }
            
            // 주문 생성
            createOrder(productId, quantity);
        }
    }

    특징

    • 장점: 가장 빠른 성능, 간단한 구현
    • 단점: 복잡한 비즈니스 로직 적용 어려움
    • 사용: 단순한 카운터 증감 작업

    5. 적용 사례: 좋아요 기능 구현

    // 유니크 제약조건으로 중복 방지
    @Entity
    @Table(uniqueConstraints = {
        @UniqueConstraint(columnNames = {"post_id", "user_id"})
    })
    public class PostLike {
        private Long postId;
        private Long userId;
    }
    
    @Service
    @Transactional
    public class PostService {
        
        public void likePost(Long postId, Long userId) {
            try {
                PostLike like = new PostLike(postId, userId);
                postLikeRepository.save(like);
                
                // 좋아요 수 원자적 증가
                postRepository.increaseLikeCount(postId);
                
            } catch (DataIntegrityViolationException e) {
                throw new DuplicateLikeException("이미 좋아요를 눌렀습니다");
            }
        }
    }
    상황 추천 방법 이유
    단순 카운터 (재고, 좋아요) 원자적 연산 가장 빠르고 간단
    읽기 많음, 업데이트 적음 낙관적 락 성능 좋음
    업데이트 빈번, 정확성 중요 비관적 락 확실한 제어
    금융, 결제 시스템 비관적 락 데이터 정합성 최우선

    6. 주의사항: 데드락 방지

    여러 락을 동시에 사용할 때는 일관된 순서로 락을 획득해야 한다.

    // ID 순서로 정렬
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Long firstId = Math.min(fromId, toId);
        Long secondId = Math.max(fromId, toId);
        
        Account first = accountRepository.findByIdWithLock(firstId);
        Account second = accountRepository.findByIdWithLock(secondId);
        // 데드락 방지됨
    }

    참고 자료

    1) 우아한형제들 기술블로그, "MySQL을 이용한 분산락으로 여러 서버에 걸친 동시성 관리", 2019.05.30, https://techblog.woowahan.com/2631

    2) NHN Cloud, "Java에서의 Emoji처리에 대해", 2022.03.17 카카오엔터프라이즈, https://meetup.nhncloud.com/posts/317

     

    이 글은 『 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식』 책을 읽고 학습한 내용을 정리한 것입니다.

    '학습일지 > CS' 카테고리의 다른 글

    [스터디11] 5. 데이터베이스  (0) 2025.09.05
    4. 비동기 연동, 언제 어떻게 써야 할까?  (0) 2025.08.30
    [스터디11] 4. 네트워크  (1) 2025.08.29
    [스터디11] 3. 자료구조  (5) 2025.08.22
    [스터디11] 2. 운영체제  (5) 2025.08.07
    Comments