동시성
Concurrency
개념
여러 작업을 번갈아 겹쳐 진행해서, 하나가 무언가를 기다리는 동안 다른 하나를 처리하는 방식입니다.
등장 배경
서버가 요청을 한 번에 하나씩만 처리하던 시절이 있었습니다. 먼저 온 요청을 끝낸 뒤에 다음 요청을 집는 식입니다. 문제는 요청 하나가 실제로 계산하는 시간이 얼마 안 된다는 데 있었습니다. 문서 목록을 불러오는 요청은 데이터를 찾아오라고 시켜 놓고 답이 올 때까지 40밀리초를 그냥 기다립니다. 그 40밀리초 동안 서버는 아무것도 하지 않고 서 있고, 뒤에 줄 선 요청들도 함께 서 있습니다.
기다리는 시간이 일하는 시간보다 훨씬 길다는 것이 핵심이었습니다. 요청 하나가 서버에 머무는 50밀리초 중 실제 계산은 5밀리초뿐이고 나머지 45밀리초는 저장소나 다른 서버의 답을 기다리는 시간입니다. 열 명이 동시에 들어오면 열 번째 사람은 앞의 아홉 명이 각자 기다리는 시간까지 전부 기다린 뒤에야 차례가 옵니다.
페이지덱에서도 같은 일이 있었습니다. 점심시간이 지나 사람들이 한꺼번에 문서를 열면 목록 화면이 몇 초씩 멈춰 있었습니다. 태오가 서버 상태를 들여다봤더니 처리 능력이 모자란 것이 아니었습니다. 서버는 대부분의 시간 동안 놀고 있었고, 그저 놀고 있는 동안 다음 요청을 집지 않고 있었을 뿐입니다.
작동 방식
동시성은 여러 작업을 같은 순간에 실행하는 것이 아니라, 한 작업이 멈춰 서서 기다리는 틈에 다른 작업을 끼워 넣는 것입니다. 실제로 같은 순간에 두 계산이 굴러가려면 처리 장치가 두 개 있어야 하고, 그것은 병렬이라고 따로 부릅니다. 동시성은 장치가 하나여도 성립합니다.
식당 종업원 한 명이 테이블 다섯을 맡는 장면과 같습니다. 1번 테이블 주문을 주방에 넘긴 뒤 음식을 서서 기다리지 않고 2번 테이블 주문을 받고 3번 테이블 물을 채웁니다. 요리를 다섯 개 동시에 하는 것이 아니라 기다리는 시간을 겹쳐 쓰는 것입니다.
겹치는 방법은 크게 셋입니다.
- 작업마다 실행 흐름을 따로 두고 운영체제가 번갈아 돌립니다. 익숙한 방식이지만 흐름 하나마다 메모리가 붙어서 수천 개를 만들면 그 비용이 커집니다.
- 흐름 하나가 대기 목록을 돌면서 끝난 일부터 집습니다. 요청을 보내 놓고 답을 기다리는 대신 목록에 적어 둔 뒤 다음 일로 넘어가고, 답이 도착하면 그때 이어서 처리합니다. 기다림이 대부분인 서버에서 가장 적은 자원으로 가장 많은 요청을 받는 방식입니다.
- 프로그램을 여러 벌 띄우고 요청을 나눠 줍니다. 서로 메모리를 나누지 않아 안전한 대신 한 벌마다 자원을 더 씁니다.
셋 다 대가는 같습니다. 순서가 정해지지 않는다는 것입니다. 하나씩 처리할 때는 A 가 끝난 뒤 B 가 시작하지만, 겹쳐 처리하면 A 가 값을 읽은 뒤 쓰기 전에 B 가 같은 값을 읽을 수 있습니다. 이 틈에서 경쟁 상태가 나옵니다. 그래서 겹쳐 돌리는 설계에는 같은 요청이 여러 번 들어와도 결과가 달라지지 않는 성질인 멱등성이나, 한 번에 하나만 건드리게 막는 잠금 장치가 따라붙습니다.
쓰이는 곳
가장 먼저 만나는 자리는 요청 처리입니다. 요청을 받아 응답을 돌려주는 프로그램인 서버는 대부분 이미 동시성을 전제로 돌아갑니다. 개발자가 따로 켜지 않아도 기본값으로 켜져 있어서 코드만 봐서는 겹침이 눈에 보이지 않습니다. 겹치고 있다는 사실은 대개 그것이 어긋났을 때 처음 드러납니다.
두 번째는 응답과 떼어 낸 처리입니다. 300쪽 문서를 내보내는 데 40초가 걸린다면 사용자를 40초 동안 세워 둘 수 없으니, 곧바로 접수됐다고 답하고 실제 작업은 뒤에서 돌립니다. 이 백그라운드 작업을 여러 개 겹쳐 돌리려면 동시성이 필요하고, 한꺼번에 몰릴 때 줄 세우는 장치가 메시지 큐입니다.
세 번째는 바깥을 여러 번 부를 때입니다. 결제 정보와 사용량과 팀 정보를 각각 다른 곳에서 가져와야 하고 한 번 부를 때마다 300밀리초가 걸린다면, 차례로 부르면 900밀리초가 걸리고 겹쳐 부르면 300밀리초에 끝납니다. 셋이 서로를 기다릴 이유가 없습니다.
효과가 없는 자리도 알아 둘 만합니다. 기다림 없이 계산만 오래 하는 일은 겹쳐도 빨라지지 않습니다. 장치 하나로 무거운 계산 열 개를 번갈아 돌리면 전체 시간은 그대로이고 각각이 열 배 늦게 끝날 뿐입니다. 그쪽은 장치를 늘려야 풀립니다.
실무 용례
"지금 동시 접속 3000이면 큐가 밀려요. 워커를 늘려야 합니다."
겹쳐 처리할 수 있는 양의 한계에 닿았다는 보고입니다.
"이건 동시성 문제라 재현이 안 돼요. 스무 번 돌리면 한 번 납니다."
순서가 맞물리는 특정 타이밍에만 나는 버그라는 뜻입니다.
"순서대로 부르면 900밀리초인데 겹쳐서 부르면 300밀리초로 끝나요."
서로 기다릴 이유가 없는 호출을 묶자는 제안입니다.
페이지덱에서 이 말이 가장 또렷하게 등장한 것은 워크숍 신청 기능에서였습니다. 남은 자리가 하나인 워크숍에 두 사람이 같은 순간에 신청 버튼을 눌렀고, 두 요청이 겹쳐 처리되면서 둘 다 남은 자리를 1 로 읽었습니다. 둘 다 신청이 접수됐고 좌석 수는 마이너스 1 이 되었습니다. 수아에게 항의가 들어왔을 때 태오가 같은 상황을 재현해 보려 했지만 열 번을 눌러도 되지 않았습니다.
원인을 찾은 뒤 팀이 한 일은 겹침을 없애는 것이 아니라, 좌석을 읽고 쓰는 구간만 한 번에 하나씩 지나가게 막는 것이었습니다. 겹침을 없애면 서버 한 대가 감당하던 사람 수가 함께 무너지기 때문입니다.
흔한 오해
동시성을 켜면 빨라진다고 읽는 경우가 가장 많습니다. 여러 개를 한꺼번에 처리한다는 말이 그렇게 들립니다. 하지만 빨라지는 것은 기다림이 섞여 있을 때뿐입니다. 저장소를 부르고 답을 기다리는 일이라면 그 틈에 다른 일을 밀어 넣어 전체 시간이 줄어들지만, 계산만 하는 일은 겹쳐도 총량이 그대로입니다. 이 구분 없이 코드를 겹치게 바꾸면 복잡도만 늘고 응답 시간은 그대로인 결과가 나옵니다.
겹치는 일은 서버가 알아서 해 주니 신경 쓸 것이 없다고 보는 것도 흔합니다. 실제로 대부분의 서버 프로그램이 기본으로 겹쳐 돌리고, 아무 일 없이 몇 달이 지나가기도 합니다. 그러나 둘 이상의 요청이 같은 값을 건드리는 순간 순서가 결과를 바꿉니다. 그대로 두면 한 달에 한두 번 설명되지 않는 데이터 이상으로 돌아옵니다. 재고가 마이너스가 되거나 같은 청구가 두 번 나가는데, 로그를 봐도 각 요청은 정상으로 보입니다.
자주 헷갈리는 용어
| 용어 | 무엇을 가리키는가 | 동시성과의 차이 |
|---|---|---|
| 경쟁 상태 | 겹쳐 실행된 작업의 순서에 따라 결과가 달라지는 고장 | 동시성은 성능을 위해 일부러 택하는 처리 방식이고, 경쟁 상태는 그 선택의 대가로 생기는 문제입니다. 겹쳐 돌리지 않으면 경쟁 상태는 아예 생기지 않고, 겹쳐 돌리면 막을 장치를 따로 붙여야 합니다 |
같이 보면 좋은 페이지
마지막 업데이트 2026-09-16