/

이번에는 A Tour of C++ 1.7 Pointers, Arrays, and References를 공부하면서 이해한 내용을 정리한다. 포인터와 레런스는 처음 보면 문법이 비슷해 보여 헷갈리지만, 핵심은 간단하. 포인터(pointer) 객체의 주소를 저장하고, 퍼런스(reference)는 기존 객체의 다른 이름(alias)처럼 동작한다.

이 학습에서는 단순 문법 기보다 실제 메모리에서 어떤 일이 일어나는지를 중심으로 다. 특히 *p += 5, 배열에서의 p + 2, 레퍼런스 원본 값 변경하는 이유, 그리 nullptr의 의미까지 연결해서 이해했다.

1.7.1 포인터(Pointer)란?

변수는 메모리 어에 저장된다. 포인터는 그 변수의 값 자체 아니라 변수가 저장 메모리 주소를 리키는 변수다.

int x = 10;
    int* p = &x;

&x는 x의 주소(address)를 얻고, p는 그 주를 저장한다. 따라서 p “x를 가리킨다(points to x)”고 현할 수 있다.

의미
xx에 저된 값
&xx 메모리 주
px 주소를 저장한 포인터
*pp가 가리키는 주소에 저장된 값

역참조(Dereference)

int x = 10;
    int* p = &x;
    
    *p += 5;

결적으로 x == 15가 된다. 여기서 중요한 점은 p 자체에 5를 한 것이 아니라는 것이다. *p p가 가리키 메모리의 값을 의미하므로, x가 저장된 메모리의 값을 10서 15로 변경한 것이다.

내가 공부하면서 잡은 핵심:*p += 5 p가 포인팅하는 메모리 값에 5를 더한다.” 포인터 코드를 볼 때 주소와 그 주에 저장된 값 분하는 것이 중요하다.

1.7.2 포인터와 배열(Array)

C++에서 열과 포터는 밀접하게 연결된다. 배열 이름은 많은 표현식에서 배열의 첫 번째 원소를 가리키는 포인터럼 용할 수 있다.

int a[] = {10, 20, 30};
    
    int* p = a;

이 경우 p는 첫 째 원소 a[0]를 가리킨다.

*p       // 10
    *(p + 1) // 20
    *(p + 2) // 30

p + 2는 메모리 주소에 단순히 숫 2바이트를 더한다는 뜻이 니다. 포인터의 료형 기준으로 두 개의 원소만큼 이동한다. pint*라면 p + 2는 배열의 세 번째 int 를 킨다.

학습 중 확인한 내: 배열이 {10, 20, 30}이고 p 첫 번째 원를 가리킨다면 *(p + 2)는 30이다.

포인터를 이동해도 배 자체가 이동하는 것은 아니다

int a[] = {10, 20, 30};
    int* p = a;
    
    p = p + 1;

이제 p는 a[1], 값 20을 가리킨다. 하지만 배열 a 자체는 변하지 않는다. 변한 것 포인터 p가 가리키는 위치다.

1.7.3 레퍼런스(Reference)란?

레퍼런스는 이미 존재하는 객체에 붙는 다른 이름(alias)이라고 생각하면 이해하기 쉽다.

int x = 10;
    int& r = x;
    
    r = 20;

r은 x의 복사본이 아니다. x를 가키는 별칭이므로 r = 20 수행하면 x 역시 20이 된다.

내가 해한 방식: “r은 x의 alias이 때문 r을 변경하면 x가 변경된다.” 포인터처럼 매번 *로 역참조하지 않도 원본 객체를 직접 사용하 것처럼 쓸 수 있.

포인터와 레퍼런스의 차이

구분PointerReference
개념체의 주소를 장기존 객체의 별칭
*p로 역참조일반 변처럼 사용
다른 객를 가키기가능초기화 후 다른 객체의 별칭으로 재지정할 수 없음
아무것 가리키지 않는 상태nullptr 사용 가능상적인 reference는 객체를 참조야 함

1.7.4 nullptr

포인터는 유효한 객체 가리킬 수도 있지만, 의도적으로 무 객체도 가키지 않는 태를 표현해야 할 때가 다. 현대 C++에는 이때 nullptr를 사용다.

int* p = nullptr;

이 코드는 p가 현재 유효한 int 객체를 가리키지 않는다는 뜻이다.

if (p != nullptr) {
        // p 유효한 객체를 가리킬 때만 사용
            std::cout << *p;
            }

반대로 다과 같은 경우를 보자.

int x = 10;
    int* p = &x;
    
    if (p != nullptr) {
        // true
        }

p는 x의 주소를 가고 있으므 nullptr가 아니다. 즉, “p는 x의 주소를 가리키고 있으로 nullptr가 아니다.”라는 식으로 판단하면 된다.

중요한 점은 nullptr인 포인터를 역참조하면 안 된다 것이다.

int* p = nullptr;
    // *p = 10; // 잘못된 접근: 역참조하 안 됨

1.7.5 인터 코드를 읽는 순서

포인터 코드가 복잡해 보일 때는 다음 순서로 읽면 훨 쉽다.

int x = 10;
    int* p = &x;
    *p += 5;

먼저 x는 값 10을 가진 객체다. 다음으로 &x 통해 x의 주소를 얻고 p가 그 주소를 저장한다. 마막으로 *p를 통해 그 주소의 값, 즉 x에 접근해 5 더한다. 따라서 최종 x는 15다.

1.7.6 이번 학에서 헷갈리 쉬운 포인트

첫째, p와 *p는 다르. p는 주소고 *p는 주소에 있는 값이다. 째, p + 2는 “주소 숫자에 2를 더한다”보 “두 원소 앞로 이동한다”고 이해하 편 . 셋째, reference는 복사본이 아니라 alias다. 그래서 reference를 통한 변경은 원본에 반영된다. 넷째, nullptr는 값이 0인 일반 정수라기보다 포인터가 아무 체도 가리키지 않는 상태를 명하게 표현하기 위 현대 C++의 null pointer literal이다.

1.7.7 직접 인해 본 문제

int x = 10;
    int* p = &x;
    *p += 5;
    
    // x는?

정답: 15. p 가리키 모리의 5 증가시켰기 때문이다.

int a[] = {10, 20, 30};
    int* p = a;
    
    // *(p + 2)는?

정: 30. p는 a[0]을 가리키고 p + 2 a[2]를 가리킨다.

int x = 10;
    int& r = x;
    r = 20;
    
    // x는?

정답: 20. r은 x의 alias이기 때문이다.

정리

이번 1.7 학습에서 가장 요한 것은 문법 기호 자체보다 “지금 내가 다루는 것이 값인가, 주소인가, 아니면 기존 객체의 별칭인가?”를 구분하는 것이다.

int x = 10;
    
    int* p = &x;  // pointer: x의 주소
    int& r = x;   // reference: x의 alias
    
    *p = 20;      // x = 20
    r = 30;       // x = 30

여기에 nullptr까지 이해하면 포인터의 기본적인 상태 표현도 읽을 수 있다. 이후 함수 , 동적 메모리, 자료구조, 드라이버 코드 등을 공부할 때 포인터와 레퍼런스는 계속 등장하기 때문에 이 차이를 확실하게 잡아두는 것이 중요하다.

This article summarizes what I learned from A Tour of C++ 1.7: Pointers, Arrays, and References. The central idea is simple: a pointer stores an address, while a reference acts as another name, or alias, for an existing object.

1.7.1 Pointers

int x = 10;
    int* p = &x;
    *p += 5;

p stores the address of x. *p dereferences that address and accesses the value stored there. Therefore, *p += 5 changes x from 10 to 15.

1.7.2 Pointers and Arrays

int a[] = {10, 20, 30};
    int* p = a;
    
    *(p + 2); // 30

Here p points to the first element, a[0]. Pointer arithmetic moves in units of the pointed-to type, so p + 2 points to a[2]. Moving p does not modify the array itself; it only changes which element p points to.

1.7.3 References

int x = 10;
    int& r = x;
    r = 20;

A reference is an alias for an existing object. Since r is another name for x, assigning 20 through r also changes x to 20. Unlike a pointer, normal reference use does not require explicit dereferencing with *.

1.7.4 nullptr

int* p = nullptr;

nullptr explicitly represents a pointer that does not point to an object. If p contains &x, then p is not null. A null pointer must not be dereferenced.

if (p != nullptr) {
        std::cout << *p;
        }

1.7.5 A Practical Way to Read Pointer Code

When reading pointer code, ask three questions: What object exists? What address does the pointer store? What value is accessed when the pointer is dereferenced? This makes expressions such as *p += 5 much easier to reason about.

1.7.6 Common Points of Confusion

p and *p are not the same: one is an address and the other accesses the value at that address. p + 2 means moving two elements forward according to the pointer's type. A reference is not a copy but an alias. Finally, nullptr is the modern C++ null pointer literal used to express that a pointer currently points to no object.

Summary

int x = 10;
    
    int* p = &x;  // pointer: address of x
    int& r = x;   // reference: alias of x
    
    *p = 20;      // x becomes 20
    r = 30;       // x becomes 30

The most useful question is: Am I working with a value, an address, or an alias to an existing object? Understanding that distinction provides a strong foundation for later C++ topics such as function parameters, dynamic memory, data structures, and low-level systems programming.

규칙 파일에 무엇을 적나?

한 줄 답: 파일 상단에 YAML 프런트매터(Frontmatter)로 메타데이터를 적고, 그 아래에 마크다운으로 실제 지시사항을 작성합니다.

YAML 프런트매터에는 다음과 같은 속성을 설정합니다.

  • description: 에이전트가 이 규칙을 언제 적용할지 판단하는 텍스트입니다.
  • globs: 특정 파일 패턴에만 규칙을 제한할 때 사용합니다.
  • alwaysApply: 조건 없이 항상 적용할지 여부를 지정합니다.

규칙 적용 모드(apply mode)는 다음 4가지입니다.

  • Always: 항상 적용됩니다.
  • Intelligently: 에이전트가 컨텍스트와 description을 읽고 스마트하게 적용 여부를 판단합니다.
  • Specific Files: globs를 통해 매칭된 파일에 한해 적용됩니다.
  • Manual: 사용자가 채팅에서 @ 멘션을 통해 명시적으로 수동 적용합니다.

규칙 파일은 직접 만들 필요 없이 채팅창에서 /create-rule 명령어를 사용하거나, Cursor의 Customize → Rules 메뉴를 통해 간편하게 생성할 수 있습니다.

규칙이 안 먹을 때 어디를 보나?

한 줄 답: 파일 확장자가 .mdc인지 확인하고, YAML 프런트매터의 descriptionglobs가 현재 작업 중인 컨텍스트와 맞는지 점검합니다.

트러블슈팅 포인트는 다음과 같습니다.

  • 확장자 오류: .md로 잘못 저장하면 에이전트가 완전히 무시합니다.
  • 파일 매칭 실패: globs 패턴에 오타가 있거나 경로 설정이 대상 파일과 어긋나지 않았는지 확인합니다.
  • 불분명한 설명: description 텍스트가 너무 모호하면 Intelligently 모드에서 에이전트가 해당 규칙을 꺼내야 할 타이밍을 놓칩니다.
  • 디버깅 방법: 규칙 적용 여부가 의심될 때는 채팅창에서 @규칙이름으로 직접 멘션하여 강제로 불러온 후 제대로 동작하는지 테스트합니다.

FAQ

Q1. 기존 .cursorrules 파일은 지워야 합니까?

A1. 구식(deprecated) 방식이므로 장기적으로 유지보수하기 위해 .cursor/rules/ 하위의 .mdc 파일 구조로 이전(migrating)하는 것을 권장합니다.

Q2. 작성한 규칙을 팀원들과 공유할 수 있습니까?

A2. 네, 프로젝트의 .cursor/rules/ 폴더 전체를 버전 관리 시스템(VCS)에 커밋하여 업로드하면 팀원 모두 동일한 규칙을 공유하게 됩니다.

Q3. 프론트엔드 파일에만 특정 규칙을 적용하려면 어떻게 해야 합니까?

A3. .mdc 파일의 YAML 프런트매터에서 globs 속성에 *.tsxsrc/frontend/**와 같이 파일 패턴을 지정하면 됩니다.

출처 (Sources)

 

|

C++ 하드웨어 매핑(Mapping to Hardware)의 핵심은 기본 연산이 단일 기계 명령(machine instruction)으로 직접 변환되어 추가적인 런타임 오버헤드 없이 하드웨어 자원을 활용한다는 것입니다. 대입 연산은 메모리 칸의 값을 복사하며, 포인터는 주소를 복사하여 가리키는 대상을 바꾸는 반면, 참조(Reference)는 대상 객체의 값을 직접 바꿉니다. 이 글에서는 A Tour of C++ 1.9를 바탕으로 C++의 메모리 모델과 포인터, 참조의 차이를 설명합니다.

C++ 코드는 어떻게 하드웨어와 메모리에 매핑됩니까?

C++의 기본 연산 대부분은 특정 CPU 명령 하나에 직접 대응하도록 설계되어 있습니다. 예를 들어 int 값의 덧셈은 대부분의 아키텍처에서 정수 덧셈 명령 하나로 컴파일됩니다. 이런 저수준 대응 관계 때문에 C++은 추가적인 런타임 오버헤드 없이 하드웨어 자원을 거의 그대로 활용할 수 있습니다.

메모리는 연속된 칸(cell)들이 나열된 구조로 볼 수 있습니다. 포인터에 저장되는 값은 바로 이 칸들 중 하나를 가리키는 기계 주소입니다. 배열은 이런 메모리 모델 위에서 서로 붙어 있는 객체들의 연속을 추상화한 것이며, 그래서 배열의 첫 원소 주소만 알면 나머지 원소들의 위치도 계산할 수 있습니다.

여기서 가장 먼저 짚어야 할 규칙은 대입은 값을 복사한다는 점입니다. x = y;를 실행하면 y가 가진 값이 x의 메모리 칸에 복사될 뿐, 두 변수가 같은 칸을 공유하게 되는 것은 아닙니다. 따라서 이후 어느 한쪽을 바꿔도 다른 쪽에는 영향이 없습니다. 두 변수가 진짜로 상태를 공유해야 한다면, 포인터나 참조를 통해 그 관계를 명시적으로 만들어야 합니다.

C++에서 대입(Assignment)과 초기화(Initialization)는 어떻게 다릅니까?

대입과 초기화는 둘 다 = 기호를 쓰지만 메모리 관점에서는 서로 다른 작업입니다. 초기화(initialization)는 아직 유효한 값을 갖지 않은 메모리 칸을 유효한 객체로 만드는 과정이고, 대입(assignment)은 이미 유효한 객체로 존재하는 메모리에 새로운 값을 덮어쓰는 작업입니다.

int x = 1; // 초기화: 새 객체 x를 값 1로 만듭니다.
int y = 3; // 초기화: 새 객체 y를 값 3으로 만듭니다.

x = y;     // 대입: y의 값(3)을 x가 가진 칸에 복사합니다. (x == 3)
y = 100;   // 대입: y만 바뀝니다. x는 여전히 3입니다.

x = y; 이후 xy는 값이 같아 보일 뿐, 서로 독립된 메모리 칸입니다. 그래서 y = 100;을 실행해도 x는 영향을 받지 않고 3에 그대로 머무릅니다. 이 독립성이 대입 연산의 기본 성질이며, 뒤에서 다룰 포인터·참조와 대비되는 지점입니다.

C++ 포인터(Pointer)와 참조(Reference)의 동작 차이는 무엇입니까?

포인터와 참조는 둘 다 다른 객체를 가리킨다는 점에서 비슷해 보이지만, 대입이 일어날 때 실제로 바뀌는 대상이 서로 다릅니다.

int x = 2;
int y = 3;

int* p = &x;  // p는 x의 주소를 저장합니다.
int* q = &y;  // q는 y의 주소를 저장합니다.

int& r  = x;  // r은 x에 묶인 참조(alias)입니다.
int& r2 = y;  // r2는 y에 묶인 참조(alias)입니다.

p = q;   // 포인터 대입: 주소가 복사됩니다. 이제 p도 y를 가리킵니다. x는 여전히 2입니다.
r = r2;  // 참조 대입: r이 가리키는 대상(x)에 r2의 값(3)이 쓰입니다. x는 3이 됩니다.

포인터 대입(p = q)은 주소 값 자체를 복사하는 연산입니다. 그 결과 p가 가리키는 대상이 x에서 y로 바뀌지만, x가 들어 있던 메모리의 값은 전혀 건드리지 않습니다. 반면 참조 대입(r = r2)은 참조 자체를 재지정하는 문법이 없기 때문에, =가 항상 참조가 가리키는 대상 객체에 대한 값 대입으로 해석됩니다. 그래서 r = r2;r2가 가리키는 값(3)을 r이 가리키는 객체, 즉 x에 복사하는 것과 같습니다.

이 차이는 참조가 만들어지는 방식과도 이어집니다. int& r = x;에서 =는 대입이 아니라 rx에 묶는 초기화입니다. 참조는 반드시 어떤 객체에 묶인 상태로만 존재할 수 있어서, 초기화 없이 참조만 선언하는 것은 허용되지 않습니다. 포인터는 nullptr로 초기화하거나 나중에 다른 주소로 재대입할 수 있지만, 참조는 한 번 묶인 대상을 끝까지 바꿀 수 없습니다.

구분 포인터 대입 p = q 참조 대입 r = r2
복사되는 것 주소(가리키는 대상) 값(대상 객체의 내용)
바뀌는 대상 포인터 자신이 가리키는 곳 참조가 이미 가리키던 객체의 값
재지정 가능 여부 가능 (다른 주소로 대입 가능) 불가능 (초기화 시점에 묶인 대상 고정)

학습 내용 요약 및 다음 단계는 무엇입니까?

지금까지 정리한 내용을 위 코드 예시에 맞춰 다시 확인해 보면 다음과 같습니다.

  • x = y; 이후 y = 100;을 실행해도 x는 3으로 그대로 남습니다. 대입은 값 복사이므로 두 변수는 독립적입니다.
  • p = q; 이후 x는 여전히 2이고, pq 둘 다 y를 가리키게 됩니다. 포인터 대입은 주소 복사입니다.
  • r = r2; 이후 x는 3이 되고, r은 변함없이 x를 가리킵니다. 참조 대입은 대상 객체에 대한 값 대입입니다.

정리하면, 포인터 대입은 “무엇을 가리키는지”를 바꾸고, 참조 대입은 “가리키고 있는 대상의 값”을 바꿉니다. 이 구분을 명확히 해두면 이후 함수 인자 전달이나 자료구조 구현에서 포인터·참조를 고를 때 헷갈리지 않습니다. 1장의 다음, 그리고 마지막 절은 1.10 Advice이며, 다음 글에서 이어서 정리할 예정입니다.

정보 출처는 어디입니까?

본문 내용은 2026-09-15 기준으로 확인한 Bjarne Stroustrup의 저서와 공식 문서를 참고했습니다.

  • Bjarne Stroustrup, A Tour of C++ (3rd ed.), §1.9 Mapping to Hardware — 이 글의 주요 학습 출처입니다.
  • Memory model — cppreference.com — 메모리를 연속된 칸으로 보는 모델과 포인터가 저장하는 주소의 의미를 확인했습니다.
  • References — Standard C++ Foundation FAQ — 참조가 초기화 시점에 대상에 묶이고 이후 재지정할 수 없다는 규칙을 확인했습니다.

이 글은 Bjarne Stroustrup, A Tour of C++ 3rd ed. §1.9 Mapping to Hardware 공부 노트를 2026-09-15 기준으로 정리한 일반 설명이며, 표준·구현·개정판에 따라 세부 서술은 달라질 수 있습니다.

The core of C++ Mapping to Hardware is that basic operations translate directly into single machine instructions, utilizing hardware resources without extra runtime overhead. Assignment operations copy values between memory cells, while pointers copy addresses to change what they point to. References, however, directly change the value of the object they refer to. This article explains the C++ memory model and the differences between pointers and references based on A Tour of C++ 1.9.

How Does C++ Code Map to Hardware and Memory?

Most of C++'s basic operations are designed to translate into a single machine instruction. Adding two int values, for instance, typically compiles down to one integer-add instruction on most architectures. This tight correspondence is why C++ can use hardware resources almost directly, without extra runtime overhead layered on top.

Memory itself can be pictured as a sequence of contiguous cells. A pointer's value is nothing more than the machine address of one such cell. An array builds on this same model: it's an abstraction over a run of objects laid out back-to-back, which is exactly why knowing the address of the first element is enough to compute the location of every other element.

The rule worth internalizing first is that ordinary assignment copies a value. Executing x = y; copies whatever value y holds into x's own memory cell — it does not make the two variables share a cell. Changing one afterward has no effect on the other. If two variables genuinely need to share state, that relationship has to be created explicitly, through a pointer or a reference.

What is the Difference Between Initialization and Assignment in C++?

Initialization and assignment both use the = symbol, but they do very different things in terms of memory. Initialization takes a memory cell that doesn't yet hold a valid value and turns it into a proper object. Assignment takes a cell that already holds a valid object and overwrites its value.

int x = 1; // initialization: creates x with value 1
int y = 3; // initialization: creates y with value 3

x = y;     // assignment: copies y's value (3) into x's cell (x == 3)
y = 100;   // assignment: only y changes; x is still 3

After x = y;, x and y happen to hold the same value, but they remain two separate cells in memory. That's why y = 100; leaves x untouched at 3. This independence is the defining property of plain assignment — and it's exactly what pointers and references break out of, as the next section shows.

How Do Pointers and References Differ in C++?

Pointers and references can look similar because both let you "reach" another object, but assignment through each one touches a different thing under the hood.

int x = 2;
int y = 3;

int* p = &x;  // p stores the address of x
int* q = &y;  // q stores the address of y

int& r  = x;  // r is bound to x — an alias, not a copy
int& r2 = y;  // r2 is bound to y

p = q;   // pointer assignment: the address is copied; p now points to y. x is still 2
r = r2;  // reference assignment: r2's value (3) is written into whatever r refers to (x). x becomes 3

Pointer assignment (p = q) copies the address value itself. As a result, p now points at y instead of x, but the memory that held x is never touched. Reference assignment (r = r2) works completely differently, because C++ has no syntax to "re-point" an existing reference — = on a reference is always interpreted as a value assignment to whatever object it already refers to. So r = r2; reads the value r2 refers to (3) and writes it into the object r refers to, which is x.

This difference traces straight back to how references come into existence. In int& r = x;, that = is not an assignment at all — it's the initialization that binds r to x. A reference can only exist bound to an object, so declaring one without initializing it is not legal. A pointer can be initialized to nullptr or later reassigned to a different address, but once a reference is bound, it can never be made to refer to anything else.

  Pointer assignment p = q Reference assignment r = r2
What gets copied The address (what is pointed to) The value (the referred-to object's content)
What actually changes Where the pointer itself points The value of the object the reference already refers to
Can it be re-bound? Yes — can be reassigned to a new address No — bound permanently at initialization

What Are the Summary and Next Steps?

Running the checks against the code above confirms the distinctions made in this article.

  • After x = y;, executing y = 100; leaves x at 3. Assignment copies values, so the two variables stay independent.
  • After p = q;, x is still 2, and both p and q now point to y. Pointer assignment copies an address.
  • After r = r2;, x becomes 3, while r still refers to x exactly as before. Reference assignment is a value write to the referred-to object.

The short version: pointer assignment changes what is being pointed to, while reference assignment changes the value of what is already being referred to. Keeping that distinction clear removes a lot of confusion later, when choosing between pointers and references for function parameters or data structures. The next — and final — section of Chapter 1 is 1.10 Advice, covered in the next article in this series.

What Are the Sources?

The contents are based on Bjarne Stroustrup's writings and official documentation as of 2026-09-15.

  • Bjarne Stroustrup, A Tour of C++ (3rd ed.), §1.9 Mapping to Hardware — The main study source for this article.
  • Memory model — cppreference.com — Confirmed the model of memory as contiguous cells and the meaning of addresses in pointers.
  • References — Standard C++ Foundation FAQ — Confirmed the rule that references are bound at initialization and cannot be reseated.

This article is a general explanation based on study notes from Bjarne Stroustrup, A Tour of C++ 3rd ed. §1.9 Mapping to Hardware as of 2026-09-15. Details may vary by standard, implementation, or edition.

|

herdr 워크스페이스는 프로젝트나 작업 단위로 탭과 패인을 묶어, 여러 에이전트를 패인에 나란히 띄우고 함께 관리할 수 있게 하는 최상위 컨테이너입니다. 이 글은 2026-09-16 기준 herdr 공식 문서 내용만 다룹니다.

워크스페이스는 언제 새로 만드나?

한 줄 답: 특정 저장소나 작업, 조사 하나를 시작할 때 워크스페이스를 새로 만듭니다.

워크스페이스는 특정 저장소, 작업, 조사를 위한 최상위 컨테이너입니다. 빈 세션을 시작하면 워크스페이스가 자동으로 열립니다.

공식 권장 방식은 활성 프로젝트마다 워크스페이스를 하나씩 두는 것입니다. 워크스페이스를 만들면 첫 번째 탭과 루트 패인이 함께 생성됩니다.

워크스페이스는 탭을 소유하고, 탭은 패인 레이아웃을 정의합니다.

패인에 에이전트는 어떻게 올리나?

한 줄 답: 에이전트를 시작하려면 이미 있는 셸 패인이 필요하며, 시작 자체가 레이아웃을 만들거나 바꾸지는 않습니다.

claude, codex, pi, opencode 같은 지원되는 코딩 에이전트를 실제 터미널 패인에서 실행하면 자동으로 인식됩니다.

사이드바는 인식된 에이전트의 상태를 blocked, working, done, idle, unknown 중 하나로 모아 보여줍니다.

패인을 나눌 때는 기본 접두사인 ctrl+b 뒤에 prefix+v로 오른쪽에, prefix+minus로 아래쪽에 새 패인을 만듭니다.

detach 후에도 작업이 유지되나?

한 줄 답: 패인은 서버가 소유하고, 클라이언트는 화면을 보여주는 UI일 뿐입니다.

ctrl+b q를 누르면 클라이언트가 분리되지만, 서버와 그 안의 에이전트는 계속 동작합니다.

다시 연결할 때는 herdr 명령만 입력하면 됩니다.

herdr server stop을 실행하면 세션과 패인이 종료됩니다. 서버를 완전히 멈춘 뒤 다시 시작하면 저장된 세션 모양을 복원합니다.

FAQ

한 줄 답: 마우스 중심 조작과 몇 가지 키보드 단축키, 워크스페이스와 세션의 차이를 표로 정리했습니다.

질문
키보드가 꼭 필요한가? 아닙니다. 마우스 중심으로 동작해 클릭, 드래그로 분할, 우클릭 메뉴로 대부분을 처리할 수 있고 키보드는 선택 사항입니다.
새 워크스페이스는 어떻게 만드나? prefix+shift+n으로 만듭니다.
워크스페이스 이름은 어떻게 바꾸나? prefix+shift+w로 이름을 바꿉니다.
워크스페이스는 어떻게 닫나? prefix+shift+d로 닫습니다.
워크스페이스와 세션은 어떻게 다른가? 먼저 워크스페이스를 쓰고, 완전히 분리된 패인·소켓·런타임이 필요할 때만 이름 있는 세션을 씁니다.

출처

한 줄 답: 이 글은 herdr 공식 문서만 근거로 삼았으며, 2026-09-16 기준입니다.

A herdr workspace groups the tabs and panes for one project or task, letting you run several agents side by side in one place. This overview covers only what the official herdr documentation states as of 2026-09-16.

When Should You Create a New Workspace?

Short answer: Create a new workspace when you start work on a specific repository, task, or investigation.

A workspace is the top-level container for a given repository, task, or investigation. Starting an empty session automatically opens a workspace.

The official best practice is one workspace per active project. Creating a workspace also creates its first tab and root pane.

A workspace owns its tabs, and a tab defines the pane layout inside it.

How Do You Run an Agent in a Pane?

Short answer: Starting an agent requires an existing shell pane; the start itself does not create or rearrange the layout.

Running a supported coding agent — claude, codex, pi, or opencode — in a real terminal pane is auto-detected.

The sidebar then rolls the detected agent's state up into one of five states: blocked, working, done, idle, or unknown.

To split panes, use the default prefix ctrl+b together with prefix+v for a pane to the right or prefix+minus for a pane below.

Do Tasks Continue After Detaching?

Short answer: The server owns the panes; the client is only a UI on top of it.

Pressing ctrl+b q detaches the client, but the server and its agents keep running.

Reattaching only requires typing herdr again.

Running herdr server stop ends the session and its panes. After a full server stop, the next start restores the saved session shape.

FAQ

Short answer: The table below covers mouse and keyboard use, workspace shortcuts, and how workspaces differ from sessions.

Question Answer
Is the keyboard required? No. herdr is mouse-native: click, drag to split, and right-click menus cover most actions, and keyboard use is optional.
How do you create a new workspace? Use prefix+shift+n.
How do you rename a workspace? Use prefix+shift+w.
How do you close a workspace? Use prefix+shift+d.
How do workspaces differ from sessions? Use workspaces first; reach for a named session only when you need fully separate panes, sockets, or runtimes.

Sources

Short answer: This article is grounded only in the official herdr documentation listed below, as of 2026-09-16.

 

|

libgpiod는 리눅스 유저 스페이스(Linux userspace)가 GPIO character device를 통해 GPIO 라인과 상호작용하게 하는 C 라이브러리, 언어 바인딩 및 명령줄 도구입니다.

이 글은 libgpiod 공식 문서와 Linux 커널 GPIO 문서를 2026-09-01 기준으로 정리한 일반 설명이며, 커널 및 라이브러리 버전에 따라 세부 동작은 달라질 수 있습니다.

libgpiod는 무엇인가?

한 줄 답: 이 프로젝트는 ioctl 기반 GPIO character device 접근을 C 라이브러리, 언어 바인딩, 명령줄 도구로 감싸며, 유저 스페이스의 기본 단위로 Chip과 Line Request를 사용합니다.

기존의 GPIO sysfs 인터페이스는 커널에서 deprecated된 이전 경로이며, GPIO character device는 Linux 커널 4.8에 도입된 더 유연하고 효율적인 경로입니다. 디바이스 파일 디스크립터를 닫으면 이 인터페이스가 할당한 자원이 안전하게 해제되며, 신뢰할 수 있는 이벤트 polling, 여러 값의 동시 읽기 및 설정, open-source와 open-drain GPIO 같은 고급 기능도 제공합니다.

커널 userspace API에서 Chip은 /dev/gpiochipX로 노출되는 GPIO 컨트롤러 객체입니다. 각 Chip은 chip.lines개의 라인을 가지며, 라인은 0부터 chip.lines - 1까지의 offset으로 식별합니다. 라인을 Chip에서 요청하면 Line Request가 만들어지고, 유저 스페이스는 그 요청을 통해 라인 값에 접근하거나 edge event를 감시합니다.

공식 문서에는 고수준 언어 바인딩, D-Bus 인터페이스, 테스트 항목도 포함되지만, 이 글의 주 경로는 C API명령줄 도구입니다. 이 프로젝트는 일반 명령줄 도구만으로 다루기 번거로운 ioctl 기반 커널-유저 스페이스 상호작용을 편의 함수와 불투명 자료구조로 깔끔하게 감쌉니다.

디바이스 트리는 핀을 설명하고, 이 라이브러리는 유저 스페이스에서 라인을 요청합니다. 관련 배경은 디바이스 트리에서 확인할 수 있습니다.

임베디드 시스템이란은 보드를 전용 컴퓨터로 보는 맥락만 연결하며, 이 글에서는 그 정의를 다시 다루지 않습니다.

sysfs GPIO와 무엇이 다른가?

한 줄 답: 신규 유저 스페이스 개발에는 전역 번호를 사용하는 obsolete sysfs보다 Chip과 offset을 사용하는 GPIO character device를 선택해야 합니다.

이전 sysfs 경로는 /sys/class/gpio/ 아래에서 exportunexport를 사용하고, 개별 라인을 /sys/class/gpio/gpioN/과 전역 GPIO 번호로 표현했습니다. 반면 character device 경로는 /dev/gpiochipX라는 Chip과 그 안의 라인 offset을 함께 사용합니다.

커널 문서는 sysfs userspace APIGPIO Character Device Userspace API로 대체된 obsolete 인터페이스로 설명합니다. 이전 경로는 마이그레이션 기간 동안 유지되지만 새 기능은 character device API에만 추가되며, 신규 개발은 새 API를 사용하고 기존 개발도 가능한 한 마이그레이션해야 합니다.

character device는 이벤트 polling, 여러 라인의 값을 한 번에 읽고 설정하는 기능, open-source·open-drain 같은 전기적 구성, 디바이스 파일 디스크립터를 닫을 때의 자원 정리를 제공합니다. 따라서 sysfs의 전역 번호와 파일별 상태 항목을 새 userspace 모델의 기준으로 삼아서는 안 됩니다.

Character Device v1도 obsolete userspace API에 포함되므로, 이 글에서 설명하는 현재 경로는 kernel userspace API v2입니다. v2는 Linux 커널 5.10에 처음 추가되었으며, 커널 driver API의 gpio_chip 내부 구현은 여기서 다루지 않습니다.

라인을 요청하고 읽고 쓰는 법은?

한 줄 답: gpiodetectgpioinfo로 대상을 확인한 뒤 라인을 요청하고, gpioget 또는 gpioset으로 읽거나 설정하며, 요청 수명 안에서만 제어 권한을 유지합니다.

gpiodetect는 시스템의 GPIO Chip, 이름, label, 라인 수를 나열합니다. gpioinfo는 Chip, offset, 라인 이름, direction을 보여 주고, 사용 중인 라인이라면 consumer와 active state, bias, drive, edge detection, debounce period 같은 구성 속성도 보여 줍니다. gpioget은 지정한 라인의 값을 읽고, gpioset은 지정한 라인의 값을 설정합니다.

라인은 이름으로 지정할 수 있고, Chip을 -c 또는 --chip으로 제한한 경우에는 offset으로도 지정할 수 있습니다. gpioget--numeric은 값을 비활성 0 또는 활성 1로 표시합니다. gpiomongpionotify도 각각 edge event와 정보 변경을 기다리는 도구로 존재하지만, 기본 경로는 detect·info·get·set입니다.

gpioset은 프로세스가 종료되거나 중단될 때까지 요청한 라인을 잡고 값을 유지합니다. 프로세스가 종료되면 요청한 라인은 자동으로 해제되고 커널이나 다른 프로세스가 상태를 바꿀 수 있으므로, 종료 뒤에도 값이 유지된다고 가정할 수 없습니다. 요청한 값을 보장하기 위해 기본적으로 종료하지 않는 동작을 사용합니다.

다음은 공식 문서에 제시된 명령 예입니다. 문서의 예시에는 Raspberry Pi 4B가 언급되지만, 이 글의 보드 실측이나 보편적인 핀맵을 뜻하지 않습니다.

$ gpioget -c 0 15
$ gpioset GPIO23=1

C API에서는 gpiod_chip_open(path)으로 Chip을 열고 gpiod_chip_close(chip)으로 닫습니다. gpiod_chip_request_lines(chip, req_cfg, line_cfg)은 라인 묶음의 배타적 사용을 요청하며, req_cfg는 기본 설정을 위해 NULL일 수 있지만 line_cfg는 필요합니다. 반환된 Line Request는 gpiod_line_request_release(request)로 해제해야 합니다.

요청을 얻은 뒤 gpiod_line_request_get_value(request, offset)은 단일 라인 값을 성공 시 1 또는 0으로 반환하고 오류 시 -1을 반환합니다. gpiod_line_request_set_value(request, offset, value)는 성공 시 0, 실패 시 -1을 반환하며, 모든 요청 라인의 값을 다루는 gpiod_line_request_get_valuesgpiod_line_request_set_values도 제공됩니다.

커널 수준에서는 GPIO_V2_GET_LINE_IOCTL이 Line Request를 만들고, 그 요청 파일 디스크립터에서 라인 값을 읽고 설정하는 방식으로 대응합니다. 적절한 커널 드라이버가 있는 하드웨어는 userspace API로 직접 제어하지 않아야 합니다.

칩과 라인은 어떻게 고르나?

한 줄 답: Chip은 번호, 이름, 장치 경로로 식별하고, 선택한 Chip 안의 라인은 이름을 우선 확인한 뒤 필요하면 해당 Chip의 offset으로 지정합니다.

GPIO Chip은 번호, 이름, 경로로 식별할 수 있습니다. 예를 들어 0, gpiochip0, /dev/gpiochip0은 같은 Chip을 가리킬 수 있습니다. gpiodetect는 Chip의 label과 라인 수를 출력하고, 인자를 지정하지 않으면 시스템의 모든 Chip을 나열합니다.

gpioinfo는 라인의 Chip, offset, name, direction을 확인하는 데 사용하며, 사용 중이면 consumer와 active state, bias, drive, edge detection, debounce period 같은 속성도 표시합니다. 라인 이름을 지정하는 방식이 읽기 쉽고, 특정 Chip을 --chip으로 제한한 명령에서는 offset을 사용할 수 있습니다.

라인 offset의 유효 범위는 0부터 chip.lines - 1까지입니다. C helper인 gpiod_chip_get_line_offset_from_name(chip, name)은 라인 이름을 offset으로 매핑하며, 이름을 찾지 못하면 -1을 반환하고 errnoENOENT로 설정합니다.

다음 출력은 공식 문서의 Raspberry Pi 4B 작성 예시입니다. pinctrl-bcm2711이나 raspberrypi-exp-gpio를 모든 보드에서 쓰는 Chip 이름으로 해석해서는 안 됩니다.

$ gpiodetect
gpiochip0 [pinctrl-bcm2711] (58 lines)
gpiochip1 [raspberrypi-exp-gpio] (8 lines)

FAQ

한 줄 답: 권장 userspace 인터페이스, sysfs의 상태, Chip 선택 방식, gpioset 종료 뒤의 수명을 구분하면 기본 사용 경로를 판단할 수 있습니다.

GPIO character device와 이 라이브러리의 C API 또는 명령줄 도구를 사용합니다. Chip을 확인하고 라인을 요청한 뒤 값을 읽거나 설정하는 흐름입니다.

sysfs /sys/class/gpio는 아직 사용합니까?

마이그레이션 기간에는 유지되지만 obsolete 인터페이스입니다. 신규 개발은 GPIO character device userspace API를 사용해야 합니다.

Chip은 어떻게 고릅니까?

번호, 이름, 경로 중 하나로 고를 수 있으며, 0, gpiochip0, /dev/gpiochip0처럼 같은 Chip을 서로 다른 표기로 지정할 수 있습니다.

gpioset을 종료하면 라인 값이 유지됩니까?

보장되지 않습니다. 프로세스가 종료되면 요청한 라인이 자동으로 해제되어 커널이나 다른 프로세스가 상태를 변경할 수 있습니다.

출처

한 줄 답: 본문은 2026-09-01 기준으로 확인한 프로젝트 공식 문서와 Linux 커널 GPIO 문서만 근거로 사용합니다.

libgpiod provides the userspace C library, language bindings, and command-line tools for interacting with GPIO lines through Linux’s GPIO character device.

This article summarizes the official libgpiod documentation and Linux kernel GPIO pages as of 2026-09-01; details may differ by kernel and library version.

What role does the library play in Linux GPIO access?

One-line answer: The project wraps ioctl-based character-device access in a C library, language bindings, and command-line tools, with a chip and a line request forming the basic userspace model.

The older GPIO sysfs interface is deprecated in the kernel. The GPIO character device, introduced in Linux 4.8, provides a more flexible and efficient route; closing its device-file descriptor safely frees the resources allocated through that interface. It also supports reliable event polling, multi-line reads and writes, and configurations such as open-source and open-drain GPIOs.

In the kernel userspace model, a Chip is exposed as /dev/gpiochipX. Each Chip has chip.lines GPIO lines, identified by offsets from 0 through chip.lines - 1. Requesting lines from a Chip creates a Line Request, which then provides access to the requested values or to edge-event monitoring.

The official project documentation also lists high-level language bindings, a D-Bus interface, testing, and other project areas. This article focuses primarily on the C API and command-line tools, which hide the cumbersome ioctl-based kernel-to-userspace interaction behind convenient functions and opaque data structures.

Device Tree describes pins, whereas this interface requests lines from userspace; further context is available in Device Tree.

The broader embedded-systems context is covered by What is an embedded system?, where a board is treated as a dedicated computer.

Why is the character device preferred over sysfs GPIO?

One-line answer: New userspace development should use the chip-and-offset character-device model instead of the obsolete sysfs interface, which is based on global GPIO numbers.

The former sysfs layout used /sys/class/gpio/, write-only export and unexport files, and per-line paths such as /sys/class/gpio/gpioN/. The character-device layout instead combines a Chip such as /dev/gpiochipX with an offset belonging to that Chip.

The kernel documentation marks the sysfs userspace API as obsoleted by the GPIO Character Device Userspace API. It remains maintained during migration, but new features are added only to the newer API; new work should adopt it, and existing work is encouraged to migrate because the old interface is scheduled for removal.

The character device adds reliable event polling, simultaneous access to multiple line values, open-source and open-drain modes, and resource cleanup tied to closing the device descriptor. These are differences in the userspace interface, not a reason to reproduce the old export procedure.

Character Device v1 is obsolete as well. The current kernel userspace path discussed here is kernel userspace API v2, first added in Linux 5.10; kernel-side driver structures are outside the scope of this article.

What is the request-to-value workflow?

One-line answer: Discover the Chip, inspect its lines, make a request, read or set values through that request, and release it when the owning operation ends.

gpiodetect lists the GPIO Chips present, including their names, labels, and line counts. gpioinfo reports each line’s Chip, offset, name, and direction, plus the consumer and attributes such as active state, bias, drive, edge detection, and debounce period when relevant. gpioget reads specified line values, while gpioset assigns them.

Lines may be named directly; an offset may be used when the command is restricted to a Chip with -c or --chip. The --numeric option for gpioget renders inactive and active values as 0 and 1. gpiomon and gpionotify are also available for waiting on edge events and information changes, but detect, info, get, and set are the main tools discussed here.

gpioset holds its line request while the process runs. Once the process exits, its requested lines are released automatically, and the kernel or another process may change their state. The value, therefore, is not guaranteed after exit; by default, the tool does not exit so that the requested value can be maintained while it owns the line.

The following commands are examples from the official documentation. The page notes that its examples were created using a Raspberry Pi 4B; they are documentation examples, not measurements or a universal board pin map for this article.

$ gpioget -c 0 15
$ gpioset GPIO23=1

In the C API, gpiod_chip_open(path) opens a Chip and gpiod_chip_close(chip) closes it. gpiod_chip_request_lines(chip, req_cfg, line_cfg) requests a set of lines for exclusive use; req_cfg may be NULL for defaults, while line_cfg is required. The returned Line Request must be released with gpiod_line_request_release(request).

gpiod_line_request_get_value(request, offset) returns 1 or 0 on success and -1 on error. gpiod_line_request_set_value(request, offset, value) returns 0 on success and -1 on failure. The corresponding gpiod_line_request_get_values and gpiod_line_request_set_values functions handle all requested lines together.

At the kernel boundary, GPIO_V2_GET_LINE_IOCTL creates the Line Request, and the request file descriptor is then used to get or set line values. Hardware that already has an appropriate kernel driver should not be controlled directly through a userspace GPIO API.

How do chip identity and line names fit together?

One-line answer: Select a Chip by number, name, or device path, then identify one of its lines by name or by an offset within that Chip.

A GPIO Chip can be written as a number, a name, or a path. For example, 0, gpiochip0, and /dev/gpiochip0 can all refer to the same Chip. With no Chip argument, gpiodetect lists every available Chip and prints its label and line count.

Use gpioinfo to inspect the Chip, offset, name, direction, consumer, and configured attributes for its lines. A line name is usually the clearest identifier; an offset is available when a particular Chip is selected with --chip.

Valid offsets run from 0 through chip.lines - 1. The C helper gpiod_chip_get_line_offset_from_name(chip, name) maps a line name to its offset and returns -1 and sets errno to ENOENT when the name cannot be found.

The output below is an official documentation example created using a Raspberry Pi 4B. Names such as pinctrl-bcm2711 and raspberrypi-exp-gpio must not be treated as universal Chip names.

$ gpiodetect
gpiochip0 [pinctrl-bcm2711] (58 lines)
gpiochip1 [raspberrypi-exp-gpio] (8 lines)

FAQ

One-line answer: The practical decisions are which userspace interface to use, how sysfs is classified, how a Chip is named, and what happens when a setting process ends.

Use the GPIO character device together with the library’s C API or command-line tools: discover a Chip, request lines, and then read or set their values.

Is /sys/class/gpio still the recommended interface?

No. It remains available during migration but is obsolete, so new development should use the GPIO Character Device Userspace API.

How can a GPIO Chip be selected?

Use its number, name, or path. The forms 0, gpiochip0, and /dev/gpiochip0 can identify the same Chip.

Does a line keep its value after gpioset exits?

It is not guaranteed. Process exit releases the requested line, after which the kernel or another process may change its state.

Sources

One-line answer: The article relies only on the official project documentation and Linux kernel GPIO pages verified as of 2026-09-01.

|

Docker 빌드는 위쪽 레이어부터 캐시를 재사용하므로 자주 바뀌는 지시어를 아래에 두어야 빌드 속도를 유지할 수 있습니다. 영속·공유 데이터는 컨테이너가 삭제될 때 함께 사라지는 overlay 쓰기 레이어가 아니라 volume 또는 bind mount에 보관해야 합니다. 이 글은 Docker 스터디 시리즈의 마지막 4편으로, 공식 빌드 캐시·스토리지 문서를 기준으로 이미지 레이어 캐시를 살리는 Dockerfile 작성법과 컨테이너 삭제 후에도 데이터를 안전하게 보존하는 마운트 전략을 정리합니다.

이미지 레이어 캐시는 언제 무효화되나?

한 줄 답: Dockerfile의 한 스텝(레이어)이 변경되면 그 스텝과 이후의 모든 스텝 캐시가 무효화됩니다.

Docker 빌드는 Dockerfile의 각 명령어를 순서대로 실행하며, 각 명령어 결과를 레이어 캐시로 저장합니다. 다음 빌드 시 Docker는 위쪽(앞) 레이어부터 캐시 재사용 여부를 판단합니다. 특정 스텝의 내용이 이전 빌드와 동일하다면 해당 레이어의 캐시가 사용됩니다. 그러나 하나의 스텝이라도 변경되면, 그 스텝 이후의 모든 레이어 캐시는 전부 무효화되어 다시 실행됩니다. 이것이 레이어 캐시 무효화(cache invalidation)의 핵심 규칙입니다.

공식 빌드 캐시 문서에 따르면 다음과 같은 경우에 캐시가 무효화됩니다.

  • RUN·COPY·ADD 등 명령어의 내용 자체가 달라진 경우
  • COPY 또는 ADD로 복사하는 파일의 내용(체크섬)이 달라진 경우
  • 부모 레이어(앞 스텝)의 캐시가 무효화된 경우(연쇄 무효화)

따라서 소스 파일이 자주 바뀌더라도, 그 소스를 COPY하는 스텝 위쪽에 자주 변하지 않는 명령어(의존성 설치 등)를 배치하면 캐시 히트율을 높일 수 있습니다.

Docker BuildKit은 병렬 빌드·마운트 캐시 등 고급 캐시 기능을 제공하지만, 본편에서는 필수 스터디 범위인 레이어 캐시 무효화 규칙에 집중합니다. BuildKit의 --mount=type=cache 같은 고급 캐시 마운트는 별도 학습 주제로 남겨 둡니다.

이 캐시 원리는 Docker 볼륨 선택 전략과도 연결됩니다. 빌드 결과물이나 런타임 데이터를 overlay 쓰기 레이어에 두면 캐시 이점이 줄어들 뿐 아니라 컨테이너 삭제 시 데이터도 사라지므로, 다음 섹션에서 다루는 마운트 방식이 중요합니다.

Dockerfile에서 캐시를 살리는 지시어 순서는?

한 줄 답: 자주 변경되지 않는 지시어를 위쪽에, 자주 변경되는 지시어를 아래쪽에 배치합니다. 의존성 설치와 소스 코드 복사를 반드시 분리해야 합니다.

캐시 무효화는 순서대로 전파됩니다. 따라서 자주 바뀌지 않는 것을 먼저, 자주 바뀌는 것을 나중에 두는 것이 기본 원칙입니다. Node.js 프로젝트를 예로 들면 다음과 같습니다.

비효율적인 순서 (캐시 손실):

FROM node:20-alpine
COPY . .
RUN npm install
CMD ["node", "index.js"]

위 구조에서는 소스 파일 한 줄이 바뀔 때마다 COPY . .의 캐시가 무효화되고, 그 결과 RUN npm install도 매번 다시 실행됩니다. 의존성 패키지가 바뀌지 않았어도 전체 설치를 반복하게 됩니다.

효율적인 순서 (캐시 보존):

FROM node:20-alpine
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

이렇게 하면 package.jsonpackage-lock.json이 바뀌지 않는 한 RUN npm install 레이어의 캐시가 유지됩니다. 소스 코드가 아무리 자주 바뀌어도 의존성 설치는 건너뜁니다.

이 원칙은 언어와 무관하게 동일하게 적용됩니다.

  • Python: COPY requirements.txtRUN pip installCOPY . .
  • Go: COPY go.mod go.sumRUN go mod downloadCOPY . .
  • Java(Maven): COPY pom.xmlRUN mvn dependency:resolveCOPY src ./src

핵심은 변경 빈도가 낮은 레이어를 위쪽에 두어, 그 아래 레이어의 캐시 무효화가 발생하지 않도록 보호하는 것입니다.

named volume은 무엇이고 어디에 쓰나?

한 줄 답: named volume은 Docker가 직접 관리하는 영속 스토리지로, 컨테이너 생명주기와 독립적이며 컨테이너 간 공유도 가능합니다.

3편에서 살펴본 대로, 컨테이너의 쓰기 레이어(upperdir)는 컨테이너가 docker rm으로 삭제될 때 영구적으로 함께 사라집니다. 데이터베이스 파일·업로드 파일·애플리케이션 로그처럼 컨테이너 삭제 후에도 남아야 하는 데이터를 쓰기 레이어에 두는 것은 적절하지 않습니다.

Volumes(named volume)는 이 문제를 해결하기 위한 Docker 공식 권장 방식입니다. named volume의 주요 특징은 다음과 같습니다.

  • Docker 관리: 볼륨의 생성·저장·삭제는 Docker가 담당하며, 호스트 내 특정 경로(보통 /var/lib/docker/volumes/ 하위)에 저장됩니다.
  • 컨테이너와 독립적: 컨테이너를 삭제해도 볼륨 데이터는 남아 있으며, 새 컨테이너에 다시 연결할 수 있습니다.
  • 컨테이너 간 공유: 여러 컨테이너가 동일한 볼륨을 마운트하여 데이터를 공유할 수 있습니다.
  • write-heavy 워크로드에 적합: overlay 쓰기 레이어와 달리 볼륨은 OverlayFS를 거치지 않고 호스트 파일시스템에 직접 읽고 씁니다. 공식 스토리지 드라이버 문서는 I/O가 많은 워크로드에 볼륨 사용을 권고합니다.

named volume 사용 예시는 다음과 같습니다.

# 볼륨 생성
docker volume create mydata

# 컨테이너에 볼륨 연결 (--mount 방식)
docker run --mount type=volume,source=mydata,target=/app/data myimage

# 볼륨 목록 확인
docker volume ls

# 볼륨 상세 정보
docker volume inspect mydata

source에 지정한 이름(mydata)이 named volume의 이름입니다. 이름을 지정하지 않으면 Docker가 임의의 해시값으로 이름을 생성하는 익명 볼륨(anonymous volume)이 됩니다. 데이터 관리 편의상 이름을 명시하는 named volume이 권장됩니다.

bind mount는 volume과 어떻게 다르고 언제 쓰나?

한 줄 답: bind mount는 호스트의 임의 경로를 컨테이너에 직접 연결하는 방식으로, Docker가 관리하지 않으며 개발 환경에서 소스 코드나 설정 파일을 주입할 때 유용합니다.

Bind mounts는 호스트 파일시스템의 특정 디렉터리 또는 파일을 컨테이너 내 경로에 직접 연결합니다. named volume과 달리 Docker가 스토리지를 생성·관리하지 않으며, 호스트 경로가 미리 존재해야 합니다.

bind mount는 다음 상황에서 주로 활용됩니다.

  • 개발 시 소스 코드를 컨테이너 안에서 즉시 반영(hot reload)하고 싶을 때
  • 호스트의 설정 파일(nginx.conf, .env 등)을 컨테이너에 주입할 때
  • 컨테이너에서 생성한 파일을 호스트 경로에 직접 출력해야 할 때

단, 다음 사항에 주의해야 합니다.

  • 이식성: 호스트의 절대 경로에 의존하므로, 다른 개발 환경이나 운영 서버에서 동일한 경로가 보장되지 않으면 동작하지 않을 수 있습니다.
  • 권한: 호스트 사용자와 컨테이너 사용자의 UID/GID가 달라 권한 문제가 발생할 수 있습니다.
  • 보안: 민감한 호스트 경로를 마운트할 경우 컨테이너가 호스트에 넓게 접근할 수 있어 주의가 필요합니다.

Volume vs Bind Mount 비교

항목 Named Volume Bind Mount
관리 주체 Docker 사용자(호스트 파일시스템 직접 관리)
영속성 컨테이너 삭제 후에도 데이터를 유지합니다. 호스트 경로가 유지되는 한 데이터를 유지합니다.
이식성 높음 (Docker가 경로를 추상화) 낮음 (호스트 절대 경로에 의존)
성능 I/O 직접 접근, write-heavy에 유리 I/O 직접 접근 (동등 수준)
권장 용도 DB, 업로드, 로그 등 영속 데이터 개발 시 소스 마운트, 설정 파일 주입
초기화 동작 빈 볼륨은 이미지 내용으로 초기화됩니다. 호스트 경로 내용이 컨테이너 경로를 덮어씁니다.

--mount-v 중 무엇을 권장하나?

한 줄 답: 공식 문서는 타입을 명시적으로 지정할 수 있는 --mount 구문을 권장합니다.

Docker는 마운트를 지정하는 두 가지 플래그를 제공합니다.

-v / --volume (단축 플래그):

# named volume
docker run -v mydata:/app/data myimage

# bind mount
docker run -v /host/path:/container/path myimage

-v는 콜론(:)으로 구분된 축약 문법으로, 콜론 앞에 이름이 오면 named volume, 절대 경로가 오면 bind mount로 해석됩니다. 역사적으로 널리 사용되어 왔으나, 인수의 의미가 위치에 따라 달라져 타입 혼동이 발생할 수 있습니다.

--mount (명시적 플래그):

# named volume
docker run --mount type=volume,source=mydata,target=/app/data myimage

# bind mount
docker run --mount type=bind,source=/host/path,target=/container/path myimage

# tmpfs
docker run --mount type=tmpfs,target=/tmp/secret myimage

--mounttype=, source=, target= 키-값 쌍으로 마운트 유형과 경로를 명시적으로 지정합니다. 공식 Docker 문서는 --mount의 표현이 더 명확하여 오해를 줄일 수 있다고 안내합니다. 새로운 코드나 문서에서는 --mount를 사용하는 것이 좋습니다.

tmpfs 마운트 간략 소개:

type=tmpfs를 사용하면 컨테이너에 호스트 메모리를 임시 마운트할 수 있습니다. tmpfs의 특징은 다음과 같습니다.

  • 데이터가 호스트 메모리에만 존재하며 디스크에 기록되지 않습니다.
  • 컨테이너가 중지되면 데이터는 즉시 사라집니다(비영속).
  • API 키·세션 토큰처럼 디스크에 남기고 싶지 않은 민감한 임시 파일을 다루는 데 적합합니다.

시리즈 정리: run → load → overlay → cache/mount를 한 줄로 연결하면?

한 줄 답: 격리된 환경을 만들고(1편), 이미지 파일을 이동시키며(2편), 레이어를 합쳐 파일시스템 뷰를 구성하고(3편), 빌드 캐시를 최적화하며 영속 마운트로 데이터를 분리합니다(4편).

이 시리즈는 Docker의 필수 핵심 개념을 4편으로 나누어 살펴보았습니다.

  • 1편 — 네임스페이스·cgroups·run: Docker가 리눅스 네임스페이스와 cgroups로 프로세스를 격리하고, docker run이 이미지로부터 컨테이너를 시작하는 과정을 정리했습니다.
  • 2편 — docker load·save·export: 이미지 파일을 docker save/docker load로 이동하거나, docker export/docker import로 컨테이너 파일시스템을 추출하는 방법을 다뤘습니다.
  • 3편 — overlay2 스토리지: OverlayFS의 lowerdir·upperdir·merged 구조로 이미지 레이어가 하나의 파일시스템으로 합쳐지는 메커니즘, copy-on-write, 쓰기 레이어의 임시성을 살펴봤습니다.
  • 4편 — 레이어 캐시·볼륨·바인드 마운트 (이 글): 빌드 캐시 무효화 규칙을 활용한 Dockerfile 순서 최적화, named volume과 bind mount의 차이, --mount 구문 활용으로 시리즈를 마무리합니다.

한 줄 맵으로 정리하면: 네임스페이스·cgroups로 격리(1편 run) → tar 아카이브로 이미지 이동(2편 load) → OverlayFS로 레이어 통합(3편 overlay) → Dockerfile 캐시 순서 최적화 및 volume·bind mount로 데이터 영속화(4편 cache/mount).

이것으로 Docker 스터디 미니 시리즈를 마칩니다. 각 편의 개념이 서로 연결되어 있으므로, 앞 편의 개념이 모호하다면 해당 편을 다시 확인하시기 바랍니다.

FAQ

질문 답변
tmpfs는 언제 사용하나요? 컨테이너나 호스트 디스크에 저장하고 싶지 않은 민감한 임시 데이터(API 키, 세션 토큰 등)를 처리할 때 적합합니다. 컨테이너가 중지되면 데이터는 즉시 사라지며, 디스크에는 기록되지 않습니다.
볼륨(Volume)은 컨테이너를 삭제하면 어떻게 되나요? 볼륨은 컨테이너 생명주기와 독립적으로 관리됩니다. docker rm으로 컨테이너를 삭제해도 볼륨 데이터는 유지됩니다. 볼륨을 삭제하려면 docker volume rm을 별도로 실행해야 합니다.
docker export로 볼륨 데이터도 내보낼 수 있나요? (2편 복습) 아닙니다. docker export는 컨테이너의 파일시스템 스냅샷(overlay merged 뷰)만 내보냅니다. 마운트된 볼륨이나 bind mount의 데이터는 포함되지 않습니다. 볼륨 데이터를 백업하려면 별도의 방법(임시 컨테이너로 볼륨을 마운트 후 tar로 아카이브 등)을 사용해야 합니다.
bind mount 사용 시 주의할 점은? 호스트의 절대 경로에 직접 의존하므로 다른 환경(CI 서버, 동료 개발 머신 등)에서 동일한 경로가 존재하지 않으면 동작하지 않을 수 있습니다. 이식성이 중요한 프로덕션 데이터 보관에는 named volume을 사용하는 것이 권장됩니다.

출처

한 줄 답: 본문 사실은 2026-09-14 기준으로 확인한 Docker 공식 빌드 캐시·스토리지 문서를 사용합니다.

이 글은 Docker 공식 빌드 캐시·스토리지 문서를 기준으로 한 일반 안내이며, BuildKit 설정·엔진 버전·오케스트레이터 환경에 따라 권장 마운트 방식과 캐시 동작은 달라질 수 있습니다.

Docker builds reuse layer cache from the top down, so frequently changing instructions should be placed at the bottom of the Dockerfile to preserve build speed. Persistent or shared data must not be stored in the overlay writable layer — which is permanently destroyed when the container is removed — but instead in a Docker volume or bind mount. This is the fourth and final part of the Docker study series, covering build cache optimization and data persistence strategies based on the official Docker build cache and storage documentation.

When is the image layer cache invalidated?

Short answer: When one build step (layer) changes, the cache for that step and all subsequent steps is invalidated.

Docker builds execute each Dockerfile instruction in sequence and stores the result of each instruction as a cached layer. On the next build, Docker checks from the topmost layer downward to determine whether each layer can be reused. If a step's content is identical to the previous build, its cached layer is used. However, if any one step changes, the caches for all subsequent layers are invalidated and those steps are re-executed. This cascading behavior is the core rule of build cache invalidation.

According to the official Docker build cache documentation, cache is invalidated in the following cases.

  • The content of the instruction itself changes (e.g., a different RUN command or different arguments to COPY).
  • For COPY and ADD instructions, the checksum of the files being copied has changed.
  • A parent layer (an earlier step) had its cache invalidated, triggering cascading invalidation of all later steps.

Even if source files change frequently, placing infrequently changing instructions — such as dependency installation — above the source copy step keeps the cache hit rate high for those early layers.

Docker BuildKit provides advanced caching features such as parallel builds and cache mounts, but this article focuses on the essential cache invalidation rules relevant to this study series. Advanced features like --mount=type=cache are topics for further exploration beyond the scope of these fundamentals.

What is the optimal instruction order in a Dockerfile to preserve cache?

Short answer: Place infrequently changing instructions at the top and frequently changing instructions at the bottom. Always separate dependency installation from source code copying.

Because cache invalidation propagates downward, the guiding principle is: put what changes rarely first; put what changes often last. Using a Node.js project as an example:

Inefficient order (cache loss):

FROM node:20-alpine
COPY . .
RUN npm install
CMD ["node", "index.js"]

In this structure, any change to a source file invalidates the COPY . . layer, causing RUN npm install to re-execute every time — even when no dependency has changed.

Efficient order (cache preserved):

FROM node:20-alpine
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

With this structure, the RUN npm install layer cache is preserved as long as package.json and package-lock.json remain unchanged, regardless of how often source files are modified.

The same principle applies regardless of language or runtime.

  • Python: COPY requirements.txtRUN pip installCOPY . .
  • Go: COPY go.mod go.sumRUN go mod downloadCOPY . .
  • Java (Maven): COPY pom.xmlRUN mvn dependency:resolveCOPY src ./src

The goal is to place lower-churn layers near the top so that higher-churn layers below them can be invalidated without cascading back up to the expensive dependency installation step.

What is a named volume and where should it be used?

Short answer: A named volume is Docker-managed persistent storage that is independent of any container's lifecycle and can be shared between containers.

As established in part 3, the container's writable layer (upperdir) is permanently destroyed when the container is removed with docker rm. Placing data that must outlive a container — such as database files, uploaded files, or application logs — in that writable layer is therefore inappropriate.

Named volumes are a type of Docker volume officially recommended for persistent container data. Their key characteristics are as follows.

  • Docker-managed: Docker handles volume creation, storage location (typically under /var/lib/docker/volumes/), and lifecycle. Users interact with volumes by name, not by host path.
  • Independent of containers: Removing a container does not delete its associated volume. The volume persists and can be attached to a new container.
  • Shareable: Multiple containers can mount the same named volume simultaneously, enabling data sharing between them.
  • Preferred for write-heavy workloads: Unlike the overlay writable layer, volumes bypass the OverlayFS stack and read/write directly to the host filesystem. The official Docker storage documentation recommends volumes for I/O-intensive workloads for this reason.

Example usage of a named volume:

# Create a volume
docker volume create mydata

# Attach the volume to a container (using --mount)
docker run --mount type=volume,source=mydata,target=/app/data myimage

# List volumes
docker volume ls

# Inspect volume details
docker volume inspect mydata

The name given to source= is the volume name. Omitting a name creates an anonymous volume identified by a random hash. Named volumes are preferred because they are easier to manage, reference, and reuse.

How does a bind mount differ from a volume, and when should it be used?

Short answer: A bind mount connects an arbitrary host path directly into the container and is not managed by Docker. It is useful for mounting source code or configuration files during development.

Bind mounts map a specific directory or file on the host filesystem to a path inside the container. Unlike named volumes, Docker does not create or manage the storage; the host path must already exist.

Bind mounts are commonly used in the following scenarios.

  • Mounting source code into a container during development so that changes are immediately reflected (hot reload) without rebuilding the image.
  • Injecting host configuration files (such as nginx.conf or .env) into a container at startup.
  • Directing output files generated inside the container to a specific location on the host.

However, be aware of the following caveats.

  • Portability: Bind mounts depend on an absolute host path. If the same path does not exist on another machine or environment, the mount will fail or behave unexpectedly.
  • Permissions: Mismatched UID/GID between the host user and the container user can cause permission errors.
  • Security: Mounting sensitive host directories gives the container broad access to those paths, which requires careful consideration.

Volume vs Bind Mount Comparison

Aspect Named Volume Bind Mount
Managed by Docker User (host filesystem managed directly)
Persistence Data survives container removal Data persists as long as the host path exists
Portability High (Docker abstracts the path) Low (depends on a specific host absolute path)
Performance Direct host I/O; well-suited for write-heavy use Direct host I/O (comparable)
Best use Databases, uploads, logs, persistent app data Dev source mounts, config file injection
Initialization Empty volume can be pre-populated from image content Host path content overwrites the container path

Should you use --mount or -v?

Short answer: The official Docker documentation recommends --mount for its explicit and unambiguous syntax.

Docker provides two flags for specifying mounts when running a container.

-v / --volume (shorthand flag):

# Named volume
docker run -v mydata:/app/data myimage

# Bind mount
docker run -v /host/path:/container/path myimage

-v uses a colon-separated shorthand. If the left side is a name, Docker treats it as a named volume; if it is an absolute path, it becomes a bind mount. This positional ambiguity can cause confusion about which type of mount is in effect.

--mount (explicit flag):

# Named volume
docker run --mount type=volume,source=mydata,target=/app/data myimage

# Bind mount
docker run --mount type=bind,source=/host/path,target=/container/path myimage

# tmpfs
docker run --mount type=tmpfs,target=/tmp/secret myimage

--mount uses explicit key-value pairs. The type= field makes the intent immediately clear, reducing the chance of misconfiguration. The official Docker documentation states a preference for --mount due to its verbosity and clarity. New scripts and documentation should prefer --mount.

Brief overview of tmpfs mounts:

Using type=tmpfs mounts host memory as a temporary filesystem inside the container. Key characteristics:

  • Data exists only in host RAM and is never written to disk.
  • Data is lost immediately when the container stops (non-persistent).
  • Appropriate for sensitive temporary data — such as API keys or session tokens — that should not be written to any persistent storage.

Series summary: how do run → load → overlay → cache/mount connect in one line?

Short answer: Create isolated environments (part 1), move image files (part 2), merge layers into a filesystem view (part 3), optimize build cache order and separate data with persistent mounts (part 4).

This four-part Docker study series has covered the fundamental concepts that every developer working with Docker needs to understand.

  • Part 1 — namespaces, cgroups, and run: How Docker uses Linux namespaces and cgroups to isolate container processes, and how docker run starts a container from an image.
  • Part 2 — docker load, save, and export: How to transfer images between environments using docker save and docker load, and how docker export extracts a container filesystem snapshot (without volume data).
  • Part 3 — overlay2 storage: How OverlayFS combines read-only image layers (lowerdir) with a writable container layer (upperdir) into a merged filesystem view, how copy-on-write works, and why the writable layer is ephemeral.
  • Part 4 — layer cache, volumes, and bind mounts (this article): How cache invalidation rules shape Dockerfile instruction order, how named volumes provide Docker-managed persistence independent of the container lifecycle, and how to choose between --mount type=volume, type=bind, and type=tmpfs.

The one-line series map: Isolate with namespaces and cgroups (part 1 run) → transfer images as tar archives (part 2 load) → unify layers with OverlayFS (part 3 overlay) → optimize Dockerfile cache order and persist data with volumes and bind mounts (part 4 cache/mount).

This concludes the Docker study mini-series. The concepts across all four parts are interconnected; if any earlier topic feels unclear, revisiting the relevant part is recommended before moving on.

FAQ

Question Answer
When should I use tmpfs? Use tmpfs when you need to handle sensitive temporary data — such as API keys or session tokens — that should not be written to the container filesystem or the host disk. The data exists only in host memory and disappears immediately when the container stops.
Does a volume survive container removal? Yes. Volumes are managed independently of the container lifecycle. Removing a container with docker rm does not delete its associated volume. The volume must be explicitly removed with docker volume rm.
Does docker export include volume data? (Part 2 review) No. docker export captures only the container's filesystem snapshot (the overlay merged view at that moment). Data stored in mounted volumes or bind mounts is not included in the exported archive. To back up volume data, use a separate approach such as mounting the volume into a temporary container and archiving its contents with tar.
What should I watch out for with bind mounts? Bind mounts depend on an absolute host path, which may not exist or may differ on other machines (CI servers, teammate environments, production hosts). For data that needs to be reliably portable, use a named volume instead of a bind mount.

References

Short answer: Facts in this article are drawn from Docker's official build cache and storage documentation checked on 2026-09-14.

This article is a general overview based on Docker's official build cache and storage documentation. Recommended mount strategies and cache behaviors may vary depending on BuildKit configuration, Docker Engine version, and orchestrator environment.

 

1234···21

+ Recent posts