프로젝트를 진행하는 도중에 동시성 문제가 발생하였다. 특정 방에 동시에 인원이 여러명 접속 시도할 경우 들어갈 수 있는 인원의 최대치를 넘어가는 문제가 있었다.
처음에 시도한 방법은 아래와 같이 인원 수를 제한하는 로직을 짜고 해당 로직을 synchronized 블럭으로 묶는 것이었다.

하지만 이 방법으로는 해결되지 않았다. 아니, 해결될 수가 없었다.
우선 synchronized 블럭을 사용하는 데에는 제약이 있었는데,
JVM 단위로 동작
쉽게 말하면, synchronized는 같은 서버 안에서만 동작하기 때문에 서버가 2대 이상만 되어도 락이 아무 의미가 없어지는 것이다. 하지만 서버를 하나만 사용하고 있기 때문에 해당 이유는 아니었다.
@Transactional과의 충돌
이게 원인이었는데, 위의 synchronized 블록은@Transactional이 붙은 함수 내부 코드이다. 그런데 프록시 기반으로 동작하는 @Transactional과 함께 사용하려고 하니, synchronized 블럭이 끝나도 트랜잭션은 커밋되지 않은 상태인 것이었다.
다시 말해, 동기화를 아무리 시도한들 커밋되기 전에 다른 스레드가 synchronized 블럭에 진입하면 DB에서 읽어온 값이 아직 커밋하기 전이라 두 스레드가 동일한 값을 읽게 되는 것이다.
이러한 문제를 해결하기 위해선 DB 단에서 동기화를 시도해야 할 것 같았다.
DB 단에서 가능한 락 종류는 크게 두 가지로 나뉘는데
낙관적 락 (Optimistic Lock)
말 그대로 낙관적으로 보고 충돌이 없을 거라고 가정한 것이다. 따라서 락을 거는 방식이 아니라, 특정 버전을 기록해놓고 커밋할 때 내가 읽었던 버전이랑 현재 버전이 다르면 충돌로 판단하고 예외를 던지는 방식이다.
비관적 락 (Pessimistic Lock)
낙관적 락과는 다르게 충돌이 날 거라고 미리 가정을 하고 미리 DB에 락을 걸어버리는 것이다. 쓰기/읽기 락을 걸 수 있는데,
PESSIMISTIC_READ → 내가 읽는 동안 다른 트랜잭션이 수정 못함
PESSIMISTIC_WRITE → 내가 읽는 동안 다른 트랜잭션이 읽기/수정 모두 못함
낙관적 락의 경우 락을 실제로 잡지 않아서 성능은 좋지만 충돌 시 재시도 로직을 직접 짜야하는 단점이 있고, 비관적 락의 경우 락을 실제로 잡기 때문에 정합성은 확실하지만 대기가 생겨서 성능이 낮아지는 단점이 존재한다.
내가 구현 중인 코드에서는 동시 접근이 잦고 정합성이 중요한 경우였기 때문에 비관적 락이 적합했고 코드에 적용하여 문제를 해결하였다.