트랜잭션에 대해서 이야기 하다보면은 항상 나오는 얘기가 있는 것 같습니다.
대량의 트래픽이 한 번에 들어왔을 때 어떻게 줄을 세울 것인가?
물론 아닐 수도 있지만, 면접때는 거의 항상 비슷한 내용을 물어봤던 것 같습니다
여러 방법이 있을 것 같습니다.
- DB 의 isolation level 을 조정한다던가 (하지만, 심각하게 느려지므로 비추인 것 같습니다.)
- JPA 의 낙/비관적 락을 사용한다던가
- 레디스를 통해 받는다던가 (클러스터를 사용하면 zookeeper 를 사용해야 한다던가 하는데, 자세히는 모르겠네요.)
알아본 혹은 알고 있는 방법으로는 3가지 정도가 있는 것 같습니다.
이번엔 JPA 의 락에 대해서 알아보려고 합니다.
1. 락 없이 동시 접근을 했을 때
동일한 데이터에 접근하여 수정하는 예로는 아래와 같은 상황이 나올 수 있는데요.
10,000 이 들어있는 계좌에 접근하는 두 개의 트랜잭션에 있습니다. 하나는 8,000 을 출금하고, 다른 하나는 5,000 을 입금합니다.
통상적으로 생각하면 10,000 + 5,000 - 8,000 으로 7,000 이 계좌에 남아있을 것이라고 생각할 수 있는데요. 8,000 을 출금한 트랜잭션은 잔고가 15,000 인 계좌를 조회한 것이 아니라 10,000 인 계좌를 조회한 것이 되므로 남은 잔고가 2,000 이 될 수도 있고, 비슷하게 출금을 먼저 했을 경우, 7,000 인 계좌가 15,000 이 될 수도 있습니다.
OS와 관련된 책에서 임계 영역(Critical Section)하면 나오는 예였던 것 같네요.
뭐하튼, 이외 같은 문제를 막기 위해선 가장 쉬운 방법은 역시 1번 DB 격리 레벨을 올리는 것입니다.
하지만, 느려진다는 성능상의 이슈가 있기 때문에 대규모 트래픽에서는 어울리지 않습니다.
그렇기 때문에 동시 접근을 서비스단에서 처리하기 위해 잠금 기법이 존재합니다. JPA 에서는 두 가지 잠금 기법을 제공하는데
- 먼저 데이터에 접근한 트랜잭션이 우선선위를 갖는 비관적 락(Pessimistic Lock)
- 먼저 데이터를 수정한 트랜잭션이 우선순위를 갖는 낙관적 락(Optimistic Lock)
이 있습니다.
이 두 가지 방법을 좀 더 자세히 살펴보면...
2. 비관적 락(Pessimistic Lock)
먼저 데이터에 접근한 트랜잭션이 우선순위를 갖는 잠금 방식으로 다른 말로는 선점 잠금
이라고도 합니다.
우선 이해를 돕기 위해 그림을 먼저 보면
왼쪽 트랜잭션이 먼저 잔고에 접근했다고 했을 때, 오른쪽 트랜잭션은 왼쪽 트랜잭션의 일이 끝날 때까지 대기해야 합니다. 그리고 왼쪽 트랜잭션이 끝나면 오른쪽 트랜잭션이 접근하여 선점할 수 있는 것이죠. 이렇게 서로 다른 두 트랜잭션이 동시에 동일 데이터에 접근하여 수정하는 것을 방지하여, 데이터의 일관성이 깨지는 것을 막아줍니다.
이론은 여기까지 하고, 그럼 이걸 JPA 환경에서 어떻게 사용하느냐 인데요.
- EntityManager.find() 를 사용할 경우
- annotation 을 사용할 경우
가 있습니다. 코드로 보면
// 1. EntityManager.find() 의 경우
EntityManager entityManager = Persistence.createEntityManagerFactory("lockTest").createEntityManager();
entityManager.find(Account.class, dipositNum, LockModeType.PESSIMISTIC_WRITE);
// annotation 사용
@Lock(LockModeType.PESSIMISTIC_WRITE)
Account find(int depositNum);
이렇게 사용합니다.
어노테이션으로 간단하게 할 수 있다보니, 책을 통해서 배울 때 말고는 엔티티 매니저를 직접 호출해서 사용한 적은 없네요.
하지만, 비관적 락을 사용할 때, 주의할 점이 있는데요. 바로 교착 상태 (deadlock)
에 빠질 수 있다는 것입니다.
교착 상태 (deadlock) 란?
두 개 이상의 작업이 서로 상대방의 작업이 끝나기 만을 기다리고 있기 때문에 결과적으로 아무것도 완료되지 못하는 상태를 가리킨다.
예를 들어 A,B 트랜잭션이 계좌 정보와 사용자 정보에 접근한다고 했을 때,
1. A : 계좌 정보에 접근하여 계좌 잠금
2. B : 사용자 정보에 접근하여 사용자 정보 잠금
3. A: 사용자 정보에 접근 시도
4. B : 계좌 정보에 접근 시도
A는 계좌를 잠금했고, 사용자 정보를 가져오기 위해 대기를 하고, B는 사용자 정보를 잠금했고, 계좌 정보를 가져오기 위해 대기를 한다. 서로 끝나지 않는 상황 발생
이런 경우 때문에 보통, DB 커넥션 수준에서 잠금 대기 시간을 설정 (혹은 커넥션 타임 설정?) 등을 통해 해당 상황을 대비하는 것 같습니다.
3. 낙관적 락(Optimistic Lock)
먼저 데이터를 수정한 트랜잭션이 우선순위를 갖는 것으로 비선점 잠금
이라고도 합니다.
이번에도 먼저 그림을 보면
우선 눈에 띄는게 버전이라는 것입니다.
낙관적 락을 사용하려면 버전 값을 저정할 컬럼이 필요하고, 조회할 때 버전 값을 같이 조회합니다. 그리고 업데이를 할 때 버전도 같이 업데이트를 합니다. 그렇게 하여 왼쪽 트랜잭션이 업데이트를 하려고 하면, 버전 값이 달라서(업데이트된 버전 : 2, 왼족이 조회한 계좌 잔고에 대한 버전 : 1) 업데이트에 실패하게 됩니다.
JPA 에서 낙관적 락을 사용하려면 다음과 같습니다.
- 버전 값을 저장할 컬럼을 DB Table 에 추가
- 해당 컬럼에 @Version 어노테이션 설정
create table ACCOUNT(
id varchar(20) not null primary key
balance int(20) default 0
...
ver int(10) default 0
)
@Entity
public class Account {
@Id
private String id;
@Column(name = "balance")
private int balance;
@Version
private int ver;
...
}
이렇게 하면 낙관적 락이 적용됩니다.
@Version 으로 설정할 수 있는 데이터 타입은 int, Integer, short, Short, long, Long, java.sql.Timestamp 이 있습니다.
4. 정리
JPA 에서 사용하는 락 방법인 비관적 락 과 낙관적 락에 대해서 알아봤습니다.
해당 내용을 실제로 접해본 적은 없어서 그런가보다 하는 감이 없잖아 있지만, 대규모 트래픽을 생각한다면 짚고 넘어가야 할 내용은 맞는 것 같습니다.
참고로 JPA 를 사용할 때 추천하는 전략은 낙관적 락 이라고 합니다. 교착 상태의 비용이 커서 그런건가 싶기도 하네요. (실제로 학부에서 OS를 배울 때 교착 상태에 대해 중요하게 배우기도 했고요.)
그라고 비관적 락을 사용할 때, LockModeType.PESSIMISTIC_WRITE
라고 적었는데, 락모드타입에도 몇 가지 종류가 있다고 합니다. (나중에 필요할 때 읽어보면 될 것 같습니다.)
'개발 관련 > spring' 카테고리의 다른 글
HandlerMethodArgumentResolver 사용 (0) | 2022.07.14 |
---|---|
JPA 어노테이션 (0) | 2022.07.14 |
Mockito 정리 (1) (0) | 2022.07.14 |
SpringBoot Actuator (0) | 2022.07.14 |
WireMock 사용 (0) | 2022.07.14 |