JPA에서 제공하는 락 - 1. 낙관적 락

Hyunta·2022년 11월 18일
0

Lock이란?

동시성 제어하기 위한 수단으로 Lock을 보통 사용한다. Thread 간의 경쟁 상태를 해결하기 위한 뮤텍스,세마포어 모니터 등의 방식도 있고, DBMS에서 동시성을 제어하기 위한 Lock도 존재한다. JPA에서는 동시 요청을 처리할 수 있는 방법으로 낙관적 락과 비관적 락을 제공하는데 탐구하면서 각각의 기능을 알아보자.

낙관적 락

  • 트랜잭션 대부분은 충돌이 발생하지 않는다고 가정하는 방법이다.
  • JPA가 애플리케이션에서 제공하는 락이다.
  • 트랜잭션을 커밋하기 전까지는 트랜잭션의 충돌을 알 수 없다.

비관적 락

  • 트랜잭션의 충돌이 발생한다고 가정하고, 우선 락을 거는 방법
  • DBMS가 제공하는 락 기능을 사용한다.

두번의 갱신 분실 문제

사용자 A와 B가 동시에 같은 자원을 수정을 했을 때 발생하는 문제
예를 들어 A는 글의 제목을 제목A 로 변경하고 B는 제목을 제목B로 변경한다 했을 때 동시에 작업이 일어나면, A의 수정사항은 사라지고 나중에 완료한 사용자 B의 수정사항만 남게 된다.

두번의 갱실 분실 문제는 트랜잭션의 범위를 넘어서기 때문에 트랜잭션 만으로는 해결할 수 없다.

가능한 3가지 선택지는
1. 마지막 커밋만 인정하기 : A의 내용을 무시하고, B의 내용만 반영
2. 최초 커밋만 인정하기 : A가 수정을 했으므로, B가 수정할 때 오류를 발생
3. 충돌하는 갱신 내용 병합하기 : A와 B의 수정사항을 병합한다.

기본값은 마지막 커밋만 인정하기가 사용된다. 만약 최초 커밋만 인정하기를 사용하려면 JPA가 제공하는 버전 관리 기능을 통해서 가능하다.

구현

배경

게시글 Board의 제목을 수정한다고 하자.

@Entity
@Getter
public class Board {

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

    @Column
    private String name;

    protected Board() {
    }

    public Board(String name) {
        this.name = name;
    }

    public void update(String name) {
        this.name = name;
    }
}

    @Test
    void test() throws InterruptedException {
        Thread thread1 = new Thread(() -> boardService.update(1L, "NameA"));
        Thread thread2 = new Thread(() -> boardService.update(1L, "NameB"));

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        
        Board updateBoard = boardRepository.findById(1L).orElseThrow();
        System.out.println("updateBoard.getName() = " + updateBoard.getName());
    }

---
updateBoard.getName() = NameB

Thread의 시차를 두지 않았기 때문에 그때마다 결과값이 다르지만, 마지막 커밋만 인정되어 반영되어있음을 알 수 있다.

낙관적 락 구현

마지막 커밋을 반영하는 것이 아니라 초기값이 반영되도록 구현하려면 락을 이용할 수 있다.

가장 먼저 낙관적 락을 사용하려면 JPA가 제공하는 @Version 애노테이션을 사용해서 버전 관리 기능을 추가해야 한다.

@Entity
public class Board {

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

    @Column
    private String name;

    @Version
    private Long version;

    protected Board() {
    }

    public Board(String name) {
        this.name = name;
    }

    public void update(String name) {
        this.name = name;
    }
}

아래와 같이 코드를 실행시키면 낙관적 락이 작동한다.

정상 수행됐던 로직에서 ObjectOptimisticLockingFailureException이 발생하는 것을 확인할 수 있다.

해당 예외를 자세히 보면 TransactionalInterceptor에서 예외를 처리하고 있는 것을 볼 수 있는데, try-catch 문을 통해 Service 내부에서 예외를 잡으려고 했는데 실패했다. @ControllerAdvice를 이용해서 예외를 잡을 수 있었다. 따라서 Transaction 프록시 객체가 예외를 반환해준다는 것을 알 수 있었다.

추가로 작성한 version이 값이 수정됨에 따라 0에서 1로 변경된 것을 같이 확인할 수 있었다.

version은 엔티티의 값이 변경되면 증가한다고 하는데 확인해보고 싶어서 테스트를 돌려봤다.

이름을 똑같이 변경시키면 version은 변하지 않는다.

for (int i = 0; i < 20; i++) {
    Board board = boardRepository.findById(1L).get();
	boardService.update(1L, "name");
    System.out.println("board.getVersion() = " + board.getVersion());
}

---

<최종결과>
board.getVersion() = 1

이름을 변경 시키면 version이 계속 증가한다.

for (int i = 0; i < 20; i++) {
    Board board = boardRepository.findById(1L).get();
    boardService.update(1L, "name" + i);
    System.out.println("board.getVersion() = " + board.getVersion());
}

---

<최종결과>
board.getVersion() = 20

낙관적 락에서 발생하는 예외는 다음과 같다.

  • org.springframework.orm.ObjectOptimisticLockingFailureException (스프링 예외 추상화)
  • org.hibernate.StaleStateException (하이버네이트 예외)

락 옵션을 따로 지정하지 않고 @Version 만 있어도 낙관적 락이 적용된다.

추가로 옵션을 통해서 낙관적 락을 관리할 수 있는데 알아보자

낙관적 락의 옵션

None

@Version만 적용하면 작동한다.

Optimistic

@Version 만 적용했을 때는 엔티티를 조회할 때 버전 체크를 하지 않는다. 하지만 Optimistic 옵션을 주면 조회시에 버전을 체크한다.

None은 수정 시점까지의 일관성을 보장한다면, Optimistic은 트랜잭션이 끝날때까지 일관성을 보장한다.

이 옵션을 주면 Dirty Read와 Non Repeatable Read를 방지한다.

public interface BoardRepository extends JpaRepository<Board, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    Optional<Board> findById(Long id);
}

버전이 증가할거라고 생각했는데, 버전은 증가하지 않고 체크만 됐다. 그도 그럴것이 Select 쿼리에 Update 쿼리가 발생해야하니 문제가 될 수도 있겠다고 생각한다.

OPTIMISTIC_FORCE_INCREMENT

해당 옵션을 줬더니 테스트가 작동하지 않았다. 추후 확인해봐야겠다.

정리

JPA가 애플리케이션 단에서 관리하는 락에 대해서 알아봤다. 다음에는 비관적 락을 어떻게 관리하는지 알아보자.

Reference

자바 ORM 표준 JPA 프로그래밍 - 김영한 지음

profile
세상을 아름답게!

0개의 댓글