객체의 역할과 책임, 그리고 코드는 어떻게 실행되는가 — 발표자 노트

발표자용 상세 스크립트 · 최종 30장 기준
이 문서 사용법
각 페이지는 다음 순서로 정리되어 있습니다.
- 이 장의 역할: 이 페이지가 전체 발표에서 왜 필요한지 확인합니다.
- 앞 장에서 이어가기: 슬라이드를 넘긴 직후 말할 연결 문장입니다.
- 말하기: 청중에게 실제로 설명할 문장입니다. 그대로 읽어도 어색하지 않게 작성했습니다.
- 용어·개념 메모: 발표자만 보는 설명입니다. 슬라이드나 스크립트에 나온 낯선 말의 뜻을 바로 확인합니다.
- 헷갈리면 이렇게 구분: 비슷한 개념을 섞지 않기 위한 기준입니다.
- 질문이 나오면: 본 발표 범위를 넘기지 않으면서 한 단계 더 답하는 방법입니다.
- 다음 장으로 연결: 다음 페이지가 갑자기 등장하지 않도록 이어 주는 문장입니다.
발표할 때 모든 메모를 말할 필요는 없습니다. 기본적으로 말하기와 다음 장으로 연결만 사용하고, 설명하다 막히거나 질문이 들어왔을 때 나머지 항목을 봅니다.
기술 설명에서 그냥 Python이라고 말하더라도 실행 구조와 메모리 관리에 관한 내용은 특별한 언급이 없으면 CPython 구현 기준입니다.
01. 표지
이 장의 역할
오늘 다룰 두 주제인 객체지향 설계와 코드 실행 구조를 하나의 질문으로 묶습니다.
말하기
오늘은 Python 문법을 더 많이 외우는 시간이 아닙니다.
우리가 작성한 코드를 어떤 기준으로 나누어야 하는지, 그리고 그렇게 나눈 코드가 컴퓨터 안에서 실제로 어떻게 실행되는지를 한 흐름으로 연결해서 보겠습니다.
앞부분에서는 관심사와 책임을 기준으로 객체의 경계를 잡습니다. 뒷부분에서는 그 객체와 클래스가 C, Java, Python 같은 언어에서 어떤 실행 과정을 거치는지 봅니다.
오늘의 목표는 모든 용어를 암기하는 것이 아닙니다. 새로운 코드나 질문을 만났을 때, 지금 보고 있는 문제가 설계의 문제인지, 언어 표현의 문제인지, 실행 환경의 문제인지 구분할 수 있게 되는 것입니다.
용어·개념 메모
- 객체: 상태와 행동을 하나의 대상으로 묶어 보는 단위입니다.
- 역할: 다른 대상이 이 객체에게 무엇을 기대하고 요청할 수 있는지를 나타냅니다.
- 책임: 객체가 맡아서 수행하거나 알고 있어야 하는 일입니다.
- 런타임: 프로그램이 실제로 실행되는 동안 필요한 환경과 동작을 통틀어 부르는 말입니다.
다음 장으로 연결
먼저 오늘 발표가 답하려는 질문을 두 개로 줄여 보겠습니다.
02. 오늘의 중심 질문
이 장의 역할
청중이 발표 내내 놓치지 않아야 할 두 질문을 고정합니다.
앞 장에서 이어가기
제목에는 여러 단어가 있지만, 결국 오늘 답할 질문은 두 개입니다.
말하기
첫 번째 질문은 코드를 무엇을 기준으로 나눌 것인가입니다.
두 번째 질문은 그렇게 작성한 코드가 컴퓨터에서 어떻게 실행되는가입니다.
첫 번째는 설계의 질문이고, 두 번째는 실행의 질문입니다. 서로 다른 층의 질문이지만 실제 개발에서는 계속 이어집니다. 예를 들어 우리가 Quiz라는 클래스를 설계하는 일은 개발자가 하지만, 그 클래스의 객체를 메모리에 만들고 메서드를 실행하는 일은 언어와 런타임이 처리합니다.
오늘은 이 두 층을 따로 외우지 않고, 설계한 것이 어떻게 표현되고 실행되는지까지 연결해서 보겠습니다.
용어·개념 메모
- 설계: 코드를 어떤 단위와 관계로 구성할지 결정하는 일입니다.
- 실행: 작성된 코드가 메모리와 CPU를 사용해 실제 동작으로 이어지는 과정입니다.
- 층: 질문의 성격을 구분하기 위한 관점입니다. 물리적으로 완전히 떨어진 세 상자가 아니라, 설명을 정리하기 위한 구분입니다.
다음 장으로 연결
두 질문 사이에서 길을 잃지 않도록, 오늘 이동할 순서를 먼저 보겠습니다.
03. 발표의 지도
이 장의 역할
발표 전체의 순서와 질문의 위치를 미리 보여 줍니다.
앞 장에서 이어가기
첫 질문에서 시작해 두 번째 질문으로 내려가는 경로는 이렇게 구성되어 있습니다.
말하기
앞부분에서는 먼저 코드를 왜 나누는지 보고, 관심사와 책임을 찾습니다. 그다음 상태와 행동을 객체로 묶고, 그 경계를 클래스로 표현합니다.
뒷부분에서는 실행으로 내려갑니다. 런타임이 무엇인지 확인한 뒤 C, Java, CPython의 실행 경로를 비교하고, 마지막에 메모리 정리와 GC까지 연결합니다.
여기서 C와 Java를 보는 이유는 다른 언어를 많이 배우기 위해서가 아닙니다. 같은 소스 코드가 서로 다른 실행 경로를 선택한다는 것을 비교해야 Python과 CPython의 특징도 더 선명하게 보이기 때문입니다. 이 이유는 16페이지에서 다시 분명히 설명하겠습니다.
용어·개념 메모
- C / Java / Python: 이 발표에서는 언어 문법을 비교하지 않고, 소스가 실행되는 경로만 비교합니다.
- 메모리: 실행 중인 코드와 데이터가 올라가고 사용되는 공간입니다.
- GC: 더 이상 사용할 수 없는 객체의 메모리를 찾아 회수하는 메커니즘입니다. 언어와 구현에 따라 방식이 다릅니다.
질문이 나오면
질문을 바로 길게 답하기 전에 다음 중 어디에 관한 질문인지 확인합니다.
- 왜 이렇게 나누었는가 — 설계 층
- 언어가 어떻게 표현하는가 — 언어 층
- 컴퓨터가 어떻게 실행하는가 — 런타임 층
다음 장으로 연결
이제 첫 번째 질문, 코드를 무엇을 기준으로 나눌 것인가부터 시작하겠습니다.
04. PART 1 — 객체의 역할과 책임
이 장의 역할
클래스 문법보다 관심사와 책임이 먼저라는 1부의 방향을 선언합니다.
말하기
객체지향을 배울 때 흔히 class 문법부터 시작합니다. 하지만 문법부터 시작하면 왜 클래스를 만드는지는 놓치기 쉽습니다.
이번에는 순서를 반대로 가겠습니다. 먼저 코드가 왜 복잡해지는지 보고, 어떤 문제와 책임을 분리해야 하는지 찾습니다. 객체와 클래스는 그 결과로 등장합니다.
용어·개념 메모
- 객체지향: 프로그램을 역할과 책임을 가진 객체들의 협력으로 바라보는 설계 관점입니다. 클래스를 많이 만드는 기술과 같은 뜻이 아닙니다.
- 클래스 문법:
class, 생성자, 메서드처럼 언어가 제공하는 표현 방법입니다.
다음 장으로 연결
그렇다면 코드가 작을 때는 괜찮아 보이는데, 왜 굳이 나누어야 할까요?
05. 코드는 왜 나누는가?
이 장의 역할
코드를 나누는 목적이 파일 개수를 늘리는 것이 아니라 변경의 영향을 통제하는 것임을 보여 줍니다.
앞 장에서 이어가기
먼저 한 파일에 여러 기능이 섞였을 때 어떤 문제가 생기는지 보겠습니다.
말하기
작은 프로그램에서는 한 파일에 모든 기능이 있어도 당장 문제가 없어 보입니다. 하지만 기능이 늘어나면 문제 데이터, 사용자 입력, 저장, 점수 계산, 출력이 한곳에서 서로 얽힙니다.
오른쪽 다섯 요소는 단순한 기능 목록이 아닙니다. 각각 바뀌는 이유가 다른 역할입니다.
- 문제는 질문과 선택지, 정답을 관리합니다.
- 입력은 사용자의 선택을 받습니다.
- 저장은 데이터를 기록하고 다시 불러옵니다.
- 점수는 정답 결과를 계산하고 누적합니다.
- 출력은 사용자에게 정보를 보여 줍니다.
질문 내용이 바뀌는 것과 저장 형식이 바뀌는 것은 서로 다른 변화입니다. 그런데 코드가 한곳에 섞여 있으면 무엇을 고쳤을 때 어디까지 영향을 받는지 보기 어렵습니다.
그래서 코드를 나누는 핵심 목적은 각 요소가 맡은 역할을 명확히 하고, 하나의 변경이 영향을 주는 범위를 작게 만드는 것입니다.
용어·개념 메모
- 변경 이유: 코드가 수정되는 원인입니다. 화면 형식 변경, 저장 방식 변경, 점수 규칙 변경은 서로 다른 변경 이유입니다.
- 수정 영향 범위: 한 부분을 고쳤을 때 함께 확인하거나 수정해야 하는 코드의 범위입니다.
- 기능: 프로그램이 제공하는 동작입니다.
- 역할: 그 기능들 가운데 한 대상이 외부에 제공하기로 한 몫입니다.
헷갈리면 이렇게 구분
- 파일을 여러 개로 나누었다고 책임이 자동으로 나뉘는 것은 아닙니다.
- 한 파일 안에서도 함수와 객체의 책임을 분리할 수 있습니다.
- 반대로 파일은 여러 개인데 같은 책임이 여기저기 흩어져 있으면 경계가 명확하지 않습니다.
다음 장으로 연결
나누어야 한다는 이유는 알았습니다. 이제 무엇을 기준으로 나눌지 정해야 합니다.
06. 나누는 기준: 관심사 · 책임 · 역할
이 장의 역할
비슷하게 들리는 세 단어를 질문의 순서로 구분합니다.
앞 장에서 이어가기
변경 이유가 다른 코드를 나누려면, 먼저 세 가지 질문을 순서대로 던집니다.
말하기
첫 번째는 관심사입니다. 이 코드는 어떤 문제를 해결하려는가를 묻습니다.
두 번째는 책임입니다. 그 문제에 대해 이 대상이 무엇을 해야 하는가를 묻습니다.
세 번째는 역할입니다. 다른 대상은 이 대상에게 무엇을 요청할 수 있는가를 묻습니다.
예를 들어 저장이라는 관심사를 찾았다면, 파일을 읽고 쓰며 저장 데이터를 검증하는 것이 Storage의 책임이 될 수 있습니다. QuizGame 입장에서는 save()나 load()를 요청할 수 있는 대상이라는 것이 Storage의 역할입니다.
정리하면 문제 영역을 찾고, 그 안에서 할 일을 정하고, 협력자가 기대할 수 있는 약속을 만든다는 순서입니다.
용어·개념 메모
- 관심사(Concern): 코드가 다루는 문제나 주제입니다. “무엇에 관한 코드인가?” 또는 “어떤 문제를 해결하려는가?”로 찾습니다.
- 책임(Responsibility): 그 문제에 관해 대상이 알고 있거나 수행해야 하는 일입니다.
- 역할(Role): 협력 관계에서 다른 대상이 기대하는 기능의 묶음입니다. 실제 구현보다 외부의 기대에 초점을 둡니다.
- 약속: 다른 코드가 사용할 수 있도록 공개된 호출 방식과 의미입니다. 메서드 이름만이 아니라 입력, 결과, 동작의 의미까지 포함합니다.
헷갈리면 이렇게 구분
- 관심사는 문제의 종류에 가깝습니다.
- 책임은 그 문제에서 내가 맡은 일에 가깝습니다.
- 역할은 다른 대상이 나를 어떻게 사용하는가에 가깝습니다.
세 단어가 완전히 분리된 사전적 칸은 아닙니다. 설계를 논리적으로 설명하기 위한 질문의 순서로 이해하면 됩니다.
다음 장으로 연결
이 기준을 실제 퀴즈 레포에 적용하면 어떤 경계가 보이는지 확인해 보겠습니다.
07. 레포를 보면 관심사가 보인다
이 장의 역할
추상적인 세 기준이 실제 코드의 세 경계로 이어지는 과정을 보여 줍니다.
앞 장에서 이어가기
관심사를 실제 코드에 대입하면 퀴즈, 저장, 게임 진행이라는 세 문제 영역이 보입니다.
말하기
Quiz는 퀴즈 한 문제를 표현합니다. 문제, 선택지, 정답을 보관하고 정답 여부를 확인합니다.
Storage는 저장과 복구를 맡습니다. state.json을 읽고 쓰며 저장할 데이터가 올바른지도 확인합니다.
QuizGame은 게임 진행을 맡습니다. 메뉴를 보여 주고, 사용자 입력을 받고, 점수를 관리하며 전체 흐름을 이어 갑니다.
중요한 점은 처음부터 클래스 세 개를 만들기로 정한 것이 아니라는 것입니다. 코드를 보니 퀴즈, 저장, 게임 진행이라는 서로 다른 관심사와 변경 이유가 보였고, 그 경계를 클래스로 표현한 것입니다.
용어·개념 메모
- 레포(repository): 프로젝트의 소스 코드와 관련 파일을 모아 관리하는 저장소입니다. 여기서는 실제 퀴즈 프로젝트 전체를 뜻합니다.
- JSON: 키와 값 구조로 데이터를 표현하는 텍스트 형식입니다.
state.json은 저장 상태를 담는 파일입니다. - 검증(validation): 읽거나 저장하려는 데이터가 필요한 형식과 조건을 만족하는지 확인하는 일입니다.
- 게임 흐름: 시작, 메뉴 선택, 문제 제시, 입력, 채점, 종료처럼 실행 순서를 조정하는 책임입니다.
헷갈리면 이렇게 구분
화살표가 Quiz → Storage → QuizGame이라는 처리 순서를 뜻하는 것은 아닙니다. 이 장의 핵심은 세 클래스의 책임을 나란히 비교하는 것입니다. 실제 협력에서는 QuizGame이 Quiz와 Storage를 사용할 수 있습니다.
다음 장으로 연결
이렇게 나눈 책임을 프로그램 안에서 하나의 대상으로 바라보는 단위가 객체입니다.
08. 객체란 무엇인가?
이 장의 역할
객체를 단순 데이터 묶음이 아니라 상태와 행동을 가진 협력 단위로 설명합니다.
앞 장에서 이어가기
퀴즈라는 관심사와 책임을 하나의 대상으로 묶으면
Quiz객체로 볼 수 있습니다.
말하기
객체는 상태와 행동을 하나의 대상으로 묶어 보는 방식입니다.
Quiz 객체의 상태에는 question, choices, answer가 있습니다. 행동에는 문제를 보여 주는 display(), 정답을 확인하는 is_correct(), 저장할 형태로 바꾸는 to_dict()가 있습니다.
상태만 모으면 데이터 묶음일 수 있습니다. 객체는 그 상태를 어떤 규칙으로 다루는지까지 함께 가집니다. 그래서 다른 객체가 내부 값을 하나씩 직접 만지는 대신, is_correct() 같은 행동을 요청하며 협력할 수 있습니다.
용어·개념 메모
- 상태(state): 객체가 현재 가지고 있는 데이터입니다.
- 행동(behavior): 객체가 자신의 상태를 사용하거나 변경하며 제공하는 동작입니다.
- 정체성(identity): 값이 같더라도 서로 다른 객체로 구분될 수 있다는 성질입니다. Python 객체 모델에서는 모든 객체가 정체성, 타입, 값을 가집니다.
- 협력 단위: 다른 객체가 정해진 행동을 요청할 수 있는 하나의 대상입니다.
헷갈리면 이렇게 구분
- 객체는 현실 세계 물건만 뜻하지 않습니다. 저장소, 게임 진행, 날짜, 주문처럼 책임을 가진 개념도 객체가 될 수 있습니다.
- 모든 데이터를 반드시 객체 하나에 넣어야 한다는 뜻도 아닙니다. 책임과 변경 이유가 함께 움직이는 상태와 행동을 묶는 것이 핵심입니다.
다음 장으로 연결
그렇다면 이 객체가 어떤 상태와 행동을 갖는지 코드로 표현할 도구가 필요합니다.
09. 클래스는 무엇인가?
이 장의 역할
클래스를 객체지향 그 자체가 아니라 객체를 표현하는 언어 도구로 위치시킵니다.
앞 장에서 이어가기
객체의 상태와 행동을 Python 코드로 정의할 때 사용할 수 있는 대표적인 도구가 클래스입니다.
말하기
클래스는 객체가 어떤 상태를 가지며 어떤 행동을 제공하는지 표현하는 언어 도구입니다.
__init__은 새 객체를 만들 때 필요한 초기 상태를 설정합니다. 클래스 안의 함수는 객체가 제공하는 메서드가 됩니다. 이 클래스를 바탕으로 실제로 만들어진 각각의 대상이 인스턴스입니다.
하지만 class 키워드를 사용했다는 사실과 책임의 경계를 잘 잡았다는 사실은 같지 않습니다. 클래스는 우리가 정한 경계를 코드로 표현해 줄 뿐, 좋은 경계를 대신 결정해 주지는 않습니다.
용어·개념 메모
- 클래스(class): 객체의 상태 구조와 행동을 정의하는 틀입니다.
- 인스턴스(instance): 클래스를 바탕으로 런타임에 만들어진 실제 객체입니다.
__init__: Python 인스턴스가 생성된 뒤 초기 상태를 설정하는 초기화 메서드입니다. 엄밀히 말해 객체 메모리 자체를 만드는 메서드는__new__이지만 이번 발표 범위에서는 구분하지 않아도 됩니다.- 메서드(method): 클래스에 정의되어 객체나 클래스와 연결되어 호출되는 함수입니다.
- 인스턴스 속성: 각 객체가 따로 가지고 있는 상태입니다. 예:
self.question.
질문이 나오면
self가 무엇인지 질문이 나오면 “현재 메서드 호출의 대상이 된 인스턴스를 가리키는 관례적인 매개변수 이름”이라고 짧게 답합니다. 오늘의 핵심은 self 문법이 아니라 클래스가 책임의 경계를 표현한다는 점이라고 다시 범위를 잡습니다.
다음 장으로 연결
클래스가 도구라는 사실을 놓치면 모든 기능을 클래스 하나에 넣고도 객체지향이라고 생각할 수 있습니다.
10. 클래스는 도구일 뿐이다
이 장의 역할
문법적으로 가능한 것과 설계적으로 좋은 것을 구분합니다.
앞 장에서 이어가기
클래스 문법만 사용한다고 좋은 경계가 자동으로 생기지는 않습니다.
말하기
Everything 클래스처럼 로그인, 저장, 게임 진행, 출력, 점수 계산을 모두 한 클래스에 넣어도 문법적으로는 동작할 수 있습니다.
하지만 저장 형식이 바뀌어도 이 클래스를 고쳐야 하고, 화면이 바뀌어도 고쳐야 하며, 점수 규칙이 바뀌어도 다시 고쳐야 합니다. 서로 다른 변경 이유가 한곳에 모였기 때문입니다.
좋은 설계는 클래스 개수를 많이 만드는 것이 아닙니다. 이 클래스가 무엇을 맡고 있으며 왜 바뀌는지 한 문장으로 설명할 수 있는지를 봅니다.
용어·개념 메모
- 거대 클래스 / God Object: 너무 많은 상태와 책임을 한곳에 모은 객체나 클래스를 가리키는 표현입니다.
- 응집도: 한 모듈이나 클래스 안의 요소들이 하나의 목적을 위해 얼마나 밀접하게 모여 있는지 나타내는 개념입니다. 책임이 분명할수록 보통 응집도가 높다고 말합니다.
- 단위 테스트: 작은 기능 단위를 독립적으로 검증하는 테스트입니다. 책임이 섞이면 준비해야 할 상태와 의존성이 늘어 테스트하기 어려워집니다.
다음 장으로 연결
결국 같은 코드 재료도 어떤 문제의 대상으로 바라보느냐에 따라 전혀 다르게 묶입니다.
11. 재료보다 중요한 것은 관점이다
이 장의 역할
객체의 경계가 데이터 자체에서 자동으로 나오지 않고, 해결하려는 문제에 대한 개발자의 관점에서 나온다는 점을 설명합니다.
앞 장에서 이어가기
클래스 개수보다 먼저 필요한 것은, 지금 어떤 문제를 하나의 대상으로 보고 있는가라는 관점입니다.
말하기
같은 밥도 무엇을 만들려는지에 따라 볶음밥, 삼각김밥, 비빔밥이라는 다른 대상으로 묶입니다.
코드의 문자열, 리스트, 숫자, 함수도 그 자체로 Quiz나 Storage가 아닙니다. 개발자가 어떤 문제를 해결하는 대상으로 볼 것인지 경계를 정해야 합니다.
퀴즈라는 관점으로 보면 질문, 선택지, 정답, 정답 확인이 함께 움직입니다. 저장이라는 관점으로 보면 파일 읽기, 쓰기, 검증이 함께 움직입니다. 클래스는 이렇게 정한 관점을 코드에 이름 붙여 표현합니다.
용어·개념 메모
- 관점: 같은 데이터와 기능을 어떤 문제를 해결하는 단위로 해석하는지에 대한 시각입니다.
- 추상화: 복잡한 세부사항에서 현재 문제에 중요한 특징을 뽑아 하나의 개념으로 표현하는 일입니다.
- 경계: 무엇을 한 책임 안에 포함하고 무엇을 밖에 둘지 정한 선입니다.
다음 장으로 연결
경계를 잘 정하면 객체들은 서로의 내부를 모두 몰라도 필요한 약속만으로 협력할 수 있습니다.
12. 객체는 내부를 몰라도 협력할 수 있다
이 장의 역할
좋은 분리가 단절이 아니라 안정된 약속을 통한 협력임을 보여 줍니다.
앞 장에서 이어가기
객체를 나눴다고 서로 관계가 없어지는 것은 아닙니다. 필요한 방식으로 연결되어야 합니다.
말하기
QuizGame은 저장을 해야 하지만 Storage가 파일을 어떤 순서로 열고 JSON을 어떻게 쓰는지까지 알 필요는 없습니다.
QuizGame은 save(quizzes, best_score)라는 약속만 알고 요청합니다. Storage는 그 요청을 받아 자신의 방식으로 저장합니다.
나중에 저장 방식이 JSON 파일에서 데이터베이스나 원격 서버로 바뀌더라도 호출 약속이 유지되면 게임 진행 코드는 그대로 둘 수 있습니다.
좋은 분리는 서로 관계가 없는 상태가 아니라, 필요한 약속만 알고 불필요한 내부 구현에는 덜 의존하는 상태입니다.
용어·개념 메모
- 인터페이스: 다른 코드가 사용할 수 있도록 외부에 드러난 약속입니다. 여기서는
save()의 호출 방법과 의미를 뜻합니다. - 구현: 그 약속을 실제로 수행하는 내부 코드입니다.
- 캡슐화: 내부 상태와 구현 세부사항을 한 경계 안에 두고, 외부에는 필요한 사용 방법만 제공하는 것입니다.
- 결합도: 코드 단위들이 서로의 구체적인 내용에 얼마나 강하게 의존하는지를 나타냅니다.
- DB: Database의 줄임말입니다. 데이터를 구조적으로 저장하고 조회하는 시스템입니다.
헷갈리면 이렇게 구분
- “내부를 모른다”는 것은 아무것도 모른다는 뜻이 아닙니다.
- 외부 코드는 메서드 이름, 필요한 입력, 반환 결과, 실패 조건 같은 공개 약속은 알아야 합니다.
- 감추려는 것은 파일 처리 순서나 데이터 구조 같은 구현 세부사항입니다.
다음 장으로 연결
여기까지의 흐름을 클래스가 아니라 책임의 순서로 다시 정리하겠습니다.
13. 1부에서 기억할 흐름
이 장의 역할
1부의 핵심을 구체적인 네 단계로 고정하고 2부로 넘어갈 준비를 합니다.
앞 장에서 이어가기
지금까지 나온 용어를 하나의 설계 순서로 모으면 네 단계입니다.
말하기
첫째, 코드에서 서로 다른 문제와 변경 이유를 찾아 관심사를 나눕니다.
둘째, 각 관심사가 알고 수행해야 할 일을 책임으로 정합니다.
셋째, 함께 움직이는 상태와 행동을 객체라는 협력 단위로 묶습니다.
넷째, 그 객체의 경계를 언어가 제공하는 클래스로 표현합니다.
그래서 1부에서 남길 문장은 이것입니다.
클래스를 먼저 만드는 것이 아닙니다. 나누어야 할 관심사와 책임이 먼저이고, 클래스는 그 경계를 표현하는 도구입니다.
용어·개념 메모
- 관심사 → 책임 → 객체 → 클래스: 반드시 모든 설계가 기계적으로 이 네 단계를 따르는 것은 아닙니다. 초심자가 클래스부터 만들지 않도록 사고 순서를 잡아 주는 설명 틀입니다.
- 협력: 객체가 서로의 공개된 행동을 요청하며 전체 기능을 완성하는 과정입니다.
다음 장으로 연결
이제 이렇게 설계하고 작성한 클래스와 객체가 컴퓨터에서 실제로 어떻게 움직이는지 실행 층으로 내려가겠습니다.
14. PART 2 — 컴퓨터에서 코드는 어떻게 실행되는가?
이 장의 역할
설계 층에서 실행 층으로 주제가 전환된다는 것을 분명히 알립니다.
앞 장에서 이어가기
1부에서는 어떤 경계를 만들지 결정했습니다. 2부에서는 그 경계가 코드가 된 뒤 실제로 어떻게 실행되는지 봅니다.
말하기
Quiz, Storage, QuizGame을 잘 나누어도 코드가 실행되지 않으면 프로그램은 동작하지 않습니다.
이제부터는 클래스 설계가 좋고 나쁜지를 판단하는 이야기가 아닙니다. 소스 코드가 어떤 중간 형태로 바뀌고, 어떤 실행 환경을 거쳐, 결국 CPU와 메모리 위에서 동작하는지를 봅니다.
용어·개념 메모
- 소스 코드: 개발자가 C, Java, Python 같은 프로그래밍 언어로 작성한 텍스트입니다.
- 실행 환경: 프로그램이 동작하기 위해 필요한 운영체제, 라이브러리, VM, 인터프리터 같은 조건과 구성입니다.
- CPU: 명령을 계산하고 실행하는 하드웨어입니다.
다음 장으로 연결
실행 과정을 비교하기 전에 먼저 런타임이라는 말의 범위를 잡겠습니다.
15. 런타임이란 무엇인가?
이 장의 역할
런타임을 단순한 시간 표현이 아니라 실행 중 필요한 환경과 동작으로 이해하게 합니다.
앞 장에서 이어가기
코드가 실제로 움직이는 동안에는 단순히 명령만 실행되는 것이 아니라 여러 관리 작업이 함께 일어납니다.
말하기
런타임은 문맥에 따라 두 가지 의미로 쓰입니다.
첫째는 프로그램이 실행 중인 시간, 즉 실행 시점입니다. 둘째는 그 실행을 가능하게 하고 관리하는 환경과 구성 요소입니다.
프로그램이 실행되는 동안에는 메모리를 사용하고, 함수를 호출하고, 객체를 만들고, 라이브러리를 사용하고, 오류를 처리하고, 필요 없어진 메모리를 정리합니다.
C, Java, Python 모두 이런 실행 과정이 있습니다. 차이는 런타임이 어디까지 관여하며 개발자가 무엇을 직접 책임지는가입니다.
용어·개념 메모
- 런타임(runtime): 실행 시점 또는 실행을 지원하는 시스템을 뜻합니다. 발표에서는 주로 두 번째 의미까지 포함합니다.
- 함수 호출: 현재 실행하던 위치를 기억하고 다른 함수의 코드를 실행한 뒤 다시 돌아오는 과정입니다.
- 객체 생성: 런타임이 객체를 위한 메모리를 확보하고 초기 상태를 만드는 과정입니다.
- 라이브러리: 자주 필요한 기능을 미리 구현해 둔 코드 묶음입니다.
- 오류 처리: 실행 중 발생한 문제를 감지하고 전달하거나 복구하는 과정입니다.
- 메모리 정리: 더 이상 사용하지 않는 메모리를 다시 사용할 수 있게 반환하는 일입니다.
헷갈리면 이렇게 구분
- 컴파일 타임: 실행 전에 코드를 검사하거나 다른 형태로 변환하는 시점입니다.
- 런타임: 프로그램이 실제로 실행되는 시점입니다.
- 런타임 시스템: 실행 중 프로그램을 지원하는 VM, 인터프리터, 라이브러리 등의 구성 요소입니다.
다음 장으로 연결
이제 같은 소스 코드가 언어마다 어떤 실행 모델을 선택하는지 비교하겠습니다. 이 비교는 Python을 벗어나는 내용이 아니라 Python의 선택을 더 정확히 보기 위한 기준입니다.
16. 언어 비교는 실행 모델 비교다
이 장의 역할
C와 Java를 왜 함께 보는지 설득하고, 이후 17~26장의 비교 기준을 고정합니다.
앞 장에서 이어가기
런타임이 관여하는 범위가 언어마다 다르다고 했습니다. 그 차이를 가장 쉽게 보려면 같은 질문을 세 언어에 던져야 합니다. “내가 쓴 소스 코드는 CPU까지 어떤 길을 지나가는가?”입니다.
말하기
여기서 C와 Java를 보는 이유는 Python 발표의 범위를 넓히기 위해서가 아닙니다.
Python만 보면 바이트코드, 인터프리터, 런타임, GC가 원래 당연히 존재하는 것처럼 보일 수 있습니다. 하지만 C와 Java의 실행 경로를 나란히 놓으면, 이것들이 모든 언어에 똑같이 붙는 필수 부품이 아니라 각 언어와 구현이 선택한 실행 방식이라는 점이 보입니다.
먼저 C는 보통 실행 전에 현재 운영체제와 CPU가 바로 실행할 수 있는 네이티브 실행파일을 만듭니다.
Java는 소스를 .class 파일의 JVM 바이트코드로 바꾸고, JVM이 그 바이트코드를 실행합니다.
Python은 이 발표에서 가장 널리 쓰이는 구현인 CPython을 기준으로 봅니다. CPython은 Python 소스를 내부 Python 바이트코드로 만들고, CPython 인터프리터가 그 명령을 실행합니다.
이 세 경로를 비교하면 Python의 특징을 더 정확히 말할 수 있습니다. Python이 단순히 “컴파일하지 않는 언어”인 것이 아니라, 네이티브 실행파일을 미리 만드는 대신 CPython 안에서 사용할 중간 명령을 만들고 실행하는 모델이라는 것을 볼 수 있습니다.
따라서 여기서 비교하는 것은 언어의 우열이나 문법이 아닙니다. 오직 소스 코드가 실행되기까지 어떤 중간 형태와 실행 환경을 거치는가입니다.
슬라이드의 세 박스를 한 줄씩 풀어 말하기
- C — 실행 전에 네이티브 실행파일 생성
- C 소스를 컴파일하고 링크해 운영체제가 로드할 실행파일을 만듭니다.
- 일반적으로 프로그램을 실행할 때 소스나 컴파일러가 다시 필요하지 않습니다.
- Java —
.class바이트코드 + JVMjavac가 Java 소스를 CPU 명령이 아닌 JVM 명령으로 바꿉니다.- 실행할 때 해당 환경의 JVM이 필요합니다.
- Python / CPython — Python 바이트코드 + CPython 인터프리터
- CPython이 소스를 내부 바이트코드로 컴파일합니다.
- CPython 인터프리터가 실행 중 그 바이트코드를 처리합니다.
용어·개념 메모
- 실행 모델: 소스 코드가 어떤 중간 표현을 거치고, 누가 그 표현을 실행하며, 언제 네이티브 명령으로 이어지는지를 설명하는 방식입니다.
- 네이티브(native): 현재의 CPU와 운영체제 환경에 맞는다는 뜻입니다.
- 네이티브 실행파일: 운영체제가 메모리에 로드해 현재 CPU에서 실행할 수 있도록 만든 바이너리 파일입니다. macOS의 Mach-O, Linux의 ELF, Windows의 PE 같은 형식이 있습니다.
- 바이너리(binary): 사람이 읽는 소스 텍스트가 아니라 컴퓨터가 정해진 구조로 해석하는 바이트 데이터입니다.
- 바이트코드(bytecode): 특정 물리 CPU가 아니라 VM이나 인터프리터가 이해하도록 만든 중간 명령입니다. Java 바이트코드와 CPython 바이트코드는 서로 다른 명령 체계입니다.
- VM(Virtual Machine): 실제 CPU와 완전히 같은 물리 장치가 아니라, 정해진 가상 명령과 실행 환경을 소프트웨어로 제공하는 시스템입니다.
- 인터프리터(interpreter): 프로그램의 명령을 읽고 그 의미에 해당하는 동작을 수행하는 실행기입니다. “소스 텍스트를 한 줄씩만 읽는다”로 한정하면 부정확합니다. CPython은 바이트코드를 실행합니다.
- 컴파일러(compiler): 한 표현의 코드를 다른 표현으로 변환하는 프로그램입니다. 결과가 반드시 CPU 기계어일 필요는 없습니다. Java와 CPython도 바이트코드를 만들기 위해 컴파일 단계를 사용합니다.
Python과 CPython을 반드시 구분하기
- Python은 언어입니다. 문법, 의미, 객체 모델처럼 “Python 코드가 무엇을 뜻하는가”에 대한 규칙입니다.
- CPython은 그 언어를 실행하는 대표적인 구현입니다. 대부분 C로 작성되어 있어
C + Python이라는 이름이 붙었습니다. - CPython이라고 해서 Python 소스를 먼저 C 소스로 번역한다는 뜻은 아닙니다.
- 우리가 터미널에서
python또는python3를 실행할 때 많은 환경에서 CPython이 실행되지만, Python 구현은 CPython만 있는 것이 아닙니다. PyPy, Jython 같은 다른 구현도 있습니다. - 참조 수 중심 메모리 관리와 CPython 바이트코드는 Python 언어 전체의 영원한 규칙이 아니라 CPython 구현의 특징입니다.
헷갈리면 이렇게 구분
- 언어는 코드의 뜻을 정합니다.
- 구현은 그 뜻대로 코드를 실제로 실행하는 프로그램입니다.
- 중간 표현은 소스와 최종 실행 사이에서 사용하는 명령 형식입니다.
- 실행 환경은 그 중간 표현을 읽고 메모리와 CPU 사용을 관리합니다.
발표 중 한 문장으로 줄이면
C와 Java를 배우려는 것이 아니라, Python이 선택한 실행 경로를 보이게 해 줄 비교 기준으로 사용하는 것입니다.
질문이 나오면
“그럼 C가 항상 빠르고 Python은 항상 느린가요?”라고 물으면 이렇게 답합니다.
실행 모델은 성능에 영향을 주지만, 실제 성능은 작업 종류, 구현, 최적화, 라이브러리, 입력 크기까지 함께 봐야 합니다. 오늘은 속도 순위를 정하는 것이 아니라 실행 경로의 차이를 보는 시간입니다.
다음 장으로 연결
비교 기준을 잡았으니 가장 직접적인 경로인 C부터 보겠습니다. 소스가 어떤 단계를 거쳐 네이티브 실행파일이 되는지 화살표를 따라갑니다.
17. C 언어: 소스에서 실행파일까지
이 장의 역할
“실행 전에 네이티브 실행파일을 만든다”는 16장의 한 문장을 실제 빌드 단계로 풀어 설명합니다.
앞 장에서 이어가기
16장에서 C는 실행 전에 네이티브 실행파일을 만든다고 했습니다. 지금부터 그 말 안에 숨어 있던 전처리, 컴파일, 어셈블, 링크를 하나씩 보겠습니다.
말하기
화살표를 왼쪽에서 오른쪽으로 따라가겠습니다.
첫 번째 hello.c는 개발자가 작성한 C 소스 코드입니다.
두 번째 전처리 단계에서는 #include와 #define 같은 전처리 지시를 처리합니다. 쉽게 말해 본격적인 컴파일 전에 필요한 헤더 내용을 가져오고 매크로를 치환해, 컴파일러가 처리할 소스를 준비합니다.
세 번째 컴파일 단계에서는 전처리된 C 코드를 어셈블리 수준의 표현으로 번역하고 문법과 타입을 검사하며 최적화를 적용할 수 있습니다.
네 번째 어셈블 단계에서는 어셈블리 명령을 실제 CPU 명령 바이트가 포함된 목적 파일, 여기서는 hello.o로 바꿉니다.
하지만 hello.o는 아직 완성된 실행파일이 아닙니다. printf처럼 다른 파일이나 라이브러리에 있는 기능의 실제 위치가 아직 연결되지 않았을 수 있습니다.
그래서 다섯 번째 링크 단계에서 목적 파일과 필요한 라이브러리를 묶고, 서로 참조하는 함수와 데이터의 위치를 해결합니다.
그 결과 운영체제가 로드해서 실행할 수 있는 네이티브 실행파일이 만들어집니다.
이 전체 과정을 보통 gcc hello.c -o hello 한 명령으로 요청할 수 있습니다. 한 줄로 실행했기 때문에 단계가 없는 것이 아니라, GCC가 여러 도구와 단계를 이어서 수행한 것입니다.
화살표별 상세 메모
hello.c— 소스 코드- 사람이 C 문법으로 작성한 텍스트입니다.
.c확장자는 C 소스라는 관례이자 도구가 입력 종류를 판단하는 기준입니다.
- 전처리(preprocessing)
#include <stdio.h>는 헤더의 선언을 사용할 수 있게 준비합니다.#define SIZE 10같은 매크로를 치환합니다.- 조건부 컴파일 지시문도 처리합니다.
- 컴파일(compilation proper)
- C 문법과 타입을 분석합니다.
- 보통 어셈블리 코드
.s를 만들 수 있습니다. - GCC 내부 구현에서는 여러 중간 표현을 더 거칠 수 있지만 발표에서는 생략합니다.
- 어셈블(assembly)
- 어셈블러가 어셈블리 명령을 목적 파일의 기계어 코드 조각으로 바꿉니다.
hello.o— 목적 파일(object file)- 현재 CPU용 기계어 코드 조각을 가질 수 있습니다.
- 아직 최종 주소가 확정되지 않은 심볼과 재배치 정보를 포함할 수 있습니다.
- 운영체제가 일반 프로그램처럼 바로 실행하는 최종 파일은 아닙니다.
- 링크(linking)
- 여러
.o파일과 라이브러리를 하나로 연결합니다. - 함수 이름 같은 심볼이 실제 어느 코드와 연결되는지 해결합니다.
- 여러
- 실행파일(executable)
- 운영체제 로더가 읽을 수 있는 파일 형식입니다.
- 메모리에 코드와 데이터를 배치한 뒤 프로그램 시작 지점으로 제어를 넘길 수 있습니다.
용어·개념 메모
- 헤더(header): 함수, 타입, 상수 등의 선언을 다른 소스 파일에서 사용할 수 있게 제공하는 파일입니다. 보통
.h확장자를 사용합니다. - 매크로(macro): 전처리 단계에서 텍스트나 표현을 치환하는 규칙입니다.
- 어셈블리(assembly language): CPU 기계어와 가까운 사람이 읽을 수 있는 기호 형태의 명령 표현입니다.
- 기계어(machine code): CPU가 직접 해석해 실행하는 이진 명령입니다. CPU 아키텍처마다 명령 체계가 다릅니다.
- 목적 파일(object file): 컴파일과 어셈블을 거친 코드 조각입니다. 여기서 object는 객체지향의 객체가 아니라 링크할 대상 파일이라는 뜻입니다.
- 심볼(symbol): 함수나 전역 변수처럼 이름으로 참조되는 대상의 정보입니다.
- 재배치(relocation): 링크나 로드 과정에서 코드와 데이터의 실제 위치가 정해질 때 주소를 맞추기 위한 정보와 작업입니다.
- 링커(linker): 목적 파일과 라이브러리의 참조를 해결해 실행파일이나 라이브러리를 만드는 도구입니다.
- 라이브러리(library): 재사용 가능한 함수와 코드의 묶음입니다. 정적 라이브러리와 동적 라이브러리 형태가 있습니다.
- 로더(loader): 운영체제가 실행파일을 메모리에 배치하고 실행을 시작하게 하는 구성 요소입니다.
- 네이티브 실행파일: 특정 플랫폼의 운영체제와 CPU를 대상으로 만들어진 최종 바이너리입니다. 다른 CPU나 운영체제에서 그대로 실행되지 않을 수 있습니다.
GCC 명령 메모
gcc -E hello.c: 전처리까지만 수행합니다.gcc -S hello.c: 컴파일 후 어셈블리 파일 단계에서 멈춥니다.gcc -c hello.c: 목적 파일을 만들고 링크는 하지 않습니다.gcc hello.c -o hello: 필요한 단계를 이어 실행파일을 만듭니다.
헷갈리면 이렇게 구분
- 컴파일이라는 말을 넓게 쓰면 전처리부터 링크까지 전체 빌드 과정을 가리키기도 합니다.
- 이 슬라이드에서는 단계를 정확히 보기 위해 전처리, 컴파일, 어셈블, 링크를 따로 부릅니다.
.o에는 기계어가 들어갈 수 있지만 최종 실행파일은 아닙니다.- 네이티브 실행파일도 운영체제 로더와 시스템 라이브러리 등의 도움 없이 진공 상태에서 실행되는 것은 아닙니다.
질문이 나오면
“왜 .o를 바로 실행할 수 없나요?”라고 물으면 이렇게 답합니다.
목적 파일은 전체 프로그램의 한 조각일 수 있고, 아직 다른 파일이나 라이브러리의 함수 위치가 해결되지 않았을 수 있습니다. 링크 단계가 그 조각들을 연결해 운영체제가 로드할 최종 형식을 만듭니다.
다음 장으로 연결
C는 실행 전에 많은 결정을 끝내고 네이티브 실행파일을 만듭니다. 이 직접성은 장점이지만, 개발자가 직접 맡아야 할 책임도 함께 늘립니다.
18. C의 핵심 감각
이 장의 역할
C의 실행 모델을 세부 단계 암기가 아니라 “제어와 책임이 함께 온다”는 감각으로 정리합니다.
앞 장에서 이어가기
방금 본 빌드 과정을 한 문장으로 정리하면, C는 실행 전에 현재 플랫폼에 맞는 결과물을 만들고 많은 부분을 개발자가 직접 통제하는 모델입니다.
말하기
C의 핵심은 제어와 책임이 함께 온다는 것입니다.
개발자는 메모리를 언제 확보하고 해제할지 비교적 직접 정할 수 있습니다. 어떤 라이브러리를 어떻게 링크할지, 어떤 컴파일 옵션을 사용할지도 선택할 수 있습니다. 소스와 생성된 어셈블리나 실행파일 사이의 경로를 비교적 직접 추적할 수도 있습니다.
이런 제어는 시스템 프로그래밍과 성능 최적화에 유리할 수 있습니다.
반대로 메모리 해제 시점을 잘못 정하면 메모리 누수나 잘못된 메모리 접근이 생길 수 있습니다. 빌드와 링크 설정, 운영체제와 CPU 차이도 개발자가 더 직접 다뤄야 합니다.
그래서 “C가 더 자유롭다”와 “C에서 개발자가 더 많은 책임을 진다”는 말은 따로 떨어진 장단점이 아니라 같은 선택의 두 면입니다.
슬라이드의 두 영역 풀어 말하기
장점 쪽
- 낮은 수준 제어: 메모리 배치와 시스템 자원에 더 가까이 접근할 수 있습니다.
- 작은 런타임 의존성: Java의 JVM이나 CPython 인터프리터처럼 큰 실행기를 항상 전제로 하는 모델은 아닙니다.
- 성능 경로 확인: 컴파일 결과와 CPU에서 수행할 코드 사이를 비교적 직접 분석할 수 있습니다.
부담 쪽
- 메모리 관리 책임: 확보한 메모리의 수명과 해제를 직접 관리해야 하는 경우가 많습니다.
- 빌드·링크 이해: 여러 소스 파일과 라이브러리를 올바르게 연결해야 합니다.
- 플랫폼 차이 대응: CPU, 운영체제, 컴파일러, ABI 차이가 결과물과 동작에 영향을 줄 수 있습니다.
용어·개념 메모
- 낮은 수준(low level): 하드웨어와 운영체제의 동작에 상대적으로 가까운 추상화 수준입니다. “수준이 낮아 열등하다”는 의미가 아닙니다.
- 메모리 할당: 프로그램이 데이터를 저장할 공간을 확보하는 일입니다.
- 메모리 해제: 더 이상 필요 없는 공간을 다시 사용할 수 있도록 반환하는 일입니다.
- 메모리 누수(memory leak): 더 이상 필요하지 않지만 반환되지 않아 계속 점유되는 메모리입니다.
- 잘못된 메모리 접근: 이미 해제했거나 허용되지 않은 주소를 읽고 쓰는 문제입니다.
- 런타임 의존성: 프로그램 실행에 필요한 실행기, 라이브러리, 환경에 대한 의존입니다.
- 플랫폼: 보통 운영체제, CPU 아키텍처, 시스템 ABI 등의 조합입니다.
- ABI(Application Binary Interface): 컴파일된 코드가 함수 호출, 데이터 배치, 레지스터 사용 등을 어떤 규칙으로 맞출지 정한 바이너리 수준의 약속입니다. 본 발표에서는 이름만 알아도 충분합니다.
반드시 피할 표현
C에는 런타임이 없다.
이 표현은 너무 강합니다. C 프로그램도 시작 코드, 표준 라이브러리, 운영체제 로더, 동적 라이브러리 같은 실행 요소를 사용할 수 있습니다. 정확한 표현은 다음과 같습니다.
C는 보통 Java의 JVM이나 CPython 인터프리터처럼 언어 실행 전반을 관리하는 VM을 중심에 두지 않으며, 개발자가 직접 다루는 범위가 더 큽니다.
질문이 나오면
“C는 GC가 없나요?”라고 물으면 이렇게 답합니다.
C 언어의 일반적인 기본 모델은 Java처럼 언어 런타임이 자동으로 객체를 추적해 정리하는 GC를 제공하지 않습니다. 개발자가
malloc으로 확보한 메모리를free로 반환하는 식으로 직접 관리합니다. 별도의 GC 라이브러리를 사용할 수는 있지만 언어 기본 실행 모델과는 구분해야 합니다.
다음 장으로 연결
Java는 이 부담 일부를 JVM이라는 실행 환경으로 옮깁니다. 대신 C와 달리 소스를 바로 현재 CPU용 실행파일로 끝내지 않고 JVM용 바이트코드를 만듭니다.
19. Java: 바이트코드와 JVM
이 장의 역할
C의 네이티브 실행파일과 다른 Java의 중간 표현·실행 환경 구조를 설명합니다.
앞 장에서 이어가기
C가 실행 전에 현재 플랫폼용 결과물을 만든다면, Java는 한 단계 다른 선택을 합니다. CPU가 아니라 JVM이 이해할 명령을 먼저 만듭니다.
말하기
Main.java는 개발자가 작성한 Java 소스입니다.
javac라는 Java 컴파일러가 이 소스를 Main.class 파일로 바꿉니다. 이 안에는 현재 CPU가 바로 실행하는 명령이 아니라 JVM 바이트코드와 클래스 실행에 필요한 정보가 들어 있습니다.
java Main을 실행하면 JVM이 시작됩니다. JVM은 필요한 클래스 파일을 로드하고, 파일 구조와 바이트코드가 유효한지 검증하고, 필요한 이름과 참조를 연결한 뒤 메서드를 실행합니다.
실행할 때 JVM은 바이트코드를 인터프리터로 처리할 수도 있고, 자주 실행되는 부분을 JIT 컴파일해 현재 CPU용 네이티브 코드로 바꿀 수도 있습니다.
이 구조 덕분에 같은 .class 파일을 서로 다른 플랫폼의 호환 JVM에서 실행할 수 있습니다. 다만 “어디서나 무조건 실행된다”는 뜻은 아닙니다. 맞는 Java 버전의 JVM과 필요한 라이브러리, 운영체제 환경이 있어야 합니다.
흐름을 단계별로 보기
Main.java— Java 소스 코드javac— Java 소스를 클래스 파일로 바꾸는 컴파일러Main.class— JVM 바이트코드와 클래스 메타데이터- JVM — 클래스 파일을 로드하고 실행하는 가상 머신
- Interpreter / JIT — 바이트코드를 실행하는 대표 전략
- CPU — 최종적으로 네이티브 명령을 수행하는 하드웨어
용어·개념 메모
javac: JDK에 포함된 Java 컴파일러입니다..class파일: JVM이 읽는 플랫폼 독립적인 바이너리 형식입니다. 한 파일이 보통 하나의 클래스나 인터페이스 정보를 담습니다.- JVM(Java Virtual Machine): 클래스 파일 형식과 JVM 명령을 이해하고 실행하는 가상 머신 규격 및 그 구현을 가리킵니다.
- JVM 바이트코드: JVM 명령 집합입니다. x86이나 ARM CPU의 기계어가 아닙니다.
- 클래스 로딩(class loading): 필요한 클래스의 바이트 데이터를 찾아 JVM 안으로 가져오는 과정입니다.
- 검증(verification): 클래스 파일 구조와 바이트코드가 JVM 규칙을 지키는지 확인하는 과정입니다.
- 링킹(linking): JVM 안에서 클래스와 메서드·필드 참조를 연결하고 준비하는 과정입니다. C의 최종 네이티브 링크와 같은 말로 완전히 동일한 과정은 아닙니다.
- 메타데이터: 클래스 이름, 필드, 메서드, 상수, 타입 정보처럼 코드를 실행하는 데 필요한 부가 정보입니다.
- JDK: Java Development Kit.
javac같은 개발 도구와 실행에 필요한 구성 요소를 포함합니다. - JRE: Java Runtime Environment. Java 애플리케이션 실행 환경을 가리키는 전통적인 용어입니다. 최신 JDK 배포 방식에서는 별도 JRE 제품 구분이 예전과 달라졌지만 개념 구분용으로는 여전히 사용됩니다.
헷갈리면 이렇게 구분
javac는 Java 소스를.class로 바꿉니다.java명령은 JVM을 시작해 클래스의 메인 메서드를 실행하게 합니다..class는 네이티브 실행파일이 아닙니다.- JVM도 결국 현재 운영체제와 CPU에서 실행되는 네이티브 프로그램입니다.
질문이 나오면
“JVM은 무조건 인터프리터인가요?”라고 물으면 이렇게 답합니다.
JVM 규격은 특정 구현 전략 하나만 강제하지 않습니다. 대표적인 HotSpot JVM은 인터프리터와 JIT 컴파일을 함께 사용합니다. 어떤 코드를 언제 네이티브 코드로 바꿀지는 JVM 구현 전략입니다.
다음 장으로 연결
JVM이 바이트코드를 바로 처리할 수 있는데도 JIT 컴파일러를 함께 쓰는 이유를 다음 장에서 보겠습니다.
20. JIT는 왜 필요한가?
이 장의 역할
인터프리터와 사전 컴파일 사이에서 JIT가 어떤 문제를 해결하는지 단계적으로 설명합니다.
앞 장에서 이어가기
Java 바이트코드는 CPU 명령이 아니므로 JVM이 실행해야 합니다. 그런데 바이트코드 명령을 매번 해석하면 반복되는 코드에서도 같은 해석 비용이 계속 들 수 있습니다.
말하기
JIT는 Just-In-Time Compiler, 즉 실행 중 필요한 시점에 컴파일하는 도구입니다.
프로그램이 시작되면 JVM은 우선 바이트코드를 실행합니다. 대표적인 HotSpot JVM에서는 인터프리터가 빠르게 실행을 시작할 수 있습니다.
동시에 JVM은 어떤 메서드와 반복 경로가 자주 실행되는지 정보를 모읍니다. 이렇게 자주 실행되어 성능에 큰 영향을 주는 부분을 hot code라고 부릅니다.
JIT 컴파일러는 hot code를 현재 CPU가 직접 실행할 수 있는 네이티브 코드로 바꿉니다. 이후 같은 코드가 다시 실행되면 매번 바이트코드 명령을 하나씩 처리하는 대신 만들어 둔 네이티브 코드를 사용할 수 있습니다.
그렇다면 처음부터 모든 코드를 네이티브 코드로 만들면 되지 않을까라는 질문이 생깁니다. 컴파일 자체에도 시간과 메모리가 들고, 실제로 한 번도 실행되지 않는 코드도 많습니다. 그래서 JVM은 실행 정보를 보고 중요한 부분을 선택해 컴파일 비용과 실행 속도의 균형을 잡습니다.
슬라이드 흐름을 단계별로 말하기
- Bytecode → Interpreter
- JVM이 바이트코드를 우선 실행합니다.
- 프로그램을 시작하기 위해 모든 코드를 미리 최적화할 필요가 없습니다.
- 실행 중 프로파일링
- 어떤 메서드가 많이 호출되는지, 어떤 분기가 자주 선택되는지 관찰합니다.
- Hot code 발견
- 반복 실행되어 성능 영향이 큰 부분을 찾습니다.
- JIT Compiler
- 선택된 바이트코드를 현재 CPU용 네이티브 코드로 컴파일하고 최적화합니다.
- Native Code 재사용
- 이후 해당 경로는 컴파일된 코드를 사용해 더 빠르게 실행될 수 있습니다.
용어·개념 메모
- JIT(Just-In-Time) 컴파일: 프로그램 실행 중 실제 사용 정보를 바탕으로 코드를 네이티브 코드로 바꾸는 방식입니다.
- AOT(Ahead-Of-Time) 컴파일: 실행 전에 미리 코드를 대상 형태로 컴파일하는 방식입니다. C의 일반적인 네이티브 빌드가 대표적인 비교 예입니다.
- 인터프리터: 바이트코드 명령의 의미에 맞는 동작을 실행합니다. 빠른 시작에 유리할 수 있지만 반복 해석 비용이 생길 수 있습니다.
- 프로파일링(profiling): 실행 중 어떤 코드가 얼마나 자주, 어떤 방식으로 사용되는지 정보를 수집하는 일입니다.
- hot code / hot spot: 자주 실행되어 전체 성능에 큰 영향을 주는 코드 구간입니다.
- 네이티브 코드: 현재 CPU가 직접 실행할 수 있는 기계어 명령입니다.
- 최적화: 프로그램의 의미는 유지하면서 더 빠르거나 효율적으로 실행되도록 코드를 변환하는 작업입니다.
- 컴파일 비용: 컴파일에 드는 CPU 시간과 메모리입니다. JIT를 많이 한다고 항상 이득인 것은 아닙니다.
- 워밍업(warm-up): 프로그램이 실행 정보를 모으고 JIT 최적화를 적용하기 전후로 성능이 달라질 수 있는 현상을 설명할 때 쓰는 말입니다.
헷갈리면 이렇게 구분
- JIT는 별도의 언어가 아니라 JVM 내부 실행 전략입니다.
- JIT가 있다고 바이트코드가 사라지는 것은 아닙니다. 배포와 로딩의 입력은 여전히 클래스 파일일 수 있습니다.
- 모든 코드가 무조건 한 번에 JIT되는 것은 아닙니다.
- “인터프리터냐 컴파일러냐” 중 하나만 고르는 것이 아니라 두 방식을 함께 사용할 수 있습니다.
질문이 나오면
“JIT된 코드는 파일로 저장되나요?”라고 물으면 이렇게 답합니다.
일반적인 HotSpot 실행에서는 JIT 결과가 해당 프로세스의 메모리 안에 생성되어 그 실행 동안 사용됩니다. C 실행파일처럼 기본적으로 영구 배포 파일을 새로 만드는 과정과는 다릅니다.
“그럼 Java는 실행할수록 무조건 빨라지나요?”라고 물으면 이렇게 답합니다.
자주 실행되는 코드가 최적화되면서 빨라질 수 있지만, 실제 성능은 GC, 입력, I/O, 최적화 조건 등에도 영향을 받습니다. JIT 하나만으로 항상 단조롭게 빨라진다고 말하면 과한 단순화입니다.
다음 장으로 연결
Java가 JVM 바이트코드를 실행하듯 CPython도 내부 바이트코드를 사용합니다. 하지만 두 바이트코드는 같은 것이 아니고 실행기와 객체 모델도 다릅니다.
21. Python / CPython: 소스도 내부적으로 바이트코드가 됩니다
이 장의 역할
“Python은 컴파일하지 않는다”는 오해를 바로잡고 Python 언어와 CPython 구현의 차이를 명확히 합니다.
앞 장에서 이어가기
Java에서 소스와 CPU 사이에 바이트코드와 JVM이 있었습니다. CPython에도 소스와 실제 실행 사이에 내부 컴파일과 바이트코드 실행 단계가 있습니다.
말하기
먼저 가장 중요한 구분부터 하겠습니다.
Python은 언어이고, CPython은 그 언어를 실행하는 대표적인 프로그램입니다.
Python은 들여쓰기, 함수, 클래스, 객체, 예외 같은 문법과 의미를 정의합니다. CPython은 그 규칙에 맞춰 Python 코드를 실제로 실행하도록 대부분 C로 구현된 인터프리터입니다.
우리가 python hello.py를 실행하면, CPython은 소스 텍스트를 그대로 한 줄씩 CPU에 전달하지 않습니다.
먼저 소스를 읽고 문법 구조를 분석합니다. 그다음 실행 가능한 code object와 그 안의 Python 바이트코드를 만듭니다. CPython의 실행 루프가 바이트코드 명령을 하나씩 읽고, 각 명령에 해당하는 동작을 수행합니다.
예를 들어 a + b라는 코드가 있다면 바이트코드에는 값을 준비하고 덧셈 연산을 수행하라는 내부 명령이 들어갈 수 있습니다. CPython은 이 명령을 읽어 Python 객체의 규칙에 맞는 덧셈 동작을 실행합니다.
그래서 “Python은 컴파일을 전혀 하지 않는다”는 말은 정확하지 않습니다. CPython도 소스를 내부 바이트코드로 컴파일합니다. 다만 C처럼 실행 전에 현재 CPU용 네이티브 실행파일을 만드는 것이 기본 경로는 아닙니다.
슬라이드 흐름을 단계별로 말하기
hello.py- 개발자가 작성한 Python 소스 코드입니다.
- parser / compiler
- 소스의 문법을 분석하고 실행 가능한 내부 표현으로 컴파일합니다.
- code object + bytecode
- 함수나 모듈의 실행 정보가 code object에 들어가며, 그 안에 CPython 바이트코드가 포함됩니다.
- CPython interpreter
- 바이트코드 명령을 읽고 각 명령의 동작을 수행합니다.
- 실행
- 객체 생성, 함수 호출, 라이브러리 사용 같은 실제 프로그램 동작으로 이어집니다.
Python과 CPython의 차이 — 발표 중 헷갈리면 이 부분 보기
Python
- 프로그래밍 언어의 이름입니다.
- 어떤 문법이 유효한지, 코드가 어떤 의미를 가지는지 정의합니다.
def,class, 리스트, 예외 같은 언어 기능을 포함합니다.- 특정 구현이 내부에서 반드시 같은 바이트코드나 같은 GC를 사용해야 한다고 모두 고정하지는 않습니다.
CPython
- Python 언어의 대표적이고 가장 널리 쓰이는 구현입니다.
- 대부분 C로 작성되었습니다.
- 자체 바이트코드와 평가 루프, 객체 구조, 참조 수 기반 메모리 관리 등을 사용합니다.
- CPython 내부 바이트코드는 버전 사이에서 안정적인 배포 규격으로 보장되지 않습니다.
이름 때문에 생기는 오해
- CPython은 “C 문법과 Python 문법을 섞어 쓰는 언어”가 아닙니다.
- 일반 Python 코드가 먼저 C 코드로 번역된다는 뜻도 아닙니다.
- Python 언어를 실행하는 프로그램이 주로 C로 구현되어 있다는 뜻입니다.
다른 구현 예시
- PyPy: Python을 실행하는 다른 구현이며 JIT 기술로 잘 알려져 있습니다.
- Jython: Python 코드를 JVM 환경에서 실행하도록 만든 구현입니다.
- 이 예시는 구현마다 내부 실행 방식이 다를 수 있음을 보여 주기 위한 것입니다. 오늘 자세히 다루지는 않습니다.
용어·개념 메모
- 파서(parser): 소스의 토큰과 문법 구조를 분석해 코드의 구조를 파악하는 구성 요소입니다.
- 컴파일러: 분석된 소스를 다른 실행 표현으로 변환합니다. CPython에서는 code object와 바이트코드를 만드는 단계가 포함됩니다.
- code object: 함수나 모듈의 실행에 필요한 바이트코드, 상수, 이름, 지역 변수 정보 등을 담는 Python 객체입니다. 함수 객체 그 자체와는 구분됩니다.
- CPython 바이트코드: CPython 인터프리터가 실행하는 내부 명령입니다. Java 바이트코드와 호환되지 않습니다.
- 평가 루프 / 실행 루프: 다음 바이트코드 명령을 가져오고 그 명령의 동작을 수행하는 인터프리터의 중심 반복 구조를 설명하는 말입니다.
dis모듈: Python 코드의 CPython 바이트코드를 사람이 볼 수 있는 형태로 확인할 때 사용하는 표준 라이브러리 도구입니다..pyc파일: 모듈을 다시 불러올 때 소스를 바이트코드로 재컴파일하는 비용을 줄이기 위한 캐시입니다. 독립 네이티브 실행파일이 아닙니다.- 캐시(cache): 같은 작업을 반복하지 않도록 이전 결과를 저장해 두는 공간이나 전략입니다.
- 구현 세부사항: 언어 사용자에게 보장된 규칙이 아니라 특정 실행기 내부의 선택입니다. 버전이나 구현이 바뀌면 달라질 수 있습니다.
Java 바이트코드와 CPython 바이트코드 비교
- 둘 다 물리 CPU의 기계어가 아니라 실행기가 이해하는 중간 명령이라는 공통점이 있습니다.
- Java 클래스 파일은 JVM 규격에 맞춘 배포 형식입니다.
- CPython 바이트코드는 CPython 내부 표현이며 Python 버전 사이에서 안정적인 배포 형식으로 기대하면 안 됩니다.
- JVM과 CPython 인터프리터는 서로 다른 명령, 타입 모델, 최적화 전략을 사용합니다.
반드시 피할 표현
- “Python은 한 줄씩 읽기만 해서 컴파일을 안 한다.”
- “
.pyc는 Python 실행파일이다.” - “CPython은 Python을 C 코드로 바꾼다.”
- “Python 구현은 모두 참조 수 방식으로 똑같이 동작한다.”
질문이 나오면
“그럼 Python도 컴파일 언어인가요, 인터프리터 언어인가요?”라고 물으면 이렇게 답합니다.
두 단어 중 하나만 고르면 내부 과정을 놓칩니다. CPython은 소스를 바이트코드로 컴파일하고, 그 바이트코드를 인터프리터가 실행합니다. 그래서 컴파일 단계와 인터프리트 실행 단계가 함께 있다고 설명하는 것이 더 정확합니다.
다음 장으로 연결
CPython이 바이트코드를 실행할 때 다루는 핵심 대상은 Python 객체입니다. 이제
a = 10같은 아주 짧은 코드가 객체 모델에서 어떤 뜻인지 보겠습니다.
22. Python: 이름이 객체를 가리킨다
이 장의 역할
Python의 동적 객체 모델을 a = 10, a = "hello"의 재대입으로 설명합니다.
앞 장에서 이어가기
CPython은 바이트코드 명령을 실행하며 값을 단순한 비트 덩어리로만 다루지 않고 Python 객체 규칙에 따라 처리합니다. 그때 이름과 객체의 관계를 먼저 이해해야 합니다.
말하기
a = 10을 볼 때 a라는 상자 안에 숫자 10이 들어간다고 생각할 수 있습니다. 초반에는 편한 비유지만 Python의 동작을 더 정확히 보려면 이름이 객체를 가리킨다고 설명하는 것이 좋습니다.
첫 번째 대입 뒤에 이름 a는 값 10을 가진 int 객체와 연결됩니다.
그다음 a = "hello"를 실행하면 기존 int 객체가 문자열로 변하는 것이 아닙니다. 이름 a와 기존 객체의 연결이 바뀌고, 이제 a는 str 객체를 가리킵니다.
즉 타입은 이름 a에 영구적으로 붙어 있는 것이 아니라 객체가 가집니다. 같은 이름이 실행 중 서로 다른 타입의 객체와 연결될 수 있기 때문에 Python을 동적 타입 언어라고 설명합니다.
그림을 정확히 읽는 순서
- 첫 번째 상태: 이름
a→int객체10 - 두 번째 대입 실행
- 기존
int객체의 타입이나 값이 문자열로 변한 것이 아님 - 두 번째 상태: 이름
a→str객체"hello" - 기존
10객체에 다른 참조가 없다면 나중에 정리 대상이 될 수 있음
용어·개념 메모
- 이름(name): 현재 네임스페이스에서 객체와 연결되는 식별자입니다. 예:
a,score,Quiz. - 대입(assignment): 오른쪽 표현식의 결과 객체를 왼쪽 이름이나 대상과 연결하는 동작입니다.
- 바인딩(binding): 이름과 객체의 연결을 만드는 것입니다.
- 재바인딩(rebinding): 이미 사용하던 이름을 다른 객체와 다시 연결하는 것입니다.
- 객체(object): Python에서 데이터가 표현되는 기본 단위입니다. 모든 객체는 정체성, 타입, 값을 가집니다.
- 타입(type): 객체가 지원하는 연산과 가능한 값의 성격을 결정합니다.
int,str,list등이 타입입니다. - 값(value): 객체가 나타내는 내용입니다. 예: 정수 객체의 값 10.
- 정체성(identity): 객체가 생성된 뒤 다른 객체와 구별되는 고유한 성질입니다.
is연산은 두 참조가 같은 객체를 가리키는지 비교합니다. - 동적 타입(dynamic typing): 이름의 타입을 실행 전에 하나로 고정하기보다, 실행 중 연결된 객체의 타입에 따라 연산이 결정되는 특성입니다.
- 참조(reference): 한 곳에서 객체를 찾아갈 수 있게 하는 연결을 설명하는 말입니다. Python 수준에서 이를 C 포인터와 완전히 같은 것으로 설명할 필요는 없습니다.
- 네임스페이스(namespace): 이름과 객체의 바인딩을 관리하는 공간입니다. 지역, 전역, 클래스 네임스페이스 등이 있습니다.
헷갈리면 이렇게 구분
- 이름
a와 객체10은 같은 것이 아닙니다. a에 타입이 영구적으로 붙는 것이 아니라,a가 현재 가리키는 객체에 타입이 있습니다.- “변수”라는 말을 사용해도 괜찮지만, Python의 동작을 설명할 때는 “이름이 객체에 바인딩된다”가 더 정확합니다.
- 재대입은 기존 객체의 타입을 바꾸는 일이 아닙니다.
질문이 나오면
“그럼 a는 메모리 주소인가요?”라고 물으면 이렇게 답합니다.
Python 코드 수준에서
a는 이름입니다. CPython 내부에는 객체를 찾아가기 위한 포인터가 사용되지만, Python의 이름을 C 포인터와 동일한 개념으로 다루면 구현 세부사항과 언어 개념이 섞입니다.
“10 객체는 바로 삭제되나요?”라고 물으면 이렇게 답합니다.
다른 강한 참조가 없는 일반 객체라면 CPython에서는 참조 수가 줄어 정리될 수 있습니다. 다만 작은 정수 캐시나 불멸 객체 같은 구현 최적화가 있어 특정 객체의 실제 수명은 단순 예시와 다를 수 있습니다. 핵심은 이름의 재바인딩과 객체 변화가 다르다는 점입니다.
다음 장으로 연결
이름과 객체를 구분했으니, Python에서 무엇이 객체이고 무엇이 문법 요소인지 경계를 더 분명히 해 보겠습니다.
23. Python에서 ‘객체’인 것과 아닌 것
이 장의 역할
“Python에서는 모든 것이 객체다”라는 표현의 정확한 범위를 설명합니다.
앞 장에서 이어가기
a는 객체가 아니라 객체를 가리키는 이름이라고 했습니다. 그렇다면 Python 코드에서 정확히 무엇을 객체라고 부를 수 있을까요?
말하기
Python에서는 숫자, 문자열, 리스트뿐 아니라 사용자 정의 함수와 클래스도 객체로 다룹니다.
함수를 이름에 대입하거나 다른 함수의 인자로 전달할 수 있고, 클래스도 이름과 연결하고 전달할 수 있습니다. 코드의 실행 정보도 code object 같은 객체로 표현됩니다.
하지만 “Python에서는 모든 것이 객체다”를 “소스 코드의 모든 글자와 문법 기호가 객체다”라는 뜻으로 이해하면 안 됩니다.
if, for, return은 소스를 해석하는 문법 키워드입니다. =는 대입 문법의 일부입니다. a = 10에서 a는 이름이고, 실행 결과로 다루는 10은 int 객체입니다.
따라서 더 정확한 표현은 다음과 같습니다.
Python 프로그램에서 런타임에 다루는 데이터와 많은 코드 구성 요소가 객체 모델로 표현됩니다. 그러나 모든 문법 토큰이 객체인 것은 아닙니다.
용어·개념 메모
- 리터럴(literal): 소스에 값을 직접 표현하는 문법입니다.
10,"hello",[1, 2]등이 예입니다. 리터럴 표현을 평가하면 해당 Python 객체를 얻습니다. - 키워드(keyword): 언어 문법에서 특별한 의미를 가진 예약어입니다.
if,for,return,class등이 있습니다. - 문법 요소: 소스 코드의 구조와 의미를 정하는 규칙의 일부입니다.
- 토큰(token): 파서가 문법 분석을 위해 구분하는 최소 단위입니다. 이름, 숫자, 연산자, 키워드 등이 토큰이 될 수 있지만 토큰 그 자체와 런타임 객체는 같은 개념이 아닙니다.
- 함수 객체:
def문을 실행해 만들어지는 호출 가능한 객체입니다. - 클래스 객체:
class문을 실행해 만들어지는 객체이며, 일반적으로 호출하면 인스턴스를 생성하는 과정이 시작됩니다. - 일급 객체(first-class object): 이름에 대입하고, 인자로 전달하고, 반환값으로 사용할 수 있는 객체를 설명할 때 쓰는 말입니다. Python의 함수는 일급 객체입니다.
- 데이터 모델: 객체의 정체성, 타입, 값, 속성 접근, 연산 등의 공통 규칙입니다.
헷갈리면 이렇게 구분
def는 함수를 정의하는 문법 키워드이고, 그 문이 실행되어 만들어진 함수는 객체입니다.class는 클래스를 정의하는 문법 키워드이고, 그 문이 실행되어 만들어진 클래스는 객체입니다.10이라는 소스 표기는 리터럴이고, 실행 중에는int객체로 다뤄집니다.a는 객체의 이름이지 객체 자체가 아닙니다.
질문이 나오면
“클래스도 객체면 클래스의 타입은 무엇인가요?”라고 물으면 이렇게 답합니다.
일반적인 Python 사용자 정의 클래스는
type의 인스턴스입니다. 이것은 Python 객체 모델을 더 깊게 설명할 때 중요한 내용이지만 오늘은 함수와 클래스도 런타임에 전달하고 저장할 수 있는 객체라는 점까지만 기억하면 됩니다.
다음 장으로 연결
런타임에서 객체가 계속 만들어진다면 언젠가는 더 이상 필요하지 않은 객체도 생깁니다. 그 메모리를 어떻게 판단하고 정리할지가 다음 질문입니다.
24. GC는 왜 필요한가?
이 장의 역할
GC 알고리즘 이름보다 먼저 객체의 생명주기와 “다시 찾을 수 있는가”라는 판단 기준을 설명합니다.
앞 장에서 이어가기
숫자, 리스트, 함수, 클래스가 모두 런타임 객체로 만들어질 수 있다면, 사용이 끝난 객체의 메모리를 계속 남겨 둘 수는 없습니다.
말하기
GC부터 외우기보다 객체의 생명주기를 먼저 보겠습니다.
첫째, 객체가 생성됩니다. 런타임은 그 객체를 저장할 메모리를 확보합니다.
둘째, 이름이나 다른 객체가 그 객체를 참조하며 사용합니다.
셋째, 대입이 바뀌거나 함수가 끝나거나 컨테이너에서 제거되면서 참조가 끊깁니다.
넷째, 프로그램이 어떤 유효한 경로로도 그 객체를 다시 찾을 수 없게 되면 도달 불가능한 객체가 됩니다.
다섯째, 앞으로 사용할 수 없는 객체가 차지한 메모리는 다시 사용할 수 있도록 정리 후보가 됩니다.
GC는 이 후보를 어떤 기준으로 찾고, 언제 회수하며, 메모리를 어떻게 다시 사용할 수 있게 할지를 다루는 메커니즘입니다.
슬라이드 흐름을 단계별로 풀기
- 객체 생성: 메모리 공간과 초기 상태를 준비합니다.
- 참조하며 사용: 이름, 리스트, 다른 객체의 속성 등이 객체로 가는 경로를 가집니다.
- 참조가 끊김: 더 이상 그 경로가 필요하지 않게 됩니다.
- 더 이상 도달 불가: 프로그램의 살아 있는 시작점에서 찾아갈 경로가 없습니다.
- 정리 후보: 다시 사용할 수 있도록 메모리를 회수할 수 있습니다.
용어·개념 메모
- GC(Garbage Collection): 프로그램이 더 이상 사용할 수 없는 객체를 찾아 메모리를 회수하는 자동 메모리 관리 메커니즘입니다.
- garbage: 앞으로 프로그램이 사용할 수 없는 객체를 가리키는 표현입니다. 단순히 값이 오래됐거나 크다는 뜻이 아닙니다.
- 객체 생명주기: 객체가 생성되고, 사용되고, 더 이상 필요 없어져 정리될 때까지의 과정입니다.
- 참조(reference): 이름이나 다른 객체에서 대상 객체로 도달할 수 있게 하는 연결입니다.
- 강한 참조(strong reference): 일반적으로 객체를 살아 있게 유지하는 참조입니다. 약한 참조와 구분할 때 쓰는 말입니다.
- 도달 가능(reachable): 프로그램이 살아 있는 출발점에서 참조를 따라 객체를 찾아갈 수 있는 상태입니다.
- 도달 불가능(unreachable): 어떤 유효한 출발점에서도 객체까지 가는 참조 경로가 없는 상태입니다.
- 회수(reclaim): 더 이상 사용하지 않는 객체가 차지한 메모리를 다시 사용할 수 있게 하는 일입니다.
- heap: 런타임이 객체처럼 수명이 동적으로 결정되는 데이터를 저장하는 데 사용하는 메모리 영역입니다. 세부 구조는 구현마다 다릅니다.
- 메모리 관리: 메모리를 확보하고, 사용하고, 수명을 판단하고, 반환하는 전체 과정입니다. GC는 그중 자동 회수 방식입니다.
메모리와 외부 자원을 구분하기
GC가 있다고 파일, 네트워크 연결, 데이터베이스 연결이 원하는 시점에 자동으로 안전하게 닫힌다고 생각하면 안 됩니다.
- 메모리는 런타임이 객체 저장에 사용하는 공간입니다.
- 외부 자원은 파일 핸들, 소켓, DB 연결처럼 운영체제나 외부 시스템과 연결된 자원입니다.
- 외부 자원은 Python에서
with문이나 명시적인close()로 사용 범위를 관리하는 것이 좋습니다.
헷갈리면 이렇게 구분
- 이름 하나가 사라졌다고 객체가 무조건 정리되는 것은 아닙니다. 다른 참조가 남아 있을 수 있습니다.
- 객체가 필요 없다고 개발자가 느끼는 것과 런타임이 도달 불가능하다고 판단하는 것은 다를 수 있습니다.
- GC는 “모든 메모리 문제를 해결하는 기능”이 아닙니다. 살아 있는 객체를 계속 보관하면 논리적 메모리 누수가 생길 수 있습니다.
질문이 나오면
“GC가 있으면 메모리 누수가 없나요?”라고 물으면 이렇게 답합니다.
아닙니다. 프로그램이 필요 없는 객체를 리스트나 전역 변수에서 계속 참조하면 GC 입장에서는 아직 도달 가능한 살아 있는 객체입니다. 자동 GC가 있어도 불필요한 참조를 계속 유지하면 메모리 사용이 늘 수 있습니다.
다음 장으로 연결
필요 없는 객체를 찾는 목적은 같지만, CPython과 Java는 판단의 중심이 다릅니다. CPython은 참조 수를 중심에 두고, Java GC는 GC root에서의 도달 가능성을 중심으로 봅니다.
25. CPython과 Java GC의 큰 차이
이 장의 역할
참조 수와 도달 가능성의 차이를 설명하고, Java GC root를 발표자가 자신 있게 풀어 말할 수 있게 합니다.
앞 장에서 이어가기
24장에서는 도달할 수 없는 객체가 정리 후보가 된다고 설명했습니다. 이제 CPython과 Java가 실제로 그 후보를 찾는 중심 전략을 비교하겠습니다.
말하기
먼저 CPython입니다.
CPython의 대부분 객체는 자신을 가리키는 강한 참조가 몇 개인지, 즉 참조 수를 관리합니다. 이름이나 컨테이너가 객체를 새로 참조하면 수가 늘고, 참조가 사라지면 수가 줄어듭니다. 마지막 강한 참조가 해제되어 참조 수가 0이 되면 일반적으로 그 객체를 더 이상 사용할 경로가 없으므로 정리할 수 있습니다.
하지만 참조 수만으로 해결되지 않는 경우가 있습니다. 객체 A가 B를 가리키고 B가 다시 A를 가리키는 순환 참조입니다. 바깥에서 A와 B를 더 이상 사용하지 않아도 서로가 서로를 참조하기 때문에 각 객체의 참조 수는 0이 아닐 수 있습니다.
그래서 CPython에는 참조 수 방식을 보완하는 순환 GC, 또는 cyclic garbage collector가 있습니다. 컨테이너 객체들의 순환 중 외부에서 더 이상 도달할 수 없는 묶음을 찾아 정리합니다.
이제 Java입니다.
Java GC는 대표적으로 GC root라는 살아 있는 출발점에서 참조선을 따라갑니다. root에서 직접 또는 간접적으로 도달할 수 있는 객체는 현재 프로그램이 사용할 가능성이 있으므로 살아 있다고 봅니다. 어떤 root에서도 경로가 없는 heap 객체는 회수 후보가 됩니다.
GC root 자체는 일반 객체처럼 다른 heap 객체에서 우연히 찾아가는 출발점이 아닙니다. JVM이 실행 상태를 보고 “여기에서 시작되는 참조는 현재 살아 있다”고 알고 있는 특별한 시작점입니다.
대표적인 예는 실행 중인 스레드의 스택에 있는 지역 변수 참조, 로드된 클래스의 정적 필드가 가진 참조, JNI 같은 네이티브 인터페이스가 유지하는 참조 등입니다.
CPython 쪽 상세 메모
- 참조 수(reference count): 한 객체를 가리키는 강한 참조의 수를 중심으로 객체 수명을 관리하는 값입니다.
- 이름이 객체에 새로 바인딩되거나 컨테이너가 객체를 담으면 참조가 늘 수 있습니다.
- 이름이 재바인딩되거나 컨테이너에서 객체를 제거하면 참조가 줄 수 있습니다.
- 마지막 강한 참조가 사라져 수가 0이 되면 CPython은 일반적으로 객체의 해제 절차를 시작할 수 있습니다.
- 최신 CPython에는 불멸 객체와 free-threaded build 같은 구현 세부사항이 있어 “눈에 보이는 모든 참조 개수와 내부 값이 언제나 정확히 일치한다”는 식으로 설명하면 안 됩니다.
- 발표에서는 참조 수가 중심이며 순환 GC가 보완한다는 큰 구조까지만 말하면 충분합니다.
순환 참조를 말로 설명하는 예
외부 이름 없음
A ──참조──▶ B
▲ │
└──참조─────┘A와 B는 서로를 가리킵니다. 프로그램의 다른 곳에서는 둘을 더 이상 가리키지 않습니다.
- 참조 수만 보면 A와 B의 수가 각각 0이 아닐 수 있습니다.
- 하지만 바깥의 살아 있는 코드에서 A나 B로 들어가는 길은 없습니다.
- cyclic GC는 이런 고립된 순환을 찾아 정리하는 역할을 합니다.
Java 쪽 상세 메모 — GC root가 무엇인가?
GC root는 Java 프로그램의 모든 객체가 시작되는 하나의 물리 객체가 아닙니다.
JVM이 현재 실행 상태에서 확실히 살아 있다고 간주하는 참조의 출발점들을 묶어 부르는 개념입니다.
대표적인 GC root 예시는 다음과 같습니다.
- 실행 중인 스레드 스택의 지역 변수 참조: 현재 메서드가 사용 중인 객체입니다.
- 정적 필드의 참조: 로드된 클래스가 전역적으로 유지하는 객체 참조입니다.
- 활성 스레드와 JVM 내부 참조: JVM이 실행을 위해 유지하는 대상입니다.
- JNI 참조: Java 바깥의 네이티브 코드가 JNI를 통해 유지하는 객체 참조입니다.
발표에서는 모든 root 종류를 암기할 필요가 없습니다. 다음 한 문장만 정확히 말하면 됩니다.
GC root는 JVM이 현재 살아 있다고 아는 출발점이며, Java GC는 그 출발점에서 참조를 따라 도달 가능한 객체를 살아 있는 것으로 판단합니다.
GC root 그림을 머릿속으로 이해하기
[GC root: 현재 스택의 지역 변수]
│
▼
객체 A ─────▶ 객체 B
객체 C ─────▶ 객체 D- root에서 A로 갈 수 있고 A에서 B로 갈 수 있으므로 A와 B는 도달 가능합니다.
- C와 D끼리 연결되어 있어도 어떤 root에서도 C로 들어가는 경로가 없다면 둘은 회수 후보가 될 수 있습니다.
- 즉 Java GC에서는 단순히 “참조가 하나라도 있는가”보다 “살아 있는 출발점에서 도달할 수 있는가”가 중요합니다.
용어·개념 메모
- cyclic GC / 순환 GC: 참조 수만으로 정리되지 않는 고립된 참조 순환을 찾는 CPython의 보완 메커니즘입니다.
- 순환 참조(reference cycle): 객체들이 서로를 따라 다시 원래 객체로 돌아오는 참조 고리를 만드는 상태입니다.
- GC root: 추적 기반 GC가 살아 있는 객체를 찾기 시작하는 특별한 참조 출발점입니다.
- 도달 가능성(reachability): root에서 참조선을 따라 해당 객체까지 갈 수 있는지 여부입니다.
- 추적(tracing): root에서 시작해 객체의 참조를 따라가며 살아 있는 객체를 표시하는 방식입니다.
- heap 객체: Java에서
new등으로 생성되어 heap에 놓이는 객체입니다. 실제 메모리 배치는 JVM 구현이 관리합니다. - GC 알고리즘: 살아 있는 객체를 표시하고, 죽은 객체를 회수하거나 이동하는 구체적인 전략입니다. G1, ZGC, Parallel GC 등은 구현 전략의 예입니다.
- JNI(Java Native Interface): Java 코드와 C/C++ 같은 네이티브 코드가 상호작용하는 인터페이스입니다.
- 스택(stack): 메서드 호출마다 지역 변수와 실행 정보를 관리하는 영역입니다. Java 객체 자체는 일반적으로 heap에 있고, 스택에는 그 객체를 가리키는 참조가 있을 수 있습니다.
두 방식을 한 문장으로 비교
- CPython: “이 객체를 가리키는 강한 참조가 몇 개인가?”가 일상적인 정리의 중심이고, 순환 GC가 고립된 순환을 보완합니다.
- Java/JVM: “GC root에서 이 객체까지 갈 수 있는가?”가 살아 있는 객체를 판단하는 중심입니다.
반드시 피할 표현
- “Python GC는 참조 수만 보고 끝난다.” — 순환 GC가 보완합니다.
- “Java는 참조 수가 0이면 지운다.” — Java GC 설명의 중심은 root 기반 도달 가능성입니다.
- “GC root는 heap의 맨 위 객체다.” — root는 물리적 위아래가 아니라 추적의 시작점입니다.
- “참조가 하나라도 있으면 절대 수거되지 않는다.” — root에서 도달할 수 없는 고립된 순환은 추적 기반 GC에서 회수 후보가 될 수 있습니다.
- “모든 Python 구현이 CPython과 같은 GC를 쓴다.” — 참조 수 중심 설명은 CPython 기준입니다.
질문이 나오면
“Java에서 객체끼리 순환 참조하면 메모리 누수인가요?”라고 물으면 이렇게 답합니다.
객체끼리 순환한다는 사실만으로는 누수가 아닙니다. 어떤 GC root에서도 그 순환으로 들어갈 수 없다면 Java의 추적 GC는 전체 묶음을 도달 불가능한 것으로 판단해 회수할 수 있습니다.
“CPython의 참조 수가 0이면 항상 즉시 메모리가 운영체제로 돌아가나요?”라고 물으면 이렇게 답합니다.
객체의 해제 절차가 시작될 수 있다는 것과 프로세스가 운영체제에 메모리 페이지를 즉시 반환한다는 것은 다른 문제입니다. Python 메모리 할당기와 운영체제 관리가 중간에 있으므로 오늘은 객체 수명 판단까지만 설명합니다.
다음 장으로 연결
이제 C, Java, CPython에서 본 실행 경로와 메모리 관리 선택을 한 장에 모아 비교하겠습니다.
26. C · Java · Python 한 장 비교
이 장의 역할
17~25장의 세부 내용을 우열이 아니라 선택과 트레이드오프의 표로 압축합니다.
앞 장에서 이어가기
지금까지는 각 언어를 따로 봤습니다. 이제 같은 기준, 즉 중간 결과·실행 주체·메모리 관리로 한 줄씩 정리하겠습니다.
말하기
C는 보통 컴파일과 링크를 거쳐 현재 플랫폼용 네이티브 실행파일을 만듭니다. 실행 제어가 직접적이고 메모리 관리 책임도 개발자에게 더 많이 있습니다.
Java는 .class 바이트코드를 만들고 JVM에서 실행합니다. JVM은 인터프리터와 JIT를 함께 사용할 수 있고, heap 객체는 GC가 관리합니다.
CPython은 Python 소스를 내부 바이트코드로 만들고 CPython 인터프리터가 실행합니다. 런타임에서 타입을 가진 객체를 동적으로 다루며, 메모리 관리는 참조 수를 중심으로 순환 GC가 보완합니다.
어느 언어가 무조건 더 좋다는 결론은 없습니다. 실행 전에 더 많이 결정할지, 공통 VM에 맡길지, 동적인 객체 모델과 인터프리터에 맡길지에 따라 성능, 이식성, 개발 편의, 런타임 부담 사이의 균형이 달라집니다.
비교 기준별 상세 메모
1. 실행 전 결과물
- C: 현재 플랫폼용 네이티브 실행파일
- Java: JVM이 읽는
.class바이트코드 - CPython: 실행 중 사용할 code object와 CPython 바이트코드, 필요하면
.pyc캐시
2. 실행의 중심
- C: 운영체제가 네이티브 실행파일을 로드하고 CPU가 명령을 수행
- Java: JVM이 클래스 파일을 로드하고 인터프리터·JIT 등의 전략으로 실행
- CPython: CPython 인터프리터가 Python 바이트코드를 실행하며 Python 객체 연산을 수행
3. 메모리 관리의 중심
- C: 개발자가 명시적으로 관리하는 범위가 큼
- Java: JVM의 추적 GC가 root 기반 도달 가능성을 중심으로 관리
- CPython: 참조 수 중심, 순환 GC가 보완
용어·개념 메모
- 직접 제어: 메모리, 빌드, 시스템 호출 등에 대해 개발자가 세부 결정을 내릴 수 있는 범위가 큼을 뜻합니다.
- 이식성(portability): 서로 다른 환경에서도 코드를 옮겨 실행하기 쉬운 성질입니다. 필요한 VM과 라이브러리 호환성까지 포함해 봐야 합니다.
- 동적 객체 모델: 실행 중 객체의 타입과 연산 규칙을 확인하고 처리하는 특성이 두드러지는 모델입니다.
- 트레이드오프(trade-off): 한 장점을 얻기 위해 다른 비용이나 제약을 받아들이는 관계입니다.
- 개발 편의: 코드 작성, 실행, 디버깅, 배포에 필요한 노력의 정도입니다.
- 런타임 부담: VM, 인터프리터, GC가 실행 중 사용하는 시간과 메모리 및 관리 복잡성입니다.
헷갈리면 이렇게 구분
- 이 표는 모든 C, Java, Python 구현과 배포 방식을 예외 없이 설명하는 법칙이 아닙니다.
- C에도 동적 라이브러리와 런타임 요소가 있습니다.
- Java에도 AOT 기술이 있을 수 있습니다.
- Python도 다른 구현이나 패키징 도구를 사용할 수 있습니다.
- 발표에서는 가장 일반적인 실행 모델을 비교해 큰 감각을 잡습니다.
질문이 나오면
“PyInstaller는 Python을 C처럼 컴파일하나요?”라고 물으면 이렇게 답합니다.
일반적인 PyInstaller는 애플리케이션 코드와 CPython 인터프리터, 필요한 모듈과 리소스를 묶어 배포하기 쉽게 만듭니다. Python 코드를 모두 현재 CPU용 네이티브 기계어로 바꾸는 C 컴파일과 같은 과정으로 보면 안 됩니다.
다음 장으로 연결
비교를 한 문장으로 줄이다 보면 설명은 쉬워지지만 원인과 결과를 뒤집는 잘못된 단순화가 생길 수 있습니다. 다음 장에서 자주 나오는 표현을 바로잡겠습니다.
27. 피해야 할 단순화
이 장의 역할
초심자 설명에서 자주 나오는 세 가지 오해를 더 정확한 문장으로 교정합니다.
앞 장에서 이어가기
세 언어의 차이를 짧게 말할수록 편하지만, 짧은 표현이 내부 구조를 지워 버릴 수 있습니다.
말하기
첫 번째, “Python은 컴파일을 전혀 하지 않는다”는 표현은 피해야 합니다. CPython도 소스를 내부 바이트코드로 컴파일합니다. 정확히는 C처럼 미리 네이티브 실행파일을 만드는 것이 기본 모델이 아니라고 해야 합니다.
두 번째, “class를 쓰면 객체지향 설계가 된다”는 표현도 피해야 합니다. 클래스는 표현 도구이고, 객체지향 설계의 핵심은 관심사와 책임의 경계를 정하고 객체가 약속으로 협력하게 만드는 것입니다.
세 번째, “객체지향 때문에 비동기가 생긴다”는 표현도 원인과 결과가 맞지 않습니다. 객체지향은 책임을 나누는 설계 관점이고, 비동기는 기다리는 동안 다른 작업을 진행할 수 있게 실행 흐름을 구성하는 방식입니다.
관심사를 잘 분리하면 비동기 작업의 경계를 설계하는 데 도움이 될 수 있지만, 객체지향이 비동기의 직접 원인은 아닙니다.
용어·개념 메모
- 단순화: 복잡한 구조에서 핵심만 남겨 설명하는 것입니다. 학습에 필요하지만 핵심 원인까지 바꾸면 오해가 됩니다.
- 객체지향 설계: 책임을 가진 객체와 그 협력 관계를 구성하는 설계 방식입니다.
- 비동기(asynchronous): 어떤 작업의 완료를 그 자리에서 계속 기다리지 않고, 완료를 나중에 처리할 수 있게 실행 흐름을 구성하는 방식입니다.
- 동기(synchronous): 요청한 작업의 완료를 현재 흐름에서 기다린 뒤 다음 단계로 진행하는 방식입니다.
- 동시성(concurrency): 여러 작업이 같은 시간 구간에 함께 진전될 수 있게 구성하는 성질입니다. 비동기와 관련될 수 있지만 완전히 같은 말은 아닙니다.
- 병렬성(parallelism): 여러 작업이 실제로 같은 순간에 여러 실행 자원에서 수행되는 것입니다.
- 실행 경계: 어떤 작업이 독립적으로 시작되고 기다리거나 완료될 수 있는지를 나누는 선입니다.
헷갈리면 이렇게 구분
- 객체지향은 코드를 어떤 책임으로 나눌지에 관한 설계 질문입니다.
- 비동기는 기다림과 완료를 어떤 순서로 처리할지에 관한 실행 질문입니다.
- CPython의 바이트코드 컴파일은 언어 구현 내부 변환입니다.
- C의 네이티브 빌드는 현재 플랫폼용 실행 결과물을 만드는 과정입니다.
질문이 나오면
“Python은 인터프리터 언어라고 부르면 틀린가요?”라고 물으면 이렇게 답합니다.
입문 수준에서 실행기의 성격을 강조하려고 그렇게 부를 수는 있습니다. 다만 CPython 내부에는 바이트코드 컴파일 단계도 있으므로, 컴파일 언어와 인터프리터 언어를 완전히 배타적인 두 종류로 이해하면 부족하다고 보완하면 됩니다.
다음 장으로 연결
이제 1부의 설계와 2부의 실행을 세 층으로 다시 연결하겠습니다. 각 층이 무엇을 결정하는지 보면 오늘의 두 주제가 하나로 묶입니다.
28. 설계와 실행을 한 문장으로 연결하면
이 장의 역할
관심사·책임에서 시작한 1부와 컴파일·VM·GC를 다룬 2부를 설계–언어–런타임의 연결로 통합합니다.
앞 장에서 이어가기
지금까지 나온 개념은 따로 떨어진 목록이 아니라, 개발자의 결정이 코드가 되고 그 코드가 실행되는 연속된 세 층입니다.
말하기
설계 층에서는 프로그램을 어떤 관심사와 책임으로 나눌지 개발자가 결정합니다. 예를 들어 퀴즈와 저장은 변경 이유가 다르므로 Quiz와 Storage로 경계를 나눕니다.
언어 층에서는 그 결정을 Python의 클래스, 함수, 타입, 문법으로 표현합니다. class Quiz와 class Storage 같은 코드가 이 층에 있습니다.
런타임 층에서는 표현된 코드를 실제로 실행합니다. CPython이 클래스를 실행해 객체를 만들고, 이름을 객체와 연결하고, 메서드를 호출하고, 필요 없어진 객체의 메모리를 관리합니다.
설계가 런타임의 구체적인 메모리 주소를 모두 결정하지는 않습니다. 반대로 런타임이 우리가 왜 Quiz와 Storage를 나눴는지 대신 판단하지도 않습니다.
개발자는 의미와 경계를 정하고, 언어는 그것을 표현할 도구를 제공하고, 런타임은 그 표현을 실제 동작으로 만듭니다.
세 층 상세 메모
설계 층 — 무엇을 왜 나눌 것인가?
- 관심사
- 책임
- 역할
- 객체의 경계와 협력
- 변경 이유와 의존 관계
언어 층 — 그 설계를 어떻게 표현할 것인가?
- 타입
- 문법
- 클래스와 함수
- 메서드 호출 표현
- 모듈과 이름
런타임 층 — 표현된 코드를 어떻게 실행할 것인가?
- 소스 컴파일과 중간 표현
- VM 또는 인터프리터
- 함수 호출과 객체 생성
- 메모리 할당과 GC
- 오류와 예외의 실행 처리
용어·개념 메모
- 클래스 모델: 언어에서 클래스, 인스턴스, 상속, 메서드 등이 어떤 규칙으로 동작하는지에 대한 체계입니다.
- 메서드 호출: 객체에 연결된 행동을 실행하도록 요청하는 표현과 런타임 과정입니다.
- 의존 관계: 한 코드가 다른 코드의 기능이나 약속을 필요로 하는 관계입니다.
- 표현과 실행의 분리: 코드에 적힌 구조와 그 구조를 실제 메모리·CPU 동작으로 만드는 과정을 구분해 보는 관점입니다.
헷갈리면 이렇게 구분
Quiz와Storage를 나누는 것은 설계 결정입니다.- 그 둘을 Python
class로 쓰는 것은 언어 표현입니다. - 인스턴스를 만들고 메서드를 실행하는 것은 CPython 런타임의 동작입니다.
- 세 층은 연결되지만 같은 질문은 아닙니다.
질문이 나오면
“런타임이 다르면 객체지향 설계도 달라지나요?”라고 물으면 이렇게 답합니다.
관심사와 책임이라는 설계 원칙은 여러 언어에서 사용할 수 있습니다. 다만 언어와 런타임이 제공하는 타입, 메모리 모델, 동시성 기능에 따라 가장 자연스러운 표현과 구현 방식은 달라질 수 있습니다.
다음 장으로 연결
이제 질문을 받거나 스스로 공부할 때, 그 질문이 세 층 중 어디에서 시작하는지 찾는 방법을 정리하겠습니다.
29. 질문은 어느 층에서 시작하는가?
이 장의 역할
청중에게 질문을 막는 규칙이 아니라 질문의 출발점을 맞추는 도구를 제공합니다.
앞 장에서 이어가기
설계, 언어, 런타임이 연결되어 있기 때문에 하나의 질문이 여러 층을 걸칠 수 있습니다. 그래도 시작점을 먼저 정하면 답이 훨씬 명확해집니다.
말하기
질문이 나오면 먼저 어느 층에서 시작한 질문인지 확인합니다.
“왜 저장을 별도 클래스로 나누었나요?”는 설계 층의 질문입니다. 변경 이유와 책임의 경계로 답합니다.
“Python에서 클래스와 인스턴스는 어떻게 표현하나요?”는 언어 층의 질문입니다. 타입, 문법, 클래스 모델로 답합니다.
“객체는 메모리에 어떻게 만들어지고 언제 정리되나요?”는 런타임 층의 질문입니다. CPython 객체, 참조 수, GC로 답합니다.
질문이 여러 층에 걸쳐 있다면 잘못된 질문이라고 멈추는 것이 아닙니다. 먼저 출발점을 정하고, 필요할 때 다음 층으로 연결합니다.
예를 들어 “클래스를 나누면 성능이 빨라지나요?”라는 질문은 설계와 런타임이 섞여 있습니다. 먼저 클래스를 나누는 목적은 책임과 변경 범위를 관리하는 것이라고 답한 뒤, 실제 성능은 객체 생성과 호출 비용, 알고리즘, 런타임 최적화를 따로 측정해야 한다고 연결합니다.
층별 질문과 답변 기준
설계 층
- 질문: 왜 이렇게 나누었는가?
- 핵심어: 관심사, 책임, 역할, 객체, 협력, 변경 이유
- 답변 기준: 이 경계가 어떤 변경을 분리하고 어떤 협력을 가능하게 하는가?
언어 층
- 질문: 그 설계를 어떤 문법과 타입으로 표현하는가?
- 핵심어: 클래스, 인스턴스, 메서드, 타입, 이름, 문법
- 답변 기준: 언어가 어떤 표현과 규칙을 제공하는가?
런타임 층
- 질문: 표현된 코드를 실제로 어떻게 실행하는가?
- 핵심어: 컴파일, 바이트코드, VM, 인터프리터, JIT, 메모리, GC
- 답변 기준: 어떤 실행기와 메모리 관리 방식이 실제 동작을 만드는가?
용어·개념 메모
- 질문의 출발점: 질문에 처음 답해야 하는 층입니다. 답변이 다른 층으로 확장되는 것을 막는 개념은 아닙니다.
- 원인과 결과 분리: 설계 선택이 실행 결과에 영향을 줄 수는 있지만, 직접 원인인지 측정과 근거가 필요한지 구분하는 것입니다.
- 성능 측정: 추측보다 프로파일러와 벤치마크로 실제 시간과 자원 사용을 확인하는 과정입니다.
발표자가 질문을 받을 때 사용할 문장
그 질문은 먼저 설계 관점에서 답하고, 그다음 런타임 영향으로 연결하겠습니다.
또는
지금 질문은 CPython 구현에 관한 질문입니다. Python 언어 전체의 규칙과는 구분해서 설명하겠습니다.
다음 장으로 연결
마지막으로 오늘 전체 내용을 세 문장으로만 남기겠습니다.
30. 오늘 기억할 세 문장
이 장의 역할
발표 시작의 두 질문을 세 문장으로 해결하고 마무리합니다.
앞 장에서 이어가기
세부 용어를 모두 기억하지 않아도 괜찮습니다. 오늘 이후 코드나 질문을 볼 때 다음 세 문장만 남으면 됩니다.
말하기
첫째, 코드를 나눌 때는 클래스부터 만들지 않고 변경 이유가 다른 관심사와 책임의 경계부터 정합니다.
둘째, 클래스는 그 경계를 코드로 표현하는 도구입니다. 클래스 문법이 좋은 설계를 자동으로 만들어 주는 것은 아닙니다.
셋째, 언어와 런타임은 우리가 표현한 코드를 실제 동작으로 만듭니다. C는 보통 네이티브 실행파일을 만들고, Java는 JVM 바이트코드를 JVM에서 실행하며, CPython은 내부 바이트코드를 인터프리터에서 실행합니다.
마지막으로 화면의 문장을 천천히 읽고 닫습니다.
클래스는 도구입니다. 경계를 정하는 것은 개발자입니다.
용어·개념 최종 정리
- 관심사: 어떤 문제를 해결하려는가?
- 책임: 그 문제에 대해 무엇을 해야 하는가?
- 객체: 상태와 행동을 가진 협력 단위
- 클래스: 객체의 경계를 표현하는 언어 도구
- 언어: 코드의 문법과 의미를 정하는 규칙
- 런타임: 표현된 코드를 실제로 실행하고 메모리와 호출 등을 관리하는 환경
- 개발자의 역할: 문제의 의미와 책임의 경계를 결정하는 것
끝맺음 문장
오늘 본 C, Java, CPython의 세부 단계는 서로 다른 실행 선택을 이해하기 위한 예였습니다. 가장 중요한 것은 지금 마주한 문제가 설계, 언어, 런타임 중 어디에 있는지 구분하고, 그 층에 맞는 질문을 하는 것입니다.
공식 참고 자료
- GCC — Overall Options
- Java Virtual Machine Specification — JVM Structure
- Oracle Java Virtual Machine Technology Overview
- Oracle G1 Garbage Collector
- Python Language Reference — Alternate Implementations
- Python Glossary — bytecode
- Python Data Model — Objects, values and types
- Python
gc— Garbage Collector interface - Python C API — Reference Counting