튼튼발자 개발 성장기🏋️

part1. 동시성 이슈의 해결 방법 (2+1)가지 본문

Framework/spring

part1. 동시성 이슈의 해결 방법 (2+1)가지

시뻘건 튼튼발자 2023. 9. 5. 18:53
반응형



최근 토이 프로젝트를 진행하면서 맞이한 동시성 이슈.

그 이슈는 "게시글의 조회수 증가"에서 맞이하게 된다. 서로 다른 사용자가 동시에 게시글을 열람한다면, 조회수는 +2가 되어야할테니...

따라서 동시성 이슈를 해결하기 위해 락을 획득하기로했다.

일반적으로 알고 있는 락은 낙관적 락(Optimistic Lock)과 비관적 락(Pessimistic Lock)이있다.


낙관적 락(Optimistic Lock)

동시에 동일한 데이터에 대한 수정을 서로 연관되지 않게 방지하는 기능으로써 해당 데이터의 "version"을 확인하여 엔티티의 변경을 감지한다.

"낙관적"이라는 말처럼 "여러 트랜잭션이 동일한 데이터를 동시에 수정하지 않는다."라는 가정을 가지고 트랜잭션의 충돌을 방지한다. 한 마디로 "일단 데이터 가공 하고 commit할 때 동시성 이슈가 있으면 그 때 처리하지 뭐~" 이거다.
JPA를 사용할 때에는 아래와 같이 @Version 어노테이션을 사용한다.

@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int boardId;
    @Version
    private long version;
    ...
}

낙관적 락 충돌이 발생했을 때에는 OptimisticLockException 예외가 발생되면서 트랜잭션이 rollback된다. 다시 말하면 JPA 내부적으로 리소스의 version을 체크하여 versioning을 하기 때문에 조회했을 때와 수정했을 때의 version이 다르면 OptimisticLockException이 발생되고 rollback된다는 이야기다.

 

LockModeType

deep하게 낙관적 락을 구현할 때는 아래 코드와 같이 LockModeType을 사용하여 추가적인 옵션을 설정할 수 있다.

@Lock(LockModeType.OPTIMISTIC)
Optional<Board> findByIdForUpdate(int boardId);
  • LockModeType.NONE
    • 리소스를 수정시 version을 체크하면서 UPDATE 쿼리를 사용하여 version을 증가시킨다.
    • 데이터베이스의 version값이 현재 version이 아니면 예외 발생.
  • LockModeType.OPTIMISTIC
    • 조회 시에도 락을 발생하도록 설정한다.
    • 트랜잭션을 커밋할 때 version을 조회해서 현재 리소스의 version과 같은지 체크한다.
    • version이 서로 다르면 예외 발생.
    • 트랜잭션 시작시 version 체크가 수행되고, 트랜잭션 종료시에도 version 체크가 수행.
    • 조회시에도 version을 체크하고 트랜잭션이 종료될 때까지 다른 트랜잭션에서 변경하지 않음을 보장한다.
    • Dirty Read 와 Non Repeatable Read 를 방지한다.
  • LockModeType.OPTIMISTIC_FORCE_INCREMENT
    • version 정보를 강제로 증가시킨다.
    • 관계를 가진 다른 리소스가 수정되면 version이 변경된다.

 

장점

  • 성능이 좋다. (읽을 때 락을 사용하지 않기 때문)
  • 동시성 이슈가 없을 경우 비관적 락보다 상대적으로 빠르게 동작한다.

단점

  • 여러 트랜잭션 중 특정 트랜잭션이 리소스를 변경할 경우 다른 트랜잭션들의 작업이 실패된다.

 


비관적 락(Pessimistic Lock)

하나의 트랜잭션이 조회시점에서 락을 걸고 조회 또는 업데이트 처리가 완료(커밋)될 때까지 락을 유지한다.

[그림 1] 데드락(DeadLock)

 

[그림 1]과 같은 상황을 생각해보자. 유저1가 리소스A 락을 획득하고 유저2가 리소스B 락을 획득한 상태에서 유저1이 리소스B를 조회하거나 유저2가 리소스A를 조회한다면 분명 데드락이 걸리게될 것이다. 데드락 발생을 방지하려면 락을 시도할때 최대 잠금시간을 설정해야한다.

@Transactional(timeout = 10)
public Board updateTitle(final String title){
	//...
}

 

LockModeType

  • 공유 락 (Shared Lock): LockModeType.PESSIMISTIC_READ
    • 다른 트랜잭션은 조회만 가능하다.
    • 다른 트랜잭션은 배타 락(Exclusive Lock)을 획득할 수 없다.
    • 조회한 데이터가 트랜잭션 내내 변경되지 않는다.(조회만 가능하니까.)
  • 배타 락 (Exclusive Lock): LockModeType.PESSIMISTIC_WRITE
    • 베타 락을 획득한 트랜잭션은 조회와 쓰기 모두 실행할 수 있다.
    • 다른 트랜잭션은 배타 락이 걸린 데이터에 대해 CRUD 모두 수행할 수 없다. (블로킹(Blocking) 상태)

장점

  • 트랜잭션의 동시접근을 조회시부터 방지해서 순차적인 처리가 가능하다.

단점

  • 한 트랜잭션 내용이 완료되기 전까지 다른 트랜잭션이 조회조차 할 수 없기때문에 동시성이 떨어진다.
  • 대기시간이 길어지는 만큼 성능도 떨어질 수 있다.
  • 데드락(DeadLock) 상황이 발생할 수 있다.
반응형

'Framework > spring' 카테고리의 다른 글

part2. 동시성 이슈의 해결 방법 (2+1)가지  (0) 2023.09.06
배치 처리 테스트하기  (0) 2022.09.22
클라우드 네이티브 배치  (0) 2022.09.21
#18 : Lombok  (0) 2020.07.22
#17 : H2 database 연동 준비  (0) 2020.06.13