- 들어가며
- 동시성과 병렬성
- 동기와 비동기
- 스레드와 연결(멀티스레드 환경)
- 마무리
개발자가 되기 위해 CS 공부를 할 때 참으로 다양한 주제가 있고 학습 난이도도 쉽지 않습니다. 그 중에서 저를 여러 번 좌절시킨 주제가 있는데요. 바로 동시성 입니다. 동시성과 관련된 주제를 공부하다보면 번번히 개념이 꼬이고 벽을 만난 것처럼 이해가 쉽지 않아 매번 공부를 하다 포기를 하거나, 학습을 했더라도 금방 잊곤 했습니다.
이번 네이버 부스트캠프 챌린지 미션 중 동시성에 대한 미션이 나왔습니다. CS 학습이 주제이다보니 언젠가 한 번은 나올 것이라 생각하고 마음의 준비는 하고 있었지만, 막상 미션을 마주하니 막막하기도 하고 걱정도 됐습니다. 하지만 매번 동시성 주제가 나올 때마다 작아지는 저를 이제는 극복하고 싶었기에 이번에는 "정말 포기하지말고 한 번 이해를 해보자" 라는 각오를 가지고 학습을 하고 미션을 수행했습니다.
감사하게도 이번에는 이전보다는 더 많이 이해할 수 있었고 주제 자체에 대한 두려움도 사라진 것 같습니다. 물론 여전히 더 이해해야 할 부분도 많고 제 이해가 잘못된 부분도 있겠지만, 매번 꼬이던 개념을 잘 풀어내고 동료에게 설명할 수 있을 정도의 이해와 자신감을 가지게 된 것은 정말 큰 수확인 것 같습니다.
제가 정리하는 이 글은 당연하게도 완벽하지 않을 것입니다. 여전히 제가 놓치고 오해하는 부분도 많을 것입니다. 하지만 제가 이해하고 극복한 동시성에 대해 기록을 남기고자 글을 작성해보려고 합니다. '이 사람은 이렇게 이해했구나' 라고 가볍게 봐주시면 감사하겠습니다.
동시성과 병렬성은 항상 짝으로 등장하는 개념입니다. 그리고 이름도 비슷해서 저도 공부하다 보면 참 많이 헷갈리고 혼란스러운 지점이 많이 생겼던 기억이 있습니다.
한국인이 받아들이기에 동시와 병렬은 상당히 유사한 개념입니다. 저만 그런가요? 무언가가 함께 동작한다는 의미로 다가오기 때문입니다. 각 개념을 지칭하는 단어의 의미를 사전에서 찾아봤는데요.
동시(Concurrent) / 병렬(Parallel) 단어의 의미
- concurrent: 동시에
- parallel: 평행의, 나란한
영어 단어의 의미를 보면 살짝 의미가 다른 것을 발견할 수 있습니다. 동시성(Concurrent)은 "동시" 를 의미하고 병렬성(Parallelism) 은 "나란히" 뭔가 진행되는 느낌을 줍니다. 하지만 저는 오히려 이 단어의 의미가 동시성과 병렬성에 대한 이해를 더 어렵게 만든다고 생각합니다.
동시성과 병렬성에 대한 프로그래밍적 정의를 정리하면 다음과 같습니다.
동시성과 병렬성의 프로그래밍적 정의
- 동시성: 동시에 실행되는 것처럼 보이는 것
- 병렬성: 실제로 동시에 여러 작업이 처리되는 것
굉장히 당황스럽습니다. 동시성은 '동시에 실행되는 것처럼' 이고 오히려 병렬성이 '실제로 동시에' 라고 하네요. 이것은 CPU가 프로그램을 처리하는 것과 밀접한 관련이 있습니다.
오늘날 출시되는 대부분의 CPU는 멀티 코어를 가지고 있습니다. 개인 PC에 들어가는 CPU부터 스마트폰에 들어가는 AP까지 모두 다 멀티 코어가 보편화 돼 있습니다.
여기서 코어란 CPU의 핵심 연산 장치를 의미합니다. CPU가 처리해야 할 모든 계산과 명령어 실행을 실제로 담당하는 부분으로서, CPU의 진짜 두뇌 또는 일꾼이라고 이해하시면 좋습니다.
이름부터 'core' 라 중요한 티가 팍팍 나는군요.
우리가 열심히 작성한 코드 역시 이 CPU 코어에서 실행이 됩니다. 그런데 이 코어가 동시성과 병렬성의 개념을 이해하는데 어떤 의미가 있는 것일까요? 그것은 동시성과 병렬성이 코어의 개수와 밀접한 연관이 있다는 점에 있습니다.
동시성의 정의를 다시 보겠습니다. 핵심이 되는 문구는 ~되는 것처럼 인데요. 이것이 의미하는 바는 충격적이게도 동시성은 실제로 진짜 '동시'가 아니라는 것입니다.
이런 의미에 비추어 볼 때 동시성은 싱글 코어의 차원에서 접근해서 이해하면 좋습니다. 현대의 CPU는 멀티 코어이지만 동시성은 코어의 개수를 신경쓰지 않는 개념입니다. 극단적으로 말해서 싱글 코어 환경에서 벌어지는 작업의 동작 성질이라고 이해하시면 좋습니다.
그림을 보면 빨간색과 파란색이 계속 번갈아 가면서 실행되는 것을 확인할 수 있습니다. 이 부분을 처음에 제가 학습할 때는 배신감이 상당했습니다. 이름과 개념이 전혀 일치하지 않았기 때문입니다. 이런 부분이 이해를 더 어렵게 만드는 지점이라고 생각합니다.
마찬가지로 병렬성의 정의도 다시 보겠습니다. 핵심이 되는 문구는 실제로 입니다. 일반적으로 '동시에 작업이 실행된다' 는 느낌에 더 부합하는 것은 사실 병렬성입니다. 왜냐하면 병렬성은 멀티 코어 차원에서 접근해야 하는 개념이기 때문입니다. 즉, 싱글 코어 환경에서는 아예 병렬성이라는 개념 자체가 성립할 수 없습니다. 왜냐하면 실제로 동시에 작업이 실행되기 위해서는 최소한 2개 이상의 코어가 필요하기 때문입니다.
그림을 보면 이제서야 비로소 빨간색과 파란색이 나란히 평행하게 실행되는 것을 확인할 수 있습니다. 병렬성은 맨 처음 살펴본 단어의 정의에도 부합하고, 프로그래밍 정의에도 잘 부합하는 것 같습니다.
동시성과 병렬성의 각각의 정의에 대해서 의미를 파악했습니다. 동시성의 이름이 얼마나 큰 사기를 치고 있는지도 깨달았습니다. 이것을 다시 한 번 정리해보면 다음과 같습니다.
| 구분 | 동시성 | 병렬성 |
|---|---|---|
| 의미 | 논리적 동시 | 물리적 동시 |
| 코어 수 | 싱글 코어(또는 멀티 코어) | 멀티 코어 필수 |
| 처리 방식 | 작업 전환을 통해 빠르게 번갈아 가면서 처리 | 여러 작업을 실제로 동시에 처리 |
| 비유 | 1명의 바리스타가 주문 여러 개 번갈아 가면서 제조 | 바리스타가 2명 이상 |
그러면 이런 의문이 생길 수도 있습니다. 현대의 CPU는 멀티 코어가 일반적인데 동시성이 무슨 의미가 있는 것일까? 라는 궁금증 말이죠. 하지만 멀티 코어에서도 동시성의 개념은 여전히 중요합니다. 예를 들어 노래를 들으면서 코딩도 하고 슬랙에 있는 메시지도 확인하고 브라우저에서 검색도 하는 상황을 생각해보겠습니다.
CPU 코어가 2개인 상황을 가정해보죠. 예를 들어 저는 애플 뮤직으로 노래를 듣는데요, 그러면 CPU 코어1 에서는 애플 뮤직이 실행이 됩니다. CPU 코어2 에서는 슬랙이 실행 중입니다. 그러다가 제가 갑자기 검색할 것이 생겨서 크롬을 사용해야 상황일 때, CPU 코어1, 2는 모두 이미 작업이 실행 중이기 때문에 브라우저 사용을 할 수 없을까요? 그렇지 않습니다. CPU 코어1 이든 2든 빠르게 작업을 전환해서 크롬을 실행하면 검색도 무리없이 수행할 수 있는 것입니다.
즉, 현대의 멀티 코어 CPU 환경에서는 동시성과 병렬성 개념이 항상 같이 존재하는 것입니다. 각각의 코어 내에서는 동시성 개념에 맞게 작업들이 빠르게 전환되면서 끊김없이 작업이 동시에 실행되는 것처럼 보이게 하고, 전체 CPU 차원에서 보면 여러 개의 코어에서 나란히 다양한 프로그램이 실행이 되고 있는 것입니다.
달리기 트랙을 생각해보면 이해가 쉽습니다. 달리기 트랙은 여러 개이지만, 트랙별로는 한 번에 하나의 주자만 달릴 수 있습니다. 달리기 트랙에서 여러 명이 동시에 뛰는 것은 병렬성, 개별 트랙 내에서 주자가 계속 바뀌면서 달린다고 하면 그것이 바로 동시성인 것입니다.
이제 동기와 비동기를 알아보겠습니다. 동시성 주제를 공부할 때 항상 빼놓지 않고 세트로 나오는 개념이 바로 동기와 비동기입니다. 반응성 좋은 애플리케이션을 만들기 위해서는 이 개념을 정확히 이해하는 것이 중요합니다.
먼저 동기와 비동기의 사전적 정의를 보겠습니다.
동기(synchronous) 와 비동기(asynchronous)
- 동기(synchronous): 동시의, 동시에 일어나는, 동일한 속도로 진행하는
- 비동기(asynchronous): 비동시성의
사전적 정의를 보자마자 머리가 뜨거워집니다. 왜냐하면 프로그래밍에서 정의하는 동기와 비동기 개념과 정반대 되는 것 같이 느껴지기 때문입니다.
프로그래밍에서 동기는 작업을 순차적으로 실행하는 방식을 의미하고, 비동기는 여러 작업을 동시에 처리할 수 있는 방식을 의미하는데요. 단어의 의미가 정반대라고 느껴지지 않나요? 또 저만 그런가요?
프로그래밍에서 의미하는 동기를 조금 더 이해하기 쉽게 해석을 해보자면 이렇게 할 수 있을 것 같습니다. → '작업을 요청하는 쪽과 그 결과를 받는 쪽의 시간이 함께 묶여있다.'
예시를 한 번 들어보겠습니다. 작업 A와 B가 있습니다. 동기식으로 동작한다면 이렇게 동작할 것입니다.
- A가 B에게 작업을 요청합니다.
- A는 요청을 하고 B가 일을 마칠 때까지 잠깐 쉽니다.
- B가 일을 마치고 A에게 결과를 주면, 그제서야 A는 다시 다음 일을 시작합니다.
이 과정에서 A의 시간과 B의 시간은 서로 맞물려(in sync) 있습니다. A의 시간은 B의 작업이 끝날 때까지 B의 시간에 종속적입니다. 마치 전화 통화를 하는 것이라고 이해하면 좋습니다. 전화를 걸면 상대방과 제 시간은 하나로 묶이게 됩니다. 통화가 끝날 때까지는 다른 사람과 전화할 수도 없고, 상대방의 말에 바로 대답을 해야 합니다.
즉, 동기와 비동기를 조금 더 프로그래밍스럽게 이해를 하기 위해서는 작업이 몇 개가 실행되고 있는지에 집중할 것이 아니라 여러 개의 작업의 요청과 결과가 하나의 흐름으로 묶여 있는지에 초점을 두는 것이 좋습니다.
비동기는 동기와 정반대되는 개념이기 때문에 동기의 관점을 반대로 뒤집어서 생각하면 되겠죠? 즉 ''작업을 요청하는 쪽과 그 결과를 받는 쪽의 시간이 분리되어 있다'. 이렇게 이해하면 좋습니다.
마찬가지로 예시를 들어보겠습니다.
- A가 B에게 작업을 요청합니다.
- A는 B에게 요청을 한 후에 B가 일을 시작하든지 말든지 신경쓰지 않고 자기 할 일을 이어서 합니다.
- B가 일을 시작하고 완료되면 A에게 본인이 일이 끝났다고 알리고 결과를 넘겨줍니다.
- A는 받은 결과를 받아 그 결과가 필요한 일을 마저 수행합니다.
이 과정에서 A의 시간과 B의 시간은 서로 전혀 다르게 흐르고 있습니다. A가 요청을 B에게 보내고 B의 시간에 종속되지 않고 A는 자기의 시간에 맞게 계속해서 일을 수행합니다. 극단적으로 B가 일을 안 해도 A는 묵묵히 자기 일을 합니다. 흔히 비동기는 '여러 개의 작업이 동시에 실행되고 있다' 라고 생각하면서 여러 작업이 함께 실행되는 개념으로 이해하고는 하는데요. 아주 틀린 이해는 아니지만, 좀 더 자연스럽게 이해를 하기 위해서는 이렇게 이해하기 보다는 '서로 각자 알아서 일을 처리하고 일 끝나면 연락줘' 라고 이해하면 좋을 것 같습니다.
동시성과 비동기는 자주 함께 등장하는 개념이라고 앞서 언급했는데요. 도대체 무슨 관계가 있길래 둘은 항상 세트로 다뤄지는 개념인 것일까요? 이렇게 한 마디로 정리할 수 있을 것 같습니다.
"여러 작업을 어떻게 효과적으로 다룰 것인가에 대한 목표로서의 동시성, 동시성을 이루기 위한 프로그래밍적 기법(수단)으로서의 비동기"
즉, 비동기라는 프로그래밍 기법으로서 우리는 CPU에게 동시성을 만족시키며 실행시키도록 명령을 내릴 수 있는 것입니다.
그렇다면 비동기는 동시성과만 관계가 있는 개념일까요? 그렇지 않습니다. 비동기는 병렬성과도 함께 묶일 수 있습니다. 이해를 돕기 위해 시나리오를 두 가지로 나눠서 생각해보겠습니다.
- 싱글 코어 환경
싱글 코어 환경이기 때문에 병렬성은 존재할 수 없습니다. 바리스타가 한 명 밖에 없는 상황인데, 커피도 만들고 주문도 받는 등 여러 작업을 효율적으로 전환하며 동시성을 만족하고 있는 상황입니다.
- 멀티 코어 환경
멀티 코어 환경이기 때문에 병렬성을 달성할 수 있습니다. 바리스타가 여러 명 있는 상황이기 때문에 A 바리스타는 주문만 받고, B 바리스타는 커피만 만드는 등 서로 다른 작업을 병렬적으로 수행할 수 있는 것입니다. 물론 각 바리스타는 자신이 하는 작업을 계속해서 전환하며 비동기적으로 일할 수 있기 때문에 동시성도 달성할 수 있습니다.
동기/비동기의 개념을 살펴봤습니다. 단어의 의미와 프로그래밍에서 정의한 개념이 달라 여기서도 또 한 번 배신감을 느끼기도 했습니다. 동기/비동기 개념을 이해할 때 제일 핵심이 되는 것은 실행되고 있는 작업의 개수가 아닙니다. 각 작업이 서로 요청과 결과의 흐름에서 묶여있는가 혹은 서로 신경 쓰지 않고 각자 알아서 일을 하는 것인가 입니다.
그리고 비동기는 구체적인 프로그래밍 기법이고 동시성은 비동기를 통해 달성하고자 하는 목표라는 것도 이해했습니다. 이제는 왜 동시성과 비동기가 함께 다뤄지는 주제인지 당당히 말할 수 있습니다. 바로 둘은 수단과 목표로 묶여 더 우수한 애플리케이션을 만들기 위해 함께 하는 관계였던 것입니다.
지금까지 동시성/병렬성, 동기/비동기 등의 개념에 대해서 학습했습니다. 각각의 개념들을 이해하는 것만으로도 이전에 막혔던 것보다 충분히 더 많은 이해를 달성했지만, 조금 더 나아가고 싶었습니다. 그래서 지금까지 배운 개념들과 스레드를 연결지어 생각해보고자 합니다.
스레드와 동시성/병렬성의 개념을 연결하기 앞서 프로세스와 스레드를 정말 간단하게 정리하고 넘어가면 다음과 같습니다.
| 구분 | 프로세스 | 스레드 |
|---|---|---|
| 개념 | 운영체제에서 실행 중인 하나의 애플리케이션 | 프로세스 내에서 실행되는 실제 작업의 흐름 단위 |
| 메모리 | 자신만의 독립된 메모리 공간(코드, 데이터, 힙)을 가짐 | 같은 프로세스의 스레드들끼리는 메모리를 공유 |
멀티 스레드 환경이란 스레드를 2개 이상을 동시에 실행하는 환경을 의미합니다. 즉, 하나의 애플리케이션이 여러 개의 작업 흐름을 가지고 동시에 여러 일을 처리하는 것입니다. 그렇다면 왜 멀티 스레드 환경을 사용할까요? 이유는 성능과 밀접한 관련이 있습니다.
- 응답성 향상: 시간이 오래 걸리는 작업을 별도의 스레드에 할당함으로써 메인 스레드는 계속해서 작업을 이어나갈 수 있습니다.
- 자원 효율성: 스레드는 하나의 프로세스 내에서 메모리를 공유하기 때문에 여러 작업을 위해 매번 새로운 프로세스를 생성하는 것보다 비용이 저렴합니다.
- 멀티 코어 활용: 각 코어가 다른 스레드를 병렬적으로 실행시킴으로써 작업 속도를 극대화 할 수 있습니다.
이처럼 스레드는 동시성, 병렬성 등과 떨어져서 생각할 수 없는 밀접한 개념입니다.
동시성/병렬성, 동기/비동기는 모두 작업을 어떻게 처리하느냐에 관한 관점의 개념입니다. 그리고 모든 작업은 스레드에서 실행됩니다. 즉, 스레드가 코어에 올라가는 단위라는 것입니다. 그렇기 때문에 지금까지 학습한 개념과 스레드를 연결 지어 생각해보는 것도 좋은 시도인 것 같습니다.
그렇다면 동시성과 병렬성을 이렇게도 정의해볼 수 있을 것 같습니다.
- 동시성: 싱글 코어가 여러 스레드를 빠르게 전환하며 실행
- 병렬성: 멀티 코어가 각기 다른 스레드를 나란히 실행
이전에 정리한 동시성/병렬성 개념에서 '작업' 이라는 단어가 '스레드' 로 변했을 뿐 큰 차이가 없습니다. 스레드가 작업의 단위라는 정의에 비추어 볼 때 이것은 자연스럽습니다. 동시성과 병렬성 개념을 스레드와 연결하는 것은 생각보다 어렵지 않았네요.
그렇다면 스레드를 동기/비동기 개념과는 어떻게 연결을 지어야 할까요? 먼저 '동기'와 스레드를 연결하는 것을 보겠습니다. 이것을 이해하기 위해서는 스레드들이 메모리를 공유하기 때문에 발생하는 문제를 이해해야 합니다.
앞서 정리한 것처럼 하나의 프로세스 내에서 생성된 스레드는 메모리를 공유하는데요. 이로 인해 문제가 발생합니다. 바로 경쟁 상태(Race Condition) 문제입니다. 메모리를 공유하는 스레드가 공유 자원(데이터)에 접근하여 데이터를 수정하려 할 때 데이터가 오염되는 상황이 발생합니다.
위의 경쟁 상태 문제를 해결하기 위해 동기 개념이 필요합니다. 여러 스레드가 한 번에 공유 자원에 접근하여 데이터를 접근하는 것을 막기 위해 동기 기법이 필요합니다. 공유 자원에 접근하는 코드 영역을 '임계 구역' 이라고 하는데요. 임계 구역에 진입하는 스레드를 하나로 지정할 때 동기 방식을 사용합니다.
공유자원에 접근하려는 스레드는 임계 구역에 접근할 때 락(Lock)을 획득해야 합니다. 한 스레드가 임계 구역에 들어가기 위해 이 락을 획득했다면 다른 스레드들은 그 스레드가 나와서 락을 반환할 때까지 꼼짝없이 기다리고 있어야만 합니다. 이 기다리는 행위가 특정 스레드의 작업 흐름이 다른 스레드의 작업 흐름에 종속되는 동기화인 것입니다.
그렇다면 비동기는 어떻게 연결 지을 수 있을까요? 비동기 방식으로 작업을 처리할 때는 주로 응답 효율성을 높일 때입니다. 예를 들어 메인 스레드가 시간이 아주 오래 걸리는 작업을 처리해야 되는 순간이 왔다고 해보겠습니다. 파일 다운로드, 네트워크 요청 같은 것들 말이죠. 이 작업을 메인 스레드가 처리한다면 해당 작업을 처리하는 동안 다른 작업을 처리할 수 없으니 프로그램 전체가 멈추게 됩니다.
따라서 이 작업을 별도의 작업자 스레드(Worker Thread)에게 위임하고 끝나면 연락을 달라고 합니다. 그러고선 메인 스레드는 연락을 기다리지 않고 일단 바로 다른 일을 계속해서 처리합니다. 이처럼 메인 스레드와 별도의 워커 스레드가 서로의 작업 시간 흐름은 생각하지 않고 각자 할 일 열심히 하는 것이 대표적인 비동기 방식으로 스레드를 활용하는 것입니다.
그렇다면 여기서 이런 의문이 들 수 있습니다. 비동기는 멀티 스레드에서만 가능한 것인가? 라는 의문 말이죠. 정답부터 말하자면 그렇지 않습니다. 싱글 스레드에서도 얼마든지 비동기 방식으로 동작하게 할 수 있습니다. 이것을 가능하게 하는 대표적인 것들이 코틀린의 '코루틴', 자바스크립트의 '이벤트 루프' 등이 있는 것입니다. 그렇다면 이 문장도 성립을 할 수 있겠네요.
싱글 코어, 싱글 스레드 환경에서도 비동기 방식으로 동작하게 할 수 있다.
그리고 이 문장을 더 확장해서 이해하면 싱글 코어이기 때문에 비록 병렬성은 달성할 수 없겠지만, 싱글 스레드 내에서 코루틴을 활용해 서로 다른 코루틴이 다른 코루틴의 작업의 시간의 흐름을 의식하지 않고 비동기적으로 작업한다면 곧 동시성을 달성했다고도 할 수 있겠네요.
지금까지 스레드와 동시성/병렬성, 동기/비동기 개념을 연결해보는 과정을 정리해봤습니다.
- 작업은 스레드에서 실행된다. → 스레드는 작업의 단위이다.
- 동기/비동기는 프로그램의 작업을 효율적으로 처리하게 하기 위한 프로그래밍 기법(수단)이다.
- 동시성/병렬성 등은 프로그램의 작업을 효율적으로 처리하기 위한 구조(목표)이다.
이런 각각의 정의에 비추어 볼 때 세 가지 개념은 모두 유기적으로 연결되어 있다는 것을 확인할 수 있습니다.
동시성과 관련된 주제는 언제나 저룰 너무 힘들게 하는 개념이었습니다. 개념들이 다 비슷한 거 같고 너무 힘들어서 공부를 해도 내가 완전히 이해했다고 할 수 있을까 하는 확신이 서지 않았거든요. 그렇지만 이번 부스트캠프 챌린지 과정을 통해 이 개념에 대해 한층 더 깊은 이해를 할 수 있었던 것 같아서 기분이 너무 좋습니다.
누군가는 이 개념이 단번에 쉽게 이해가 됐을 수도 있습니다. 그런 분들에게는 오히려 이 글이 큰 의미가 없고 오히려 오류가 많은 글로 받아들여질 수도 있겠네요🥺. 하지만 저처럼 이 개념이 계속 헷갈리고 어렵게 느껴지는 분이 있다면 제가 공부한 이 내용이 조금이나마 도움이 되었으면 합니다.
감사합니다