데이터베이스에서의 Locking 메커니즘은 무엇인가요?

김상욱·2024년 12월 15일
0

데이터베이스에서의 Locking 메커니즘은 무엇인가요?

데이터베이스에서의 Locking(잠금) 메커니즘은 동시성 제어(concurrency control)의 핵심 요소로, 여러 트랜잭션이 동시에 데이터에 접근할 때 발생할 수 있는 충돌을 방지하고 데이터의 일관성과 무결성을 유지하기 위해 사용됩니다.

경합 상태(Race Condition) : 여러 트랜잭션이 동시에 같은 데이터를 수정하려 할 때 발생
일관성 문제 : 한 트랜잭션이 데이터를 변경하는 동안 다른 트랜잭션이 그 데이터를 읽거나 변경하면 데이터의 일관성이 깨질 수 있음.
데이터 무결성 : 동시 접근으로 인해 데이터 무결성 제약 조건이 위반될 수 있음.

잠금

  • 공유 잠금(Shared Lock, S Lock) : 여러 트랜잭션이 동시에 데이터를 읽을 수 있도록 허용합니다. 단 데이터를 수정하려는 트랜잭션은 공유 잠금을 획득한 모든 트랜잭션이 완료될 때까지 대기해야 합니다.
  • 배타 잠금(Exclusive Lock, X Lock) : 데이터를 수정하거나 삭제하려는 트랜잭션에 부여됩니다. 배타 잠금을 획득한 트랜잭션 외에는 해당 데이터에 대한 다른 잠금을 요청할 수 없으며, 다른 트랜잭션은 데이터를 읽거나 수정할 수 없습니다.

잠금의 세부화(Granularity) : 물리적 범위에 따른 세분화

  • 행 수준 잠금(Row-Level Lock) : 특정 행에 대해 잠금을 설정합니다. 높은 동시성르 제공하지만 잠금 관리 오버헤드가 큽니다.
  • 페이지 수준 잠금(Page-Level Lock) : 데이터베이스 페이지 단위로 잠금을 설정합니다. 행 수준과 테이블 수준의 중간 정도의 동시성을 제공.
  • 테이블 수준 잠금(Table-Level Lock) : 전체 테이블에 대해 잠금을 설정합니다. 잠금 관리가 간단하지만 동시성이 낮아질 수 있습니다.
  • 데이터베이스 수준 잠금(Database-Level Lock) : 전체 데이터베이스에 대해 잠금을 설정합니다. 주로 관리 작업 시 사용되며, 동시성이 매우 낮아집니다.

2단계 잠금 프로토콜(Two-Phase Locking, 2PL)

2단계 잠금 프로토콜을 모든 트랜잭션이 잠금을 획득하는 단계와 잠금을 해제하는 단계의 두 단계를 거치도록 규정.

  • 확장 단계(Expanding Phase) : 트랜잭션이 잠금을 획득하는 단계. 이 단계에서는 잠금을 해제할 수 없습니다.
  • 축소 단계(Shrinking Phase) : 트랜잭션이 잠금을 해제하는 단계입니다. 이 단계에서는 더 이상 잠금을 획득할 수 없습니다.

2PL은 직렬화를 보장하여 데이터의 일관성을 유지하지만, 교착 상태의 가능성을 증가시킵니다.

교착상태(DeadLock)

교착 상태란?
교착 상태는 두 개 이상의 트랜잭션이 서로가 가진 잠금을 기다리면서 무한 대기 상태에 빠지는 상황을 말합니다. 예를 들어, 트랜잭션 A가 자원 X에 대한 배타 잠금을 보유하고 있으며 트랜잭션 B가 자원 Y에 대한 배타 잠금을 보유하고 있을 때, 트랜잭션 A가 자원 Y을, 트랜잭션 B가 자원 X을 잠그려고 하면 교착상태가 발생할 수 있습니다.

교착 상태 해결 방법

  • 교착 상태 예방 : 잠금 획득 순서를 정하거나, 트랜잭션이 필요한 모든 잠금을 한 번에 요청하도록 하여 교착 상태가 발생하지 않도록 합니다.
  • 교착 상태 탐지 및 회복 : 주기적으로 교착 상태를 탐지하고, 교착 상태가 발견되면 하나 이상의 트랜잭션을 강제로 종료하여 교착 상태를 해소.
  • 타임아웃 설정 : 트랜잭션이 일정 시간 내에 필요한 잠금을 획득하지 못하면 트랜잭션을 롤백하여 교착 상태를 방지

격리 수준(Isolation Levels)과 잠금

데이터베이스는 여러 격리 수준을 제공하여 트랜잭션 간의 상호 작용을 제어. 각 격리 수준은 잠금 전략에 영향을 미친다.

  • Read Uncommitted : 가장 낮은 격리 수준으로 트랜잭션이 커밋되지 않은 데이터를 읽을 수 있습니다. 잠금이 거의 없거나 공유 잠금만 사용
  • REad Committed : 트랜잭션이 커밋된 데이터만을 읽을 수 있습니다. 공유 잠금을 사용하여 데이터 읽기 시 잠금을 설정하고, 읽기 후 즉시 해제됩니다.
  • Repeatable Read : 트랜잭션이 시작된 시점의 데이터를 계속 읽을 수 있습니다. 데이터를 읽을 때 공유 잠금을 유지하여 다른 트랜잭션이 데이터를 수정하지 못하도록 합니다.
  • Serializable : 가장 높은 격리 수준으로, 트랜잭션들이 완전히 직렬화된 것처럼 동작하도록 보장합니다. 범위 잠금과 같은 추가적인 잠금 전략을 사용하여 팬텀읽기를 방지.

격리 수주이 높을수록 데이터 일관성은 높아지지만, 동시성은 낮아지고 잠금 오버헤드가 증가할 수 있습니다.

낙관적 잠금과 비관적 잠금

잠금 메커니즘은 전통적인 비관적 잠금(Pesssimistic Locking) 외에도 낙관적 잠금(Optimistic Locking) 방식을 사용할 수 있습니다.

비관적 잠금 : 비관적 잠금은 트랜잭션이 데이터를 수정하기 전에 반드시 잠금을 획득하여 다른 트랜잭션의 접근을 차단. 주로 데이터 충돌 가능성이 높을 때 사용.
낙관적 잠금 : 낙관적 잠금은 트랜잭션이 데이터를 수정할 때 충돌이 발생하지 않을 것이라고 가정하고 잠금을 사용하지 않습니다. 대신, 데이터 수정 시점에 충돌 여부를 검사하여 문제가 있을 경우 트랜잭션을 롤백하거나 재시도. 주로 데이터 충돌 가능성이 낮을 때 사용됩니다.

잠금 관리의 최적화

잠금 메커니즘으 데이터 성능에 큰 영향을 미치므로, 다음과 같은 최적화 방법 고려.

잠금 범위 최소화: 필요한 최소한의 데이터에 대해서만 잠금을 설정하여 동시성을 높입니다.

트랜잭션 크기 축소: 트랜잭션이 빠르게 완료되도록 하여 잠금 유지 시간을 줄입니다.

적절한 격리 수준 선택: 응용 프로그램의 요구사항에 맞는 적절한 격리 수준을 선택하여 성능과 일관성 간의 균형을 맞춥니다.

인덱스 최적화: 효율적인 인덱스를 사용하여 잠금 범위를 줄이고 쿼리 성능을 향상시킵니다.


신입 또는 취업 준비 중인 Java와 Spring 백엔드 개발자라면 데이터베이스의 Locking 메커니즘을 이해하고 실습해보는 것이 매우 중요합니다. 이를 통해 동시성 제어와 트랜잭션 관리에 대한 실무 감각을 키울 수 있습니다. 아래에 몇 가지 실습 아이디어와 단계별 가이드를 제시하겠습니다.

1. 기본 트랜잭션 관리 실습

목표:

Spring의 트랜잭션 관리 기능을 이해하고, 간단한 CRUD 애플리케이션에서 트랜잭션을 적용해보기.

실습 내용:

  • 프로젝트 설정:

    • Spring Boot 프로젝트 생성 (Spring Initializr 사용)
    • 의존성: Spring Web, Spring Data JPA, H2 Database (개발 및 테스트용 인메모리 DB)
  • 엔티티 및 리포지토리 생성:

    @Entity
    public class Account {
        @Id @GeneratedValue
        private Long id;
        private String owner;
        private Double balance;
        // getters and setters
    }
    
    public interface AccountRepository extends JpaRepository<Account, Long> {}
  • 서비스 계층에 트랜잭션 적용:

    @Service
    public class AccountService {
        @Autowired
        private AccountRepository accountRepository;
    
        @Transactional
        public void transfer(Long fromId, Long toId, Double amount) {
            Account from = accountRepository.findById(fromId).orElseThrow();
            Account to = accountRepository.findById(toId).orElseThrow();
    
            from.setBalance(from.getBalance() - amount);
            to.setBalance(to.getBalance() + amount);
    
            accountRepository.save(from);
            accountRepository.save(to);
        }
    }
  • 테스트 작성:

    • 여러 스레드를 사용하여 동시 트랜잭션을 수행하고, 데이터 일관성을 확인합니다.
    • 예를 들어, 두 개의 스레드가 동시에 동일한 계좌에서 출금하려고 시도할 때의 동작을 관찰합니다.

2. 동시성 문제 및 Locking 이해하기

목표:

동시성 문제(예: Lost Update, Dirty Read)를 직접 경험하고, 이를 해결하기 위한 Locking 메커니즘을 적용해보기.

실습 내용:

  • 동시성 문제 시나리오 구현:

    • 동일한 계좌에 대해 동시에 두 개의 트랜잭션이 업데이트를 시도하는 상황을 만듭니다.
    • @Transactional을 사용하여 트랜잭션 범위를 설정하고, PropagationIsolation 수준을 조정해봅니다.
  • Locking 메커니즘 적용:

    • 낙관적 잠금(Optimistic Locking):

      @Entity
      public class Account {
          @Id @GeneratedValue
          private Long id;
          private String owner;
          private Double balance;
      
          @Version
          private Integer version;
          // getters and setters
      }
      • @Version 어노테이션을 추가하여 낙관적 잠금을 구현합니다.
      • 동시 업데이트 시 OptimisticLockingFailureException이 발생하는 것을 확인합니다.
    • 비관적 잠금(Pessimistic Locking):

      public interface AccountRepository extends JpaRepository<Account, Long> {
          @Lock(LockModeType.PESSIMISTIC_WRITE)
          @Query("SELECT a FROM Account a WHERE a.id = :id")
          Account findAccountForUpdate(@Param("id") Long id);
      }
      
      @Service
      public class AccountService {
          @Autowired
          private AccountRepository accountRepository;
      
          @Transactional
          public void transferWithLock(Long fromId, Long toId, Double amount) {
              Account from = accountRepository.findAccountForUpdate(fromId);
              Account to = accountRepository.findAccountForUpdate(toId);
      
              from.setBalance(from.getBalance() - amount);
              to.setBalance(to.getBalance() + amount);
      
              accountRepository.save(from);
              accountRepository.save(to);
          }
      }
      • LockModeType.PESSIMISTIC_WRITE를 사용하여 비관적 잠금을 구현합니다.
      • 동시 접근 시 데이터가 안전하게 업데이트되는 것을 확인합니다.

3. 교착 상태(Deadlock) 시뮬레이션 및 해결

목표:

교착 상태 상황을 이해하고, 이를 회피하거나 해결하는 방법을 학습합니다.

실습 내용:

  • 교착 상태 시나리오 구현:

    • 두 개 이상의 트랜잭션이 서로 다른 자원을 잠그고, 상대방이 가진 자원을 기다리는 상황을 만듭니다.
    • 예를 들어, 트랜잭션 A는 계좌 1을 잠그고 계좌 2를 잠그려 하고, 트랜잭션 B는 계좌 2를 잠그고 계좌 1을 잠그려 할 때 발생하는 교착 상태를 구현합니다.
  • 교착 상태 탐지 및 회복:

    • 데이터베이스에서 교착 상태를 탐지하고, 특정 트랜잭션을 롤백하는 메커니즘을 이해합니다.
    • Spring에서 예외 처리 로직을 구현하여 교착 상태 발생 시 트랜잭션을 재시도하거나 사용자에게 알림을 제공합니다.

4. 격리 수준(Isolation Levels) 실험

목표:

다양한 격리 수준이 동시성 및 데이터 일관성에 미치는 영향을 실습을 통해 이해합니다.

실습 내용:

  • 격리 수준 설정:

    • @Transactional 어노테이션의 isolation 속성을 사용하여 다양한 격리 수준을 설정합니다.
      @Transactional(isolation = Isolation.READ_COMMITTED)
      public void someMethod() { ... }
  • 각 격리 수준별 동작 확인:

    • Read Uncommitted: 더티 리드(Dirty Read) 가능 여부 확인.
    • Read Committed: 더티 리드는 방지되지만 반복 불가능한 읽기(Non-repeatable Read) 가능.
    • Repeatable Read: 반복 불가능한 읽이는 방지되지만 팬텀 리드(Phantom Read) 가능.
    • Serializable: 모든 종류의 읽기 문제 방지.
  • 동시성 테스트 작성:

    • 여러 트랜잭션이 동시에 실행될 때 각 격리 수준에서 데이터가 어떻게 변하는지 관찰합니다.
    • 이를 통해 각 격리 수준의 장단점을 체감할 수 있습니다.

5. 실제 데이터베이스와의 연동

목표:

MySQL이나 PostgreSQL과 같은 실제 데이터베이스를 사용하여 잠금 메커니즘을 더 깊이 이해합니다.

실습 내용:

  • 데이터베이스 설정:

    • MySQL이나 PostgreSQL을 설치하고, Spring Boot 애플리케이션과 연동합니다.
    • 해당 데이터베이스의 잠금 메커니즘과 설정을 학습합니다.
  • Gap Lock, Next-Key Lock 등의 고급 잠금 기능 실습:

    • MySQL의 InnoDB 스토리지 엔진을 사용하여 Gap Lock이나 Next-Key Lock을 경험해봅니다.
    • PostgreSQL의 MVCC를 활용하여 잠금 없는 읽기(Read without Locking) 등을 실습합니다.
  • 교착 상태 및 성능 테스트:

    • 실제 데이터베이스 환경에서 교착 상태를 유발하고, 이를 해결하는 방법을 실습합니다.
    • 잠금 설정에 따른 애플리케이션의 성능 변화를 측정하고 최적화 방안을 모색합니다.

6. 추가 학습 자료 및 도구 활용

목표:

잠금 메커니즘과 동시성 제어에 대한 이해를 심화하기 위한 추가 자료와 도구를 활용합니다.

추천 자료:

도구 활용:

  • JMeter: 동시성 테스트를 자동화하고, 애플리케이션의 동시 접근 시 동작을 모니터링합니다.
  • VisualVM: JVM의 스레드 상태와 잠금 대기 상태를 시각화하여 분석합니다.
  • Database 클라이언트 도구: MySQL Workbench나 pgAdmin을 사용하여 잠금 상태를 실시간으로 모니터링합니다.

7. 프로젝트 예제: 은행 계좌 관리 시스템

목표:

위에서 학습한 내용을 종합하여 실제 애플리케이션에 적용해봅니다.

프로젝트 구성:

  • 기능:

    • 사용자 계좌 생성, 조회, 입금, 출금, 송금 기능
    • 동시 송금 요청 처리
    • 트랜잭션 관리 및 잠금 메커니즘 적용
  • 구현 단계:

    1. 엔티티 및 리포지토리 설계
    2. 서비스 계층에 트랜잭션 및 잠금 로직 구현
    3. 컨트롤러 및 API 엔드포인트 생성
    4. 동시성 테스트 작성 및 수행
    5. 교착 상태 시나리오 구현 및 해결 방안 적용
  • 확장 가능성:

    • 웹 인터페이스 또는 RESTful API를 통해 외부에서 접근 가능하도록 구현
    • 실제 데이터베이스로 전환하여 성능 및 잠금 메커니즘을 실험

결론

데이터베이스의 Locking 메커니즘은 백엔드 개발에서 매우 중요한 개념입니다. 위에서 제시한 실습들을 통해 트랜잭션 관리, 동시성 제어, 교착 상태 해결 등 다양한 측면을 체험하고 이해할 수 있습니다. 이러한 실습 경험은 실제 업무에서 발생할 수 있는 문제를 효과적으로 해결하는 데 큰 도움이 될 것입니다. 또한, GitHub에 실습 코드를 올려 포트폴리오로 활용하면 취업 준비에도 유리할 것입니다.

실습 중 궁금한 점이나 추가적인 도움이 필요하면 언제든지 질문해 주세요!

0개의 댓글