Now Loading ...
-
Medical Viewer & AI Prediction Project
프로젝트 개요
프로젝트 내용
병원의 의료 이미지를 제품에 연결하여, 병변 및 장기에 대한 Segmentation 수행 및 3d 모델을 생성
이를 의료진에게 보여주어 보기 어려운 CT 대신, 시각화를 시킨 환자의 수술 부위를 보여줌으로써 의료진의 수술을 보조하는 제품
현재 제품의 구성
React 기반의 Viewer 역할을 담당하는 FE
FE와 AI WAS를 연결해주는 Web Server
각종 컴퓨터비전 알고리즘, AI 모델, 3D 모델 제작 등의 로직으로 구성된 WAS
팀원 구성 및 담당 역할
FE : 4인, BE : 2인, AI 연구 : 7인
BE 개발 및 컴퓨터 비전 관련 AI 연구 담당
AI 알고리즘을 WAS로 제작(gRPC 기반의 Microservice)
혈관 Segmentation 모델 개발 담당
Client로부터 의료이미지를 전달 -> AI 알고리즘 WAS 실행 -> FE로 결과 반환 관련 기능 개발
파일 시스템에 저장된 의료이미지, PostgreSQL에 저장된 파일 경로, Supabase의 저장된 파일 등의 동기화 및 CRUD
관련 기술스택
BE
InfraStructure Stack : Nginx, Garfana, Loki, Garfana_Alloy, Prometheus, PostgreSQL, Supabase
Container Stack : Docker, Docker Compose
Development Stack : Git, Github
API Stack : Restful API, gRPC
구현 : Python, FastAPI
AI
Computer Vision Algorithm
Segmentation Task
Regression Task
하드웨어 환경 : A100 GPU * 8 Linux 서버, RTX 3090 * 4 Linux 서버 * 2
제품 개발을 위한 인사이트를 정리한 링크
BE
1. FE와의 연결을 위한 브라우저 동작 인사이트
2. 프로젝트 관리를 위한 Git 인사이트
3. 오케스트레이션 적용을 고려하기 위한 인사이트(Docker 및 k8s)
4. nginx를 통한 역프록시 및 https 통신을 위한 인사이트
5. Garfana 스택을 통한 로깅 및 매트릭 수집, 시각화를 위한 인사이트
AI
1. AI 연구 관련 저자 링크
[2. ]
-
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 서버 구축
-
2580
문제개요
문제 이름 : 2580, 스도쿠
백준 스도쿠 바로가기
스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루어진 정사각형 판 위에서 이뤄지는데, 게임 시작 전 일부 칸에는 1부터 9까지의 숫자 중 하나가 쓰여 있다.
나머지 빈 칸을 채우는 방식은 다음과 같다.
각각의 가로줄과 세로줄에는 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.
굵은 선으로 구분되어 있는 3x3 정사각형 안에도 1부터 9까지의 숫자가 한 번씩만 나타나야 한다.
위의 예의 경우, 첫째 줄에는 1을 제외한 나머지 2부터 9까지의 숫자들이 이미 나타나 있으므로 첫째 줄 빈칸에는 1이 들어가야 한다.
또한 위쪽 가운데 위치한 3x3 정사각형의 경우에는 3을 제외한 나머지 숫자들이 이미 쓰여있으므로 가운데 빈 칸에는 3이 들어가야 한다.
이와 같이 빈 칸을 차례로 채워 가면 다음과 같은 최종 결과를 얻을 수 있다.
게임 시작 전 스도쿠 판에 쓰여 있는 숫자들의 정보가 주어질 때 모든 빈 칸이 채워진 최종 모습을 출력하는 프로그램을 작성하시오.
문제 복기
시간 초과
해당 문제는 스도쿠를 해결한 출력을 시간제한 1초 안에 완성해야했다.
코드는 백트래킹을 기본적인 방법으로 작성하였으며,
81개의 스도쿠 칸의 목록을 순회하면서, 3개의 boolean 배열로 열, 행, 칸의 제약 조건을 만족하는지 검사하고, 만족한다면 다음 스도쿠 칸으로 순회, 아니라면 새로운 숫자를 탐색하는 방향으로 작성되었다.
그런데 해당 문제를 해결하는 과정 속에서 시간 초과가 발생하였다.
시간 초과에서 개선한 3가지 방향
조기 리턴을 별도의 변수를 수행하여, 완료가 되었을 때 추가적인 연산을 방지
boolean 배열을 활용하여 각 칸에 넣을 수 있는 숫자의 탐색을 좀 더 간편하게
0(빈칸)의 좌표를 별도의 배열로 저장하여 순회 횟수를 감소
시간복잡도 계산
시간 복잡도를 계산할 때 시간 제한 1초당 약 1억번의 연산횟수를 기준으로 삼는다고 한다.
나의 경우 백트래킹이 있기에 연산이 좀더 복잡해지지만, 대략적으로
모든 칸의 순회 + 한번의 스도쿠 탐색 연산 : (9 * 9 * C)
백트래킹 경우의 수 : 최악의 경우 약 5^81(평균적인 경우의수 ^ (스도쿠 칸의 개수))
그러나 해당 문제는 스도쿠를 작성할 때 이렇게 탐색 횟수가 많은 예시를 주지 않는다는 제약조건이 발생하여, 최악의 탐색횟수를 가정하지 않아도 되었다.
다만 반대로 시간복잡도 계산을 어떻게 수행할지 감이 잡히지 않아, 연산을 최적화하는데 중점을 주었다
추가적으로 개선이 가능해보이는 부분
1. 제약조건 검색을 더 빠르게
기존코드
for(int k = 0; k < 9; k++){
if(this.rows[i][k] || this.cols[j][k] || this.blocks[(i / 3) * 3 + (j / 3)][k]) continue;
개선코드
int candidates = this.rowMask[i] | this.colMask[j] | this.boxMask[(i/3) * 3 + (j / 3)];
candidates = (~candidates) & 0x3FE;
for(int mask = candidates; mask != 0; mask &=(mask - 1)){
int bit = mask & -mask;
int digit = Integer.numberOfTrailingZeros(bit);
지금은 각 제약조건을 boolean 배열로 저장하여 순회하여 검사하는 방식을 채택했지만, 이를 비트 연산으로 변경하면 실제 제약을 만족하는 숫자만 탐색이 가능해
9+9+9 의 탐색을 만족하는 숫자만 탐색으로 줄일 수 있다.
2. 경우의 수가 작은 부분부터 탐색
현재는 그냥 0이 0,0->9,9로 갈 때 순서로 위치가 저장되지만 스도쿠 제약조건에 따라 넣어야하는 숫자가 적은 칸부터 탐색을 수행하면 더 적게 순회를 수행할 수 있다.
기존 결과
개선된 코드를 적용한 결과
구분
적용 내용
실행 시간 (ms)
비고
방법론 1
비트마스크 기반 최적화만 적용, 레거시 코드 남아있음
164 ms
가장 빠른 결과
방법론 1 + 2, 코드 정리
추가 알고리즘 최적화 적용, 레거시 코드 삭제
176 ms
오히려 느려짐
방법론 1, 코드 정리
방법론 2 관련 코드 제거 및 레거시 코드 삭제
180 ms
속도 더 하락
방법론 1 + 2, 레거시 코드 포함
사용하지 않는 메서드 및 변수 잔존
168 ms
다시 속도 향상
176ms 결과(방법론 1+2) 가 가장 효율적인 코드로 보이지만,
실제 실행 결과는 방법론 1만 적용하고, 정리되지 않은 레거시 코드가 남아 있는 버전(164ms) 이 오히려 가장 빠른 속도를 보였다.
GPT의 의견이라 확실하지 않지만, 이는 단순히 알고리즘 복잡도 차이가 아니라, Java JIT(Just-In-Time) 컴파일러의 최적화 방식이
실제 실행 속도에 더 큰 영향을 미친 결과로 보인다.
오히려 사용하지 않는 메서드를 넣음으로써 JAVA 실행기에서 자주 사용되는 함수를 빠르게 호출하는 것이 중요하다고 인식하게 만들어, 컴파일되게 만듬으로써 속도가 빨라지는 것 같다는 의견을 남겼다.
따라서 정리되지 않은 코드를 다시 넣어서 최종테스트를 수행해본 결과 168ms의 결과가 나왔다.
레거시 코드가 남은 상황이 오히려 속도를 빠르게 만드는 것이 신기한 상황이었다.
레거시 코드가 없다면 1+2를 동시에 사용하는 것이, 레거시 코드가 있다면 2가 있는 쪽이 오히려 느려진다. 다만 가장 최적의 상황은 레거시 코드가 있는 상황에서 빠르게 연산되는 메소드가 있을 때 오히려 Sorting을 하는 과정이 더 디메리트를 줬다.
더 빠른 연산 순서를 위해 둔 정렬 코드가 이번과 같은 빠른 탐색의 제한이 사전에 주어진 케이스에선 디메리트로 작용할 수 있다는 것
알고리즘만이 아닌 Java 코드 실행기에 의해 코드가 빨라질 수 있다는 것
2가지 신기한 사실을 볼 수 있는 문제였다.
-
Cookie
쿠키 개요
쿠키는 서버가 클라이언트에게 특정 정보를 저장하도록 전달하는 수단으로, 서버가 HTTP 응답에 쿠키를 포함하면 이후 클라이언트는 서버와의 요청마다 해당 쿠키를 함께 전송한다.
이 용어는 원래 유닉스·네트워크 시스템에서 프로세스 간 통신 시 신원 식별이나 권한 확인을 위해 주고받던 작은 데이터 조각인 매직 쿠키(Magic Cookie) 개념에서 비롯되었다. 이러한 아이디어를 웹에 적용하면서, HTTP 통신에서도 클라이언트와 서버가 교환하는 작은 데이터 조각이라는 공통된 특성을 반영해 쿠키라 불리게 되었다.
실제 사용 예로는 로그인 상태 유지(세션 관리), 장바구니와 같은 사용자 활동 기록, 언어·테마 설정과 같은 개인화, 그리고 광고나 분석을 위한 사용자 행동 추적 등이 있다.
브라우저 저장소
브라우저가 인증 등에 사용하는 정보들을 저장하기 위한 방법론에는 쿠키, 로컬 스토리지, 세션 스토리지, 웹 스토리지 등이 있다.
각 저장소에 대한 간략한 설명은 아래와 같다.
쿠키
쿠키란 서버가 클라이언트에게 보내는 작은 데이터 파일로, 서버가 쿠키를 브라우저에게 적용을 시키면 그 이후 서버와 클라이언트에 모든 요청과 응답에는 쿠키가 포함되어 전송된다. 이러한 특징 때문에 저장할 수 있는 용량이 작으며 보안의 취약하다는 단점이 있다.
로컬 스토리지
브라우저 자체에 영구적으로 데이터를 저장하고, 브라우저가 종료되더라도 데이터가 유지가 된다.
로컬 스토리지 세팅 예시
세션 스토리지
탭 윈도우 단위로 스토리지가 생성이 되며, 탭 윈도우를 닫을 때 데이터가 삭제되는 특징을 가진다. 페이지 새로고침으론 데이터가 삭제되지 않는다.
세션 스토리지 세팅 예시
로컬이나 세션 스토리지의 경우, CrossSite 공격에 취약하기 때문에 민감한 정보를 저장하면 안된다.(SOP 정책이 있는 이유인 공격에 취약)
또한 JS를 통해 데이터에 접근하며 쿠키와 다르게 항상 붙여져서 서버에 전송되지 않는다.
쿠키의 구성
response.set_cookie(
key="access_token",
value=token.access_token,
httponly=True,
secure=True, # dev use only https
samesite="None",
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
)
위와 같은 예시로 쿠키를 서버가 설정하여 브라우저가 특정 쿠키를 저장하도록 설정할 수 있다.
이때 쿠키에 적용될 수 있는 옵션들은 아래와 같은 의미를 갖는다.
참고링크
1. Key & value
쿠키에 저장되는 정보는 기본적으로 key와 value의 쌍으로 저장되며, 각 쿠키는 브라우저와 통신하는 서버별로 따로 저장된다.
만약 여러 개의 key, value를 저장하고 싶다면 각 key, value별로 옵션을 설정하여 set_cookie를 실행해야한다.
2. Domain
쿠키를 보낼 호스트를 정의하는 옵션이다.
쿠키는 쿠키가 생성된 도메인 별로 종속되어서 저장된다. 예를 들어 example.com에서 생성된 쿠키는 www.example.com에선 사용될 순 있지만, 반대로 www.example.com에서 생성된 쿠키는 example.com에선 사용될 수 없다.
이런 식으로 쿠키가 저장될 도메인을 설정할 옵션은 자신과 관련된 서브 도메인까지만 가능하다.
예를 들어 우리의 주소 dev.medai.im 도메인에서 쿠키를 설정할 땐 해당 옵션을 사용하여 medai.im 도메인에 쿠키를 저장하여 다른 서브 도메인들에서 해당 쿠키를 사용하게 만들 순 있지만, 다르게 ai.im 같은 전혀 다른 도메인에 쿠키를 저장할 순 없다.
3. Expires, max_age : 쿠키의 만료시간과 관련된 옵션
이 두가지 옵션은 쿠키가 유지되는 시간을 정의하는 옵션이다.
MaxAge : 쿠키가 유지되는 시간을 정의하며, 만약 Expires와 동시에 설정된다면 우선되는 옵션이다. 현재 시간에 해당 옵션의 시간이 추가되어 만료시간이 계산된다.
Expires : HTTP Date형식으로 쿠키가 만료되는 날짜와 시간을 정의한다.
만약 2개의 옵션을 모두 설정하지 않으면, 쿠키는 브라우저가 종료되는 시점까지 유지되는데 이를 Session Cookie라고 한다.
4. HttpOnly, Secure : 쿠키의 보안과 관련된 옵션
HttpOnly : 해당 옵션을 활성화할 시, 쿠키를 JavaScript를 통해서 저장된 곳을 접근해 확인하는 것이 불가능해진다. 이 옵션이 true가 되었다면 개발자도구를 통해서 직접 확인하거나, 서버와 클라이언트가 통신 중 헤더에 붙은 쿠키를 직접 확인하는 방법으로만 체크할 수 있게 된다.
Secure : HTTP는 기본적으로 평문으로 통신하기에 쿠키 역시도 확인이 가능하다. 하지만 Secure 옵션이 존재하는 순간 http에 쿠키를 붙여서 보낼 수 없게되며, 오로지 https를 통해서 전송할 시에만 쿠키를 붙일 수 있게 된다.
5. Partitioned
쿠키는 기본적으로 요청을 받는 서버별로 브라우저가 쿠키를 저장한다. 이와 같은 특징으로 인해 만약 광고 tracker.com 이라는 사이트가 특정 js를 광고용으로 여러 사이트 a.com, b.com에 넣어두면, 브라우저가 a.com, b.com을 방문했을 때 같은 tracker.com으로 요청이 들어감으로써 쿠키를 통해 브라우저를 고유하게 식별해서 사용자 추적이 가능해진다.
이러한 사용자 추적을 막기위한 기능이 Partitioned로 쿠키를 저장한 버킷을 tracker.com으로만 두는 것이 아닌 탑레벨 사이트 + 쿠키 도메인으로 a.com, b.com도 쿠키 저장소를 구분하는 기준에 추가하여, 탑레벨 사이트만으로 쿠키가 공유되지 않도록 한다.
6. Path
해당 쿠키를 클라이언트-서버가 통신할 때 전부 붙여서 보내는 게 아닌,
Path=/target-path로 설정하면 www.example.com/other-path 같은 url에선 쿠키가 전송되지 않고, www.example.com/target-path 및 해당 url의 하위 경로로 요청을 보낼때만 브라우저가 쿠키를 포함시킨다.
7. SameSite
Strict, Lax, None 3가지 옵션을 가질 수 있으며 각 옵션의 의미는 아래와 같다.
Strict
브라우저가 동일한 사이트 요청에만 쿠키를 전송한다. 예를 들어 www.a-example.com에서 백엔드인 www.be-example.com으로 요청을 보내 백엔드에서 쿠키를 설정해 보낸다면, 2개의 사이트의 도메인이 다르기에 Strict 옵션을 사용한 쿠키라면 저장이 되지 않는다.
만약 a-example.com에서 api.a-example.com 백엔드로 보낸다면 2개의 사이트의 도메인이 같기에 Strict 옵션을 통과하고 저장된다.
이때 동일한 사이트로 구분되는 기준은 Public Domain과 앞의 접미사까지 동일해야 동일한 사이트로 취급된다. public domain 리스트
ex) .com, .github.io 등 Public Domain에 대해 그 다음 항목인 a.com a.github.io | b.com b.github.io 는 다른 사이트이다.
Lax
Lax 옵션은 SameSite가 아니더라도, 특정 접근에 관해서는 쿠키를 붙이는 것을 허용하는 옵션이다. 여기서 특정 접근은 아래와 같다.
사용자가 직접 사이트에서 클릭을 해서 이동하는 경우
해당하는 내용은 특정 이미지 등의 리소스 요청이 아닌 전체 사이트가 변경되는 GET 요청(특정 사이트에서 동일한 로그인 정보를 공유하는 다른 사이트로 갈 때 등)에는 Lax 옵션을 통해 허용해준다.
None
오로지 Secure 옵션이 true 인 것과 병행될 때만 사용 가능한 옵션으로, 백엔드와 요청을 보낸 사이트가 samesite가 아니더라도 언제나 쿠키를 보낼 수 있는 옵션이다. 해당 옵션이 활성화되지 않는다면, 서버와 클라이언트의 도메인 주소가 다르다면 쿠키를 사용할 수 없다.
개발환경에서 localhost:3000에서 arna.medai.im으로 요청을 보낸다면 SameSite가 None이 아닌 경우 Cookie를 설정할 수 없다.
-
JWT
JWT 개요
JWT란 토큰의 일종으로, http 통신을 수행할 때 해당 토큰을 요청이나 응답의 일부로 넣어줌으로써 서버와 클라이언트가 상태유지를 수행할 수 있게 해주는 개념이다.
JWT는 header, payload, verify signature로 구성되어 JWT 토큰을 인코딩하고, 반대로 JWT 토큰을 디코딩하면 해당 값들을 획득할 수 있다.
이와 관련된 예시는 JWT 예시 사이트를 접속해 확인해보자
JWT 토큰 예시
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
이렇게 구성된 토큰은 Based64Decode 등의 방법론을 통해서 encode, decode 가능하다. 즉 별도의 키가 필요한 해독방식은 아니다.
토큰을 살펴보면 .으로 구분된 3개의 영역이 있는데 이를 각각 디코딩을 수행하면 아래와 같은 정보들이 생성된다.
디코딩된 정보 예시
1. header
{
"alg": "HS256",
"typ": "JWT"
}
토큰의 타입(typ)는 보통 JWT로 고정되어있고, alg는 알고리즘의 약자로, 3번 서명값을 만드는데 사용될 알고리즘의 약자가 적혀있다.
2. payload
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
}
Json 형식으로 서비스가 사용자에게 해당 토큰을 통해 공개하기를 원하는 내용, 사용자 닉네임, 서비스 상의 레벨, 관리자 여부 등의 정보를 저장할 수 있는 부분이다. 이렇게 토큰에 담긴 사용자에 대한 정보를 Claim이라고 한다. 이렇게 사용자에 대한 정보를 애초에 포함한 정보가 보내지기에, 서버가 DB 등을 뒤질 필요성도 적어진다.
3. verify signature
a-string-secret-at-least-256-bits-long
해당 값에는 header + payload + 서버에 존재하는 비밀값을 기반으로 헤더에 적힌 암호화알고리즘을 통해 생성된 secret string 서명값이 기록되어있다. 별도의 키 값 없이 payload나 header가 해독되더라도, 그 값을 멋대로 수정해버리면 서버에 존재하는 비밀키 값과 조합되서 알고리즘을 실행시켰을 때 verify signature값이 달라져버리기에 JWT가 인증의 수단으로 사용될 수 있다.
JWT의 특징
위와 같은 JWT의 구성방식을 보면 알 수 있듯이, JWT의 기록되는 상태 정보는 시간에 따라 달라지는 것이 아닌 해당 인증이 유효할 때 항상 동일한 정보를 기반으로 사용자를 구분한다. 이런 식으로 시간에 따라 바뀌지 않는 상태값을 갖는 것을 stateless라고 불리며, 반대로 세션은 stateful이라고 불린다.
이러한 특징으로 인해
인증에 관련된 사용자의 상태정보가 시간에 따라 바뀌지 않는다.
서버가 사용자의 정보를 별도로 기록할 필요가 없기 때문에, 비용적인 측면에서 장점이 존재한다.
토큰이 탈취되면 서버는 이를 구분할 방법이 없기에, 탈취에 대한 대처가 취약하다.
토큰 탈취의 위험을 막기 위해 보통 refresh Token과 access Token을 따로 구현한 후 위에서 이야기한 인증에 관련된 기능은 유효기간이 짧은 access Token으로 refresh Token은 access Token을 재발급해줄 수 있는 역할과 이를 서버가 기억함으로써 서버가 로그인을 관리할 수 있도록 한다.
실제 구현(파이썬 프로젝트와 관련된 예시)
.env 등 공개되지 않는 환경설정 파일에서 아래와 같이 JWT에 사용될 서버가 저장하는 비밀키를 저장한다.
JWT_SECRET_KEY=~
파이썬 코드 내부에서 사용될 알고리즘과 Access Token 만료시간, refresh Token 만료시간을 저장한다.
SECRET_KEY = JWT_SECRET_KEY
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 4 * 60 # AccessToken 만료 시간
REFRESH_TOKEN_EXPIRE_DAYS = 14 # RefreshToken 만료 시간
로그인이 완료된 후 payload에 입력될 정보들을 서버에서 입력해주어 Access Token을 생성해준다.
new_access_token = create_access_token(
data={"sub": username}, expires_delta=access_token_expires
)
def create_access_token(data: dict, expires_delta: Union[timedelta , None] = None):
to_encode = data.copy()
print("to_encode", to_encode)
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
현재 구현된 코드에선 사용자의 username과 access token이 만료되는 시간을 인증 payload에 넣은 모습을 볼 수 있다. AlGORITHM은 위에서 상수로 고정되어있으며, SECRET KEY는 .env파일을 통해 가져와서 jwt 코드를 인코딩해 Access Token을 사용하는 모습을 볼 수 있다.
또한 refresh Token은 서버에 저장해서 비교하는 용도이기 때문에, JWT만을 쓰지 않고 랜덤한 난수 등을 활용해 키를 만들 수도 있으면, 탈취되었다면 서버 DB에서 제거하는 등으로 대처할 수 있다.
다만 현재 구현되는 시스템은 병원의 내부망에서 사용될 예정이기에, 보안상 access Token의 탈취 위험이 적기에 현재는 별도의 refresh token을 사용하지 않게 변경되었다.
async def get_current_doctor(request: Request, db: Session = Depends(get_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token = request.cookies.get("access_token")
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
try:
# this decode will check expire token, if expired, raise exception
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(payload)
username: str = payload.get("sub")
print("get_current_doctor: ", username)
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError as e:
print("JWTError", e)
raise credentials_exception
doctor = get_doctor_by_username(username=token_data.username, db = db)[0]
if doctor is None:
raise credentials_exception
return doctor
마지막으로 위처럼 각 http 요청이 들어올 때마다, Middleware로 JWT를 디코딩하고 만약 서명에 문제가 발생시 JWTError를 발생시키는 코드를 통해 JWT를 통한 인증을 구현할 수 있다.
-
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는 포함할 수 있다.
참고링크
웹 개발자가 봐야할 하나의 지도 강의
-
상태관리 개요
개요
서버와 클라이언트의 상태관리를 위해서 웹 서버는 인증(Authentication)과 보안(Security)을 고려해야한다. 인증을 통해 사용자가 누구인지, 해당 사용자가 어떠한 권한을 가지고 있는지를 증명할 수 있고, 보안을 통해 이러한 사용자를 인증하는 요소를 다른 사람이 탈취하고나, 사용자의 개인정보를 열람할 수 없도록 보호할 수 있다. 이러한 요소를 구현하는 방법론에 대하여 정리해보자.
Authentication(인증)
왜 인증이 필요한가?
HTTP라는 프로토콜은 무상태(stateless)라는 특징을 갖고 있다.
이 말은 곧, 서버와 클라이언트가 HTTP로 통신할 때 요청 하나하나는 독립적이며, 이전 요청과의 연관성을 HTTP 자체만으로는 알 수 없다는 뜻이다.
예를 들어, 사용자가 어떤 사이트에서 장바구니에 물건을 담았다고 해보자. 그 뒤 다시 장바구니 페이지를 열었을 때, 이전에 담은 상품이 반영되어 있어야 한다. 하지만 HTTP 프로토콜만으로는 “이 사용자가 방금 전에 장바구니에 물건을 담았다”는 사실을 알 길이 없다. 서버는 새로운 요청이 들어올 때마다 단순히 그 순간의 요청 데이터만 보고 응답할 뿐, 누가 어떤 상태를 유지하고 있는지는 구분할 수 없다.
따라서 상태를 유지하기 위해서는 서버가 별도의 장치(세션 저장소, 데이터베이스, 쿠키, 토큰 등)를 활용해 요청과 요청을 연결할 수 있는 수단을 마련해야 한다. 여기서 등장하는 개념이 바로 인증(Authentication)이다.
인증이란 무엇인가?
인증은 말 그대로 서버와 클라이언트가 서로를 식별하는 절차를 의미한다.
즉, 클라이언트가 “나는 누구다”라고 주장할 때, 서버가 그것이 진짜인지 확인할 수 있어야 한다.
사용자가 로그인하면 서버는 그 사용자를 구분할 수 있는 식별자(세션ID, 토큰 등)를 발급한다.
이후 클라이언트는 새로운 요청을 보낼 때마다 이 식별자를 함께 전달한다.
서버는 이를 확인함으로써 “아, 이 요청은 앞에서 로그인한 그 사용자구나” 하고 신원을 이어서 파악할 수 있다.
이 과정을 통해 서버는 단순히 독립된 요청을 처리하는 것을 넘어, 연속된 사용자 경험(로그인 유지, 장바구니 상태 반영, 권한 확인 등)을 제공할 수 있게 된다.
인증된 상태를 유지하는 수단
보통 인증은 로그인, MFA, OTP, 하드웨어 토큰 등 강력하지만 복잡한 알고리즘을 사용하기에 무거운 1차적인 인증과, 아래의 나열한 한번 인증된 상태를 유지하여 HTTP 통신에 대한 인증된 상태를 유지하는 인증이 있다. 해당 포스트는 상태 유지와 관련된 인증에 대해서 소개한다.
Cookie
쿠키란 서버가 브라우저에 사용자에 대한 정보를 넣을 수 있는 수단으로, 응답을 보낼 때 쿠키를 설정하면 브라우저는 이를 저장하고 앞으로 해당 서버와 통신을 수행할 때 항상 쿠키를 같이 붙여서 보낸다. 서버와 클라이언트의 상태관리를 브라우저가 서버별로 저장하는 것이다.
Token TODO
토큰이란 문자열로 서버가 사용자를 인증하기 위해 특정 문자열을 통해 사용자를 인증하는 방식을 말한다. 브라우저가 존재하지 않는 Android나 IOS 등은 쿠키를 사용할 수 없기에 이러한 토큰을 통해 인증을 수행한다.
Session TODO
세션은 서버가 세션 DB라는 별도의 데이터베이스를 생성하여, 브라우저에 쿠키 등을 통해 세션 ID를 저장하게 해 접속을 수행하게 하는 방법으로 서버가 각 사용자에 대한 모든 인증 정보를 세션 DB에 저장하기에 모든 리퀘스트가 들어올 때마다 탐색하는 자원이 필요하지만 유저의 접속을 종료시키는 등의 추가적인 기능을 개발할 수 있게 해준다.
JWT
세션의 탐색 비용 및 추가적인 DB의 필요성이라는 단점을 해결하기 위한 방법론으로, 유저의 ID를 기반으로 서버가 Sign을 수행하여 JWT라는 토큰을 생성하여 유저에게 넘겨주고, 유저가 JWT를 기반으로 인증을 수행하는 방법이다. 서버가 해야할 일을 토큰의 유효성만 판단하면 되기에 비용이 값싸지만 유저의 계정 접속 종료등 유저를 관리하기 위한 구분을 수행할 수 없기에 추가적인 기능의 개발은 힘들다. (Redis 등의 세션 DB를 위한 값싼 DB를 사용)
Security(보안)
CORS
브라우저는 Origin이 다른 출처에 대한 접근을 원칙적으로 차단한다. 이러한 정책으로 인해 실제 서버를 구성할 때 오류가 나는 경우가 많은데, 이러한 CORS Error의 CORS에 대해서 자세하게 알아보자.
HTTPS
SSL 인증서
웹에 대하여
-
CORS
Cross Origin Resource Sharing(CORS)
우리가 API요청을 보내다보면 자주 CORS Error라는 표현을 들어볼 수 있다. 각종 통신을 수행할 때 오류를 쉽게 걸리게 하는 머리가 아픈 녀석이다. 그렇다면 브라우저는 왜 이런 정책을 사용하고 우린 어떻게 이런 Error를 해결할 수 있을까?
SOP 정책
SOP란?
사실 실제 오류가 걸리게하는 정책은 CORS라 불리는 것이 아닌 SOP(Same Origin Policy)이다. 브라우저는 기본적으로 Origin이 다른 곳에 요청을 보낼 때 해당 요청을 차단하는 정책을 기본값으로 가지고 있다. 그렇다면 여기서 Origin이 정확히 무엇일까?
http://front.com:80/pages/10?search=good 이런 URL이 있다고 가정해자
프로토콜은 http
host주소는 front.com
포트 번호는 80
이 3가지 요소를 합쳐져서 Origin이라고 불린다.
기본적으로 브라우저는 우리가 이런 Origin이 다른 곳에 요청을 보낼 때 요청을 차단을 수행하지만, 실제 우리가 사용하는 환경은 다른 출처에 요청을 보내야하는 경우가 존재하기에, 이러한 정책을 통과시켜주는 방법론이 CORS이다. 즉 브라우저는 너가 다른 Origin으로 요청을 보내고 싶으면 CORS를 통해 허용을 하라고 표현해주는 것이다.
왜 SOP가 필요할까?
아래와 같은 시나리오를 생각해보자.
무수한 CORS 오류를 겪은 철수는 이 에러에 진절머리가나 SOP 정책을 없애리도록, 자신이 만든 사이트에
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
처럼 허용을 해버렸다.
영희는 철수에 사이트를 가입하고, 플래시 게임을 하려 evil.com에 접속을 한다. 그런데 evil.com 관리자는 철수의 동료라 SOP를 무시하는 코드를 실행시켰다는 것을 알아 아래와 같은 코드를 넣어두었다.
<script>
fetch('http://철수.com').then(/**/) // 이후 공격자에게 전송
</script>
영희의 브라우저는 요청을 받았을 때, Source와 fetch의 요청되는 사이트의 Origin이 다르다는 사실을 확인했지만, 철수의 사이트는 어떤 사이트가 출처든 허용되는 설정을 해버렸다.
공격자는 영희가 evil.com에 접속했을 때 철수의 사이트에 화면을 그대로 볼 수 있게 되버렸고, 영희의 해당 사이트의 개인정보가 탈취되었다…
즉 SOP 정책을 통해서 Origin이 다른 사이트의 fetch를 브라우저가 자체적으로 차단함으로써, 해당 사이트의 정보를 다른 사이트가 탈취할 수 없도록 만드는 것이 해당 정책이 있는 이유이다. 즉 사용자가 악성 사이트에 들어가더라도, 다른 사이트의 정보, 토큰, 세션 ID 등이 탈취되지 않도록 보호하는 정책이다.
CORS
CORS란?
현대 웹에서는 특정 사이트가 다른 사이트의 데이터를 활용해야 하는 경우가 많다. 예를 들어, 프론트엔드에서 자체 백엔드 서버로 요청을 보내거나, 공공 데이터 포털처럼 외부에서 제공하는 API에 접근하는 경우가 있다. 하지만 이러한 요청은 대체로 서로 다른 Origin을 가지므로, 브라우저의 SOP 정책에 의해 기본적으로 차단된다.
이를 해결하기 위해 등장한 것이 CORS(Cross-Origin Resource Sharing) 정책이다.
예를 들어, 프론트엔드에서 fetch 명령으로 어떤 백엔드 서버에 데이터를 요청한다고 하자. 이때 프론트엔드의 URL과 백엔드 서버의 URL이 서로 다른 Origin을 가지면, 브라우저는 SOP 규칙에 따라 해당 요청을 차단한다.
그러나 만약 백엔드 서버가 CORS 설정을 통해 프론트엔드 서버의 Origin을 허용한다면, 브라우저는 이 요청을 정상적으로 실행할 수 있다. 그 결과, 사용자는 브라우저를 통해 해당 백엔드 서버의 데이터에 접근할 수 있게 된다.
즉, CORS라는 것은 미리 특정 사이트에 대한 사용자가 접근할 수 있는 페이지나, 정보를 다른 사이트가 봐도 된다고 허용을 해주는 것이다. 또한 이런 허용은 브라우저에서 설정하는 것이 아닌 특정 사이트(즉 백엔드 서버나, API 서버 등등)에서 허용을 해주는 것이다.
CORS의 세가지 시나리오
JavaScript, Browser, API Server의 통신은 위와 같은 그림으로 나타낼 수 있다. 여기서 빨간 부분이 CORS의 시나리오에 해당하는 부분이다. 각 동작에 따라 CORS는 크게 3가지의 시나리오로 나누어서 판단을 수행한다.
Simple Request
Simple Request는 Get과 Post 같은 요청을 보낼 때 수행되며, 요청을 보내는 걸 별도의 인증 없이 바로 수행할 수 있으며, 만약 인증이 수행되지 않더라도 응답에 대한 데이터를 JavaScript가 못받는 것 뿐이다. 해당 요청은 Access-Control-Allow-Origin에 Origin이 포함만 되어있다면, 별도의 요청없이 바로 수행된다.
Preflight Request
HTTPS의 특별한 메소드인 Option 메소드를 활용하며 예비 요청을 사전에 보낸다. 이 예비 요청의 역할은 Origin과 Access-Control-Allow-Origin을 비교해주어 Origin이 CORS에 등록되어있으면 200을 보내는 역할이다. 그 후 해당 요청을 통과하면 본 요청을 보내주는 역할이다. 대부분의 요청은 Preflight Request로 검증을 수행한다.
CORS가 생기기 이전 SOP만 가능하다는 가정하에 만들어진 서버들이 CORS 처리 매커니즘이 있는지 확인해주기 위해 Preflight Request를 통해 브라우저가 사전에 확인을 해줌으로써, 해당 사항을 처리할 수 있는 서버만 해당 동작이 수행될 수 있도록 만든 것이다.
Credentitaled Request
Cookie나 Session에 대한 정보가 담긴 HTTPS 요청이 들어올 때 Credentitaled Request가 수행된다. 기본적인 매커니즘은 Origin을 검사하는 Preflight Request와 동일하지만, 해당 Request를 수행할 때 Access-Control-Allow-Origin이 Wild Card : * 로 설정되어있을 때 브라우저가 자동으로 차단하며, Access-Control-Allow-Credentials가 true일 때만 해당 요청을 수락하는 추가적인 보안 사항이 존재한다.
CORS와 개발환경
CORS는 기본적으로 Origin이 사전에 허용되어있거나, 동일해야지만 브라우저가 fetch를 허용해준다. 하지만 여기서 문제가 발생한다. 개발환경인 경우 모든 개발자의 컴퓨터를 어딘가에 배포한 것이 아닌 각자 컴퓨터에서 프론트엔드를 실행하기에 Origin을 하나로 고정할 수 없다는 문제가 발생한다.
또한 리다이렉트 등의 문제가 발생하면, 기존의 허용해두었던 Origin이 변경되어 무수한 CORS 에러를 만나게 되었다.
근본적인 문제는 각 개발환경은 ip가 모두 다른 것이다. 그렇기에 FE 개발환경과 백엔드 서버를 CORS 오류가 없이 연결하기 위해서는, localhost:3000과 같은 특정 도메인을 프론트엔드 개발환경에서 만드는 브라우저가 인식하는 Origin을 모든 개발환경의 개발자들이 통일할 필요성이 있고, 통일한 Origin을 백엔드에서 allow origin으로 허용해주어야 SOP 정책을 통과할 수 있다.
프론트엔드와 백엔드를 연결하는 과정에서 인증과 CORS 정책이 겹쳐 해결한 방식을 아래에 포스트에 정리하였다.
```
-
Web의 개요
개요
프론트 엔드 뷰어와, 백엔드 서버를 결합하는 작업을 수행하면서 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 상태가 된다.
-
프로그래밍의 기본 요소 설명
개요
출처: 쉬운코드 영상
객체와 클래스
객체와 클래스
객체란?
=> 상태가 있고 행동을 하는 실체
클래스란?
=> 객체의 관점에서의 클래스는, 객체가 어떠한 속성이 있고 어떠한 행동을 하는지를 기술한 설계도이다.
// example1
class Car {
private String name;
private double speed;
private Size size;
...
public void start() { ... }
public void stop() { ... }
...
}
Car myCar = new Car("니로");
Car yourCar = new Car("소나타");
Car ourCar = new Car("스포티지");
Class Car는 Car라는 객체들이 어떠한 상태(name, speed, size)를 가질 수 있고, 어떠한 행동을 할 수 있는지를 설명한다.
또한 이런 Class로 myCar, yourCar, outCar 각 객체는 선언되며, 속성은 모두 동일하지만 각 객체가 가질 수 있는 상태는 모두 다를 수 있다!
// example2
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int get() {
return count;
}
}
Counter appleCounter = new Counter();
Counter orangeCounter = new Counter();
카운터는 어떤 것을 세는 것을 정의한 클래스
내부적으로 count(개수라는 상태), 카운트를 증가시키는 행동, 숫자를 센 값을 가져오는 행동을 기술
instantiate란?
new라는 키워드처럼 객체화시켜서 만든 객체(object)를 instance라고 말한다.
// example3
class Switch {
private int state = 0;
public void on() {
this.state = 1;
}
public void off() {
this.state = 0;
}
public boolean isOn() {
return this.state == 1;
}
}
Switch tvSwitch = new Switch();
내가 원하는 속성, 행동을 구체적으로 기술하고 이를 실체화한 것이 객체이다.
이러한 클래스와 객체의 개념은 현실세계를 효율적으로 프로그래밍으로 옮길 수 있게 된다.
코드의 재사용성과 확장성
클래스를 만들면 반복해서 객체를 생성할 수 있음 -> 생산성과 유지보수 용이
데이터와 행동을 함께 묶음
클래스는 속성(데이터)과 기능(메서드)를 하나로 묶음으로써 큰 시스템을 설계할 떄 클래스 단위로 나누면 역할 분담이 쉽다.
객체 지향 프로그래밍(OOP)의 기반
OOP는 캡슐화, 상속, 다형성을 통해 유연하고 강력한 프로그램을 만듦
클래스와 객체가 없다면 이러한 구조적 설계가 불가능
변수와 값
변수란 값을 담을 수 있는 이름이 있는 그릇!
변수는 어떠한 객체가 있는 주소나, 실제 값이 담긴다. ex) 1, “안녕”, Object(“곡괭이”)
변수는 값이 바뀔 수 있다.
클린코드를 위해서는 변수를 꼭꼭꼭 모르는 사람이 봐도 이해하기 쉽게 적자!
함수
아래에 2개 중 무엇이 함수인가?
class Add:
def add(a, b):
return a + b
vs
def add(a, b):
return a + b
함수란?
독립적으로 존재하며 임무(task)를 수행하는 코드들의 집합
함수 이름으로 호출한다.
매개변수를 받을 수도 않을 수도 있다.
결과 값을 리턴할 수도 안할 수도 있다
재사용이 가능하다
매서드란?
객체 혹은 클래스에 종속되어 임무를 수행하는 코드들의 집합
클래스나 객체의 상태 정보에 접근 가능
매소드는 객체의 상태에 영향을 받기에, 같은 클래스로 선언된 메소드더라도, 객체 혹은 클래스에 종속되어 임무를 수행한다!
그래서 질문에 대한 대답은, 객체의 상태에 영향을 받는 클래스의 add는 메소드, 그냥 add는 함수이다!
변수와 객체는 메모리에 어떻게 저장되는가?
어플리케이션은 어떻게 실행되는가?
어플리케이션은 일반 사용자가 사용할 기능을 제공하는 컴퓨터가 실행할 수 있는 명령어들의 집합
메모리는 실행된 애플리케이션이 상주하는 곳 => 어플리케이션이 메모리에 있어야 실행이 가능하다.
public class Main {
public static void main(String[] args){
int a = 7;
int b = 3;
int c = a + b;
}
}
a = 7이라는 명령을 cpu에서 실행하고, 이를 메모리에 7이란 값을 올리고, 그 곳에 a라는 이름을 붙임
b = 3이라는 명령을 cpu에서 실행하고, 이를 메모리에 3이란 값을 올리고, 그 곳에 a라는 이름을 붙임
…
이런 식의 명령과 저장이 계속되는 것.
runtime => application이 메모리에 올려져서 실행되고 있는 순간을 말한다.
사실, 이런 변수와 변수의 연산과정만이 아닌, 함수도 메모리에 저장된다.
메모리 구조
애플리케이션에 할당되는 메모리는 stack 메모리와 heap 메모리 등의 여러 영역으로 나눠진다.
| 메모리 영역 | 역할 | 주의 점|
| -------- | --------- | --------- |
| Stack | 함수나 메서드의 지역 변수와 매개 변수가 저장되는 곳, 함수나 메서도가 호출될 때도 스택 프레임이 쌓인다. | 스택 프레임은 개발자가 이를 신경 쓸 필요는 보통 없지만, 함수를 재귀적으로 많이 호출하여 스택 메모리 이상을 저장하게 되면 문제가 발생한다.|
| Heap | 객체가 저장되는 공간 | |

// example
public class Main {
public static void main(String[] args) { // 매개변수
Counter c = new Counter(); // 객체, 지역 변수
}
}
public class Counter {
private int state = 0; //상태를 나타내는 instance 변수
public void increment() {
state++;
}
public int get() {
return state;
}
}
3가지 변수가 존재!
객체가 클래스로 부터 생성될 때 생성자가 실행되는 스택프레임이 먼저 생성되고, 이것이 힙에 객체를 생성한 후, 스택프레임이 사라진다. 이때 생성자 스택 프레임에서 this라는 보이지 않는 변수가 힙 주소를 저장하였다가 반환한다.
그리고 해당 stack의 지역변수로 객체가 선언되었으므로, 스택 프레임에 방금 생성된 객체의 heap 주소가 저장된다.
매서드 역시도 stackframe을 통해 생성되며, 매서드는 객체에 종속되어있는데 이 정보가 this를 통해 어떤 객체를 가리키도록 생성된다.
호출된 함수나 매서드는 파라매터로 해당 객체의 주소를 전달받아 상태를 변경시키면, 해당 함수나 매서드가 종료되어 스택메모리에서 사라지더라도 변화된 정보는 힙 메모리에서 유지된다.
쓰레기 객체(garbage object)
public class Main {
public static void main(String[] args) {
Counter c = make();
}
public static Counter make() {
Counter c = new Counter(); // => 새로운 객체를 heap에 생성 그러나 이를 전달해주지 않고
return new Counter(); // => 새로운 객체를 heap에 생성해 전달
}
}
public class Counter {
private int state = 0;
public void increment() {
state++;
}
public int get() {
return state;
}
}
위의 예시처럼 접근할 수 없는 객체가 생겨버리면, 쓸모없는 객체가 heap 메모리를 차지하게 된다. 이런 객체를 쓰레기 객체라고 한다.
이런 것을 삭제해주기 위해 2가지 방법이 있다.
개발자가 직접 해당 객체를 지정해 메모리를 해제
gc(garbage collector)를 지원하는 언어에서 이러한 객체를 자동으로 삭제
파이썬은 모든 것이 객체이기 때문에
def wow(num):
print(num)
a = 1
wow(a)
파이썬은 모든 것을 객체로 저장하기 위해 스택에 올릴 때 글로벌 프레임을 사용한다.
-
Touch background to close