배경
모놀리식으로 개발을 해왔었고, 혼자 테스트를 할 때도 postman을 통해 간단히만 했었습니다. 하지만 앞으로 다닐 회사에서는 서비스에 크든 여러 요청이 들어올 것이고 수정, 삭제가 빈번하게 일어난다면 대체 어떻게 client에게 신뢰성있고 일된 data를 넘겨주게 될까? 의문이 들었습니다. 예를 들어서 DB에 특정 data를 수정하는 API가 들어가서 수정하는 도중에 있는데 또다시 같은 data를 수정하는 API가 들어오게 된다면? 수정하는 도중에 조회 API가 들어오게 된다면? 등등에 관해 의문점이 들어서 이러한 것들은 개발자들이 어떻게 해결할까 구글링을 해보았습니다. 이러한 문제의 보편적인 해결방법이 Lock이라는 사실을 알게 되었습니다.
내용
Lock이란?
앞서 말씀드렸듯이, 특정 resource에 대해 여러 요청이 한번에 들어올 경우 Lock을 사용합니다. 바로 아래와 같이 말이죠.
Connection A의 요청이 끝난 후에 나머지 Connection들이 순서대로 들어가 다시 Lock을 걸고 작업을 하게 되는 형식입니다.
Lock과 Transaction 격리수준
처음에 Lock을 알게 되면 Transaction의 격리수준과 헷갈리는 경우가 있습니다. Trasaction 격리수준은 여러 transaction을 어떻게 처리할지에 대한 것이고, Lock은 데이터 일관성을 지키기 위한 메커니즘으로 Transaction 격리수준을 구현하는 방법 중의 하나가 Lock입니다.
Lock의 전략
Lock의 전략으로는 크게 낙관적 lock(optimistic lock)과 비관적 lock(pessimistic lock)이 있습니다. 낙관적 lock을 간단하게 설명하면 트랜잭션 충돌이 일어나지 않는다는 것을 가정하고, 충돌이 일어나면 조치를 취하는 방식입니다. 반대로 비관적 lock은 트랜잭션이 충돌이 일어날 것이라는 것을 가정하고 미리 조치를 취해놓는 방식입니다. 각각 애플리케이션 lock, 데이터베이스 트랜잭션 lock이라고도 합니다.
예를 들어 보겠습니다. 5개의 쿠폰이 있고 20명의 사람들에게 나눠주어야 합니다. 이를 lock없이 동시성 테스트를 통해 나눠주게 된다면 어떻게 될까요? thread pool을 만들어 테스트 해보면 다음과 같이 나오게 됩니다.
이제 앞서 말씀드린 낙관적 lock을 사용해서 lock을 걸어보겠습니다. 낙관적 lock이 애플리케이션 lock이라고도 한다고 했었죠? 이는 java에서처럼 애플리케이션 레벨에서 version 정보를 주어서 특정 data에 접근 시 version이 맞는지 확인 후 수정이 일어나게 됩니다. 이러면 이제 정확하게 5명에게 쿠폰이 발급이 될까요?
아래의 결과를 보시게 되면 결과가 이상하다는 것이 보일 겁니다. 왜 그럴까요? 이는 JPA에서의 낙관적 lock은 최초의 요청만 commit을 하기 때문입니다. 다른 요청들은 version이 맞지 않게 되는 것이죠. 아래 그림을 보시게 되면 이해에 도움이 될 겁니다.
다음으로 JPA에서의 비관적 lock에 대해 알아보겠습니다. @Lock을 사용해 간단하게 구현할 수 있는데요,
정확히 5개만 쿠폰이 발급되는 것을 볼 수 있습니다. 이는 하나의 요청이 data에 접근한 경우 lock을 걸고, 나머지 요청들은 '대기'를 하게 됩니다. 이렇게 되면 하나의 요청에 대한 트랜잭션이 완전히 끝난 후에 lock을 풀고 다음 요청에 대한 트랜잭션이 시작되기 때문에 정확한 쿠폰수를 발급해 줄 수 있게 됩니다. 아래의 그림을 참고하시면 됩니다.
그럼 무조건 비관적 lock만 쓰면 되지 않을까? 라고 생각하실 수 있지만, 비관적 lock은 다른 트랜잭션들이 수정이 끝날 때까지 대기를 해야 되고, 혹시나 이 트랜잭션에서 문제가 발생했을 경우 무한 대기에 빠질 수가 있게 됩니다. 서비스의 상황을 보고 선택을 해야겠죠?
비관적 lock에 대해 조금 더 자세히 알아보겠습니다. 비관적 lock의 연산 종류에는 공용 lock과 베타 lock이 있습니다.
먼저 공용lock의 경우 read 연산은 실행 가능하고 write연산은 실행 불가능하게 하는 경우입니다. 이는 데이터에 대한 사용권을 여러 트랜잭션이 함께 가질 수도 있습니다.(read의 경우)
베타 lock의 경우 read와 write 연산 모두 가능하지만 write 연산이 걸려있는 경우 다른 트랜잭션은 대기상태가 됩니다. 조금 헷갈리실 수도 있는데, lock을 하나만 쓰는 것이 아니라 여러 종류의 lock이 혼합되어져서 사용된다고 생각해야합니다.
< 로킹 단위 >
정보처리기사 공부를 할 때 locking 단위에 대해 다들 들어보셨을 겁니다. 로킹 단위를 작게 가져가면 구현이 복잡하지만 다른 트랜잭션들과 병행성이 높아지고, 로킹 단위를 크게 하면 구현은 단순하지만 병행성 수준은 떨어지게 됩니다.
그럼 어떻게 해야 할까요? 무조건 크게? 무조건 작게? 이는 프로젝트 상황에 맞춰 다가올 문제점들을 생각해보고 설계를 해야합니다.
첫번째 문제점으로 될 수 있는것은 Blocking 문제입니다. lock들의 경합이 발생해서 특정 세션이 작업을 진행 못하고 멈춰 선 상태가 된 경우를 말하는데요, 이는 한 트랜잭션이 베타 lock을 걸어두고 이를 풀려면 commit과 rollback을 쳐야하는데 문제가 생겨서 못 친 경우, 다른 트랜잭션들은 무한 대기를 하게 됩니다.
이 해결방안으로는 트랜잭션을 짧게 정의하고, 되도록 같은 데이터를 수정하는 트랜잭션이 동시에 수행되지 않도록 하는것이 좋습니다. 또한 Lock Timeout을 이용해 lock의 잠금해제 시간을 조절할 수도 있죠.
또다른 문제점으로는 DeadLock이 있습니다.
위 그림처럼 트랜잭션이 특정 리소스들에 접근해서 처리가 되어야하는데 서로 lock이 걸려버려서 무한 대기상태에 빠지는 경우입니다.
해결방안으로는 트랜잭션 진행방향을 같은 방향으로 한다거나, 처리속도를 늦춘다거나, Blocking과 마찬가지로 Lock Timeout을 설정하는 방법이 있습니다.
DB의 종류마다 lock을 적용시킨 것도 다를 수 있으니 잘 알아보고 어떤 lock을 걸어놓을지 생각해봐야겠습니다.
다음 글에는 트랜잭션 격리 수준에 대해 자세히 알아보겠습니다.
'개발 > DB' 카테고리의 다른 글
트랜잭션 격리수준(Isolation Level) (MySQL) (0) | 2024.11.30 |
---|---|
DB 인덱싱 (2) | 2024.11.28 |