백엔드 개발 실무 지식(3)

유기훈·2025년 7월 26일

동시성, 데이터가 꼬이기 전에 잡아야 한다

많이 공부했었던 내용이니 용어정리만 하고 넘어간다.

경쟁 상태 (Race Condition)

여러 스레드가 동시에 공유 자원에 접근할 때, 접근 순서에 따라 결과가 달라지는 상황을 경쟁 상태라고 한다.

임계 영역 (Critical Section)

임계 영역은 동시에 둘 이상의 스레드나 프로세스가 접근하면 안 되는 공유 자원에 접근하는 코드 영역을 말한다.

뮤텍스 (mutex)

뮤텍스는 mutual exclusion의 줄임말인데 뮤텍스를 다른 말로 잠금이라고도 한다. 프로그래밍 언어에 따라 뮤텍스를 사용하기도 한다. 자바 언어는 이름이 Lock인 타입을 사용한다.

세마포어 (Semaphore)

세마포어는 동시에 실행할 수 있는 스레드 수를 제한한다. 자원에 대한 접근을 일정 수준으로 제한하고 싶을 때 세마포어를 사용할 수 있다.

CAS

CAS는 Compare And Swap의 약자로 이름 그대로 비교 후에 교체하는 연산을 말한다. CAS 연산은 원자적이다.

비관적 잠금

비관적 잠금을 사용하면 동일한 레코드에 대해 한 번에 하나의 트랜잭션만 접근할 수 있도록 제어할 수 있다.

낙관적 잠금

값을 비교해서 수정하는 방식이다. 쿼리 실행 자체를 막지 않으면서도 데이터가 잘못 변경되는 것을 막을 수 있다.

증분 쿼리

update SUBJECT set joinCount = joinCount + 1 where id = ?

DB는 동일 데이터에 대한 원자적 연산이 실행될 경우 이를 순차적으로 실행한다.
증분 쿼리는 DB에 따라 원자적 연산이 아닐 수도 있기 때문에 사용하는 DB가 원자적으로 처리하는지 반드시 검증해야 한다.

교착 상태 (deadlock)

교착 상태는 2개 이상의 스레드가 서로가 획득한 잠금을 대기하면서 무한히 기다리는 상황을 말한다.

라이브락 (livelock)

계속 상태가 변경되지만 교착 상태가 지속되는 것을 뜻한다. (ex. 철학자의 만찬 문제)
우선 순위를 두는 방식으로 라이브락을 해소할 수 있다.

기아 상태

우선순위가 높은 작업이 많아 우선 순위가 낮은 작업이 실행이 안 될 수 있다. 이렇게 프로세스나 스레드가 자원을 할당받지 못해 실행되지 못하는 상태를 기아 상태라고 부른다.

단일 스레드로 처리하기

동시성 문제가 발생하는 주된 이유는 여러 스레드가 동시에 동일 자원에 접근하기 때문이다. 이를 방지하기 위해 잠금과 같은 수단을 사용하지만 잘못 사용하면 교착상태 같은 상황이 발생할 수 있다.

여러 스레드가 접근하려는 동일 자원에 대해 하나의 스레드만 접근 가능하게 하면 동시성 문제가 발생하지 않게 된다. 작업 요청 스레드 여러개가 단일 접근 스레드에게 작업 큐에 담아 요청을 보내면 접근 스레드가 작업을 처리하여 반환한다. 병목 상태가 발생하는 것 같이 느껴질 수도 있지만, 생각해보면 여러 스레드가 하나의 자원을 획득하기 위해 기다리나 접근 스레드가 작업 결과를 반환해주길 기다리나 그게 그거다.

IO 병목, 어떻게 해결하지

네트워크 IO와 자원 효율

서버 API를 만들때 개발자가 직접 네트워크 프로그램을 작성하지는 않지만, 서버는 네트워크 통신을 기반으로 동작한다. 네트워크를 통해서 데이터 읽기/쓰기(데이터 입출력, I/O)를 한다.

데이터 입출력이 완료될 때까지 스레드는 아무 작업도 하지 않고 입출력이 끝나기를 기다린다. 스레드가 대기하는 시간을 소요한다는 것은, 그 스레드를 실행하는 CPU로 아무것도 하지 않는 시간이 생긴다는 의미이다. 요청당 스레드 방식으로 구현한 서버가 이에 해당한다. (ex. Tomcat)

블로킹 방식은 스레드 개수가 많아도 문제이다.

메모리를 늘려 스레드를 많이 만들어서 여러 요청을 동시에 수행할 수 있도록 하면 문제가 해결되는가? 아니다. 그럼 또 다른 문제가 생기는데 바로 컨텍스트 스위칭이다.

컨텍스트 스위칭

운영체제는 여러 스레드를 번갈아 가면서 CPU에 할당한다. 한 스레드를 짧은 시간 동안 실행하고 다음 스레드를 짧은 시간 실행하는 식이다. CPU가 스레드를 전환할 때 현재 실행 중인 스레드의 상태를 기록하고 다음 스레드의 상태를 불러온다. 이러한 과정을 컨텍스트 스위칭이라고 한다.

IO 병목 해결 방법

  • 가상 스레드나 고루틴 같은 경량 스레드 사용
  • 논블로킹 또는 비동기 IO 사용

가상 스레드로 자원 효율 높이기

코드를 블로킹 IO로 작성했는데, 입출력 동안 스레드가 대기하지 않고 다른 일을 할 수 있는 방법. 특별한 노려 없이 CPU 효율을 높일 수 있는 방법

가상 스레드

  • 플랫폼 스레드: OS 스레드에 1-1 대응하는 래퍼
  • 가상(경량) 스레드: 플랫폼 스레드에 의해 실행되는 스레드
  • 캐리어 스레드: 가상 스레드를 실행하는 플랫폼 스레드를 캐리어 스레드라고 표현한다.

가상 스레드는 실제 업무에서 쓰고자 할 때 다시 자세히 공부하자. 지금으로써는 코드를 블로킹 IO로 작성했는데 IO 병목 문제가 발생할 때 해결할 수 있는 방안 중에 하나라고 기억하자.

논블로킹 IO로 성능 더 높이기

가상 스레드도 결국에 많아질수록 더 많은 메모리를 사용하고 스케줄리에 더 많은 시간을 사용하게 되는 문제가 있다. 사용자가 폭발적으로 증가하면 가상 스레드도 한계가 온다. 이럴 때 논블로킹 IO를 사용할 수 있다.

언제 어떤 방법을 택할까

논블로킹 IO나 가상 스레드를 적용할 때는 먼저 다음을 검토해야 한다.

  • 문제가 있는가?: 성능 문제가 없고, 트래픽이 엄청나게 몰리는 것이 확정된 게 아니라면 가상 스레드나 논블로킹 IO는 적용하지 말아라. 코드가 복잡해지고, 유지보수가 어렵다.
  • 문제가 있다면 네트워크 IO 관련 성능 문제인가?: 가상 스레드, 논블로킹 IO는 네트워크 블로킹을 개선하는데 효과적인 방법이지 CPU 작업에는 효과적이지 않다. 그리고 DB 쿼리 시간이 늦어지는 건 DB 쿼리 튜닝을 해야되는 거다.
  • 구현 변경이 가능한가?: 문제가 IO 관련이라면 그때는 구현 변경이 가능한지를 따져봐야 한다. 그게 안된다면, 또는 일정이 바쁘다면, 메모리를 추가하고 스레드 개수를 늘리는 방법도 있고 수평 확장을 하는 방법도 있을 것이다. 가상 스레드 & 논블로킹 IO는 정말 필요한 경우에 검토를 통해서 도입하자.
profile
개발 블로그

0개의 댓글