Chapter 0

큰 그림: 요청 하나가 겪는 일

브라우저에 주소를 치고 화면이 뜨기까지. 이후 모든 챕터는 이 여정의 한 구간이에요.

요청 여정 따라가기

https://api.ssobbs13.pp.ua/diaries를 호출하면…

요청은 결국 네 가지 질문을 통과해요

질문담당실패하면 보이는 것
어디로?DNSDNS_PROBE_FINISHED_NXDOMAIN, dig 결과 없음
닿나?IP · TCP · 방화벽timeout, connection refused
믿을 수 있나?TLS · 인증서NET::ERR_CERT_*, 525/526
들어가도 되나?WAF · 인증 · 인가 · CORS403, 401, 콘솔의 CORS 에러

고장 지점 찾기: 이 단계가 망가지면 무엇이 보일까?

이 교과서 사용법각 챕터 끝에 퀴즈가 있어요. 다 읽었으면 "이 챕터 완료"를 눌러 진도를 기록하세요. 진도는 이 브라우저에만 저장돼요.
Chapter 1

도메인: 인터넷 주소의 "이름"

IP 주소는 외우기 어려우니 사람이 읽는 이름을 붙인 것. 그 이름을 누가, 어떻게 관리할까요?

도메인 해부하기 (조각을 눌러보세요)

조각을 선택하세요.

도메인을 둘러싼 세 주체

주체역할이번 경우
레지스트리 (Registry)TLD 전체를 관리하는 기관. 장부의 원본을 가짐.ua 레지스트리
레지스트라 (Registrar)레지스트리에 대신 등록해주는 판매 대리점nic.ua
등록자 (Registrant)도메인 주인나

nic.ua에서 한 일은 레지스트리 장부에 "이 도메인의 네임서버는 Cloudflare야"라고 적은 것뿐이에요. 도메인 소유권은 여전히 nic.ua를 통해 관리돼요.

서브도메인은 공짜

내가 산 건 ssobbs13.pp.ua 하나예요. 그 아래 api., www., admin.은 DNS 레코드 한 줄로 무한히, 무료로 만들 수 있어요. 서브도메인에는 "서버"가 필요 없어요. 그냥 이름표예요.

Public Suffix List (PSL)

pp.ua, co.kr, github.io, pages.dev처럼 "이 아래 이름은 각자 다른 주인"이라고 정한 공개 목록이에요. 브라우저는 이 목록으로 쿠키를 격리하고, Cloudflare는 ssobbs13.pp.ua를 독립 도메인으로 인정해요.

도메인의 일생: 만료되면 바로 사라질까?

아니에요. 만료일이 지나도 며칠~수십 일 동안 되찾을 기회가 있어요. 대신 뒤로 갈수록 비싸지고, 마지막엔 아무도 못 막아요.

만료 타임라인 (gTLD 기준, 단계를 눌러보세요)

.com 같은 gTLD 기준이에요. .ua 같은 국가 도메인(ccTLD)은 기간과 규칙이 레지스트리마다 달라요. 정확한 건 nic.ua 약관을 확인하세요.

WHOIS와 RDAP: 도메인 등기부 등본

whois ssobbs13.pp.ua          # 레지스트라, 만료일, 네임서버, 상태 코드
curl -s https://rdap.org/domain/example.com | jq   # WHOIS의 후속 표준 (JSON)
상태 코드 (EPP)뜻
clientTransferProhibited이전 잠금. 남이 몰래 다른 레지스트라로 옮기지 못하게. 켜두는 게 정상
clientHold / serverHoldDNS에서 빠짐. 만료, 미납, 신원 확인 미완료 때 걸려요. 사이트가 통째로 사라져요
redemptionPeriod만료 후 복구 기간. 비싼 수수료를 내야 되찾을 수 있어요
pendingDelete삭제 대기. 이 상태가 되면 누구도 못 되돌려요
꼭 기억할 것만료일 2027-09-25, 자동 갱신 꺼짐. 만료되면 사이트가 통째로 죽고 남이 가져갈 수 있어요. Auth code(EPP)는 도메인 이전용 비밀번호라 절대 공유하면 안 돼요.
Q. admin.ssobbs13.pp.ua를 쓰고 싶다. 무엇을 해야 할까?
서브도메인은 DNS 레코드 한 줄이에요. 구매도, 서버도 필요 없어요. 터널을 쓰면 route 추가 시 레코드가 자동 생성돼요.
Q. 만료일을 3일 넘겨서 사이트가 안 뜬다. 가장 알맞은 설명은?
만료 직후는 보통 유예 기간(Grace)이에요. 레지스트라가 DNS를 멈추거나 주차 페이지로 바꾸지만, 갱신하면 그대로 돌아와요.
Chapter 2

DNS: 이름을 주소로 바꾸는 전화번호부

브라우저가 가장 먼저 하는 일은 "이 이름의 IP가 뭐지?"를 묻는 것.

DNS 조회 시뮬레이터

두 종류의 DNS 서버

  • 권한 있는 네임서버 (Authoritative): 정답 원본을 가진 서버. 지금은 cash / virginia.ns.cloudflare.com
  • 리졸버 (Recursive Resolver): 대신 물어보고 캐시하는 서버. 통신사 DNS, 8.8.8.8, 1.1.1.1

레코드 타입 탐색기

TTL과 "전파"의 정체

TTL(Time To Live)은 리졸버가 답을 캐시하는 시간(초)이에요. "DNS 전파에 24시간 걸린다"는 말의 정체는 전 세계 리졸버의 캐시가 만료되기를 기다리는 시간이에요. 뭔가 퍼져나가는 게 아니에요.

TTL 계산기: 레코드를 바꾸면 언제 모두에게 반영될까?

권한 네임서버는 어떻게 답할까: zone 조회 시뮬레이터

아래는 ssobbs13.pp.ua zone의 예시 레코드예요. 이름과 타입을 넣으면 Cloudflare 네임서버가 어떻게 답하는지 보여줘요. 와일드카드, CNAME 따라가기, Cloudflare 프록시의 원본 숨김, NXDOMAIN과 NODATA의 차이를 직접 확인해보세요.

zone 조회 시뮬레이터


    

캐시는 한 군데가 아니에요

"레코드를 바꿨는데 나만 옛날 IP가 나와요"의 원인은 대부분 아래 캐시 중 하나예요. 위에서부터 차례로 확인해요.

순서캐시 위치비우는 법
1브라우저 DNS 캐시 (Chrome은 약 1분)chrome://net-internals/#dns → Clear host cache
2OS 캐시 + /etc/hosts (hosts 파일이 DNS보다 우선)macOS sudo dscacheutil -flushcache, Windows ipconfig /flushdns
3공유기 DNS 프록시재부팅하거나 리졸버를 1.1.1.1로 직접 지정
4리졸버 (통신사, 8.8.8.8, 1.1.1.1)못 비워요. TTL 만료를 기다리거나 dig @1.1.1.1로 다른 리졸버에 물어봐요. Cloudflare는 1.1.1.1 캐시 삭제 페이지를 제공해요
없다는 답도 캐시돼요 (negative caching)레코드를 만들기 전에 한 번 조회했다면, 리졸버는 "없음(NXDOMAIN)"이라는 답도 SOA 레코드에 적힌 시간만큼 캐시해요. 그래서 레코드를 먼저 만들고 나서 조회하는 습관이 좋아요.
DoH / DoT원래 DNS 질문은 평문 UDP 53번이라 통신사가 보고 바꿀 수도 있어요. DNS over HTTPS(443)와 DNS over TLS(853)는 이걸 암호화해요. 브라우저의 "보안 DNS" 설정이 DoH예요. 켜두면 OS나 공유기 캐시를 건너뛰기도 해서, 디버깅할 때 결과가 다르게 나올 수 있어요.
apex와 CNAME@(= ssobbs13.pp.ua 자체)는 원래 규칙상 CNAME을 못 가져요. Cloudflare는 CNAME Flattening으로 몰래 A 레코드처럼 바꿔줘서 가능하게 해줘요.
Q. api 레코드를 xxx.cfargotunnel.com으로 연결하려면 어떤 타입을 써야 할까?
이름 → 다른 이름(별칭)은 CNAME. A는 이름 → IP 주소예요.
Q. Cloudflare 프록시(Proxied)인 api CNAME 레코드를 dig +short api.ssobbs13.pp.ua로 조회하면?
Proxied 레코드는 밖에서 보면 CNAME이 숨겨지고 엣지 IP만 보여요. 그래서 오리진 정보가 노출되지 않아요.
Chapter 3

Zone: Cloudflare에서 도메인을 다루는 단위

"Zone을 추가한다" = "Cloudflare야, 이 도메인의 DNS를 네가 맡아줘"

한 줄 정의Zone = 한 도메인에 대한 DNS 레코드 묶음 + 그걸 관리할 권한. Cloudflare에서는 여기에 더해 그 도메인에 적용되는 모든 설정의 단위예요(SSL, 캐시, WAF, 규칙…).

Zone 상태 변화

Zone 안에 들어있는 것 (대시보드 왼쪽 메뉴)

메뉴하는 일
DNS레코드 관리 (A, CNAME…), Proxied / DNS only 전환
SSL/TLS암호화 모드(Flexible/Full/Strict), 인증서
SecurityWAF, 봇 차단, Rate limit
Caching엣지 캐시 규칙
Rules리다이렉트, 헤더 변경
Workers Routes이 도메인 경로에 Worker 연결

네임서버를 옮길 때 가장 무서운 함정: DNSSEC

DNSSEC은 DNS 답에 전자서명을 붙여 위조를 막는 기술이에요. 상위 zone(레지스트리)에 DS 레코드("이 도메인은 이 키로 서명해요")가 등록돼 있으면, 검증하는 리졸버는 서명이 그 키와 맞는지 확인해요.

NS 이전 순서 비교

nic.ua 기본 설정에서 DNSSEC이 꺼져 있었다면 이번 이전에는 해당 없어요. 하지만 다른 도메인을 옮길 땐 레지스트라에서 DNSSEC(DS) 상태를 먼저 확인하세요. Active가 된 뒤 Cloudflare의 DNS → Settings에서 DNSSEC을 켜고, 알려주는 DS 값을 레지스트라에 등록하면 돼요.

Q. 터널 route 화면의 도메인 목록에 ssobbs13.pp.ua가 안 보인다. 가장 유력한 이유는?
Active가 되기 전엔 Cloudflare가 이 도메인의 주인이라고 인정하지 않아서 목록에 안 나와요.
Chapter 4

IP, 포트, 방화벽: "어디로, 몇 번 문으로"

IP는 건물 주소, 포트는 호실 번호, 방화벽은 경비원.

IP 주소의 종류

이 IP는 어떤 종류? (입력해보세요)

IP를 입력하세요.
Docker에서 가장 헷갈리는 것컨테이너 안의 localhost는 그 컨테이너 자신이에요. 호스트 서버가 아니에요. cloudflared 컨테이너가 localhost:8080으로 Spring에 닿으려면 같은 네트워크 네임스페이스를 공유하거나(network_mode), 서비스 이름(http://be:8080)을 써야 해요.

CIDR: IP 범위를 한 줄로 쓰는 법

10.0.0.0/24에서 /24는 "앞의 24비트는 고정, 나머지 8비트는 자유"라는 뜻이에요. 방화벽 규칙, VCN 서브넷, Docker 네트워크가 전부 이 표기를 써요. 0.0.0.0/0은 "모든 IP", /32는 "딱 이 IP 하나"예요.

CIDR 계산기

NAT: 사설 IP가 인터넷에 나가는 방법

집 공유기 안의 노트북(192.168.0.10)도, OCI VCN 안의 서버(10.0.0.5)도 사설 IP예요. 밖으로 나갈 때 공유기나 인터넷 게이트웨이가 출발 주소를 공인 IP로 바꿔서 내보내고, 돌아오는 응답을 기억해뒀다가 다시 안쪽으로 돌려줘요. 이게 NAT예요.

  • 그래서 밖에서 먼저 사설 IP로 연결을 걸 수는 없어요. 받으려면 포트 포워딩이 필요해요.
  • cloudflared가 NAT 뒤에서도 잘 되는 이유: 안에서 먼저 나가는 연결이라 NAT가 응답 경로를 기억해줘요.
  • OCI 인스턴스의 "공인 IP"도 실제로는 VCN의 인터넷 게이트웨이가 1:1 NAT로 붙여준 거예요. 서버 안에서 ip addr를 치면 사설 IP만 보여요.

자주 쓰는 포트

포트용도
22SSH (서버 접속)
80 / 443HTTP / HTTPS
5432 / 3306PostgreSQL / MySQL
8080Spring Boot 기본값

인바운드 vs 아웃바운드

연결 방향 비교

OCI의 방화벽은 두 겹

인터넷 → [OCI Security List / NSG] → [서버 OS: iptables/ufw] → [Docker] → 앱
         클라우드 콘솔에서 설정          서버 안에서 설정

"포트 열었는데 안 돼요"의 90%는 OS 방화벽을 안 열어서예요. 터널을 쓰는 지금은 둘 다 닫아두는 게 정답. SSH(22)만 열어두세요.

상태 기반(stateful) 방화벽OCI Security List의 기본 규칙은 stateful이에요. 안에서 나간 연결의 응답은 인바운드 규칙이 없어도 자동으로 들어와요. 그래서 cloudflared는 인바운드 규칙 하나 없이 엣지와 대화할 수 있어요.

방화벽 규칙 시뮬레이터 (OCI 인바운드 규칙)

규칙을 켜고 끄고, 들어오는 패킷을 바꿔보세요. 허용 규칙 중 하나라도 맞으면 통과, 아무것도 안 맞으면 버려져요.

Q. 터널을 쓰면 OCI에서 443 포트를 열어야 할까?
터널은 서버가 먼저 바깥으로 연결을 열고, 요청은 그 연결을 타고 들어와요. 인바운드 포트는 필요 없어요.
Q. SSH를 집에서만 접속하게 하고 싶다. 집 공인 IP가 203.0.113.7이면 소스 CIDR은?
/32는 32비트 전부 고정, 즉 IP 딱 하나예요. 다만 가정용 IP는 바뀔 수 있으니, 바뀌면 콘솔에서 규칙을 고쳐야 해요.
Chapter 5

HTTPS, TLS, 인증서: nginx SSL이 어려웠던 이유

HTTPS = HTTP + TLS. TLS는 암호화와 신원 확인을 해줘요.

  • 인증서: 믿을 수 있는 기관(CA)이 "이 서버는 이 도메인 주인 맞음"이라고 서명한 파일
  • Let's Encrypt: 무료 자동 CA. 90일마다 갱신 필요

인증서 체인: 브라우저는 누구를 믿나

브라우저와 OS에는 루트 CA 약 150개의 공개키가 미리 들어있어요(신뢰 저장소). 루트는 금고에 보관하고, 실제 발급은 중간 CA가 해요. 서버는 "내 인증서 + 중간 인증서"를 함께 보내고, 브라우저가 루트까지 서명을 거슬러 올라가며 확인해요.

체인 검증 따라가기

이 인증서로 이 도메인을 쓸 수 있나?

인증서에는 쓸 수 있는 이름 목록(SAN, Subject Alternative Name)이 적혀 있어요. 와일드카드 *는 딱 한 단계만 대신해요. Cloudflare 무료 인증서(Universal SSL)는 ssobbs13.pp.ua와 *.ssobbs13.pp.ua만 담고 있어요.

인증서 이름 매칭기

nginx로 직접 하면 해야 하는 일

체크리스트 (몇 개나 할 줄 아나요?)

Cloudflare를 쓰면: TLS가 두 구간으로 나뉨

SSL/TLS 모드 선택기

터널을 쓰면②구간이 터널 자체로 암호화돼요. http://localhost:8080처럼 평문으로 적어도 안전해요. 평문 구간은 서버 내부뿐이에요. nginx도, certbot도, 인증서 파일도 필요 없어요.

터널 없이 직접 오픈한다면: Origin CA 인증서

Full (strict)를 쓰려면 오리진에도 믿을 만한 인증서가 있어야 해요. Let's Encrypt 대신 Cloudflare가 무료로 주는 Origin CA 인증서(최대 15년)를 쓸 수 있어요. 단, 이 인증서는 Cloudflare 엣지만 믿어요. 브라우저가 오리진에 직접 접속하면(DNS only) 경고가 떠요.

HSTS: "이 사이트는 앞으로 무조건 HTTPS"

Strict-Transport-Security: max-age=31536000; includeSubDomains

이 헤더를 한 번 받은 브라우저는 max-age 동안 http://로 접속하려 해도 스스로 https://로 바꿔요. 처음 한 번의 평문 요청을 가로채는 공격(SSL stripping)을 막아요. Cloudflare의 SSL/TLS → Edge Certificates에서 켤 수 있어요.

HSTS는 되돌리기 어려워요includeSubDomains를 켜면 모든 서브도메인이 HTTPS를 강제당해요. HTTPS가 안 되는 서브도메인이 하나라도 있으면 그곳은 max-age 동안 접속 불가예요. max-age=300처럼 짧게 시작해서 늘리세요. preload는 브라우저에 하드코딩되는 거라 빼는 데 몇 달 걸려요.
Q. 브라우저엔 자물쇠가 뜨는데 Cloudflare→서버 구간이 평문인 위험한 모드는?
Flexible은 뒤쪽이 HTTP예요. 리다이렉트 무한루프의 단골 원인이기도 해요.
Q. v1.api.ssobbs13.pp.ua를 만들었더니 인증서 오류가 난다. 이유는?
와일드카드는 라벨 하나만 대신해요. 두 단계 이상은 Advanced Certificate(유료)로 *.api.ssobbs13.pp.ua를 따로 발급하거나, v1-api처럼 한 단계로 이름을 지어요.
Chapter 6

프록시, 리버스 프록시, CDN, 엣지

Cloudflare의 정체는 "전 세계에 깔린 거대한 리버스 프록시"예요.

용어뜻
포워드 프록시클라이언트 쪽 대리인 (회사 프록시, VPN)
리버스 프록시서버 쪽 대리인. 요청을 대신 받아 뒤로 넘김 (nginx, Caddy, Cloudflare)
CDN전 세계에 서버를 두고 가까운 곳에서 응답하는 네트워크
엣지CDN의 각 서버. 커넥터가 붙어있던 icn(인천)이 그 예
오리진진짜 원본 서버. OCI의 Spring 앱
Anycast여러 서버가 같은 IP를 쓰고, 가장 가까운 곳으로 자동 연결

Cloudflare 프록시(Proxied) vs DNS only

프록시 상태를 눌러 바꿔보세요

캐시

정적 파일(JS, CSS, 이미지)은 엣지 캐시로 빨라져요(Pages가 자동으로 해줘요). API 응답은 기본적으로 캐시 안 돼요. 캐시 규칙을 잘못 만들면 남의 일기가 보이는 사고가 날 수 있어요. Spring에서 Cache-Control 헤더로 제어하세요.

엣지 캐시 시뮬레이터: 같은 요청을 두 번 보내면?

CF-Cache-Status뜻
HIT엣지 캐시에서 바로 응답. 오리진은 몰라요
MISS캐시에 없어서 오리진에 갔다 옴. 다음 요청부터 HIT
EXPIRED / REVALIDATED캐시가 만료돼서 오리진에 다시 받음 / 오리진이 304로 "그대로 써"라고 함
BYPASS캐시 대상이지만 오리진 헤더(private, no-store, Set-Cookie) 때문에 건너뜀
DYNAMIC애초에 캐시 대상이 아님 (HTML, API 기본값). 가장 흔한 값

브라우저 개발자 도구 → Network → 응답 헤더에서 바로 볼 수 있어요. curl -sI https://…/app.js | grep -i cf-cache로도 확인해요.

리버스 프록시가 덧붙이는 헤더

프록시를 거치면 오리진이 보는 "접속자"는 프록시 자신이에요. 그래서 프록시는 원래 정보를 헤더에 적어서 넘겨줘요.

헤더담긴 정보Spring에서
X-Forwarded-For원래 클라이언트 IP (프록시를 지날 때마다 뒤에 추가)request.remoteAddr (설정 후)
X-Forwarded-Proto원래 스킴 (https)request.isSecure, 리다이렉트 URL
X-Forwarded-Host원래 Host절대 URL 생성
CF-Connecting-IPCloudflare가 확인한 사용자 IP 하나직접 꺼내 쓰기 가장 쉬움
헤더는 위조할 수 있어요누구나 X-Forwarded-For: 1.2.3.4를 붙여 보낼 수 있어요. 믿어도 되는 건 내가 신뢰하는 프록시가 붙인 값뿐이에요. 터널 구조에선 오리진에 들어오는 모든 요청이 Cloudflare를 거치니 안전하지만, 포트를 직접 열었다면 Cloudflare IP 대역에서 온 요청만 믿어야 해요.
Q. 터널용 api CNAME 레코드의 프록시 상태는?
터널은 Cloudflare 엣지를 거쳐야만 동작해요. 반드시 Cloudflare 프록시(Proxied)여야 해요(자동으로 그렇게 생성돼요).
Q. "전부 캐시" 규칙을 /diaries에 걸었다. 사용자 A의 일기 목록이 B에게 보이는 사고를 막으려면 Spring이 뭘 보내야 할까?
private/no-store면 공유 캐시(엣지)는 저장하지 않아요. 애초에 사용자별 API엔 캐시 규칙을 걸지 않는 게 가장 안전해요. Spring Security는 기본으로 no-cache, no-store를 붙여줘요.
Chapter 7

브라우저 보안 모델: CORS와 쿠키

FE와 API 도메인이 달라지는 순간 반드시 만나는 것.

Origin과 Site

  • Origin = 스킴 + 호스트 + 포트. 하나라도 다르면 다른 origin
  • Site = 등록 가능한 도메인(PSL 기준). api.ssobbs13.pp.ua와 ssobbs13.pp.ua는 같은 site

Origin / Site 판별기

CORS

브라우저는 기본적으로 다른 origin의 응답을 JS가 못 읽게 막아요. CORS는 서버가 "이 origin은 읽어도 돼"라고 허락하는 헤더예요.

Access-Control-Allow-Origin: https://ssobbs13.pp.ua
Access-Control-Allow-Credentials: true

PUT/DELETE, JSON 본문, Authorization 헤더를 쓰면 브라우저가 먼저 OPTIONS 요청(preflight)을 보내요. Spring Security가 OPTIONS를 막으면 CORS 에러가 나요.

CORS는 서버 보안이 아니다브라우저만 지키는 규칙이에요. curl이나 Postman은 무시해요. 인증과 권한은 Spring Security가 따로 해야 해요.

Preflight 시뮬레이터: 이 fetch는 OPTIONS를 먼저 보낼까?


    

Spring Security에서 CORS 켜기 (Kotlin)

@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
    val config = CorsConfiguration().apply {
        allowedOrigins = listOf("https://ssobbs13.pp.ua", "http://localhost:5173")
        allowedMethods = listOf("GET", "POST", "PUT", "PATCH", "DELETE")
        allowedHeaders = listOf("Authorization", "Content-Type")
        allowCredentials = true      // 쿠키를 쓴다면
        maxAge = 3600                // preflight 결과를 1시간 캐시
    }
    return UrlBasedCorsConfigurationSource().apply { registerCorsConfiguration("/**", config) }
}

@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain = http
    .cors { }                         // 위 Bean을 사용. 인증 필터보다 먼저 OPTIONS를 처리해요
    .build()
자주 하는 실수① allowCredentials = true인데 allowedOrigins = listOf("*") → Spring이 시작할 때 막아요. 와일드카드가 필요하면 allowedOriginPatterns. ② origin 끝에 /를 붙임(https://ssobbs13.pp.ua/) → 불일치. ③ @CrossOrigin만 붙이고 Security에 .cors { }를 안 켬 → preflight가 401로 막혀요.

쿠키 속성

속성뜻
Domain=ssobbs13.pp.ua서브도메인끼리 공유
SameSite=Lax같은 site 요청 + 링크 이동 시 전송 (기본값)
SameSite=None; Secure다른 site에도 전송 (서드파티)
HttpOnlyJS에서 못 읽음 (XSS 방어)
SecureHTTPS에서만 전송
그래서FE를 pages.dev가 아니라 ssobbs13.pp.ua 커스텀 도메인에 두면, API와 같은 site가 되어 쿠키 인증이 훨씬 쉬워져요.

CSRF: 쿠키가 자동으로 따라가서 생기는 문제

쿠키는 요청을 누가 시켰든 브라우저가 알아서 붙여요. 악성 사이트가 <form action="https://api.ssobbs13.pp.ua/diaries/7" method="POST">를 몰래 제출하면, 로그인된 사용자의 쿠키가 함께 가요. 이게 CSRF예요. CORS는 응답을 읽는 것만 막고 요청 전송은 막지 않아서 CSRF를 못 막아요.

인증 방식CSRF 위험대책
쿠키 세션 / 쿠키에 JWT있음SameSite=Lax(cross-site POST에 쿠키 안 보냄) + Spring Security CSRF 토큰
Authorization: Bearer 헤더없음브라우저가 자동으로 붙이지 않아서 안전. 대신 토큰 보관 위치가 XSS에 노출돼요 (암호학 챕터 참고)

주의: api.ssobbs13.pp.ua와 evil.ssobbs13.pp.ua는 same-site예요. SameSite는 같은 site 안의 다른 서브도메인은 막지 못해요. 서브도메인을 남에게 내주지 마세요.

Q. https://ssobbs13.pp.ua 페이지에서 https://api.ssobbs13.pp.ua를 fetch하면?
호스트가 달라서 cross-origin이지만, 등록 도메인이 같아서 same-site예요.
Q. fetch(url, {method:"POST", headers:{"Content-Type":"application/json"}, body}). 콘솔에 CORS 에러가 났는데 Network 탭엔 OPTIONS 401이 보인다. 원인은?
preflight에는 쿠키도 Authorization도 안 붙어요. 인증 필터보다 CORS 처리가 먼저 돼야 해요. Security 설정에 .cors { }를 켜세요.
Chapter 8

부하 분산과 가용성

커넥터 2개가 이미 레플리카 구조예요.

레플리카 시뮬레이터: 요청을 보내고 레플리카를 죽여보세요

레플리카를 눌러 죽이거나 살릴 수 있어요.
  • 헬스체크: /actuator/health를 주기적으로 찔러 죽은 레플리카를 빼요
  • SPOF: 그게 죽으면 전체가 죽는 지점. 커넥터 2개가 같은 서버라면 서버가 SPOF, DB가 1개면 DB가 SPOF
  • 무상태(stateless): 레플리카가 여럿이면 세션을 메모리에 두면 안 돼요 → JWT 또는 Redis 세션
터널 레플리카는 로드밸런서가 아니에요cloudflared 레플리카는 하나가 죽어도 다른 하나가 받아주는 고가용성용이에요. 트래픽이 반반 나뉜다는 보장은 없어요(보통 가까운 연결로 몰려요). 위 시뮬레이터는 개념 설명용이에요. 균등 분배가 필요하면 Cloudflare Load Balancer나 서버 앞 LB를 써요.

로드밸런싱 알고리즘

알고리즘방식잘 맞는 경우
라운드 로빈차례대로 하나씩요청 처리 시간이 비슷할 때 (기본값)
가중 라운드 로빈성능 좋은 서버에 더 많이서버 사양이 다를 때
최소 연결지금 연결이 가장 적은 서버로요청마다 처리 시간이 크게 다를 때 (파일 업로드, 긴 쿼리)
IP 해시 / 스티키 세션같은 사용자는 같은 서버로세션을 메모리에 둔 레거시. 새로 만든다면 stateless로 가세요

헬스체크 두 종류와 우아한 종료

  • Liveness (/actuator/health/liveness): "프로세스가 살아있나?" 실패하면 재시작
  • Readiness (/actuator/health/readiness): "지금 요청 받을 준비됐나?" 실패하면 트래픽에서 잠시 뺌 (기동 중, DB 연결 끊김)
  • Graceful shutdown: 배포할 때 처리 중인 요청을 끝내고 종료. Spring Boot 3.4부터 server.shutdown=graceful이 기본값이에요. 레플리카를 하나씩 교체하면 무중단 배포가 돼요.

가용성은 곱셈이에요

요청 경로에 있는 부품은 전부 살아있어야 해요(직렬: 가용성을 곱함). 같은 역할을 하는 부품을 둘 두면 둘 다 죽어야 멈춰요(병렬: 1 − 둘 다 죽을 확률). 어디를 이중화해야 효과가 큰지 직접 켜보세요.

가용성 계산기

가정: 엣지 99.99%, 커넥터 99.9%, 서버(VM) 99.5%, Spring 99.5%, DB 99.9%. 장애는 서로 독립이라고 단순화했어요.

Q. 레플리카 2개에서 로그인이 자꾸 풀린다. 원인으로 가장 유력한 건?
요청이 다른 레플리카로 가면 그 서버엔 세션이 없어요. JWT나 공유 세션 저장소(Redis)를 쓰세요.
Q. 커넥터 2개가 같은 OCI VM에 떠 있다. VM이 재부팅되면?
같은 서버의 레플리카는 프로세스 장애만 막아요. 서버 장애까지 막으려면 다른 VM(가능하면 다른 가용 영역)에 두세요. 가용성 계산기에서 "서버 2대로 분리"를 켜보면 차이가 보여요.
Chapter 9

보안 계층: 누가 무엇을 막나

여러 겹의 방어. 한 겹이 뚫려도 다음 겹이 막아요.

공격을 골라 어디서 막히는지 보세요

인증 vs 인가

  • 인증 (Authentication): 너 누구야? → 로그인, JWT
  • 인가 (Authorization): 너 이거 해도 돼? → RBAC (ROLE_USER, ROLE_ADMIN)
  • 객체 단위 권한: 이 일기가 네 거야? ← 가장 많이 놓치는 곳
IDOR 주의GET /diaries/123에서 123만 바꾸면 남의 일기가 보이는 취약점. 역할(RBAC)만으로는 못 막아요. 매 요청마다 "이 리소스 주인 == 현재 사용자"를 확인하세요.
// 나쁜 예: 인증만 확인
fun get(id: Long) = diaryRepository.findById(id).orElseThrow()

// 좋은 예: 조회 조건에 주인을 넣으면 남의 것은 "없음"(404)이 돼요
fun get(id: Long, @LoginUser user: User) =
    diaryRepository.findByIdAndOwnerId(id, user.id) ?: throw NotFoundException()

Rate Limiting: 토큰 버킷

가장 흔한 요청 제한 방식이에요. 버킷에 토큰이 일정 속도로 차고, 요청 하나가 토큰 하나를 써요. 토큰이 없으면 429 Too Many Requests. 버킷 크기만큼은 한꺼번에 몰려도 허용해요(burst).

토큰 버킷 체험 (용량 5, 초당 1개 충전)

보안 헤더: 브라우저에게 시키는 방어

헤더를 눌러보세요

터널 뒤의 Spring은 자기가 HTTP라고 생각해요cloudflared → Spring 구간이 http://라서, 설정 없이는 request.isSecure == false예요. 그러면 Spring Security가 HSTS 헤더를 안 붙이고, 리다이렉트 URL이 http://로 만들어져요. server.forward-headers-strategy=native를 켜면 X-Forwarded-Proto: https를 믿고 제대로 동작해요.

Cloudflare Access는?

엣지에서 동작하는 문지기예요(터널과 별개 제품). BE에 Spring Security로 인증과 권한이 제대로 있으면 필수가 아니라 추가 방어층이에요. 이번 프로젝트는 제거 예정이에요.

Q. 로그인한 사용자 A가 /diaries/999(B의 일기)를 조회할 수 있었다. 무엇이 빠졌나?
A는 인증됐고 ROLE_USER도 있어요. 빠진 건 "이 일기의 주인이 A인가" 체크예요.
Chapter 10

Cloudflare 제품 지도 (풀스택 관점)

제품을 눌러 상세를 보세요.

제품 카드

터널 vs ngrok vs 직접 오픈

Cloudflare Tunnelngrok직접 오픈 (Caddy)
서버 인바운드안 엶안 엶80/443 엶
DNSCloudflare 필수아무 데나 (CNAME)아무 데나 (A)
커스텀 도메인무료유료무료
서버 IP숨김숨김노출
DDoS 방어강함기본없음
BE 코드 변경셋 다 없음. BE는 localhost:8080만 알면 됨

터널 ingress 규칙: 위에서부터 첫 번째로 맞는 것

cloudflared는 들어온 요청의 호스트명과 경로를 규칙 목록과 위에서부터 비교해서, 처음 맞는 규칙의 서비스로 보내요. 마지막엔 반드시 아무것에나 맞는 catch-all 규칙이 있어야 해요. 대시보드의 "Published application routes"도 같은 규칙이에요.

ingress 매칭기 (규칙을 고쳐봐도 돼요)

형식: 한 줄에 호스트명 | 경로정규식 | 서비스. 비워두면 "아무거나". 경로는 정규식이라 ^/actuator처럼 써요.

actuator는 밖에 내보내지 마세요/actuator/env, /actuator/heapdump는 설정값과 메모리 속 비밀을 그대로 보여줄 수 있어요. 위 예시처럼 ingress에서 http_status:404로 막거나, management.server.port를 따로 두고 터널에 연결하지 마세요.
핵심 원리인터넷에서 접속하려면 어딘가에는 반드시 인바운드가 열린 공인 서버가 있어야 해요. 터널은 인바운드를 없애는 게 아니라 남의 엣지가 대신 열어주는 것이에요.
Chapter 11

내 프로젝트: 전환 전과 후

diary 프로젝트의 실제 구성 (대시보드와 저장소에서 확인한 내용).

단계별 구조 비교

전환 작업 체크리스트

최종 구조의 서버 구성 (예시)

# docker-compose.yml (OCI 서버)
services:
  be:
    image: diary-be:latest
    environment:
      SERVER_FORWARD_HEADERS_STRATEGY: native   # X-Forwarded-* 신뢰
    # ports 없음! 밖으로 포트를 공개하지 않아요
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
      interval: 10s

  cloudflared:
    image: cloudflare/cloudflared:latest
    command: tunnel --no-autoupdate run
    environment:
      TUNNEL_TOKEN: ${TUNNEL_TOKEN}              # .env 파일, 깃에 올리지 않기
    network_mode: "service:be"                   # be와 네트워크 공유 → localhost:8080으로 닿음
    depends_on:
      be:
        condition: service_healthy
    restart: unless-stopped

왜 network_mode를 쓰는지, ports:를 왜 빼는지는 "Docker 네트워크" 챕터에서 자세히 다뤄요.

요청 비용 비교: 전환 전 vs 최종

Chapter 12

문제 생겼을 때: 도구와 에러 코드

명령어 (터미널에서 실행)

dig +short api.ssobbs13.pp.uaDNS가 어떤 IP를 주는지 봐요. Cloudflare IP(104.x, 172.67.x)면 정상이에요.
dig +short NS ssobbs13.pp.ua네임서버가 누구인지 봐요. cash / virginia.ns.cloudflare.com이 나와야 해요.
curl -v https://api.ssobbs13.pp.ua/actuator/health연결, TLS, 헤더, 상태 코드를 한 번에 볼 수 있어요. 가장 많이 쓰는 도구예요.
curl -I -X OPTIONS https://api.ssobbs13.pp.ua/diaries -H "Origin: https://ssobbs13.pp.ua" -H "Access-Control-Request-Method: POST"CORS preflight 응답에 Access-Control-Allow-Origin이 있는지 확인해요.
dig @1.1.1.1 api.ssobbs13.pp.ua vs dig @cash.ns.cloudflare.com api.ssobbs13.pp.ua리졸버 답(캐시일 수 있음)과 권한 네임서버의 원본 답을 비교해요. 다르면 아직 캐시가 안 풀린 거예요. +trace를 붙이면 루트부터 따라가는 과정이 다 보여요.
openssl s_client -connect api.ssobbs13.pp.ua:443 -servername api.ssobbs13.pp.ua </dev/null | openssl x509 -noout -subject -issuer -dates -ext subjectAltName실제로 받은 인증서의 이름(SAN), 발급자, 만료일을 봐요. -servername(SNI)을 빼면 다른 인증서가 올 수 있어요.
curl -w "dns:%{time_namelookup} tcp:%{time_connect} tls:%{time_appconnect} ttfb:%{time_starttransfer} total:%{time_total}\n" -o /dev/null -s https://api.ssobbs13.pp.ua/actuator/health느린 게 DNS인지, 연결인지, TLS인지, 서버 처리(TTFB)인지 구간별로 쪼개서 봐요. 각 값은 시작부터 누적된 초예요.
ss -ltnp (서버에서)어떤 프로세스가 어떤 포트를 듣고 있는지. 127.0.0.1:8080이면 서버 안에서만, 0.0.0.0:8080이면 모든 인터페이스에서 받아요.
docker compose logs -f cloudflared / docker compose ps터널이 엣지에 붙었는지(Registered tunnel connection), 컨테이너가 healthy인지 확인해요.
브라우저 개발자 도구 → Network실패한 요청을 누르면 상태 코드, 응답 헤더(cf-ray, cf-cache-status), preflight 여부가 보여요. CORS 에러는 콘솔에만 이유가 나와요. 요청을 우클릭하면 "Copy as cURL"로 터미널에서 그대로 재현할 수 있어요.

진단 흐름도: 질문에 답하면서 원인 좁히기

무엇이 문제일까?

Cloudflare 에러 코드 검색

Q. curl로 API를 부르면 200이 잘 오는데, 브라우저 화면에서만 실패한다. 가장 먼저 볼 곳은?
curl이 된다는 건 DNS·연결·TLS·서버가 정상이라는 뜻이에요. 남는 건 브라우저만 지키는 규칙이에요.
Q. 엑셀 내보내기 API가 딱 100초쯤에 Cloudflare 524 에러를 낸다. 올바른 해결은?
100초는 Cloudflare 엣지가 오리진 응답을 기다리는 한도예요(무료 플랜은 못 바꿔요). 오래 걸리는 작업은 요청-응답 한 번에 끝내지 말고 작업 ID를 돌려주는 구조로 바꿔요.
Chapter

OSI 계층 모델: 네트워크의 지도

지금까지 배운 것 대부분이 L7(HTTP)이었어요. 이 챕터는 그 전체를 한 장의 지도로 정리해요.

왜 계층으로 나누나각 계층은 바로 아래 계층만 믿고 자기 일만 해요. HTTP는 TCP가 데이터를 순서대로 전달해준다고 믿고, TCP는 IP가 목적지까지 패킷을 옮겨준다고 믿어요. 그래서 Spring 개발자는 광케이블을 몰라도 API를 만들 수 있어요.

계층을 눌러 살펴보세요

실무에서는 L5/L6를 따로 나누지 않고 TCP/IP 4계층(응용 · 전송 · 인터넷 · 네트워크 접근)으로 보는 경우가 많아요. 대화에서 가장 자주 나오는 건 L3(IP) · L4(포트/TCP) · L7(HTTP) 세 개예요.

캡슐화: 요청이 계층을 내려갔다 올라오는 과정

캡슐화 애니메이션

L4와 L7: 무엇을 볼 수 있나

L4 장비가 보는 것:  203.0.113.7:51234 → 104.21.5.9:443  (내용은 암호화된 바이트 덩어리)
L7 장비가 보는 것:  GET /diaries/123  Host: api.ssobbs13.pp.ua  Authorization: Bearer …
구성 요소계층이유
OCI Security List / iptablesL3/L4IP와 포트로만 허용/차단
Cloudflare DDoS 방어L3/L4 L7패킷 폭탄(L3/4)과 HTTP 요청 폭탄(L7) 둘 다
Cloudflare 엣지 (Proxied 레코드)L7TLS를 풀고 HTTP를 읽어서 캐시·규칙 적용
WAF · Rate Limiting · AccessL7URL, 헤더, 본문을 봐야 판단 가능
Tunnel 호스트명 라우팅L7Host 헤더로 ingress 규칙 매칭
Tunnel 연결 자체L4cloudflared ↔ 엣지 QUIC(UDP 7844) 연결
Spring Security · CORSL7전부 HTTP 헤더 기반
실제 사용자 IP는 L7 헤더에L7 프록시(Cloudflare → cloudflared)를 거치면 Spring이 보는 접속 IP(L3)는 127.0.0.1이에요. 진짜 사용자 IP는 CF-Connecting-IP 또는 X-Forwarded-For 헤더에서 꺼내야 해요. IP 기준 Rate Limit이나 로그에 꼭 필요해요. Spring Boot에서는 server.forward-headers-strategy=native 설정을 알아두세요.

증상 진단기: 몇 계층 문제일까?

Q. 로그인 API를 초당 수천 번 정상 HTTP 요청으로 두드리는 공격. OCI 방화벽(L4)으로 막을 수 있을까?
L4는 포트와 IP만 봐요. 요청 내용과 빈도를 봐야 하는 L7 공격은 WAF나 Rate Limiting이 막아요.
Chapter

TCP, UDP, HTTP 버전, 그리고 지연시간

"왜 엣지가 가까우면 빠를까?"를 숫자로 이해하는 챕터.

TCP vs UDP

TCPUDP
연결3-way handshake로 연결 후 전송연결 없이 바로 전송
보장순서 보장, 유실 시 재전송보장 없음 (앱이 알아서)
속도느림 (확인 절차)빠름
용도HTTP/1.1, HTTP/2, DB, SSHDNS, 영상통화, 게임, QUIC(HTTP/3)

TCP 3-way handshake

RTT: 지연시간의 기본 단위

RTT(Round Trip Time)는 패킷이 갔다 오는 시간이에요. 서울↔서울 엣지는 수 ms, 서울↔미국은 150ms 이상이에요. 빛의 속도는 못 이기니까, 왕복 횟수를 줄이거나 거리를 줄이는 것이 성능 최적화의 핵심이에요.

첫 요청 지연시간 계산기

CDN이 빠른 이유TCP/TLS 핸드셰이크 왕복은 가까운 엣지(RTT 수 ms)와 해요. 엣지 ↔ 오리진 구간은 이미 열린 연결을 재사용해서, 먼 거리를 왕복하는 횟수가 1번으로 줄어요.

HTTP 버전

버전전송특징
HTTP/1.1TCP연결당 요청 하나씩 (keep-alive로 재사용). 앞 요청이 막히면 뒤도 대기 (HOL blocking)
HTTP/2TCP한 연결에 여러 요청 동시 전송(멀티플렉싱), 헤더 압축. 하지만 TCP 패킷 하나가 유실되면 전부 대기
HTTP/3QUIC (UDP)TCP의 HOL 문제 해결, 핸드셰이크와 TLS를 합쳐 더 빠름. 모바일 네트워크 전환에도 강함

HOL blocking 체험: 패킷을 눌러 유실시켜 보세요

요청 세 개(A: 일기 목록, B: 프로필, C: 이미지)가 한 연결에서 패킷 3개씩 번갈아 도착해요.

우리 구조에서브라우저 ↔ Cloudflare는 HTTP/2나 HTTP/3(자동), cloudflared ↔ Cloudflare는 QUIC(UDP 7844)이에요. 서버 방화벽이 아웃바운드 UDP를 막으면 cloudflared는 HTTP/2(TCP 7844)로 자동 전환해요. cloudflared → Spring은 평범한 HTTP/1.1이에요.

연결 재사용과 타임아웃 (백엔드 입장)

핸드셰이크가 비싸니까 연결은 재사용하는 게 기본이에요. Spring이 다른 API나 DB를 부를 때도 같아요. DB는 HikariCP가 연결을 풀(pool)로 들고 있고, HTTP 클라이언트도 커넥션 풀을 써요.

타임아웃무엇을 기다리나안 걸면 생기는 일
connect timeoutTCP(+TLS) 연결이 맺어지기까지상대 서버가 방화벽에 막혀 조용하면 OS 기본값(수십 초~2분)만큼 멈춤
read (socket) timeout연결 후 응답 바이트가 오기까지상대가 느리면 스레드가 무한정 묶임 → 스레드 풀 고갈 → 내 서버도 죽음
pool acquire timeout풀에서 빈 연결을 받기까지 (Hikari connectionTimeout, 기본 30초)느린 쿼리가 연결을 다 잡고 있으면 모든 요청이 줄 서서 대기
타임아웃은 바깥이 안쪽보다 길어야 해요브라우저 → Cloudflare(100초) → Spring → 외부 API. Spring의 외부 API read timeout이 120초면, Cloudflare는 이미 524를 줬는데 Spring은 계속 일하고 있어요. 안쪽으로 갈수록 짧게 두세요.
Q. HTTP/2에서 요청 A의 패킷 하나가 유실됐다. 요청 B와 C는?
HTTP/2는 요청을 나눠 보내지만, 그 아래 TCP는 하나의 바이트 줄이에요. 앞 바이트가 없으면 뒤 바이트를 앱에 못 넘겨요. QUIC은 스트림별로 순서를 관리해서 B, C는 계속 진행돼요.
Q. HTTP/3가 사용하는 전송 계층 프로토콜은?
HTTP/3는 UDP 위의 QUIC을 써요. 재전송과 순서 보장은 QUIC이 스트림 단위로 직접 해요.
Chapter

HTTP 의미론: 메서드, 상태 코드, 헤더

L7의 언어. API 설계와 캐시, 재시도, 에러 처리가 전부 여기서 나와요.

메서드 속성 탐색기

  • 안전(Safe): 서버 상태를 안 바꿈 → 크롤러, 프리페치가 마음대로 호출해도 됨
  • 멱등(Idempotent): 여러 번 해도 결과가 같음 → 네트워크 오류 시 자동 재시도해도 안전
  • 캐시 가능: 엣지나 브라우저가 응답을 저장해도 됨
POST 중복 문제POST는 멱등이 아니라서, 타임아웃 후 재시도하면 일기가 두 번 저장될 수 있어요. 결제처럼 중요한 곳에서는 Idempotency-Key 헤더로 중복을 막아요.

상태 코드 사전

401 vs 403 (Spring Security에서 자주 헷갈림)401 = "너 누군지 모르겠어" (로그인 필요). 403 = "누군지 아는데 넌 안 돼" (권한 부족). 남의 일기 조회(IDOR)는 403이나, 존재 자체를 숨기려면 404를 주기도 해요.

꼭 알아야 할 헤더

헤더방향역할
Host요청어느 도메인에 대한 요청인지. 터널 ingress 매칭의 기준
Authorization요청Bearer <JWT> 형태의 인증 정보
Cookie / Set-Cookie요청 / 응답세션 쿠키 주고받기
Content-Type양쪽본문 형식 (application/json)
Cache-Control응답캐시 가능 여부와 기간. API는 no-store나 private
Origin요청CORS 판단 기준. 브라우저가 자동으로 붙임
CF-Connecting-IP / X-Forwarded-For요청프록시 뒤에서 진짜 사용자 IP
CF-Ray응답Cloudflare 요청 추적 ID. 문제 신고와 로그 검색에 사용

HTTP 요청은 그냥 텍스트예요

HTTP/1.1 요청은 사람이 읽을 수 있는 텍스트예요(HTTP/2, 3는 같은 내용을 바이너리로 압축할 뿐 의미는 같아요). 옵션을 바꾸면서 실제로 어떤 텍스트가 전송되는지 보세요.

요청 원문 빌더

전송되는 원문
같은 요청을 curl로

조건부 요청: ETag와 304

ETag 흐름 따라가기

Cache-Control 값뜻어디에 쓰나
no-store어디에도 저장하지 마개인정보, 일기 본문
no-cache저장은 해도 되지만 쓸 때마다 서버에 확인(ETag)자주 바뀌는 공개 데이터, index.html
private브라우저만 저장, 엣지·프록시는 금지로그인 사용자별 응답
public, max-age=31536000, immutable1년간 확인 없이 사용파일명에 해시가 붙은 JS/CSS (app.3f9a.js)
s-maxage=60공유 캐시(엣지)용 max-age엣지에서만 잠깐 캐시하고 싶을 때
no-cache ≠ 캐시 안 함이름이 헷갈리지만 no-cache는 "매번 확인하고 써"예요. 진짜 저장 금지는 no-store예요.

HTTP는 무상태(stateless)

HTTP 요청은 서로를 기억하지 못해요. 로그인 상태를 유지하려면 매 요청마다 쿠키(세션 ID)나 토큰(JWT)을 들고 와야 해요. 이게 8장의 레플리카와 세션 문제, 7장의 쿠키 이야기로 이어져요.

Q. 네트워크가 끊겨 응답을 못 받았다. 자동 재시도해도 안전한 요청은?
PUT은 멱등이라 여러 번 해도 결과가 같아요. POST는 일기가 중복 생성될 수 있어요.
Chapter

암호학 기초: TLS와 JWT를 이해하기 위한 최소한

수학은 몰라도 돼요. 도구 네 가지가 각각 무엇을 보장하는지만 알면 돼요.

도구보장하는 것비유우리 프로젝트에서
해시무결성 (바뀌었는지 확인), 되돌릴 수 없음지문비밀번호 저장 (BCrypt)
대칭키 암호기밀성. 같은 키로 잠그고 품집 열쇠TLS의 실제 데이터 암호화 (AES)
비대칭키 암호공개키로 잠그면 개인키로만 풀림우체통 (누구나 넣고, 주인만 꺼냄)TLS 키 교환
전자서명진위 (누가 썼는지), 위변조 방지도장인증서, JWT 서명

해시 체험 (SHA-256)

한 글자만 바꿔도 결과가 완전히 달라져요(눈사태 효과). 결과로 원문을 되돌릴 수 없어요.

비밀번호는 SHA-256으로 저장하면 안 돼요SHA-256은 너무 빨라서 무차별 대입에 약해요. Spring Security의 BCryptPasswordEncoder처럼 일부러 느리고 솔트(salt)를 섞는 전용 해시를 쓰세요. 같은 비밀번호라도 사용자마다 다른 결과가 나와요.

TLS 핸드셰이크 (단순화)

TLS 1.3 핸드셰이크 따라가기

비대칭키는 느려서 키 교환에만 쓰고, 실제 데이터는 빠른 대칭키로 암호화해요. 인증서의 서명이 "이 공개키는 진짜 api.ssobbs13.pp.ua의 것"임을 보증해요.

JWT: 서명된 출입증

JWT는 헤더.페이로드.서명 세 부분을 점으로 이은 문자열이에요. 앞의 두 부분은 암호화가 아니라 Base64 인코딩이라 누구나 읽을 수 있어요. 서명은 "서버가 발급했고 내용이 안 바뀌었다"를 보장할 뿐이에요.

JWT 해부기 (브라우저 안에서만 디코딩, 외부 전송 없음)

JWT 규칙① 페이로드에 비밀번호·개인정보를 넣지 마세요(누구나 읽음). ② 서명 키는 서버에만. ③ exp(만료)를 짧게. ④ 실제 JWT를 온라인 디코더 사이트에 붙여넣지 마세요.

토큰은 어디에 보관할까

보관 위치 비교

Access Token + Refresh Token

JWT는 서버가 기억하지 않아서 중간에 취소하기 어려워요. 그래서 수명을 짧게(5~15분) 하고, 새 토큰을 받는 데 쓰는 긴 수명의 Refresh Token을 따로 둬요. Refresh Token은 서버 DB에 저장해서 취소할 수 있게 해요.

Refresh Token 회전(rotation) 따라가기

Q. JWT 페이로드에 "role":"USER"가 있다. 사용자가 이걸 "ADMIN"으로 바꿔서 보내면?
내용은 누구나 바꿀 수 있지만, 서명 키가 없으니 올바른 서명을 만들 수 없어요. 서버가 서명을 검증하면 거부돼요. 그래서 서명 검증은 절대 생략하면 안 돼요.
Chapter

Docker 네트워크: 컨테이너 안의 localhost

"로컬에선 되는데 컨테이너에 넣으니 안 돼요"의 대부분이 이 챕터 내용이에요.

컨테이너 = 작은 가상 컴퓨터 한 대컨테이너마다 자기만의 네트워크(IP, 포트 목록, localhost)를 가져요. 그래서 컨테이너 A의 localhost:8080은 컨테이너 B의 8080과 전혀 상관없어요. 호스트 서버의 8080과도 상관없어요.

네트워크 모드

모드동작다른 컨테이너 부르기
기본 bridge (docker run)컨테이너마다 사설 IP(172.17.x.x)IP로만 가능. 이름으로는 안 됨
사용자 정의 bridge (compose 기본)compose 프로젝트마다 네트워크 하나를 자동 생성서비스 이름이 DNS 이름: http://be:8080
host호스트의 네트워크를 그대로 사용 (격리 없음)localhost:8080이 호스트의 8080
service:be다른 컨테이너의 네트워크를 공유 (같은 localhost)localhost:8080이 be의 8080
none네트워크 없음불가

cloudflared가 Spring을 찾는 세 가지 방법


    

포트 공개(ports:)는 누구에게 열리나

ports: "8080:8080"은 "호스트의 8080으로 들어온 걸 컨테이너 8080으로 넘겨"라는 뜻이에요. 앞의 호스트 쪽에 IP를 안 쓰면 0.0.0.0, 즉 모든 네트워크 인터페이스에 열려요.

포트 설정을 골라보세요

Docker는 ufw를 무시해요Docker는 포트를 공개할 때 iptables에 자기 규칙을 ufw보다 먼저 끼워 넣어요. 그래서 ufw deny 8080을 해도 ports: "8080:8080"이면 밖에서 접속돼요. 마지막 방어선은 OCI Security List뿐이에요. 터널 구조에서는 ports를 아예 쓰지 않는 게 정답이에요.

자주 겪는 증상

증상원인해결
Spring이 localhost:5432로 DB에 연결 실패DB가 다른 컨테이너jdbc:postgresql://db:5432/diary (서비스 이름)
컨테이너에서 호스트의 서비스에 닿아야 함컨테이너 localhost ≠ 호스트host.docker.internal (Linux는 extra_hosts: ["host.docker.internal:host-gateway"])
앱이 떴는데 포트 매핑으로도 접속 안 됨앱이 컨테이너 안에서 127.0.0.1에만 바인딩앱을 0.0.0.0에 바인딩 (Spring은 기본이 0.0.0.0)
cloudflared 로그에 connection refusedSpring이 아직 기동 중이거나 주소가 틀림depends_on: condition: service_healthy, 주소 재확인
Q. compose에 be와 db 서비스가 있다. be의 DB URL로 알맞은 것은?
같은 compose 네트워크에서는 서비스 이름이 DNS 이름이에요. localhost는 be 컨테이너 자신이고, 공인 IP를 쓰면 DB 포트를 밖에 열어야 해서 위험해요.
Q. 터널 구조에서 Spring 컨테이너의 ports: 설정은?
포트 공개는 호스트 바깥에서 들어올 때만 필요해요. cloudflared가 같은 네트워크에 있으면 필요 없고, 없애면 실수로 노출될 일도 없어요. 서버에서 직접 curl로 확인하고 싶다면 "127.0.0.1:8080:8080"까지만 허용하세요.
Chapter 13

공부 순서와 용어집

추천 학습 순서

기억할 것nginx와 SSL을 직접 못 해도 지금 구조에선 문제없어요. 대신 DNS, CORS, 쿠키, 인증은 어떤 호스팅을 쓰든 개발자가 직접 다뤄야 하니 여기에 먼저 투자하세요.

용어집