티스토리 뷰

우리가 매일 사용하는 컴퓨터와 스마트폰은 마치 마법처럼 동시에 수많은 작업을 처리합니다. 웹 브라우저에서 동영상을 시청하면서 메신저로 친구와 대화하고, 백그라운드에서는 운영체제 업데이트가 진행되는 등의 일들이 끊임없이 벌어지죠. 이 모든 것이 가능한 이유는 운영체제(Operating System, OS)가 프로세스(Process)와 스레드(Thread)라는 두 가지 핵심 개념을 통해 효율적으로 작업을 관리하고 조율하기 때문입니다.
이 글은 운영체제의 가장 근간이 되는 이 두 개념, 즉 프로세스란 무엇이며 스레드란 무엇인지에 대한 깊이 있는 이해를 돕고, 둘의 결정적인 차이점과 실제 시스템에서 어떻게 활용되는지에 대한 통찰을 제공하고자 합니다. 운영체제 기본 개념에 관심 있는 일반인부터 컴퓨터 공학 전공 학생, 그리고 효율적인 시스템 설계를 고민하는 개발자까지, 이 글을 통해 복잡해 보이는 컴퓨터 내부의 동작 원리를 명확하게 파악할 수 있을 것입니다. 컴퓨터가 "생각"하고 "일"하는 방식의 본질을 함께 탐구하며, 여러분의 시스템 이해도를 한 단계 높여보세요.
프로그램 실행의 비밀: 프로세스와 스레드는 왜 등장했을까?
현대 컴퓨팅 환경에서 우리는 컴퓨터가 동시에 여러 작업을 수행하는 것에 너무나도 익숙해져 있습니다. 워드 프로세서로 문서를 작성하는 동시에 웹 브라우저에서 정보를 검색하고, 백그라운드에서 음악을 재생하거나 파일을 다운로드하는 것은 이제 일상적인 풍경입니다. 이러한 동시성(Concurrency)은 컴퓨터가 여러 작업을 실제로 동시에 처리하는 것처럼 보이게 하는 능력을 의미하며, 이는 운영체제의 핵심 역할 중 하나입니다.
프로그램과 실행의 개념
우선, 프로그램(Program)과 실행(Execution)의 차이를 명확히 이해해야 합니다. 프로그램은 단순히 하드디스크에 저장된 정적인 명령어의 집합, 즉 "어떻게 일할지"에 대한 설계도 또는 요리책과 같습니다. 예를 들어, 워드 프로세서 애플리케이션 파일은 그 자체로 하나의 프로그램입니다. 반면, 실행은 이 설계도를 바탕으로 실제 작업이 이루어지는 동적인 상태를 의미합니다. 컴퓨터가 워드 프로세서 아이콘을 클릭했을 때, 해당 프로그램의 명령어가 메모리에 로드되고 CPU에 의해 처리되기 시작하면서 비로소 '실행 중'이 되는 것입니다.
이러한 실행 과정을 관리하고 제어하는 것이 바로 운영체제(OS)의 주된 역할입니다. 운영체제는 CPU, 메모리, 스토리지, 네트워크 등 컴퓨터의 모든 하드웨어 자원을 효율적으로 배분하고, 사용자 및 애플리케이션의 요구에 따라 이 자원들을 활용하여 프로그램을 실행시킵니다.
멀티태스킹의 등장과 운영체제의 고민
초기의 컴퓨터 시스템은 한 번에 하나의 프로그램만 실행할 수 있었습니다. 이를 단일 프로그래밍 시스템(Single-programming system)이라고 합니다. 하지만 사용자의 요구가 점점 복잡해지면서, 컴퓨터가 여러 작업을 동시에 처리해야 할 필요성이 대두되었습니다. 예를 들어, 긴 계산이 필요한 프로그램을 실행하는 동안 사용자가 다른 작업을 할 수 없다는 것은 매우 비효율적입니다.
이러한 비효율성을 극복하기 위해 멀티태스킹(Multitasking) 개념이 등장했습니다. 멀티태스킹은 CPU가 여러 프로그램(또는 작업) 사이를 빠르게 전환하며 마치 동시에 실행되는 것처럼 보이게 하는 기술입니다. CPU의 처리 속도가 워낙 빠르기 때문에, 인간의 눈에는 여러 작업이 한꺼번에 진행되는 것처럼 느껴집니다. 운영체제는 이러한 멀티태스킹을 구현하기 위해 각 프로그램에 CPU 시간을 시분할(Time-sharing) 방식으로 할당하고, 각 프로그램의 실행 상태를 저장하고 복원하는 복잡한 작업을 수행해야 했습니다.
이 과정에서 운영체제는 단순히 프로그램을 메모리에 로드하고 CPU가 실행하도록 지시하는 것을 넘어, "하나의 실행 단위"를 정의하고 관리할 필요성을 느끼게 되었습니다. 단순히 프로그램 코드만으로는 실행 상태, 할당된 메모리, 열린 파일 목록 등 프로그램이 작동하는 데 필요한 모든 동적인 정보를 담을 수 없었기 때문입니다.
실행 단위의 필요성: 프로세스와 스레드의 등장
이러한 배경 속에서 운영체제는 프로그램이 실행될 때 필요한 모든 자원과 실행 상태를 묶어 관리하는 개념을 만들어냈는데, 이것이 바로 프로세스(Process)입니다. 각 프로세스는 운영체제로부터 독립적인 메모리 공간과 기타 자원(파일 핸들, I/O 장치 등)을 할당받아 실행되는 독립적인 작업 단위로 정의됩니다. 워드 프로세서, 웹 브라우저, 음악 플레이어 등이 각각 하나의 독립적인 프로세스로 실행되는 것이죠.
하지만 시간이 지나면서 하나의 애플리케이션 내에서도 여러 작업을 동시에 처리해야 할 필요성이 생겨났습니다. 예를 들어, 워드 프로세서가 문서를 저장하는 동안에도 사용자의 입력을 받아서 화면에 표시해야 하거나, 웹 브라우저가 여러 탭에서 각각 다른 페이지를 로드하는 등의 상황입니다. 이때, 각 작업마다 완전히 독립적인 프로세스를 생성하는 것은 자원 소모가 너무 크고, 프로세스 간의 통신이 복잡해지는 문제가 발생합니다.
이러한 한계를 극복하기 위해 스레드(Thread)라는 개념이 등장했습니다. 스레드는 하나의 프로세스 내에서 실행되는 더 작은 실행 단위입니다. 프로세스가 제공하는 자원(메모리 공간 등)을 공유하면서, 각 스레드는 고유의 실행 흐름을 가지고 독립적으로 작업을 수행합니다. 이는 마치 하나의 공장(프로세스) 안에서 여러 작업자(스레드)가 같은 장비와 재료를 공유하면서 각자의 작업 라인을 따라 일을 하는 것과 유사합니다.
결론적으로, 운영체제가 프로그램을 효율적으로 실행하고 관리하며 현대적인 동시성 환경을 제공하기 위해 프로세스와 스레드라는 두 가지 핵심적인 실행 단위가 등장하게 된 것입니다. 다음 섹션에서는 이 두 개념을 각각 더 깊이 있게 살펴보겠습니다.
운영체제의 독립적인 일꾼: 프로세스(Process)란 무엇인가?
컴퓨터가 여러 작업을 동시에 수행하는 것처럼 보이는 현상의 중심에는 프로세스(Process)라는 개념이 있습니다. 프로세스는 운영체제가 프로그램을 실행하고 관리하는 가장 기본적인 독립적인 작업 단위입니다. 단순히 하드디스크에 저장된 프로그램(Program) 파일이 아니라, 그 프로그램이 실제로 실행되어 CPU에 의해 처리되는 동적인 상태를 의미합니다.
프로세스의 정의와 핵심 특성
컴퓨터 공학에서 프로세스는 다음과 같이 정의됩니다:
프로세스(Process): 실행 중인 프로그램의 인스턴스로서, 운영체제로부터 자원(메모리, CPU 시간, 파일 등)을 할당받아 독립적으로 실행되는 작업 단위.
이 정의에서 가장 중요한 특성은 바로 독립성입니다. 각 프로세스는 운영체제로부터 자신만의 독립적인 실행 환경과 자원 공간을 할당받습니다. 이는 마치 각 회사(프로세스)가 자체적인 사무실, 직원, 장비(메모리, CPU, 파일 등)를 가지고 독립적으로 사업을 운영하는 것에 비유할 수 있습니다. 한 회사의 문제가 다른 회사에 직접적인 영향을 주지 않는 것처럼, 한 프로세스의 오류가 다른 프로세스에 치명적인 영향을 미치지 않도록 설계됩니다.
프로세스의 구성 요소
운영체제가 프로세스를 효율적으로 관리하기 위해서는 단순히 코드만으로는 부족합니다. 프로세스는 실행에 필요한 다양한 정보와 자원을 포함하고 있습니다. 주요 구성 요소는 다음과 같습니다:
- 코드 영역 (Text Segment):
- 프로그램의 실행 코드가 저장되는 영역입니다. 읽기 전용(Read-Only)이며, 여러 프로세스가 동일한 프로그램을 실행할 때 이 코드를 공유할 수 있습니다.
- 데이터 영역 (Data Segment):
- 프로그램이 사용하는
전역 변수(Global variables)와정적 변수(Static variables)가 저장되는 영역입니다.
- 프로그램이 사용하는
- 힙 영역 (Heap Segment):
- 프로그램 실행 중에
동적으로 할당되는 메모리영역입니다. 개발자가 필요할 때마다 메모리를 할당받고 해제할 수 있습니다.
- 프로그램 실행 중에
- 스택 영역 (Stack Segment):
- 함수 호출 시
지역 변수(Local variables),매개변수(Parameters),리턴 주소(Return address)등이 임시로 저장되는 영역입니다.후입선출(LIFO)방식으로 동작합니다.
- 함수 호출 시
- 프로세스 제어 블록 (Process Control Block, PCB):
- 운영체제가 프로세스를 관리하기 위해 필요한 모든 정보를 담고 있는
핵심 데이터 구조입니다. 각 프로세스마다 고유의 PCB를 가지며, 여기에는 다음과 같은 정보들이 포함됩니다:- 프로세스 상태: 생성, 준비, 실행, 대기, 종료 등.
- 프로세스 ID (PID): 운영체제가 프로세스를 식별하기 위한 고유 번호.
- 프로그램 카운터 (Program Counter): 다음에 실행할 명령어 주소.
- CPU 레지스터: CPU 레지스터 값들.
- 메모리 관리 정보: 페이지 테이블, 세그먼트 테이블 등.
- 파일 관리 정보: 열고 있는 파일들의 목록과 상태.
- I/O 상태 정보: 할당된 I/O 장치, 열려 있는 I/O 버퍼 등.
- 운영체제가 프로세스를 관리하기 위해 필요한 모든 정보를 담고 있는
PCB는 운영체제가 여러 프로세스 사이를 전환하며 작업을 처리할 때, 각 프로세스의 상태를 저장하고 복원하는 데 결정적인 역할을 합니다. 이를 통해 CPU는 마치 여러 프로세스가 동시에 실행되는 것처럼 보이게 할 수 있습니다.
다중 작업을 가능하게 하는 프로세스의 원리
운영체제는 이러한 독립적인 프로세스들을 스케줄링(Scheduling)하여 CPU 시간을 할당하고, 여러 프로세스가 동시에 실행되는 것처럼 보이게 합니다. 즉, CPU는 초고속으로 한 프로세스에서 다른 프로세스로 작업(Context Switching)을 전환하며 각 프로세스의 작업을 조금씩 처리합니다.
예를 들어, 웹 브라우저(프로세스 A)를 실행하고 워드 프로세서(프로세스 B)를 동시에 실행한다고 가정해 봅시다. 운영체제는 먼저 프로세스 A에 일정 시간 CPU를 할당하고 실행시킵니다. 할당된 시간이 끝나거나 프로세스 A가 I/O 작업(예: 네트워크에서 데이터 가져오기)으로 인해 잠시 대기 상태로 전환되면, 운영체제는 프로세스 A의 현재 상태(PCB에 저장)를 저장하고, 이어서 프로세스 B의 저장된 상태를 복원하여 CPU에 할당합니다. 이러한 빠른 전환 덕분에 사용자 입장에서는 두 프로그램이 동시에 활발히 작동하는 것처럼 느끼게 되는 것입니다.
프로세스 예시
일상생활에서 접하는 대부분의 애플리케이션은 독립적인 프로세스로 실행됩니다.
- 웹 브라우저: 구글 크롬이나 파이어폭스 같은 웹 브라우저 자체는 하나의 프로세스입니다. 최근 브라우저들은 안정성을 위해 각 탭을 별도의 프로세스로 실행하는
멀티프로세스구조를 채택하기도 합니다. - 워드 프로세서: 마이크로소프트 워드나 한글 같은 문서 편집기도 하나의 독립적인 프로세스입니다.
- 음악 플레이어: 멜론, 스포티파이 같은 음악 스트리밍 앱도 독립적인 프로세스로 실행됩니다.
이처럼 프로세스는 운영체제가 프로그램을 독립적인 단위로 분리하고, 각 작업에 필요한 자원을 할당하여 안정적으로 멀티태스킹을 구현하는 핵심 메커니즘입니다. 그러나 하나의 프로세스 내부에서 더 세밀한 동시성 제어가 필요할 때, 스레드라는 개념이 등장하게 됩니다.
프로세스 안의 작은 실행 단위: 스레드(Thread)란 무엇인가?
프로세스가 운영체제의 독립적인 작업 단위라면, 스레드(Thread)는 이 프로세스 안에서 실행되는 더 작고 가벼운 실행 단위입니다. 스레드는 프로세스의 자원을 공유하면서 고유의 실행 흐름을 갖는 작업의 흐름이라고 할 수 있습니다. 마치 하나의 대형 공장(프로세스) 안에 여러 명의 작업자(스레드)가 같은 작업 공간, 장비, 재료를 공유하면서 각자의 할당된 업무를 동시에 수행하는 모습에 비유할 수 있습니다.
스레드의 정의와 등장 배경
스레드(Thread): 프로세스 내에서 실행되는 여러 실행 흐름의 단위. 프로세스의 코드, 데이터, 힙 메모리 영역을 공유하며, 자신만의 스택과 레지스터 집합을 가진다.
스레드가 등장하게 된 배경은 하나의 프로세스 내에서 여러 작업을 동시(Concurrent)에 처리해야 할 필요성이 커졌기 때문입니다. 예를 들어, 워드 프로세서를 생각해 봅시다. 사용자가 키보드로 문자를 입력하는 동안, 프로그램은 동시에 맞춤법을 검사하고, 자동 저장을 수행하며, 화면을 갱신하는 등의 여러 작업을 처리해야 합니다. 만약 이 모든 작업을 하나의 실행 흐름(단일 스레드)으로 처리한다면, 한 작업이 완료될 때까지 다른 작업은 대기해야 하므로 사용자 경험이 매우 나빠질 것입니다.
이러한 문제를 해결하기 위해, 운영체제는 프로세스 내부에 스레드라는 개념을 도입했습니다. 스레드는 프로세스의 모든 자원을 복사하여 새로운 프로세스를 만드는 대신, 기존 프로세스의 자원을 그대로 공유하면서 자신만의 독립적인 실행 경로를 가지게 됩니다.
스레드의 구성 요소와 프로세스와의 자원 공유 특성
각 스레드는 프로세스의 주소 공간 내에서 실행되며, 다음과 같은 자원들을 공유합니다.
- 코드(Text) 영역: 프로세스의 실행 코드를 공유합니다.
- 데이터(Data) 영역: 전역 변수 및 정적 변수를 공유합니다.
- 힙(Heap) 영역: 동적으로 할당된 메모리 공간을 공유합니다.
반면, 각 스레드는 다음을 독립적으로 가집니다.
- 스택(Stack) 영역: 함수 호출 시의 지역 변수, 매개변수, 리턴 주소 등을 저장하는 공간입니다. 각 스레드마다 독립적인 호출 스택을 가짐으로써 서로 다른 함수를 동시에 실행할 수 있습니다.
- 레지스터 집합 (Register Set): 프로그램 카운터(PC), 스택 포인터(SP), 범용 레지스터 등 CPU의 상태를 저장하는 공간입니다. 이 역시 각 스레드가 독립적으로 가짐으로써 문맥 교환 시 각 스레드의 실행 상태를 저장하고 복원할 수 있습니다.
- 스레드 ID (Thread ID, TID): 운영체제가 스레드를 식별하기 위한 고유 번호.
이러한 자원 공유 특성 덕분에 스레드들은 같은 프로세스 내에서 매우 효율적으로 서로 통신하고 데이터를 주고받을 수 있습니다. 별도의 복잡한 프로세스 간 통신(Inter-Process Communication, IPC) 메커니즘 없이도 공유 메모리 영역을 통해 직접 데이터에 접근할 수 있기 때문입니다. 이는 스레드 간 통신이 프로세스 간 통신보다 훨씬 빠르고 효율적이라는 것을 의미합니다.
멀티스레딩(Multithreading)의 원리
하나의 프로세스 내에 여러 스레드가 동시에 실행되는 것을 멀티스레딩(Multithreading)이라고 합니다. CPU는 여러 스레드 사이를 빠르게 전환하며 각 스레드의 작업을 조금씩 처리합니다. 프로세스와 마찬가지로 스레드도 시분할 방식을 통해 동시에 실행되는 것처럼 보이게 합니다.
예를 들어, 웹 브라우저가 멀티스레딩을 사용한다고 가정해 봅시다.
- 스레드 1: 사용자 인터페이스(UI)를 담당하여 사용자의 마우스 클릭이나 키보드 입력을 처리합니다.
- 스레드 2: 네트워크에서 웹 페이지 데이터를 다운로드합니다.
- 스레드 3: 다운로드된 데이터를 파싱하고 화면에 렌더링(그리기)합니다.
- 스레드 4: 자바스크립트(JavaScript) 코드를 실행합니다.
만약 이 모든 작업이 단일 스레드로 처리된다면, 네트워크에서 데이터를 받아오는 동안 UI는 멈춰 있을 것이고, 사용자는 웹 페이지가 "먹통"이 되었다고 느낄 것입니다. 하지만 멀티스레딩을 통해 각 스레드가 독립적으로 작업을 수행하면, 네트워크 다운로드가 진행되는 동안에도 UI는 반응성을 유지하고, 자바스크립트 실행이 늦어져도 페이지 렌더링에는 지장을 덜 주게 됩니다.
스레드 예시
- 웹 브라우저: 여러 탭, 네트워크 통신, UI 렌더링, 스크립트 실행 등을 각각 다른 스레드가 처리하여 부드러운 사용자 경험을 제공합니다. (최신 브라우저는 각 탭을 별도 프로세스로 띄우고, 각 프로세스 내에서 멀티스레딩을 활용하기도 합니다.)
- 워드 프로세서: 자동 저장 기능, 맞춤법 검사, 문서 내용 렌더링 등 여러 기능이 별도의 스레드로 동작하여 사용자가 문서를 편집하는 동안 백그라운드에서 다른 작업이 원활히 이루어집니다.
- 게임: 그래픽 렌더링, 물리 엔진 계산, 인공지능(AI) 처리, 사용자 입력 처리 등을 각각 다른 스레드로 분리하여 실시간으로 복잡한 환경을 구현합니다.
- 데이터베이스 서버: 여러 클라이언트의 쿼리(질의) 요청을 동시에 처리하기 위해 각 클라이언트 요청마다 별도의 스레드를 생성하여 처리합니다.
이처럼 스레드는 프로세스의 자원을 효율적으로 공유하며, 하나의 애플리케이션 내에서 동시성(Concurrency)을 높여 사용자에게 더 빠르고 반응성이 뛰어난 경험을 제공하는 데 필수적인 개념입니다. 다음 섹션에서는 프로세스와 스레드의 핵심적인 차이점을 더욱 상세하게 비교 분석하여, 언제 어떤 것을 사용해야 할지에 대한 이해의 기반을 마련하겠습니다.
핵심 비교: 프로세스와 스레드의 결정적인 차이점
프로세스와 스레드는 모두 운영체제에서 작업을 실행하는 중요한 단위이지만, 그 구조와 동작 방식, 그리고 활용 목적에서 분명한 차이를 보입니다. 이러한 차이점을 명확히 이해하는 것은 효율적인 시스템 설계와 문제 해결에 필수적입니다. 다음은 프로세스와 스레드의 결정적인 차이점을 비교 분석한 내용입니다.
1. 독립성 및 자원 공유 방식
- 프로세스:
- 완전 독립적: 각 프로세스는 운영체제로부터 독립적인 메모리 공간(코드, 데이터, 힙, 스택)과 자원(파일 핸들, I/O 장치 등)을 할당받습니다. 다른 프로세스에 영향을 주지 않고 독자적으로 실행될 수 있습니다.
- 자원 공유 없음: 기본적으로 프로세스 간에는 자원이 직접 공유되지 않습니다. 데이터 교환을 위해
IPC(Inter-Process Communication)라는 복잡하고 비용이 많이 드는 메커니즘(파이프, 소켓, 공유 메모리 등)이 필요합니다.
- 스레드:
- 부분적 공유: 스레드는 자신이 속한 프로세스의 코드, 데이터, 힙 영역을 공유합니다. 즉, 같은 프로세스 내의 다른 스레드와 이 자원들을 함께 사용합니다.
- 독립적 자원: 스택과 레지스터 집합은 각 스레드마다 독립적으로 가집니다. 이는 각 스레드가 자신만의 실행 흐름을 유지할 수 있도록 합니다.
- 자원 공유 용이: 프로세스 자원을 공유하므로 스레드 간 데이터 교환이 매우 쉽고 빠릅니다.
2. 생성 및 종료 비용
- 프로세스:
- 높은 비용: 새로운 메모리 공간과 PCB 등 모든 필요한 자원을 할당받아야 하므로, 많은 시스템 자원(메모리, CPU 시간)이 소모됩니다. 따라서 생성 및 종료 비용이 높고, 시간이 오래 걸립니다. 프로세스는 '무거운' 실행 단위로 간주됩니다.
- 스레드:
- 낮은 비용: 기존 프로세스의 자원을 공유하고 스택과 레지스터만 독립적으로 할당받으므로, 프로세스 생성보다 훨씬 적은 자원이 소모됩니다. 따라서 생성 및 종료 비용이 낮고, 빠르게 생성하고 소멸시킬 수 있습니다. 스레드는 '가벼운' 실행 단위로 간주됩니다.
3. 문맥 교환(Context Switching) 비용
문맥 교환(Context Switching)이란 CPU가 현재 실행 중인 프로세스 또는 스레드의 상태를 저장하고, 다음으로 실행할 프로세스 또는 스레드의 상태를 복원하여 CPU에 할당하는 작업을 말합니다. 운영체제가 멀티태스킹을 구현하는 핵심 메커니즘입니다.
- 프로세스 문맥 교환:
- 높은 비용: 현재 프로세스의 모든 PCB 정보(메모리 맵, 열린 파일 정보, 레지스터 값 등)를 저장하고, 다음 프로세스의 모든 PCB 정보를 로드해야 합니다. 특히 메모리 관리 정보를 교체해야 하므로
TLB(Translation Lookaside Buffer)캐시를 플러시(초기화)하는 경우가 많아 상당한 오버헤드를 유발합니다.
- 높은 비용: 현재 프로세스의 모든 PCB 정보(메모리 맵, 열린 파일 정보, 레지스터 값 등)를 저장하고, 다음 프로세스의 모든 PCB 정보를 로드해야 합니다. 특히 메모리 관리 정보를 교체해야 하므로
- 스레드 문맥 교환:
- 낮은 비용: 같은 프로세스 내에서 동작하므로, 프로세스의 메모리 맵이나 파일 정보 등 대부분의 자원은 그대로 유지됩니다. 오직 스택, 레지스터 집합, 프로그램 카운터 등 스레드 고유의 정보만 저장하고 복원하면 됩니다. 따라서 프로세스 문맥 교환보다 훨씬 빠르고 비용이 적게 듭니다.
4. 안정성 (Fault Isolation)
- 프로세스:
- 높은 안정성: 각 프로세스는 독립적인 메모리 공간을 가지므로, 하나의 프로세스에서 오류가 발생하더라도 다른 프로세스나 시스템 전체에 직접적인 영향을 미치지 않습니다. 운영체제는 해당 프로세스만 종료시키고 나머지 시스템은 계속 정상 작동할 수 있도록 보호합니다. 이를
결함 격리(Fault Isolation)라고 합니다.
- 높은 안정성: 각 프로세스는 독립적인 메모리 공간을 가지므로, 하나의 프로세스에서 오류가 발생하더라도 다른 프로세스나 시스템 전체에 직접적인 영향을 미치지 않습니다. 운영체제는 해당 프로세스만 종료시키고 나머지 시스템은 계속 정상 작동할 수 있도록 보호합니다. 이를
- 스레드:
- 낮은 안정성: 스레드는 프로세스의 자원(특히 힙 메모리)을 공유하기 때문에, 하나의 스레드에서 치명적인 오류(예: 공유 메모리 손상)가 발생하면 해당 프로세스 전체에 영향을 미쳐 모든 스레드가 함께 비정상 종료될 수 있습니다.
- 동기화 문제: 자원을 공유하므로 여러 스레드가 동시에 공유 자원에 접근할 때
경쟁 상태(Race Condition)나교착 상태(Deadlock)와 같은동기화(Synchronization)문제가 발생하기 쉽습니다.
5. 통신 방식
- 프로세스:
IPC(Inter-Process Communication)메커니즘을 통해 통신. 비교적 느리고 복잡합니다. - 스레드: 공유 메모리 공간을 통해 직접 데이터 접근 가능. 빠르고 효율적이지만 동기화 문제에 유의해야 합니다.
비교 요약표
| 특징 | 프로세스 (Process) | 스레드 (Thread) |
|---|---|---|
| 자원 공유 | 독립적인 메모리 공간 및 자원 | 프로세스 내 코드, 데이터, 힙 공유; 스택, 레지스터는 독립적 |
| 생성/종료 비용 | 높음 (무겁다) | 낮음 (가볍다) |
| 문맥 교환 비용 | 높음 (PCB 전체 교체, TLB 플러시 가능) | 낮음 (스택, 레지스터 등 일부만 교체) |
| 안정성 | 높음 (결함 격리, 다른 프로세스에 영향 없음) | 낮음 (하나의 스레드 오류가 프로세스 전체에 영향 가능) |
| 통신 방식 | IPC (파이프, 소켓, 공유 메모리 등) | 공유 메모리를 통한 직접 접근 (동기화 필요) |
| 메모리 독립성 | 완전한 독립성 (자신만의 가상 주소 공간) | 프로세스의 주소 공간 공유 |
이러한 차이점들을 바탕으로, 시스템 설계자는 애플리케이션의 요구사항(성능, 안정성, 자원 효율성 등)에 따라 프로세스와 스레드 중 어떤 것을 활용할지 신중하게 결정해야 합니다. 다음 섹션에서는 이러한 실용적인 활용 전략에 대해 더 자세히 알아보겠습니다.
실제 상황에서 언제 무엇을 선택해야 할까? 프로세스와 스레드 활용 전략
프로세스와 스레드의 차이점을 이해했다면, 이제 실제 애플리케이션 개발이나 시스템 설계에서 언제 어떤 것을 선택해야 할지에 대한 실용적인 가이드라인을 제시할 차례입니다. 멀티프로세스와 멀티스레드는 각각의 장단점이 명확하므로, 작업의 특성, 자원 요구사항, 안정성 등을 종합적으로 고려하여 전략적으로 선택해야 합니다.
멀티태스킹, 동시성, 병렬성 이해
활용 전략을 논하기 전에, 이와 관련된 세 가지 중요한 개념을 명확히 할 필요가 있습니다.
- 멀티태스킹(Multitasking):
- 단일 CPU 코어에서 여러 프로그램이나 작업을 번갈아 가며 실행하여 동시에 실행되는 것처럼 보이게 하는 기술입니다. 운영체제의
시분할(Time-sharing)기법을 통해 구현됩니다.
- 단일 CPU 코어에서 여러 프로그램이나 작업을 번갈아 가며 실행하여 동시에 실행되는 것처럼 보이게 하는 기술입니다. 운영체제의
- 동시성(Concurrency):
- 여러 작업이 동시에 진행되는 것처럼 보이는 논리적인 상태를 의미합니다. 물리적으로 동시에 실행되지 않더라도(싱글 코어 CPU에서도 가능), 여러 작업이 서로 독립적으로 진행되고 있다는 인상을 줍니다. 주로
멀티스레딩이 동시성 구현에 많이 사용됩니다.
- 여러 작업이 동시에 진행되는 것처럼 보이는 논리적인 상태를 의미합니다. 물리적으로 동시에 실행되지 않더라도(싱글 코어 CPU에서도 가능), 여러 작업이 서로 독립적으로 진행되고 있다는 인상을 줍니다. 주로
- 병렬성(Parallelism):
- 실제로 여러 작업을
동시에(at the exact same time)실행하는 물리적인 상태를 의미합니다. 이는 멀티 코어(Multi-core) CPU나 멀티 프로세서(Multi-processor) 시스템과 같이 물리적으로 여러 개의 처리 장치가 있을 때만 가능합니다.멀티프로세싱이나 멀티스레딩 모두 병렬성을 달성할 수 있습니다.
- 실제로 여러 작업을
요약:
- 멀티태스킹: 운영체제가 여러 작업을 관리하는 방식.
- 동시성: 여러 작업이 '진행 중'이라는 논리적 상태.
- 병렬성: 여러 작업이 '실제로 동시에' 실행되는 물리적 상태.
멀티스레딩은 단일 코어에서 동시성을 달성할 수 있으며, 멀티 코어에서는 병렬성까지 활용할 수 있습니다. 멀티프로세싱은 멀티 코어 환경에서 가장 큰 시너지를 냅니다.
1. 멀티프로세스(Multi-process)의 장단점 및 활용 전략
장점:
- 높은 안정성: 프로세스 간 독립적인 메모리 공간을 가지므로, 하나의 프로세스에 문제가 발생해도 다른 프로세스나 시스템 전체에 미치는 영향이 적습니다. (결함 격리)
- 높은 보안성: 프로세스 간 메모리 영역이 분리되어 있어 보안 측면에서 유리합니다.
- 간단한 프로그래밍 모델: 각 프로세스는 독립적이므로, 스레드처럼 복잡한 공유 자원 동기화 문제를 신경 쓸 필요가 적습니다.
- 멀티 코어 활용 용이: 각 프로세스가 독립적인 CPU 코어에 할당되어 진정한 병렬 처리를 쉽게 구현할 수 있습니다.
단점:
- 높은 자원 소모: 각 프로세스가 독립적인 자원(메모리, 파일 핸들 등)을 할당받으므로, 많은 프로세스를 생성하면 시스템 자원 소모가 큽니다.
- 느린 문맥 교환: 프로세스 문맥 교환 비용이 높아 성능 저하를 야기할 수 있습니다.
- 복잡한 IPC: 프로세스 간 데이터 교환이 필요할 경우 IPC 메커니즘을 사용해야 하며, 이는 구현이 복잡하고 오버헤드가 발생합니다.
활용 전략:
- 안정성과 독립성이 최우선인 경우: 각 작업이 서로에게 영향을 주지 않아야 할 때 (예: 웹 서버의 워커 프로세스 모델).
- 복잡하고 독립적인 장기 실행 작업: 컴파일러, 대용량 데이터 처리, 배치 작업 등.
- 보안이 중요한 애플리케이션: 샌드박스(Sandbox) 환경이 필요한 경우 (예: 웹 브라우저의 각 탭을 별도 프로세스로 실행).
- CPU-bound 작업: 계산 집약적인 작업으로, 멀티 코어 시스템에서 진정한 병렬 처리를 위해 유용합니다.
대표적인 활용 사례:
- 웹 서버: Apache HTTP Server, Nginx 등은 클라이언트 요청마다 새로운 프로세스(또는 프로세스 풀)를 할당하여 처리합니다.
- 데이터베이스 시스템: 여러 클라이언트의 질의를 독립적인 프로세스로 처리하여 안정성을 확보합니다.
- 유닉스/리눅스 명령어:
ls,grep,awk등 각 명령어가 독립적인 프로세스로 실행되고 파이프(|)를 통해 데이터를 주고받습니다.
2. 멀티스레드(Multi-thread)의 장단점 및 활용 전략
장점:
- 낮은 자원 소모: 프로세스의 자원(메모리)을 공유하므로, 프로세스 생성보다 적은 자원으로 많은 스레드를 생성할 수 있습니다.
- 빠른 문맥 교환: 스레드 문맥 교환 비용이 낮아 빠른 작업 전환이 가능합니다.
- 쉬운 자원 공유 및 통신: 같은 프로세스 내에서 공유 메모리를 통해 데이터 교환이 매우 효율적이고 빠릅니다.
- 반응성 향상: UI를 담당하는 스레드와 백그라운드 작업을 담당하는 스레드를 분리하여 사용자 인터페이스의 반응성을 높일 수 있습니다.
단점:
- 복잡한 동기화 문제: 자원을 공유하므로
경쟁 상태(Race Condition),교착 상태(Deadlock)등의 문제가 발생하기 쉽습니다. 이를 해결하기 위한동기화 메커니즘(뮤텍스, 세마포어)구현이 복잡하고 오류 가능성이 높습니다. - 낮은 안정성: 하나의 스레드에 문제가 생기면 해당 프로세스 전체가 영향을 받아 비정상 종료될 수 있습니다.
- 디버깅의 어려움: 공유 자원과 동기화 문제로 인해 버그를 찾고 해결하기가 어렵습니다.
- GIL(Global Interpreter Lock)의 영향: 파이썬과 같은 일부 언어는 GIL 때문에 멀티스레딩이 CPU-bound 작업에서 진정한 병렬성을 제공하지 못하고 동시성만 제공하는 경우가 있습니다.
활용 전략:
- 동일한 프로세스 내에서 여러 작업을 동시에 처리해야 할 때: 하나의 애플리케이션 내에서 여러 기능을 병행해야 할 때 적합합니다.
- I/O-bound 작업: 네트워크 통신, 파일 입출력 등 대기 시간이 많은 작업에 효율적입니다. 한 스레드가 I/O 작업으로 대기하는 동안 다른 스레드가 CPU를 사용할 수 있습니다.
- 반응성(Responsiveness)이 중요한 애플리케이션: GUI 애플리케이션, 게임 등에서 사용자 인터페이스의 응답성을 유지하면서 백그라운드 작업을 처리할 때 사용합니다.
- 작업 간 데이터 공유가 빈번하고 효율이 중요한 경우: 복잡한 데이터 구조를 공유해야 하는 경우.
대표적인 활용 사례:
- GUI 애플리케이션: 사용자 입력 처리, 화면 갱신, 백그라운드 데이터 처리 등을 각각 별도의 스레드로 처리합니다.
- 웹 서버 (스레드 풀): 클라이언트 요청을 스레드 풀에서 처리하여 프로세스 생성 오버헤드를 줄이고, 빠르게 요청을 처리합니다.
- 게임: 그래픽 렌더링, 물리 계산, AI 로직, 사용자 입력 처리 등을 멀티스레딩으로 분리합니다.
- 워드 프로세서: 자동 저장, 맞춤법 검사, 인쇄 미리보기 등을 백그라운드 스레드로 처리합니다.
3. 파이썬으로 살펴보는 멀티프로세스와 멀티스레드 예제
파이썬은 multiprocessing 모듈과 threading 모듈을 제공하여 각각 멀티프로세싱과 멀티스레딩을 쉽게 구현할 수 있습니다. 파이썬의 GIL(Global Interpreter Lock) 특성 때문에 멀티스레딩이 CPU-bound 작업에서는 병렬성을 제공하지 못하고 동시성만 제공한다는 점을 유의해야 합니다. 그러나 I/O-bound 작업에서는 여전히 멀티스레딩이 효율적입니다.
예제 1: CPU-Bound 작업 (멀티프로세싱이 유리)
CPU-bound 작업은 CPU 연산이 주를 이루는 작업입니다. 파이썬에서는 GIL 때문에 멀티스레딩으로는 CPU 코어를 효율적으로 사용하기 어렵고, 멀티프로세싱이 진정한 병렬성을 제공합니다.
import multiprocessing
import time
import os
def cpu_bound_task(n):
"""지정된 숫자까지 제곱의 합을 계산하는 CPU-bound 작업"""
process_id = os.getpid()
print(f"[{process_id}] Process '{multiprocessing.current_process().name}' started with {n}...")
# 순수 CPU 연산
total_sum = 0
for i in range(n):
total_sum += i * i
print(f"[{process_id}] Process '{multiprocessing.current_process().name}' finished, result sum up to {n}...")
return total_sum
if __name__ == "__main__":
test_range = 20_000_000 # 총 연산량
print("--- 1. 단일 프로세스 (Single Process) ---")
start_time = time.time()
result_single = cpu_bound_task(test_range)
print(f"단일 프로세스 실행 시간: {time.time() - start_time:.4f} 초")
print(f"결과: {result_single}\n")
print("--- 2. 멀티 프로세싱 (Multi-Processing for CPU-bound) ---")
start_time = time.time()
# 작업을 2개로 나누어 2개의 프로세스에 할당
numbers_for_processes = [test_range // 2, test_range // 2]
# 2개의 프로세스를 사용하여 병렬 처리
with multiprocessing.Pool(processes=2) as pool:
results_multi = pool.map(cpu_bound_task, numbers_for_processes)
print(f"멀티 프로세싱 실행 시간: {time.time() - start_time:.4f} 초")
print(f"결과: {sum(results_multi)}\n")
# 주석 처리된 멀티스레딩 예제를 실행해보면 GIL 때문에 성능 향상이 미미함을 알 수 있습니다.
# import threading
# def threaded_cpu_task(n):
# print(f"[{threading.current_thread().name}] Thread started with {n}...")
# total_sum = 0
# for i in range(n):
# total_sum += i * i
# print(f"[{threading.current_thread().name}] Thread finished, result sum up to {n}...")
# return total_sum
# print("--- (참고) 멀티 스레딩 (Multi-Threading for CPU-bound) ---")
# start_time = time.time()
# threads = []
# results = [0, 0] # 스레드에서 결과를 직접 반환받기 어려우므로 공유 리스트 사용
# # 파이썬 스레드는 GIL 때문에 CPU-bound 작업에 병렬성을 제공하지 못합니다.
# # 이를 보여주기 위한 예제이므로 실제 활용에는 멀티프로세싱을 사용해야 합니다.
# def wrapper(idx, n):
# results[idx] = threaded_cpu_task(n)
#
# numbers_for_threads = [test_range // 2, test_range // 2]
# for i, n_val in enumerate(numbers_for_threads):
# thread = threading.Thread(target=wrapper, args=(i, n_val,))
# threads.append(thread)
# thread.start()
#
# for thread in threads:
# thread.join()
# print(f"멀티 스레딩 실행 시간: {time.time() - start_time:.4f} 초")
# print(f"결과: {sum(results)}\n")
실행 결과 해석:
위 코드를 실행해 보면, 단일 프로세스 실행 시간과 멀티 프로세싱 실행 시간의 차이를 명확히 볼 수 있습니다. 멀티프로세싱은 시스템의 멀티 코어 CPU를 효과적으로 활용하여 CPU-bound 작업을 병렬로 처리함으로써, 단일 프로세스보다 훨씬 빠른 결과를 보여줍니다.
예제 2: I/O-Bound 작업 (멀티스레딩이 유리)
I/O-bound 작업은 네트워크 요청, 파일 입출력 등 외부 장치와의 상호작용으로 인해 대기 시간이 긴 작업입니다. 한 스레드가 I/O 대기 상태일 때 GIL이 해제되므로, 다른 스레드가 CPU를 사용하여 작업을 진행할 수 있어 효율적입니다.
import threading
import time
import requests # 실제 네트워크 요청을 위해 requests 모듈 사용
def io_bound_task(url):
"""URL에서 데이터를 가져오는 I/O-bound 작업"""
thread_name = threading.current_thread().name
print(f"[{thread_name}] Fetching {url}...")
try:
# 실제 네트워크 요청 (시간이 걸리는 I/O 작업)
# timeout 설정으로 무한 대기 방지
response = requests.get(url, timeout=5)
print(f"[{thread_name}] Fetched {url} (Status: {response.status_code}, Length: {len(response.content)} bytes)")
except requests.exceptions.RequestException as e:
print(f"[{thread_name}] Error fetching {url}: {e}")
print(f"[{thread_name}] Finished fetching {url}...")
return url
if __name__ == "__main__":
urls_to_fetch = [
"https://www.google.com",
"https://www.naver.com",
"https://www.daum.net",
"https://www.example.com" # 더미 URL 추가
]
print("--- 1. 단일 스레드 (Single Thread) ---")
start_time = time.time()
for url in urls_to_fetch:
io_bound_task(url)
print(f"단일 스레드 실행 시간: {time.time() - start_time:.4f} 초\n")
print("--- 2. 멀티 스레딩 (Multi-Threading for I/O-bound) ---")
start_time = time.time()
threads = []
for url in urls_to_fetch:
thread = threading.Thread(target=io_bound_task, args=(url,))
threads.append(thread)
thread.start() # 스레드 시작
# 모든 스레드가 종료될 때까지 대기
for thread in threads:
thread.join()
print(f"멀티 스레딩 실행 시간: {time.time() - start_time:.4f} 초\n")
# 참고: I/O-bound 작업에 멀티프로세싱을 사용해도 되지만, 스레드보다 오버헤드가 크고 자원 공유가 복잡해집니다.
# import multiprocessing
# print("--- (참고) 멀티 프로세싱 (Multi-Processing for I/O-bound) ---")
# start_time = time.time()
# with multiprocessing.Pool(processes=len(urls_to_fetch)) as pool:
# pool.map(io_bound_task, urls_to_fetch)
# print(f"멀티 프로세싱 실행 시간: {time.time() - start_time:.4f} 초")
실행 결과 해석:
이 코드를 실행하면, 단일 스레드 환경에서는 각 URL 요청이 순차적으로 처리되어 전체 시간이 각 요청 시간의 합과 비슷하게 나옵니다. 반면, 멀티스레딩 환경에서는 여러 스레드가 거의 동시에 네트워크 요청을 보내고 대기 시간이 겹치므로, 전체 실행 시간이 가장 오래 걸리는 하나의 요청 시간과 비슷하게 단축되는 것을 확인할 수 있습니다. 이는 I/O-bound 작업에서 멀티스레딩이 효과적임을 보여줍니다.
결론적으로, 프로세스와 스레드 중 어떤 것을 선택할지는 애플리케이션의 특정 요구사항에 따라 달라집니다.
- 높은 안정성과 독립성, CPU 자원의 진정한 병렬 처리가 필요할 때는
멀티프로세스를 고려합니다. (예: 독립적인 서비스, CPU-bound 작업) - 자원 효율성, 빠른 응답성, 잦은 데이터 공유, I/O 작업의 동시 처리가 필요할 때는
멀티스레드를 고려합니다. (예: GUI 애플리케이션, 웹 서버의 I/O-bound 작업)
현대의 복잡한 시스템에서는 멀티프로세스와 멀티스레드를 조합하여 사용하는 하이브리드(Hybrid) 방식도 흔히 채택됩니다. 예를 들어, 웹 서버는 여러 워커 프로세스를 띄우고, 각 워커 프로세스 내부에서 다시 여러 스레드를 사용하여 클라이언트 요청을 처리하는 식입니다. 이러한 방식은 프로세스의 안정성과 스레드의 효율성을 동시에 활용할 수 있는 장점을 가집니다.
마치며: 효율적인 시스템 이해와 설계를 위한 통찰
지금까지 운영체제의 핵심 개념인 프로세스(Process)와 스레드(Thread)에 대해 깊이 있게 탐구하고, 그 결정적인 차이점 및 실제 활용 전략까지 살펴보았습니다. 프로그램이 단순한 코드의 집합에서 벗어나 어떻게 살아 숨 쉬는 작업 단위가 되는지, 그리고 운영체제가 이들을 어떻게 관리하며 동시성과 병렬성을 구현하는지에 대한 이해의 폭이 넓어졌기를 바랍니다.
프로세스는 운영체제로부터 독립적인 자원을 할당받아 실행되는 '무거운' 작업 단위로서, 높은 안정성과 독립성을 제공합니다. 이는 마치 각자의 독립된 공간에서 일하는 전문가들과 같습니다. 반면, 스레드는 하나의 프로세스 자원을 공유하면서 고유한 실행 흐름을 갖는 '가벼운' 작업 단위이며, 자원 효율성과 빠른 문맥 교환을 통해 동시성 및 반응성을 극대화합니다. 이는 한 공간에서 협력하여 일하는 팀원들과 유사합니다.
핵심 개념 요약
- 프로세스: 운영체제가 관리하는 독립적인 실행 단위. 독립된 메모리 공간과 자원을 가지며, 안정성이 높지만 생성 및 문맥 교환 비용이 높습니다. 멀티프로세싱은 진정한 병렬성에 유리합니다.
- 스레드: 프로세스 내에서 실행되는 작은 실행 단위. 프로세스의 자원을 공유하며, 생성 및 문맥 교환 비용이 낮아 효율적입니다. 동기화 문제가 발생할 수 있으며 안정성은 상대적으로 낮습니다. 멀티스레딩은 동시성 및 I/O-bound 작업에 유리합니다.
- 활용 전략: 안정성과 독립성이 중요하거나 CPU-bound 작업에는 멀티프로세스, 효율성과 반응성, 빈번한 데이터 공유 또는 I/O-bound 작업에는 멀티스레드를 고려해야 합니다.
효율적인 시스템 설계와 개발을 위한 통찰
프로세스와 스레드에 대한 명확한 이해는 단순히 이론적 지식을 넘어, 실용적인 개발과 시스템 설계에 있어 매우 중요한 통찰을 제공합니다.
- 자원 관리 최적화: 어떤 작업을 프로세스로, 어떤 작업을 스레드로 분리할지 결정함으로써 메모리, CPU 시간 등의 시스템 자원을 효율적으로 사용할 수 있습니다. 이는 불필요한 자원 낭비를 줄이고 시스템 성능을 최적화하는 기반이 됩니다.
- 성능 향상: 동시성 또는 병렬성이 필요한 애플리케이션에서 프로세스와 스레드를 적절히 활용함으로써, 사용자에게 더 빠르고 반응성이 뛰어난 경험을 제공할 수 있습니다. 특히 멀티 코어 환경을 최대한 활용하여 CPU-bound 작업을 가속화하거나, I/O-bound 작업의 대기 시간을 효과적으로 숨길 수 있습니다.
- 안정적인 시스템 구축: 각 작업의 중요도와 의존성을 고려하여 프로세스 기반으로 분리할지, 스레드 기반으로 통합할지 결정함으로써 시스템의 전체적인 안정성을 높일 수 있습니다. 오류 발생 시의 파급 효과를 최소화하는 설계가 가능해집니다.
- 복잡한 문제 해결 능력 향상: 다중 실행 환경에서 발생하는
경쟁 상태(Race Condition),교착 상태(Deadlock)와 같은 동기화 문제를 이해하고 해결하기 위한 기반 지식이 됩니다. 이는 고품질의 견고한 소프트웨어를 개발하는 데 필수적입니다.
현대의 소프트웨어는 점점 더 복잡해지고, 대규모 사용자 트래픽과 실시간 처리 요구사항이 늘어나고 있습니다. 이러한 환경에서 프로세스와 스레드 개념은 운영체제가 어떻게 이러한 요구사항을 충족시키는지에 대한 근본적인 답을 제시합니다. 이 두 가지 "운영체제의 기둥"에 대한 깊이 있는 이해는 여러분이 더 효율적이고 강력하며 안정적인 시스템을 설계하고 구현하는 데 있어 가장 중요한 출발점이 될 것입니다. 끊임없이 변화하는 기술 환경 속에서 이러한 기본 개념에 대한 확고한 이해는 여러분의 역량을 한 단계 더 성장시킬 핵심 열쇠가 될 것입니다.
#hashtags
'DEV > ETC' 카테고리의 다른 글
| Google AI Studio 실전 가이드: Gemini AI 핵심 활용법 (1) | 2026.01.04 |
|---|---|
| Akamai 봇 탐지 우회 완벽 가이드: 헤드리스 브라우저 방어 전략과 실제 적용 기술 (0) | 2026.01.03 |
| GEO 심화 학습: 지리 공간 AI, 비전공자도 전문가 되는 핵심 가이드와 실전 전략 (0) | 2026.01.03 |
| Core Web Vitals 완전 정복: 웹사이트 성능, SEO, 사용자 경험까지 한 번에 잡는 실전 가이드 (0) | 2026.01.03 |
| SEO와 GEO: 검색 엔진 최적화의 지리적 중요성과 성공 전략 (1) | 2026.01.03 |
- Total
- Today
- Yesterday
- 미래ai
- 웹개발
- 업무자동화
- AI
- 로드밸런싱
- SEO최적화
- 자바개발
- 개발자성장
- 마이크로서비스
- LLM
- Java
- 생성형AI
- 배민
- AI기술
- 성능최적화
- 웹보안
- AI반도체
- 프론트엔드개발
- 개발자가이드
- 클린코드
- n8n
- 인공지능
- 클라우드컴퓨팅
- 개발가이드
- springai
- restapi
- 프롬프트엔지니어링
- 개발생산성
- 데이터베이스
- 백엔드개발
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |