[트러블슈팅] 모두의 후기 CDN과 CORS 배포 설정 정리
들어가며
모두의 후기 프로젝트를 운영 관점에서 다시 정리하면서 CDN과 CORS 설정을 손봤다.
초기에는 이미지 URL 곳곳에 CloudFront 기본 도메인이 직접 들어가 있었다. 기능은 동작했지만, 배포 환경이 정리될수록 이런 하드코딩된 URL이 유지보수에 부담이 될 수 있다고 느꼈다.
또한 프론트엔드 도메인과 백엔드 API 도메인이 분리되어 있었기 때문에, 운영/개발 환경에서 CORS 설정도 명확히 관리해야 했다.
이번 작업은 새로운 기능을 추가하기보다는 배포 환경을 안정적으로 정리하는 작업이었다.
문제 1. CloudFront URL 하드코딩
기존 코드에는 다음과 같은 CloudFront 주소가 여러 곳에 직접 들어가 있었다.
1
https://d1izijuzr22yly.cloudfront.net/
예를 들어 기본 프로필 이미지, 기본 썸네일 이미지, 게시글 이미지 URL, 프로필 이미지 URL 생성 로직에서 이 주소를 사용하고 있었다.
처음에는 빠르게 구현하기 위해 직접 문자열을 넣는 방식이 편했다.
하지만 시간이 지나고 서비스 도메인을 정리하면서 커스텀 CDN 도메인을 사용해야 했다.
1
https://cdn.modu-review.com
이렇게 바꾸면 외부에 노출되는 이미지 주소가 서비스 도메인 체계와 맞아지고, 나중에 CDN 제공자나 배포 구성이 바뀌더라도 코드 변경 범위를 줄일 수 있다.
문제 2. 이미지 URL 생성 책임
이미지 URL은 여러 곳에서 만들어지고 있었다.
- 게시글 상세 응답
- 게시글 목록 응답
- 댓글 목록 응답
- 마이페이지 프로필 이미지
- S3 업로드 서비스
이런 곳마다 CDN 주소를 직접 문자열로 붙이면, 도메인이 바뀔 때 모든 코드를 찾아 수정해야 한다.
그래서 custom.image.url 값을 설정으로 주입받도록 정리했다.
1
2
@Value("${custom.image.url}")
private String cdnUrl;
이 방식은 아주 거창한 구조는 아니지만, 최소한 CDN 주소가 코드 곳곳에 흩어지는 문제를 줄여준다.
설정값은 환경별로 다르게 둘 수 있고, 코드에서는 cdnUrl을 기준으로 이미지 주소를 만들면 된다.
문제 3. S3 삭제 로직과 CDN URL
이미지 삭제 로직도 함께 정리했다.
기존에는 S3 버킷 주소를 기준으로 key를 잘라내는 방식이었다.
하지만 이미지 URL이 CDN 도메인으로 바뀌면 기존 방식만으로는 key를 제대로 추출하지 못할 수 있다.
그래서 삭제 로직에서 다음 경우를 처리하도록 했다.
- URL이 CDN 도메인으로 시작하는 경우
- URL 안에 bucket 이름이 포함된 경우
- 그 외 일반 URL인 경우 URI path에서 key를 추출하는 경우
이미지를 보여주는 URL과 실제 S3에서 삭제해야 하는 key는 다르다.
CDN을 붙이면 이 차이가 더 분명해진다.
이번 작업을 하면서 업로드보다 삭제 로직이 더 섬세해야 한다는 생각이 들었다.
문제 4. CORS 설정
프론트엔드와 백엔드가 다른 도메인에서 동작하면 CORS 설정이 필요하다.
특히 쿠키나 인증 정보를 포함한 요청에서는 단순히 *로 열어두는 방식이 맞지 않는다.
허용할 origin을 명확히 지정하고, credentials를 허용해야 한다.
Spring Security 설정에 CORS 구성을 추가했다.
1
2
3
4
5
6
7
8
configuration.setAllowedOrigins(List.of(
frontendUrl,
"https://dev.modu-review.com:3000",
"http://dev.modu-review.com:3000"
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
또한 preflight 요청을 위해 OPTIONS 요청을 허용했다.
1
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
이 설정이 없으면 브라우저가 실제 요청을 보내기 전에 CORS preflight 단계에서 막힐 수 있다.
문제 5. 설정 파일 추적 제거
이번 작업에서 중요한 부분 중 하나는 설정 파일 추적 제거였다.
application.yaml, application-server.yaml 같은 파일에는 DB 정보, JWT secret, OAuth client secret, API key처럼 민감한 값이 들어갈 수 있다.
실제 PR에서도 로컬 설정 파일을 Git 추적에서 제거하고 .gitignore에 추가했다.
1
2
src/main/resources/application.yaml
src/main/resources/application-server.yaml
운영 환경에서는 이런 설정을 코드 저장소에 직접 올리기보다, 환경 변수나 서버 설정으로 주입하는 방식이 더 안전하다.
이 작업을 하면서 배포 설정은 기능 코드만큼이나 보안과 직결된다는 것을 다시 느꼈다.
테스트 결과와 남은 문제
작업 후 테스트를 실행했을 때 Java 컴파일은 통과했다.
하지만 NotificationSseIntegrationTest의 SSE 스트림 테스트에서 실패가 있었다.
이 실패는 컴파일 문제라기보다는 기존 SSE 통합 테스트의 timeout 또는 이벤트 수신 타이밍과 관련된 문제로 보였다.
이 지점은 중요한 기록이라고 생각한다.
배포 설정을 바꾸는 작업은 기능 코드와 직접 관련이 없어 보여도, 기존 테스트 전체와 함께 확인해야 한다.
그리고 실패가 발생했을 때는 “이번 변경 때문에 깨진 것인지”, “기존 테스트가 환경에 민감한 것인지”를 나누어 판단해야 한다.
배운 점
이번 CDN/CORS 정리에서 배운 점은 크게 세 가지다.
첫 번째는 하드코딩된 인프라 주소는 기술 부채가 되기 쉽다는 점이다.
초기 개발에서는 빠르게 붙일 수 있지만, 운영 도메인이 정리되면 변경 범위가 커진다.
두 번째는 CORS는 백엔드만의 설정이 아니라 프론트엔드 배포 환경과 함께 봐야 한다는 점이다.
운영 도메인, 개발 도메인, 인증 쿠키, preflight 요청이 모두 맞아야 브라우저에서 정상 동작한다.
세 번째는 설정 파일 관리가 보안의 시작이라는 점이다.
API key나 secret이 담긴 파일은 Git에 남기지 않는 것이 기본이다.
마무리
이번 작업은 기능 추가보다는 운영 환경 정리와 보안 정리에 가까웠다.
CloudFront 기본 도메인을 cdn.modu-review.com으로 바꾸고, 이미지 URL 생성 로직을 설정 기반으로 정리하고, CORS 설정을 명확히 추가하고, 민감 설정 파일을 Git 추적에서 제거했다.
서비스를 만들 때는 기능 구현이 가장 먼저 보이지만, 시간이 지날수록 이런 배포 설정과 운영 정리가 서비스 안정성에 큰 영향을 준다는 것을 느꼈다.
앞으로는 이미지 URL, 프론트 도메인, API 도메인, secret 관리 같은 부분도 초기 설계 단계에서 더 명확히 잡아두려고 한다.
Leave a comment