개발을 시작한 지 얼마 되지 않은 개발자들이 가장 마주치기 싫어하는 오류가 무엇일까? 난 당당하게 CORS라고 답해주고 싶다. 본인도 처음 프로젝트를 도전했을 때도 결국 CORS 때문에 API 연결에 실패해 마무리하지 못한 슬픈 기억을 가지고 있다. 그만큼 CORS는 웹 개발자라면 가장 먼저 넘어야 할 산이라고 불린다고 해도 과언이 아니다.
평소에 관심을 가지고 있었던 스벨트 프레임워크를 통해 간단한 어드민 관리자 페이지를 구현하는 도중, API를 서버와 연결하는 과정에서 CORS 오류를 마주치고 말았다. 개발을 본격적으로 시작한 지 어연 1년 하고도 6개월이 지났지만, 아직도 CORS의 늪에 빠져 허우적댈 줄은 꿈에도 몰랐다.. 심지어 이번에는 백엔드 개발자의 입장이 아닌, 프런트엔드 개발자의 입장에서 마주친 CORS 오류라서 더욱 답답하기도 하였다.
CORS는 대체 무엇이길래 우리를 이렇게 힘들게 하고, 이를 어떻게 해결할 수 있었을까?
결론부터 얘기하자면, 예상대로 서버 측 코드에서 해당 오류를 수정해주어야 했었다. 무엇보다 이를 해결한 방식이 이전과는 조금 달라 원인을 파악하는 데 상당한 시간이 소요되었다..🥺
CORS
CORS(Cross-Origin Resource Sharing) 정책은 간단히 말해 다른 출처(origin)에서 정보를 요청하는 것에 대한 정책을 의미한다.
웹 서비스(자바스크립트)는 기본적으로 보안상의 이유로 동일 출처(Same-Origin) 정책을 적용한다. 하지만 우리가 웹 개발을 하다 보면, 기능에 따라 다른 출처로 리소스를 요청하거나, 별도의 서버에 리소스를 요청해야 하는 경우가 분명히 존재한다. 이 때이때 다른 출처로 요청을 보내려 할 때, 브라우저는 이를 허용하기 전에 서버가 해당 요청을 받아들일 의사가 있는지 확인하는 전처리 과정을 거친다. 이때 바로 CORS 정책이 동작하게 되며, 해당 과정에서 서버(출처)를 검증하기 위한 사전 요청(preflight request)은 일반적으로 OPTIONS 메서드를 통해 이루어진다.
작동 방식
간단한 예시를 통해 빠르게 작동 방식을 이해해보자!
- 브라우저 주소가 http://localhost:5173이고, 서버 주소가 http://12.34.56.78:8080 일 경우, 브라우저는 실제 API 요청 전에 OPTIONS 메서드로 사전 요청(preflight)을 보낸다.
- 서버가 응답 헤더(Access-Control-Allow-Origin)를 통해 요청 접근을 허용하는 url을 담아서 전송해 준다.
- 브라우저는 응답 헤더(Access-Control-Allow-Origin)의 url과 요청 origin(http://localhost:5173)을 비교한다.
- 만약 응답 헤더와 요청 origin이 일치하지 않다면, CORS 에러가 발생하게 된다.
위 과정에서 알 수 있듯이, 결국 CORS 에러의 해결책은 서버에서 Access-Control-Allow-Origin 헤더에 url을 담아주는 것이다.
스프링 부트에서는 이를 Spring Security를 통해 해결해 줄 수 있다.
Spring Security
CORS 관련 설정은 Spring Security의 기본적인 설정을 다루는 설정 클래스에서 필터 체인 설정을 통해 해결해 줄수도 있지만, CORS 설정을 구성하는 별도의 클래스를 통해 해결해 줄 수도 있다. 내가 요청했던 서버 코드의 경우, Security 필터 체인과 CORS 설정값을 별도의 클래스에서 진행해 주었으며, 해당 과정에서 문제가 발생한 것을 뒤늦게 알 수 있었다.
WebConfig(CORS 설정)
서버 로직은 다음과 같이 CORS 관련 설정 부분을 별도의 클래스로 분리하여 구현해 주었다. 우리가 눈여겨볼 부분은 가장 아래의 addCorsMapping() 메서드이다.
- allowedOrigins: 스프링 환경설정을 통해 주입해 준 도메인 목록들에서 오는 요청들을 허가해 준다.
- allowedMethods: 허용할 HTTP 요청 방식을 정의해 준다. OPTIONS 방식으로 오는 사전 요청(preflight)을 허가해 주었다.
- allowedHeaders: 모든 헤더를 와일드카드 방식으로 전부 허용해 준다.
해당 코드만 보면, OPTIONS 방식도 허가해 주고 헤더도 전부 허용해 주었는데, CORS 에러가 뜨는 것이 이상할 따름이었다. 대체 무엇이 문제였던 걸까?
문제의 원인: Spring 프레임워크의 요청 처리 우선권
위 코드와 같이 CORS 정책을 설정한 경우, 스프링 프레임워크 내에서 해당 설정값은 컨트롤러 계층(MVC)에서 적용된다. 하지만 Spring Security는 해당 계층에 도달하기 전에 필터체인 단에서 요청을 확인한다. 즉, WebConfig 클래스를 통해 CORS 설정에서 OPTIONS 방식이 허용되어 있더라도, 필터체인이 요청 관리에 대한 우선권을 지니고 있기 때문에 결국 해당 설정값은 적용되지 않는 것이다.
따라서 Spring Security의 설정값을 다루는 SecurityConfig 클래스를 수정함으로써 CORS 에러를 해결해 줄 수 있었다.
SecurityConfig
기존 Spring Security 설정 코드이다. 사전 요청(OPTIONS 방식의 preflight 요청)에 대해 별도의 허용 규칙이 없으므로, Spring Security 필터 체인에서 preflight 요청이 차단되어 CORS 오류가 나는 것이었다..🥺
CorsUtils.isPreFlightRequest 메서드를 통해 사전 요청을 명시적으로 허용해 주어 의도한 대로 CORS 정책이 동작하도록 설정해 주었다.
결론
Spring Security를 수정해 주고 나서야, 모든 API가 정상적으로 동작하기 시작했다!
기존에는 Spring Security 필터체인 내부에서 CORS 설정을 함께 설정했기 때문에 CORS 관련 오류를 거의 맞이할 일이 없었다. 그러나 이번에는 CORS 설정을 Spring Security와는 별도로 처리해 주어 모든 요청이 필터 단에서 차단되었고, 스프링 프레임워크에서 요청을 처리하는 과정에 대한 이해도가 낮아 이슈를 해결하는 데 보다 오랜 시간이 걸렸던 것 같다.
이번주에 진행하는 동아리 마지막 세션에서 해당 내용을 간략하게 공유해 나와 같은 어려움을 동아리원들이 겪지 않도록 해야겠다는 계획이다..☺️
'스프링 부트 > 오류' 카테고리의 다른 글
[gradle] 멀티모듈에서 컴포넌트 스캔이 되지 않는 문제 (0) | 2025.01.13 |
---|---|
[Kotlin] Spring Security에서 코틀린 DSL 적용하기 (0) | 2024.11.25 |
[JPA] ExecutorService를 통한 엔티티가 변경되지 않는 않는 오류 (1) | 2024.11.19 |
[gradle] 멀티 모듈에서 특정 모듈을 삭제했을 때 빌드가 실패하는 오류 (0) | 2024.11.19 |