인프라 레벨 CORS 처리 - 앱을 건드리지 않고 해결하기
TL;DR
- CORS는 브라우저의 SOP 정책이며, 올바른 응답 헤더가 없으면 응답을 버린다.
- Reverse Proxy로 Same-Origin을 만들거나, 프록시/Ingress에서 CORS 헤더를 주입할 수 있다.
- 인프라 중앙에서 정책을 관리하면 마이크로서비스 환경에서 누락과 불일치를 줄인다.
1. 개념
애플리케이션을 수정하지 않고 프록시·Ingress·게이트웨이 등 인프라 계층에서 CORS 정책을 설정·주입하는 방식이다.
2. 배경
브라우저의 Same-Origin Policy 때문에 다른 Origin 호출은 차단되며, 서비스가 많아질수록 백엔드별 CORS 설정이 중복·누락되기 쉬워졌다.
3. 이유
CORS 정책을 중앙에서 통제해 변경 비용을 줄이고, 환경별 정책을 일관되게 적용하기 위해 인프라 레벨 처리가 필요하다.
4. 특징
Same-Origin을 만드는 Reverse Proxy 경로 분기, Preflight 대응과 헤더 주입, Nginx/Ingress/ALB/API Gateway 등 다양한 계층 적용이 핵심이다.
5. 상세 내용
인프라 레벨 CORS 처리 - 앱을 건드리지 않고 해결하기
작성일: 2026-02-26 카테고리: Infra / Network / Security 포함 내용: CORS, Reverse Proxy, Nginx, Kubernetes Ingress, ALB, API Gateway, Same-Origin, 인프라 CORS, Istio, Service Mesh
1. 먼저 복습: CORS가 뭔데?
1.1 Origin(출처)이 뭔가?
┌─────────────────────────────────────────────────────────────────┐
│ Origin(출처)이란 무엇인가? │
│ │
│ Origin = Protocol + Domain + Port │
│ │
│ 비유: "집 주소" │
│ ├── 시(市) = Protocol (https, http) │
│ ├── 구(區) = Domain (klpark.com) │
│ └── 동(洞) = Port (443, 3000, 8095) │
│ │
│ 시/구/동 중 하나라도 다르면 = 완전히 다른 집 = 다른 Origin │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 예시로 보는 Origin 비교 │ │
│ │ │ │
│ │ 기준: https://klpark.com │ │
│ │ (Protocol=https, Domain=klpark.com, Port=443) │ │
│ │ │ │
│ │ ✅ 같은 Origin: │ │
│ │ https://klpark.com/users ← 경로만 다름, Origin 동일 │ │
│ │ https://klpark.com/v1/api ← 경로만 다름, Origin 동일 │ │
│ │ │ │
│ │ ❌ 다른 Origin: │ │
│ │ http://klpark.com ← Protocol 다름 (http) │ │
│ │ https://klpark.com:3000 ← Port 다름 (3000) │ │
│ │ https://api.klpark.com ← Domain 다름 (서브도메인) │ │
│ │ https://google.com ← Domain 완전히 다름 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Port 보충 설명: │
│ ├── https의 기본 포트 = 443 (생략 가능) │
│ ├── http의 기본 포트 = 80 (생략 가능) │
│ ├── https://klpark.com = https://klpark.com:443 (같은 것) │
│ └── https://klpark.com:3000 은 443이 아니므로 다른 Origin! │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 브라우저의 보안 정책 (SOP)
┌─────────────────────────────────────────────────────────────────┐
│ SOP (Same-Origin Policy) = 동일 출처 정책 │
│ │
│ SOP란? │
│ "브라우저가 가진 보안 규칙" │
│ "다른 Origin의 응답은 기본적으로 차단한다" │
│ │
│ 비유: "남의 집 택배를 가져가면 안 된다" │
│ ├── 내 집(Origin A)으로 온 택배만 가져갈 수 있음 │
│ ├── 옆집(Origin B)으로 온 택배를 가져가려 하면 → 차단! │
│ └── 택배 기사(서버)는 이미 배달 완료. 가져가는 걸 막는 건 규칙 │
│ │
│ 핵심: SOP는 "서버"가 아니라 "브라우저"의 규칙이다! │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 왜 이런 규칙이 있나? │ │
│ │ │ │
│ │ 1. 사용자가 은행 사이트 로그인 (쿠키 저장됨) │ │
│ │ 2. 악성 사이트 방문 (evil.com) │ │
│ │ 3. evil.com이 은행 API를 호출 시도 │ │
│ │ → fetch("https://bank.com/api/내계좌") │ │
│ │ 4. 브라우저가 차단! (SOP 덕분에 안전) │ │
│ │ │ │
│ │ SOP가 없었다면? │ │
│ │ → 악성 사이트가 내 쿠키로 은행 API 마음대로 호출 │ │
│ │ → 내 계좌 정보 탈취 가능! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Postman이나 curl에서는 CORS 에러가 안 나는 이유: │
│ ├── SOP는 "브라우저"에만 있는 규칙 │
│ ├── Postman, curl = 브라우저가 아님 │
│ ├── 그래서 SOP 적용 안 됨 → 어디든 요청 가능 │
│ └── "CORS 에러 = 브라우저 에러"라는 점 꼭 기억! │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 CORS 에러의 본질
┌─────────────────────────────────────────────────────────────────┐
│ CORS 에러의 본질 │
│ │
│ 가장 중요한 사실: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "서버는 정상 응답했다. 브라우저가 응답을 버린 것이다." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 동작 흐름: │
│ │
│ 브라우저 (https://app.klpark.com) │
│ │ │
│ │ 1. API 요청 보냄 │
│ ├──────────────────────► 서버 (https://api.klpark.com) │
│ │ │ │
│ │ 2. 서버는 정상 응답! (200 OK) │ │
│ │◄────────────────────── │ │
│ │ │
│ │ 3. 브라우저: "응답 헤더를 확인해볼게..." │
│ │ Access-Control-Allow-Origin 헤더가... │
│ │ │
│ │ ├── 있고, 내 Origin이 포함됨 → ✅ 응답 사용 │
│ │ └── 없거나, 내 Origin 없음 → ❌ 응답 폐기! │
│ │ │
│ 해결 방법은 결국 하나: │
│ "응답에 올바른 CORS 헤더를 넣어주면 된다" │
│ │
│ CORS 헤더를 넣어주는 곳: │
│ ├── ① 백엔드 앱에서 (Spring, Express 등에서 설정) │
│ │ → backend-기본개념.md에서 다룬 내용 │
│ └── ② 인프라에서 (Nginx, Ingress, ALB 등에서 설정) │
│ → 이 문서에서 다루는 핵심! │
│ │
└─────────────────────────────────────────────────────────────────┘
2. 왜 인프라 레벨에서 CORS를 처리하는가?
2.1 백엔드에서 처리하는 것의 문제점
┌─────────────────────────────────────────────────────────────────┐
│ 백엔드 앱에서 CORS 처리 시 문제점 │
│ │
│ 마이크로서비스란? │
│ ├── 하나의 큰 앱을 작은 서비스 여러 개로 쪼갠 것 │
│ ├── 예: 사용자 서비스, 주문 서비스, 결제 서비스... │
│ └── 각각 독립적인 백엔드 앱 │
│ │
│ ❌ 각 서비스마다 CORS 설정을 반복해야 함: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 사용자 서비스 (user-service) │ │
│ │ └── CORS: allow https://app.klpark.com ← 설정 1 │ │
│ │ │ │
│ │ 주문 서비스 (order-service) │ │
│ │ └── CORS: allow https://app.klpark.com ← 설정 2 │ │
│ │ │ │
│ │ 결제 서비스 (payment-service) │ │
│ │ └── CORS: allow https://app.klpark.com ← 설정 3 │ │
│ │ │ │
│ │ 알림 서비스 (notification-service) │ │
│ │ └── CORS: allow https://app.klpark.com ← 설정 4 │ │
│ │ │ │
│ │ ... 서비스 10개면 10곳에서 동일한 설정 반복! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 발생하는 문제들: │
│ ├── 서비스 하나가 설정을 빼먹으면 → 프론트에서 갑자기 에러 │
│ ├── 프론트 도메인 변경 시 → 모든 서비스를 수정 + 재배포 │
│ ├── 서비스마다 CORS 설정이 미묘하게 다를 수 있음 │
│ └── 신규 서비스 추가 시 CORS 설정 빼먹기 쉬움 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 인프라에서 처리하면 좋은 점
┌─────────────────────────────────────────────────────────────────┐
│ 인프라에서 CORS 처리 시 장점 │
│ │
│ ✅ 한 곳에서 모든 CORS 정책 관리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 인프라 앞단 (Nginx / Ingress / ALB) │ │
│ │ └── CORS: allow https://app.klpark.com ← 설정 1곳만! │ │
│ │ │ │ │
│ │ ├──► 사용자 서비스 (CORS 모름) │ │
│ │ ├──► 주문 서비스 (CORS 모름) │ │
│ │ ├──► 결제 서비스 (CORS 모름) │ │
│ │ └──► 알림 서비스 (CORS 모름) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 한 곳만 수정하면 모든 서비스에 적용 │
│ ├── 백엔드 앱은 CORS를 몰라도 됨 (관심사 분리) │
│ ├── 프론트 도메인 변경 시 인프라 설정만 수정 │
│ ├── 신규 서비스 추가해도 CORS 걱정 없음 │
│ └── 일관된 CORS 정책 보장 │
│ │
│ 관심사 분리 = "각자 자기 일만 하자" │
│ ├── 백엔드 앱 → 비즈니스 로직에만 집중 │
│ └── 인프라 → CORS 같은 네트워크/보안 정책에 집중 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 “앞단에서 처리한다”는 개념
┌─────────────────────────────────────────────────────────────────┐
│ 비유: 아파트 건물의 경비실 │
│ │
│ ❌ 방법 1: 각 집마다 인터폰 (= 백엔드 앱마다 CORS) │
│ 각 집이 알아서 방문자 확인 → 비효율적! │
│ 102호는 깜빡하고 안 함 → 보안 구멍! │
│ │
│ ✅ 방법 2: 건물 입구에 경비실 (= 인프라 CORS) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 방문자 → 경비실 → "이 사람 OK!" → 각 집으로 통과 │ │
│ │ │ │
│ │ ┌────────────┐ │ │
│ │ │ 경비실 │ → "허용 목록에 있는 사람만 통과!" │ │
│ │ └────────────┘ │ │
│ │ ├──► 101호 (경비 신경 안 씀) │ │
│ │ ├──► 102호 (경비 신경 안 씀) │ │
│ │ └──► 103호 (경비 신경 안 씀) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실제 인프라 흐름: │
│ 브라우저 ──► [인프라 앞단] ──► 백엔드 서비스들 │
│ (CORS 헤더 추가) (CORS 모름, 비즈니스만 집중) │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 방법 1: Reverse Proxy로 “같은 Origin” 만들기 (가장 깔끔)
3.1 Reverse Proxy(역방향 프록시)가 뭔가?
┌─────────────────────────────────────────────────────────────────┐
│ Proxy(프록시)란 무엇인가? │
│ │
│ Proxy = 대리인. "요청을 대신 전달해주는 중간 서버" │
│ │
│ Forward Proxy (정방향): │
│ ├── 내가 프록시를 통해 바깥에 나감 (예: VPN) │
│ └── 서버는 내가 누군지 모름 │
│ │
│ ┌──────┐ ┌───────┐ ┌──────┐ │
│ │나(PC)│──►│Forward│──►│서버 │ │
│ └──────┘ │Proxy │ └──────┘ │
│ └───────┘ │
│ │
│ Reverse Proxy (역방향): │
│ ├── 바깥 요청이 프록시를 거쳐 내부 서버로 감 │
│ ├── 클라이언트는 프록시만 봄. 뒤에 서버가 몇 개인지 모름 │
│ ├── 비유: 회사 전화 교환원 │
│ │ 외부에서 전화 → 교환원이 받고 → 해당 부서로 연결 │
│ └── 예: Nginx, Apache, Caddy, HAProxy │
│ │
│ ┌──────┐ ┌───────┐ ┌──────┐ │
│ │브라 │──►│Reverse│──►│서버A │ │
│ │우저 │ │Proxy │──►│서버B │ │
│ └──────┘ └───────┘──►│서버C │ │
│ 클라이언트는 └──────┘ │
│ 서버A,B,C를 모름 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 이 방법의 핵심 아이디어
┌─────────────────────────────────────────────────────────────────┐
│ 핵심: Origin을 같게 만들어서 CORS 자체를 없앤다 │
│ │
│ 생각의 흐름: │
│ ├── CORS 에러가 나는 이유 = Origin이 다르기 때문 │
│ ├── "그러면 Origin을 같게 만들어버리면?" │
│ └── Reverse Proxy 하나를 앞에 세우고 경로로 분기하자! │
│ │
│ ❌ 변경 전 (Origin이 다름 → CORS 에러): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 브라우저 │ │
│ │ ├── https://app.klpark.com (프론트엔드) Origin A │ │
│ │ └── https://api.klpark.com/v1/users (API) Origin B │ │
│ │ │ │
│ │ Origin A ≠ Origin B → CORS 에러! 💥 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ✅ 변경 후 (Same Origin → CORS 자체가 없음): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 브라우저 │ │
│ │ ├── https://klpark.com/ (프론트엔드) │ │
│ │ └── https://klpark.com/v1/users (API) │ │
│ │ 둘 다 https://klpark.com = Same Origin! │ │
│ │ → CORS 자체가 발생하지 않음! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 어떻게? │
│ Reverse Proxy(Nginx)가 경로를 보고 분기: │
│ ├── / → 프론트엔드 서버로 보냄 │
│ └── /v1/ → 백엔드 API 서버로 보냄 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 Nginx 설정 예시 (초보자용 라인 바이 라인 설명)
┌─────────────────────────────────────────────────────────────────┐
│ Nginx 설정 - Same Origin 만들기 │
│ │
│ Nginx = 가장 많이 쓰이는 웹 서버 / Reverse Proxy ("엔진엑스") │
│ │
│ 설정 파일 (nginx.conf): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ server { │ │
│ │ listen 80; │ │
│ │ # ↑ 80번 포트에서 요청을 받겠다 │ │
│ │ # (HTTP의 기본 포트가 80번) │ │
│ │ │ │
│ │ server_name klpark.com; │ │
│ │ # ↑ klpark.com으로 오는 요청을 이 설정으로 처리 │ │
│ │ │ │
│ │ # 일반 경로 → 프론트엔드로 보냄 │ │
│ │ location / { │ │
│ │ proxy_pass http://frontend:3000; │ │
│ │ # ↑ 이 요청을 frontend 서버의 3000포트로 전달 │ │
│ │ } │ │
│ │ │ │
│ │ # /v1/ 경로 → 백엔드 API로 보냄 │ │
│ │ location /v1/ { │ │
│ │ proxy_pass http://backend:8095; │ │
│ │ # ↑ 이 요청을 backend 서버의 8095포트로 전달 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 용어 설명: │
│ ├── server { } = 하나의 가상 서버 설정 블록 │
│ ├── listen 80 = 80번 포트에서 요청을 받겠다 │
│ ├── server_name = 이 도메인 요청을 여기서 처리 │
│ ├── location = 경로에 따라 다른 처리를 하겠다 │
│ ├── proxy_pass = 요청을 지정한 서버로 전달해줘 │
│ └── frontend:3000 = Docker/K8s에서 서비스 이름으로 접근 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 이 방법의 전체 그림
┌─────────────────────────────────────────────────────────────────┐
│ Same Origin 아키텍처 - 전체 요청 흐름 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 브라우저에서 보이는 주소: │ │
│ │ ├── 프론트 페이지: https://klpark.com/ │ │
│ │ └── API 호출: https://klpark.com/v1/users │ │
│ │ → 둘 다 klpark.com = Same Origin! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실제 내부 동작: │
│ │
│ https://klpark.com │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Nginx │ ← Reverse Proxy │
│ │ (앞단) │ │
│ └────────────┘ │
│ │ │ │
│ 경로: / │ │ 경로: /v1/ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Frontend │ │ Backend │ │
│ │ :3000 │ │ :8095 │ │
│ │ (React) │ │ (Spring) │ │
│ └──────────┘ └──────────┘ │
│ │
│ 브라우저 입장: "나는 klpark.com하고만 통신하고 있어" │
│ 실제: Nginx가 경로에 따라 서로 다른 서버로 분기 │
│ 결과: CORS 설정 자체가 불필요! │
│ │
└─────────────────────────────────────────────────────────────────┘
3.5 장점과 한계
┌─────────────────────────────────────────────────────────────────┐
│ 방법 1의 장점과 한계 │
│ │
│ ✅ 장점: │
│ ├── CORS 설정 자체가 불필요 (가장 깔끔한 해결) │
│ ├── Preflight 요청도 없음 (성능 이점) │
│ ├── 백엔드에 CORS 관련 코드 0줄 │
│ └── 설정이 단순하고 이해하기 쉬움 │
│ │
│ ❌ 한계: │
│ ├── 프론트와 백엔드가 같은 도메인이어야 함 │
│ ├── 경로 규칙 설계 필요 (/v1/은 API, 나머지는 프론트) │
│ ├── API와 프론트를 별도 도메인으로 운영해야 하는 경우 불가 │
│ └── 여러 프론트엔드 앱이 같은 API를 쓸 때 복잡해질 수 있음 │
│ │
│ 적합한 상황: │
│ ├── 프론트 1개 + 백엔드 1개 구성 │
│ ├── Docker Compose로 로컬/소규모 운영 │
│ └── 단일 도메인으로 서비스 가능한 경우 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. 방법 2: Reverse Proxy에서 CORS 헤더 주입
4.1 언제 이 방법을 쓰나?
┌─────────────────────────────────────────────────────────────────┐
│ 방법 2가 필요한 상황 │
│ │
│ 프론트와 백엔드가 반드시 다른 도메인이어야 할 때: │
│ ├── 프론트: https://app.klpark.com │
│ ├── API: https://api.klpark.com │
│ └── 서브도메인이 달라도 = 다른 Origin! │
│ │
│ 이런 경우가 생기는 이유: │
│ ├── CDN으로 프론트 배포 (Vercel 등), API는 별도 인프라 │
│ ├── 여러 프론트가 같은 API 공유 (웹, 관리자 페이지 등) │
│ └── 조직 정책으로 도메인 분리 필수 │
│ │
│ 이 경우: Same Origin 불가 → CORS 헤더가 필요! │
│ → 인프라 앞단(Nginx)에서 CORS 헤더를 주입하자 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 먼저 알아야 할 것: Preflight 요청
┌─────────────────────────────────────────────────────────────────┐
│ Preflight(사전 요청)란 무엇인가? │
│ │
│ Preflight = "본 요청 보내기 전에 먼저 허락을 구하는 것" │
│ 비유: 식당에 전화해서 "자리 있나요?" 확인 후 방문 │
│ │
│ 브라우저가 Preflight를 보내는 조건: │
│ ├── Content-Type이 application/json일 때 │
│ ├── Authorization 헤더가 있을 때 │
│ ├── PUT, DELETE 등의 메서드를 쓸 때 │
│ └── 즉, 대부분의 실무 API 호출에서 발생! │
│ │
│ Preflight 동작: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 브라우저 ──── OPTIONS /v1/users ────► 서버 │ │
│ │ (자동) "이 요청 보내도 되나요?" │ │
│ │ "POST 메서드, JSON 보낼건데요" │ │
│ │ │ │
│ │ 브라우저 ◄─── 204 No Content ─────── 서버 │ │
│ │ "네, POST OK, JSON OK" │ │
│ │ Access-Control-Allow-Origin: ... │ │
│ │ Access-Control-Allow-Methods: ... │ │
│ │ │ │
│ │ 브라우저 ──── POST /v1/users ──────► 서버 │ │
│ │ (실제) {"name": "Kim", ...} ← 이제야 본 요청! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ OPTIONS 메서드 = "이 요청 허용하나요?" 물어보는 HTTP 메서드 │
│ ├── 브라우저가 자동으로 보냄 (개발자가 코딩하는 게 아님) │
│ └── Max-Age: 3600 → 1시간 동안 Preflight 생략 (캐싱) │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 Nginx에서 CORS 헤더 추가 설정
┌─────────────────────────────────────────────────────────────────┐
│ Nginx에서 CORS 헤더 주입 설정 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ location /v1/ { │ │
│ │ │ │
│ │ # ── Preflight (OPTIONS) 요청 처리 ── │ │
│ │ if ($request_method = 'OPTIONS') { │ │
│ │ add_header 'Access-Control-Allow-Origin' │ │
│ │ 'https://app.klpark.com'; │ │
│ │ add_header 'Access-Control-Allow-Methods' │ │
│ │ 'GET, POST, PUT, DELETE'; │ │
│ │ add_header 'Access-Control-Allow-Headers' │ │
│ │ 'Content-Type, Authorization'; │ │
│ │ add_header 'Access-Control-Max-Age' 3600; │ │
│ │ return 204; # 본문 없이 OK │ │
│ │ } │ │
│ │ │ │
│ │ # ── 실제 요청에도 CORS 헤더 추가 ── │ │
│ │ add_header 'Access-Control-Allow-Origin' │ │
│ │ 'https://app.klpark.com'; │ │
│ │ add_header 'Access-Control-Allow-Credentials' │ │
│ │ 'true'; │ │
│ │ proxy_pass http://backend:8095; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 각 헤더의 의미: │
│ ├── Allow-Origin = 이 Origin의 요청을 허용한다 │
│ ├── Allow-Methods = 이 HTTP 메서드들을 허용한다 │
│ ├── Allow-Headers = 이 헤더들을 보내도 된다 │
│ ├── Max-Age = 1시간 동안 Preflight 캐시 가능 │
│ ├── Allow-Credentials = 쿠키/인증 정보도 보낼 수 있음 │
│ │ (true 시 Allow-Origin에 * 사용 불가! 구체적 Origin 필수) │
│ └── return 204 = 본문 없이 OK (Preflight는 확인만 하는 것) │
│ │
│ 주의: Preflight와 실제 요청 둘 다 헤더가 필요하다! │
│ ├── OPTIONS에만 넣으면 → 실제 응답에 헤더 없어서 차단 │
│ └── 실제 응답에만 넣으면 → Preflight 자체가 실패 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 Preflight 포함 전체 흐름
┌─────────────────────────────────────────────────────────────────┐
│ CORS 헤더 주입 - 전체 요청 흐름 │
│ │
│ 1단계: Preflight (브라우저가 자동으로 보냄) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 브라우저 Nginx Backend │ │
│ │ │ │ │ │ │
│ │ │──OPTIONS /v1/users──► │ │ │ │
│ │ │ │ (백엔드까지 │ │ │
│ │ │◄──204 + CORS 헤더── │ 안 감!) │ │ │
│ │ │ │ │ │ │
│ │ "OK, 허용됐구나!" │ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2단계: 실제 요청 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 브라우저 Nginx Backend │ │
│ │ │ │ │ │ │
│ │ │──POST /v1/users────► │ │ │ │
│ │ │ │──요청 전달──►│ │ │
│ │ │ │◄──응답──────│ │ │
│ │ │◄──응답 + CORS 헤더── │ │ │ │
│ │ │ (Nginx가 헤더 추가) │ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 포인트: │
│ ├── Preflight는 Nginx에서 바로 응답 (백엔드까지 안 감) │
│ ├── 실제 요청은 백엔드로 전달 후, 응답에 CORS 헤더 추가 │
│ └── 백엔드는 CORS에 대해 아무것도 모름 (관심사 분리) │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 방법 3: Kubernetes Ingress에서 CORS 처리
5.1 Kubernetes Ingress가 뭔가?
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes 관련 용어 정리 (처음부터) │
│ │
│ Kubernetes(K8s) = 컨테이너 여러 개를 관리하는 시스템 │
│ Pod = 앱이 돌아가는 컨테이너 (K8s 최소 배포 단위) │
│ Service = Pod에 접근하는 내부 주소 (고정 도메인) │
│ (상세: k8s-기본개념.md 참고) │
│ │
│ Ingress(인그레스)란? │
│ ├── "외부 HTTP 요청을 내부 Service로 연결하는 문" │
│ ├── 도메인/경로를 보고 어떤 Service로 보낼지 결정 │
│ ├── 비유: 공항 입국 심사대 │
│ │ 여권(도메인/경로)을 보고 어디로 보낼지 결정 │
│ └── Ingress Controller = Ingress 규칙을 실행하는 SW │
│ (보통 Nginx Ingress Controller → 결국 Nginx!) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 외부 요청 ──► Ingress ──► Service ──► Pod (앱) │ │
│ │ (브라우저) (문) (내부주소) (컨테이너) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 Ingress에서 CORS 설정
┌─────────────────────────────────────────────────────────────────┐
│ Kubernetes Ingress CORS 설정 │
│ │
│ YAML이란? │
│ ├── 설정 파일 형식 (JSON과 비슷하지만 더 읽기 쉬움) │
│ └── Kubernetes의 모든 설정은 YAML로 작성 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ apiVersion: networking.k8s.io/v1 │ │
│ │ kind: Ingress │ │
│ │ metadata: │ │
│ │ name: api-ingress │ │
│ │ annotations: # ← 추가 설정을 메모처럼 적는 곳 │ │
│ │ nginx.ingress.kubernetes.io/enable-cors: "true" │ │
│ │ nginx.ingress.kubernetes.io/cors-allow-origin: │ │
│ │ "https://app.klpark.com" │ │
│ │ nginx.ingress.kubernetes.io/cors-allow-methods: │ │
│ │ "GET, POST, PUT, DELETE" │ │
│ │ nginx.ingress.kubernetes.io/cors-allow-headers: │ │
│ │ "Content-Type, Authorization" │ │
│ │ nginx.ingress.kubernetes.io/cors-allow-credentials: │ │
│ │ "true" │ │
│ │ nginx.ingress.kubernetes.io/cors-max-age: "3600" │ │
│ │ spec: # ← 실제 라우팅 규칙 │ │
│ │ rules: │ │
│ │ - host: api.klpark.com │ │
│ │ http: │ │
│ │ paths: │ │
│ │ - path: / │ │
│ │ pathType: Prefix │ │
│ │ backend: │ │
│ │ service: │ │
│ │ name: backend-service │ │
│ │ port: │ │
│ │ number: 8095 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이렇게 하면: │
│ ├── Ingress Controller(내부적으로 Nginx)가 annotations를 읽음 │
│ ├── 자동으로 CORS 헤더를 응답에 추가 + Preflight도 자동 처리 │
│ └── 방법 2의 Nginx 설정을 YAML 한 장으로 대체! │
│ │
│ 팁: 방법 1처럼 Same-Origin도 가능! │
│ ├── 하나의 Ingress에서 path: /v1/ → backend, / → frontend │
│ └── 같은 도메인 = Same Origin → CORS 불필요 │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 Ingress CORS 흐름 그림
┌─────────────────────────────────────────────────────────────────┐
│ Ingress CORS 전체 아키텍처 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Kubernetes Cluster │ │
│ │ │ │
│ │ 외부 요청 ──► Ingress Controller ──► Service ──► Pod │ │
│ │ (CORS 헤더 추가) (내부주소) (앱) │ │
│ │ (Preflight 처리) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Pod(Spring)은 CORS에 대해 아무것도 모름! │
│ Ingress Controller가 모든 CORS 처리를 담당 │
│ │
└─────────────────────────────────────────────────────────────────┘
6. 방법 4: AWS ALB에서 CORS 처리
6.1 ALB가 뭔가?
┌─────────────────────────────────────────────────────────────────┐
│ ALB (Application Load Balancer)란? │
│ │
│ Load Balancer(로드 밸런서)란? │
│ ├── 여러 서버에 요청을 골고루 나눠주는 역할 │
│ ├── 비유: 은행 번호표 기계 │
│ │ 손님이 오면 빈 창구로 안내 → 한 창구에 몰리지 않게 │
│ └── 서버 하나가 죽어도 다른 서버로 보내줌 (고가용성) │
│ │
│ ALB = Application Load Balancer │
│ ├── AWS(아마존 클라우드)가 제공하는 로드 밸런서 │
│ ├── "Application" = HTTP/HTTPS 내용을 이해할 수 있음 │
│ │ (경로, 헤더, 쿠키 등을 보고 분배 가능) │
│ ├── 비유: 똑똑한 안내 데스크 │
│ │ "1번 창구는 계좌 개설, 2번은 대출" 이런 식으로 분류 │
│ └── 반대: NLB(Network LB)는 IP/포트만 봄 (단순 분배) │
│ │
│ Target Group(대상 그룹)이란? │
│ ├── "이 요청을 받을 서버들의 그룹" │
│ ├── 예: 백엔드 서버 3대를 하나의 Target Group으로 묶음 │
│ └── ALB가 Target Group에 요청을 분배 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 인터넷 ──► ALB ──► Target Group A (서버1, 서버2) │ │
│ │ └──► Target Group B (서버3, 서버4) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 ALB CORS 해결법
┌─────────────────────────────────────────────────────────────────┐
│ ALB에서 CORS 해결하는 두 가지 방법 │
│ │
│ 방법 A: 같은 ALB에서 경로 기반 라우팅 (Same Origin) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ALB 리스너 규칙: │ │
│ │ 규칙 1: 경로 /v1/* → Backend Target Group (8095) │ │
│ │ 규칙 2: 그 외 → Frontend Target Group (3000) │ │
│ │ │ │
│ │ 브라우저 ──► klpark.com ──► ALB │ │
│ │ │ │ │
│ │ /v1/* │ /* │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │Backend │ │Frontend │ │ │
│ │ │Target │ │Target │ │ │
│ │ │Group │ │Group │ │ │
│ │ └─────────┘ └─────────┘ │ │
│ │ │ │
│ │ → 같은 도메인 = Same Origin → CORS 불필요! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 리스너(Listener)란? │
│ ├── ALB가 "어떤 포트에서 요청을 받을지" 설정하는 것 │
│ └── 리스너 안에 "규칙(Rule)"을 만들어서 경로별 라우팅 │
│ │
│ 방법 B: ALB 응답 헤더 규칙으로 CORS 헤더 추가 │
│ ├── 도메인 분리 필수 시 (app.klpark.com ↔ api.klpark.com) │
│ ├── ALB가 응답 전달 시 CORS 헤더를 추가할 수 있음 │
│ ├── 참고: Nginx보다 세밀한 제어가 어려움 │
│ └── 복잡한 CORS 규칙 필요 시 API Gateway 사용 권장 │
│ │
│ AWS 용어 참고: │
│ ├── ECS = AWS의 컨테이너 실행 서비스 │
│ ├── EKS = AWS에서 Kubernetes를 실행하는 서비스 │
│ └── S3 = AWS의 파일 저장소 (정적 웹사이트 호스팅 가능) │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 방법 5: API Gateway에서 CORS 처리
7.1 API Gateway가 뭔가?
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway란 무엇인가? │
│ │
│ API Gateway = 모든 API 요청의 "정문" │
│ │
│ 비유: 호텔 컨시어지 (안내 데스크) │
│ ├── 손님(요청)이 오면 │
│ ├── 신분 확인 (인증) │
│ ├── 어떤 서비스가 필요한지 파악 (라우팅) │
│ ├── 너무 자주 오는 손님 제한 (속도 제한) │
│ ├── 요청 형식 변환 (요청 변환) │
│ └── CORS 같은 보안 정책도 처리 │
│ │
│ Reverse Proxy vs API Gateway: │
│ ├── Reverse Proxy = 요청 전달 + 로드밸런싱 (단순) │
│ ├── API Gateway = 위 기능 + 인증 + 속도 제한 + CORS 등 │
│ └── API Gateway가 더 많은 기능을 제공 (상위 호환) │
│ │
│ 대표적인 API Gateway: │
│ ├── Kong: 오픈소스, Nginx 기반, 플러그인 풍부 │
│ ├── AWS API Gateway: AWS 관리형 서비스 │
│ ├── Istio Gateway: Kubernetes Service Mesh 기반 │
│ └── Nginx Plus: Nginx의 유료 버전 (API Gateway 기능 내장) │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 API Gateway에서 CORS 설정
┌─────────────────────────────────────────────────────────────────┐
│ API Gateway에서 CORS 설정하기 │
│ │
│ 대부분의 API Gateway가 CORS를 기본 기능으로 제공! │
│ │
│ Kong 예시 (CORS 플러그인): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ plugins: │ │
│ │ - name: cors │ │
│ │ config: │ │
│ │ origins: │ │
│ │ - "https://app.klpark.com" │ │
│ │ methods: [GET, POST, PUT, DELETE] │ │
│ │ headers: [Content-Type, Authorization] │ │
│ │ credentials: true │ │
│ │ max_age: 3600 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ AWS API Gateway: 콘솔에서 클릭 몇 번이면 끝 │
│ ├── 1. API Gateway 콘솔 → 해당 API 선택 │
│ ├── 2. "CORS" 탭 → Allow Origin 입력 │
│ ├── 3. "Save" 클릭 → Preflight 처리도 자동! │
│ └── GUI에서 설정하므로 코드 작성 불필요 │
│ │
│ Istio VirtualService (Service Mesh): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Service Mesh란? │ │
│ │ ├── 마이크로서비스 간 통신을 관리하는 인프라 계층 │ │
│ │ └── Istio = 가장 유명한 Service Mesh 구현체 │ │
│ │ │ │
│ │ spec: │ │
│ │ http: │ │
│ │ - corsPolicy: │ │
│ │ allowOrigins: │ │
│ │ - exact: "https://app.klpark.com" │ │
│ │ allowMethods: [GET, POST, PUT, DELETE] │ │
│ │ allowHeaders: [content-type, authorization] │ │
│ │ allowCredentials: true │ │
│ │ maxAge: "3600s" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ API Gateway의 장점: │
│ ├── CORS + 인증 + 속도 제한 등 한 곳에서 관리 │
│ ├── 대규모 MSA(마이크로서비스 아키텍처)에 적합 │
│ └── 관리 포인트가 한 곳으로 수렴 │
│ │
└─────────────────────────────────────────────────────────────────┘
8. 방법 비교 및 선택 가이드
8.1 전체 비교표
┌─────────────────────────────────────────────────────────────────┐
│ 5가지 방법 비교표 │
│ │
│ ┌────────────────┬─────────┬─────────┬──────────────────┐ │
│ │ 방법 │구현 난이│관리 편의│ 적합한 환경 │ │
│ ├────────────────┼─────────┼─────────┼──────────────────┤ │
│ │ 1. Reverse │ 쉬움 │ 매우 │ Docker Compose │ │
│ │ Proxy │ │ 좋음 │ 소규모 서비스 │ │
│ │ Same-Origin │ │ │ │ │
│ ├────────────────┼─────────┼─────────┼──────────────────┤ │
│ │ 2. Reverse │ 보통 │ 좋음 │ 도메인 분리 필수 │ │
│ │ Proxy │ │ │ 시 │ │
│ │ CORS 헤더 │ │ │ │ │
│ ├────────────────┼─────────┼─────────┼──────────────────┤ │
│ │ 3. K8s │ 보통 │ 좋음 │ Kubernetes 환경 │ │
│ │ Ingress │ │ │ │ │
│ ├────────────────┼─────────┼─────────┼──────────────────┤ │
│ │ 4. ALB │ 쉬움 │ 좋음 │ AWS ECS/EKS │ │
│ │ 경로 라우팅 │ │ │ │ │
│ ├────────────────┼─────────┼─────────┼──────────────────┤ │
│ │ 5. API │ 보통 │ 매우 │ 대규모 MSA │ │
│ │ Gateway │ │ 좋음 │ │ │
│ └────────────────┴─────────┴─────────┴──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 선택 가이드
┌─────────────────────────────────────────────────────────────────┐
│ 어떤 방법을 선택할까? │
│ │
│ 질문 1: 프론트와 백엔드를 같은 도메인으로 할 수 있나? │
│ ├── YES → 방법 1 (Same Origin) 추천! CORS 자체를 없앰 │
│ └── NO → 질문 2로 │
│ │
│ 질문 2: 어떤 인프라를 쓰고 있나? │
│ ├── Docker Compose → 방법 2 (Nginx CORS 헤더) │
│ ├── Kubernetes → 방법 3 (Ingress annotations) │
│ ├── AWS → 방법 4 (ALB 경로 라우팅 or 헤더 규칙) │
│ └── 대규모 MSA → 방법 5 (API Gateway) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 가장 추천하는 방법: │ │
│ │ │ │
│ │ 1순위: Same-Origin 만들기 │ │
│ │ (CORS 문제 자체를 없앤다 = 가장 깔끔) │ │
│ │ │ │
│ │ 2순위: 인프라 앞단에서 CORS 헤더 주입 │ │
│ │ (한 곳에서 관리, 백엔드 앱은 깔끔) │ │
│ │ │ │
│ │ ❌ 비추천: 각 백엔드 앱마다 CORS 설정 │ │
│ │ (관리 지옥, 설정 누락 위험) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.3 흔한 실수와 해결
┌─────────────────────────────────────────────────────────────────┐
│ 흔한 실수 모음 │
│ │
│ ❌ 실수 1: CORS 헤더 중복 │
│ Nginx + Spring 둘 다 CORS 설정 → 헤더 2개 → 에러! │
│ ✅ 해결: 한 곳에서만 설정! (인프라 OR 백엔드) │
│ │
│ ❌ 실수 2: Credentials + 와일드카드(*) │
│ Allow-Origin: * + Allow-Credentials: true → 에러! │
│ ✅ 해결: 구체적 Origin 명시 (https://app.klpark.com) │
│ │
│ ❌ 실수 3: OPTIONS 처리 누락 │
│ 실제 요청에만 CORS 헤더 → Preflight 실패 → 본 요청 안 나감 │
│ ✅ 해결: OPTIONS에 대한 처리를 반드시 추가 │
│ │
│ ❌ 실수 4: Allow-Headers 누락 │
│ 프론트가 Authorization 보내는데 Allow-Headers에 없음 → 에러! │
│ ✅ 해결: 프론트가 보내는 모든 커스텀 헤더를 Allow에 추가 │
│ │
│ 디버깅 팁: │
│ ├── 브라우저 F12 → Network 탭 → 실패한 요청의 Headers 확인 │
│ ├── Console 탭의 CORS 에러 메시지를 꼼꼼히 읽기 │
│ └── curl -I -X OPTIONS <URL> 로 Preflight 응답 직접 확인 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 정리
┌─────────────────────────────────────────────────────────────────┐
│ 전체 방법 요약 그림 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ "CORS를 어디서 해결할 것인가?" │ │
│ │ │ │
│ │ 방법 1: Same Origin 만들기 (CORS 자체를 없앰) │ │
│ │ ┌──────────┐ ┌────────────┐ │ │
│ │ │ 브라우저 │──►│ Nginx/ALB │──► 프론트/백엔드 │ │
│ │ │ (같은 │ │ (경로 분기) │ │ │
│ │ │ Origin) │ └────────────┘ │ │
│ │ └──────────┘ │ │
│ │ │ │
│ │ 방법 2~5: 인프라에서 CORS 헤더 주입 │ │
│ │ ┌──────────┐ ┌────────────┐ │ │
│ │ │ 브라우저 │──►│Nginx/Ingres│──► 백엔드 │ │
│ │ │ (다른 │ │s/ALB/GW │ (CORS 모름) │ │
│ │ │ Origin) │ │(CORS 헤더 │ │ │
│ │ └──────────┘ │ 추가) │ │ │
│ │ └────────────┘ │ │
│ │ │ │
│ │ ❌ 비추천: 각 백엔드 앱마다 CORS 설정 │ │
│ │ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ 브라우저 │──►│서비스1 │ │서비스2 │ │서비스3 │ │ │
│ │ └──────────┘ │(CORS) │ │(CORS) │ │(누락!) │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ ↑ 에러! │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 한 줄: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "CORS는 브라우저의 규칙이다. │ │
│ │ 인프라에서 해결하면 앱 코드는 깔끔해진다." │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 기억할 원칙: │
│ ├── 1순위: Same Origin으로 CORS 문제 자체를 없애기 │
│ ├── 2순위: 인프라 한 곳에서 CORS 헤더 주입 │
│ ├── CORS 헤더 중복 주의 (인프라 OR 백엔드, 한 곳에서만!) │
│ ├── Preflight(OPTIONS) 처리를 절대 빼먹지 않기 │
│ └── 백엔드 앱은 비즈니스 로직에만 집중하게 하기 (관심사 분리) │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
CORS, SOP, Same-Origin Policy, Reverse Proxy, Nginx, Kubernetes Ingress, ALB, API Gateway, Preflight, OPTIONS, Access-Control-Allow-Origin, Kong, Istio, Service Mesh, Forward Proxy, Load Balancer, Target Group