이 글은 한 권으로 읽는 컴퓨터 구조와 프로그래밍을 읽고 정리한 내용입니다.
멀티코어는 전혀 새로운 것이 아니며, 결국 병렬 컴퓨터 구조의 새로운 제조 형태일 뿐입니다.
과거에는 독립적으로 있었던 프로세서들이 반도체 기술 덕택에 하나의 칩에 제조될 수 있으니, 이것이 바로 멀티코어 프로세서
의 탄생입니다. 따라서 병렬 컴퓨터 구조 자체를 먼저 살펴볼 필요가 있습니다.
여러 개의 **계산 장치(PE)**가 협력해 큰 문제를 더 빠르게 푸는 시스템
유형 | 특징 | 예시 |
---|---|---|
강력한 소수 코어 | 코어 수는 적지만 성능이 강력 | 데스크탑 CPU (듀얼/쿼드코어) |
작지만 다수 코어 | 단순한 코어를 수백~수천 개 | GPU |
이종 결합 | 성능 코어 + 효율 코어 | CPU + GPU 조합 (예: 스마트폰 SoC) |
현재 데스크탑에 쓰이는 듀얼/쿼드 코어는 수는 적지만 매우 강력한 코어를 모은 것입니다. 이 반대로 작지만 그 수가 많은 병렬 구조도 있어요.
대표적인 예가 바로 GPU인데, GPU
는 간단한 구조의 작은 프로세서를 수백 개의 단위로 모아놓은 것이에요.
또한 어느 정도 강력한 코어 몇 개와 작지만 많은 코어를 섞어 놓을 수도 있습니다다. 혹은 전혀 다른 프로세서, 예를 들어 CPU와 GPU를 모아 멀티코어를 만들 수 있습니다.
서로 데이터를 주고받는 방식에 따라 구조가 달라집니다.
유형 | 설명 |
---|---|
SISD | 일반적인 단일 코어 |
SIMD | 하나의 명령으로 여러 데이터 처리 (예: GPU 벡터 연산) |
MISD | 여러 명령, 같은 데이터 처리 (거의 안 씀) |
MIMD | 각각의 명령과 데이터를 병렬 처리 (멀티코어 CPU) |
수준 | 설명 |
---|---|
ILP (명령어 수준) | 싱글코어에서 내부 명령을 병렬 실행 |
TLP (스레드 수준) | 여러 스레드를 동시에 실행 |
➕ 루프 수준 / 태스크 수준 / 프로그램 수준 병렬성도 존재 |
멀티코어가 병렬 컴퓨터 구조의 실현이라면, 그 구체적인 구현 방식은 메모리 구조와 크게 관련이 있습니다.
즉, 여러 계산 장치가 같은 메모리를 바라보느냐, 아니면 각자 고유의 메모리를 가지느냐에 따라 병렬 시스템의 동작 방식이 달라집니다.
우리가 사용하는 대부분의 멀티코어 컴퓨터가 여기에 해당합니다. 여러 프로세서(코어)가 같은 주소 공간의 메모리를 공유합니다.
이 구조에서는 각 코어가 서로 동일한 메모리 공간에 접근할 수 있기 때문에 멀티스레드 프로그래밍이 자연스럽습니다.
또한 이 공유 메모리 구조는 다음과 같이 세부 구현 방식에 따라 나뉘게 됩니다:
✅ NUMA는 확장성과 성능을 동시에 고려한 구조로, 멀티코어 최적화 시 반드시 고려해야 하는 아키텍처입니다.
각 프로세서가 독립적인 메모리를 갖고 있으며, 서로 데이터를 주고받기 위해선 반드시 명시적 메시지 교환이 필요합니다.
이 구조는 흔히 클러스터, 그리드 컴퓨팅, 슈퍼컴퓨터에서 사용됩니다.
수천~수만 개의 노드로 구성되며, 프로그래밍 방법도 완전히 달라집니다.
이처럼 병렬 메모리 구조는 멀티코어 프로세서가 어떤 방식으로 데이터를 공유하고 처리할지 결정하는 핵심 요소입니다.
멀티코어는 단순히 코어가 여러 개라는 의미를 넘어서, 그 내부에서 어떤 구조로 연결되고, 어떤 방식으로 협력하는가에 따라 성능과 동작 방식이 크게 달라집니다.
이처럼 병렬 컴퓨터는 다양한 방식으로 계산 장치를 구성하고 협력하게 만들어 더 빠른 처리를 가능하게 해줍니다.
그리고 이 병렬 구조의 핵심 개념은, 이제 하나의 칩에 여러 코어를 집적한 멀티코어 프로세서라는 형태로 우리 일상 속 컴퓨터나 스마트폰 안에 구현되고 있죠.
그렇다면, 멀티코어는 구체적으로 어떻게 동작하고, 어떤 병렬 처리를 가능하게 하는 걸까요? 이제부터 멀티코어 시스템 내부를 좀 더 깊이 들여다보겠습니다.
불과 몇십 년 전까지만 해도, 컴퓨터는 사실상 멀티태스킹의 흉내만 내고 있었습니다. CPU는 단 하나였고, 여러 프로그램을 조금씩 번갈아 실행하며 동시성처럼 보이게 만들었죠.
하지만 이제는 다릅니다. 여러분이 쓰는 스마트폰, 노트북, 심지어 냉장고조차도 멀티코어 시스템입니다.
멀티프로세싱(Multiprocessing)은 더 이상 슈퍼컴퓨터의 전유물이 아닙니다.
여러분의 손안에서 이미 ‘진짜 동시 실행’이 일어나고 있습니다.
물론입니다. 아래는 해당 글의 내용을 핵심적으로 정리한 내용입니다. 멀티코어와 멀티 프로세서의 차이, 장단점, 그리고 멀티 프로세서를 활용한 시스템 구조까지 정리했습니다.
항목 | 멀티코어(Multi-Core) | 멀티프로세서(Multi-Processor) |
---|---|---|
구조 | 하나의 CPU 안에 여러 개의 코어 탑재 | CPU 자체가 여러 개 존재 (물리적으로 분리된 CPU들) |
속도 | 코어 간 데이터 공유가 빠름 (같은 칩 내부 공유 캐시 활용) | CPU 간 통신은 느림 (버스 공유, 메모리 접근 경합 발생) |
전력 효율 | 전력 소비가 적고 열 방출도 적음 | CPU가 많아질수록 전력 소비와 발열 증가 |
일관성 문제 | 코어 간 캐시 일관성 유지가 상대적으로 쉬움 | 각 CPU의 캐시 일관성 문제 발생 가능 |
확장성 | 물리적 제약이 있으므로 확장 한계 존재 | CPU를 더 추가하면 병렬 성능을 계속 확장할 수 있음 |
사용 예 | 일반 데스크탑, 스마트폰, 노트북 등 | 서버, 고가용성 시스템, 클러스터, 클라우드 인프라 등 |
하나의 CPU 내부에 두 개 이상의 독립적인 core가 있는 기술.
여러 개의 Processor(=CPU)를 사용하는 것.
최근 멀티 프로세서의 시스템이 각광받고 있는데, 그 이름은 바로 클라우드
입니다.
클라우드의 정의란, 여러 개의 분산 시스템을 하나의 자원으로 묶어서, 컴퓨팅 자원을 활용하는 방식입니다.
하나의 컴퓨터 내의 멀티 프로세서를 사용하는 경우는 여러 가지 단점들이 존재하지만, 분산된 시스템을 멀티 프로세서 개념으로 묶어서 처리한다면, 멀티 프로세서의 단점은 없어지고 장점이 더 돋보이게 됩니다.
출처 : 멀티 프로세서와 멀티 코어의 차이점
여러 독립 컴퓨터를 LAN으로 연결하고, 마치 하나의 컴퓨터처럼 동작
고가용성(HA) 제공 목적 → 장애 대비 구조 구성 가능
종류
용어 | 설명 |
---|---|
Fail-over | 한 시스템이 실패할 경우, 다른 시스템이 즉시 이를 대체 |
Graceful Degradation | 일부 시스템 장애에도 전체 서비스가 완전히 중단되지 않도록 함 |
Fault Tolerant | 장애 발생 시에도 시스템이 정상 작동하도록 설계 |
SAN(Storage Area Network) | 고속 네트워크로 스토리지를 클러스터링하여 공유 |
멀티코어와 멀티프로세서는 하드웨어의 병렬성을 실현하는 방식이며, 각각 장단점과 활용처가 다릅니다.
클라우드 시대에서는 이 개념들이 분산 시스템, 고가용성 구조와 결합되어 더욱 확장된 형태로 사용됩니다.
기본 구조의 차이를 이해하면, 클라우드, 고가용성, 병렬 컴퓨팅 등 다양한 시스템 아키텍처를 더 잘 설계하고 이해할 수 있습니다.
멀티코어가 멋지기만 한 건 아닙니다. 처리 순서가 중요한 작업에서는 '동시 실행'이 오히려 독이 되기도 하죠.
💥 만약 동시에 인출을 허용한다면?
이 문제는 바로 **Race Condition (경합 조건)**이라고 합니다.
둘 이상의 연산이 같은 자원을 동시에 사용하려 할 때, 실행 순서에 따라 결과가 달라지는 오류죠.
옛날 옛적, CPU 나라에는 혼자 사는 CPU 씨가 있었습니다.
그는 매우 착하고 성실한 친구였지만, 한 번에 단 한 손님만 집에 들일 수 있었어요.
예를 들어, A라는 손님이 집에 와서 요리를 하다가, **식재료가 다 떨어져 시장(I/O)**에 가버리면…
CPU 씨는 빈 집에서 멍하니 기다려야 했죠.
"이거 너무 비효율적인데… 누가 시장 갔는지 알면서도 멍하니 기다리는 나…"
이것이 바로 단일 프로세스 시스템입니다.
어느 날, CPU 씨는 친구의 조언을 듣고 이렇게 말했어요.
“그래! 집에 여러 명을 들여놓고 빈 시간 없이 번갈아 일 시키면 되잖아!”
그는 이제 하숙생(A, B, C…)을 여러 명 들였습니다.
A가 요리하다 식재료 사러 가면, 곧바로 B가 청소를 시작합니다.
B가 빨래를 하러 나가면, 다시 돌아온 A가 일을 이어가는 식이죠.
이것이 바로 멀티 프로그래밍(Multi Programming입니다.
하지만 문제가 하나 있었죠…
A라는 하숙생이 말이 너무 많아 요리를 엄청 오래하면?
“내 차례는 도대체 언제 오는 거야!” – B의 분노 😡
아무래도 CPU의 처리속도가 빠르다보니 번갈아가면서 처리하는 것이 우리 눈에는 "동시에" 처리되는 것으로 보이지만 실제로 "동시에"는 아니다!
그래서 CPU 씨는 큰 결심을 했습니다.
모든 하숙생에게 공정한 시간표를 나눠주기로요.
“너희 모두 1분씩만 써! 시간 되면 바로 바꾸자!”
이제 A가 요리하다 1분이 지나면 B가 청소, C가 세탁기 돌리기를 하며
작업이 교대로, 빠르게 이어졌죠.
이 방식이 바로 멀티 태스킹(Multi Tasking)입니다.
시간이 흐르고, CPU 씨는 쌍둥이 형제들과 함께 살기로 합니다.
A CPU, B CPU, C CPU…
이제는 A가 요리하고, B가 청소하고, C가 빨래를 동시에!
이것이 바로 **멀티 프로세싱(Multi Processing)**입니다.
현대의 컴퓨터는 이 둘을 동시에 사용합니다.
각 CPU는 멀티 태스킹을 수행하고,
여러 CPU는 멀티 프로세싱으로 병렬 작동!
방식 | 구성 | 설명 |
---|---|---|
멀티 태스킹 | 1 CPU + 여러 작업 | 시간 분할 처리 |
멀티 프로세싱 | 여러 CPU | 병렬 작업 처리 |
둘의 조합 | 여러 CPU + 각 CPU가 멀티 태스킹 | 동시에 여러 작업 + 빠른 응답성 |
여기서 헷갈리기 쉬운 개념!
멀티 프로세싱 vs 멀티 프로세스
구분 | 의미 | 목적 | 예시 |
---|---|---|---|
멀티 프로세싱 | 여러 CPU 사용 | 빠른 병렬 처리 | 4코어 CPU |
멀티 프로세스 | 하나의 프로그램이 여러 프로세스로 구성됨 | 안정성, 격리성 확보 | 크롬 브라우저 (탭마다 프로세스 분리) |
즉, 하나는 하드웨어가 멀티이고,
다른 하나는 프로그램(소프트웨어)이 멀티입니다.
용어 | 핵심 개념 | 특징 |
---|---|---|
단일 프로세스 | 하나의 프로그램만 실행 | I/O 동안 CPU는 멈춤 |
멀티 프로그래밍 | 여러 프로그램을 메모리에 올림 | I/O 대기 중 다른 작업 실행 |
멀티 태스킹 | 작업 시간 쪼개서 번갈아 처리 | 문맥 교환 필요, 응답성 향상 |
멀티 프로세싱 | 여러 CPU 코어가 병렬 처리 | 성능 향상, 진짜 동시에 실행 |
멀티 프로세스 | 프로그램이 여러 프로세스로 나뉨 | 안정성 향상, 예: 웹 브라우저 |
위의 경우, 하나의 주문에 하나의 작업대를 무조건 만들어야 합니다. 즉, 100개의 햄버거를 만들기 위해서는 100개의 작업대가 필요하다는 것이죠..
이번 단체 주문은 햄버거 100개를 한꺼번에 주문했네요. 그런데 각 요리마다 작업대를 새로 깔려고 하는게 여간 시간이 오래 걸리는 일이 아니었습니다. 100개의 작업대를 순서대로 깔다가 지친 커넬은 다시 고민에 빠지게 됩니다.
“어차피 레시피랑 재료는 같은데, 한 작업대에서 같은 요리를 여러 개 만들 수 있지 않을까?”
비슷한 작업을 반복하는 거면 굳이 새 작업대를 깔 필요도 없었고, 한 작업대에서 음식을 조리하니 시간도 절약되었죠.
뭔가 깨달은 커널은 커널은 새 요리사를 한 명 더 고용했습니다. 이제 주방에는 두 명의 요리사가 있습니다. 커널은 두 요리사에게 하나의 작업대만 갖다주며 이야기합니다.
“햄버거를 100개 만들어야 하는데, 어차피 레시피랑 재료가 같잖아? 작업대를 100개 깔면 시간이 오래 걸리니까 이번에는 한 작업대에서 각각 50개씩 햄버거를 만들도록 해.”
이 명령을 들은 CPU들은 요리해야 할 양을 절반씩 나누어 만들기 시작합니다. 뭐, 커널의 말대로 레시피랑 재료가 같으니 그저 각자 조리 중인 햄버거가 섞이지 않게만 조심하면 문제가 없을 것 같네요. 친절한 커널은 CPU를 배려해서, 하나의 작업대에서 여러 요리를 만들 때에도 주문표에 각 요리사들이 어디까지 조리를 했는지를 적어줍니다
쓰레드(Thread) 는 프로세스 내에서 실행되는 CPU 스케줄링의 기본 단위이다. 하나의 프로세스 내에서 여러 개의 쓰레드가 실행될 수 있으며 이를 멀티 쓰레드 라고 부른다.
각 쓰레드는 자기 자신만의 실행 컨텍스트를 가질 수 있기 때문에, 서로 다른 CPU에서도 동작할 수 있다. 이 덕분에 여러 CPU를 단일 회로로 통합한 멀티 코어 프로세서를 이용하면 물리적으로 병렬 처리가 가능하다.
기본적으로 하나의 프로세스 내에서 실행되는 각 쓰레드는 프로세스의 메모리를 공유한다. 하지만 쓰레드 별로 실행 중인 코드 위치와 컨텍스트가 다를 수 있기 때문에, 프로그램 카운터는 따로 저장하고 스택은 분할해서 사용한다.
각 쓰레드의 정보를 저장하기 위해 커널은 쓰레드 제어 블록(TCB, Thread Control Block) 을 만들어 PC 메모리에 저장한다.
이런 상황을 막기 위해, 우리는 ‘락(Lock)’이라는 개념을 도입합니다.
계좌 인출 코드처럼 서로 영향을 주는 연산은 한 번에 단 한 사람만 접근할 수 있게 막는 것이죠.
🔒 한 사람의 인출이 끝나기 전까지, 다른 사람은 잠깁니다.
→ 이것이 동기화(synchronization)의 핵심입니다.
항목 | 설명 |
---|---|
멀티태스킹 | 여러 작업을 동시에 처리 (진짜 동시 또는 번갈아 실행) |
멀티프로세싱 | 실제로 여러 CPU/코어가 동시에 실행 |
경합 조건 | 여러 작업이 공유 자원에 동시에 접근해 실행 순서에 따라 결과가 달라지는 현상 |
락(Lock) | 공유 자원에 한 번에 한 작업만 접근하도록 막는 장치 |
문제점 | 락이 너무 많아지면 → 속도 느려짐, 락이 없으면 → 에러 발생 |
“멀티코어는 강력하지만, 우리가 신중하지 않다면 서로 충돌하고 말 것이다.”
– 병렬성의 이득은 동기화의 대가를 감수할 준비가 될 때 비로소 온전히 누릴 수 있습니다.
읽어보면 좋은 글 : 레스토랑에 비교해서 알아보는 운영체제
프로세스는 독립된 메모리 공간을 사용하지만,
공유 자원을 사용하려면 반드시 통신(IPC) 또는 공유 메모리 등으로 연결되어야 함.
병렬 실행이 무조건 경합 조건을 만드는 건 아님.
자원을 공유해야만 경합이 발생함.
서로 다른 프로세스가 자원을 공유하려면 명시적으로 설정되어야 함.
대표적인 자원 공유 예:
프로세스 안에서 여러 작업을 병렬로 처리할 수 있게 해주는 실행 단위
스레드는 다음을 공유함:
하지만 스택과 **레지스터(문맥 상태)**는 각 스레드가 별도로 소유
📌 그래서 스레드는 하나의 프로그램 안에서 자체 흐름을 갖는 독립 실행 경로입니다.
스레드? 프로세스 안에 여러 작업자를 만들어서 동시에 작업하자는 것이에요.
1명이 3가지 역할을 가지고 있기보단, 3명이 1가지 역할을 맡는 것처럼 스레드
는 여러 작업 중에 하나를 맡아 할 사람을 추가하는 것입니다.
기존의 프로세스 개념만 있었을때는
프로세스
자체가 작업을 처리하는 단위였지만, 이제 프로세스는 스레드를 감싸는 "컨테이너"의 개념이 되었고
스레드가 작업을 처리하는 단위로 바뀌었습니다.
항목 | 프로세스 | 스레드 |
---|---|---|
메모리 공간 | 독립적 | 같은 프로세스 내 공유 |
문맥 전환 비용 | 높음 | 낮음 (경량 프로세스) |
사용 예 | 독립 실행 프로그램 | 병렬 처리, 핸들러 분리 |
우리(개발자)가 실제로 처리해야 할 문제는 공유 자원이 아닌, 실제로 처리해야 할 문제는 여러 작은 연산으로 이뤄진 작업을 어떻게 원자적(Atomic) 으로 만들 수 있을까 하는 문제를 다뤄야 한다고 합니다.
저자는 컴퓨터에 '은행 잔고를 조정하라' 와 같은 명령어가 있다면 위와 같은 문제를 논할 필요도 없을 것이라고 합니다.
중간에 나눌 수 없고 인터럽트되지 않는 연산 블록
예:읽기 → 계산 → 쓰기
가 중간에 끊기면 잘못된 결과 발생
구분 | 설명 |
---|---|
공유 자원 문제 | 여러 스레드/프로세스가 같은 데이터(메모리, 프린터 등)를 접근함 |
원자성 문제 | 연산이 중간에 끊기지 않고 한 덩어리로 실행돼야 하는데, 그렇지 못해서 문제가 생김 |
“공유 자원만 조심하면 돼!” → ❌ 틀린 말
“공유 자원을 어떻게 끊기지 않게 다룰까?” → ✅ 정확한 고민
상황:
잔고를 읽음 → 100만 원
+ 10만 원 → 110만 원
잔고에 씀 → 110만 원
잔고를 읽음 → 100만 원
+ 50만 원 → 150만 원
잔고에 씀 → 150만 원
이 두 연산이 동시에 실행되면?
A와 B가 동시에 읽음 → 둘 다 100만 원으로 읽음
A가 먼저 씀 → 110만 원 됨
B가 나중에 씀 → 150만 원 됨 (A의 결과 덮어씀)
❌ 결과: 입금 60만 원인데, 실제로는 50만 원만 반영됨
이 세 단계를 중간에 끊을 수 없게 해야 해요.
그게 바로 원자적(Atomic) 연산이라는 거예요.
“우리 개발자가 처리해야 할 진짜 문제는
‘데이터 공유 자체’가 아니라
그 데이터를 다루는 연산 블록이 원자적이지 않다는 것이다.”
잔고를 조정하라
는 명령어를 하나로 실행할 수 없음읽기 → 계산 → 쓰기
3단계를 중간에 끊기지 않게 묶어줘야 함방법 | 설명 |
---|---|
뮤텍스/락 | 연산 전체에 락을 걸어, 중간에 끼어들지 못하게 함 |
트랜잭션 | 여러 연산을 묶어서 모두 성공 or 모두 실패하게 만듦 |
원자적 명령어 (CAS, Test-and-Set) | 하드웨어 차원에서 연산 자체를 원자적으로 처리 |
C언어 atomic 키워드 | 컴파일러 수준에서 원자성 제공 |
공유 자원보다 무서운 건, 그 자원을 다루는 연산이 중간에 끊긴다는 사실이다.
그래서 우리는 데이터를 보호하는 게 아니라,
"연산 전체 흐름"이 끊기지 않게 보호해야 한다는 겁니다. 💡
명령어를 만들고 처리하기 위해 우리는 코드에 중요한 부분을 상호 배제(mutual exculusion)
메커니즘을 통해 원자적으로 처리하게 만듭니다.
이런 목표의 프로그램을 만들면서 충돌을 피하기 위해 어드바이저리 락(advisory lock)
을 만든듭니다.
결국 우리는 요구사항들에 필요한 추상화된 원자 단위의 명령어로 만들어요.
이 때 데이터의 무결성을 유지하고, 충돌을 방지하기 위해 락(Lock)
을 사용하게 됩니다.
위쪽 프로그램이 락을 먼저 얻었다. 따라서 아래쪽 프로그램은 락이 해제될 때까지 기다려야 합니다.
📌 락의 위치가 중요하다
예: 은행이 락을 지키게 만들면 신뢰 가능 (그림 12-5)
💡 트랜잭션 단위로 락을 최소화해야 동시성이 좋아짐
락 크기 = 작을수록 좋다 (fine-grained)
종류 | 설명 |
---|---|
Fine-grained Lock | 작은 영역만 잠금 (예: 한 계좌) |
Coarse-grained Lock | 큰 범위 잠금 (예: 전체 은행) |
스핀 락(Spin Lock) | 락이 열릴 때까지 계속 시도 (busy wait) |
블로킹 락 | 락 얻을 때까지 프로그램 일시 중단 |
논블로킹 락 | 락 못 얻으면 다른 일 하다가 재시도 |
이런 상황을 교착 상태deadlock라고 합니다.
이 상황은 두 악당이 서로 상대방의 머리에 권총을 겨누고 있는 상황과 같습니다.
✅ 이 중 하나라도 깨면 교착 상태는 발생하지 않음
전략 | 예시 |
---|---|
공유 자원으로 바꾸기 | 읽기 전용 자원은 락 없이 사용 |
자원 한번에 요청 | 모든 자원을 동시에 요구 |
선점 허용 | 타이머 초과 시 락 회수 |
자원 순서 고정 | 자원마다 우선순위 지정 |
명령어 | 기능 |
---|---|
Test and Set | 값이 0이면 1로 바꾸고 락 성공, 아니면 실패 |
Compare and Swap | 값이 기대값이면 새로운 값으로 교체 |
✅ 원자적(Atomic) 명령어로, CPU가 중간에 끊지 않도록 보장
이 명령어들은 대부분 시스템 모드에서만 실행 가능
→ 사용자 영역에선 고수준 API와 라이브러리 사용 (예: pthread_mutex_lock()
)
문서 편집 프로그램 등에서 긴 시간 동안 자원 독점 필요
파일 락(File Lock)을 사용해 구현
단순히 "공유 자원"을 막는 것이 아니라
"중요한 연산은 원자적으로 만들어야 한다"
회사에서 이전에 쓰던 api를 재사용 했는데 일부 사용자만 쿠폰 발급이 안되는 현상이 있었습니다.
모니터링을 확인해보니 트랜잭션이 select => update => select을 해오는 구조였는데...일부 사용자에서는 update는 잘 되나 commit이 되기전 select를 하면서 문제가 생긴 것 그 때 코드를 일부 보면 아래와 같습니다.
if (조건문) {
Map<String, Object> map = 서비스.getCoupon(reqMap);
if (map == null) {
서비스.updateCoupon(reqMap); // DB 갱신
tmap = 서비스.getCoupon(reqMap); // 다시 조회
}
}
updateCouponInfo()
는 실행됐지만,
그 다음 getCouponInfo()에서는 여전히 null로 보이는 현상이 발생했죠?
🍱 음식점에서 밥을 시켰어 → 그런데 바로 다음에 음식을 달라고 하니까
어떤 손님한테는 밥이 오고, 어떤 손님한테는 “아직 안 지어졌어요”라고 하는 거야.
updateCouponInfo()
는 DB에 값을 넣긴 했는데트랜잭션 시작
→ update 실행
→ (커밋 안 됨)
→ 다른 데서 select → 데이터 안 보여요 ❌
이게 진짜 문제야!
// Controller
서비스.updateCouponInfo(); // 트랜잭션 A
서비스.getCouponInfo(); // 트랜잭션 B ← 분리된 트랜잭션
➡ update는 트랜잭션 A 안에서 했고
➡ get은 트랜잭션 B (또는 아예 트랜잭션 없음)에서 하니까
아직 A에서 커밋 안 된 내용이 B에는 안 보여요!
// Controller
서비스.getCoupon() // 트랜잭션 A 없이 실행됨
서비스.updateCoupon() // 트랜잭션 B 시작, 근데 아직 커밋 전
서비스.getCoupon() // 트랜잭션 A에서 읽음 → 아직 안 보임
@Transactional
public Map<String, Object> handleCoupon(Map<String, Object> reqMap) {
Map<String, Object> tmap = 서비스.getCoupon(reqMap);
if (tmap == null) {
서비스.updateCouponInfo(reqMap);
tmap = 서비스.getCoupon(reqMap);
}
return tmap;
}
➡ 이렇게 하면 조회-수정-재조회가 같은 트랜잭션 안에서 처리되므로 문제 없음.
굳이 다시 DB에서 읽지 않고 update
에서 값을 만들어서 넘겨도 OK.
진짜 해결법: "밥 짓고 → 확인"은 같은 공간(트랜잭션) 안에서 처리해야 돼!
@Service
public class CouponService {
@Transactional
public Map<String, Object> getOrUpdateCoupon(Map<String, Object> reqMap) {
Map<String, Object> tmap = getCoupon(reqMap); // 트랜잭션 A
if (tmap == null) {
updateCoupon(reqMap); // 같은 트랜잭션 A
tmap = getCoupon(reqMap); // 같은 트랜잭션 A ← OK!
}
return tmap;
}
}
➡ 이렇게 하면 update한 내용을 get에서 확실히 볼 수 있어요
왜냐하면 둘 다 같은 트랜잭션 안에서 일어나니까!
update
랑 get
이 같은 서비스 메서드 안에 있도록 만들기@Transactional
붙이기락 관련 더 자세히 정리 한 글 : 나는 select만 했을 뿐인데.. 조회도 방심 금물, 트랜잭션이 묶이는 이유
자바스크립트는 처음부터 멀티스레드 환경에서 동작하기 위해 만들어진 언어가 아닙니다.
그 기원은 **"사용자 경험 향상과 트래픽 절감"**에 있었습니다.
초기의 자바스크립트 목적은 단순했습니다.
"사용자 입력을 서버로 보내기 전에 브라우저에서 먼저 확인해보자!"
예를 들어, 신용카드 번호 입력란에 숫자가 아닌 글자가 들어가면, 굳이 서버에 데이터를 보내고 응답받을 필요 없이 브라우저에서 즉시 오류를 보여주는 것이죠.
이렇게 간단하고 빠른 피드백을 줄 수 있도록, 브라우저 안에서 사용자와 상호작용하는 작은 프로그램으로 자바스크립트가 시작된 것입니다.
그래서 자바스크립트는 단일 스레드(single-thread) 환경에서 동작합니다.
즉, 한 번에 하나의 작업만 처리할 수 있는 구조죠.
이 질문은 자바스크립트의 핵심 비밀인 "이벤트 루프(event loop)"를 이해하면 풀립니다.
자바스크립트는 이렇게 일합니다:
즉, 이벤트 하나 처리 → 다음 이벤트 순서로 진행하는 구조죠.
이건 일종의 가상의 동시성입니다. 실제로 동시에 처리하는 게 아니라, 빠르게 번갈아가며 처리하는 것이죠.
var album_id;
var album_art_url;
$.post("/get_album_id", { artist: "아이유" }, function(data) {
album_id = data.album_id;
});
$.post("/get_album_art", { id: album_id }, function(data) {
album_art_url = data.url;
});
$(body).append('<img src="' + album_art_url + '"/>');
이렇게 짜면 앨범 ID가 아직 도착하기도 전에 album_art_url을 요청하고, 심지어 img 태그도 먼저 추가될 수 있어요.
왜냐하면, 각각의 요청은 서버에 갔다가 응답을 받는 비동기 작업(asynchronous task) 이기 때문이에요.
실제 실행 순서를 보장할 수 없다는 말이죠!
자바스크립트는 이런 동시성 문제를 해결하기 위해 세 가지 방법을 제시해왔어요:
콜백 함수는 요청이 끝났을 때 실행될 함수를 인자로 전달하는 방식입니다.
$.post('/get_album_id', { artist }, function(data) {
$.post('/get_album_art', { id: data.album_id }, function(data) {
$(body).append('<img src="' + data.url + '"/>');
});
});
이런 구조를 계속 중첩하면 어떤 문제가 생길까요?
📉 바로 '죽음의 피라미드(Pyramid of Doom)'입니다.
중첩이 깊어지면 가독성이 떨어지고, 에러 핸들링도 점점 어려워집니다.
ES6부터 자바스크립트는 Promise
객체를 통해 비동기 코드를 순차적으로 연결할 수 있는 방법을 제공합니다.
post("/get_album_id", { artist })
.then((data) => post("/get_album_art", { id: data.album_id }))
.then((data) => {
$(body).append('<img src="' + data.url + '"/>');
})
.catch((err) => console.error(err));
이 방식의 장점:
.catch()
하나로 처리 가능async/await
는 프로미스 기반 위에 더 직관적인 코드 흐름을 제공해 줍니다.
async function loadAlbumArt() {
const albumIdData = await post("/get_album_id", { artist });
const artData = await post("/get_album_art", { id: albumIdData.album_id });
$(body).append('<img src="' + artData.url + '"/>');
}
await는 '기다려줘!' 라는 뜻입니다.
이 방식은:
단, await는 async 함수 내부에서만 사용 가능합니다.
자바스크립트는 단일 스레드입니다. 하지만:
결국 자바스크립트의 동시성은 이벤트 루프와 콜백 큐의 협업을 통해 이루어집니다.
자바스크립트는 브라우저에서 멈추지 않고 사용자와 상호작용하기 위해 이런 구조를 택했습니다.
만약 post()
가 동기적으로 작동한다면, 네트워크 요청이 끝날 때까지 브라우저 전체가 멈추게 될 것입니다.
즉, 비동기 구조는 사용자의 흐름을 끊지 않기 위한 선택이었고, 그 때문에 우리는 callback, promise, async/await와 같은 기법을 활용하게 된 것이죠.
방식 | 설명 | 장점 | 단점 |
---|---|---|---|
콜백 함수 | 함수 안에 함수 넣기 | 간단한 구조, 오래된 방식 | 중첩 발생, 에러 처리 어려움 |
프로미스 | then().then().catch() 체인 연결 | 에러 관리, 흐름 명확 | 문법이 복잡할 수 있음 |
async/await | 비동기 코드를 동기처럼 표현 | 가독성 최고, 유지보수 쉬움 | 구버전 브라우저 지원 제한, 예외 처리 주의 |
단일 스레드 환경에서도 자바스크립트는 놀라운 방식으로 동시성과 비동기성 문제를 해결해왔습니다.
처음에는 단순한 사용자 이벤트만 처리하던 언어가, 이제는 대규모 서버 백엔드도 가능할 만큼 강력한 언어가 되었죠.
이 모든 것의 핵심은:
"하나의 흐름 안에서도, 여러 흐름을 유연하게 다루는 방법"을 제공한다는 것입니다.
자바스크립트는 단일 스레드이지만, 이벤트 루프, 프로미스, async/await의 조합을 통해
복잡한 비동기 세계를 지능적으로 다뤄내는 동시성의 대표 주자입니다.
여러분이 웹페이지에서 버튼을 클릭하면 어떤 일이 벌어질까요?
단순히 글씨가 바뀌거나, 이미지가 나타나는 걸로 보일 수 있지만, 이 작디작은 변화 하나를 위해 브라우저 내부에서는 어마어마한 일이 벌어집니다. HTML이 펼쳐지고, JavaScript가 실행되고, DOM이 조작되고, 스타일이 다시 계산되고, 레이아웃이 조정되며, 마지막에 다시 그려지는 과정을 거칩니다. 그런데 여기서 가장 핵심적인 건 뭘까요?
바로 브라우저는 사용자의 요청을 "기다리고", 응답하는 "이벤트 기반 구조"를 갖춘 일종의 작은 운영체제처럼 행동한다는 사실입니다.
일단 브라우저는 단순히 웹페이지를 보여주는 도구가 아닙니다. 다음과 같은 요소들이 합쳐진 거대한 프로그램입니다:
구성 요소 | 설명 |
---|---|
HTML 파서 | 마크업 언어를 파싱해서 DOM 트리를 구성 |
CSS 파서 | 스타일을 파싱해서 렌더 트리 생성 |
JavaScript 엔진 (예: V8) | JS 코드를 해석하고 실행 |
렌더링 엔진 | DOM, CSSOM을 기반으로 실제 레이아웃과 픽셀 출력 |
네트워킹 스택 | 요청과 응답을 처리하는 통신 모듈 |
이벤트 루프 | 사용자 인터랙션과 스케줄된 작업을 처리하는 구조 |
{% hint style="danger" %}
이 모든 것을 조화롭게 연결하는 핵심은 무엇일까요?
{% endhint %}
바로 이벤트 루프(Event Loop)입니다.
브라우저가 한 번에 하나의 작업만 할 수 있는 단일 스레드(single-threaded) 환경
이라면, 우리는 어떻게 동시에 여러 가지 일을 할 수 있을까요?
바로 "이벤트 루프" 덕분입니다.
브라우저의 동작 타이밍을 제어하는 관리자라고 보면 됩니다.
이벤트 루프의 동작 과정을 간단히 살펴보자면, 자바스크립트의 setTimeout이나 fetch 와 같은 비동기 자바스크립트 코드를 브라우저 Web APIs
에게 맡기고, 백그라운드 작업이 끝난 결과를 콜백 함수 형태로 큐(Callback Queue)
에 넣고 처리 준비가 되면 호출 스택(Call Stack)
에 넣어 마무리 작업을 진행합니다.
이벤트 루프를 이용한 프로그램 방식을 이벤트 기반(Event Driven) 프로그래밍이라고 합니다.
이벤트 기반 프로그래밍은 프로그램의 흐름이 이벤트에 의해 결정되는 방식이에요. 예를 들어 사용자의 클릭이나 키보드 입력과 같은 이벤트가 발생하면, 그에 맞는 콜백 함수가 실행하는데. 대표적으로 자바스크립트의 addEventListener(이벤트명, 콜백함수) 입니다.
이벤트 기반 프로그래밍은 비동기 작업을 쉽게 처리할 수 있고, 멀티 스레드 언어에 비해 단순하고 직관적인 코드 작성을 가능하게 하며, 브라우저와 같은 환경에서도 안정적인 실행을 가능하게 하여 사용자와의 상호작용을 높일 수 있습니다.
따라서 이를 이해하고 적절한 방식으로 비동기 작업을 처리하는 것은, 자바스크립트를 이용한 웹 애플리케이션 개발에 있어서 매우 중요합니다.
자바스크립트는 HTML에 종속되어있는 언어입니다. HTML 조작과 변경을 위해 사용합니다.
💡 HTML은 웹페이지에 글쓰고, 그림넣는 언어이다.\
특징 ) 안 움직임, 글 넣고 그림 넣고 끝
정적 언어인 HTML을 조작해서 웹페이지를 다이나믹하게 바꿔주는 기능을 하는게 자바스크립트입니다.
JavaScript
는 싱글쓰레드 언어라고 많이 알려져 있습니다. 싱글쓰레드라고 한다면 여러 개의 작업이 있더라도 한 번에 하나의 작업만 수행할 수 있습니다. 하지만 JavaScript
를 사용해 보면 멀티쓰레드처럼 동시에 여러 작업을 수행할 수 있다는 것을 알 수 있습니다.
{% hint style="danger" %}
그렇다면 JavaScript
는 정말 싱글쓰레드 언어가 맞을까요?
{% endhint %}
맞습니다. 그 이유는 JavaScript의 메인쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문입니다. 반면 Java 나 Python은 멀티 스레드를 지원하여 원하는 코드 로직을 동시에 수행 시키는 멀티 작업이 가능합니다.
하지만 JavaScript 이벤트 루프만 독립적으로 실행되는것이 아닌 웹 브라우저나 NodeJS 같은 멀티쓰레드 환경에서 실행되고 이를 적절하게 사용함으로써 멀티쓰레드처럼 사용이 가능한 것입니다.(다만 Web worker 최신 기술을 통해 자바스크립트도 멀티 스레드 구현이 가능해졌습니다. )
바로 브라우저입니다. 브라우저에는 자바스크립트 해석 엔진이 있습니다. 기존에는 자바스크립트를 인터넷 브라우저 위에서만 실행할 수 있었습니다.
그러나 2008년에 구글이 V8 엔진을 사용하여 크롬을 출시했습니다. V8 엔진은 엄청 빨랐고, 오픈 소스로 코드도 공개되었습니다. V8 엔진이 너무 뛰어나서 기능을 좀 더 더해서 V8 엔진 기반에 노드 프로젝트를 시작했고, Node.js(V8) 등장했습니다. Node.js는 브라우저 내에서 말고도 다른 환경에서 자바스크립트를 사용할 수 있게 해줍니다.
따라서 Node.js는 JavaScript 실행 환경(=런타임)입니다. (V8과 Node.js에 대한 설명은 뒤에서)
웹 애플리케이션에서는 네트워크 요청이나 이벤트 처리, 타이머와 같은 작업을 멀티로 처리해야 하는 경우가 많은데.. 만일 싱글 스레드로 브라우저 동작이 한번에 하나씩 수행하게 되면, 우리가 파일을 다운로드 받을 동안 브라우저는 파일을 다 받을 때까지 웹서핑도 못하고 멈춰 대기해야 할 것입니다.
따라서 파일 다운, 네트워크 요청, 타이머, 애니메이션 이러한 오래 걸리고 반복적인 작업들은 자바스크립트 엔진이 아닌 브라우저 내부의 멀티 스레드인 Web APIs에서 비동기 + 논블로킹으로 처리됩니다.
비동기 + 논블로킹(Async + Non blocking)Visit Website는 메인 스레드가 작업을 다른 곳에 요청하여 대신 실행하고, 그 작업이 완료되면 이벤트나 콜백 함수를 받아 결과를 실행하는 방식을 말합니다.
비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저라는 소프트웨어가 가지고 있다고 보면 됩니다. Node.js 에서는 libuv 내장 라이브러리가 처리합니다.
이벤트 루프는 브라우저 내부의 태스크 스케줄러 역할을 합니다. 비동기적인 요청과 사용자 인터랙션을 큐에 넣어 하나씩 처리하며, 이로써 동시성을 흉내낼 수 있게 해줍니다.
이벤트 루프는 JavaScript의 실행 컨텍스트와 콜백 큐, 마이크로태스크 큐, 태스크 큐를 조율합니다.
자바스크립트(JavaScript)는 그 자체로는 "명령어의 나열일 뿐"입니다. 이걸 실제로 실행시켜주는 게 바로 JavaScript 엔진입니다.
JavaScript 엔진은 코드를 이해하고 실행을 도와주는 역할을 합니다.
💡 마치 "배우가 대본을 해석하고 연기하는 것"처럼, 엔진은 JS 코드를 해석하고 실행해줍니다.
브라우저는 각자 자신만의 JS 엔진을 가지고 있는데요:
그중에서도 가장 유명한 엔진이 바로 V8 엔진입니다. 구글이 만든 이 엔진은 빠른 실행 속도와 효율적인 메모리 처리로 유명하죠.
엔진은 코드를 실행할 때 내부적으로 두 개의 핵심 구조를 사용합니다:
📦 예시:\
const cat = { name: "복이", age: 3 }
\
→ 이 객체는 메모리 힙에 저장됩니다.
📞 예시:
function hello() { console.log("안녕!"); } hello();
→
hello()
가 호출되면 Call Stack에 쌓이고, 실행 후 제거됩니다.
둘의 관계는?
- Memory Heap은 "무엇을 저장할까?"
- Call Stack은 "무엇을 언제 실행할까?"
예를 들어, 여러분이 add(2, 3)
같은 함수를 호출하면:
add()
가 올라가고이 구조를 알면 다음과 같은 자바스크립트 동작을 이해하기 쉬워집니다:
Maximum call stack size exceeded
에러가 뜰까?┌───────────────┐
│ Memory Heap │ ← { name: '복이' }
└───────────────┘
┌───────────────┐
│ Call Stack │ ← hello()
│ │ ← main()
└───────────────┘
먼저 Memory Heap
에 있는 사용자가 작성한 코드들은 Call Stack
에서 Stack
방식으로 쌓이며 코드를 실행하게 되는데 이때 동기 함수들은 그대로 실행하게 되고 비동기 함수들은 Web API
로 처리하게 되며 일을 분배합니다.
브라우저 전체 구성도를 보면 위와 같다.
구성 요소 | 역할 |
---|---|
Call Stack | 현재 실행 중인 코드의 함수 호출이 쌓이는 공간 (LIFO 구조) |
Memory Heap | 동적으로 생성된 객체, 함수 등 데이터가 저장되는 공간 |
Web APIs | 브라우저가 제공하는 비동기 API 집합 (AJAX, Timer 등) |
Callback Queue | 완료된 비동기 작업의 콜백들이 대기하는 큐 (setTimeout 등) |
Microtask Queue | Promise와 같은 고우선 비동기 작업의 콜백 대기 공간 |
Event Table | 이벤트와 콜백의 관계를 관리하는 ‘주소록’ 역할 |
Event Loop | Call Stack이 비어 있는지 감시 → Queue에서 콜백 실행 |
Javscript를 사용하면서 우리가 많이 사용하는 API 들은 사실 JavaScript에서 지원하는 것이 아닌 웹 브라우저에서 제공하는 API로 DOM
,AJAX
, Timeout
등이 있습니다.
자바스크립트 자체에는 비동기 기능이 없음
→ 브라우저가 비동기 동작을 Web API로 제공함
Call Stack
에서 실행된 비동기 함수는 Web API
에서 처리를 하게 되고 그동안에 Call Stack
은 나머지 동기 함수들을 처리하게 됩니다.
Web API
는 비동기 함수들을 처리하며 작업이 완료된 비동기 함수들을 Callback Queue
로 넘겨주게 됩니다.
API 종류 | 설명 | 동기/비동기 |
---|---|---|
DOM API | 요소 선택, 조작 등 | 동기 |
Timer API | setTimeout , setInterval | 비동기 |
XHR / Fetch | 서버와 비동기 통신 | 비동기 |
Canvas API | 그래픽 렌더링 | 동기 |
Geolocation | 위치 정보 제공 | 비동기 |
Console API | 로그 출력 | 동기 |
구조 상 비동기 Web API는 별도의 스레드로 작동 → 메인 스레드를 블로킹하지 않음
- Web API가 작업을 마치면 콜백을 Queue에 넣음
- 이 Queue는
Event Loop
가 Call Stack이 비면 하나씩 꺼내 실행함
Callback Queue
는 비동기 함수들을 보관하는 장소로 Event Loop
에서 비동기 함수를 꺼내기 전까지는 계속 Queue
방식으로 보관하게 됩니다.
Event Loop
는 Call Stack
과 Callback Queue
를 상태를 계속 감시하며 Call Stack
에 함수들이 존재하지 않는다면 Callback Queue
에 있는 비동기 함수들을 Call Stack
에 밀어 넣게 됩니다. 그 후 Call Stack
에서 비동기 함수를 실행시키게 됩니다.
Promise.then
, process.nextTick
, MutationObserver
등이 여기에 들어감Promise가
setTimeout
보다 먼저 실행되는 이유는 이 구조 때문입니다
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
출력 순서는 어떻게 될까요?
A
D
C
B
왜 이런 순서로 출력될까요?
단계 | 설명 |
---|---|
A, D | 동기 실행 – 콜 스택에서 바로 실행 |
C | 마이크로태스크 큐 → 이벤트 루프는 스택이 비면 우선 처리 |
B | setTimeout의 콜백 → 태스크 큐에 들어가고, 마이크로태스크 다음에 실행 |
결국, Event Loop은 전체를 연결하는 조율자입니다.
- Call Stack이 비었는지 계속 감시
- Stack이 비면 Microtask Queue → Callback Queue 순서로 콜백 실행
- 매 프레임마다 한 번씩 돌면서 애플리케이션을 부드럽게 실행
이벤트 루프의 동작 순서를 간단히 그림으로 표현해보면 다음과 같습니다:
1. 콜 스택 실행
2. 마이크로태스크 큐 처리
3. 렌더링 단계
4. 태스크 큐에서 콜백 처리 (setTimeout, 이벤트 등)
5. 다시 반복
이 구조는 CPU 스케줄러와 유사하게 작동하며, 태스크 간 우선순위와 대기열을 관리합니다.
이를 통해 브라우저는 다음과 같은 일을 효율적으로 처리할 수 있습니다:
역할 | 구성 요소 | 설명 |
---|---|---|
지휘자 | Event Loop | 큐와 스택을 관리하며 타이밍을 조율함 |
무대 위 배우 | Call Stack | 현재 실행되는 함수들 |
대기실 | Task Queue, Microtask Queue | 순서에 따라 무대에 오를 콜백들 |
조명/효과팀 | Web APIs | 타이머, 네트워크, 이벤트를 처리하는 브라우저 기능들 |
최적화 항목 | 설명 |
---|---|
Promise 를 활용해 순차 실행 | Microtask로 빠르게 처리 가능 |
requestAnimationFrame | 애니메이션 최적 타이밍에 실행됨 |
setTimeout(..., 0) 남용 자제 | Task Queue는 한 프레임 지연됨 |
대량의 DOM 조작은 DocumentFragment 사용 | Stack이 과도하게 쌓이지 않도록 조절 |