[ 5주차 과제 ] 데이터베이스 심화 (비관적 락 동시성 테스트 문제 및 해결)
2025. 4. 19. 15:57ㆍ향해99 8기
비관적 락 동시성 테스트 문제 및 해결
- 문제: 테스트 메소드 @Transactional 때문에 thread1, thread2가 트랜잭션 공유 → Lock Timeout 발생.
- 원인: 스레드별 트랜잭션 분리가 안 됨.
- 해결: 테스트 메소드 @Transactional 제거 + flush() 호출 + 서비스에 REQUIRES_NEW.
- 결과: 락 정상 작동, 재고 40, 테스트 성공.
비관적 락은 스레드마다 새 트랜잭션이 필요하다.
1. 애플리케이션 서비스 (Application Service):
- 외부 요청을 받아 도메인 로직을 조율하고 실행하는 역할.
- 도메인 서비스를 호출하여 비즈니스 로직을 처리.
- 예시: 주문과 고객을 조회하고 할인 계산 후 변경된 주문을 저장.
2. 도메인 서비스 (Domain Service):
- 도메인 로직을 담당하는 서비스로, 엔티티 간 비즈니스 로직을 처리.
- 예시: 할인 계산(VIP 고객 10%, 일반 고객 5% 할인).
3. 애플리케이션 서비스에서 도메인 서비스 분리:
- 애플리케이션 서비스는 조율만 하고, 도메인 서비스는 비즈니스 로직을 처리.
- 책임 분리로 유지보수성과 확장성 향상.
4. 인프라 서비스 (Infrastructure Service)
- 인프라 서비스는 기술적인 부분을 다루며, 도메인이나 애플리케이션 로직에 직접 참여하지 않습니다.
- 외부 시스템과의 연동 또는 기술적 기능(예: 메일 전송, 캐시 관리, 트랜잭션 처리, 메시징 등)을 담당합니다.
도메인 서비스와 애플리케이션 서비스의 분리
1. DiscountService (도메인 서비스)
할인 계산을 담당하는 도메인 서비스입니다.
고객이 VIP인지 여부에 따라 할인 금액을 계산합니다.
할인 계산 로직은 도메인 서비스에서 수행됩니다.
public class DiscountService {
public BigDecimal calculateDiscount(Order order, Customer customer) {
BigDecimal discount = BigDecimal.ZERO;
if (customer.isVip()) {
discount = order.getTotalAmount().multiply(BigDecimal.valueOf(0.10)); // 10% 할인
} else {
discount = order.getTotalAmount().multiply(BigDecimal.valueOf(0.05)); // 5% 할인
}
return discount;
}
}
2. OrderApplicationService (애플리케이션 서비스)
외부 요청을 받아 주문과 고객을 조회하고, 도메인 서비스인 DiscountService를 호출하여 할인 계산을 실행합니다.
애플리케이션 서비스는 도메인 로직을 조율하며, 실제 비즈니스 로직은 도메인 서비스에서 처리합니다.
변경된 주문을 저장합니다.OrderApplicationService (애플리케이션 서비스):
public class OrderApplicationService {
private final DiscountService discountService;
@Transactional
public void applyDiscountToOrder(Long orderId, Long customerId) {
Order order = orderRepository.findById(orderId);
Customer customer = customerRepository.findById(customerId);
if (order == null || customer == null) {
throw new IllegalArgumentException("Order or Customer not found");
}
BigDecimal discount = discountService.calculateDiscount(order, customer); // 도메인 서비스 호출
order.applyDiscount(discount); // 주문에 할인 적용
orderRepository.save(order); // 변경된 주문 저장
}
}
- 도메인 서비스는 비즈니스 로직을 담당하고, 애플리케이션 서비스는 이를 조율하는 역할을 합니다.
- 애플리케이션 서비스는 외부 요청을 처리하고, 도메인 서비스를 호출하여 실제 할인 계산 등을 수행합니다.
인프라 서비스 예시: 트랜잭션 관리
async createUserWithTransaction(userData) {
// 트랜잭션 시작
const transaction = await this.sequelize.transaction();
try {
// 트랜잭션 내에서 유저 생성
const user = await this.userModel.create(userData, { transaction });
// 트랜잭션이 정상적으로 끝났을 때 커밋
await transaction.commit();
return user;
} catch (error) {
// 오류 발생 시 롤백
await transaction.rollback();
throw error; // 에러를 다시 던져서 호출한 곳에서 처리하게 할 수 있습니다.
}
}
동시성 문제 핵심 정리
- Race Condition: 여러 작업이 같은 자원에 동시에 접근할 때 발생하는 문제. 예) 데이터 수정 후 조회 시 예상과 다른 값 반환.
- 상호 배제 (Mutual Exclusion): 한 번에 하나의 작업만 자원에 접근할 수 있도록 막는 방식. DB Lock 사용.
- 교착 상태 (Deadlock): 서로 다른 작업이 다른 자원을 잠그고 서로 기다려 무한 대기 상태에 빠지는 문제.
- 동시성 문제는 데이터 무결성과 정합성을 깨뜨릴 수 있으며, Lock과 동기화가 해결책입니다.
DB의 동시성 문제 핵심 정리
- Concurrency Control (동시성 제어):
- 여러 트랜잭션이 동시에 실행되도록 허용하면서, 데이터 일관성과 무결성을 유지하고 정합성을 보장하는 방법.
- 주요 문제:
- 분실 갱신 (Lost Update): 두 트랜잭션이 동시에 데이터를 수정하고, 한 트랜잭션의 작업 결과가 누락되는 문제.
- 커밋되지 않은 의존 (Uncommitted Dependency): 롤백되는 트랜잭션의 데이터를 읽어, 잘못된 데이터가 반영되는 문제.
- 모순 감지 (Inconsistency Analysis): 트랜잭션 실행 중 다른 트랜잭션이 데이터를 수정하여 데이터 일관성이 깨지는 문제.
- DB 트랜잭션 격리 수준에 따른 문제:
- Dirty Read: 다른 트랜잭션의 변경이 롤백되었을 때, 잘못된 값을 읽는 문제.
- Non-repeatable Read: 트랜잭션이 데이터를 여러 번 읽을 때, 다른 트랜잭션이 데이터를 변경하여 읽은 값이 달라지는 문제.
- Phantom Read: 트랜잭션 실행 중 다른 트랜잭션이 데이터를 추가하여, 새로운 데이터가 나타나는 문제.
- 동시성 문제는 데이터 무결성을 깨뜨리고, 예상치 못한 결과를 초래할 수 있습니다. 트랜잭션 격리 수준에 따라 다양한 읽기 문제가 발생할 수 있습니다.
Database Lock (데이터베이스 락)
목적: DB의 데이터를 동시에 접근하는 것을 제어하여 동시성 문제를 해결하는 방법.
Lock의 종류
- s-lock (공유 락)
- 읽기 잠금(select .. for share)
- 트랜잭션은 읽기 작업만 가능, 다른 트랜잭션도 동일 Row에 대해 s-lock을 설정할 수 있음.
- 쓰기 잠금은 설정되지 않음.
- x-lock (배타 락)
- 쓰기 잠금(select .. for update)
- 트랜잭션은 읽기/쓰기 작업이 모두 가능.
- 다른 트랜잭션은 해당 Row에 대해 s-lock이나 x-lock을 설정할 수 없음.
- 다른 트랜잭션은 대기 상태로 들어가게 됨.
Lock을 이용한 동시성 제어
- Optimistic Lock (낙관적 락):
- 충돌 빈도가 적을 경우 적합.
- 트랜잭션 및 Lock 설정 없이 데이터 변경 여부를 체크하여 동시성 제어.
- 성능 우위: 충돌이 적으면 DB Lock을 사용하지 않아 성능에 유리함.
- 주의: 충돌이 자주 발생하면 롤백과 재시도가 많아져 DB Connection과 스레드 점유 등의 부작용이 있을 수 있음.
- Pessimistic Lock (비관적 락):
- 충돌 빈도가 많을 경우 적합.
- 특정 자원에 대해 Lock을 설정하여 정합성을 보장.
- s-lock 또는 x-lock을 걸어 다른 트랜잭션의 접근을 막음.
- 주의: 불필요한 Lock이 성능 저하를 유발할 수 있고, 데드락을 발생시킬 수 있음. READ 작업이 잦은 테이블에서는 적합하지 않음.
핵심 정리
- Optimistic Lock: 충돌 빈도가 적을 때 성능 우위, 트랜잭션과 Lock을 사용하지 않음.
- Pessimistic Lock: 충돌 빈도가 많을 때 적합, Lock을 사용하여 다른 트랜잭션을 차단함.
동시성 제어 - 낙관적 락 vs 비관적 락
- 상품의 재고 차감 및 복원
- 비관적 락
- 이유: 재고는 중요한 자원이고, 여러 사용자가 동시에 재고를 차감하려 할 때 충돌을 방지하기 위해 작업 시작 전에 락을 걸어야 합니다.
- 유저의 포인트 잔액
- 낙관적 락
- 이유: 포인트는 충돌이 드물고, 수정 여부를 확인한 후 재시도하는 방식이 더 효율적이며 성능에 유리합니다.
- 선착순 쿠폰
- 비관적 락
- 이유: 선착순으로 쿠폰을 받을 때 동시 접근을 제어하고, 정확한 순서 보장을 위해 작업 전에 락을 설정해야 합니다.
'향해99 8기' 카테고리의 다른 글
| [ 6주차 과제 ] 대용량 트래픽&데이터 처리 - 피드백 (0) | 2025.05.12 |
|---|---|
| [ 6주차 과제 ] 대용량 트래픽&데이터 처리 - 정리 (0) | 2025.04.29 |
| [ 3주차 과제 ] 이커머스 Clean + Layered Architecture 설계 (0) | 2025.04.05 |
| [ 2주차 과제 ] API명세, API에러 상황 정리, API에러 상황 정의 (0) | 2025.04.03 |
| [ 2주차 과제 ] 시퀀스 다이어그램, ERD (0) | 2025.04.01 |