Now Loading ...
-
docker와 k8s
개요
백엔드 시스템을 gRPC 기반의 MSA로 구성을 완료하고, 각 코드 베이스가 분리되었으므로 이를 관리하기 위한 수단으로 docker-compose와 k8s 둘을 비교하고 선택하기 위한 인사이트를 공부한 포스트이다.
Docker
Docker란?
운영체제는 하드웨어를 직접적으로 다루는 커널과, 그 외의 부분으로 구성되어있다. 일반적인 소프트웨어는 이러한 운영체제를 통해 간접적으로 하드웨어를 동작하게 만들 수 있는데, 도커 엔진은 이러한 운영체제 그 중에서도 Linux 운영체제를 기반으로 동작한다.
Linux 운영체제의 커널 외의 부분을 컨테이너 안에 같이 구성하고, 도커 엔진이 Linux 커널과 컨테이너를 연결해준다. 이렇듯 운영체제 전체를 가상화하는 VirtualBox 등과 다르게 커널 외의 부분만 컨테이너 안에 넣어 같은 컴퓨터 안의 독립된 격리환경을 만들기에 상대적으로 가볍다. 또한 Linux 운영체제를 기반으로 환경을 구성하기에 윈도우 등에서 도커를 사용할 때 wsl이나 Hyper-V 등이 설치되야 하는 것이다.
Docker의 구성
Docker Image와 Container
도커는 기본적으로 이미지라는 읽기 전용 파일을 통해 컨테이너를 생성한다. 그렇다면 이 이미지라는 것이 정확히 어떤 역할을 수행해주는 것일까?
우리가 DockerFile 등의 Build를 정의하는 파일들을 보면 RUN, FROM, COPY, ADD 등 다양한 명령들로 어떤 파일을 복사해올지, 어떤 걸 설치할지 등을 정의할 수 있다. 이러한 명령이 하나 실행될 때마다 도커는 Layer라는 각 명령이 실행된 후의 파일 시스템의 스냅샷을 저장한다.
이렇게 명령어 별로 스냅샷인 Layer를 기록하기에 같은 명령어 구조를 공유하는 이미지가 많으면 이미지를 저장하는 용량을 줄일 수 있는 소소한 팁이 있다. 도커 이미지에서 RUN 명령에 pip를 엄청 뭉쳐서 실행하는 경우가 있는데, RUN을 각각 따로 실행해버리면 Layer 저장량이 많아지기에 한꺼번에 설치를 해버리는 것이다. 즉 중복된 Layer의 용량을 절약할 수 있는 점과, Layer가 많아지면 저장공간이 더 필요한 점을 각각 따져서 도커파일을 구성하는 것이 좋다.
즉 이런 구성으로 컨테이너 : 우리가 실제 도커를 실행하는 시스템
이 시작되는 시점의 스냅샷이 이미지의 역할이다.
그런데 이러한 Layer는 이미지에만 존재하지 않는다. 우리가 실제 격리된 환경을 구현하는 Container에서 Layer가 존재한다. 물론 차이는 존재한다. Docker Image의 Layer는 읽기전용으로 한번 빌드가 완료된 후 수정되지 않는다. 하지만 Container별로 존재하는 Layer는 writable Layer를 각각 갖는다. 스냅샷 이후의 수정사항을 새로운 Layer로 저장하는 것이다.
여기서 어떤 강점이 보일까? 사전에 파일을 복사하고, 다른 프로그램을 설치하고 빌드라는 과정의 결과인 스냅샷을 복사할 필요 없이 모든 컨테이너가 하나의 이미지로 공유가 가능한 점이다.
Pytorch 등 무거운 라이브러리를 사용하면 이미지의 용량이 10GB가 넘어가기도 하는데, 이런 이미지를 사용하는 컨테이너를 n개 띄워도 하나의 이미지로 공유하여 사용이 가능한 것이다.
우리가 흔히 이미지를 틀러 컨테이너를 찍어낸다라는 비유가 있는데, 사전에 공유되는 스냅샷을 만드는 과정과 저장한 결과를 모두 컨테이너가 공유하여 자기 자신들의 수정사항을 반영하는 Writiable Layer로 가볍게 찍어낼 수 있기 때문에 이러한 비유가 있는 것이다.
이러한 기능을 구현하는 근간은 Linux의 UnionFS(Union File System)이다.
또한 도커는 만들어진 container를 기반으로 이미지를, 이미지를 컨테이너로, 컨테이너 재실행, 허브에 올리기 등 이미지와 컨테이너를 기반으로 다양한 명령을 가지는데 구조는 아래와 같다.
다만 도커 허브로 이미지를 가져올 때, 베이스 이미지의 코드를 수정하여 공격하는 기법도 존재하므로, 항상 베이스 이미지를 신뢰할 수 있는 사용자에 이미지를 사용하도록 하자.
Docker-Compose
도커 컨테이너를 생성할 때, 볼륨, 네트워크, 호스트 자원할당, 환경변수 설정 등 다양한 설정을 하나의 컨테이너를 생성하고 실행할 때 사용할 수 있다. 이러한 컨테이너를 일일히 명령어로 적는 것은 힘들기 때문에 이를 한꺼번에 yaml 파일로 정리해서 실행할 수 있게 만든 것이 docker compose 이다.
Docker-Compose 기능을 활용하여 얻을 수 있는 이점은 아래와 같다.
컨테이너 단위로 실행하는 것이 아닌, 여러 개의 컨테이너가 모인 Application 단위로 정의 및 관리가 가능하다.
서비스간 자동으로 네트워크를 연결해주고, 서비스를 실행할 때 depends_on 옵션으로 실행 순서(의존관계)의 정의도 가능하다.
docker-compose.dev , docker-compose.prod 등으로 설정파일을 여러개를 만들어 실행 가능하다.
동일한 서비스의 컨테이너를 여러 개 띄울 수 있다.
Compose의 설정 파일 정리
주 항목
명령어 이름
설명
services
컨테이너를 정의한다
networks
도커 네트워크를 정의한다
volumes
볼륨을 정의한다
secrets
각 비밀이 보장되어야하는 정보들이 기록된 파일을 이름을 붙여서 저장할 수 있게 한다
서비스 정의 항목
명령어 이름
설명
구체적인 옵션
CLI 대체 여부(docker run 기준)
image
해당 서비스가 어떠한 이미지를 사용할지를 결정
이미지 이름:이미지 버전
CLI에 고정으로 사용
networks
해당 서비스가 어떠한 도커 네트워크에 속할지를 결정
x
–net
build
해당 서비스가 어떠한 Dockerfile을 빌드하여 사용할지를 결정
context : 해당 도커 빌드 시작 폴더 위치를 지정, dockerfile : 어떤 도커파일로 빌드할지를 결정
x
volumes
스토리지 마운트를 결정
여러개 지정 가능
-v, –mount
ports
호스트와 연결될 포트를 지정
호스트 포트번호:컨테이너 포트번호
-p
environment
환경변수 설정
여러개 지정 가능
-e
depends on
다른 서비스에 대한 의존 관계 설정(먼저 실행되야 하는 서비스)
여러개 지정 가능
없음
restart
컨테이너 종료 시 재시작 여부를 설정
1. no (재시작 x) 2.always (항상 재시작) 3. on-failure (프로세스가 0 외의 상태로 종료될 시 재시작) 4. unless-stopped (종료시 재시작 x, 그 외엔 재시작)
없음
command
커맨드 시작 시 기존 커맨드를 오버라이드
실행될 명령
CLI에 고정으로 사용
container_name
실행될 컨테이너의 이름을 지정
x
–name
dns
DNS 서버를 명시적으로 지정
x
–dns
env_file
환경설정 정보를 기재한 파일을 로드
x
없음
entrypoint
컨테이너 시작 시 ENTRYPOINT 설정을 오버라이드
x
–entrypoint
links
네트워크 없이 컨테이너를 다른 컨테이너와 연결되어 사용하게 해줌, etc/hosts/ 에서 각 컨테이너의 ip를 확인 가능
여러개 지정 가능
x
external_links
컴포즈 외부의 컨테이너와 연결되어 사용하게 해줌
여러개 지정 가능
–link
extra_hosts
컨테이너 내의 /etc/hosts/에 외부 호스트 정보를 추가, DNS를 지정할 때, 특정 IP를 도메인으로 매핑할 떄 사용
“host.docker.internal:host-gateway” 를 지정해야 외부 DNS 서버를 통해 통신 가능하다, 여러개 지정 가능
–add-host
secret
사전에 구성된 secret에 서비스가 접근하도록 한다, /run/secrets 디렉토리에서 secret 이름으로 접근
여러개 지정 가능
x
logging
컨테이너의 로그가 어떻게 저장될지 옵션을 설정한다
1. json-file : 컨테이너 내부에서 자체적으로 저장 2. syslog : 각 서비스의 로그를 통합해서 관리하는 서버로 전송 3. none : 로그 저장 안함
x
kubernates
k8s란?
쿠버네티스란 오케스트레이션 도구의 일종으로, 오케스트레이션 도구는 여러 개의 호스트를 가지는 서버를 관리할 떄 쓰이는 도구이다.
Docker compose와 같은 도구는 여러개의 컨테이너를 한꺼번에 띄울 수는 있지만, 만약 물리적인 서버가 여러대라면 이 작업을 반복해서 수행하고, 여러개의 물리적 서버를 통합 관리할 수는 없다. 이러한 상황에 도움을 주는 도구가 쿠버네티스와 같은 오케스트레이션 도구이다.
k8s의 구성
1. 노드
마스터 노드 : 컨테이너를 직접 실행시키진 않지만 각 워커 노드를 관리하여 이상적인 상태(컨테이너 개수, 볼륨 개수)를 유지하도록 관리하는 주체
워커 노드 : 실제 컨테이너가 실행되는 주체, 도커 등의 엔진 프로그램이 필요하다.
2. 관리 단위
포드(pod) : 컨테이너 하나 혹은 여러 개로 이루어진 같은 로컬 네트워크 ip, 포트 번호, 생명주기를 공유하는 쿠버네티스의 최소 관리 단위이다.
docker compose 등을 보면 컨테이너끼리 link 등을 통해 묶여있는데 실제로 어플리케이션에서는 하나의 컨테이너만 사용되는게 아닌,
여러개의 컨테이너가 모여 하나의 서비스를 구성한다. nginx, 로깅, metric 컨테이너 등은 실제 핵심 로직 컨테이너와 함께 움직인다.
이러한 밀접하게 연결된 컨테이너들을 하나의 관리 대상으로 만들어 줄 수 있는게 pod라는 단위이다.
서비스(service) : 같은 구성을 가지는 pod들을 여러개 모은 단위를 서비스라고 한다.
여러 개의 pod가 여러 개의 워커 노드(물리적 서버)에 존재하더라도 한꺼번에 관리해주는 단위이다.
서비스는 고정적인 ip 주소(cluster ip)를 부여받으며 같은 구성의 pod에 로드밸런서 역할을 수행해준다.
하지만 서비스가 분배하는 통신은 한 워커 노드 안으로 국한되며, 워커 노드간의 분배는 실제 로드밸런서 혹은 인그레스(ingress)가 담당한다.
이들은 마스터 노드도 워커 노드도 아닌 별도의 노드에서 동작한다.
디플로이먼트(deployment)와 레플리카세트(replicaset) : pod의 수를 관리하는 주체이다.
정의파일에 의해 정의된 pod의 수에 따라 실제 pod의 수를 줄이고 늘린다.
이런 ReplicaSet의 관리하는 동일한 구성의 pod를 Replica라고 부른다.
Deployment는 Pod의 Deploy를 관리하는 요소로, Pod가 사용하는 이미지 등의 정보를 가지고 있다. ReplicaSet은 이러한 Deployment와 함께 사용된다.
결론
쿠버네티스같은 오케스트레이션 시스템은 기본적으로 서버가 여러개가 필요할 정도로 대규모 트래픽이 필요한 서비스에서 제 역할을 할 수 있는 시스템이다.
현재 우리의 백엔드 서버는 하나의 물리적 서버를 가지고 있으며, MSA로 나눠진 AI 컨테이너 등의 로깅 수집, 자동 재시작 등의 필요한 기능은 Docker Compose 내부의
설정만으로도 어느정도 대체가 가능하기에, 현재 시스템에는 k8s 채택을 미루기로 결정하였다.
참고자료
1. 도커 컴포즈 정리 블로그 글
2. 그림과 실습으로 배우는 도커와 쿠버네티스
3. 도커 secret에 대해
4. 도커 logging 옵션에 대하여
5. 도커 syslog 서버 구축
-
nginx의 개요
개요
프론트 엔드 뷰어와, 백엔드 서버를 결합하는 작업을 수행하면서 CORS 오류로 인한 통신후 Body를 못보는 문제, 분명히 통신과 쿠키는 생성되었는데 쿠키를 읽어드리지 못하는 문제 등, 통신 자체는 성공했는데 웹의 자체적인 보안 및 시스템으로 인해 버그가 자주 발생하였다. 웹에 대한 지식이 없이 이런 버그를 고치는 것은 시행착오가 많을 것 같아, cs-note 섹션에 해당 정보들을 정리하려 한다.
웹에 대하여
초창기 웹은 단순히 URI(Uniform Resource Identifier) 을 통해 클라이언트에 리소스를 보내주고, HTML로 규정된 문서 규칙을 통해 문서끼리, 다른 문서를 쉽게 링크를 통해 가져올 수 있는 구조였다. 하지만 웹서버가 발전하며, 웹 서버는 기존의 서버에서, 클라이언트로 HTML 문서를 보내주는 것을 넘어서, 동적으로 움직이고 디자인이 가능한 문서의 송수신, 자원의 송수신을 넘어선 로직의 실행, 클라이언트와 서버 간의 상태의 저장 등 더욱 다양한 역할을 수행해주게 발전되었다.
해당 문서는 이러한 웹에 발전에 따라, 웹에서 구동되는 제품이 구현되기 위한 백엔드의 구성 요소와 도움이 되는 방법론 등을 정리하는 문서이다.
어떻게 통신할 것인가?
TODO Resource의 송수신
초창기 웹의 주요 역할은 서버에 저장된 리소스(HTML 문서, 이미지·동영상·오디오 같은 미디어 파일, 데이터 파일 등)를 URI를 통해 탐색하고, 이를 클라이언트로 전달하는 것이었다. 이후 웹은 단순한 파일 전송과 링크 연결을 넘어, HTML로 문서를 표현하고, CSS로 시각적 디자인을 더하며, JavaScript로 동적 기능을 구현하는 방향으로 발전하였다. 최근에는 단순한 정적 파일 전송을 넘어, XML·JSON과 같은 데이터 포맷을 송수신하여 브라우저가 직접 HTML을 생성하거나 동적 데이터 처리를 수행한다. 이러한 내용을 정리해보자.
정적 리소스
HTML (문서 구조)
하이퍼링크(URI 기반 파일 연결)
이미지·미디어 파일 전송
문서의 표현력 강화 & 동적 요소
CSS (스타일·디자인)
JavaScript (동적 상호작용)
현대 웹 (데이터 송수신 중심)
XML, JSON (데이터 교환 포맷)
AJAX (비동기 데이터 요청)
TODO 표준화된 데이터 송수신 방식
웹이 단순한 문서 전송을 넘어 다양한 데이터 교환을 필요로 하게 되면서, 서버와 클라이언트 간의 데이터 송수신을 표준화하는 프로토콜이 등장하였다. 이러한 프로토콜은 웹 서비스가 확장될수록 일관성 있는 데이터 접근과 통신 효율성을 보장하는 핵심 역할을 한다.
다룰 내용 예시 : RestAPI, GraphQL, gRPC
클라이언트와 서버의 상태관리
웹이 발전하면서 로그인, 장바구니 같은 기능을 제공하기 위해 서버와 클라이언트는 서로의 상태를 유지할 필요가 생겼다. 이를 위해 상태 관리와 인증 방식이 사용되며, 만약 인증 정보가 유출되면 다른 사용자가 이를 도용해 사칭할 수 있다. 따라서 안전한 인증과 보안 기능이 필수적이다.
인증(Authentication) 대표 기술
세션(Session) + 쿠키(Cookie)
토큰 기반 인증(JWT, OAuth2)
다중 인증(MFA, OTP)
보안(Security) 대표 기술
HTTPS/TLS(데이터 암호화 전송)
CSRF/XSS 방어 기법
세션 하이재킹 방지(만료시간, HttpOnly, Secure 옵션 등)
어떻게 배포환경을 파악할 것인가?
어떻게 WAS를 관리할 것인가?
데이터베이스 최적화
##
참고자료
용어설명
URI
URI(Uniform Resource Identifier)란 인터넷에 있는 자원을 어디에 있는지 자원 자체를 식별하는 방법이다. 우리가 어떠한 자원을 식별할 때는 그 자원이 어디에 있는가? 혹은 그 자원을 뭐라고 하는가? 2가지 방법을 통해 식별을 할 수 있다. 이들이 각각 URL(Uniform Resource Locator)와 URN(Uniform Resource Name)이다.
예시 URI : https://example.com:8080/articles/index.html?search=chatgpt#intro
Type
Context
Schema
https://
Host
example.com
Port
8080
Path
/articles/index.html
Query
?search=chatgpt
Fragment
#intro
우리는 Host + Port + Path를 통해 어떠한 자원이 어느 서버의 어느 위치에 있는지를 알아낼 수 있으며, 이런게 URL이다. 이런 자원이 만약 어디에서 접근하든 고유한 이름으로 구분 가능하면 URN이다. URI는 이 모든 개념을 포함하며, Fragment처럼 자원의 위치만이 아닌 해당 자원 내부를 가르키는 특정 지점에 대한 정보까지도 URI는 포함할 수 있다.
참고링크
웹 개발자가 봐야할 하나의 지도 강의
-
Git의 object와 refs 저장방식
Git의 저장방식
git으로 프로젝트를 진행하다보면, .git이라는 숨김폴더가 생성되는 것을 볼 수 있다. 이러한 .git 폴더 안에는 프로젝트 작업을 수행하면서 commit add 등 명령어를 통해 수행한 작업들의 결과들이 생성되는데 이번 포스트에서 해당 정보에 대한 저장방식, 그 중에서도 git의 objects와 refs에 저장되는 정보에 대해 알아보자.
Git objects
위의 그림에서 git이 실제로 어떻게 저장되느냐에 대한 설명이 잘 나와있다.
우리가 working directory의 작업을 commit을 수행하면,
해당 파일들과 디렉토리들은 Blob과 Tree라는 구조로 .git/objects에 저장된다.
commit 객체가 생성되어 해당하는 커밋에 프로젝트 폴더를 가르키는 Tree 객체를 가르킨다. 또한 해당 커밋 이전의 부모 커밋 또한 가르킨다.
commit object를 Head 포인터가 가르킨다.
자 전체적인 구조는 위와 같이 수행되지만, 하나하나가 잘 이해되지 않는다. 그러니 자세히 파악해보자.
git objects 파일을 자세히 볼 땐
git cat-file -p 파일의 hash코드
명령을 통해 해당 오브젝트 파일을 자세히 살펴볼 수 있다.
이 글을 포스트하는 github 블로그 역시도 git으로 관리되고 있다. 그러면 한번 예시를 살펴봐볼까?
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ ls
0a/ 1b/ 34/ 4c/ 67/ 7e/ 8f/ a2/ b5/ c1/ ca/ e8/ fb/
0f/ 24/ 36/ 50/ 68/ 80/ 92/ a9/ b8/ c2/ d2/ ea/ fc/
13/ 25/ 38/ 51/ 76/ 85/ 9a/ ae/ ba/ c4/ d3/ ee/ fd/
19/ 2f/ 3e/ 60/ 7a/ 86/ 9e/ b0/ bc/ c5/ d5/ f3/ info/
1a/ 30/ 44/ 62/ 7d/ 8b/ 9f/ b2/ bd/ c7/ db/ f9/ pack/
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ ls 1b/
2948a17f027fd4f4359c53a60b9582a3b1c265
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git cat-file -p 1b2948a17f027fd4f4359c53a60b9582a3b1c265
040000 tree 7a6a6a64b2bbae6b6cec73624b610e31830b56ac Projects
040000 tree 51fb9a02110fa88741738be230342544256e1f73 Tutorial
040000 tree 7a5657ae0999f622f6509b8416f659a0262eba33 cs-note
040000 tree c472bd9693f8ca55e7590d9ace65ee51b6f99179 dev-log
100644 blob a49ba48448f906d814cc83e50fc18f81cae53844 index.md
040000 tree b56fcfa45f3a0afaf2c480470f2b38f1b44fc26d tech-review
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git cat-file -p a49ba48448f906d814cc83e50fc18f81cae53844
---
---
자 이러한 결과가 나왔다. 그러면 이 결과를 한번 자세히 분석해보자!
Git Blob
먼저 git이 파일 버전 관리를 할 때 핵심이 되는 저장방식인 Blob(Binary Large Object)에 대해서 알아보자.
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git cat-file -p a49ba48448f906d814cc83e50fc18f81cae53844
---
---
자 Blob파일을 우리가 열어보았을 때 해당 파일인 index.md의 실제 저장된 값과 정확히 일치하는 결과를 확인할 수 있다.
그렇다면 파일이 수정되었을 때 Blob은 어떻게 저장할까?
# Blob 1
첫 번째 파일
# Blob 2
첫 번째 파일
두 번째 파일
파일이 위와 같이 수정되었을 때 Blob은 각 파일의 전체 내용을 기반으로 Hash함수를 통과시켜 40글자에 해당하는 문자열을 파일이름으로 사용한다. 이때 앞선 2글자는 폴더로, 뒤의 38글자는 파일이름으로 사용된다.
처음 objects의 폴더에 나온 수많은 2글자들은 이런 식으로 생성된 hash의 결과값들이다. git에서 각 오브젝트들은 이러한 hash값을 기반으로 접근할 수 있다.
각 파일이 조금의 수정이라도 일어난다면 git은 새로운 Blob파일로 저장하며, 수정이 일어나지 않았을 시 동일한 Blob파일로 저장된다.
이러한 구조의 장점은 무엇일까?
바로 hash함수의 특징인 같은 값을 통과시킬 땐 같은 결과가 나온다는 것이 핵심이다.
git은 버전관리 프로그램이다. 커밋을 수행할 때마다 파일의 버전을 기록해야하고 변화를 추적할 수 있어야한다. 하지만 우리가 git을 사용할 때 항상 모든 파일들이 커밋을 수행할 때마다 바뀌는가?
아니다. 대부분의 파일들은 이전버전과 변하지 않는 경우가 많으며 수십 번의 커밋 후에도 바뀌지 않을 수도 있다. 이때 Blob의 저장방식이 강점을 발휘한다. Blob은 파일 내용을 기반으로 hash를 생성해 파일이름을 생성한다. 즉 어떤 컴퓨터에서든 어떤 파일로 저장되었든 어느 시점에 저장하든, 파일 내용이 동일하다면 동일한 Blob으로 저장된다.
이러한 방식은 git이 버전관리를 수행하면서도 저장 공간을 효율적으로 사용하게 해준다. 각 커밋을 수행할 때 시점별로 commit 오브젝트는 tree오브젝트를 가르키고 각 tree 오브젝트는 Blob을 가르키는데, 이때 동일한 내용은 여러 커밋 시점에서 하나의 Blob만을 가르키게 저장할 수 있다.
여기서 또 하나의 용량을 감소하는 트릭이 존재한다.
git 저장구조, wikipedia
Blob 파일의 생성 원리를 보면 파일 전체를 스냅샷처럼 저장해 전체 파일 내용을 기반으로 hash를 생성한다. 하지만 이런 말을 들어보았을 것이다. git은 파일의 수정된 내역만 저장하여 효율적이게 디스크 공간을 활용한다.
실제로 git은 델타압축이라는 파일의 수정된 내역만을 저장하여 공간을 절약시켜, 각각의 Blob을 이전 버전에서의 수정 내역만을 기반으로 생성시키고 checkout 등으로 특정 버전을 불러와야할 때 이러한 수정 내역을 일괄적으로 읽어 해당 버전을 복구한다. 그렇기에 전체 내용을 저장하는 것도 수정된 내역만 저장하는 것도 맞는 말이다.
Git Tree
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ ls 1b/
2948a17f027fd4f4359c53a60b9582a3b1c265
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git cat-file -p 1b2948a17f027fd4f4359c53a60b9582a3b1c265
040000 tree 7a6a6a64b2bbae6b6cec73624b610e31830b56ac Projects
040000 tree 51fb9a02110fa88741738be230342544256e1f73 Tutorial
040000 tree 7a5657ae0999f622f6509b8416f659a0262eba33 cs-note
040000 tree c472bd9693f8ca55e7590d9ace65ee51b6f99179 dev-log
100644 blob a49ba48448f906d814cc83e50fc18f81cae53844 index.md
040000 tree b56fcfa45f3a0afaf2c480470f2b38f1b44fc26d tech-review
이번엔 Tree 오브젝트를 살펴보자, 위의 그림을 보듯이 tree object는 자신이 가지고 있는 Tree와 Blob을 가르키는 오브젝트이다.
트리는 각 ‘디렉토리’별로 ‘파일 이름’을 저장한다.
현재 블로그에서 디렉토리를 담당하는 cs-note, dev-log 등은 tree로 저장되고ㅡ, index.md같은 실제 파일은 Blob으로 저장된 모습을 볼 수 있다.
또한 이러한 Tree 역시도 Blob과 마찬가지고 hash코드로 파일명이 지정된 것을 볼 수 있다.
Blob과 달리 Tree는 디렉토리 전체를 기반으로 hash값을 생성하기에, 포함된 일부 파일이 하나만 수정되더라도 Tree 역시도 새로운 Tree 객체로 생성된다.
하지만 이를 통해 git은 파일의 내용만 저장하는 Blob과 달리 파일의 이름, 실행권한, 디렉토리 구조 등을 Tree 객체를 통해 저장할 수 있다.
여기서 .git/index 파일을 한번 살펴보자.
$ git ls-files --stage | tail
100644 d344d060ac0c5db0f9bb01c4f1ce7e0d156598b9 0 search.html
100644 f259c5a08dd5d121a34a000017cd197ea02dc90b 0 sitemap.xml
100755 764df0355bdab53e2362b2f821e6c649162694d5 0 start.sh
100644 c9712bd9af8e82846391e82f7c50077b095f87fc 0 tag.html
100755 bdfb10641d93f265a382b3014341a15c68e4b139 0 tool/find-orphan-post-img.sh
100755 537c62c2bb5465d7594f085f8cf4935cf07ac4c4 0 tool/fix-image-references.sh
100755 01433e92888c8ce58bb79792ef786aa58d1acfc9 0 tool/pre-commit
100755 95b19d9863b6ad564bf6208f719a2bcc657a9ffc 0 tool/save-images.sh
100755 e36c4a696d2e351dc0efcd40db81d87e7ef1fb11 0 tool/to-skeleton.sh
100644 50fe65a76cb17611bb041bd5d2cc517ec863323f 0 utterances.json
git index 자료 출처
해당 파일의 컬럼은 각각 staging area로 옮겨진 파일들에 대한 나열을 수행하며, 각 staged 파일들에 대해
[mode] : 파일의 타입과 권한을 의미
[object] : 해당 파일을 나타내는 .git 내부 hash값
[stage] : 기본값 0 충돌이 났을 때 충돌난 각 파일 그룹들을 구분해주는 키
[file] : 실제 파일 경로
를 의미한다.
tree는 확실하게 commit된 디렉토리(Tree)와 Blob들을 기록하지만, Staging 공간은 오로지 add된 파일들을 Blob으로 생성하고 이들을 나열하는 차이를 확인할 수 있다.
하지만 만약 현재 staing 공간에 존재하는 파일들을 tree로 만들고 싶다면, git write-tree 명령을 통해 Tree객체를 생성해주고, 이에 대한 hash 값을 커맨드에 출력해준다.
Git Commit
프로젝트의 디렉토리에 대한 정보와 파일 내용에 대한 정보를 Tree와 Blob을 통해 저장하였다.
하지만 이는 한 시점에 프로젝트의 상태를 기록하는 방법론일 뿐이다.
git은 우리가 commit을 수행할 때마다 해당 시점에 누가 기록했는지, 이 이전 시점의 기록은 무엇인지 추적이 가능해야한다. 이러한 역할을 수행해주는 것이 commit 객체이다.
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git log
commit f342a499d9ea17b2956f9bea91c33354d1870984 (HEAD -> main, origin/main, origin/HEAD)
Author: SeongWooJo <abc8325767@gmail.com>
Date: Wed Aug 13 22:35:28 2025 +0900
search
commit 1a733fefb50c137ab2b95285b82f084dc60193aa
Author: SeongWooJo <abc8325767@gmail.com>
Date: Wed Aug 13 22:29:42 2025 +0900
Update _config.yml
commit 80646a438cff8f50884f6bf2945b2f4e15887ff4
Author: SeongWooJo <abc8325767@gmail.com>
Date: Wed Aug 13 22:23:09 2025 +0900
.
commit 19781c03fd83f4c185f9ab5051c8bba72983bace
Author: SeongWooJo <abc8325767@gmail.com>
Date: Wed Aug 13 22:22:41 2025 +0900
우리가 git log 명령을 수행하면 위와 같이 hash값과 함께 각 커밋에 대한 정보를 나열해주는 것을 볼 수 있다. 어떻게 이런 작업이 가능한 것일까?
아까처럼 hash 코드를 기반으로 파일을 열어보자.
abc83@develop MINGW64 ~/development/SeongWooJo.github.io/.git/objects (GIT_DIR!)
$ git cat-file -p 19781c03fd83f4c185f9ab5051c8bba72983bace
tree ba3c9fe46ea8e11d47e739b69c347cf1d5efd75e
parent dba0935cb5910000496ffac60c49ba2534de4001
author SeongWooJo <abc8325767@gmail.com> 1755091361 +0900
committer SeongWooJo <abc8325767@gmail.com> 1755091361 +0900
robot
해당 커밋은 git blog를 배포하기 위해 robot.txt를 시험하던 시점의 커밋이다. commit 메세지로 robot으로 간단하게 적었다.
이 객체를 분석해보자.
tree : 해당 커밋 시점의 프로젝트 전체를 가르키는 tree 객체의 hash값이다.
parent : 해당 커밋의 이전 커밋 객체를 가르키는 hash값이다.
author : 해당 커밋 시점의 코드를 작성한 사람에 대한 정보이다.
committer : 해당 커밋을 작성한 사람에 대한 정보이다.
“robot” 해당 커밋을 작성할 때 메세지이다.
이처럼 버전관리를 위해 필요한 추적을 위한 정보들이 커밋 객체에 담긴 것을 확인할 수 있으며, 커밋 객체역시도 tree,blob과 마찬가지로 내용을 기반으로 hash값을 폴더/파일명으로 삼는다.
Git Tag
자 hash를 기반으로 Blob, Tree, Commit의 접근하는 것은 저장도 효율적이고 버전 관리도 될 수 있는 방법이지만, 40글자에 해당하는 hash 코드는 사람이 구분하기에 어렵다.
이를 해결해주기 위한 객체가 바로 Tag 객체이다.
git tag [옵션] <tagname> [<commit>]
git tag -a v1.0 -m "First release"
git tag -s v1.0 -m "Signed release"
git tag v1.1 <commit_hash>
우리는 위와 같은 명령을 통해 각 hash 코드에 사람이 접근하기 좋은 이름을 붙여줄 수 있다.
이를 기반으로 기존에는 git checkout hash코드 처럼 40글자의 hash코드를 사용해야하는 명령에서, git checkout 태그명으로 미리 지정한 tagname을 기반으로 hash코드를 대체할 수 있다.
다만 위의 예시처럼 우리가 어떻게 태그를 생성했느냐에 따라 구현방식이 조금씩 다르다.
-a 옵션
$ git cat-file -p c3d5f2a
object 7a9b74c6b6f4d4c85b8e4cf59ef1fa3e63c5c3ad
type commit
tag v1.0
tagger Alice <alice@example.com> 1713250000 +0900
First release
-a 옵션을 사용했을 때는 해당 hash 파일에 대한 태그를 생성할 때 누가 태그를 붙였는지, 해당 태그를 붙일 떄 시점 및 메세지를 같이 남긴다.
-s 옵션
object 7a9b74c6b6f4d4c85b8e4cf59ef1fa3e63c5c3ad
type commit
tag v1.0
tagger Alice <alice@example.com> 1713250000 +0900
Signed release
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2
iQEzBAABCAAdFiEE...
-----END PGP SIGNATURE-----
-s 옵션은 -a 옵션에 더해 해당 tagger를 검증할 수 있는 키를 남겨, public key를 통해 검증할 수 있게 한다.
그냥 만들시
이때는 tag 객체가 생성되는 것이 아닌, refs/tags에 해당 태그가 가르키는 hash코드에 대한 정보만이 한 줄로 기록된다.
git refs
.git/refs 폴더에는 이러한 객체를 가르키는 포인터를 만들 수 있으며 이곳에 저장되는 포인터 정보는
.git/refs/heads
각 브랜치별 최신 작업 커밋을 가르키는 hash값 및 working directory가 작업하고 있는 버전의 commit hash값
.git/refs/tags
사전에 태그로 등록한 commit, tree, blob hash값이 저장되는 장소
.git/refs/remotes
local 저장소가 아닌 remote 저장소에 브랜치들에 대한 최신 commit hash값이 저장되는 장소
와 같이 앞서 공부한 git의 객체들에 대한 hash값을 refs 폴더에서 실제 git에서 작업되는 hash값을 관리해줌으로써 우리가 브랜치명, 원격 브랜치명, 태그명으로 실제 hash값을 사용하지 않고 쉽게 이동할 수 있다.
참고자료
git의 저장방식 영상
git blob, tree, commit
git tags
git에 대한 추후 볼 내용1
-
Git 정리
Git
개요
백엔드 시스템을 개발하면서, 기존의 연구 개발과는 달리 여러 사람이 병렬로 작업을 수행하게 되었다. 이에 따라 Git을 사용하게 되었고, 버전 및 브랜치 관리를 철저히 해야 할 필요성을 절실히 느꼈다. 이에 따라 Git에 대해 정리할 필요성을 느꼈고, 그 첫 번째로 Git의 공간과 파일의 상태에 대해 기초적인 내용을 정리한다.
Git이란?
Git이란 Version Control System의 일종으로, 소프트웨어 개발 및 협업 프로젝트에서 소스 코드, 문서, 기타 파일의 변경 사항을 추적하고 관리하는 데 사용된다.
혼자 작업하든 팀으로 작업하든, 버전 관리를 도입하면 다음과 같은 장점이 있다:
파일과 프로젝트를 이전 상태로 쉽게 되돌릴 수 있다.
누가 언제 어떤 작업을 했는지 추적할 수 있으며, 문제 발생 시 관련 이력을 확인할 수 있다.
파일을 잃어버리거나 실수로 수정했을 때 쉽게 복구할 수 있다.
서로의 작업을 덮어쓰지 않고 병렬로 작업하며, 변경 사항을 손쉽게 병합할 수 있다.
버전 관리 시스템(VCS)은 Git 외에도 다양한 도구들이 존재한다. 예를 들어 GUI 기반의 간편함을 선호하는 개발자들은 Mercurial을 사용하기도 하고, 코드 외의 리소스까지 통합 관리하는 Perforce (P4V) 도 있다. 하지만 현재까지 가장 널리 사용되는 VCS는 단연 Git이다.
Git의 공간
Git은 파일들의 변경 이력을 추적하여 버전을 관리하는 도구다.
그렇다면 파일을 수정할 때마다 Git이 모든 변화를 자동으로 기록하는 것일까?
→ 그렇지 않다.
Git은 우리가 작업하는 디렉토리의 파일 상태를 추적하고, 특정 명령을 실행할 때마다 스냅샷처럼 그 시점의 파일 상태를 기록한다(그러나 저장 방식은 파일을 복제하는 것이 아닌 변화를 기록하는 차이가 존재한다). 이러한 과정에서 Git은 변경된 파일들을 다양한 단계에서 관리한다. Git이 추적하는 파일 상태는 아래와 같은 네 가지 주요 공간을 통해 이해할 수 있다:
1. Working Directory
Git으로 관리하기 위해 git init 등을 실행한 후 작업하게 되는 실제 디렉토리다.
이곳은 개발자가 직접 파일을 수정하거나 저장하는 공간이며, Git이 자동으로 버전을 기록하지 않는다.
즉, 파일을 수정하거나 새로 생성해도 git add 또는 git commit 명령을 실행하지 않으면, Git이 버전 이력에는 반영하지 않는다.
2. Staging Area
Staging Area는 로컬 저장소에 기록(commit) 하기 전에 Git이 변경 내용을 임시로 저장하는 공간이다. 해당 공간이 존재함으로써 Merge 등의 작업을 수행할 때 충돌을 해결하거나, 한번에 커밋에 어떠한 정보들만 업데이트할지 천천히 파일들을 추가할 수 있다. 메모리 등에 임시로 저장되는게 아닌 별도의 공간에 존재하기에 이런 작업들을 오래 수행할 수 있다.
git add 명령을 사용하면, 해당 파일은 Staging Area에 추가된다.
Staging Area를 사용하면 원하는 변경 사항만 선택적으로 커밋할 수 있어, 커밋의 목적과 단위를 깔끔하게 분리할 수 있다.
3. Local Repo
git commit 명령을 실행하면, Staging Area에 있던 변경 내용이 로컬 저장소에 커밋된다. 이 저장소는 .git 폴더 내부에 존재하며, 모든 커밋 이력, 브랜치 정보, 메타데이터 등을 포함한다. Git은 이곳에 실제 파일 내용뿐 아니라, 이전 버전과의 차이점, 커밋 메시지, 작성 시간, 작성자 등의 정보를 구조화하여 저장한다.
구체적인 .git 폴더의 저장 방식에 대해서는 링크를 참조하자 : 저장방법 로직
4. Remote Repo
로컬 저장소의 커밋 내역을 다른 사람과 공유하려면, git push 명령을 사용하여 Remote Repository로 전송한다.
Remote Repository는 GitHub, GitLab, Bitbucket 등의 원격 저장소이며, .git 폴더와 동일한 커밋 이력을 저장하고 관리한다.
원격 저장소를 통해 팀원 간의 협업이 가능해지며, pull, fetch, merge 등을 통해 변경 사항을 주고받을 수 있다.
Git의 파일의 상태
Git은 결국 각 파일이 시간에 따라 어떻게 변화해왔는지를 기록하는 도구이다. 이에 따라 각 공간으로 이동하는 것에 더해 Git은 사용자가 명시적으로 실행하는 명령에 따라 각 파일의 상태(state)를 구분하고 관리한다.
Git에서 파일이 가질 수 있는 상태는 다음과 같다:
Untracked / Tracked → (Unmodified, Modified, Staged)
1. Untracked
Git은 파일의 변경 이력을 추적하고 버전을 관리하지만, 한 번도 Git에 추가되지 않은 파일은 Git 입장에서 “변경”이 아닌 “새로운 파일” 일 뿐이다.
즉, Git이 한 번도 기록한 적이 없는 파일은 과거 상태가 없기 때문에 변경 이력을 추적할 수 없다. 이러한 상태를 Untracked 이다.
실제 Git에선 git 프로젝트 내에서 새로운 파일을 생성시켰을 때, 기존의 추적되던 파일을 삭제하였을 때,
또한, .gitignore에 등록된 파일들도 일부러 추적하지 않기 때문에 Untracked 상태로 간주된다.
이는 예를 들어 다음과 같은 경우 유용하다:
대용량 파일로 인해 Git 저장소에 포함시키고 싶지 않은 경우
민감 정보(예: API 키, 비밀번호)가 포함된 파일을 외부 저장소에 업로드하고 싶지 않은 경우
위의 상태 도식과 달리 Tracked 상태의 파일을 삭제하지 않으면서 Untracked로 만들고 싶을 수 있다. 그런 경우 위처럼 git add, unstaged 같은 명령으론 수행할 수 없고 git rm --cached example.txt 라는 명령처럼 실수로 추적한 파일을 local repo와 staging area에서 없앨 수 있다.
2. Staged
위의 공간 설명에서 Staging Area란 공간은 commit을 수행하기 전 기록할 파일의 상태를 임시로 모아두는 공간이다. 이처럼 해당 공간에 파일들이 옮겨졌을 때 해당 파일들은 Staged 상태를 가지게 된다. 만약 Untracked 파일이 Staged 상태가 된다면 그 때부터 추적이 시작되는 것이고, 다시 unstaged하면 Untracked 상태로 돌아간다.
만약 당신이 파일을 Staged 상태로 바꾸고 워크 디렉토리에서 파일을 수정한다면 어떻게 될까?
Staging Area에는 git add 시점의 파일 상태가 기록되는 것이다. 즉, 워킹 디렉토리의 파일과 Staged에 기록된 파일이 다른 것이다.
워킹 디렉토리에서 해당 파일을 수정하면, Git은 "Staging Area 버전" ≠ "Working Directory 버전" 인 것을 인식한다.
3. Unmodified
Commit을 수행하면, Staging Area의 파일 상태가 Local Repository에 기록된다.
이후 git status 같은 명령이 실행될 때마다 Git은 Local Repo(HEAD) 와 워킹 디렉토리를 비교한다.
두 상태가 완전히 동일하다면 해당 파일은 Unmodified 상태이다.
예를 들어, 파일을 Staged 상태로 변경하고, 추가 수정 없이 commit을 하면
commit 직후 그 파일은 Unmodified 상태로 남는다.
4. Modified
위의 상태처럼 Commit 등을 통해 Staged를 Local Repo로 옮긴 후 추가적으로 워킹 디렉토리에서 파일을 수정했을 때, 해당 파일은 Local Repo와 상태가 다른 Modified 상태가 된다.
Touch background to close