동시성 제어 기본 개념

말하는 감자·2025년 5월 16일

내일배움캠프

목록 보기
61/73

동시성 제어라고 하ㅣㄴ깐 저번에 면접 스터디에서 프로세스 동기화 그거랑 비슷한건가?? 싶었다.

솔루션에 보면 락 얘기가나오는데
https://velog.io/@tofha054/면접스터디-DAY2-프로세스-동기화-CPU-스케줄러
여기 있는 그 "락"이랑 비슷한거같음.. 근데 코드로는 첨봄




동시성 제어가 뭐게

하나의 데이터베이스에 여러 트랜잭션 요청이 동시에 왔을 때, 하나씩 순서대로 처리해서 데이터베이스의 일관성과 신뢰성을 보장하는 것


일반적인 어플리케이션은 다수의 사용자의 요청을 처리하고 이러한 요청에는 DB 접근을 필요로 하는 요청도 포함한다.
이때 한번에 다수의 DB 접근을 필요로 하는 요청이 들어올 때 트랜잭션 단위로 DB 접근을 하게 되는데 DB 접근을 동시적으로 모두 허용해주면 데이터베이스의 일관성과 무결성이 깨지게 된다.
이를 방지하기 위해 동시성 제어 을 통해 데이터베이스를 보호할 수 있다.

동시성 제어 는 병행 제어 라고도 불리며 한번에 들어온 복수의 트랜잭션을 직렬화 하는 수행을 보장해야 함




Race Condition

  • 두 개 이상의 스레드가 동시에 같은 데이터를 접근하여 값을 변경하고자 할 때, 데이터의 예상치 못한 변경이 발생할 수 있다.

    사진 을 풀어서 설명하자면
    User1 이 Counter라는 값을 수정하고 커밋하기전에
    User2 가Counter를 접근하면 User1이 수정한 값을 User2가 반영하지 못해서
    최종적으로 커밋되는 값은 두 유저중, 마지막으로 커밋한 User2의 값만 반영이된다.

이런 문제를 동시성 이슈라고 하면서 해결하는 방법은 여러가지가 있음.








동시서 제어 방법

1.Synchronized

보통 요청당 하나의 스레드를 할당받게 되는데 여러 스레드가 동시다발적으로 메서드를 접근하지 못하게 해줌
데이터 베이스 전에 어플리케이션 단에서 처리

메서드 키워드 추가

public synchronized void decrease() {
		entity.decrease();
}

여러 스레드가 들어오면 하나씩만 처리하게 함. 단일스레드처럼 일한다는 뜻 ㅇㅇ

@Transactional + synchronized

synchronized 선언된 메소드에 여러 스레드들이 접근해서 대기중일 때,
함수내부에서 데이터를 변경하고 save 함수를 갈겨도
@Transactional 어노테이션이 있으면 바로 db에 반영이 안돼서,
synchronized가 제대로 작동되지 않는 것 처럼 보인다.

@Transactional(synchronized가 붙은) 메서드가 종료되고나서 db에 반영된다.

예를들어서 이런일을한다고치자

각 스레드가 해당 함수를 호출하는데, 함수가 그 조합이고 SAVE함수를 쓴다.

함수가 끝나서 커밋-> DB반영까지 찰나의 시간이 걸리는데 다음 스레드가 들어와서 findById로 db 조회해가버리는 경우가 생긴다.
이럼 두번째 쓰레드는 첫 번재 쓰레드가 1000->999로 만든것을 모르는 상태가 되어버림

이 외에도 단일 스레드처럼 일하다보니 겁나 느림 => 잘안쓴다

어노테이션 추가

@Synchronized
public void decrease() {
		entity.decrease();
}

코드에 synchronized 블럭 추가

메서드 내에 부분적으로만 적용하고 싶을 때 적용할 부분에다가 synchronized(this)블럭 추가
=> 여러 스레드들이 같이 메서드에 들어가는데 블럭에서 접근제한걸려서 순차 기다리고~~ 하는거

public void decrease() {
		synchronized(this) { 
			entity.decrease();
		}
}







2. DB에서 Lock 제어

비관전 락 (Pessimistic Lock)

어떻게 보면 synchronized는 메서드단위로 Lock을 거는 것이라면
해당 락은 데이터베이스에서 거는 Lock이다.(멀티 스레드 환경은 동일하다.)
비슷하게 일을하고 있으면 다른 트랜잭션이 대기해야해서 느릴 수 있음.

  1. 현재 트랜잭션이 접근한 데이터에 row 단위로 Lock을 걸어 다른 트랜잭션이 읽기(Shared Lock) 혹은 쓰기(Exclusive Lock) 접근을 하지 못하게 하는 방1법

  2. Shared lock (읽기 잠금, s-lock)

    1. 락을 획득한 트랜잭션에서만 대상 레코드를 수정, 삭제 할 수 있으며 락을 획득하지 못한 트랜잭션은 읽기만 허용하는 방법. (PESSIMISTIC_READ)
      유도리 있따.
  3. Exclusive Lock (쓰기 잠금, x-lock)

    1. 락을 획득하지 못한 트랜잭션에서 대상 레코드를 수정, 삭제 뿐 아니라 읽기도 허용하지 않는 방법. (PESSIMISTIC_WRITE)
    2. 읽기를 허용하지 않기 때문에 한 번에 하나의 트랜잭션만 작업을 수행함을 보장.
      냅다 안됨!!




장점

  • 테이블에 락을 걸어 다른 트랜잭션에서의 접근을 하지 못하게 하기 때문에 데이터의 일관성이 보장
  • 데이터를 변경 중에 다른 트랜잭션과 충돌 가능성이 낮다.

단점

  • 데이터 일관성을 보장하지만 동시 접속자가 많은 환경에서는 락 대기 시간으로 인해 성능에 영향을 줄 수 있다.
  • 다수의 트랜잭션이 서로 다른 순서로 여러 데이터에 락을 요청하면 데드락이 발생할 수 있다.

비관적 락은 특정 레코드를 대상으로 충돌이 자주 발생할 것으로 추측하는 경우 사용하기 적합하다.
그러나 레코드 자체에 락을 걸기 때문에 동시성이 크게 저하될 수 있고 반드시 타임아웃을 지정하여 무한 대기에 따른 데드락이 발생하지 않도록 주의가 필요함.
=> row에 여러 컬럼이있는데, 트랜잭션1 과 트랜잭션2가 서로 다른 column을 수정하려고 할 때 (사실 이런 상황이면 굳이 락 안걸어도 충돌 일어나지 x 라고 생각할 수 있음)
서로 충돌이 일어나지 않음에도 트랜잭션2는 트랜잭션1이 끝날 때까지 대기해야 함.

데드락 피해를 최소화 하기 위해 타임아웃이 필요하다.




사용법

public interface StockRepository extends JpaRepository<Stock, Long> {

	@Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value="3000")})// value = ms 단위 (3초)
	@Query("select s from Stock s where s.id = :id")
	Stock findByIdWithPessimisticLock(Long id);
}




낙관적 락 (Optimistic Lock)

실제로 Lock을 거는 것이 아니라 version(or Column)을 메겨서 버전이 같을때만 변화하도록 수정
기본적으로 race condition이라고 인지를 하는 상태임


버전다르면 취소됨. -> 여기서 예외가 발생하는데 이것을 Catch해서 처음부터 다시돌리면 바뀐 DB 값을 정상적으로 받아올지도 몰라서 실행

즉, 본인(트랜잭션)이 잘못된 트랜잭션이란 것을 인지하고 Exception을 뽑게 해주는 것이 낙관적 락이다


장점

  • 동시 요청(많은 재시도 횟수가 아님)에 대해 DB에 락을 걸지 않기 때문에, 비관적 락보다 성능 향상에 이점이 있다.
    • 락이없으니깐 동시 요청을 다같이 처리할 수 있음.
  • 낙관적 락은 충돌이 자주 발생하지 않는다고 가정하기 때문에, 많은 사용자가 동시에 데이터에 접근할 수 있음. 즉, 처리량을 향상시킬수 있다.

단점

  • 동시에 요청하여 데이터 충돌이 발생했을 때 이를 해결하기 위한 추가적인 로직(재시도 로직)이 필요하여 구현 복잡성이 있다.
  • 데이터의 변경 빈도가 높은 시스템에서는 충돌이 자주 발생하기 때문에, 이를 해결하기 위한 추가적인 시간이 필요함.
    • 충돌이 많이 일어나는 경우에는 비관적 락보다 구림




사용법

public interface StockRepository extends JpaRepository<Stock, Long> {

	@Lock(LockModeType.OPTIMISTIC)
	@Query("select s from Stock s where s.id in :id")
	Optional<Stock> findByIdWithPessimisticLock(Long id);
}

낙관적 락 버전 불 일치시 일어나는 예외 이름
`>> ObjectOptimisticLockingFailureException <<<

@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT) 여기는 옵션이 두가지가 있다.
1. OPTIMISTIC : 위에서 설명한 수정 사항이 있을 때 version 값이 오름
2. OPTIMISTIC_FORCE_INCREMENT : 수정 뿐만 아니라 읽기가 일어나도 version 값이 오름 => 충돌이 더 자주 발생한다.





DB에서 Lock 에 대한 문제

비관적 락은 데이터베이스에 Lock을 거는게 맞긴한데 낙관적락은 락.. 보다는 버전관리에 가까운듯.

낙관적 락은 실패 시 예외처리 + 재시도 떄문에 성능 저하 위험이 있고,
비관적 락은 row 한줄에 락을 걸어서 읽기 작업같은 단순한 작업임에도 대기시간이 필요하고 DeadLock이 발생할 수 있음

다른 방법은 뭐가 있을까?







3. 분산 락

만약에 데이터베이스가 한개가 아니라 여러개의 데잉터 베이스가 존재하게 된다.

분산 데이터 베이스란 ?

(https://www.cockroachlabs.com/blog/what-is-a-distributed-database/#what-is-a-distributed-database)

  • 단일 시스템이 아닌 여러 시스템에 걸쳐 실행되고 저장되는 데이터베이스.
  • 일반적으로, 두개 이상의 서버에서 작동하며 각각을 "인스턴스" 혹은 "노드"라 한다.
    즉, 여러 컴퓨터에 나눠 저장된 데이터를 하나처럼 관리하는 시스템임.

이렇게 나누어져 있고 여러개인 데이터베이스에 접근하고, 동시성 제어하기는 생각만해도 어려워보인다.

그래서 조건을 row가 아니라 "데이터 베이스에 접근"에 초점을 두어서, 데이터베이스 접근 전에 문지기를 만들어줌

따라서, 분산 락은 여러 사용자(서버 인스턴스)가 "데이터 베이스를 요청"하려고 하면 앞에서 허가를 받고, 허가를 받은 요청만 데이터베이스에 순차적으로 접근할 수 있게 교통정리를 하는 기술이다.
요런 분산락은 보통 Redis를 많이 사용하기 때문에
국룰 처럼 분산락을 쓰겠다 -> Redis 를 쓰겠다. 라고 생각해도 될정도라고함.

왜 Redis를 많이쓰냐?

Redis는 빠른 성능과 단일 명령의 원자성 보장 덕분에 분산 락에 자주 쓰임.

  • 디스크를 사용하는 RDBMS 등의 DB보다 최소 수만배 이상 빠름
  • 싱글 스레드이기 때문에 락을 안걸고도 동시성 제어 가능

하지만 진짜 분산 환경에서 안정성을 높이려면, Redlock 알고리즘처럼 복수의 Redis 인스턴스를 이용한 분산 락 전략이 필요하다.




Lettuce 와 Redission

항목LettuceRedisson
락 방식스핀락 (setIfAbsent + 재시도)뮤텍스 (Java 스타일 락 추상화)
기능 다양성단순 락만분산락, 재진입락, 세마포어 등
재시도 정책직접 구현내부적으로 제공
락 유지 시간직접 설정watchdog으로 자동 갱신 가능
실무 추천단순 락만 필요할 때안정성과 기능이 중요할 때

레투스는 스핀락이다보니깐 성능관련해서 문제가 좀 많아보이는데,
레디션과 비교했을 때 더 빠르긴 하다. 근데 굳이 이걸?
CPU 낭비가 심한거같음..

가장 대표적인게 저 두 방식이고, 다른 방식도 있따.



SETNX (기본 락 방식)

가장 널리 사용되는 방식이다.
(SETNX는 “set if not exists”의 의미로, 해당 키가 없을 때만 설정됨.)

  • 장점: 빠르고 간단
  • 단점: 여러 Redis 인스턴스에서 동시에 사용하는 분산 락에는 불완전 (→ Redlock 패턴 필요)



Lua 스크립트 기반 락 해제

Redis는 락 해제 시 조건 검사 없이 DEL만 하면 위험하기 때문에, Lua 스크립트로 락 소유자 확인 + 해제를 원자적으로 처리하는 게 안전하다.

  • 장점: 락 해제를 소유자 확인과 함께 원자적으로 처리 가능
  • 단점: 구현에 Lua 스크립트 필요



    왜 DEL만 하면 위험한가??

    1. A 사용자가 락을 얻음
    2. 시간이 지나 타임아웃 됨
    3. B 사용자가 새로 락을 얻음
    4. A가 자신이 락을 가지고 있다고 착각하고 DEL → B의 락이 삭제됨
      → 소유자 확인 없이 DEL하면 교착 상태 발생 위험
profile
대충 데굴데굴 굴러가는 개발?자

1개의 댓글

comment-user-thumbnail
2025년 5월 16일

동시성 제어가 뭘까요 기대돼요

답글 달기