문제 상황

프로젝트를 진행하던 중, 모임을 참여하는 기능에서 동시에 참여하는 경우 제한 인원수에 비해 더 많은 인원이 참가하게 되는 문제점이 발생하였습니다. Transactional을 걸어줬으니까 당연히 동시성 문제는 생기지 않았겠지.. 하는 안일함이 불러온 참사였습니다.

실제로 여러명의 크루들과 함께 테스트를 해 보니, 여러명의 인원이 동시에 모임을 참여하는 경우 동시성 문제가 생기는 것을 알 수 있었습니다. 왜 이런 문제가 생기게 된것일까요? 모임을 참가하는 로직을 가져와 보겠습니다.

 

 

대충 봤을땐 문제가 없어보이지만, 사실 네모친 칸이 문제입니다. 두 사용자가 동시에 참여를 한다고 가정했을 때, 최대 인원수가 5명이고, 현재 인원수가 4명인 경우 두 사용자 시점에서는 모두 4명으로 처리되어 둘 다 참여가 되기 때문입니다.

그렇다면, 이러한 문제는 어떻게 해결할 수 있을까요? 자바에서는 아주 적절한 기능을 제공합니다. 바로 synchronized 키워드입니다. 한번에 한 사용자만이 참여를 하고, 나머지 사용자들은 대기하도록 메소드에 Synchronized 키워드를 붙여주면 됩니다.

 

 

이제 해결됬으니 끝났을까요? 아닙니다. 저희 서비스는 아쉽게도(?) 로드밸런싱을 도입하게 되었습니다. 하나의 서버에서는 한명만이 보장되겠지만, 여러대의 서버에서 한명이 보장되지는 않았습니다. 따라서 저희는 Synchronized보다 락의 필요성을 느끼게 되었습니다. 그렇다면 락이란 무엇일까요?

 

데이터베이스 락

락이란 데이터베이스의 일관성을 보장하기 위한 방법입니다. 락의 종류에는 여러 락이 있는데, 이번에는 낙관적 락비관적 락에 대해서 학습해 보겠습니다.

 

낙관적 락이란?

  • 트랜잭션 대부분은 충돌이 발생하지 않는다고 낙관적으로 가정하는 방법입니다.
  • 애플리케이션 단에서 Version 관리 기능을 사용합니다.
  • 버전 정보를 비교하여 가져온 버전 정보와 변경할 때의 버전 정보가 다른 경우 예외가 발생합니다.

예시 쿼리는 다음과 같습니다.

 

update board
set title=?,version = 버전 + 1
where id = ? and version = 버전;

변경할 때의 버전이 초기에 읽어들인 버전이 아니라면 update 쿼리의 실행 결과 행의 수가 0개일 것이므로, 예외가 발생하는 방식입니다. 그렇다면 실제로 어떻게 쿼리가 날라가는지 한번 테스트해 보겠습니다. 

 

우선 도메인은 다음과 같습니다. 버전 관리를 위한 필드를 하나 추가해 주어야 합니다.

public class Raccoon {

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Version
    private Long version;

    private String name;
}

 

이후 Repository에서 낙관적 락을 걸어줍니다. Version 필드가 존재하는 경우, OPTIMISTIC을 생략한다면 데이터가 변경되었을 때만 Version 필드가 업데이트 됩니다.

public interface RaccoonRepository extends JpaRepository<Raccoon, Long> {

    @Lock(LockModeType.OPTIMISTIC)
    Optional<Raccoon> findById(Long id);
}
@Service
@AllArgsConstructor
@Transactional
public class RaccoonService {

    private final RaccoonRepository repository;

    public Raccoon save() {
        final Raccoon raccoon = repository.save(new Raccoon(null, 1L, "cute"));
        
        return raccoon;
    }

    public Raccoon findById(Long id) {
        final Raccoon raccoon = repository.findById(id).get();

        return raccoon;
    }
}

 

위 코드에 대한 테스트코드를 작성해 보았습니다.

    @DisplayName("낙관적 락으로 인해 어떠한 쿼리가 나가는지 테스트한다.")
    @Test
    void optimisticTest() {
        final Raccoon raccoon = raccoonService.save();
        entityManager.flush();
        entityManager.clear();
        
        final Raccoon foundRaccoon = raccoonService.findById(raccoon.getId());
        foundRaccoon.setName("kim");
        entityManager.flush();
        entityManager.clear();
    }

 

날라가는 쿼리는 다음과 같습니다.

데이터가 변경됨과 동시에 Version이 1에서 하나 올라간 2로 저장됨을 볼 수 있습니다. 이 때 만일 update 된 row가 하나도 없는 경우에는 예외가 발생합니다.

 

장/단점

  1. 아무래도 어플리케이션 단에서 락을 걸기 때문에 비관적 락에 비해 성능이 더 좋습니다.
    • 엔티티를 수정하지 않고 조회만 해오는 경우에는 LockModeType의 OPTIMISTIC 옵션을 걸지 않으면 버전을 체크하지 않습니다.
  2. 어플리케이션 단에서 락을 걸기 때문에 예외가 발생하는 경우 개발자가 직접 후처리를 해주어야 합니다.
  3. 도메인에 버전 관리를 위한 필드가 추가됩니다.
  4. 엔티티를 수정하는 경우, 매번 update 쿼리가 나가는 문제점이 있습니다.

 

비관적 락이란?

  • 트랜잭션의 충돌이 발생한다고 가정하고 락을 걸고 보는 방법입니다.
  • select for update 구문을 사용합니다. 
  • 데이터베이스 트랜잭션 락 메커니즘에 의존하는 방법입니다.

그렇다면 한번 비관적 락도 어떻게 쿼리가 날라가는지 테스트해 보겠습니다. Version 필드는 제거해도 되지만, 혼동될 수도 있으므로 제거하지 않고 Repository에서의 Lock 수준만 변경하겠습니다.

public interface RaccoonRepository extends JpaRepository<Raccoon, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    Optional<Raccoon> findById(Long id);
}

 

그렇다면, 테스트 결과는 어떠한 쿼리가 날라가게 될까요?

 

원하던 대로 select for update 구문으로 조회 쿼리가 나가는 모습을 볼 수 있습니다. select for update 쿼리로 조회한 데이터는 트랜잭션이 끝나기 전까지 다른 트랜잭션에서 수정하거나 select for update 쿼리 또는 for share로 조회할 수 없습니다.

 

장/단점

  • 데이터를 수정하는 즉시 트랜잭션 충돌을 감지할 수 있습니다.
  • 데이터베이스 단에서 락을 걸기 때문에 동시성 문제를 해결하기 위한 Version 필드가 따로 필요하지 않으며, 개발자는 로직 자체에 더 집중할 수 있습니다.
  • 데이터베이스 단에서 락을 걸기 때문에 성능이 비교적 좋지 않습니다.

 

프로젝트에서는?

프로젝트에서는 비관적 락을 적용했는데, 이유는 동시에 접근하는 충돌이 많이 일어날 것이라고 예상했기 때문입니다. 만일 낙관적 락을 사용한다면 예외처리를 하는데 많은 시간이 들 것 같았습니다. 따라서 비관적 락을 사용하여 동시성 문제를 해결하였습니다.

 

참고

  • 자바 ORM 표준 JPA 프로그래밍

'프레임워크 > Spring' 카테고리의 다른 글

QueryDSL 핥아만 보기  (0) 2022.11.21
Jpa의 @Query 탐험기  (0) 2022.11.19
N+1 문제와 해결 방법  (0) 2022.11.06
스프링 이벤트를 통한 양방향 의존성 풀기  (0) 2022.11.04
Where절의 Subquery 성능 문제  (0) 2022.10.30

+ Recent posts