인프라 레벨 CORS 처리 - 앱을 건드리지 않고 해결하기
TL;DR
- 인프라 레벨 CORS 처리 - 앱을 건드리지 않고 해결하기의 핵심 개념을 빠르게 파악할 수 있다.
- 배경과 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
- 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.
1. 개념
인프라 레벨 CORS 처리 - 앱을 건드리지 않고 해결하기의 핵심 정의와 문제 공간을 간단히 정리한다.
2. 배경
이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.
3. 이유
왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.
4. 특징
핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.
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