생활기록부

Spring Boot Postman은 되는데 브라우저는 CORS 오류? 원인과 해결 방법 본문

개발/Spring Boot

Spring Boot Postman은 되는데 브라우저는 CORS 오류? 원인과 해결 방법

chansungs 2026. 10. 29. 04:17
728x90
반응형

 

Spring Boot Postman은 되는데 브라우저는 CORS 오류? 원인과 해결 방법

Spring Boot API를 개발하다 보면 이상한 상황을 만날 수 있습니다.

Postman에서는 API가 정상적으로 호출됩니다.

POST /api/orders

200 OK

그런데 React나 Vue에서 같은 API를 호출하면 브라우저에서 다음과 같은 오류가 발생합니다.

Access to fetch at 'http://localhost:8080/api/orders'
from origin 'http://localhost:3000'
has been blocked by CORS policy

또는:

No 'Access-Control-Allow-Origin' header is present
on the requested resource.

이런 상황에서는 자연스럽게 이런 생각이 듭니다.

Postman에서는 되는데
왜 브라우저에서는 안 되지?

결론부터 말하면 Postman과 브라우저는 CORS를 처리하는 방식이 다르기 때문입니다.

Spring Boot API 자체에는 문제가 없더라도 브라우저의 Same-Origin Policy와 CORS 정책 때문에 Front-end 요청이 차단될 수 있습니다.

이번 글에서는 Postman에서는 정상인데 브라우저에서만 CORS 오류가 발생하는 이유와 Spring Boot에서 확인해야 할 설정을 알아보겠습니다.


1. 먼저 상황을 만들어보자

Spring Boot API가 있다고 가정해보겠습니다.

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @PostMapping
    public String createOrder() {

        return "created";
    }
}

Spring Boot는 다음 주소에서 실행 중입니다.

http://localhost:8080

API:

POST http://localhost:8080/api/orders

입니다.

Postman에서 요청합니다.

POST
http://localhost:8080/api/orders

결과:

200 OK

정상입니다.


2. React에서 같은 API를 호출해보자

React가 다음 주소에서 실행 중이라고 가정해보겠습니다.

http://localhost:3000

JavaScript:

fetch("http://localhost:8080/api/orders", {
    method: "POST"
});

브라우저에서는 다음 오류가 발생할 수 있습니다.

blocked by CORS policy

구조를 보면 다음과 같습니다.

React

http://localhost:3000

        ↓

Spring Boot

http://localhost:8080

포트가 다르기 때문에 서로 다른 Origin입니다.


3. Postman에서는 왜 정상일까?

여기서 가장 중요한 부분입니다.

CORS는 기본적으로 브라우저가 적용하는 Cross-Origin 보안 정책입니다.

브라우저는 JavaScript가 다른 Origin의 서버에 접근하려 할 때 서버의 CORS 정책을 확인합니다.

Browser

↓

Cross-Origin 요청?

↓

YES

↓

CORS 정책 확인

반면 Postman은 일반적인 브라우저 페이지에서 JavaScript를 실행하는 환경이 아닙니다.

따라서 브라우저의 Same-Origin Policy가 동일한 방식으로 적용되지 않습니다.

그래서 다음 상황이 가능합니다.

Postman

↓

Spring Boot

↓

200 OK

하지만:

Browser

↓

CORS 검사

↓

Spring Boot CORS 설정 부족

↓

요청 또는 응답 접근 차단

이 될 수 있습니다.


4. Postman 성공 = CORS 정상은 아니다

이 부분을 기억하는 것이 중요합니다.

Postman에서 API 성공

했다고 해서:

CORS 설정도 정상

이라는 뜻은 아닙니다.

Postman 테스트로 알 수 있는 것은 주로:

API URL이 맞는가?

Controller가 동작하는가?

Request Body가 정상인가?

서버가 응답하는가?

등입니다.

브라우저의 CORS 정책까지 검증했다고 볼 수는 없습니다.


5. Origin이란?

CORS를 이해하려면 Origin을 알아야 합니다.

Origin은 기본적으로 다음 요소로 구분됩니다.

Protocol

+

Host

+

Port

예를 들어:

http://localhost:3000

과:

http://localhost:8080

은 포트가 다릅니다.

따라서 다른 Origin입니다.


6. 같은 localhost인데 왜 CORS가 발생할까?

처음 CORS를 접하면 많이 헷갈리는 부분입니다.

둘 다:

localhost

인데 왜 Cross-Origin일까요?

다음을 비교해보겠습니다.

Front-end

http://localhost:3000
Back-end

http://localhost:8080

Host는 같습니다.

localhost

하지만 Port가 다릅니다.

3000

8080

Origin이 다릅니다.

따라서 브라우저에서는 Cross-Origin 요청으로 처리할 수 있습니다.


7. React와 Spring Boot에서 자주 발생하는 구조

Front-end:

React

localhost:3000

Back-end:

Spring Boot

localhost:8080

요청:

React

↓

http://localhost:8080/api/users

↓

Spring Boot

브라우저는:

현재 Origin
localhost:3000

요청 대상
localhost:8080

↓

Origin 다름

으로 판단합니다.

Spring Boot에서 적절한 CORS 응답을 제공해야 합니다.


8. Vue나 Vite에서도 마찬가지

Vite 개발 서버는 예를 들어:

http://localhost:5173

에서 실행될 수 있습니다.

Spring Boot:

http://localhost:8080

이라면 역시 다른 Origin입니다.

localhost:5173

≠

localhost:8080

따라서 CORS 정책이 필요할 수 있습니다.


9. 가장 간단한 해결 - @CrossOrigin

특정 Controller만 허용한다면 @CrossOrigin을 사용할 수 있습니다.

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

    @PostMapping
    public String createOrder() {

        return "created";
    }
}

이제 해당 Origin에서 들어오는 Cross-Origin 요청을 허용하도록 설정할 수 있습니다.


 

반응형

 

10. 메서드 하나에만 @CrossOrigin 사용

특정 API만 허용할 수도 있습니다.

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @CrossOrigin(
            origins = "http://localhost:3000"
    )
    @PostMapping
    public String createOrder() {

        return "created";
    }
}

다른 API에는 CORS 설정이 적용되지 않도록 범위를 제한할 수 있습니다.


11. Controller가 많으면 전역 CORS 설정

Controller가 많다면 @CrossOrigin을 반복하는 것보다 전역 설정을 고려할 수 있습니다.

@Configuration
public class WebConfig
        implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(
            CorsRegistry registry) {

        registry.addMapping("/api/**")
                .allowedOrigins(
                        "http://localhost:3000"
                )
                .allowedMethods(
                        "GET",
                        "POST",
                        "PUT",
                        "DELETE",
                        "OPTIONS"
                );
    }
}

이제:

/api/users

/api/orders

/api/products

등 /api/** 범위에 공통 CORS 정책을 적용할 수 있습니다.


12. allowedOrigins 주소를 정확히 확인하자

다음 설정을 했습니다.

.allowedOrigins(
        "http://localhost:3000"
)

그런데 실제 Front-end는:

http://localhost:5173

에서 실행되고 있습니다.

이 경우 설정과 실제 Origin이 다릅니다.

허용

localhost:3000

실제

localhost:5173

CORS 오류가 계속 발생할 수 있습니다.


13. localhost와 127.0.0.1도 다르다

Spring Boot 설정:

.allowedOrigins(
        "http://localhost:3000"
)

그런데 브라우저 주소:

http://127.0.0.1:3000

이라면 Origin 문자열이 다릅니다.

따라서 실제 브라우저의 Origin을 확인해야 합니다.


14. 브라우저 Console에서 Origin 확인

CORS 오류 메시지를 자세히 보면 실제 Origin이 표시되는 경우가 많습니다.

예:

Access to fetch at
'http://localhost:8080/api/users'

from origin
'http://localhost:5173'

has been blocked by CORS policy

여기서 가장 먼저 확인할 부분은:

from origin
'http://localhost:5173'

입니다.

Spring 설정도 여기에 맞아야 합니다.

.allowedOrigins(
        "http://localhost:5173"
)

15. Postman에는 Origin이 없을 수도 있다

Postman 요청과 브라우저 요청을 비교하면 Header에도 차이가 있을 수 있습니다.

브라우저:

Origin: http://localhost:3000

Postman:

Origin Header가 없거나
브라우저와 다른 방식

일 수 있습니다.

Spring의 CORS 처리는 Origin Header와 Cross-Origin 요청 여부를 기준으로 동작하기 때문에 결과가 달라질 수 있습니다.


16. GET은 되는데 POST만 실패하는 경우

다음 상황도 자주 발생합니다.

GET
→ 정상

POST
→ CORS 오류

왜 그럴까요?

POST 요청이 다음과 같다고 해보겠습니다.

fetch("http://localhost:8080/api/orders", {
    method: "POST",
    headers: {
        "Content-Type": "application/json"
    },
    body: JSON.stringify({
        productId: 100
    })
});

브라우저가 실제 POST 전에 Preflight OPTIONS 요청을 보낼 수 있습니다.

OPTIONS

↓

POST

OPTIONS가 실패하면 실제 POST도 실행되지 않을 수 있습니다.


17. Network 탭에서 OPTIONS를 확인하자

브라우저 개발자 도구의 Network 탭을 엽니다.

다음 요청을 찾아봅니다.

OPTIONS /api/orders

예를 들어:

OPTIONS /api/orders

Status
403

이라면 Preflight 단계에서 실패하고 있습니다.

반면:

OPTIONS 200

POST 401

이라면 CORS Preflight는 성공했고 실제 문제는 인증일 가능성이 있습니다.


18. Postman은 Preflight 없이 바로 요청할 수 있다

이 차이 때문에 Postman은 되고 브라우저는 실패할 수 있습니다.

Postman:

POST /api/orders

↓

Spring Boot

↓

200

Browser:

OPTIONS /api/orders

↓

실패

↓

POST 요청 중단

즉 같은 POST API라도 요청 과정이 다를 수 있습니다.


19. Authorization Header를 넣었더니 안 되는 경우

JWT를 적용하면서 자주 발생합니다.

기존 Front-end:

fetch(
    "http://localhost:8080/api/users"
);

정상.

JWT 적용:

fetch(
    "http://localhost:8080/api/users",
    {
        headers: {
            "Authorization":
                "Bearer token"
        }
    }
);

이후 CORS 오류.

Authorization Header 때문에 Preflight가 발생했고 해당 요청이 실패했는지 확인해볼 수 있습니다.


20. allowedHeaders 확인

Spring Boot CORS 설정:

.allowedHeaders(
        "Content-Type"
)

그런데 실제 요청은:

Authorization

Content-Type

을 사용합니다.

Authorization이 빠져 있습니다.

다음처럼 필요한 Header를 허용할 수 있습니다.

.allowedHeaders(
        "Authorization",
        "Content-Type"
)

21. allowedMethods도 확인

Front-end:

fetch(url, {
    method: "DELETE"
});

Spring CORS 설정:

.allowedMethods(
        "GET",
        "POST"
)

DELETE가 없습니다.

실제 API에서 DELETE를 사용한다면 정책에 추가해야 합니다.

.allowedMethods(
        "GET",
        "POST",
        "PUT",
        "DELETE",
        "OPTIONS"
)

22. Spring Security를 사용하는지 확인

Spring Security가 적용되어 있다면 CORS 문제가 조금 더 복잡해질 수 있습니다.

요청 흐름:

Browser

↓

Spring Security

↓

Spring MVC

↓

Controller

Security Filter Chain에서 요청이 먼저 차단되면 MVC CORS 설정만 수정해서는 해결되지 않을 수 있습니다.


23. SecurityFilterChain CORS 설정

예를 들어 다음처럼 CORS를 활성화할 수 있습니다.

@Bean
public SecurityFilterChain securityFilterChain(
        HttpSecurity http) throws Exception {

    http
            .cors(cors -> {})
            .authorizeHttpRequests(auth -> auth
                    .anyRequest()
                    .authenticated()
            );

    return http.build();
}

프로젝트에 따라 CorsConfigurationSource를 직접 구성할 수도 있습니다.


24. CorsConfigurationSource 예제

@Bean
public CorsConfigurationSource
        corsConfigurationSource() {

    CorsConfiguration config =
            new CorsConfiguration();

    config.setAllowedOrigins(
            List.of(
                    "http://localhost:3000"
            )
    );

    config.setAllowedMethods(
            List.of(
                    "GET",
                    "POST",
                    "PUT",
                    "DELETE",
                    "OPTIONS"
            )
    );

    config.setAllowedHeaders(
            List.of(
                    "Authorization",
                    "Content-Type"
            )
    );

    UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration(
            "/api/**",
            config
    );

    return source;
}

Security를 사용하는 경우 이 부분도 확인해야 합니다.


 

728x90

 

25. Postman은 되는데 브라우저에서 401이 발생한다면?

브라우저 Network에서 다음과 같이 보일 수 있습니다.

OPTIONS
200

GET
401

이 경우 Preflight는 정상입니다.

실제 API가 인증되지 않은 것입니다.

확인할 부분:

Authorization Header 존재?

JWT가 유효한가?

Token이 만료됐는가?

입니다.

이 상황을 CORS 문제라고 생각하고 allowedOrigins("*")를 추가해도 해결되지 않습니다.


26. 403도 같은 방식으로 구분

Network:

OPTIONS
200

GET /api/admin
403

이라면 CORS보다는 권한 문제일 가능성이 있습니다.

Preflight
→ 정상

Authorization
→ 실패

입니다.


27. 405도 CORS와 구분

다음 결과:

OPTIONS
200

PUT
405

이라면 Preflight는 정상입니다.

Controller가 PUT을 지원하는지 확인해야 합니다.

예:

@PostMapping("/api/orders")

만 존재한다면:

PUT /api/orders

은 지원되지 않습니다.

이 경우:

HttpRequestMethodNotSupportedException

관련 문제일 수 있습니다.


28. CORS 오류라고 모든 설정을 "*"로 만들지 말자

검색하다 보면 다음과 같은 설정을 쉽게 볼 수 있습니다.

.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")

이렇게 설정하면 개발 중 특정 문제를 확인하기 쉬울 수 있습니다.

하지만 운영에서는 허용 범위를 신중하게 정해야 합니다.

예를 들어:

Origin
https://www.example.com

Methods
GET, POST

Headers
Authorization, Content-Type

처럼 필요한 범위만 허용할 수 있습니다.


29. Credentials를 사용하는 경우

쿠키 기반 인증 등을 사용할 경우 Front-end에서 다음을 사용할 수 있습니다.

fetch(
    "http://localhost:8080/api/users",
    {
        credentials: "include"
    }
);

서버에서는 상황에 따라:

.allowCredentials(true)

를 설정할 수 있습니다.


30. Credentials와 wildcard Origin 주의

다음 조합은 주의해야 합니다.

.allowedOrigins("*")
.allowCredentials(true)

Credential이 포함된 Cross-Origin 요청에서는 모든 Origin을 wildcard로 허용하는 것과 함께 사용할 수 없는 제약이 있습니다.

따라서:

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

처럼 명시적으로 허용하는 방식이 필요할 수 있습니다.


31. 개발에서는 되고 운영에서 안 되는 경우

개발 환경:

Front-end
http://localhost:3000

운영:

Front-end
https://www.example.com

Spring 설정:

.allowedOrigins(
        "http://localhost:3000"
)

만 되어 있다면 개발에서는 정상인데 운영에서는 CORS 오류가 발생할 수 있습니다.

운영 Origin도 정책에 맞게 설정해야 합니다.


32. 환경별 CORS 설정 관리

예를 들어 application-dev.yml:

app:
  cors:
    allowed-origin: http://localhost:3000

운영:

app:
  cors:
    allowed-origin: https://www.example.com

처럼 환경별로 관리할 수 있습니다.

Java 코드:

@Value("${app.cors.allowed-origin}")
private String allowedOrigin;

그리고:

.allowedOrigins(allowedOrigin)

로 적용할 수 있습니다.


33. Nginx나 Gateway가 있다면?

로컬에서는 정상인데 운영 브라우저에서만 CORS 오류가 발생한다면 애플리케이션 외부도 확인해야 할 수 있습니다.

구조:

Browser

↓

Nginx / Gateway / Load Balancer

↓

Spring Boot

중간 계층에서:

OPTIONS 차단

CORS Header 변경

Redirect 발생

등의 문제가 있을 수 있습니다.


34. Spring Boot까지 요청이 도착하는지 확인

Network에서는:

OPTIONS /api/orders

가 실패합니다.

그런데 Spring Boot 로그에는 요청이 없습니다.

그렇다면:

Browser

↓

중간 Proxy에서 차단?

↓

Spring Boot에 도달하지 않음

가능성을 확인할 수 있습니다.

이 경우 Spring Boot의 @CrossOrigin만 계속 수정해서는 해결되지 않을 수 있습니다.


35. Redirect도 확인해보자

요청 URL을 잘못 호출해서 Redirect가 발생하는 경우도 CORS 분석을 어렵게 만들 수 있습니다.

예를 들어:

http → https Redirect

/api → /login Redirect

등입니다.

Network 탭에서 실제 요청이 다른 URL로 이동하고 있는지 확인해보는 것도 좋습니다.


36. 가장 중요한 것은 Network 탭

Postman은 정상인데 브라우저가 실패한다면 가장 먼저 개발자 도구의 Network를 확인하는 것을 추천합니다.

다음 항목을 봅니다.

Request URL

Request Method

Status Code

Origin

OPTIONS 여부

Response Headers

그리고 요청 순서를 봅니다.

예:

OPTIONS 403

POST 없음

또는:

OPTIONS 200

POST 401

이 둘은 완전히 다른 문제입니다.


37. 상황별 원인 빠르게 찾기

Postman OK / Browser CORS Error

확인:

Origin
CORS 설정
OPTIONS

OPTIONS 403

확인:

Spring Security

CorsConfiguration

Preflight

OPTIONS 200 / API 401

확인:

JWT
Authorization
Authentication

OPTIONS 200 / API 403

확인:

권한
Authorization

OPTIONS 200 / API 405

확인:

@GetMapping
@PostMapping
@PutMapping
@DeleteMapping

38. 디버깅 체크리스트

Postman에서는 되는데 브라우저에서는 CORS 오류가 발생한다면 아래 순서로 확인해보세요.

  • Front-end와 Spring Boot의 Origin이 다른가?
  • 브라우저의 실제 Origin을 확인했는가?
  • allowedOrigins에 정확한 Origin이 등록되어 있는가?
  • 포트 번호까지 일치하는가?
  • Network에서 OPTIONS 요청이 있는가?
  • OPTIONS가 2xx 응답인가?
  • allowedMethods에 실제 HTTP Method가 포함되어 있는가?
  • allowedHeaders에 Authorization 등이 포함되어 있는가?
  • Spring Security를 사용하고 있는가?
  • SecurityFilterChain의 CORS 설정을 확인했는가?
  • OPTIONS 성공 후 실제 API의 Status를 따로 확인했는가?
  • 운영 환경이라면 Proxy/Gateway를 확인했는가?
  • CORS와 401/403/405를 혼동하고 있지 않은가?

39. 가장 간단한 해결 예제

React:

http://localhost:3000

Spring Boot:

http://localhost:8080

라면 다음과 같이 전역 CORS를 설정할 수 있습니다.

@Configuration
public class WebConfig
        implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(
            CorsRegistry registry) {

        registry.addMapping("/api/**")
                .allowedOrigins(
                        "http://localhost:3000"
                )
                .allowedMethods(
                        "GET",
                        "POST",
                        "PUT",
                        "DELETE",
                        "OPTIONS"
                )
                .allowedHeaders(
                        "Authorization",
                        "Content-Type"
                );
    }
}

프로젝트에서 Spring Security를 사용한다면 Security CORS 구성도 함께 확인합니다.


핵심 정리

Postman에서는 정상인데 브라우저에서만 CORS 오류가 발생하는 것은 이상한 상황이 아닙니다.

가장 큰 이유는 CORS가 브라우저의 Cross-Origin 보안 정책과 관련되어 있기 때문입니다.

Postman

↓

브라우저 CORS 정책과 동일하게 동작하지 않음

↓

API 정상 호출 가능

반면:

Browser

↓

Origin 확인

↓

Preflight 필요 여부 확인

↓

CORS 정책 확인

↓

조건 불일치

↓

CORS 오류

가 될 수 있습니다.

따라서:

Postman 성공

만 보고 서버가 완전히 정상이라고 판단하기보다는 브라우저 Network 탭에서 CORS 흐름을 확인해야 합니다.


마무리

이번 글에서는 Spring Boot API가 Postman에서는 정상인데 React, Vue 등의 브라우저에서는 CORS 오류가 발생하는 이유를 알아봤습니다.

이 상황에서는 가장 먼저 다음을 확인하는 것이 좋습니다.

1. 실제 Origin 확인

2. OPTIONS Preflight 확인

3. allowedOrigins 확인

4. allowedMethods 확인

5. allowedHeaders 확인

6. Spring Security 확인

7. 실제 API Status 확인

특히 다음 상황을 구분하는 것이 중요합니다.

OPTIONS 실패
→ CORS / Preflight 설정 확인

OPTIONS 성공 + 401
→ 인증 확인

OPTIONS 성공 + 403
→ 권한 확인

OPTIONS 성공 + 405
→ Controller HTTP Method 확인

CORS 오류를 해결할 때는 모든 Origin과 Method를 무조건 열기보다 브라우저가 실제로 어떤 요청을 보내고 어느 단계에서 실패하고 있는지 확인하면서 설정 범위를 정하는 것이 좋습니다.

 

728x90
반응형