동시성 문제와 Lock

김태훈·2024년 1월 17일
0

Spring

목록 보기
14/16

트랜잭션

데이터베이스의 상태를 변경시키기 위해 수행하는 작업 단위 또는 한꺼번에 수행되어야 할 일련의 연산들을 의미합니다.

하나의 트랜잭션은 여러 단계로 이루어져 있으며, 모든 단계가 완료되면 커밋(commit)되어 데이터베이스에 변경사항이 저장됩니다.
하지만 중간에 문제가 발생하여 트랜잭션이 완료되지 못할 경우, 롤백(Rollback)을 통해 트랜잭션의 모든 변화를 원상태로 복구 시킵니다.

트랜잭션은 데이터의 일관성을 보장하는 중요한 역할을 하며 JPA에서도 트랜잭션 개념을 사용하여 Entity를 관리하고 있습니다.

트랜잭션의 특징

  • 원자성(Atomicity) : 트랜잭션에 포함된 작업들이 DB에 모두 반영되거나, 전혀 반영되지 않아야 한다는 특성입니다.(All or Nothing)
  • 일관성(Consistency) : 트랜잭션 내의 모든 작업들이 모두 성공적으로 완료된 후에, 데이터베이스 상태가 일관성을 유지해야 한다는 뜻입니다.
  • 격리성(Isolation) : 각각의 트린잭션은 서로에게 영향을 주지 않아야 한다는 특성입니다. 트랜잭션 수행 시 다른 트랜잭션이 동시에 접근 하지 못하도록 하는 특성으로, 이를 보장하기 위해 락과 같은 잠금 메커니즘이 사용됩니다.
  • 지속성(Durability) : 트랜잭션이 성공적으로 완료되면, 그 결과는 영구적으로 반영되어야 한다는 특성입니다. 시스템이 문제를 일으키는 경우(ex: 시스템이 다운되는 경우)에도 완료된 트랜잭션의 결과는 손실되지 않아야 합니다.

일관성과 동시성

일관성이 보장될 경우에 여러 클라이언트에게 요청을 받는 DB의 특성상 응답의 지연이 발생하게 됩니다.
트랜잭션이 데이터에 접근하는 순간, 그 데이터는 다른 트랜잭션으로부터 잠기게되고, 그로 인해 다른 트랜잭션은 대기 상태에 놓이게 되어 동시에 수행하는 트랜잭션의 수가 줄고 시스템의 성능이 저하되게 됩니다.

동시성 제어

만약 하나의 게시글에 1000명이 동시에 좋아요를 누르게 된다면 어떻게 될까,
당연히 1000개의 좋아요가 있어야 합니다.
하지만 DB에 1000개의 데이터가 한 번에 조회하게 되면서 동시성에 문제가 발생할 수 있습니다.

간단한 코드 예시

	@Transactional
	public void addBoardLike(BoardDto boardDto, Long boardId) {

		Long userId = jwtUtil.getUserId();
		Board board = findBoard(boardId);

		if (!checkLike(userId, boardId))
			throw new IllegalArgumentException("이미 좋아요를 눌렀습니다.");
		board.addLike(boardDto);
	}
    
    
public void addLike(Board board) {
		this.likes += 1;
	}


jMeter로 1000개의 좋아요를 보낸다고 했을 때,


요청 보내기 전

요청 보낸 후

1000개의 데이터를 보냈지만 좋아요의 갯수가 1000개가 아닌 103개인 것을 확인할 수 있습니다.

이렇게 동시성 제어를 하지 않게되면 데이터의 일관성과 무결성을 보장할 수 없게 됩니다.

동시성 제어를 하지 않을 경우 발생하는 문제

  1. 분실된 갱신(Lost Update)
    여러 트랜잭션이 같은 데이터를 갱신하는 작업을 진행하면서 하나의 작업이 진행되지 않는 경우
  2. 모순성(Inconsistency)
    여러 트랜잭션이 같은 데이터를 동시에 갱신가게 되어 원하는 결과와 일치하지 않는 경우
  3. 연쇄복귀(Cascading Rollback)
    여러 트랜잭션이 같은 데이터를 갱신하는 작업을 진행하는 과정에서 하나의 트랜잭션이 실패하면 원자성에 의해 두 트랜잭션이 실패하면 원자성에 의해 두 트랜잭션 모두 복귀하는 경우
  4. 비완료 의존성(Uncommitted Dependency)
    한 개의 트랜잭션이 실패했을 때, 이 트랜잭션이 회복하기 전에 다른 트랜잭션이 실패한 수행 결과를 참조하는 경우

Lock

이러한 문제를 Lock을 사용해서 해결할 수 있습니다.
Lock은 공유 자원에 대한 동시 액세스를 제어하는 기법입니다.

Lock의 종류

  1. 공유 락(Shared lock, S-lock)
  • 여러 프로세스가 동일한 자원을 읽을 때 사용됩니다.
  • 한 프로세스가 공유락을 획득하면, 다른 프로세스도 동일한 자원에 대해 공유락을 획득할 수 있습니다.
  • 다중 읽기 연산이 동시에 일어날 수 있기 때문에 동시성을 향상시킬 수 있습니다.
  • 공유락을 가진 프로세스는 데이터를 읽을 수만 있고, 쓰기 연산은 할 수 없습니다.

2) 배타 락(Exclusive Lock, X-lock)

  • 한 프로세스가 자원을 쓸 때 사용됩니다.
  • 배타락을 가진 프로세스는 자원에 대한 독점적인 접근 권한을 가지며, 다른 프로세스는 해당 자원에 대한 공유락이나 배타락을 얻을 수 없습니다.
  • 다중 쓰기 연산이 발생할 때 데이터의 일관성을 보장할 수 있습니다.
  • 배타락을 가진 프로세스는 읽기와 쓰기 모두 가능하며, 다른 프로세스는 해당 자원에 대해 어떠한 락도 획득할 수 없습니다.
  • 다른 프로세스가 공유락이나 배타락을 획득하려고 할 때는 대기합니다.

Lock의 동시성 제어 기법

1. 낙관적 락(optimistic lock)

  • 충돌이 발생할 가능성이 낮다고 가정하고 사용하는 동시성 제어 기법입니다.
  • 충돌이 발생하면 재시도 혹은 병합을 통해 해결합니다.
  • lock을 사용하지는 않고 @Version을 이용하여 업데이트 시 내가 읽은 version이 맞는지 확인하여 충돌 여부를 판단합니다.
  • 즉, 자원에 직접 lock 걸어 선점하지 않고, 실제로 동시성 문제가 발생하면 처리하는 방식입니다.

예시)

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private double price;

    @Version
    private Long version; // 버전 번호

    // Getters and setters
}

2. 비관적 락(pessimistic lock)

  • 충돌이 발생할 가능성이 높은 경우 사용되는 동시성 제어 기법입니다.
  • 데이터를 읽거나 수정하기 전에 lock을 획득하여 다른 사용자의 액세스를 차단하고, lock을 가진 스레드만 접근하도록 제어합니다.
  • 데이터에 대한 배타적인 액세스 권한을 보장하여 충돌을 방지합니다.
  • 실제로 데이터에 lock을 걸어서 정합성을 맞추는 방법으로, 자원 요청에 따른 동시성 문제가 발생할 것이라고 예상하고 lock을 걸어버리는 방법입니다.
  • 즉, 트랜젝션이 시작할 때 s-lock이나 x-lock을 실제로 걸고 시작합니다.

0개의 댓글