Welcome! Everything is fine.

[STUDY] 동시성 제어 개념과 방법 알아보기🧐 본문

카테고리 없음

[STUDY] 동시성 제어 개념과 방법 알아보기🧐

개발곰발 2025. 5. 22.
728x90

 

최종 프로젝트가 끝난지 벌써 2주가 됐다...!

최종 프로젝트에서 나는 예약 및 이벤트 동시성 제어를 구현했지만 아쉬운 점이 많았다.

  • 좀 더 다양한 동시성 제어 방법에 대해 공부하지 않은 점
  • DB 락 구현이 잘못된 점
  • 내가 구현한 부분은 말로 설명하는 연습이 부족한 점

따라서 팀원들과 매주 모여 자신이 공부한 내용을 발표하고 의견을 나눠보기로 했다.

주제는 최종 프로젝트의 핵심적인 내용 위주로 진행될 예정이다.

첫 번째 주제는 "동시성 제어"이다.

내가 맡은 부분이긴하지만 어렵고 헷갈리는 부분이 많아 쉽지 않은 주제인 것 같다.

🧐동시성(Cocurrency) 제어란?

둘 이상의 실행 흐름(스레드 또는 트랜잭션)이 동시에 동일한 자원에 접근할 때 발생할 수 있는 데이터 충돌이나 정합성 오류를 방지하기 위한 제어 기법.

 

동시성 제어가 없다면 다음과 같은 문제가 생길 수 있다.

  • 선착순 100명 이벤트에 동시에 200명이 신청할 경우, 100명을 초과해 참여가 완료됨.
  • 재고가 100개 남았을 때 동시에 100건의 주문이 들어올 경우, 재고 수량과 실제 판매량이 맞지 않음.

🫱🏻‍🫲🏻동시성 제어를 하지 않는다면...

경쟁 상태(Race Condition)

여러 프로세스 혹은 스레드에서 자원에 접근하는 순서에 따라 데이터 상태나 시스템의 동작 결과가 달라지는 현상.

 

🏃🏻경쟁상태의 대표적인 예시 - 너무 많은 우유 문제

냉장고는 공유자원, 엄마와 아빠는 각각의 프로세스라고 생각하고 아래 예시를 보자.

  • 엄마가 냉장고를 열어 우유가 없는 것을 확인한다.
  • 엄마가 우유를 사러 마트에 간다.
  • 엄마가 우유를 사서 돌아오는 길에 아빠가 냉장고에 우유가 없는 것을 확인한다.
  • 아빠가 우유를 사러 마트에 간다.
  • 아빠가 우유를 사서 집에 돌아온다.

에상한 결과는 우유가 1개가 되는 것이었지만, 실제로는 2개가 되어버렸다.

두 프로세스(엄마, 아빠)가 거의 동시에  자원의 상태를 읽고 독립적으로 행동했기 때문이다.

 

이런 경쟁 상태로 아래 예시와 같이 데이터의 무결성이 손상될 수 있다.

  • 재고 수량이 음수가 되는 현상
  • 이벤트 정원을 초과해서 성공 처리되는 현상
  • 포인트 중복 적립 또는 누락
✅ 임계 영역 : 공유 자원에 접근할 수 있고 접근 순서에 따라 결과가 달라지는 코드 영역.
 (위 예시에서는 냉장고에 우유 유무를 판단하고 우유를 추가하는 부분)

- 임계 영역에서 경쟁 상태가 발생하는 것을 방지하려면 여러 프로세스가 공유 자원에 접근해도 데이터의 일관성이 유지되도록 프로세스 동기화를 해야한다.
- 임계 영역에 대한 접근을 동기화하는 것이 동시성 제어의 핵심이다. 
동기(Blocking) : 호출자가 요청에 대한 결과가 나올 때까지 요청자에게 제어권을 돌려주지 않는 것.
비동기(non-blocking) : 호출자가 요청에 대한 결과가 나오지 않더라도 요청자에게 제어권을 돌려줌.
싱크(Sync) : 호출자와 요청자의 결과 확인이 동시에 일어남.
어싱크(Async) : 호출자와 요청자의 결과 확인이 동시에 일어나지 않아도 됨.

교착 상태(Deadlock)

2개 이상의 프로세스가 각각 자원을 가지고 있으면서 서로의 자원을 요구하며 기다리는 상태.

 

🍽️교착 상태의 대표적인 예시 - 철학자들의 식사 문제

  • 철학자들은 포크(자원)를 양손에 들어야 식사를 할 수 있다.
  • 모두 동시에 왼쪽 포크를 들고, 오른쪽 포크가 놓이길 기다리면?
  • 서로가 서로의 포크를 기다리는 상태에서 영원히 멈추게 된다.

위 예시와 같이 자원을 점유하고 있으면서 동시에 다른 자원을 기다리는 상황은 교착 상태를 유발한다.

 

아래의 조건을 모두 만족할 때 교착 상태가 발생한다.

  • 상호배제(mutual exclusion) : 하나의 공유 자원에 하나의 프로세스만 접근할 수 있다.
  • 점유와 대기(hold and wait) : 프로세스가 최소 하나의 자원을 점유하고 있는 상태에서 추가로 다른 프로세스에서 사용 중인 자원을 점유하기 위해 대기한다.
  • 비선점(non-preemption) : 다른 프로세스에 할당된 자원을 뺏을 수 없다.
  • 환형 대기(circular wait) : 프로세스가 자신의 자원을 점유하면서 앞이나 뒤에 있는 프로세스의 자원을 요구한다.

🎫동시성 제어 방법

동시성 제어 방식은 단일 서버 내 스레드 수준의 처리부터, 데이터베이스 트랜잭션, 분산 환경까지 다양한 계층에서 이뤄질 수 있다.

synchronized

자바에서 제공하는 가장 기본적인 동시성 제어 키워드.

 

  • 단일 JVM 내에서 여러 스레드가 동시에 공유 자원에 접근할 때 충돌을 방지하기 위해 사용된다.
  • 단일 서버, 단일 JVM 내부의 멀티스레드 환경에서만 효과있다.
  • 락 범위가 커지면 성능 병목이 발생할 수 있다.

사용 예시

다음과 같이 간단한 재고 감소 로직으로 테스트 해봤다.

 

동시에 100개 요청 테스트를 해보니 재고가 10개밖에 감소되지 않았다.

 

이번에는 synchronized 키워드를 사용했다.

 

정상적으로 100개가 감소되었다.

ReentrantLock

synchronized보다 더 세밀한 제어가 가능한 명시적 락 구현체.
  • 자바의 java.util.concurrent.locks 패키지에 포함되어 있다.
  • 임계 영역을 직접 lock/unlock 할 수 있는 구조를 제공한다.
  • 락 획득 방식, 공정성, 대기 시간 설정 등에서 synchronized보다 유연하다.

낙관적 락 vs 비관적 락

언제 어떤 락을 써야 할까? 어떤 상황인지가 중요하다.

  • 중돌이 자주 발생하는 상황인가?
  • 읽기와 수정하기의 비율은 어디에 가까운가?

낙관적 락 (Optimistic Lock)

충돌이 없을 것이라고 낙관적으로 예상하는 방법.

 

  • 실제로 Lock을 이용하지 않고 Version을 이용해 정합성을 맞춘다. ( JPA에서는  @Version 어노테이션 사용 )
  • 어플리케이션 락
  • 데드락의 가능성이 적고 성능의 이점이 있다.
  • 충돌이 발생하면 오버헤드가 발생한다.

비관적 락(Pessimistic Lock)

충돌을 예상하고 미리 락을 거는 방법.

 

  • 실제로 데이터에 락을 걸어 정합성을 맞춘다.
  • 데이터베이스 락
  • 충돌에 대한 오버헤드가 줄어들고 무결성을 지키기 용이하다.
  • 충돌이 없으면 오버헤드가 발생한다. →  비관적 락은 실제로 충돌이 자주 발생할 것으로 예상되는 상황에서 사용해야 한다.

최종 프로젝트에서 선착순 이벤트 역시 한 번에 수많은 요청이 몰릴 것이 분명한 구조였기 때문에,

충돌이 실제로 발생할 가능성은 높다고 판단했다. 따라서 다음과 같이 비관적 락을 사용했다.

  • findByIdForUpdate()는 내부적으로 SELECT ... FOR UPDATE를 수행하여 이벤트 row를 트랜잭션 동안 잠금 상태로 유지한다.
  • 이벤트 상태 확인, 중복 참여 검사, 좌석 감소, 참여 저장까지의 일련의 작업은 모두 하나의 트랜잭션 안에서 수행되며, 동시 접근으로부터 보호된다.

Redis 분산 락

(내용 추가 예정)