데이터 정합성과 동시성 처리

EunBeen Noh·2024년 5월 21일
0

SpringAdvanced

목록 보기
4/6

기본 개념

1. 동시성(Concurrency)

  • 여러 작업이 동시에 실행될 수 있는 능력
  • 티스레딩, 비동기 프로그래밍, 병렬 처리를 포함

2. 스레드(Tread)

  • 스레드는 프로그램의 실행 단위
  • 하나의 프로세스는 여러 개의 스레드를 가질 수 있으며, 각각의 스레드는 독립적으로 실행

3. 멀티스레딩 (Multithreading)

  • 여러 스레드를 사용하여 여러 작업을 동시에 처리하는 기법

1. 동기화가 왜 필요한가?

1.1 Race Condition (경쟁상태)

  • 여러 개의 프로세스가 공유 자원에 동시에 접근할 때, 실행 순서에 따라 결과값이 달라질 수 있는 현상
  • 공유 메모리를 사용하는 프로세스끼리 경쟁상태가 발생할 수 있음.
    -> 이에 대한 해결책이 바로 동기화

2. 더티체킹과 동시성 문제

2.1 더티체킹

  • ORM(Object-Relational Mapping) 프레임워크. 특히 JPA(Java Persistence API)와 같은 프레임워크에서 사용되는 개념
  • 엔티티 객체의 상태가 변경되었는지를 감지하고, 이를 데이터베이스에 자동으로 반영하는 매커니즘

2.2 더티체킹 작동 방식

  1. 엔티티 로드
    엔티티가 데이터베이스에서 로드될 때, JPA는 엔티티의 현재 상태를 저장해 둔다.
  2. 엔티티 변경
    애플리케이션에서 엔티티의 필드가 변경된다.
  3. 트랜잭션 커밋
    트랜잭션이 커밋될 때, JPA는 엔티티의 현재 상태와 초기 상태를 비교한다.
  4. 변경 감지
    변경 사항이 있는 경우, JPA는 해당 변경 사항을 SQL UPDATE 문으로 변환하여 데이터베이스에 반영한다.

2.3 더티체킹 방식에서의 Race Condition 발생

  • 멀티스레드 환경에서 여러 스레드가 동시에 동일한 엔티티를 수정하려고 할 때, 더티 체킹이 제대로 동작하지 않아 데이터 정합성에 문제가 생길 수 있음.
  • 예) 두 스레드가 동일한 엔티티를 수정하고 각각의 변경 사항을 데이터베이스에 커밋하려고 할 때, 하나의 스레드가 다른 스레드의 변경 사항을 덮어쓰는 경우가 발생할 수 있음.

3. 데이터 정합성과 유지 방법

3.1 데이터 무결성 (Data Integrity)

  • 데이터의 정확성, 일관성, 유효성을 보장하는 것
  • 데이터가 일관되고 정확하게 유지되는 것

3.2 데이터 정합성(Data Consistency)

  • 어떤 데이터들이 값이 서로 일치하는 상태
  • 비정규형을 사용해 아노말리 (anomaly : 이상현상)가 발생하면 정합성이 지켜지지 않는다.
  • 데이터 정합성을 보장하는 것은 다음과 같은 동시성 환경에서 매우 중요
    • 여러 트랜잭션이 동시에 데이터에 접근할 때
    • 데이터 갱신 중에 발생할 수 있는 충돌을 방지할 때

데이터 무결성의 종류

  • 엔티티 무결성(Entity Integrity)
    • 모든 인스턴스는 고유한 값 또는 NULL 값을 가지지 않는 속성이나 속성 그룹을 가져야 한다.
    • 식별자(Identifier)에 의해서 지켜질 수 있다.
  • 도메인 무결성(Domain Integrity)
    • 엔터티의 특정 속성 값은 같은 데이터 타입과 길이, 같은 널 여부, 같은 기본 값, 같은 허용 값 등 동일한 범주의 값만이 존재해야 한다.
    • 도메인 무결성은 기본 값이나 널 여부, 체크 조건 등으로 지켜질 수 있다.
  • 참조 무결성(Referential Integrity)
    • 엔터티의 외래 식별자 속성은 참조되는 엔터티의 주 식별자 값과 일치하거나 널(Null) 값이어야 한다.
    • 참조 무결성은 외래 키 제약조건(foreign key constraint)에 의해서 지켜진다.
  • 업무 무결성(Business Integrity)
    • 기업에서 업무를 수행하는 방법이나 데이터를 처리하는 규칙을 의미한다.
    • 업무 무결성을 물리적으로 강제하는 대표적인 방법에 트리거(Trigger)가 존재

3.3 데이터 정합성을 유지하기 위한 방법

  • 트랜잭션 관리

    • 트랜잭션 경계를 명확히 하여 일관된 데이터 상태를 유지
  • 제약 조건 설정

    • PRIMARY KEY, FOREIGN KEY, UNIQUE, CHECK 등의 데이터베이스 제약 조건을 사용하여 데이터의 무결성을 유지
  • 락 (Lock)

    • 동시성 문제를 해결하기 위해 데이터베이스 락을 사용
    • Pessimistic Locking, Optimistic Locking 등을 통해 동시 접근을 제어
  • 검증 로직

    • 애플리케이션 레벨에서 데이터 검증 로직을 추가하여 데이터베이스에 잘못된 데이터가 저장되는 것을 방지

3.4 동시성 처리 방법

1. Synchronized

  • Java의 synchronized 키워드를 사용하여 메서드나 블록을 동기화
  • 이를 통해 특정 코드 섹션을 한 번에 하나의 스레드만 접근할 수 있도록 한다.

2. Pessimistic Locking (비관적 락)

  • 데이터에 접근하는 동안 다른 트랜잭션이 접근하지 못하도록 잠금(Lock)을 거는 것
  • 주로 데이터베이스에서 레코드를 읽거나 쓸 때 사용됩니다.

3. Optimistic Locking(낙관적 락)

  • 데이터 갱신 시 충돌을 검출하고, 충돌이 발생하면 갱신을 실패시키는 방식
  • 이를 위해 주로 버전 번호를 사용

4. Named Locking

  • 이름 기반의 락을 사용하여 특정 리소스를 보호
  • 주로 Java의 java.util.concurrent.locks.ReentrantLock을 사용

5. Redis (Lettuce, Redisson)

  • Redis를 사용한 분산 락 메커니즘
  • Lettuce와 Redisson은 Redis 클라이언트 라이브러리로, 분산 환경에서 동기화 문제를 해결하는 데 사용

6. TreadLocal 사용

  • 각 스레드마다 독립적인 변수를 가질 수 있게 하는 방법
  • 이를 통해 특정 스레드에만 데이터를 저장하고 사용할 수 있도록 함

4. 예제 코드

@Getter
@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;
    ...
}
public class AccountService {
    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public void updateBalance(Long accountId, BigDecimal amount) {
        Account account = entityManager.find(Account.class, accountId);
        BigDecimal newBalance = account.getBalance().add(amount);
        account.setBalance(newBalance);
    }
}
// 스레드 1
public void run() {
    accountService.updateBalance(1L, new BigDecimal("100.00"));
}

// 스레드 2
public void run() {
    accountService.updateBalance(1L, new BigDecimal("50.00"));
}

[ 문제 상황 ]

  • 두 스레드가 동시에 동일한 계정(Account)의 잔액(balance)을 업데이트하려고 시도한다.

작업 순서

  1. 스레드 1이 Account 엔티티를 조회하여 잔액을 가져온다.
  2. 스레드 2도 같은 엔티티를 조회하여 잔액을 가져온다.
  3. 스레드 1이 새로운 잔액을 계산하고 설정한다.
  4. 스레드 2도 새로운 잔액을 계산하고 설정한다.
  5. 스레드 1이 트랜잭션을 커밋하여 데이터베이스에 반영한다.
  6. 스레드 2도 트랜잭션을 커밋하여 데이터베이스에 반영한다.

이 과정에서 스레드 2의 변경 사항이 스레드 1의 변경 사항을 덮어쓰게 된다.
스레드 1이 먼저 커밋되었으므로, 데이터베이스의 잔액은 1100.00이 된다.

결국 최종 잔액은 두 업데이트의 합이 아닌, 마지막으로 커밋된 값이 되어버린다.
-> Race Condition에 의한 데이터 불일치 문제 발생

해결 방법

1. Pessimistic Locking (비관적 락)

  • 엔티티를 수정하기 전에 잠금을 걸어 다른 트랜잭션이 접근하지 못하도록 한다.
  • LockModeType.PESSIMISTIC_WRITE를 사용하여 엔티티에 쓰기 잠금을 건다.
    • 이 잠금을 사용하면 다른 트랜잭션이 이 엔티티를 수정할 수 없으며, 읽기 시에도 잠금을 걸 수 있게 된다.
  • 잠금이 걸린 동안에는 다른 트랜잭션이 해당 엔티티를 읽거나 수정하려고 하면 대기하거나 예외가 발생
    -> 잔액을 업데이트하고 트랜잭션을 커밋하고 나서야 다른 트랜잭션이 접근 가능하게 된다.
@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
    Account account = entityManager.find(Account.class, accountId, LockModeType.PESSIMISTIC_WRITE);
    BigDecimal newBalance = account.getBalance().add(amount);
    account.setBalance(newBalance);
}

2. Optimistic Locking (낙관적 락)

  • 엔티티에 @Version 필드 추가
    • JPA는 엔티티를 저장할 때 이 필드의 값을 자동으로 증가
    • 이 필드는 JPA에 의해 자동으로 관리
  • 트랜잭션이 엔티티를 수정하고 저장할 때, 현재 버전 번호와 데이터베이스에 저장된 버전 번호를 비교
    • 만약 다른 트랜잭션이 먼저 해당 엔티티를 수정하여 버전 번호가 변경되었다면 OptimisticLockException이 발생
  • 이 방법은 충돌이 발생할 가능성이 낮은 경우에 적합하며, 트랜잭션이 커밋될 때만 충돌을 감지한다.
@Entity
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private BigDecimal balance;
    @Version
    private Long version;
    ...
}

@Transactional
public void updateBalance(Long accountId, BigDecimal amount) {
    Account account = entityManager.find(Account.class, accountId);
    BigDecimal newBalance = account.getBalance().add(amount);
    account.setBalance(newBalance);
    // 버전 체크를 통한 Optimistic Locking
    entityManager.flush(); // 트랜잭션이 커밋될 때 버전 번호를 확인하고, 충돌이 발생하면 예외를 던짐
}

Pessimistic Locking vs. Optimistic Locking

  • 비관적 락 (Pessimistic Locking)
    • 데이터에 접근할 때 즉시 잠금을 걸어 다른 트랜잭션이 접근하지 못하게 함.
    • 충돌 가능성이 높을 때 사용

  • 낙관적 락 (Optimistic Locking)
    • 충돌이 드물다고 가정하고, 트랜잭션이 끝날 때 충돌을 감지
    • 버전 필드를 사용하여 구현합니다. 충돌 가능성이 낮을 때 사용

3. etc

  • Atomic Operations (원자적 연산)
    • 동시성 문제를 해결하기 위해 java.util.concurrent.atomic 패키지의 원자적 변수를 사용
    • 하지만 이는 주로 간단한 데이터 타입에 한정
  • Application-Level Locking
    • 비즈니스 로직 레벨에서 명시적으로 잠금을 관리하는 방법 - 예) Redis와 같은 분산 캐시를 사용하여 분산 락 구현

0개의 댓글