다양한 Lock 기법을 활용한 동시성 제어 방법 ( Synchronized , Database , Redis )

2024. 12. 30. 01:16·[Spring]

📌 개요

이번 포스팅에서는 다양한 Lock 기법을 활용하여 동시성을 제어하는 방법들에 대해 다뤄보고자 합니다. 

순서는 다음과 같습니다. 

 

 

1. Java (Server) 수준에서 동시성을 제어하는 방법

2. Database 수준에서 동시성을 제어하는 방법

3. Redis 로 동시성을 제어하는 방법

 


❓ Race Condition 이란

동시성을 제어하기 위한 이유로는 Race Condition을 방지하기 위함임을 먼저 인지 해야 합니다.

 

Race Condition이란 2개 이상의 스레드가 하나의 공유 자원을 가져갈 수 있고, 동시에 변경을 하려 할 때 발생하는 문제입니다.  예를들어, 100개라는 자원이 존재하고 두 개의 스레드가 하나씩 가져가는 상황이 된다면 총 98 이라는 결과를 기대하지만, 100 일 때의 자원을 동시에 가져가고 둘 다 99로 갱신시켜서 하나의 스레드의 작업이 덮어 씌워 지는 문제로 예시를 들 수 있습니다. 

 

 

🔍 대응책

이를 해결하려면, 하나의 스레드가 변경 작업이 이루어질 때 다른 스레드가 접근하지 못하도록 막아야합니다


📌 JAVA(서버) 수준에서의 첫 번째 해결 방법 : Synchronized

먼저, Database 까지 가지 않고 서버 수준에서의 제어가 가능합니다.

아래와 같이 100개의 상품이 있고 하나씩 재고를 감소시키고자 할 때 , synchronized를 메서드단에 달아주는 것 만으로 해결이 가능합니다. 

  
적용 전

    public void decrease(Long id, Long quantity){   //감소시킬 상품과 양
        // Stock 조회
        Stock stock = stockRepository.findById(id).orElseThrow();
        // 재고를 감소시킨 뒤
        stock.decrease(quantity);
        // 갱신된 값 저장
        stockRepository.saveAndFlush(stock);    
    }


적용 후

    public synchronized void decrease(Long id, Long quantity){   //감소시킬 상품과 양
        // Stock 조회
        Stock stock = stockRepository.findById(id).orElseThrow();
        // 재고를 감소시킨 뒤
        stock.decrease(quantity);
        // 갱신된 값 저장
        stockRepository.saveAndFlush(stock);   
    }

 



synchronized 한계

서버 단에서 제어가 마무리 되면 좋겠지만, 아쉽게도 Database수준의 제어가 아닌 해당 방법으로는 한계가 존재합니다. 


1) @Transactional 어노테이션과 호환 불가
- 메서드단에 synchronized를 걸었을 때, @Transactional 과 함께 있으면, 트랜잭션이 끝나기전에 새로운 공유자원 접근이 가능해진다.

2) 다중서버에서의 한계
- synchronized는 하나의 프로세스에서만 적용됨. 다중 서버일 경우, DB에 동시 접근이 가능하기 때문에 아무 소용이 없게 된다.


따라서 synchronized는 DB의 부하를 줄이는 용도나, 1차적인 수준의 제어 방법으로 활용할 뿐 실질적인 해결 방법으로는 Database 수준까지 넘어가야 합니다.

 


 

🔥 Database 수준에서의 첫 번째 해결 방법 : Pessimistic Lock (비관적 락)

먼저 Pessimistic Lock 방법이며 실제로 데이터에 Lock 을 걸어서 정합성을 맞추는 방법입니다.

 

exclusive lock 을 걸게되며 다른 트랜잭션에서는 lock 이 해제되기전에 데이터를 가져갈 수 없게됩니다. 이는 데드락이 걸릴 수 있기때문에 주의하여 사용하여야 합니다.

✅ 장점
  - 만일, 충돌이 빈번하게 일어난다면 Pessimistic Lock 을 거는것이 Optimistic Lock을 거는 것 보다 더욱 성능이 좋다.
  - Lock을 통해 Update를 제어하기 때문에, 데이터 정합성이 보장된다. 

❌ 단점
  - 결국 별도의 Lock을 잡기때문에 성능이 저하될 수 있다.


🔥 Database 수준에서의 두 번째 해결 방법 : Optimistic Lock ( 낙관적 락 )

다음으로는 실제로 Lock 을 이용하지 않고 버전을 이용함으로써 정합성을 맞추는 방법입니다. 

 

먼저 데이터를 읽은 후에 update 를 수행할 때 현재 내가 읽은 버전이 맞는지 확인하며 업데이트 합니다. 내가 읽은 버전에서 수정사항이 생겼을 경우에는 application에서 다시 읽은후에 작업을 수행해야 합니다.

 

별도의 재실행 로직이 추가로 필요합니다. 

✅ 장점
  - 별도의 Lock을 잡지 않기 때문에, 동시 작업이 가능하여 Pessimistic Lock 보다 성능상의 이점이있다.


❌ 단점

  - Update가 실패 했을 때 재실행하는 로직을 개발자가 직접 정의해야하는 번거로움이 있다.
  - 충돌이 빈번하게 일어난다면 오히려 재실행으로 인해 성능이 더 안좋다.

 

 

🔥 Database 수준에서의 세 번째 해결 방법 : Named Lock

마지막으로는 이름을 가진 metadata locking 방법 입니다. 

 

이름을 가진 lock 을 획득한 후 해제할때까지 다른 세션은 이 lock 을 획득할 수 없도록 합니다.  
주의할점으로는 transaction 이 종료될 때 lock 이 자동으로 해제되지 않습니다. 별도의 명령어로 해제를 수행해주거나 선점시간이 끝나야 해제됩니다.


NamedLock 은 주로, 분산락을 구현할 때 사용합니다. 또한 Pessimistic Lock 은 Time out 을 구현하기 힘들지만, Named Lock은 MySQL 기준 `getLock`을 통해 Lock을 획득하고, `release`를 통해 쉽게 해제가 가능합니다. 

 
여기서, Pessimistic Lock은 Stock이라는 행에 직접 Lock을 걸어줬다면, Named Lock은 별도의 공간에서 Lock을 걸어준다.

 

✅ 장점
  - Pessimistic Lock은 Time out을 구현하기 힘들지만, NamedLock은 time out 을 구현하기 쉽다.
  - 데이터 삽입시에 데이터 정합성을 맞춰야하는 경우에도 NamedLock을 사용할 수 있다.

❌ 단점
  - Transaction 종료시에 Lock해제, Session 관리등을 잘 해줘야하기 때문에 주의해서 사용해야한다.
  - 실제로 사용할 때에는 구현 방법이 복잡하다. 

 

🔍 실무에서는?
  - 실무에서는 데이터 소스를 따로 분리해서 사용하는 것을 추천합니다.
  - 만일, 같은 데이터 소스를 사용하게 된다면, Connection pool이 부족해지는 현상으로 인해서 다른 서비스에도 영향을 끼칠 수 있습니다.

 


 

마지막으로는 Database의 부하를 줄이기 위해, Redis로 Lock을 구현하는 방법입니다.

Lettuce 방식과 Redisson 방식 2가지가 존재하는데, 먼저 Lettuce 방식부터 말씀드리겠습니다.

 

🔨 Redis 를 활용한 첫 번째 해결 방법 : Lettuce ( setnx - 스핀락 )

- setnx 명령어를 활용하여 분산락 구현이 가능합니다.
  - Set if not exist의 약자로 key-value 가 없을 때 사용합니다


- spin lock 방식입니다.
  - 실패 했을 때 reTry 로직을 개발자가 직접 구현 해야합니다.

 


Setnx 를 통해 키 설정

- key가 없으면 삽입 성공
- key가 있으면 삽입 실패  
=> Named Lock 과 방식 동일 (다른점은 redis를 통해 관리, Session을 신경 안써도 됨)



✅ 장점
  - 구현이 간단하다
  - spring data redis 를 이용하면 lettuce 가 기본이기때문에 별도의 라이브러리를 사용하지 않아도 된다.

❌ 단점
  - spin lock 방식이기때문에 동시에 많은 스레드가 lock 획득 대기 상태라면 redis 에 부하가 갈 수 있다.


🔨 Redis 를 활용한 두 번째 해결 방법 : Redisson ( pub - sub 기반)

Redisson은 Redis의 외부 라이브러리로, pub-sub 기반으로 Lock 구현이 가능합니다.

해당 방법을 소개하자면, redis는 자신이 lock을 해제할 때 원하는 채널에 메시지를 보내줌으로서 lock을 해제했다는 표시가 가능합니다. 그러면, 구독하고 있는 채널에 메시지가 도착하게 되면 다른 스레드들은 해당 메시지를 기반으로 lock 획득이 가능힌 방식입니다.  


✅ 장점
  - Lettuce는 지속적으로 lock을 확인하는 반면에, redisson은 메시지를 받은 한 번(몇 번)만 확인하면 된다.  
    - redis의 부하를 줄일 수 있다.
  - 락 획득 재시도를 기본으로 제공한다. 

❌ 단점
  - lock 을 라이브러리 차원에서 제공해주기 떄문에 사용법을 공부해야 한다. (별도의 라이브러리를 사용해야 한다.)
  - 구현이 조금 복잡하다.


🔍 실무에서는 ?
- 재시도가 필요하지 않은 lock 은 lettuce 활용합니다.
- 재시도가 필요한 경우에는 redisson 를 활용합니다

 

📌 결론 : Database 수준의 제어 VS Redis를 활용한 제어?

- Database
  - 이미 RDBMS를 사용하고 있다면 별도의 비용없이 사용가능하다.
  - 어느정도의 트래픽까지는 문제없이 활용이 가능하다.
  - Redis보다는 성능이 좋지 않다.

- Redis
  - 활용중인 Redis가 없다면 별도의 구축비용과 인프라 관리비용이 발생한다.
  - RDBMS 보다 성능이 좋다.

 

 


💪 마무리

이번 포스팅에서는 다양한 Lock 기법을 통한 동시성 제어를 간단히 알아보았습니다.

 

실무에선 다양한 방법들이 상호작용하며 동시성 제어가 이루어진다고 하는데요. 그만큼 공유되는 자원에 대해서 엄격하면서도, 성능을 위한 유동성이 공존하는 것 같습니다. 

 

이상 해당 포스팅을 읽어주셔서 감사합니다. 🙇‍♀️

 

 

📃 참고자료

https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C/dashboard

'[Spring]' 카테고리의 다른 글

Spring - @Controller 와 @RestController 차이  (0) 2025.03.01
Spring Boot JPA : @Entity를 사용할 때 @NoArgsConstructor를 하지 않으면 에러가 나는 이유  (6) 2024.10.20
Bean을 등록하는 2가지 방법 (@Component / @Bean + @Configuration)  (0) 2024.08.04
Spring Security로 로그인 구현해보기  (0) 2024.07.25
Lombok 롬복의 @Builder, @NoArgsConstructor 와의 충돌 이유  (0) 2024.07.08
'[Spring]' 카테고리의 다른 글
  • Spring - @Controller 와 @RestController 차이
  • Spring Boot JPA : @Entity를 사용할 때 @NoArgsConstructor를 하지 않으면 에러가 나는 이유
  • Bean을 등록하는 2가지 방법 (@Component / @Bean + @Configuration)
  • Spring Security로 로그인 구현해보기
7.06com
7.06com
우당탕탕 코딩하기
  • 7.06com
    우당탕탕 개발자의 이야기
    7.06com
  • 전체
    오늘
    어제
    • 분류 전체보기 (54)
      • [Spring] (7)
      • [JAVA] (3)
      • [디자인패턴] (1)
      • [TIL] (7)
      • [CI,CD] (5)
      • [협업] (1)
      • [Database] (4)
      • [CS] (1)
      • [코딩테스트] (15)
      • [알고리즘] (0)
      • [후기-회고] (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
7.06com
다양한 Lock 기법을 활용한 동시성 제어 방법 ( Synchronized , Database , Redis )
상단으로

티스토리툴바