모모 서비스의 가장 메인 기능은 모임을 생성하고 참여하는 것이다. 모임을 생성하는 것은 없던 정보를 생성하는 것이므로 아무 문제가 없지만 참여 기능은 자칫하면 동시성 문제가 발생할 가능성이 있다.

동시성이란 여러 개의 프로세스 또는 스레드가 매우 짧은 주기로 번갈아가며 로직을 수행하여 동시에 동작하는 것처럼 보이게 하는 방식이다. 하지만 실제로는 동시에 동작하는 것이 아닌 잦은 스위칭 방식으로 짧게 로직을 수행할 뿐이다. 이러한 특징 때문에 객체 내에서 공유하는 자원에 대해 발생하는 동시성 문제가 대표적이다.

공유하는 자원뿐만 아니라 모임의 참여하기 기능에서도 동시성 문제가 발생할 수 있다. 예를 들어, 두 명 이상의 사용자가 동일한 모임에 대해 참여하기를 요청했다 가정하자. 우연히도 이 모임은 한 자리만 남았을 경우 거의 동시에 요청을 했다면 모임에 한 명이 아닌 여러 명이 참여가 될 수 있을 가능성이 있다. 모임 참여하기 로직은 참여가 가능한지 알기 위해 모임의 참여자 목록 정보를 조회한다. 정말 운이 나쁘게도 거의 동시에 두 개 이상의 요청이 모임의 참여자 목록을 조회했다면 둘 다 한자리가 남은 정보를 확인할 것이다. 이런 경우 동시성 문제가 발생할 가능성이 있다. (수작업으로 확인해본 결과 실제로 문제가 발생하였다….)

synchronized

모임 참여하기에 대한 동시성 문제를 해결하기 위해 처음에는 참여 메서드에 synchronized 키워드를 추가하였다. synchronized 는 어느 한 요청이 해당 메서드에 접근하여 요청을 처리중이면 다른 요청이 락에 걸려 대기하는 역할을 한다. 성능은 떨어질 지라도 동시성 문제는 해결할 수 있다. 하지만 이 방식은 성능도 떨어질 뿐만 아니라 서비스 확장으로 서버를 여러 대를 둔다면 여전히 동시성 문제가 발생한다. synchronized 가 유효한 것은 단일 서버 내에서 하나의 프로젝트로 구동하고 있을 경우 뿐이다.

로드 밸런싱을 도입한다면 synchronized 도 동시성 문제가 발생한다. 이는 동시성 문제를 애플리케이션 계층에서 해결하려고 했기 때문이었다. 결국에는 DB 에 저장된 정보를 기반으로 조회하여 조건을 판단했기 때문에, 동시성 문제 해결을 DB 계층으로 내렸다. 실제 데이터를 보관하는 DB 계층에서 특정 데이터에 락을 걸어 데이터를 보호한다면 로드 밸런싱 환경에서도 동시성 문제를 해결할 수 있으며, 애플리케이션 계층에서 락을 거는 것보다 성능도 크게 나빠지지 않는다.

낙관적 락(Optimistic Lock)

DB 계층에서 락을 거는 방법은 대표적으로 낙관적 락과 비관적 락이 있다. 낙관적 락(Optimistic Lock) 은 사실 DB 에 락을 거는 방식은 아니다. 동시성 문제를 해결하고 싶은 테이블에 version 을 관리하는 컬럼을 추가하여 조회 시점과 수정 시점의 version 으로 이전에 수정된 이력이 있는지 확인한다. 예를 들어, 두 개의 요청이 각각 동일한 데이터를 조회하면 두 데이터가 가지는 version 은 동일하다. 둘 중 하나의 데이터가 먼저 조회한 데이터를 수정한다면 version 이 변경되고 이후에 변경하려는 요청에서는 version 변경을 감지하여 동시성 문제를 해결한다. JPA 등의 도구를 이용하면 version 관리를 자동으로 할 수는 있으나 낙관적 락의 치명적인 단점은 롤백이다. 결국은 애플리케이션 레벨에서 해결했기 때문에 충돌이 발생하여 롤백을 해야한다면 개발자가 직접 데이터를 수정해주어야 한다.

그래도 낙관적 락은 실제로 락을 거는 방식이 아니기 때문에 비관적 락에 비해 성능이 좋다는 장점이 있다. 또한 낙관적 락이 가능한 경우도 있다. 일반적으로 API 를 설계할 때는 원자성이 보장되어야 하는 단위로 나누어 트랜잭션의 이점을 최대한 활용한다. 하지만 경우에 따라서 하나의 기능을 수행하는데 두 개 이상의 API 로 분리해야 하는 경우도 발생할 수 있다. 예를 들어, 게시글을 수정하는 권한이 모두에게 있다고 가정하자. 이럴 경우 일단 게시글을 조회하는 API 를 호출할 것이다. 두 명 이상의 사용자가 조회한 데이터를 변경한 뒤 수정 API 를 요청할 경우 이는 트랜잭션 범위 내 데이터를 관리할 수 없어 낙관적 락을 사용하는 것이 좋다.

비관적 락**(Pessimistic lock)**

비관적 락(Pessimistic lock) ****은 일반적으로 예상할 수 있는 락이다. 실제로 DB 계층에서 락을 걸어 데이터를 보호한다. 낙관적 락보다는 성능이 떨어질 수 있으나 동시성 문제에 대해 완벽하게 보호받을 수 있고 트랜잭션도 이용할 수 있다. 따라서 참여 기능에 대해서 비관적 락으로 구현하였다.

GroupSearchRepository.java

public interface GroupSearchRepository extends Repository<Group, Long>, GroupSearchRepositoryCustom {

    Optional<Group> findById(Long id);

    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query("select g from Group g where g.id = :id")
    Optional<Group> findByIdForUpdate(@Param("id") Long id);

		...
}

GroupSearchRepository 는 모임에 대한 정보를 조회하는 메서드들만 모아놓은 레포지토리이다. @Lock 어노테이션을 이용하여 원하는 메서드에 비관적 락을 설정하였다.

findById 와 findByIdForUpdate 둘 다 동일한 쿼리문을 실행하지만 findById 는 수정없이 단순 조회만을 하는 로직에 사용도록 분리하여 조금이나마 성능을 높였다.