///
Search
🧻

면회신청관리체계 같은 신청서가 여러 개..?!

태그
Project
면회신청체계개발

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 등)에 대해 익혀볼 수 있었음