개발이군고구마

OS와 CPU의 작동 원리, 그리고 CPU 100% 문제 해결 본문

SERVER/CS

OS와 CPU의 작동 원리, 그리고 CPU 100% 문제 해결

김구황 2026. 3. 27. 07:40
728x90

1. 핵심 개념 정리 — 먼저 용어부터

  • 프로그램(Program): 디스크에 저장된 실행 파일 (ex. .jar, .exe)
  • 프로세스(Process): 프로그램이 메모리에 올라가 실행 중인 상태. OS가 자원을 할당하는 단위.
  • 스레드(Thread): 프로세스 안에서 실제로 코드를 실행하는 흐름. CPU Core가 처리하는 최소 단위.

💡 흔한 오해: "프로세스 = CPU"가 아님. CPU는 하드웨어이고, 프로세스는 CPU 위에서 돌아가는 소프트웨어 개념.

 

① 컴퓨터의 물리적 구조 — 하드디스크 vs 메모리 vs CPU

CPU 자체에는 아무것도 "저장"되지 않음 — CPU는 RAM에서 코드를 읽어와서 실행할 뿐

  • 하드디스크는 "창고", 메모리(RAM)는 "책상", CPU는 "손"임.
  • 손(CPU)은 항상 책상(RAM) 위에 있는 것만 처리 가능. 창고(HDD)에서 꺼내서 책상에 올려야 비로소 작업 가능

 

② OS는 어디에 "위치"하는가 — 부팅 전후 비교

ㄴ OS는 두 곳에 동시에 존재함. 부팅 전/후 상태가 다름.

 

2. OS는 무엇을 하는가

OS(운영체제)는 CPU라는 하드웨어 위에서 돌아가는 특별한 프로그램. 역할은 크게 두 가지.

  • 자원 관리자: CPU, 메모리, 디스크 등 하드웨어 자원을 어떤 프로세스에게 얼마나 줄지 결정
  • 중재자: 여러 프로그램이 CPU를 공정하게 나눠 쓸 수 있도록 스케줄링

스케줄링의 핵심 원리: OS는 스레드를 아주 짧은 시간 단위(time slice)로 번갈아 실행함. 사람 눈에는 동시에 실행되는 것처럼 보이지만, 실제로는 빠르게 교체(Context Switch)되는 것.

 

 

3. 스레드와 CPU의 관계 — 코드 한 줄이 실행되는 과정

 

OS 스케줄러는 스레드가 대기 상태일 때 다른 스레드에게 CPU를 양보하도록 조율함. 이것이 멀티태스킹의 실체.

 

 


4. 실전 — API 서버 CPU 100% 문제

Consuming 하여 Thread Pool Queue 에 집어넣는 것도 **Thread**

 

✳ "Worker Thread 10개"와 "Thread Pool Queue"가 어떻게 다른지부터 짚고 넘어가야 이슈가 이해됨.

  • Kafka 토픽 = 주문이 들어오는 주방 벨
  • Consumer (1개) = 홀 직원 1명 (주문 접수 담당)
  • Thread Pool Queue = 주방 안 대기 주문 슬롯 (메모판)
  • Worker Thread 10개 = 요리사 10명

 

▶ 문제 상황 — Queue 가득 찼을 때 else 없는 루프

while (true) {
    List<Event> events = consumer.poll(Duration.ofMillis(100));

    for (Event event : events) {
        if (workerPool.getQueue().isEmpty()) {   // Queue에 여유 있으면
            workerPool.submit(() -> process(event)); // Task 제출
        }
        // else 없음! Queue 가득 차도 그냥 통과
    }
    // 다시 while 처음으로 → 무한 반복
}

else가 없으면 Queue가 꽉 찼을 때 아무것도 안 하고 루프만 계속 돔. CPU 입장에서는 "일은 없지만 계속 달려야 하는" 상태.

 

  해결 원리

해결 — else에 Thread.sleep 추가

while (true) {
    List<Event> events = consumer.poll(Duration.ofMillis(100));

    for (Event event : events) {
        if (workerPool.getQueue().isEmpty()) {
            workerPool.submit(() -> process(event));
        } else {
            // Queue 꽉 찼을 때 → CPU 양보하고 잠시 대기
            Thread.sleep(50);
        }
    }
}

Thread.sleep(50)을 호출하는 순간 일어나는 일:

  1. 해당 스레드가 OS에게 "나 50ms 동안 필요 없어"라고 신호
  2. OS 스케줄러가 스레드를 RUNNABLE → TIMED_WAITING 상태로 변경
  3. CPU를 Worker Thread들에게 할당 → Worker들이 밀린 Task를 소화
  4. 50ms 후 OS가 Consumer 스레드를 깨워 다시 Queue 상태 확인

 

 

5. 더 나은 패턴 — Worker Pool + Queue

Thread.sleep은 임시방편. 실무에서는 Worker Pool + BlockingQueue 패턴을 사용함.

// 올바른 패턴: BlockingQueue 사용
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();

// Worker는 take()로 블로킹 대기 — CPU 낭비 없음
while (true) {
    Task task = queue.take(); // 큐가 비면 자동으로 Blocked 상태
    process(task);
}

BlockingQueue.take()는 OS 레벨에서 스레드를 Blocked 상태로 만들어 줌. 큐에 작업이 생기면 OS가 알아서 스레드를 깨움. Thread.sleep처럼 개발자가 직접 타이머를 관리할 필요 없음.