| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- springboot예외처리
- Spring
- ControllerAdvice
- mybatis
- ApacheFlink
- KafkaProducer
- ApacheKafka
- 맥북
- RestControllerAdvice
- 백엔드개발
- 400badrequest
- KafkaFlink
- SQL
- SpringBootKafka
- java
- RequestMethodNotSupported
- SpringKafka
- springboot mybatis
- VUE
- SpringBoot Security
- springboot jwt
- mapper
- HttpRequestMethodNotSupportedException
- SpringBoot에러
- 시스템설정
- springboot
- java17
- ios
- 실시간데이터처리
- 405MethodNotAllowed
- Today
- Total
생활기록부
Spring Boot Postman은 되는데 브라우저는 CORS 오류? 원인과 해결 방법 본문

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를 사용하는 경우 이 부분도 확인해야 합니다.
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를 무조건 열기보다 브라우저가 실제로 어떤 요청을 보내고 어느 단계에서 실패하고 있는지 확인하면서 설정 범위를 정하는 것이 좋습니다.
'개발 > Spring Boot' 카테고리의 다른 글
| CORS Preflight란? OPTIONS 요청이 발생하는 이유와 실패 해결 방법 (0) | 2026.10.28 |
|---|---|
| Spring Security CORS 오류 해결 - SecurityFilterChain 설정 방법 (0) | 2026.10.27 |
| Spring Boot 전역 CORS 설정 방법 - WebMvcConfigurer로 한 번에 적용하기 (0) | 2026.10.26 |
| [Apache Flink #7] Flink CEP란? 실시간 이벤트 패턴 탐지 쉽게 이해하기 (0) | 2026.09.24 |
| [Apache Flink #6] Flink Checkpoint란? 장애가 발생해도 State를 복구하는 원리 (0) | 2026.09.21 |
