Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 오블완
- OPENPATH
- 백엔드개발자
- JPA
- 국비지원취업
- baekjoon
- Be
- 환급챌린지
- 오픈패스
- 국비지원교육
- Spring
- 국비지원
- 내일배움카드
- mysql
- 백준
- 티스토리챌린지
- UXUI챌린지
- 디자인챌린지
- 오픈챌린지
- 백엔드 부트캠프
- UXUI기초정복
- 디자인강의
- UXUIPrimary
- 객체지향
- 부트캠프
- Java
- 패스트캠퍼스
- 자바
- 디자인교육
- KDT
Archives
- Today
- Total
군만두의 IT 개발 일지
5. 동시성 문제 해결: 비관적 락, 낙관적 락, 원자적 연산 본문
목차
책 '주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 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

