분산락에 대한 코드 정당성 검증

구범모·2023년 12월 11일
0

서론

이전 글에서 분산락에 대한 기준을 어느정도 세웠다.

이번에는 분산락을 적용한 다른 코드가 있는데, 내가 세웠던 기준에 대해 부합하여 코드를 작성했는지 검증해 보고 개선점이 있다면 그 포인트를 잡아보려 한다.

비즈니스 용어

💡 제안 : A사용자의 물건으로 B사용자의 물건에게 거래를 제안하는 것. 같은 가격대의 물건일 시 **오퍼**, 다른 가격대의 물건일 시 **찔러보기**라는 이름으로 제안 요청을 한다.

전제 조건

  1. 우리 프로젝트는 향후 다중 서버로 확장할 것을 가정한다.
  2. 다중 서버에서 A→B요청, B→A요청들이 들어올 시 block해야 한다.
  3. 단일 서버 Multi thread에서 동시에 A→B 제안 요청이 두개 들어오거나 A→B, B→A 제안요청이 들어올 시 block해야 한다.
  4. 단일 서버 Single thread에서 따당 이슈로 인해 A→B 제안요청이 짧은 시간 안에 두개가 들어올 경우에도 마찬가지로 block해야 한다.
  5. 단일 서버 Single thread에서는 A→B, B→A요청이 짧은 시간 안에 들어올 수 없으므로, 이에 대해서는 따로 방어로직이 필요하지 않다.
  6. A→B요청, B→A요청이 동시에 생길 수 없다. db에서 unique key로 관리한다.

분산락 적용 코드

@Transactional
@DistributedLock(key = "#lockName")
public SuggestionResponseDTO createSuggestion(
    String token,
    String lockName,
    String suggestionType,
    SuggestionRequestDTO requestDto
) {
		...

    Card fromCard = cardRepository.findByCardIdAndUser(requestDto.fromCardId(), user)
        .orElseThrow(() -> new BaseException(ErrorCode.USER_NOT_MATCHED));

    Card toCard = cardRepository.findById(requestDto.toCardId())
        .orElseThrow(() -> new BaseException(ErrorCode.CARD_NOT_FOUND));
	
		//validation 로직
    validateCard(fromCard, toCard);

    ...

    return SuggestionResponseDTO.from(savedSuggestion);
}

위 코드의 로직을 간단히 설명하자면 다음과 같다.

  1. requestDTO는 제안을 보낸 카드Id(fromCardId), 제안을 받는 카드Id(toCardId)가 있다.
  2. @DistributedLock(커스텀 어노테이션. AOP를 이용하여 새로운 트랜잭션을 만들어서 파라미터로 받은 lockName을 fromCardId, toCardId를 key로써 이용하여 내부적으로 redisson.tryLock을 호출한다.)으로 메소드단에 락을 건다.
  3. 이때, A → B제안요청과 B → A제안요청은 같은 lockName으로 만들어지도록 처리해 두었다.
  4. 따라서 전제조건 2~4번을 다음과 같이 커버할 수 있다.
    1. 2번 : redis의 분산락을 이용한다.
    2. 3번 : 분산락 + (A→B요청, B→A요청)을 같은 키로 만들었으므로, 동시에 요청이 들어와도 하나의 요청은 block된다. 먼저 온 요청이 entity로 만들어 진 후 이후에 들어온 요청이 entity로 만들어지려 할 때, validation 로직에 의해 예외를 던진다. (결과적으로 A→B요청, B→A요청 둘다 존재할 수 없게 된다.)
    3. 4번 : 분산락에 의해 먼저 들어온 A→B요청이 메소드 lock을 먼저 잡고, 이후 들어온 A→B요청은 block된다. 먼저 온 요청이 entity로 만들어 진 후 이후에 들어온 요청이 entity로 만들어지려 할 때, validation 로직에 의해 예외를 던진다. (결과적으로 A→B요청 2개가 존재할 수 없게 된다.)

더 가벼운 방법으로 해결할 수 없을까?

나는 이전 글에서 분산락 vs Non분산락 판단 기준을 다음과 같이 세웠다.

  1. 다중 서버 환경.
  2. 메소드의 사용 순서가 중요한 상황.
    (ex : A쓰레드 실행 이후 B쓰레드 실행 결과와 B쓰레드 실행 이후 A쓰레드 실행 결과가 다를 수 있는경우. 즉, 이후에 실행될 쓰레드에게 멱등성이 보장되지 않을 경우.)
  3. 실시간 업데이트가 중요한 상황.
    로컬 캐시에 저장 이후 추후에 업데이트 해야하지 않고, 당장 메소드단에 락을 걸어서 바로 db에 업데이트를 해야 할 상황.
    (ex : 재고가 한정되어 있어 바로바로 업데이트 하여 다음 사용자가 구매할 수 있는지 여부를 표시해야 할 경우.)

여기에 제안을 생성하는 상황을 대입해 보자.

1번 : 우리 프로젝트 상황과 부합한다.

2번 : A→B요청과 B→A요청은 엄밀히 다르다. (제안 entity에서 fromCard와 toCard가 별도의 column으로 관리되기 때문에.) 따라서 사용 순서가 중요하다.

3번 : A→B요청을 실시간으로 영속화 하여, 이후 들어올 A→B요청 혹은 B→A요청을 block하여 데이터베이스 무결성을 지켜야 하기 때문에 실시간 업데이트(영속화) 또한 중요하다.

결론 : 일단 분산락 사용 기준에는 부합한다.

로컬 캐시로는 해결할 수 없을까?

전제조건 2번 상황 : 하나의 WAS에서 A→B요청을 캐싱하고 다른 WAS서버에서 A→B요청 혹은 B→A요청을 날린다고 가정해 보자. 이럴땐 못 막는다.

전제조건 3번 상황 : 하나의 WAS에서 A→B요청을 캐싱하고 동시에 A→B요청 혹은 B→A요청이 들어올 시, 캐시에 존재하는지 검사하면 되므로 막을 수 있다.

전제조건 4번 상황 : 3번 상황과 같이 막을 수 있다.

위의 방법들은 synchronized를 적용해도 해당 키워드는 각각의 서버에서만 적용되기 때문에, 결국 2번 상황을 막지 못한다.

db unique key로 해결할 수 없을까?

일단 unique key를 걸어두긴 하였다. 하지만 어플리케이션단에서 방어로직을 작성하지 않아 write작업이 실패할 때 생기는 비용(작업 롤백) vs 어플리케이션단에서 방어로직이 실행되는 비용(캐싱 확인)을 비교했을 때 어플리케이션단에서 체크하는 것이 더 이득이라고 판단을 했다.

결론

위의 내용을 종합하여, 결론적으로 이 기능에는 기존의 선택대로 분산락 적용이 맞다고 생각한다.

profile
우상향 하는 개발자

0개의 댓글