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.comhttp://api.my-service.com -> 다른 출처 (Protocol 불일치)
  • https://www.my-service.comhttps://api.my-service.com -> 다른 출처 (Host 불일치)
  • https://my-service.com:8080https://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를 마주하게 됩니다.

  1. Preflight Request (예비 요청): 브라우저는 실제 요청을 보내기 전에, OPTIONS라는 HTTP 메소드를 사용하여 서버에 “내가 앞으로 이런 요청을 보낼 건데, 괜찮니?”라고 먼저 물어봅니다.
    • 언제 발생하는가?: GET, POST, HEAD가 아닌 메소드(PUT, DELETE 등)를 사용하거나, 특정 기본 헤더 외에 커스텀 헤더(Authorization 등)를 포함할 때 발생합니다.
  2. 서버의 응답: 서버는 이 OPTIONS 요청에 대한 응답으로 Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers 등의 헤더를 담아 보냅니다. “응, 그 출처에서 오는 요청 괜찮아. 이런 메소드랑 헤더도 허용해 줄게.”라는 의미죠.
  3. 브라우저의 판단: 브라우저는 서버의 Preflight 응답 헤더를 보고, 자신이 보내려던 실제 요청이 허용되는지 판단합니다.
  4. 실제 요청 (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를 무력화시키고, 인증 정보가 탈취될 경우 심각한 보안 사고로 이어질 수 있습니다. 반드시 화이트리스트 방식으로 허용할 출처를 명시적으로 관리해야 합니다.

🔮 면접관의 꼬리 질문

자, 여기까지 잘 따라오셨네요. 면접관이라면 이 주제에 대해 다음과 같은 질문들을 추가로 던져볼 수 있습니다. 스스로 답변을 고민해 보세요.

  1. @CrossOrigin 어노테이션 방식과 WebMvcConfigurer를 이용한 전역 설정 방식의 장단점은 무엇이며, 어떤 상황에서 각각을 사용하는 것이 좋을까요?
  2. 인증을 위해 Authorization 헤더에 JWT 토큰을 담아 보내는 요청에서 CORS 에러가 발생했습니다. Preflight 요청과 관련하여 원인이 무엇이고 어떻게 해결해야 할지 설명해 보세요.
  3. CORS 설정에서 allowCredentials(true) 옵션은 무엇이며, 이 옵션을 사용할 때 보안적으로 특별히 주의해야 할 점은 무엇인가요?

results matching ""

    No results matching ""