2025.11.22~23일에 화정체육관에서
진행된 한로로의 4번째 단독콘서트다.

개인적으로 이전 EP인 이상비행, 집에 비하면 그렇게까지 애정이 가는 앨범은 아니다.

또 평소에 라이브가 조금 불안하다고 생각한 가수였다. 그러다 공식 유튜브에 올라온 발아콘 영상을 보고 무조건 가봐야겠다는 생각이 들었다.

티켓팅

티켓팅 당일 일하느라 티켓팅에 실패했는데, 취켓팅으로 맨 뒷열을 겨우겨우 잡았다.

취켓팅은 이번에 처음 해봤는데. 계속 새로고침하고 들여다보고.. 할 짓이 못 된다.
(그러나 후회는 없었다)

대기

산 위라 그런지 대기할만한 곳이 많지는 않았다.
일찍 가서 MD 구매 후 밥 먹고 와서는 정처 없이 떠돌아다녔다.

한 가지 특이한 점은 유달리 대기 중에 나눔이 많았다.

사랑을 모토로 삼는 아티스트라 그런가 유달리 사랑이 많은 팬덤이었다.

입장 시에는 한지水를 증정해 주었다.
한로로의 얼굴이 새겨진 스티커가 붙여진 생수였는데 CJ 쪽에서 만들었더라.

CJ TUNEUP에서 이어진 인연인 듯하다.
이쪽에서 선정된 아티스트들이 취향에 맞는 경우가 많다. 앞으로 주시해보려고 한다.

본공연

첫곡은 내일에서 온 티켓이다. 역시나 익숙한 내레이션과 함께 공연을 시작한다.

시작할 때 무대에 실루엣을 만들어놓고 뒤쪽 서브무대에서 등장하더라. 깜짝 놀랐다.

ㅈㅣㅂ과 같은 2집 라인업은 공연에서 언제나 든든하다. 레이저 눈뽕이 조금 있긴 했지만 그마저도 사랑할 수 있는 웅장한 연출이다.

이 와중에 한로로의 목상태가 너무 좋았다. 그야말로 역대급 라이브를 보여준 것 같다.

옆에서 계속 따라 부르는 약간의 관크가 있긴 했지만
머리에 새긴 사랑으로 이겨냈다.

가장 사랑하는 자처, 거울, 놀이터 등 서정적인 감성의 노래들은 약간 떨리는 창법이 더 돋보이게 만들어주었다. 공연을 내가 하는 것도 아닌데 마음이 떨렸다.

총평

역대급 목상태와 연출로
여운이 오래가는 콘서트였다.

무엇보다 한로로의 노래는 데뷔 때부터 지금까지 버릴곡이 없기에 콘서트 대부분의 시간을 알차게 즐길 수 있었다.

'라이프 > 리뷰' 카테고리의 다른 글

F1 더 무비  (5) 2025.08.05
케이팝 데몬 헌터스  (3) 2025.07.23

이전 글 : https://decompression.tistory.com/19

 

컨테이너 런타임 만들기 : Phase 3

이전 글 : https://decompression.tistory.com/14 컨테이너 런타임 만들기 : Phase 2이전 포스트https://decompression.tistory.com/12 컨테이너 런타임 만들기 : Phase 1컨테이너의 동작을 이해하기 위해 컨테이너 런타임

decompression.tistory.com

 

평범한 컨테이너의 사용사례는 아래와 같다.

docker pull image
docker run image

 

이렇듯 컨테이너 프로세스 생성은 image를 기반으로 동작을 처리하게 되는데.
이 이미지 내부에는 저수준 컨테이너 런타임이 사용할 bundle directory가 포함된다.

bundle directory는 아래와 같은 구조를 지닌다.

bundle directory
ㄴ config.json (spec)
ㄴ root directory

 

이전에 만든 ubuntufs를 번들 디렉터리로 만들어보자.

Makefile#L9-65

setup-ubuntu:
    docker create --name temp-ubuntu ubuntu:22.04
    mkdir -p /root/testbundle/ubuntufs
    docker export temp-ubuntu -o /tmp/ubuntu.tar
    tar -xf /tmp/ubuntu.tar -C /root/testbundle/ubuntufs
    rm /tmp/ubuntu.tar
    docker rm temp-ubuntu
    printf '%s\n' '{' \
    '"ociVersion": "1.0.2",' \
    '"process": {' \
        '"terminal": true,' \
        '"user": { "uid": 0, "gid": 0 },' \
        '"args": ["/bin/bash"],' \
        '"env": [' \
            '"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",' \
            '"TERM=xterm"' \
        '],' \
        '"cwd": "/",' \
        '"capabilities": {' \
            '"bounding": ["CAP_AUDIT_WRITE","CAP_KILL","CAP_NET_BIND_SERVICE"],' \
            '"effective": ["CAP_AUDIT_WRITE","CAP_KILL","CAP_NET_BIND_SERVICE"],' \
            '"inheritable": ["CAP_AUDIT_WRITE","CAP_KILL","CAP_NET_BIND_SERVICE"],' \
            '"permitted": ["CAP_AUDIT_WRITE","CAP_KILL","CAP_NET_BIND_SERVICE"],' \
            '"ambient": ["CAP_AUDIT_WRITE","CAP_KILL","CAP_NET_BIND_SERVICE"]' \
        '},' \
        '"rlimits": [{ "type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024 }],' \
        '"noNewPrivileges": true' \
    '},' \
    '"root": { "path": "/root/testbundle/ubuntufs", "readonly": false },' \
    '"hostname": "oci-ubuntu",' \
    '"mounts": [' \
        '{ "destination": "/proc", "type": "proc", "source": "proc" },' \
        '{ "destination": "/dev", "type": "tmpfs", "source": "tmpfs", "options": ["nosuid","strictatime","mode=755","size=65536k"] },' \
        '{ "destination": "/dev/pts", "type": "devpts", "source": "devpts", "options": ["nosuid","noexec","newinstance","ptmxmode=0666","mode=0620","gid=5"] },' \
        '{ "destination": "/dev/shm", "type": "tmpfs", "source": "shm", "options": ["nosuid","noexec","nodev","mode=1777","size=65536k"] },' \
        '{ "destination": "/dev/mqueue", "type": "mqueue", "source": "mqueue", "options": ["nosuid","noexec","nodev"] },' \
        '{ "destination": "/sys", "type": "sysfs", "source": "sysfs", "options": ["nosuid","noexec","nodev","ro"] }' \
    '],' \
    '"linux": {' \
        '"resources": { "devices": [{ "allow": false, "access": "rwm" }] },' \
        '"namespaces": [' \
            '{ "type": "pid" },' \
            '{ "type": "network" },' \
            '{ "type": "ipc" },' \
            '{ "type": "uts" },' \
            '{ "type": "mount" }' \
        '],' \
        '"maskedPaths": [' \
            '"/proc/kcore", "/proc/latency_stats", "/proc/timer_list", "/proc/timer_stats",' \
            '"/proc/sched_debug", "/proc/scsi", "/sys/firmware"' \
        '],' \
        '"readonlyPaths": [' \
            '"/proc/asound", "/proc/bus", "/proc/fs", "/proc/irq",' \
            '"/proc/sys", "/proc/sysrq-trigger"' \
        ']' \
    '}' \
    '}' > /root/testbundle/config.json

 

Makefile에 setup-ubuntu로 만들었다.
각 필드에 대해 자세한 사항은 runtime-spec을 참고하자 먼저 스펙을 다룰 유틸리티 파일을 만들었다.

 

internal/container/spec.go

package container

import (
"encoding/json"
"fmt"
"os"

"github.com/opencontainers/runtime-spec/specs-go"
)

func loadSpec(specPath string) (*specs.Spec, error) {
var spec specs.Spec
f, err := os.Open(specPath)
if err != nil {
return nil, fmt.Errorf("container: failed to open spec file at %s: %w", specPath, err)
}
defer f.Close()

if err := json.NewDecoder(f).Decode(&spec); err != nil {
return nil, fmt.Errorf("container: failed to decode spec JSON from %s: %w", specPath, err)
}

return &spec, nil

}

 

그리고 create등을 spec기반으로 처리할 수 있도록 변경한다.

// Create initializes a new container with the given ID and root filesystem path.
func Create(containerID, bundlePath string) error {

	bundlePath, err := filepath.Abs(bundlePath)
	if err != nil {
		return fmt.Errorf("container: failed to get absolute path for bundle: %w", err)
	}

	configPath := filepath.Join(bundlePath, "config.json")

	state := newContainerState(containerID, bundlePath)

	spec, err := loadSpec(configPath)
	if err != nil {
		return err
	}

	if err := saveState(state); err != nil {
		return fmt.Errorf("container: failed to save initial state: %w", err)
	}

	cgroup.SetupCgroups()

	selfExe, err := os.Executable()
	if err != nil {
		return fmt.Errorf("container: failed to get executable path: %w", err)
	}

	cmd := exec.Command(selfExe, append([]string{"init"}, spec.Process.Args...)...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	var cloneFlags uintptr
	for _, ns := range spec.Linux.Namespaces {
		switch ns.Type {
		case specs.PIDNamespace:
			cloneFlags |= syscall.CLONE_NEWPID
		case specs.UTSNamespace:
			cloneFlags |= syscall.CLONE_NEWUTS
		case specs.MountNamespace:
			cloneFlags |= syscall.CLONE_NEWNS
		case specs.IPCNamespace:
			cloneFlags |= syscall.CLONE_NEWIPC
		case specs.NetworkNamespace:
			cloneFlags |= syscall.CLONE_NEWNET
		case specs.UserNamespace:
			cloneFlags |= syscall.CLONE_NEWUSER
		case specs.CgroupNamespace:
			cloneFlags |= syscall.CLONE_NEWCGROUP
		case specs.TimeNamespace:
			cloneFlags |= syscall.CLONE_NEWTIME
		}
	}

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: cloneFlags,
	}

	r, w, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("container: failed to create pipe: %w", err)
	}
	defer w.Close()

	cmd.ExtraFiles = []*os.File{r}

	if err := cmd.Start(); err != nil {
		return fmt.Errorf("container: failed to start command: %w", err)
	}

	if err := json.NewEncoder(w).Encode(&spec); err != nil {
		return fmt.Errorf("container: failed to encode spec: %w", err)
	}

	state.Pid = cmd.Process.Pid
	state.Status = specs.StateCreated

	if err := saveState(state); err != nil {
		return fmt.Errorf("container: failed to update state with PID: %w", err)
	}

	return nil
}

 

이제 create에서 분리되는 init process은 이전의 고정적인 실행구조에서
탈피하여 spec을 전해받을 매개가 필요하다.

 

create함수에서는 os pipe를 생성하여 init process에 ExtraFiles로 넘겨준다.

	r, w, err := os.Pipe()
	if err != nil {
		return fmt.Errorf("container: failed to create pipe: %w", err)
	}
	defer w.Close()

	cmd.ExtraFiles = []*os.File{r}

 

넘김받은 파이프는 첫 번째 FD(File Descriptor)로 들어가게 되고.
이는 표준 입/출력/에러를 제외하고 fd3에 열리게 된다.

 

그럼 init에서는 전달받은 파이프를 리슨하여 spec을 받는다.
이후 spec에 정의된 작업을 수행한다.

func Init() {
	pipe := os.NewFile(3, "pipe")
	if pipe == nil {
		log.Fatalf("container: failed to create pipe")
	}
	defer pipe.Close()

	var spec specs.Spec
	if err := json.NewDecoder(pipe).Decode(&spec); err != nil {
		log.Fatalf("container: failed to decode spec: %v", err)
	}

	ch := make(chan os.Signal, 1)

	signal.Notify(ch, syscall.SIGCONT)
	<-ch

	if spec.Hostname != "" {
		if err := syscall.Sethostname([]byte(spec.Hostname)); err != nil {
			log.Fatalf("container: failed to set hostname: %v", err)
		}
	}

	rootfs := spec.Root.Path

	if err := syscall.Mount(rootfs, rootfs, "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
		log.Fatalf("container: failed to bind mount rootfs: %v", err)
	}

	pivotDir := filepath.Join(rootfs, ".old_root")

	if err := os.MkdirAll(pivotDir, 0755); err != nil {
		log.Fatalf("container: failed to create pivot directory %s: %v", pivotDir, err)
	}

	if err := syscall.PivotRoot(rootfs, pivotDir); err != nil {
		log.Fatalf("container: failed to pivot root to %s: %v", rootfs, err)
	}

	if err := os.Chdir("/"); err != nil {
		log.Fatalf("container: failed to change directory to /: %v", err)
	}

	for _, m := range spec.Mounts {
		if err := os.MkdirAll(m.Destination, 0755); err != nil {
			log.Fatalf("container: failed to create mount destination %s: %v", m.Destination, err)
		}

		if err := syscall.Mount(m.Source, m.Destination, m.Type, 0, ""); err != nil {
			log.Fatalf("container: failed to mount %s: %v", m.Destination, err)
		}
		defer func(dest string) {
			if err := syscall.Unmount(dest, 0); err != nil {
				log.Printf("container: failed to unmount %s: %v", dest, err)
			}
		}(m.Destination)
	}

	if err := syscall.Exec(spec.Process.Args[0], spec.Process.Args, os.Environ()); err != nil {
		log.Fatalf("container: failed to exec command %s: %v", spec.Process.Args[0], err)
	}
}

 

추가로 cmd 디렉터리에 main.go 하나로 통일했다.
굳이 나눌 필요가 없었다.

 

실제 실행시 잘 작동한다.

[yhw@hyunwooyoon containeruntime]$ sudo ./containeruntime create ubuntu /root/testbundle
ubuntu
[yhw@hyunwooyoon containeruntime]$ sudo ./containeruntime state ubuntu
{
  "ociVersion": "1.2.1",
  "id": "ubuntu",
  "status": "created",
  "pid": 5158,
  "bundle": "/root/testbundle"
}[yhw@hyunwooyoon containeruntime]$ sudo ./containeruntime start ubuntu
[yhw@hyunwooyoon containeruntime]$ root@oci-ubuntu:/#

 

 

이외에도 Cgroup관련 설계를 진행했다.

디렉터리 구조

 

cgroup SubSystem에 맞는 구조체를 짜넣고 Cgroup Manager에 넣어두면

CgroupManager.Setup()을 통해 Cgroup을 spec에 맞게 설정할거다.

package cgroup

import (
	"fmt"
	"os"
	"path/filepath"
)

// CgroupManager manages the cgroups for a container.
type CgroupManager struct {
	root          string
	containerName string
	subsystems    []SubSystem
}

// SubSystem represents a cgroup v2 controller.
type SubSystem interface {
	Name() string
	Setup(path string) error
	Clean(path string) error
}

// NewCgroupManager creates a new CgroupManager for a given container name.
func NewCgroupManager(containerName string, subsystems []SubSystem) *CgroupManager {
	return &CgroupManager{
		root:          "/sys/fs/cgroup",
		containerName: containerName,
		subsystems:    subsystems,
	}
}

// Setup creates the cgroup hierarchy and configures all subsystems.
func (m *CgroupManager) Setup() error {
	containerCgroup := filepath.Join(m.root, m.containerName)
	if err := os.Mkdir(containerCgroup, 0755); err != nil && !os.IsExist(err) {
		return fmt.Errorf("cgroup: failed to create container cgroup: %w", err)
	}

	var controllers []string
	for _, s := range m.subsystems {
		controllers = append(controllers, "+"+s.Name())
	}
	if len(controllers) > 0 {
		ctrl := []byte(fmt.Sprintf("%s", controllers))
		if err := os.WriteFile(filepath.Join(containerCgroup, "cgroup.subtree_control"), ctrl, 0700); err != nil {
			return fmt.Errorf("cgroup: failed to set controllers: %w", err)
		}
	}

	for _, s := range m.subsystems {
		if err := s.Setup(containerCgroup); err != nil {
			return fmt.Errorf("cgroup: subsystem %s setup failed: %w", s.Name(), err)
		}
	}
	return nil
}

// Clean removes the cgroup hierarchy and cleans up all subsystems.
func (m *CgroupManager) Clean() error {
	containerCgroup := filepath.Join(m.root, m.containerName)
	for _, s := range m.subsystems {
		if err := s.Clean(containerCgroup); err != nil {
			return fmt.Errorf("cgroup: subsystem %s clean failed: %w", s.Name(), err)
		}
	}
	if err := os.Remove(containerCgroup); err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("cgroup: failed to remove container cgroup: %w", err)
	}
	return nil
}

 

서브시스템들을 구현하면서는 Cgroup의 동작원리등의 대한 설명을
더 자세히 하려고 하기에 다음 글에서 이어서 진행하려고 한다.

 

이제 어느 정도 골자가 설계되었고 상세한 부분을 구현하며
실제 동작원리나 이론적인 부분들도 다루어보겠다.

 

설명되지 않은 변경사항은 다음 PR을 참고하자

https://github.com/yoonhyunwoo/containeruntime/pull/4

 

Config based processing by yoonhyunwoo · Pull Request #4 · yoonhyunwoo/containeruntime

need squash merge

github.com

 

 

참고자료

https://github.com/opencontainers/runtime-spec

https://namu.wiki/w/%EC%9D%B8%EA%B0%90

인감인감인감..
인감(도장) 인감.. (인가의 음슴체)

'깔깔유머' 카테고리의 다른 글

레거시세계에서의 탈출  (0) 2025.08.05

평소 좋아하던 싱어송라이터인 한로로(본명 : 한지수)의 책이 나와서 사봤다.

 

사실 큰 기대를 하며 책을 구매하지는 않았다.
호기심 반, 팬심 반에 마침 HTTP 완벽 가이드를 구매하려고 했던 터라 같이 주문했다.

 

나는 노래를 들을 때 가사를 제일로 보는데,
한로로는 작사 능력이 상당히 출중하다. 국어국문학과 출신의 기예라고 생각한다.

 

같은 이유로 좋아하는 가수들은 비틀즈, 아이유, 백현진 등..

여러 가수, 그룹들이 있다.

 

여기서부터는 소설 "자몽살구클럽"에 대한 스포와
주관적 해석이 있으니 주의하자.

자몽 살구 클럽

 

작중에서 화자로 등장하는 중학교 1학년 김소하는 삶의 의미를 찾지 못하는 청춘이다.

아빠는 알콜중독자에 가정폭력봄이고, 자신에게 유일하게 친절하던 엄마는 도망갔다. 친구도 하나 없다.

 

말 그대로 삶의 기둥 없는 나날들을 보내다

어느 날 자몽살구클럽이라는 동아리 모집 공고를 본다.

 

죽고 싶지만 실은 살고 싶은 자들의 모임, 서로를 지탱하자는 그
이야기에 오랜만의 흥미를 느낀 소하는 그들을 찾아간다.

 

자몽살구클럽의 멤버들은 총 4명으로 각자의 아픔을 지니고 있다.

엄마의 높은 기대와 억압 속에 괴로워하지만 밝은 척하는 리더 하태수

암 투병중인 엄마를 사랑하는, 영화감독을 꿈꾸는 이보현

태수의 오랜 친구인 나유민 

 

화자인 소하의 입장에서 그들은 어른처럼 보일지 몰라도
사실은 모두 앳된 소녀들이다.

 

이야기가 진행되며 소하는 자몽살구클럽의 회원들과 유대를 쌓게 된다.

함께 일탈하고, 이야기하고, 여행하고, 즐기며 삶의 의미를 찾게 된다. 

 

그러던 중 엄마와의 갈등이 심화된 태수는 자해의 흔적과 함께 나타나
그 응어리를 풀기 위해 한바탕 회원들과 노래하며 춤춘다.

 

그리고 그날 새벽 학교 옥상에서 자살한다.

 

유민은 자신을 지탱하던 가장 절친한 친구를 잃었고,
태수의 엄마는 강박적으로 키워내던 소중한 딸을 잃었다.

각자가 최선이라 생각한 길은 모두의 최악으로 돌아왔다.

 

이건 언뜻 엄마를 향한 태수의 복수라고 생각한다.
딸을 위해 살던 엄마에게 희생당한 태수가 그녀에게서 딸을 빼앗았다.

 

태수를 떠나보낸 절친한 친구 유민은 자몽살구클럽 회원들과 함께

그녀가 좋아하던 음악선생님의 도움을 받아 장송곡을 준비한다.

 

보현과 소하는 어설픈 실력으로 캐스터네츠와 트라이앵글을, 음악선생님은 피아노를 유민은 노래를 한다.

생전 태수는 유민의 노래를 좋아했기 때문이다.

이런 배경설정과 가사는 자몽살구클럽 EP의 수록곡인 To. __에 잘 녹아져 있다.

 

그러던 중 소하는 자몽살구클럽 회원들과 함께 엄마를 찾아 나선다.
우여곡절 끝에 찾아낸 엄마는 학대받던 지난날과는 달랐다.
생기 넘치는 얼굴에 어여쁜 카페사장과 예쁜 아이와 남편이 있었다.

 

소하를 지탱하던 마지막 조각이 무너졌다.
유일하게 친절하던 기억 속의 엄마는 내가 아닌 다른 아이를 안고 있었고
소하가 돌아갈 집은 없었다.

 

돌아온 소하는 여느 때처럼 아빠의 학대를 받는다.

그때, 태수에게 받은 용기였을까, 혹은 어린 날의 조종이었을까.
소하는 자고 있는 아빠를 식칼로 찔러 살해한다.

 

자기밖에 모르던 아빠는 딸에게 자신을 잃었다.

 

결국 소설은 끊임없이 등장인물들을 고문한다.

아픔은 쉴 새 없이 그들을 조여 오고 작가는 이를 특유의 표현법으로 풀어낸다.

 

지금 와서 읽어본 클럽의 제목 자몽살구클럽은 
실제로 먹어보면 굉장히 쓴 자몽과, 달달한 살구가 번갈아가며 느껴지기에
자몽살구클럽이 아닌가 싶다.

 

잡설이 길었는데 한마디로 아픔을 조명하는 소설이다.

 

여기에 소설을 기반으로 한 자몽살구클럽 EP의 수록곡들까지 더해지며

독자는 공감과 몰입의 경험을 얻게 된다.

내용과 전개 자체는 별 거 없지만 특유의 표현력에 몰입하게 한다.

혹시라도 페이지를 되돌리면 태수가 돌아올까 앞으로 넘어가기도 했다.

 

큰 기대 없이 봤는데 생각보다 몰입감 있게 보았다. 

혹시라도 책을 읽게 된다면 동명의 EP도 들어보길 추천하낟.

 

마지막으로 자몽살구클럽의 해산멘트를 외치며 마무리한다.

 

나는 살구싶다!

나는 살구싶다!
나는 살구싶다!

 

살구처럼 살고 싶었는데
현실은 자몽이더라.

'' 카테고리의 다른 글

실용주의 프로그래머(20주년 기념판)  (3) 2025.08.01

이전 글 : https://decompression.tistory.com/14

 

컨테이너 런타임 만들기 : Phase 2

이전 포스트https://decompression.tistory.com/12 컨테이너 런타임 만들기 : Phase 1컨테이너의 동작을 이해하기 위해 컨테이너 런타임을 만들어본다. 세상에 컨테이너 런타임이라고 알려진 소스들은 너무

decompression.tistory.com

 

전 글에서는 OCI(Open Container Initiative) Runtime의
CLI(Commad Line Interface) 골자를 만들었다.

 

이번엔 OCI Runtime Spec의 State와 그를 기반으로 한
컨테이너 동작을 구현한다.

 

컨테이너 state의 예시는 다음과 같다.

{
    "ociVersion": "0.2.0",
    "id": "oci-container1",
    "status": "running",
    "pid": 4422,
    "bundle": "/containers/redis",
    "annotations": {
        "myKey": "myValue"
    }
}

 

어려울 건 없다. State가 가져야 할 값은 다음과 같다.

  • ociVersion : 현재 state가 준수하는 OCI Runtime Spec의 버전
  • id : 컨테이너의 ID, 호스트 내에서 고유해야 함
  • status : 컨테이너 런타임 상태 (creating, created, running, stopped)
  • pid : 컨테이너 프로세스 ID
  • bundle : 컨테이너 번들 디렉터리의 절대 경로
  • annotations : 컨테이너의 주석 목록

이전에 만들었던 container 패키지에 직접 구조체를 생성할 수도 있겠지만
OCI Runtime Spec은 specs-go 패키지에서 사용할 수 있는 구조체등을 제공한다,

 

specs-go가 제공하는 State구조체는 아래와 같다.

// State holds information about the runtime state of the container.
type State struct {
	// Version is the version of the specification that is supported.
	Version string `json:"ociVersion"`
	// ID is the container ID
	ID string `json:"id"`
	// Status is the runtime status of the container.
	Status ContainerState `json:"status"`
	// Pid is the process ID for the container process.
	Pid int `json:"pid,omitempty"`
	// Bundle is the path to the container's bundle directory.
	Bundle string `json:"bundle"`
	// Annotations are key values associated with the container.
	Annotations map[string]string `json:"annotations,omitempty"`
}

 

이러한 state를 어디서 관리할지는 자율적이다. 
이번 contaeruntime 프로젝트의 경우 /run/containeruntime에 저장한다.

 

/run 디렉터리는 런타임 변수 데이터를 저장하는 디렉터리로 시스템이 부팅된 이후부터의 데이터를 가진다.
containerd와 같은 고수준 런타임은 상태 저장형으로 동작하기에 /var/lib/containerd와 같은 영구적인 디렉터리를 사용하지만

저수준 런타임의 경우 어디까지나 런타임 스코프 내에서 동작해야 한다.

 

container패키지에 state관리를 위한 state.go파일을 만들어 관리용 함수 몇 개를 추가했다.

package container

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/opencontainers/runtime-spec/specs-go"
)

const (
	containeruntimeStateDir string = "/run/containeruntime"
)

func initStateDir() error {
	return os.MkdirAll(containeruntimeStateDir, 0755)
}

func getStatePath(containerId string) string {
	return filepath.Join(containeruntimeStateDir, containerId+".json")
}

func saveState(state *specs.State) error {
	statePath := getStatePath(state.ID)
	tempPath := statePath + ".tmp"

	f, err := os.Create(tempPath)
	if err != nil {
		return fmt.Errorf("Can not save state: %v", err)
	}
	defer f.Close()
	defer os.Remove(tempPath)

	if err := json.NewEncoder(f).Encode(state); err != nil {
		return fmt.Errorf("Can not parsing state : %v", err)
	}

	f.Close()
	return os.Rename(tempPath, statePath)
}

func loadState(containerId string) (*specs.State, error) {
	statePath := getStatePath(containerId)
	state := &specs.State{}

	f, err := os.Open(statePath)
	if err != nil {
		return nil, fmt.Errorf("Can not open state: %v", err)
	}
	defer f.Close()

	if err = json.NewDecoder(f).Decode(state); err != nil {
		return nil, fmt.Errorf("Can not parsing state : %v", err)
	}

	return state, nil
}

func deleteState(containerId string) error {
	statePath := getStatePath(containerId)
	if err := os.Remove(statePath); err != nil {
		return fmt.Errorf("Can not delete state: %v", err)
	}
	return nil
}

func listState(containerId string) ([]*specs.State, error) {
	var states []*specs.State
	files, err := os.ReadDir(containeruntimeStateDir)
	if err != nil {
		return nil, fmt.Errorf("Can not list state: %v", err)
	}

	for _, file := range files {
		if strings.HasSuffix(file.Name(), ".json") {
			state, err := loadState(file.Name())
			if err != nil {
				continue
			}
			states = append(states, state)
		}
	}

	return states, nil
}

func newContainerState(id, bundlePath string) (*specs.State, error) {
	state := &specs.State{
		Version:     specs.Version,
		ID:          id,
		Status:      specs.StateCreating,
		Pid:         0,
		Bundle:      bundlePath,
		Annotations: nil,
	}
	return state, nil
}

func setContainerPID(containerId string, pid int) error {
	state, err := loadState(containerId)
	if err != nil {
		return fmt.Errorf("Can set container pid: %v", err)
	}

	state.Pid = pid
	if err := saveState(state); err != nil {
		return fmt.Errorf("Can set container pid: %v", err)
	}

	return nil
}

 

 

이외에도 이전 포스트에서 사용한 SIGCON 시그널을 start에서 실행되도록 만든다.

func Start(containerId string) error {
	state, err := loadState(containerId)
	if err != nil {
		fmt.Printf("container : %v\n", err)
		return fmt.Errorf("container : %v", err)
	}
	state.Status = specs.StateRunning
	syscall.Kill(state.Pid, syscall.SIGCONT)
	saveState(state)
	return nil
}

 

 

이후 start 함수를 cli에서 호출할 수 있도록 한다.

cmd/start.go

package cmd

import (
    "context"
    "fmt"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var StartCommand = &cli.Command{
    Name: "start",
    Action: func(ctx context.Context, command *cli.Command) error {
       if command.Args().Len() != 1 {
          return nil
       }

       containerId := command.Args().First()
       err := container.Start(containerId)
       if err != nil {
          fmt.Printf("Can not start conateinr %s", containerId)
       }
       return nil
    },
}

 

이제 컨테이너 state를 반환하는 state명령어를 만든다. 

출력을 위해 직렬화된 state는 아래와 같다. (state-schema.json)

{
    "description": "Open Container Runtime State Schema",
    "$schema": "http://json-schema.org/draft-04/schema#",
    "type": "object",
    "properties": {
        "ociVersion": {
            "$ref": "defs.json#/definitions/ociVersion"
        },
        "id": {
            "description": "the container's ID",
            "type": "string"
        },
        "status": {
            "type": "string",
            "enum": [
                "creating",
                "created",
                "running",
                "stopped"
            ]
        },
        "pid": {
            "type": "integer",
            "minimum": 0
        },
        "bundle": {
            "type": "string"
        },
        "annotations": {
            "$ref": "defs.json#/definitions/annotations"
        }
    },
    "required": [
        "ociVersion",
        "id",
        "status",
        "bundle"
    ]
}

 

이게 출력되면 아래와 같은 예시가 나온다.

{
    "ociVersion": "0.2.0",
    "id": "oci-container1",
    "status": "running",
    "pid": 4422,
    "bundle": "/containers/redis",
    "annotations": {
        "myKey": "myValue"
    }
}

 

이에 맞게 state구조체를 반환하는 State 함수를 만든다.

internal/container/container.go

func State(containerId string) (*specs.State, error) {
    state, err := loadState(containerId)
    if err != nil {
       fmt.Printf("container : %v\n", err)
    }
    return state, err
}

 

그리고 cmd/state.go에서 이를 받아 출력한다.

package cmd

import (
    "context"
    "encoding/json"
    "fmt"
    "os"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var StateCommand = &cli.Command{
    Name: "state",
    Action: func(ctx context.Context, command *cli.Command) error {
       if command.Args().Len() != 1 {

          return nil
       }

       containerId := command.Args().First()
       containerState, err := container.State(containerId)
       if err != nil {
          fmt.Printf("Can not get conateinr %s", containerId)
       }

       containerStateBytes, _ := json.MarshalIndent(containerState, "", " ")
       os.Stdout.Write(containerStateBytes)
       return nil
    },
}

 

 

빌드 이후 실행해 보면 포맷에 맞게 state가 잘 떨어지는 모습을 볼 수 있다.

[hwyoon@rocky9 containeruntime]$ ./containeruntime state id
{
 "ociVersion": "1.2.1",
 "id": "id",
 "status": "created",
 "pid": 19977,
 "bundle": "/rootfs/ubuntu"
}

 

 

이외에도 cli기본 뼈대를 위해 나머지를 간단히 만든다.

 

kill에 대한 설명은 아래와 같다.
create, running상태인 컨테이너를 대상으로 지정된 시그널을 보내는 것이다.

Kill
kill <container-id> <signal>

This operation MUST generate an error if it is not provided the container ID. Attempting to send a signal to a container that is neither created nor running MUST have no effect on the container and MUST generate an error. This operation MUST send the specified signal to the container process.

 

간단히 kill 함수를 구현해 주었다.

internal/container/container.go

func Kill(containerId string, signal syscall.Signal) error {
    state, err := loadState(containerId)
    if err != nil {
       fmt.Printf("container : %v\n", err)
    }
    if state.Status == specs.StateRunning || state.Status == specs.StateCreated {
       return fmt.Errorf("You can send a signal only to containers in the running or created state.")
    }
    syscall.Kill(state.Pid, signal)
    return nil
}

 

이제 kill signal을 어떻게 받을 것 인지가 중요한데.

SIGTERM과 같이 human-readable 하게 보내지는 않는다. 

 

linux 시스템의 kill 커맨드와 같이 숫자로 받는다. (사실 시스템콜이 다 int 기반이다)

흔히 아래와 같은 명령어로 익숙할 것이다.

kill -9 <PID>

 

이런 번호를 아는 방법은 여러 가지 있겠지만

가장 편한 건 kill -l을 통해 조회할 수 있다.

[hwyoon@rocky9 containeruntime]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

 

go에서 이를 다루는 방법은 간단하다.

syscall 패키지에서 제공하는 Signal 객체로 캐스팅해 주면 된다.

cmd/kill.go

package cmd

import (
    "context"
    "fmt"
    "strconv"
    "syscall"

    "github.com/urfave/cli/v3"
    "github.com/yoonhyunwoo/containeruntime/internal/container"
)

var KillCommand = &cli.Command{
    Name:      "kill",
    Usage:     "This command sends a specific signal to the main process of a container.",
    ArgsUsage: "<containerid> <signal>",
    Action: func(ctx context.Context, command *cli.Command) error {

       if command.Args().Len() != 2 {
          return nil
       }

       containerId := command.Args().First()

       signalNumber, err := strconv.Atoi((command.Args().Get(1)))
       if err != nil {
          fmt.Printf("Invalid signal number: %s\n", command.Args().Get(1))
          return nil
       }

       signal := syscall.Signal(signalNumber)
       err = container.Kill(containerId, signal)
       if err != nil {
          fmt.Printf("Can not start conateinr %s", containerId)
       }
       return nil
    },
}

 

다음은 delete다. 이를 수행 시 SIGKILL을 통해 프로세스를 종료한다.

종료가 무사히 완료되면 컨테이너를 위해 생성했던 cgroups, state 등을 정리해야 한다.

 

관행적으로 프로세스의 생존유무는 signal 0으로 체크한다.

여기선 가볍게 5초 정도 기다리도록 했다.

internal/container/container.go

func Delete(containerId string) error {
    _ = Kill(containerId, syscall.SIGKILL)
    for range 5 {
       time.Sleep(1 * time.Second)
       if err := Kill(containerId, 0); err != nil {
          return deleteState(containerId)
       }
    }
    return errors.New("The container is still running")
}

 

또한 Cgroups도 정리해야 한다.

internal/linux/cgroup/v2.go

func CleanCgroups() error {
	cgroupRoot := "/sys/fs/cgroup"
	processCgroup := filepath.Join(cgroupRoot, "gamap-container", "processes")
	if _, err := os.Stat(processCgroup); err == nil {
		procsFile := filepath.Join(processCgroup, "cgroup.procs")
		err = os.WriteFile(procsFile, []byte(""), 0700)
		if err != nil {
			return errors.New("Error removing cgroup processes")
		}
		time.Sleep(100 * time.Millisecond)
		err = os.Remove(processCgroup)
		if err != nil {
			return errors.New("Error removing cgroup processes")
		}
	}

	return nil
}

 

 

이후 역시 이를 delete커맨드에서 사용하도록 만들어준다.

cmd/delete.go

package cmd

import (
	"context"
	"errors"

	"github.com/urfave/cli/v3"
	"github.com/yoonhyunwoo/containeruntime/internal/container"
	"github.com/yoonhyunwoo/containeruntime/internal/linux/cgroup"
)

var DeleteCommand = &cli.Command{
	Name:      "delete",
	Usage:     "This command deletes a container and its associated resources.",
	ArgsUsage: "<container-id>",
	Action: func(ctx context.Context, command *cli.Command) error {
		if command.Args().Len() != 1 {
			return errors.New("container-id is required")
		}

		containerId := command.Args().First()
		if err := container.Delete(containerId); err != nil {
			return err
		}

		return cgroup.CleanCgroups()
	},
}

 

[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime create
Running: []
[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime state id
{
 "ociVersion": "1.2.1",
 "id": "id",
 "status": "created",
 "pid": 21245,
 "bundle": "/rootfs/ubuntu"
}[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime delete id
[hwyoon@rocky9 containeruntime]$ sudo ./containeruntime state id
container : Can not open state: open /run/containeruntime/id.json: no such file or directory
Can not get conateinr idnull[hwyoon@rocky9 containeruntime]$ 
[hwyoon@rocky9 containeruntime]$ ps -ef | grep -i 21245
hwyoon     21285   19793  0 22:52 pts/2    00:00:00 [rosetta] /usr/bin/grep grep --color=auto -i 21245
[hwyoon@rocky9 ~]$ ls /sys/fs/cgroup/gamap-container/processes
ls: cannot access '/sys/fs/cgroup/gamap-container/processes': No such file or directory

 

프로세스 및 state, cgroups까지 잘 지워진다.

 

만들면서 계속 빌드 및 테스트가 반복되고 있다.
어딘가에는 명시를 해두어야 할 것 같아서 Makefile을 만들었다.

BINARY_NAME=containeruntime
GOOS=linux

.PHONY: build setup-ubuntu lint

build:
    GOOS=$(GOOS) go build -o $(BINARY_NAME) .

setup-ubuntu:
    sudo docker create --name temp-ubuntu ubuntu:22.04
    sudo mkdir -p /root/ubuntufs
    sudo docker export temp-ubuntu -o /tmp/ubuntu.tar
    sudo tar -xf /tmp/ubuntu.tar -C /root/ubuntufs
    sudo rm /tmp/ubuntu.tar
    sudo docker rm temp-ubuntu

lint:
    go fmt ./...

 

복잡하게 만들 필요는 없을 것 같고

build, setup-ubuntu, lint정도만 만들었다.

 

지금까지의 플로우는 아래와 같다.

containeruntime의 컨테이너 라이프사이클

 

 

이외에 따로 작성하지 않은 변경사항은 PR(Pull Request)을 참고하자

https://github.com/yoonhyunwoo/containeruntime/pull/2/files

 

feat : Handles container lifecycle based on state by yoonhyunwoo · Pull Request #2 · yoonhyunwoo/containeruntime

State-based container lifecycle management Utilizing /run/containeruntime as the state directory

github.com

 

 

다음 포스트에선 에러 처리 및 config기반 처리를 수행하려고 한다.

 

참고자료

https://pkg.go.dev/github.com/opencontainers/runtime-spec/specs-go

https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html

https://github.com/opencontainers/runtime-spec/blob/main/schema/state-schema.json

https://www.linuxquestions.org/questions/linux-newbie-8/signal-0-in-linux-835606/

인텔의 메인보드는 시스템의 관리를 위한 BMC(BaseBoard Management Controller)를 제공한다.

물론 다 제공하는 건 아니고 주로 서버용의 일부 모델들이다.

이 BMC에서는 디버깅을 위한 debug log를 제공한다.
로그를 다운로드하면 zip파일이 떨어지는데 여기 비밀번호가 떨어진다.

골 때린다.
이런 거 설정한 적도 없다.

알고 보니 인텔 내부에서 점검할 때 쓰는 용도더라
그런데 어떤 용자가 이걸 발견했다.
아래 링크에서 볼 수 있다.
https://gist.github.com/GarryLai/a958665250c2dbc67c66e27877c01925

 

Intel BMC System Debug Log zip password

Intel BMC System Debug Log zip password. GitHub Gist: instantly share code, notes, and snippets.

gist.github.com

땡큐 GarryLai!

어떻게 발견했나 했더니

펌웨어 이미지 중 하나를 압축 해제하니 zip 파일을 만드는 쉘 스크립트에 하드코딩되어 있었다고 한다.

'인프라' 카테고리의 다른 글

Playwright 종속성 설치 시 Node.js 경로 지정  (2) 2025.07.22

레미시세계로의 입장

https://c-knou.com/knou_photo/608996

거시세계 <-> 미시세계에 도레미 음계까지 있는
훌륭한 깔깔유머다.

'깔깔유머' 카테고리의 다른 글

인력감축 서류에 찍는 인감도장은?  (0) 2025.08.18

F1 더 무비 포스터

F1 더 무비.
코엑스 메가박스에서 4D로 감상했다.


캬라멜 팝콘과 갈릭 팝콘이다.
나는 캬라멜 팝콘을 먹었는데,
광고중 거의 반은 먹은 것 같다.

스탠다드한 팝콘 맛이다.

의자는 생각보다 평범했다.
이게 움직이려나 싶은 정도?

영화 내용은 사고 이후 F1을 떠난 한물 간 드라이버
소니 헤이스가 가상의 팀 에이펙스 GP의 시트로
들어와 우여곡절 끝에 그랑프리를 우승하는 내용이다.

평범한 갈등-전개-위기-결말 구성이다.
어차피 스포츠 영화는 몰입감이 가장 중요하다.

그런 의미에서 몰입하는 경험은 아주 좋았다.
카메라는 드라이버의 시선을 따라 움직이고
실제 드라이버들이 출연해 시트를 지킨다.
물론 4DX또한 몰입감을 더해준다.

취향에 맞으면 정말 재밌게 즐길 수 있겠다.

이 영화로 F1에 입문하는 경우도 꽤 있던데
F1팬들은 뉴비, 패션 팬 배척 성향이 꽤 적은 것 같다.

덕분에 아마 꽤 많은 수가 코어 팬층으로
자리잡을 것 같다.

여기서 대표적인 부정적인 사례는
락팬들인데, 그들은 열심히 갈라파고스를 만들고 있다.

QWER, 데이식스와 그 팬들을 물어뜯으며
사람들은 락의 멋짐을 몰라.. 같은 태도를 취한다.

물론 전부가 그렇다는 건 아니고..
일부 락 성골론자들의 경우다.

그런 의미에서 이번 영화를 계기로
모터스포츠에 대한 파이가 더 커질 수 있는 계기가 될 것 같다.

'라이프 > 리뷰' 카테고리의 다른 글

한로로 단독콘서트 - 자몽살구클럽  (0) 2025.12.30
케이팝 데몬 헌터스  (3) 2025.07.23

 

실용주의 프로그래머 20주년 기념판

 

실용주의 프로그래머(the pragmatic programmer) 20주년 기념판을 읽었다.
출퇴근길에 가볍게 1 회독했고 한번 정도 더 읽어볼 생각이다.

 

책에서는 저자의 개발에 대한 전반적인 팁을 서술한다.

 

대체로 white paper 스러운 느낌을 주지만 특히 인상 깊게 느낀 부분을 몇 개 적어본다.

 

예광탄(tracer bullet)은 불빛을 통해 총알이 날아가는 궤적이 보이는 탄환이다.

 

예광탄 개발 방법은 마치 예광탄이 타깃을 관통하듯 핵심적인 부분을 먼저 개발하고

점점 살을 붙여 나가는 개발 방법론이다.

 

이는 근본적으로 문제를 쪼개고 점진적 개발을 추구하는 현대적 아키텍처와 일맥상통한다.
MSA(micro service architecture), Agile, 기타 등등..

 

인간이 유지할 수 있는 컨텍스트는 유한하기에 이를 작은 단위로 쪼개고

차근차근 문제를 해결해 나가는 것 이다.

 

다음은 Topic 30 변환 프로그래밍이다.

이 토픽의 핵심적인 철학은 모든 프로그램은 입력을 출력으로 바꾸는 작업이라는 것이다.

 

위대한 엔지니어들은 데이터에 큰 의미를 가진다.

대표적으로는 1973년 튜링상 수상 강연인 Programmer as Navigator(항해자로서의 프로그래머)를 볼 수 있다.

자기 테이프 시대에서 하드 디스크 시대로 넘어오며 극초기의 데이터베이스 설계자인 Charles W. Bachman은

이 발표에서 순차적 디스크 접근시대를 벗어나며 관찰자에서 항해자로 프로그래머의 역할이 바뀌었다고 말한다.

 

Topic 30 변환 프로그래밍에서는 이런 순수성을 다시 일깨워준다.

우리가 다루어야 할 문제는 결국 데이터의 인풋과 아웃풋 사이의 현상이란 것이다.

데이터의 이동에 비즈니스 로직을 맞추면 프로그래머가 설계에 있어 이를 더 쉽게 이해할 수 있게 만든다.

 

Topic 44 이름 짓기는 우리가 이름을 어떻게 지어야 하는지에 대해, 이름의 의미에 대해 말한다.

코드를 짤 때 최대 난제인 변수명 짓기.. 요즘은 생성형 AI들이 아주 잘해준다지만 컨텍스트를 이해하지 못하면

아직도 답답한 경우가 많고, 남의 코드를 읽을 때에는 큰 도움이 되지 않는다.

 

어쩔 땐 과도한 이름 짓기가 불편하게 느껴지기도 한다. 예를 들어 요즘은 Ops가 너무 많다.

  • DevOps
  • SecOps
  • DevSecOps
  • DataOps
  • FinOps
  • GitOps
  • AIOps
  • MLOps
  • ChatOps

그것만으로 문제가 되는가? 아니다. 용어는 우리가 소통하는 데 있어

DRY(Don't Repeat Yourself) 원칙을 지키게 해 준다.

 

하지만 용어라는 틀에 갇히는 순간 행위는 틀에 종속된다. 

"우리는 GitOps라서 이렇게 하면 안 돼"
"우리는 AIOps를 실현하기 위해서 어떻게 해야 해"

 

용어의 등장은 그저 창시자가 정의하고픈 어떤 것에 붙인 이름일 뿐임에도

이에 갇혀 능동적인 행위를 막는다. 일종의 Cargo cult인 것이다.

사실은 용어도 어떤 요구사항에서 나왔으며, 조직에 따라 달라져야 한다.

 

전반적으로 무난하게 좋은 책이었고

꽤 흥미롭게 읽었다. 철학서에 가깝기에 가벼운 마음으로 읽었다.

 

다만 기술 용어들은 한글과 영어를 좀 더 크게 병용하면 더 좋았을 것 같다.

한글로 번역되어 익숙하지 않은 용어들은 오히려 책을 읽을 때 이물감을 느끼게 했다.

 

수많은 챕터 중에서도 적은 챕터에 대한 간략한 리뷰와 총평을 써봤는데

이 책을 볼까 말까 고민된다면 책에서 제공하는 수록 팁을 보길 바란다.

취향에 맞는 책인지 아닌지 판단할 수 있을 것이다.

 

수록 팁

더보기
  1. 자신의 기예(craft)에 관심을 가져라
  2. 자기 일에 대해 생각하라.
  3. 당신에게는 에이전시(agency)가 있다.
  4. 어설픈 변명 말고 대안을 제시하라.
  5. 깨진 창문을 내버려 두지 말라.
  6. 변화의 촉매가 돼라.
  7. 큰 그림을 기억하라.
  8. 품질을 요구 사항으로 만들어라.
  9. 지식 포트폴리오에 주기적으로 투자하라.
  10. 읽고 듣는 것을 비판적으로 분석하라.
  11. 한국어든 영어든 하나의 프로그래밍 언어일 뿐이다.
  12. 무엇을 말하는가와 어떻게 말하는가 모두 중요하다.
  13. 문서를 애초부터 포함하고, 나중에 집어넣으려고 하지 말라.
  14. 좋은 설계는 나쁜 설계보다 바꾸기 쉽다.
  15. DRY: 반복하지 말라 Don’t Repeat Yourself
  16. 재사용하기 쉽게 만들어라.
  17. 관련없는것들간에서로영향이없도록하라.
  18. 최종 결정이란 없다.
  19. 유행을 좇지 말라.
  20. 목표물을 찾기 위해 예광탄을 써라.
  21. 프로토타이핑으로 학습하라.
  22. 문제 도메인에 가깝게 프로그래밍하라.
  23. 추정으로 놀람을 피하라.
  24. 코드와 함께 일정도 반복하며 조정하라.
  25. 지식을 일반 텍스트로 저장하라.
  26. 명령어 셸의 힘을 사용하라.
  27. 에디터를 유창하게(fluency) 쓸 수 있게 하라.
  28. 언제나 버전 관리 시스템을 사용하라.
  29. 비난 대신 문제를 해결하라.
  30. 당황하지 말라.
  31. 코드를 고치기 전 실패하는 테스트부터.
  32. 그놈의(damn…) 오류 메시지 좀 읽어라.
  33. “select”는 망가지지 않았다.
  34. 가정하지 말라. 증명하라.
  35. 텍스트 처리 언어를 익혀라.
  36. 여러분은 완벽한 소프트웨어를 만들 수 없다.
  37. 계약으로 설계하라.
  38. 일찍 작동을 멈춰라.
  39. 단정문으로 불가능한 상황을 예방하라.
  40. 자신이 시작한 것은 자신이 끝내라.
  41. 지역적으로 행동하라.
  42. 작은 단계들을 밟아라. 언제나.
  43. 예언하지 말라.
  44. 결합도가 낮은 코드가 바꾸기 쉽다.
  45. 묻지 말고 말하라 Tell, Don’t Ask, TDA.
  46. 메서드 호출을 엮지 말라.
  47. 전역 데이터를 피하라.
  48. 전역적이어야 할 만큼 중요하다면 API로 감싸라.
  49. 프로그래밍은 코드에 관한 것이지만, 프로그램은 데이터에 관한 것이다.
  50. 상태를 쌓아 놓지 말고 전달하라.
  51. 상속세를 내지 말라.
  52. 다형성은 인터페이스로 표현하는 것이 좋다.
  53. 서비스에 위임하라. Has-A가 Is-A보다 낫다.
  54. 믹스인으로 기능을 공유하라.
  55. 외부 설정으로 애플리케이션을 조정할 수 있게 하라.
  56. 작업 흐름 분석으로 동시성을 개선하라.
  57. 공유 상태는 틀린 상태다.
  58. 불규칙한 실패는 동시성 문제인 경우가 많다.
  59. 공유 상태 없는 동시성을 위하여 액터를 사용하라.
  60. 칠판으로 작업 흐름을 조율하라.
  61. 여러분 내면의 파충류에게 귀 기울여라.
  62. 우연에 맡기는 프로그래밍을 하지 말라.
  63. 사용하는 알고리즘의 차수를 추정하라.
  64. 여러분의 추정을 테스트하라.
  65. 일찍 리팩터링하고, 자주 리팩터링하라.
  66. 테스트는 버그를 찾기 위한 것이 아니다.
  67. 테스트가 코드의 첫 번째 사용자다.
  68. 상향식이나 하향식이 아니라 끝에서 끝까지(end-to-end) 만들어라.
  69. 테스트할 수 있도록 설계하라.
  70. 여러분의 소프트웨어를 테스트하라. 그러지 않으면 사용자가 테스트하게 된다.
  71. 속성 기반 테스트로 가정을 검증하라.
  72. 단순함을 유지하고 공격 표면을 최소화하라.
  73. 보안 패치를 신속히 적용하라.
  74. 이름을 잘 지어라. 필요하면 이름을 바꿔라.
  75. 자신이 뭘 원하는지 정확히 아는 사람은 아무도 없다.
  76. 프로그래머는 사람들이 자신이 원하는 바를 깨닫도록 돕는다.
  77. 요구 사항은 피드백을 반복하며 알게 된다.
  78. 사용자처럼 생각하기 위해 사용자와 함께 일하라.
  79. 정책은 메타데이터다.
  80. 프로젝트 용어 사전을 사용하라.
  81. 생각의 틀을 벗어나지 말고, 틀을 찾아라.
  82. 코드에 혼자 들어가지 말라.
  83. 애자일은 명사가 아니다. 애자일은 무언가를 하는 방식이다.
  84. 작고 안정적인 팀을 유지하라.
  85. 실현하려면 계획하라.
  86. 모든 기능을 갖춘 팀을 조직하라.
  87. 유행하는 것이 아니라 실제로 잘 맞는 것을 사용하라.
  88. 사용자에게 필요할 때 제공하라.
  89. 버전 관리 시스템으로 빌드, 테스트, 릴리스를 운용하라.
  90. 일찍 테스트하고, 자주 테스트하라. 자동으로 테스트하라.
  91. 모든 테스트가 끝날 때까지는 코딩이 끝난 게 아니다.
  92. 버그를 심어 놓고 테스트를 테스트하라.
  93. 코드 커버리지만 올리지 말고 상태 조합을 테스트하라.
  94. 버그는 한 번만 잡아라.
  95. 수작업 절차를 사용하지 말라.
  96. 사용자를 기쁘게 하라. 그저 코드만 내놓지 말라.
  97. 자신의 작품에 서명하라.
  98. 먼저, 해를 끼치지 말라.
  99. 쓰레기 가은 인간을 돕지 마라
  100. 결국 당신의 삶이다.삶을 사람들과 나누고, 삶을 축하하고, 삶을 만들어가라. 그리고 그걸 즐겨라

 

'' 카테고리의 다른 글

자몽살구클럽  (3) 2025.08.08

+ Recent posts