여러 요청이 같은 계정 상태를 바꿉니다. 요청마다 뮤텍스를 잡는 방법과 계정별 큐에서 순차 실행하는 방법은 대기·순서·오류 처리에서 어떻게 다른가요?
잠깐, 답변을 보기 전에
먼저 내 말로 설명해 볼까요?
답변과 점검 포인트 펼치기답변 접기
구두 답변
정의를 외우기보다, 원리와 조건을 연결해 설명해 보세요.
둘 다 동시 접근을 제어할 수 있지만, 상태를 다루는 방식이 다릅니다. 뮤텍스는 여러 스레드가 공유 상태를 번갈아 잠그고 사용하게 합니다. 직렬 실행 큐는 그 상태를 변경하는 작업을 한곳으로 보내 하나씩 실행하게 합니다. 저는 짧은 공유 연산을 보호하려는지, 작업의 처리 순서와 소유권을 정하려는지부터 구분하겠습니다.
예를 들어 계정 잔액을 메모리에서 읽고 바로 변경하는 짧은 작업이라면 뮤텍스가 단순합니다. 같은 뮤텍스로 모든 접근을 보호하면 동시에 변경하지 못하게 할 뿐 아니라, 앞선 쓰기를 다음 스레드가 볼 수 있도록 메모리 가시성도 보장합니다. 반면 계정별로 여러 요청을 정해진 순서에 따라 처리해야 한다면, 계정 키별 큐로 보내는 구조가 이해하기 쉬울 수 있습니다. 일반적인 뮤텍스는 먼저 온 요청이 먼저 락을 얻는다는 보장은 없기 때문입니다.
다만 큐에 넣는 것만으로 작업 전체가 원자적이 되지는 않습니다. 첫 작업이 잔액을 읽고 비동기 DB 요청을 보낸 뒤 반환했다고 하겠습니다. 그사이에 다음 작업이 잔액을 바꾸면, 첫 작업의 완료 콜백은 이미 달라진 상태를 보게 됩니다. 시작과 콜백을 각각 직렬로 실행해도 둘 사이에 다른 작업이 끼어들 수 있는 것입니다.
요청 순서를 끝까지 지켜야 한다면 첫 작업이 완료될 때까지 같은 계정의 다음 작업을 보류하겠습니다. 대신 느린 DB 요청 하나가 뒤의 요청을 모두 막는 비용을 받아들여야 합니다. 그 지연이 허용되지 않는다면 작업을 중간 단계로 나누고 완료 시 버전이 바뀌었는지 확인하는 방법이 있지만, 취소와 실패 처리가 더 복잡해집니다. 저는 먼저 단순한 쪽으로 정확성을 확보하고, 계정별 큐 대기 시간이 실제 문제가 될 때 구조를 확장하겠습니다.
따라서 락과 큐 중 무엇이 빠르다고 정하기보다 어디까지 한 작업으로 보호해야 하는지를 먼저 정하겠습니다. 락 보유 시간과 큐 대기 시간을 재서 병목을 찾고, 서로 독립적인 계정만 병렬화하겠습니다. 두 계정을 함께 바꾸는 작업은 계정별 큐만으로 보호되지 않으므로 별도 조정이 필요합니다.
상호 배제와 작업의 원자적 완료를 구분합니다
공유 상태에 동시에 들어가지 못하도록 보호하는 범위를 임계 구역(critical section)이라고 하고, 한 번에 하나의 실행 흐름만 들어가게 하는 성질을 상호 배제(mutual exclusion)라고 합니다. 같은 락의 해제와 이후 획득 사이에는 앞선 쓰기가 뒤의 읽기에 보이도록 하는 순서 보장도 있습니다. 이를 메모리 모델에서는 happens-before 관계로 설명합니다. 변수 자체가 다르거나 모든 접근이 같은 규칙을 따르지 않으면 락 하나가 프로그램 전체의 가시성을 자동으로 해결하지는 않습니다.
계정별 큐는 상태 변경을 특정 실행 경로에 모으는 직렬 실행(serial execution) 방식입니다. 단일 소유자가 메시지를 받아 자신의 상태를 바꾸는 actor 모델과도 연결되지만, 큐를 하나 만들었다고 actor의 수명·격리·오류 처리 계약까지 생기는 것은 아닙니다. 실행기가 매번 다른 스레드를 골라도 작업이 겹치지 않고 이전 결과가 보이도록 안전하게 전달한다면 직렬화는 가능합니다. 중요한 것은 스레드 번호가 아니라 상태의 접근 권한과 실행 순서입니다.
비동기 대기에서 다음 작업을 허용하는지에 따라 보장은 달라집니다. 첫 요청이 잔액 100을 읽고 외부 확인을 기다리는 사이 두 번째 요청이 30을 썼다면, 첫 요청이 나중에 예전 잔액 기준으로 결과를 덮어쓸 수 있습니다. 이러한 실행 단계의 끼어듦을 인터리빙(interleaving)이라고 합니다. 메모리를 동시에 읽고 쓰는 데이터 레이스가 없더라도, 논리적 요청 사이의 경쟁으로 결과가 틀릴 수 있습니다. 여러 단계의 작업을 외부에서 하나처럼 보이게 하는 원자성은 각 콜백을 순차 실행하는 것보다 더 강한 요구입니다.
한 계정 밖으로 범위가 넓어지면 다시 설계합니다
완료까지 같은 계정의 다음 요청을 막으면 이해하기 쉽지만, 맨 앞의 느린 요청이 뒤의 짧은 요청까지 지연시키는 선두 차단(head-of-line blocking)이 생깁니다. I/O 전에 필요한 상태를 예약하고, 완료 뒤 예약 ID와 버전을 확인하는 방식으로 대기를 풀 수 있지만, 실패·취소 때 예약을 되돌릴 책임과 만료 후 늦은 완료 처리까지 필요합니다. 순서를 느슨하게 해서 얻는 지연 이득이 이 복잡성을 감당할 만큼 큰지 먼저 측정하겠습니다.
두 계정 사이의 이체는 한 계정의 큐만으로 끝나지 않습니다. 두 락을 함께 잡는다면 계정 ID처럼 전역적으로 같은 순서를 정해 교착을 피하고, 두 큐 사이에서 서로의 완료를 기다리는 구조도 의존 사이클이 생기지 않는지 봐야 합니다. DB가 원본이면 이체의 두 변경을 같은 DB 트랜잭션에서 보호하는 쪽이 더 명확할 수 있습니다. 서버를 여러 프로세스로 확장했을 때 로컬 뮤텍스와 로컬 큐는 다른 프로세스까지 막지 못하므로, 최종 상태를 확정하는 저장소나 단일 소유권 경계가 어디인지 명시해야 합니다.
득점 포인트
이해가 드러나는 지점을 확인해 보세요.
- 작업 성격에 따라 방식을 고른다. 짧은 공유 연산과 계정별 작업 순서 관리에 각각 적합한 이유를 설명한다.
- 상호 배제와 가시성을 함께 이해한다. 모든 접근이 같은 동기화 규칙을 따라야 한다는 전제를 짚는다.
- 비동기 경계에서 생기는 인터리빙을 설명한다. 콜백을 직렬화해도 작업 전체가 원자적이지 않은 사례를 든다.
- 선택의 비용과 변경 시점을 말한다. 완료까지 보류하는 단순한 설계와 재진입 가능한 설계의 복잡성을 비교한다.
감점 포인트
놓치기 쉬운 조건과 오해를 짚어 보세요.
- “큐에서 하나씩 실행하니 모든 작업이 원자적이다”라고 말한다. 비동기 중단 사이에 다른 작업이 들어갈 수 있다.
- “뮤텍스는 요청 도착 순서도 보장한다”고 말한다. 일반적인 뮤텍스의 상호 배제와 FIFO 순서는 다르다.
- “큐는 락이 없어서 항상 빠르다”고 말한다. 큐 내부 동기화와 스케줄링 비용, 작업 본문의 대기는 남을 수 있다.
- “키별 직렬화면 두 키의 동시 변경도 안전하다”고 말한다. 여러 키에 걸친 불변식은 별도의 원자성 경계가 필요하다.
더 파고들 거리
하나 더 질문하며 이해의 범위를 넓혀 보세요.
- 계정별 직렬 실행기를 사용하는데 두 계정 사이 이체가 필요합니다. 상위 조정·잠금 순서·버전 검사는 어떻게 선택하나요?
- 직렬 실행기가 매번 다른 OS 스레드에서 작업을 실행해도 메모리 가시성을 보장하려면 무엇이 필요할까요?
- 느린 작업을 여러 단계로 나눌 때 재진입과 취소에 안전한 상태 머신은 어떻게 만들까요?
- FIFO 큐의 등록 순서와 요청 사이에 반드시 지켜야 하는 적용 순서가 다를 때 순서 번호는 어디에서 부여해야 할까요?
득점·감점 포인트는 학습용 자기 점검 기준이며, 특정 회사의 실제 채점표가 아닙니다.
원리부터 공부하고 싶다면
답변을 보충하는 개념·예제·슈도코드를 읽어 보세요.