Situation: 같은 신청서가 여러개!
어느 날, 문의 전화가 왔습니다. ‘네, 정보체계관리단 권혁태 상병입니다’
‘저 분명히 신청을 한 번만 했는데, 신청서가 여러 개 들어가있어서요… 어떻게 된 건지 좀 알 수 있을까요?’
확인해보니 신청서 테이블에 한 사람이 한 날짜에 신청한 신청서가 여러 개였습니다. 해당 상황 자체는 DB에서 신청서를 지우는 방식으로 빠르게 해소가 되었으나, 중복으로 들어간 이유를 찾아야했습니다.
중복으로 들어간 이유는 웹 응답이 느려서, 여러 번 버튼을 클릭해서 같은 API가 여러 번 호출되었기 때문입니다.
Task: 동시성 이슈를 극복하자
첫 프로젝트이다보니 다양한 이슈 상황에 대한 이해가 적었습니다. 스프링 자체 로직은 잘 신경쓰지 못했습니다.
몇 가지 자료를 조사했고 다음과 같은 해결 방법이 있다는 것을 알게 되었습니다.
1.
Unique Key 설정
2.
Lock 획득 w/ DB Isolation Level
해당 자료들을 조사해본 후 적절한 방법을 찾아 적용합니다.
Action
•
Unique Key 설정
◦
DB에서 UK, PK 개념이 있습니다. 보통 PK는 해당 테이블의 id로 auto-increment를 해서 많이 지정하고, UK는 컬럼을 각자 지정합니다. PK는 null을 가질 수 없고, UK는 null을 가질 수 있습니다.
◦
DB에서 복합키라는 개념이 있습니다. 한 개의 컬럼이 아닌 여러 개의 컬럼을 가지고 키를 설정하는 것입니다.
◦
우리 신청서 테이블에는 이 복합키를 활용한 UK가 설정되지 않았습니다.
◦
UK로서 (군벌, 군번, 날짜)를 지정해주었습니다.
•
Lock 획득
◦
DB에는 비관적 락과 낙관적 락이 있습니다.
◦
비관적 락은 row 또는 테이블 단위에서 리소스에 하나의 프로세스만 접근하도록 막는 것입니다.
◦
낙관적 락은 실제로 락을 사용하는 것이 아닌 application level에서 특정 리소스에 대한 충돌이 일어날 것 같으면 롤백하는 방식입니다. timestamp나 version 등을 읽은 후, 다시 insert나 update시 조건에 맞지 않으면 rollback하는 방식입니다.
◦
해당 방법을 적용하기 위해서는 모든 사람에 대해서 미리 신청서가 전부 작성되어있어야합니다. 신청서가 작성되지 않은 상태에서 특정 신청 row에 대한 락을 잡을 수 없기 때문입니다. 극단적으로 신청서 테이블 전체에 락을 잡을 수도 있겠지만, 이는 서버 응답이 느려질 수 있기에 시도하지 않았습니다.
◦
우선은 Unique Key 설정을 통해 이슈를 해결하였기에 Lock을 적용하지는 않았습니다만, 다음과 같은 상황에서 충분히 발생할 수 있습니다.
▪
신청서는 언제든 신청되고, 수정되고 승인될 수 있습니다. 이 때 승인권자와 신청자가 다르기 때문에 이 상황에서 동시성 문제가 발생할 수 있습니다.
▪
예를 들어, 승인을 하려는 시점에서 바라본 정보가 A라고 할 때, 승인과 신청서 수정이 동시에 일어나게 되면 승인 후에 바라보는 정보는 A’ 일 수 있습니다.
▪
이런 경우, 수정과 승인 각각을 하나의 트랜잭션으로 묶어주고 각각에 대한 순서를 보장해야합니다.
▪
이 때 비관적 락 중 pessimistic_write를 사용한다면 각 트랜잭션의 읽기/쓰기 작업 순서가 무조건 보장되므로 문제가 없지만 pessimistic_read를 사용한다면 읽기는 가능하기 때문에 DB isloation level을 수정해야합니다.
▪
예를 들어 승인 이후에는 수정을 하지 못한다는 가정이 있다고 할 때, pessimistic_read를 사용하는 경우 다음과 같은 순서로 전개될 수 있습니다.
•
승인자 트랜잭션 시작 → 수정자 트랜잭션 시작 → 승인자 승인 → 신청자 수정 시도 → 승인자 트랜잭션 종료 → 신청자 트랜잭션 종료
•
어떻게 보면 괜찮을 것 같지만 괜찮지 않습니다. 원래 승인자가 승인한 이후에는 신청자가 수정을 시도했을 때, 실패해야하지만 이 때는 성공합니다. 왜냐하면 신청자의 트랜잭션 시작이 승인자의 실질적인 승인보다 빠르기 때문입니다. 그렇기에 status 값이 ‘승인 전’으로 읽힐 것이고 application level에서는 수정 가능하게 됩니다.
▪
이를 수정하기 위해서는 DB isolation level을 아예 READ UNCOMMITED 로 바꿔줘야하는데, 이는 너무 위험할 것 같다.
Result
•
일단 해당 건은 Insert에 대해서만 처리해주면 되기에 Unique Key로 간단하게 해결이 가능하였다.
◦
하지만 Unique Key는 복합키인 경우에 포함되는 컬럼이 nullable이면 동작하지 않을 가능성이 있다.
◦
Unique key는 개별 컬럼 값들이 nullable인 경우, null인 컬럼이 중복으로 들어갈 수 있다. 어떻게 보면 당연하지만, 이를 잘 고려하여 사용하여야한다.
◦
현재 신청서는 시간이 무조건 포함되기 때문에 이 방법을 적용했으나, 복잡한 로직에서는 조심해서 사용하는 것이 좋을 듯하다.
•
DB Unique Key를 설정하여 동시성을 제어하는 법, DB Lock의 종류(낙관적 잠금, 비관적 잠근, S-Lock, X-Lock 등), DB Isolation Level(Repeatable Read, Read Uncommited, Serializable 등)에 대해 익혀볼 수 있었음