[컴퓨터 밑바닥의 비밀] 프로그래밍 개념 파헤치기
프로그래밍의 기본 동기와 비동기
프로그래밍을 시작하면서 가장 많이 듣는 개념 중 하나인 동기와 비동기에 대해 알아봅시다. 동기 개념은 일상 속에서도 쉽게 접할 수 있습니다. 여러분이 건강검진을 하면서 엑스레이와 의사와 상담을 받아야 된다고 가정해봅시다. 여러분은 엑스레이를 촬영 후 엑스레이 결과가 나올 때까지 기다릴 것입니다. 엑스레이 결과가 나오고 나서야 여러분은 의사에게 상담을 받으러 움직이게 될 것입니다. 이처럼 저희가 작업을 요청하고 완료할 때 까지 기다리는 시나리오를 동기라고 합니다.
위의 그림은 비동기의 과정입니다. 여러분이 채혈 검사를 받고 검사지가 완성되는 것에 오래걸리기 때문에 따로 기다리지 않고 바로 시력검사를 받으러 갔습니다. 그 뒤에 검사지가 작성되어 전달 받았고 의사에게 상담을 받아 검진을 마쳤습니다. 이처럼 따로 요청한 작업을 기다리지 않고 바로 다음 단계를 진행하는 것을 비동기라고 합니다.
건강 검진으로 알아보는 동기의 특징
- 종속적이다.
- 의존하고 있다.
- 끝날 때까지 기다린다.
건강 검진으로 알아보는 비동기의 특징
- 종속적이지 않다.
- 의존하지 않는다.
- 기다리지 않는다.
- 동시에 처리한다.
그외에도 가장 대표적인 예시는 통화와 이메일이 있습니다. 통화는 상대방의 말이 끝날 때 까지 기다리는 반면 이메일은 전송 후에 답변이 올 때까지 다른 작업을 할 수 있으니 통화는 동기, 이메일은 비동기로 볼 수 있습니다.
동기 호출의 이해
public class SyncExample {
// printHello 메서드: "Hello"를 출력하고 메서드 종료
public static void printHello() {
System.out.println("Hello");
}
// printGoodbye 메서드: "Goodbye"를 출력하고 메서드 종료
public static void printGoodbye() {
System.out.println("Goodbye");
}
public static void main(String[] args) {
// ① printHello() 호출
printHello();
// ② printHello()가 끝난 다음에야 printGoodbye() 호출
printGoodbye();
}
}
이제는 코드를 통해 알아봅시다. 위의 코드를 보면 printHello 함수가 완료될 때 까지 printGoodbye 함수는 호출되지 않습니다. printHello와 printGoodbye는 같은 스레드 안에서 실행되며 우리가 흔히 보는 실행의 흐름입니다. 하지만 두개의 스레드에서 동기적으로 동작하는 경우가 있는데 파일을 입출력할 때가 대표적인 예시입니다. 아래의 그림을 살펴봅시다.
위의 그림처럼 writeFile이 파일 쓰기 호출을 하게 되면, 최하위 단계에서 운영체제에 시스템 호출(System call)을 보내게 됩니다. 이때 운영체제는 해당 스레드를 대기 상태(blocked)로 전환하고, 커널 모드에서 디스크 쓰기 작업을 수행합니다. 디스크 I/O가 완료되면 커널은 해당 스레드를 실행 대기열로 올려 다시 사용자 모드로 복귀시킵니다. 이처럼 제어 권한을 커널에게 넘기고 요청한 작업이 완료될 때까지 스레드가 대기하는 방식을 블로킹 입출력(blocking I/O)이라고 합니다.
동기식 호출의 장단점
장점
- 이해하기 쉽다: 호출된 함수가 끝나야 다음 함수가 실행되므로, 코드가 위에서 아래로 순차적으로 실행된다. 개발자 입장에서는 프로그램의 흐름을 직관적으로 파악할 수 있다.
- 직관적인 디버깅: 함수 호출이 순차적으로 일어나서 중단점(breakpoint)이나 로그를 사용해 디버깅하기가 상대적으로 쉽다.
단점
- CPU 리소스 비효율: 호출한 함수가 I/O(네트워크, 디스크 등) 작업을 수행하면, 해당 스레드는 결과가 올 때까지 대기 상태로 블로킹된다. 이 동안 다른 일을 처리하지 못해 CPU 리소스를 효율적으로 사용하지 못한다.
- 확장성 저하: I/O가 많은 애플리케이션에서는 많은 스레드가 대기 상태에 머물러, 동시에 처리할 수 있는 작업 수가 줄어들 수 있다.
- 응답 지연: 호출한 함수가 오래 걸리는 작업이라면 호출 스레드가 결과를 기다리는 동안 전체 프로그램의 응답성이 떨어질 수 있다.
비동기 호출의 이해
비동기 호출은 동기 호출과 달리, 호출한 작업의 결과가 완료될 때까지 기다리지 않고 바로 다음 작업을 실행하는 방식입니다. 주로 데이터베이스 쿼리, 네트워크 요청, 디스크 I/O 같은 시간이 오래 걸리는 작업을 백그라운드 형태로 처리하기 위해 사용됩니다. 비동기 호출은 크게 두 가지 시나리오로 설명드릴 수 있습니다.
- 호출 후 결과를 신경쓰지 않아도 되는 경우 (Fire-and-Forget)
- 호출 후 반드시 결과를 받아야 하는 경우 (결과를 처리해야 하는 비동기)
비동기 호출 방법 예시
- 콜백 방식 (Callback)
- 호출자 스레드는 작업을 요청하고 즉시 제어권을 돌려받아 다른 작업을 계속 수행합니다.
- 별도의 스레드 또는 프로세스가 해당 작업을 처리합니다.
- 이 방식은 상대적으로 간단하지만, 콜백 지옥(Callback Hell)이나 예외 처리가 복잡해질 수 있습니다.
- 이벤트/알림 방식 (Notification/Event)
- 콜백 방식과 유사하게 호출자 스레드는 대기하지 않고 즉시 다른 작업을 수행합니다.
- 작업이 완료되면 이벤트나 신호를 통해 호출자에게 결과가 전달됩니다. 호출자는 이 이벤트를 수신한 뒤 후속 작업을 수행합니다.
- 고급 수준 레벨의 언어의 이벤트 방식은 Promise나 Future과 같이 한층 더 추상화된 형태로 쉽게 활용할 수 있습니다.
두번째 시나리오인 알림(Notification)을 활용한 비동기 호출 방법은 첫번째 방식과 마찬가지로 호출한 뒤 자기 작업을 이어가는 것은 똑같지만 호출했던 요청으로 부터 처리 결과가 왔을 때 이어서 작업을 처리하는 방식에서 큰차이를 보입니다.
비동기 호출 방법은 I/O Bound 작업에서 리소스를 효율적으로 활용할 수 있다는 장점이 있지만 동시성을 주의 해야하고 디버깅 하기 어렵다는 단점들을 가지고 있습니다.
웹 서버에서 동기와 비동기 구현
책을 판매하는 웹서버를 구현한다고 가정해봅시다. 읽고 싶은 책을 장바구니에 담기 위해서는 아래의 과정을 거쳐야지 담을 수 있습니다.
- 토큰 복호화: 유저 토큰을 복호화하여 유저 정보를 해석한다.
- 장바구니 조회: 기존에 담은 장바구니가 있는지 디비에서 조회
- 장바구니 체크: 장바구니가 있는지 없는지 확인
- 장바구니 담기: 장바구니가 있다면 기존 장바구니에 책을 추가합니다. 없다면 새로운 장바구니를 생성하고 책을 담습니다.
- 장바구니 저장: 지금까지 담긴 장바구니 정보를 디비에 갱신합니다.
- 장바구니 객체 리턴: 장바구니 객체를 응답합니다.
위의 그림을 보면 저희는 메인 스레드에 공백이 보이는데 이 시간을 유휴시간(idle time)이라고 합니다. 이 작은 공간들이 처음에는 크게 느껴지지 않겠지만 100만 유저들이 초당 최대 장바구니를 1만번 넘게 호출한다고 가정하면 결코 작은 숫자는 아닐 것입니다. 이것은 마치 저희가 8시간에 급여를 주고 6시간의 업무만 시키는 것과 같습니다. 매일 2시간의 급여가 무료로 나가게 되는 것이죠. 이런 문제를 해결하기 위해 나온 개념이 이전에 설명했던 비동기 호출입니다.
웹서버에서 비동기식 호출
장바구니를 담는 과정에서 마케팅 이벤트 로그와 시스템 로그를 심어야 한다는 요구사항이 들어왔습니다. 이 요구사항은 현재 비즈니스 로직과 연관이 없는 요구사항 임에도 불구하고 동기식으로 처리하게 된다면 또다시 유휴시간이 생겨 더욱 비효율적인 리소스 활용이 될 것입니다.효율적으로 CPU리소스를 쓸 수 있도록 비동기로 한번 설계해보겠습니다.
// 2️⃣ 로그 기록 (Fire-and-Forget)
DBClient.save(cart, logSystemEvent(userId, 'ADD_TO_CART', '장바구니에 아이템 추가'));
// 장바구니 생성 시 마케팅 로그 전송
if (isNewCart) {
DBClient.save(cart, logMarketingEvent(userId, item));
}
콜백 함수의 역활
- 디비 스레드는 데이터베이스 요청만을 처리해야되는 것이 역활이다.
- 디비 스레드 관점에서는 외부에서 진행되는 일에 대해 관심을 가지면 안된다.
- 콜백 함수를 활용하여 사용자 요청에 맞게 유연하게 대응이 가능하다.
비동기 호출로 변환하며 생긴 이점
- 동기식 처리로 발생하는 유휴타임을 줄여 CPU를 다른 작업에 활용할 수 있게 되었다.
- 시스템 리소스를 효율적으로 활용하며 처리시간이 빨라졌다.
비동기 호출로 변환하며 생긴 단점
- 복잡도가 높아져 시스템 유지보수가 이전보다 어려워졌다.
- 디버깅과 트러블 슈팅의 난이도가 상승하였다.
- 동시성 문제가 발생할 수 있다.
웹서버에서 알림 방식의 비동기를 활용
이번에는 장바구니를 결제되면서 포인트가 적립되고 포인트가 적립되면 유저에게 알림을 보내는 요구사항이 들어왔습니다. 이번에는 별개로 실행되는 방식과 다르게 데이터베이스의 처리 결과를 기반으로 처리가 되어야 합니다.
이벤트/알림 방식 호출로 변환하며 생긴 이점
- 동기식 처리보다는 효율적으로 CPU 리소스를 활용할 수 있다.
- 비동기 방식과 마찬가지로 처리 시간이 빨라지고 응답 시간이 개선되었다.
블로킹과 논블로킹
블로킹과 논블로킹은 Node.js를 활용하는 사람들에게 익숙한 개념 중 하나일 것입니다. 블로킹은 printHello 함수가 writeHello 함수를 호출할 때 printHello가 실행중인 스레드나 프로세스를 중지 시키면 블로킹 그렇지 않다면 논블로킹입니다. 물론 무조건 모든 함수들이 다른 함수를 호출한다고 중지시키는 것은 아닙니다.
블로킹의 핵심
블로킹이 나오게 된 배경은 바로 CPU의 처리 속도와 깊은 관계를 가지고 있습니다. 2025년 평균 cpu는 초당 최대 60억번의 클럭(일정한 박자(타이밍 신호)에 맞춰 동작하도록 만드는 신호)을 할 수 있습니다. 하지만 디스크와 네트워크는 아무리 빠르더라도 ms단위에 수준이기 때문입니다. 만약 이런 시간이 cpu에게 주어졌다면 정말 많은 기계어를 처리할 수 있기 때문에 다른 스레드가 cpu를 활용할 수 있도록 제어권을 넘겨줘야 합니다.
블로킹 과정
const fs = require('fs').promises;
async function writeHello() {
await fs.writeFile('hello.txt', 'Hello');
console.log('파일 생성 완료: hello.txt');
}
async function printHello() {
console.log('printHello() 실행 시작');
await writeHello();
console.log('printHello() 실행 종료');
}
// 실행
printHello();
- CPU 제어권 - A: printHello를 실행시키기 위해 CPU 제어권을 할당 받음.
- CPU 제어권 - B: A가 파일을 전부 쓸 동안 일시 중지 상태로 변경하고 다른 스레드에게 제어권 전달
- CPU 제어권 - B: 파일 쓰기 완료
- CPU 제어권 - A: 제어권 회수하고 일시 중지된 시점부터 실행
이처럼 시간이 많이 걸리는 i/o작업에는 일시중지 상태로 변환하며 운영체제는 CPU 리소스를 최대한 활용할 수 있게 되었고 블로킹 입출력 방식이 필수적으로 활용하는 이유가 이런 이유 때문입니다.
동기와 블로킹의 차이
- 동기는 함수 호출 결과가 끝날 때까지 호출자가 기다리는 구조입니다. (순차적 코드 흐름).
- 블로킹은 호출자 스레드가 멈춰서 다른 작업을 처리하지 못하고 대기 상태로 있는 현상.
논블로킹 호출
논블로킹 방식은 호출한 스레드를 중지시키고 CPU 제어권을 다른 스레드에게 넘기는 블로킹 방식과 다르게 호출한 스레드에게 제어권을 다시 반환합니다. 그리고 데이터 쓰기 작업은 커널에서 처리합니다.
커널에서 작업이 완료되었는지 알 수 있는 방법
- 결과를 확인하는 함수를 만들어서 해당 함수를 통해 작업 완료 여부를 확인한다.
- 데이터 쓰기가 완료되면 스레드에 메시지나 신호 등을 전달하는 방법
- writeHello 함수를 호출할 때 파일쓰기 하는 함수를 콜백 함수로 만들어서 파라미터로 전달.
이런 방식을 논블로킹 호출입니다. 비동기 I/o라고도 합니다. 블로킹 방식과 비교해보면 직관적이지 않다는 것을 알 수 있습니다.
비동기와 논블로킹의 차이
배달 앱으로 주문을 시키고 배달을 올 때까지 저희는 문 앞까지 기다리고 있지 않습니다. 배달이 올 때까지 책을 읽거나 Ott를 보면서 다른 일들을 할 수 있죠. 그때 식사가 도착하면 그제서야 식사를 하게 됩니다. 이처럼 호출한 뒤 바로 반환이 되어 다른 작업을 마저 하는 것을 비동기라고 생각하면 됩니다.
내가 배달앱으로 주문을 받는 음식점입니다. 배달 앱으로 주문이 들어오면 음식점은 주문표에 추가하고 주문표를 추가하는 동안 요리사는 이전에 들어온 메뉴를 계속 만들 수 있습니다. 이처럼 호출한 뒤 작업을 기다리는 동안 다른 사람이 작업을 처리하는 것을 논블로킹이라고 생각하면 됩니다.
요약
- 비동기는 호출한 작업의 완료를 기다리지 않고 바로 반환되므로, CPU가 작업 완료 여부와 상관없이 다른 작업을 준비할 수 있다.
- 논블로킹은 CPU가 I/O를 기다리지 않고 다른 태스크(스레드나 이벤트 루프의 콜백)를 계속 실행할 수 있는 상태이다.
멀티 프로세스
효율적으로 CPU를 활용하기위해 가장 먼저 나온 개념은 다중 프로세스를 였습니다. 저희가 활용하고 있는 크롬이 가장 대표적인 예시라고 할 수 있습니다. linux 생태계에서는 fork 방식을 활용하여 모든 요청에 대응하는 프로세스들을 만들 수 있습니다.
멀티 프로세스 장점
- 프로그래밍의 복잡성이 낮고 이해하기 쉽다.
- 개별의 프로세스가 격리되어 있기 때문에 서로 영향을 주지 않는다.
- 다중 코어 리소사를 효율적으로 활용할 수 있다.
멀티 프로세스 단점
- 프로세스 간의 통신이 필요할 때 구현 난이도가 올라간다.
- 프로세스 생성에 대한 부담이 크고 잦은 프로세스의 생성과 종료는 시스템에 큰 부담을 준다.
멀티 스레드
멀티 프로세스에서는 프로세스의 생성 부담을 크고 프로세스 끼리의 통신이 부담스러웠지만멀티 스레드는 서로 같은 주소 공간을 가지고 있고 스레드는 프로세스보다 생성 비용이 적고 무엇보다 스레드풀에 사용할 스레드들을 미리 만들어놓기 때문에 효율적으로 리소스를 사용할 수 있습니다. 대표적인 예시로 저희가 이전에 사용했던 브라우저인 인터넷 익스플로러가 있습니다.
멀티 스레드의 장점
- 여러 작업을 동시에 실행할 수 있어 프로그램의 처리량이 증가한다.
- 사용할 스레드를 미리 만들어놓기 때문에 효율적으로 리소스를 활용할 수 있다.
- 서로 다른 스레드끼리 자원을 쉽게 공유할 수 있다.
멀티 스레드 단점
- 하나의 스레드에 문제가 생겨 강제 종료되면 이 스레드와 주소공간을 공유하고 있는 모든 프로세스와 스레드가 종료된다.
- 여러 스레드가 자원을 공유하면서 동기화 문제가 발생하여 데이터 무결성이 깨질 수 있다.
- 멀티스레드는 동기화 문제, 교착 상태(데드락), 스케줄링 등의 이유로 단일 스레드 프로그램보다 디버깅과 테스트가 어렵다.
이벤트 루프와 이벤트 핸들러
- 이벤트: 네트워크 수신 여부 확인, 파일 읽기/쓰기 등과 같이 입출력과 관련된 신호들을 이벤트라고 합니다.
- 이벤트 핸들러: 이벤트를 처리하는 함수를 이벤트 핸들러라고 합니다.
이벤트 핸들러 문제점
- 하나의 함수에 여러 이벤트를 처리할 수 없다.
- 이벤트를 처리하는 핸들러 함수가 이벤트 루프와 같은 스레드를 쓰면서 서로 영향을 받는다.
i/o 멀티플렉싱
- epoll이 들어오는 i/o를 감지하여 이벤트 루프에게 전달합니다.
- 이벤트 루프를 통해 이벤트를 실행하는 핸들러에게 이벤트를 전달합니다.
- 이벤트 핸들러는 각각의 워커 스레드를 통해 실행되어 처리됩니다.
위와 같은 구조를 통해 하나의 함수에 여러 개의 이벤트를 처리하지 못하는 문제와 하나의 스레드에서 문제가 발생하면 전체가 영향받는 문제가 해결되게 되었습니다. 다중 스레드는 멀티코어 시스템을 최대한 활용하기 때문에 요청 처리를 가속화합니다. 물론 워커 스레드도 스레드 풀로 관리가 됩니다. 이런 설계를 반응자 패턴이라고 합니다.
반응자 패턴이란?
음식점에서 한 명의 주문 담당 직원(이벤트 루프)이 모든 손님의 주문 상태를 감시하면서 주문이 들어오면 즉시 적절한 요리사(핸들러)에게 전달해 처리하게 합니다. 주문 담당 직원은 요리, 포장, 배달 등 모든 요청의 상태를 관리하면서 이벤트가 발생할 때마다 요리사에게 전달합니다. 이렇게 이벤트를 감지하고 담당자에게 분배하는 전체 구조가 반응자 패턴에 해당합니다.
이벤트 루프의 입출력
입출력에 대응하는 논블로킹 인터페이스가 있을 경우 스레드가 일시중지 되지 않고 즉시 반환하기 때문에 이벤트 루프에서 직접 호출하는 것이 가능하지만 이벤트 루프를 중단 시키는 블로킹 인터페이스를 호출하는 것은 전체 시스템을 멈추게 할 수 있기 때문에 안됩니다. 그래서 작업자 스레드에게 i/o 작업을 위임해야합니다. 그래야 호출받은 스레드가 중단되어도 다른 스레드에 영향을 주지 않습니다.
비동기와 콜백 함수
하나의 디비를 호출하는 것은 매우 간단하겠지만 두세개의 서버를 호출해야된다면 구현이 복잡할 것입니다. 검색기능을 구현한다고 가정했을 때 RPC 통신으로 하나하나의 서버에 블로킹 된다면 점점 사용할 수 있는 스레드가 많지 않게 될 것이고 빈번하게 중단될 수 있을 것입니다. 이걸 콜백을 활용하여 효율적으로 바꿔보겠습니다.
void handler_after_GetStrokInfo(response) {
// 처리 코드
}
void handler_after_GetQueryInfo(response) {
// 처리 코드
GetStrokeInfo(request, handler_after_GetStrokInfo) // 서버 c에 요청
}
void handler_after_GetUserInfo(response) {
// 처리 코드
GetQueryInfo(request, handler_after_GetQueryInfo) // 서버 c에 요청
}
void handle(response) {
// 처리 코드
GetUserInfo(request, handler_after_GetUserInfo) // 서버 c에 요청
}
- handle(response)
- 최상위 진입점으로, 응답 데이터를 처리한 후 GetUserInfo(request, handler_after_GetUserInfo)를 호출하여 서버 C에 사용자 정보 요청.
- GetUserInfo(request, handler_after_GetUserInfo)
- 서버 C에 사용자 정보 요청을 비동기로 전송.
- 응답이 오면 이벤트 루프(또는 비동기 프레임워크)가 handler_after_GetUserInfo(response) 콜백을 호출.
- handler_after_GetUserInfo(response)
- 사용자 정보 응답을 처리한 후 GetQueryInfo(request, handler_after_GetQueryInfo)를 호출하여 서버 C에 쿼리 정보 요청.
- GetQueryInfo(request, handler_after_GetQueryInfo)
- 서버 C에 쿼리 정보 요청을 비동기로 전송.
- 응답이 오면 이벤트 루프(또는 비동기 프레임워크)가 handler_after_GetQueryInfo(response) 콜백을 호출.
- handler_after_GetQueryInfo(response)
- 쿼리 정보 응답을 처리한 후 GetStrokeInfo(request, handler_after_GetStrokInfo)를 호출하여 서버 C에 스트로크 정보 요청.
- GetStrokeInfo(request, handler_after_GetStrokInfo)
- 서버 C에 스트로크 정보 요청을 비동기로 전송.
- 응답이 오면 이벤트 루프(또는 비동기 프레임워크)가 handler_after_GetStrokInfo(response) 콜백을 호출.
- handler_after_GetStrokInfo(response)
- 스트로크 정보 응답을 처리한다.
코루틴을 활용한 비동기 프로그래밍
코루틴이 일시 중지가 되더라도 작업자 스레드가 블로킹되지 않습니다. 코루틴이 일시 중지되면 작업자 스레드가 다른 준비 완료된 코루틴을 위해 활용되며 다른 코루틴이 반환하면 다시 준비 완료된 코루틴을 찾습니다. 코루틴이 일시정지 후 i/o가 완료되면 다시 준비 상태가 되어 스케줄링 차례가 오길 기다립니다. 위의 그림처럼 계속 중단된 지점으로 부터 계속 이어서 실행이 됩니다.
이처럼 코루틴은 CPU에서 스레드로 스레드에서 코루틴으로 서로 다른 계층에서 실행되며 코루틴이 얼마나 생성되었든 스레드에 따라 CPU의 사용시간을 지정합니다. 프로그래머는 스레드의 할당된 cpu 시간을 스레드 라이브러리를 활용하여 할당할 수 있습니다.
코드, 데이터, 변수, 포인터
- 함수: 일정한 명령어의 집합을 반복해서 만들지 않기 위해 만들어진 것이 함수
- 변수: 데이터 정보를 특정 별칭으로 지칭하여 사용하기 위해 만들어진 것이 변수
- 포인터: 여러 변수가 동일한 데이터를 참조하기 위해 만들어진 것이 포인터, 참조라고도 함.
콜백 함수와 클로저
- 일급 객체: 코드를 할당, 사용, 파리미터로 전달, 반환 값으로 사용하는 것.
- 콜백 함수: 함수가 다른 함수에 매개변수로 전달되는 것이 콜백 함수
- 클로저: 콜백 함수를 일부 데이터와 함께 묶어서 변수로 취급하는 것을 클로저라고 함.
컨테이너와 가상머신 차이
컨테이너(Container)
- 애플리케이션과 그 실행에 필요한 라이브러리, 설정 파일 등을 패키징해 운영체제의 커널을 공유하며 독립적으로 실행되는 환경.
- Docker, Kubernetes 등이 대표적인 컨테이너 기술이다.
- 호스트 OS의 커널을 공유하기 때문에 오버헤드가 적고 빠르게 기동된다.
가상머신(Virtual Machine, VM)
- 호스트 OS 위에 하이퍼바이저(VMware, Hyper-V, KVM 등)를 설치해 완전한 운영체제(OS)를 가상으로 실행.
- 각 VM은 게스트 OS를 포함하고 있어, 호스트 OS와 완전히 독립적으로 동작한다.
- 하드웨어 자원을 가상화해 각각의 VM이 별도의 OS와 환경을 갖도록 제공한다.
컨테이너 | VM | |
OS 공유 | 호스트 OS 커널 공유 | 각자 게스트 OS 필요 |
부팅 속도 | 빠름 (초 단위) | 느림 (분 단위) |
리소스 효율 | 상대적으로 적음 | 상대적으로 많음 |
격리성 | 프로세스 수준 | 완전한 OS 수준 |
사용 사례 | 마이크로서비스, DevOps | OS별 테스트, 강력한 격리 필요 |