Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
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
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

결제시스템구현: 동시성 문제&LOCK 개념 본문

단체개발일지

결제시스템구현: 동시성 문제&LOCK 개념

최밤빵 2024. 8. 25. 23:50

결제 기능을 구현하게되면서, 기술 매니저님의 조언에 따라 꼭 알아두어야 한다는 동시성 이슈LOCK 개념에 대해서 공부하면서 개념을 정리했다. 예시마다 이해가 쉽지않아서 시간이 오래걸렸다🥺

▶ 동시성 이슈란? 

동시성 문제(concurrency issue)란, 여러 사용자가 동시에 같은 자원 (데이터, 메모리 등)에 접근하거나 수정할 때 발생하는 문제를 의미한다. 결제 시스템에서는 이러한 문제가 심각한 버그나 금전적 손실로 이어질 수 있어 특히 주의가 필요하다.

 

▷ 온라인 쇼핑몰에서 100개 한정판 상품을 판매한다고 가정했을 때  : 재고 관리 문제 

public class InventoryService {
    // 주문을 처리하는 메서드
    public void processOrder(Long productId, int quantity) {
            // 1.상품 ID로 해당 상품을 데이터 베이스에서 조회
        Product product = productRepository.findById(productId);
            // 1.재고가 주문 수량보다 크거나 같은지 확인
        if (product.getStock() >= quantity) {
            // 3.재고를 주문 수량만큼 차감
            product.setStock(product.getStock() - quantity);
            // 4.변경된 재고 정보를 데이터베이스에 저장
            productRepository.save(product);

            // 5.주문 처리 로직 (ex.주문 생성)
            createOrder(product, quantity);
        } else {
            // 6.재고가 부족한 경우 얘외를 던져서 알림
            throw new OutOfStockException();
        }
    }

→ 여러 사용자가 동시에 주문을 시도 할 겨우, 실제 재고보다 많은 주문이 처리 될 수 있다. 

 

▷ 사용자 A가 100,000원 짜리 상품을 구매하려 하는데, 네트워크 지연으로 인해 사용자 A가 실수로 결제버튼을 두 번 클릭했다고 가정했을 때 : 중복 결제 문제 

public class PaymentService {
    // 사용자의 결제를 처리하는 메서드
    public void processPayment(Long userId, Long productId, BigDecimal amount) {
        // 1. 사용자 ID로 해당 사용자를 데이터베이스에서 조회
        User user = userRepository.findById(userId);
        // 2. 상품 ID로 해당 상품을 데이터베이스에서 조회
        Product product = productRepository.findById(productId);

        // 3. 사용자의 잔액이 결제 금액보다 크거나 같은지 확인
        if (user.getBalance().compareTo(amount) >= 0) {
            // 4. 사용자의 잔액에서 결제 금액을 차감
            user.setBalance(user.getBalance().subtract(amount));
            // 5. 변경된 사용자 잔액을 데이터베이스에 저장
            userRepository.save(user);

            // 6. 결제 처리 로직 (예: 결제 내역 생성)
            createPaymentRecord(user, product, amount);
        } else {
            // 7. 사용자의 잔액이 부족한 경우 예외를 던져서 알림
            throw new InsufficientBalanceException();
        }
    }
}

→ 두 개의 결제 요청이 거의 동시에 처리 될 경우, 둘 다 잔액 체크를 통과하여 중복 결제가 발생 할 수 있다. 

 

이러한 동시성 문제를 해결하지 않으면, 데이터의 일관성이 깨지거나 시스템이 예기치 않게 동작 할 수 있다. 

 

▶ LOCK 개념

위의 동시성 문제를 해결하기 위해 사용되는것이 LOCK개념으로, 결제 시스템을 설계할 때 주로 데이터베이스에서 사용된다. 여러 트랜잭션이 동시에 데이터에 접근하거나 수정할 때 데이터의 일관성을 유지하기 위한 방법과, LOCK을 통해 여러 사용자가 동시에 같은 데이터를 변경하지 못하도록 하는 방법이 있고, 크게 두 가지 주요 개념이 있다. 

 

▷ 비관적 락 (Pessimistic Lock)

→ 이 방식은 자원에 접근할 때, 다른 작업이 접근하지 못하도록 먼저 LOCK을 걸고, LOCK이 해제 되기 전까지는 다른 작업이 해당 자원에 접근 할 수 없게 된다. 

- 동시성 출돌이 자주 일어날 것으로 예상될 때 유용하다. 

 

→ 장점: 동시성 문제가 발생할 확률이 적다.

         데이터의 안전성을 보장한다.

→ 단점 : 자원이 잠겨 있는동안 다른 작업이 대기해야 하므로 성능이 저하될 수 있다. 

          많은 사용자가 동시에 접근하면 대기 시간이 길어질 수 있다. 

@Transactional  // 이 메서드가 하나의 트랜잭션으로 처리되도록 설정. 만약 에러가 발생하면 모든 변경 사항이 롤백됨.
public void updateAccount(Long accountId, BigDecimal amount) {
    // 1. 주어진 accountId를 가진 계좌 정보를 데이터베이스에서 조회하며, PESSIMISTIC_WRITE 락을 사용하여 다른 트랜잭션이 계좌를 수정하지 못하게 함
    Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
    
    // 2. 조회한 계좌의 현재 잔액에 주어진 amount를 더함
    account.setBalance(account.getBalance().add(amount));

    // 3. 변경된 계좌 정보를 데이터베이스에 저장 (merge는 엔티티의 변경된 상태를 영속성 컨텍스트에 반영)
    entityManager.merge(account);
}

→ LockModeType.PESSIMISTIC_WRITE :

이 LOCK모드는 다른 트랜잭션이 이 계좌를 읽거나 쓰지 못하게 "잠그는"역할을 한다. 현재 트랜잭션이 완료될 때 까지 다른 사용자는 이 계좌를 수정할 수 없다. 이를 통해 동시성 문제를 방지한다. 

→ account.setBalance(account.getBalance().add(amount)); :

조회한 account 객체의 현재 잔액에 amount를 더한다. 

예를 들어, 계좌의 현재 잔액이 1,000원이었고 amount가 500원이라면, 새로운 잔액은 1,500원이 되는 것. 

→ entityManager.merge(account); :

entityManager.merge() 메서드는 변경된 account 객체를 데이터베이스에 저장한다.

이 메서드는 변경된 엔티티를 영속성 컨텍스트에 병합해서, 해당 변경 사항이 데이터베이스에 반영되도록 한다. 

 

→ 비관적 락을 사용해서 계좌의 잔액을 안전하게 업데이트 하는 방법을 보여준다. 이 방식은 여러 사용자가 같은 계좌를 수정하려 할 때 발생할 수 있는 동시성 문제를 방지하는데 유용하다. 결제 시스템과 같은 중요한 금융 애플리케이션에서 데이터의 무결성과 일관성을 유지하는 데 매우 중요한 개념이다. 

 

▷ 낙관적 락 (Optimistic Lock)

→ 이 방식은 자원에 접근할 때 별 다른 제약 없이 자유롭게 접근하도록 허용한다. 다만 자원을 수정하기 전에 변경된 내용이 없는지 확인하고, 만약 변경 내용이 있다면 수정 작업을 중단하고 다시 시도하도록 한다. 

 

→ 장점 : 자주 충돌이 일어나지 않는 경우, 시스템 성능이 높아질 수 있다. 

          데이터 접근이 빠르다. 

→ 단점 : 충돌이 자주 발생하면 성능이 떨어질 수 있다.

          이 경우 트랜잭션을 다시 시도해야 한다. 

// Account 엔티티 클래스 정의
@Entity  // JPA 엔티티임을 나타내는 어노테이션
public class Account {
    @Id  // 기본 키를 나타내는 어노테이션
    private Long id;

    @Version  // 낙관적 락을 위한 버전 필드. 데이터 변경 시 버전이 자동으로 증가하여 충돌을 방지.
    private Long version;

    private BigDecimal balance;  // 계좌의 잔액을 나타내는 필드

    // ======== 기타 필드 ==========

    // Getter와 Setter 메서드들
    // 계좌 ID, 버전, 잔액에 대한 getter와 setter를 제공하여 외부에서 접근 및 수정 가능
    public Long getId() {
        return id;
    }
}
// 계좌의 잔액을 업데이트하는 서비스 메서드 정의
@Transactional  // 이 메서드가 트랜잭션 내에서 실행되도록 보장하는 어노테이션
public void updateAccount(Long accountId, BigDecimal amount) {
    // 1. 주어진 accountId로 계좌를 조회하고, 계좌가 없으면 AccountNotFoundException을 던짐
    Account account = accountRepository.findById(accountId)
            .orElseThrow(() -> new AccountNotFoundException());

    // 2. 조회한 계좌의 현재 잔액에 주어진 amount를 더함
    account.setBalance(account.getBalance().add(amount));

    // 3. 변경된 계좌 정보를 데이터베이스에 저장 (이때 @Version 필드가 변경되어 낙관적 락 충돌을 방지)
    accountRepository.save(account);
}

→ @Version 필드 version

낙관적 락을 위한 버전필드. 데이터가 업데이트 될 때마다 JPA는 이 필드의 값을 자동으로 증가시키고, 동시에 여러 트랜잭션이 같은 데이터를 변경하려 할 때 충돌을 감지하고 방지할 수 있다. 

 

→ 낙관적 락을 통해 동시에 여러 트랜잭션이 동일한 계좌 데이터를 수정할 때 발생할 수 있는 동시성 문제를 방지하는 방법을 보여준다. @Version 필드는 데이터 충돌을 감지하는데 중요한 역할을 하고, 이를 통해 데이터의 무결성을 유지할 수 있다. 

 

▶ LOCK의 주요역할 

결제 과정 중에 발생할 수 있는 모든 데이터의 일관성을 유지하는 것. 결제는 단순히 돈이 오고가는 과정이 아닌 재고관리, 사용자 정보 업데이트, 주문 내역 기록 등 다양한 데이터가 복잡하게 얽혀있는 작업으로 이러한 데이터가 제대로 관리되지 않으면, 큰 문제가 발생할 수 있다 .

 

→ 재고관리 : 재고를 올바르게 관리 하려면 LOCk이 필요하다 

→ 결제처리 : 결제 과정에서 돈이 중복으로 빠져나가는 것을 방지한다

→ 주문기록 : 주문 내역이 중복되거나 누락되지 않도록 보장한다. 

 

▶결론

LOCK개념은 시스템 전체의 데이터 일관성을 유지하고, 안전성을 보장하기 위한 필수적인 도구로 결제라는 민감한 과정에서 오류를 최소화하고 사용자에게 신뢰할 수 있는 서비스를 제공하기 위해서는 LOCK개념을 잘 이해하고 적용하는 것이 중요하다. 비관적 락과 낙관적 락은 각각의 장단점이 있고, 특성과 요구사항을 고려하면서 결정해야한다. 

실제 프로젝트에서는 이 두가지 방식을 혼합해서 사용하는 경우도 많은데, 중요한 건 동시성 이슈가 발생할 수 있는 상황을 인지하고, 적합한 전략을 선택하여 대응하는 것이다. 

 

🤓배운 점 

- 동시성 문제는 항상 예기치 못한 형태로 나타날 수 있다. 시스템의 모든 측면을 고려해야한다. 

- 트랜잭션 관리와 상태 확인은 데이터 일관성을 유지하는데 매우 중요하다. 특히 우리 프로젝트의 견적 제시나 선택 과정에서 트랜잭션을 적절히 관리하고 상태를 확인하여 데이터 불일치를 방지할 수 있을것 같다. 

- 동시성 문제에서 자주 일어나는 여러 구매자의 동시접근을 걱정했지만, 한 의뢰당 한 구매자만 존재하는 시스템의 특성을 이해하고 나니 더 적합한 해결책을 찾을 수 있을 것 같다. 

- 지속적인 리뷰와 개선이 필요하다. 초보개발자로서 시스템을 개발하고 운영하면서 계속해서 새로운 상황을 마주하게 될것이므로, 주기적으로 시스템을 검토하고 개선점을 찾아야 할 것 같다.