TIL
오늘 한 것
반가워요, 주니어 개발자님. 앞으로 멘토이자 모의 면접관이 되어드릴 “CS공부”입니다. 편하게 질문하세요. 오늘은 웹 개발자라면 반드시 알아야 할 CORS에 대해 이야기해 보죠.
✅ 면접관의 시선
면접에서 CORS를 물어보는 이유는 간단합니다. “이 지원자가 웹의 기본적인 동작 방식과 보안 모델을 이해하고 있는가?”를 확인하기 위해서죠. 단순히 ‘다른 출처의 리소스를 요청하는 것’이라고만 알고 있으면 부족합니다. 면접관은 CORS가 왜 필요한지(Same-Origin Policy), 어떻게 동작하는지(Preflight), 그리고 실무에서 어떻게 설정하고 어떤 점을 주의해야 하는지를 궁금해합니다. 특히 MSA(Microservice Architecture) 환경에서는 서버 간 통신이 잦기 때문에 CORS에 대한 이해는 필수적입니다.
1. CORS란 무엇인가요? 왜 필요한가요?
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)를 이해하려면, 먼저 브라우저의 동일 출처 정책(Same-Origin Policy, SOP) 을 알아야 합니다.
1.1. 배경: 동일 출처 정책 (Same-Origin Policy)
MDN 공식 문서에 따르면, SOP는 “어떤 출처(Origin)에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호작용하는 것을 제한하는 중요한 보안 방식”입니다. 여기서 ‘출처(Origin)’는 Protocol + Host + Port의 조합을 의미합니다.
https://api.my-service.com
와http://api.my-service.com
-> 다른 출처 (Protocol 불일치)https://www.my-service.com
와https://api.my-service.com
-> 다른 출처 (Host 불일치)https://my-service.com:8080
와https://my-service.com:3000
-> 다른 출처 (Port 불일치)
🤔 왜 이런 정책이 필요한가요? (문제 해결)
만약 SOP가 없다면, 여러분이 my-bank.com
에 로그인한 상태에서 악의적인 hacker.com
사이트에 접속했다고 상상해 보세요. hacker.com
의 스크립트가 아무런 제약 없이 my-bank.com
의 API를 호출해서 여러분의 계좌 정보를 탈취할 수 있을 겁니다. SOP는 이처럼 CSRF(Cross-Site Request Forgery)와 같은 공격을 막는 기본적인 브라우저 보안 장치입니다.
1.2. 등장: 교차 출처 리소스 공유 (CORS)
하지만 현대 웹 애플리케이션은 API 서버와 웹 프론트 서버의 도메인을 분리하는 등 여러 출처의 리소스를 사용하는 것이 일반적입니다. 예를 들어, 프론트엔드 서버는 https://my-app.com
인데, 백엔드 API 서버는 https://api.my-app.com
일 수 있죠.
이때 SOP 때문에 my-app.com
에서 api.my-app.com
으로 API를 호출하면 브라우저가 차단해 버립니다. 이 문제를 해결하기 위해 등장한 것이 바로 CORS입니다.
CORS는 서버가 HTTP 헤더에 추가적인 정보를 담아, “이 출처에서 오는 요청은 허용해도 괜찮아”라고 브라우저에게 알려주는 메커니즘입니다. 즉, SOP라는 보안 원칙은 지키되, 신뢰할 수 있는 출처 간의 리소스 공유는 선택적으로 허용해 주는 정책입니다.
📌 트레이드오프 (Trade-off)
- 장점: SOP의 보안적 이점을 유지하면서도, 서로 다른 출처의 리소스를 안전하게 사용할 수 있어 현대적인 웹 아키텍처(MSA, API 기반 서비스 등) 구현이 가능해집니다.
- 단점: 잘못 설정하면 보안에 취약해질 수 있습니다. 예를 들어 모든 출처(
*
)를 허용하면 SOP를 무력화시켜 CSRF 공격 등에 노출될 위험이 있습니다. 또한, 특정 요청(Preflight)의 경우 추가적인 HTTP 왕복이 발생하여 약간의 성능 저하가 생길 수 있습니다.
2. CORS는 어떻게 동작하나요? (Preflight Request)
CORS 요청은 크게 두 가지로 나뉩니다. ‘단순 요청(Simple Request)’과 ‘예비 요청(Preflight Request)’입니다. 대부분의 실무 상황에서는 Preflight Request를 마주하게 됩니다.
- Preflight Request (예비 요청): 브라우저는 실제 요청을 보내기 전에,
OPTIONS
라는 HTTP 메소드를 사용하여 서버에 “내가 앞으로 이런 요청을 보낼 건데, 괜찮니?”라고 먼저 물어봅니다.- 언제 발생하는가?:
GET
,POST
,HEAD
가 아닌 메소드(PUT
,DELETE
등)를 사용하거나, 특정 기본 헤더 외에 커스텀 헤더(Authorization
등)를 포함할 때 발생합니다.
- 언제 발생하는가?:
- 서버의 응답: 서버는 이
OPTIONS
요청에 대한 응답으로Access-Control-Allow-Origin
,Access-Control-Allow-Methods
,Access-Control-Allow-Headers
등의 헤더를 담아 보냅니다. “응, 그 출처에서 오는 요청 괜찮아. 이런 메소드랑 헤더도 허용해 줄게.”라는 의미죠. - 브라우저의 판단: 브라우저는 서버의 Preflight 응답 헤더를 보고, 자신이 보내려던 실제 요청이 허용되는지 판단합니다.
- 실제 요청 (Main Request): 허용된다고 판단되면, 브라우저는 그제야 원래 보내려던
GET
,POST
등의 실제 요청을 서버로 보냅니다.
3. Spring Boot 실무 코드 예시
Spring Framework는 CORS를 매우 쉽게 설정할 수 있는 방법을 제공합니다. (관련 내용은 Spring 공식 문서에서 더 자세히 확인할 수 있습니다.)
3.1. Controller 레벨에서 개별 설정: @CrossOrigin
특정 컨트롤러나 메소드에만 CORS를 적용하고 싶을 때 유용합니다.
package com.example.cors.controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/products")
public class ProductController {
// http://localhost:3000 출처의 GET, POST 요청을 허용
@CrossOrigin(origins = "http://localhost:3000")
@GetMapping
public String getProducts() {
return "List of products";
}
// 모든 출처의 GET 요청을 허용 (주의해서 사용해야 함)
@CrossOrigin(origins = "*")
@GetMapping("/all")
public String getAllProducts() {
return "List of all products for everyone";
}
}
3.2. 전역 설정: WebMvcConfigurer
애플리케이션 전체에 일관된 CORS 정책을 적용하고 싶을 때 사용하는 방법입니다. 유지보수 측면에서 권장됩니다.
package com.example.cors.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**") // "/api/"로 시작하는 모든 경로에 적용
.allowedOrigins("http://localhost:3000", "https://my-app.com") // 허용할 출처
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 허용할 HTTP 메소드
.allowedHeaders("*") // 모든 헤더 허용
.allowCredentials(true) // 쿠키/인증 정보 포함 요청 허용
.maxAge(3600); // Preflight 요청 결과를 캐싱할 시간 (초 단위)
}
}
allowCredentials(true)
를 사용할 때는 allowedOrigins
에 와일드카드(*
)를 사용할 수 없습니다. 보안상 특정 출처를 명시해야 합니다.
4. 대규모 트래픽 환경에서의 고려사항
- Preflight 요청과 성능: 사용자가 많아지면 모든 CORS 요청마다 발생하는
OPTIONS
Preflight 요청이 무시 못 할 부하가 될 수 있습니다. 이는 실제 요청 외에 추가적인 네트워크 왕복(Round Trip)을 유발하여 응답 시간을 늘립니다. - 해결책: 위 전역 설정 예제의
.maxAge(3600)
처럼Access-Control-Max-Age
헤더를 설정하여 Preflight 요청의 결과를 브라우저에 캐싱하도록 할 수 있습니다. 이렇게 하면 지정된 시간(예시: 3600초) 동안은 Preflight 요청 없이 바로 실제 요청을 보낼 수 있어 부하를 줄일 수 있습니다. - 보안 설정: 절대로 운영 환경에서
allowedOrigins("*")
를 남용하면 안 됩니다. 이는 어떤 사이트에서든 내 서버의 API를 호출할 수 있게 만들어 SOP를 무력화시키고, 인증 정보가 탈취될 경우 심각한 보안 사고로 이어질 수 있습니다. 반드시 화이트리스트 방식으로 허용할 출처를 명시적으로 관리해야 합니다.
🔮 면접관의 꼬리 질문
자, 여기까지 잘 따라오셨네요. 면접관이라면 이 주제에 대해 다음과 같은 질문들을 추가로 던져볼 수 있습니다. 스스로 답변을 고민해 보세요.
@CrossOrigin
어노테이션 방식과WebMvcConfigurer
를 이용한 전역 설정 방식의 장단점은 무엇이며, 어떤 상황에서 각각을 사용하는 것이 좋을까요?- 인증을 위해
Authorization
헤더에 JWT 토큰을 담아 보내는 요청에서 CORS 에러가 발생했습니다. Preflight 요청과 관련하여 원인이 무엇이고 어떻게 해결해야 할지 설명해 보세요. - CORS 설정에서
allowCredentials(true)
옵션은 무엇이며, 이 옵션을 사용할 때 보안적으로 특별히 주의해야 할 점은 무엇인가요?