자바 백엔드 입문 32편 — CORS 설정

2026-05-17자바 백엔드 입문

자바 백엔드 입문 32편. 프론트와 백엔드가 분리된 시대 거의 필수가 된 CORS의 정체와 Spring Boot 설정 표준 패턴을 출입증 비유로 풀어쓴 학습 노트.

📚 자바 백엔드 입문 · 32편 — CORS 설정

이 글은 자바 백엔드 입문 시리즈 59편 중 32편이에요. 프론트엔드(React·Vue)와 백엔드(Spring)가 분리된 시대 — 첫 API 호출에서 가장 자주 만나는 "CORS 에러" 와 그 해결법.

CORS가 어렵게 들리는 이유

처음 프론트에서 백엔드 API 호출하면 — 브라우저 콘솔에 빨간 글씨로 "blocked by CORS policy" 가 떠요. "백엔드가 응답을 줬는데 왜 막혀?" 가 안 잡혀요.

이 글에서는 회사 출입증 비유로 풀어요. CORS = "외부 회사 직원이 우리 사옥에 들어오려면 사전 출입증 발급". 끝까지 따라오시면 "빨간 글씨" 가 사라지는 표준 설정이 한 그림에 들어와요.

CORS — 한 줄 정의

CORS(Cross-Origin Resource Sharing) = "브라우저가 다른 도메인의 API를 호출할 때 적용되는 보안 정책". 브라우저가 자체적으로 "이거 보안 위험" 이라고 차단.

핵심 — CORS는 브라우저 정책이지 백엔드 거부가 아니에요. 백엔드는 응답을 보냈는데 — 브라우저가 "다른 도메인이라 위험" 판단해 자바스크립트 코드에 응답을 전달하지 않는 거예요.

프론트 http://localhost:3000  ──→  백엔드 http://localhost:8080
                                    ↓
                                  응답 전송 OK
                                    ↓
[브라우저] ❌ "출처가 달라요. JS 코드에 못 줘요"

Origin이 다르다 = 무엇이 다르다?

"Origin" 의 정의가 명확.

  • 프로토콜 (http vs https)
  • 호스트 (localhost vs api.myshop.com)
  • 포트 (:3000 vs :8080)

이 세 가지 중 하나라도 다르면 다른 Origin. 매우 빈번하게 발생.

Origin A Origin B CORS?
http://localhost:3000 http://localhost:8080 ✓ 발생 (포트)
https://app.com https://api.app.com ✓ 발생 (호스트)
http://app.com https://app.com ✓ 발생 (프로토콜)
https://app.com/login https://app.com/api ✗ 같은 Origin

Preflight — OPTIONS 사전 요청

CORS의 가장 헷갈리는 부분 — Preflight 요청. 브라우저가 "진짜 요청 보내기 전에 OPTIONS로 미리 물어봐" 하는 절차.

1. 브라우저 → 백엔드: OPTIONS /api/orders (사전 질문)
                      "POST 보내도 되나요? Content-Type=application/json 헤더 박아도 되나요?"
2. 백엔드 → 브라우저: 200 OK + 허용 헤더 응답
                      "Access-Control-Allow-Origin: *"
                      "Access-Control-Allow-Methods: POST, GET, PUT, DELETE"
3. 브라우저 → 백엔드: POST /api/orders (진짜 요청)
4. 백엔드 → 브라우저: 정상 응답

OPTIONS 요청은 "단순 요청" 이 아닐 때 (POST + JSON, PUT, DELETE, 커스텀 헤더 등) 자동 발생. GET 단순 요청은 Preflight 없이 바로 진행.

Spring Boot에서 CORS 설정 3가지 방식

1. @CrossOrigin — 컨트롤러별

가장 단순. 컨트롤러 메서드 또는 클래스에 박기.

@RestController
@RequestMapping("/api/orders")
@CrossOrigin(origins = "http://localhost:3000")     // 클래스 레벨
public class OrderController {

    @GetMapping("/{id}")
    @CrossOrigin(origins = "*")                      // 메서드 레벨 (이 메서드만)
    public Order get(@PathVariable Long id) { ... }
}

옵션:

@CrossOrigin(
    origins = {"http://localhost:3000", "https://app.myshop.com"},
    methods = {RequestMethod.GET, RequestMethod.POST},
    allowedHeaders = "*",
    maxAge = 3600                  // Preflight 결과 캐싱 시간
)

2. WebMvcConfigurer — 전역 설정 (권장)

모든 컨트롤러에 한 번에 적용. 회사 시스템 표준.

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("http://localhost:3000", "https://app.myshop.com")
                .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

/api/** 경로 전체에 한 줄 설정. 새 컨트롤러를 추가해도 자동 적용.

3. CorsFilter Bean — Spring Security와 함께

Spring Security가 박혀 있으면 위 두 방식이 잘 안 먹혀요. SecurityFilterChain보다 먼저 동작하는 CorsFilter 박기.

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("*"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return new CorsFilter(source);
    }
}

Spring Security 박힌 회사 시스템 표준.

자주 만나는 CORS 함정 4가지

1. allowedOrigins("*") + allowCredentials(true) 충돌

쿠키·토큰 같은 인증 정보를 보내려면 allowCredentials(true) 필수. 근데 이 옵션이 켜져 있으면 — "*" 와일드카드 사용 불가. 정확한 도메인 명시해야.

// ❌ 안 됨
.allowedOrigins("*").allowCredentials(true)

// ✓ 됨
.allowedOrigins("http://localhost:3000").allowCredentials(true)

2. 여러 환경 도메인 자동 매칭

개발·스테이징·운영 도메인이 다 다를 때 — allowedOriginPatterns 가 답.

.allowedOriginPatterns(
    "http://localhost:[*]",                    // 모든 localhost 포트
    "https://*.myshop.com"                      // 모든 서브도메인
)
.allowCredentials(true)

allowedOrigins 와 달리 allowedOriginPatterns* 와일드카드를 — allowCredentials(true) 와도 함께 사용 OK.

3. 프록시·로드밸런서 환경

서비스 앞에 nginx·CloudFront 같은 프록시가 있으면 — CORS 헤더를 프록시에서 박는 게 표준. Spring에서 박으면 이중 처리·충돌 가능.

4. preflight 차단

OPTIONS 요청에 응답 안 하면 — 모든 진짜 요청이 차단. Spring Boot 기본 설정으로 거의 자동 처리되지만, 가끔 Security 필터가 OPTIONS를 막아 — 명시적 허용 필요.

http.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()   // Preflight 허용
    .anyRequest().authenticated());
💡 한국 회사 표준 패턴

개발 = allowedOriginPatterns("http://localhost:[*]"). 운영 = 정확한 도메인 명시 + allowCredentials(true). 프록시 환경 = nginx에서 처리. Spring Security 박혔으면 CorsFilter Bean 우선.

한 줄 정리 — CORS = 브라우저 보안 정책. 백엔드는 "허용 헤더" 응답으로 풀어줘야. Spring Boot = WebMvcConfigurer.addCorsMappings 또는 CorsFilter Bean 전역 설정.

시험 직전 한 번 더 — CORS 입문자가 매번 헷갈리는 것

  • CORS = Cross-Origin Resource Sharing
  • 브라우저 보안 정책 — 백엔드 거부 아님
  • Origin 정의 = 프로토콜 + 호스트 + 포트
  • 셋 중 하나라도 다르면 다른 Origin
  • 가장 흔한 시나리오 = localhost:3000 (프론트) ↔ localhost:8080 (백엔드)
  • Preflight = OPTIONS 사전 요청 (POST·PUT·DELETE·커스텀 헤더 시)
  • GET 단순 요청 = Preflight 없이 직행
  • Spring Boot 설정 3가지 = @CrossOrigin / WebMvcConfigurer / CorsFilter
  • 권장 = WebMvcConfigurer.addCorsMappings 전역 설정
  • Spring Security 사용 시 = CorsFilter Bean 추가
  • allowedOrigins("*") + allowCredentials(true) 충돌
  • 해결 = allowedOriginPatterns 사용 (와일드카드 OK)
  • 쿠키·토큰 전송 = allowCredentials(true) 필수
  • maxAge = Preflight 결과 캐싱 시간 (브라우저)
  • 프록시·CDN 환경 = 프록시에서 CORS 처리 표준
  • Spring Security가 OPTIONS 차단 시 = permitAll() 명시
  • OriginReferer — Origin은 프로토콜+호스트+포트만
  • 응답 헤더 = Access-Control-Allow-Origin·-Methods·-Headers·-Credentials
  • 한국 회사 표준 = 개발 와일드카드 + 운영 정확 도메인
  • 모바일 앱 = CORS 없음 (브라우저 정책이라)

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!