이전 ν¬μŠ€νŒ…μ—μ„œλŠ” ν”„λ‘ νŠΈ μ„œλ²„μ™€ λ°± μ„œλ²„κ°€ HTTPS 둜 ν†΅μ‹ ν•˜λ„λ‘ Apache μ„œλ²„μ— SSL μΈμ¦μ„œλ₯Ό μ„€μΉ˜ν•˜κ³ , λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ μ„€μ •μœΌλ‘œ μŠ€ν”„λ§ λΆ€νŠΈ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ— μš”μ²­μ΄ μ „λ‹¬λ˜λ„λ‘ λ§Œλ“€μ—ˆλ‹€. 이둜 인해 ν˜Όν•© μ½˜ν…μΈ  였λ₯˜λŠ” ν•΄κ²°ν–ˆμ§€λ§Œ, CORS 였λ₯˜κ°€ μƒˆλ‘­κ²Œ λ°œμƒν–ˆλ‹€.

πŸ‘©πŸ»β€πŸ’» μ—λŸ¬ 원인 μ°ΎκΈ°

1단계 : CORS μ •μ±… λ³€κ²½

제일 처음으둜 μŠ€ν”„λ§ λΆ€νŠΈ CORS μ •μ±…μ—μ„œ addAllowedOrigin 섀정을 μ‹€μ œ ν”„λ‘ νŠΈ μ„œλ²„μ˜ λ„λ©”μΈμœΌλ‘œ λ³€κ²½ν–ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ—¬μ „νžˆ CORS 였λ₯˜κ°€ λ°œμƒν–ˆλŠ”λ°β€¦ λ­”κ°€ μ΄μƒν–ˆλ‹€.

config.addAllowedOrigin("https://www.ncbt.site");

preflight request λΌλŠ” μš©μ–΄κ°€ μ‹ κ²½μ“°μ—¬μ„œ μ°Ύμ•„λ³΄λ‹ˆ, CORS κ°€ λ™μž‘ν•˜λŠ” 방식은 μš”μ²­μ— 따라 달라진닀고 ν•œλ‹€.

μ’…λ₯˜λ©”μ„œλ“œContent-Type
Simple RequestGET, POST, HEADtext/plain, application/x-www-form-urlencoded, multipart/form-data
Preflight RequestSimple Request κ°€ μ•„λ‹Œ κ²½μš°κ°€ ν•΄λ‹Ήν•œλ‹€Β 

λ‹¨μˆœ μš”μ²­μ΄ μ•„λ‹Œ 경우, λΈŒλΌμš°μ €λŠ” μš”μ²­μ„ ν•œ λ²ˆμ— 보내지 μ•Šκ³  μ‚¬μ „μš”μ²­κ³Ό λ³Έ μš”μ²­μœΌλ‘œ λ‚˜λˆ„μ–΄ μ„œλ²„μ— μ „μ†‘ν•œλ‹€κ³  ν•˜κ³  이 λ•Œμ˜ μš”μ²­μ΄ Preflight Request κ°€ λ˜λŠ” 것이닀. 이 경우 OPTIONS λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•œλ‹€κ³  ν•œλ‹€.

λ‚˜λŠ” GET μš”μ²­μ„ λ³΄λƒˆμ§€λ§Œ Content-Type 을 application/json 으둜 μš”μ²­ν–ˆκΈ°μ— Preflight Request 둜 μš”μ²­λœ λ“― ν–ˆλ‹€. κ·Έλž˜μ„œ μ•„λž˜ 섀정을 μΆ”κ°€ν–ˆμ§€λ§Œ μ—¬μ „νžˆ ν•΄κ²°λ˜μ§€ μ•Šμ•˜λ‹€.

// OPTIONS λ©”μ„œλ“œ μΆ”κ°€
config.addAllowedMethod("OPTIONS");


2단계 : μ—λŸ¬ 둜그 확인

μ—¬μ „νžˆ 감이 μ˜€μ§€ μ•Šμ•„μ„œ μŠ€ν”„λ§ μ—λŸ¬ 둜그λ₯Ό 확인해 λ³΄μ•˜λ”λ‹ˆ HTTP request header λ₯Ό νŒŒμ‹±ν•˜λŠ” κ³Όμ •μ—μ„œ 였λ₯˜κ°€ λ°œμƒν•œ 것 κ°™μ•˜λ‹€.

2025-01-04T04:20:41.214Z  INFO 1 --- [backend] [io-8080-exec-10] o.apache.coyote.http11.Http11Processor   : Error parsing HTTP request header
 Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.

java.lang.IllegalArgumentException: Invalid character found in method name [0x050x010x00...]. HTTP method names must be tokens
	at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:407) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:257) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:904) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.28.jar!/:na]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

μ°Ύμ•„λ³΄λ‹ˆ λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλ‘œ μš”μ²­μ„ μ „λ‹¬ν•˜λŠ” κ³Όμ •μ—μ„œ μš”μ²­μ„ μˆ˜μ •ν•˜κ±°λ‚˜ μ†μƒμ‹œν‚¬ 수 μžˆλ‹€κ³  ν•˜μ—¬ μ•„νŒŒμΉ˜ μ„œλ²„μ˜ μ—λŸ¬ λ‘œκ·Έλ„ ν™•μΈν•΄λ³΄μ•˜λ‹€.

# Apache μ„œλ²„λ‘œ λ“€μ–΄μ˜€λŠ” λͺ¨λ“  μš”μ²­ 확인
tail -f /var/log/apache2/access.log

# 좜λ ₯된 둜그
122.35.170.152 - - [04/Jan/2025:13:08:48 +0900] "OPTIONS /refreshToken HTTP/1.1" 403 4180 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
18.191.32.67 - - [04/Jan/2025:13:11:47 +0900] "\x16\x03\x01" 400 483 "-" "-"
122.35.170.152 - - [04/Jan/2025:13:12:28 +0900] "OPTIONS /refreshToken HTTP/1.1" 403 1021 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
122.35.170.152 - - [04/Jan/2025:13:12:40 +0900] "OPTIONS /refreshToken HTTP/1.1" 403 1021 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
18.191.32.67 - - [04/Jan/2025:13:12:47 +0900] "GET / HTTP/1.1" 301 513 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/126.0.0.0 Safari/537.36"
122.35.170.152 - - [04/Jan/2025:13:12:50 +0900] "OPTIONS /refreshToken HTTP/1.1" 403 1021 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
122.35.170.152 - - [04/Jan/2025:13:12:50 +0900] "OPTIONS /ranking/v2 HTTP/1.1" 403 489 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
122.35.170.152 - - [04/Jan/2025:13:22:43 +0900] "OPTIONS /refreshToken HTTP/1.1" 403 4180 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
122.35.170.152 - - [04/Jan/2025:13:22:52 +0900] "GET /form/email-code?email=hwy9597%40naver.com HTTP/1.1" 403 999 "https://www.ncbt.site/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"

λ‚΄κ°€ 보낸 λͺ¨λ“  μš”μ²­μ΄ Apache μ„œλ²„μ—μ„œ 403 μ—λŸ¬λ‘œ λ°˜ν™˜λ˜λŠ” 것을 ν™•μΈν–ˆλ‹€. 생각해보면 ν΄λΌμ΄μ–ΈνŠΈμ˜ μš”μ²­μ΄ 일단 Apache μ„œλ²„λ‘œ λ“€μ–΄κ°€κ³ , Apache κ°€ μŠ€ν”„λ§μœΌλ‘œ λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ ν•΄μ£ΌλŠ” 것이기 λ•Œλ¬Έμ—β€¦ Apache μ—μ„œλ„ CORS 헀더λ₯Ό μΆ”κ°€ν•΄μ•Όκ² λ‹€ μ‹Άμ—ˆλ‹€.


πŸš€ ν•΄κ²°

1단계 : Apache μ—μ„œ CORS μ„€μ •

# VirtualHost μ„€μ •νŒŒμΌ μ—΄κΈ°
vi /etc/apache2/sites-available/000-default.conf
# VirtualHost :*443 λΈ”λŸ­μ— μ•„λž˜ μ½”λ“œ μΆ”κ°€

<VirtualHost *:443>
ServerAdmin yeonguo95@gmail.com # μ„œλ²„ μ—λŸ¬μ‹œ μ‚¬μš©μžμ—κ²Œ μ•ˆλ‚΄λ  이메일
DocumentRoot /var/www/html # 이건 κ·ΈλŒ€λ‘œ 냅두기
ServerName api.ncbt.site # μ„œλ²„ 도메인

# λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ κ΄€λ ¨ μ„€μ •
ProxyRequests Off
ProxyPreserveHost On

# Apache 에 디렉토리 κΆŒν•œ λΆ€μ—¬
<Directory /var/www/html>
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>
<Directory /var/www>
    Require all granted
</Directory>

# CORS 헀더 μΆ”κ°€
<IfModule mod_headers.c>
Header always set Access-Control-Allow-Origin "https://www.ncbt.site"
Header always set Access-Control-Allow-Methods "OPTIONS, GET, POST, PUT, DELETE"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type, X-Custom-Header, Set-Cookie"
Header always set Access-Control-Allow-Credentials "true"
Header always set Access-Control-Expose-Headers "Authorization, Set-Cookie"
</IfModule>

# λ¦¬λ²„μŠ€ ν”„λ‘μ‹œ ν™œμ„±ν™”
<IfModule mod_proxy.c>
    ProxyPreserveHost On
    ProxyPass / http://localhost:8080/
    ProxyPassReverse / http://localhost:8080/
</IfModule>

# ν”„λ¦¬ν”ŒλΌμ΄νŠΈ OPTIONS μš”μ²­ 처리
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]
</VirtualHost>
# κ΄€λ ¨ λͺ¨λ“ˆ ν™œμ„±ν™”
sudo a2enmod headers
sudo a2enmod rewrite
sudo a2enmod proxy
sudo a2enmod proxy_http

# μ•„νŒŒμΉ˜ μž¬μ‹œμž‘
sudo systemctl restart apache2

이후 λ‹€μ‹œ μ‹œλ„ν•΄λ³΄λ‹ˆ, 였λ₯˜ λ©”μ‹œμ§€κ°€ 쑰금 λ³€ν–ˆλ‹€. Access-Control-Allow-Origin 헀더에 닀쀑값이 λ“€μ–΄κ°”λ‹€λŠ” 것…


2단계 : Spring CORS μ„€μ • μ‚­μ œ

μ™œ 헀더에 닀쀑값이 λ“€μ–΄κ°ˆκΉŒ? μƒκ°ν•΄λ³΄λ‹ˆ μš°λ¦¬λŠ” Spring μ„œλ²„μ™€ λ°”λ‘œ ν†΅μ‹ ν•˜λŠ” 것이 μ•„λ‹Œ, λ‘œλ“œλ°ΈλŸ°μ„œ μ—­ν• μ˜ Apache μ„œλ²„μ™€ ν†΅μ‹ ν•˜κ³  μžˆλ‹€. κ·ΈλŸ¬λ―€λ‘œ CORS 섀정은 Apache μ—β€™λ§Œβ€™ ν•΄μ£Όκ³ , Spring μͺ½μ˜ CORS 섀정은 μ‚­μ œν•΄μ•Ό ν•˜λŠ” 것이닀. κ·Έλž˜μ„œ κ΄€λ ¨ 섀정을 μ „λΆ€ 주석 μ²˜λ¦¬ν–ˆλ‹€.


3단계 : Docker μ‹€ν–‰ λ§€μ»€λ‹ˆμ¦˜ λ³€κ²½

계속 μ§„ν–‰ν–ˆμŒμ—λ„ CORS μ—λŸ¬κ°€ 계속 λ°œμƒν–ˆλ‹€. κ·Έλž˜μ„œ 번뜩 λ“  생각이… 도컀 ν—ˆλΈŒμ˜ λ¬Έμ œκ°€ μ•„λ‹κΉŒ? μ‹Άμ—ˆλ‹€. μ™œλƒν•˜λ©΄ μš”μ²­μ΄ λ“€μ–΄κ°„λ‹€λ©΄ λΆ„λͺ… μŠ€ν”„λ§ μ»¨ν…Œμ΄λ„ˆμ— λ‘œκ·Έκ°€ λœ°ν…λ° 아무것도 λœ¨μ§€ μ•ŠλŠ” 것이닀. ν˜Ήμ‹œλΌλ„ μ˜ˆμ „ λ²„μ „μ˜ μ½”λ“œκ°€ κ°±μ‹ λ˜μ§€ μ•Šκ³  μžˆλŠ” 게 μ•„λ‹Œκ°€ μ‹Άμ—ˆλ‹€. (특히 도컀 ν—ˆλΈŒμ—μ„œλŠ” μ½”λ“œλ₯Ό μ‚΄νŽ΄λ³Ό μˆ˜κ°€ μ—†μ–΄μ„œ 변경사항이 λ°˜μ˜λ˜μ—ˆλŠ”μ§€ μ•ŒκΈ° μ–΄λ ΅λ‹€)

κ·Έλž˜μ„œ κ²°κ΅­ Github λ₯Ό μ‚¬μš©ν•΄μ„œ μ»¨ν…Œμ΄λ„ˆλ₯Ό μ‹€ν–‰μ‹œν‚€κΈ°λ‘œ ν–ˆλ‹€.

# 3-1. (둜컬PCμ—μ„œ) μ†ŒμŠ€μ½”λ“œλ₯Ό κΉƒν—ˆλΈŒμ— ν‘Έμ‹œ
git push origin main 

# 3-2. (ν΄λΌμš°λ“œμ—μ„œ) μ†ŒμŠ€μ½”λ“œ λ‚΄λ €λ°›κΈ°
git pull origin main

# 3-3. λΉŒλ“œ μ‹€ν–‰
./gradlew clean build -x test

# 3-4. 도컀 이미지 생성
docker build -t 도컀이미지λͺ…:버전 .

# 3-5. 도컀 μ»¨ν…Œμ΄λ„ˆ μ‹€ν–‰
docker run -d \
--network host \
--name spring-container \
-e SPRING_DATASOURCE_URL=jdbc:mysql://λ‚΄λΆ€IP:3306/ncbt \
-e SPRING_DATASOURCE_USERNAME=아이디 \
-e SPRING_DATASOURCE_PASSWORD=λΉ„λ°€λ²ˆν˜Έ \
-e SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver \
도컀이미지λͺ…:버전

μ΄λ ‡κ²Œ ν–ˆλ”λ‹ˆ 결ꡭ… !! CORS μ—λŸ¬κ°€ 사라지고 μ •μƒμ μœΌλ‘œ 톡신이 κ°€λŠ₯ν•΄μ‘Œλ‹€ !! 고생 끝에 배포 성곡 λ§Œλ§Œμ„Έλ‹€ πŸ™ŒπŸ»


πŸ€“ 배운점

CORS λ™μž‘ 원리

배포 ν™˜κ²½μ—μ„œ 신경써야 ν•  점이 맀우 λ§Žμ•˜λŠ”λ°, CORS λŠ” 정말 λœν†΅ κ³ μƒν–ˆλ‹€.
λ‘œμ»¬μ—μ„œ κ°œλ°œν•  λ•ŒλŠ” κ·Έλƒ₯ λ¦¬μ•‘νŠΈ 포트 localhost:3000 만 ν—ˆμš©ν•΄μ£Όλ©΄ λ˜λ‹ˆκΉŒ λ¬Έμ œκ°€ μ—†μ—ˆλŠ”λ°,
배포λ₯Ό ν•˜κ³  SSL μΈμ¦μ„œλ₯Ό μ μš©ν•΄λ³΄λ‹ˆ κ³ λ €ν•΄μ•Ό ν•  점이 λ§Žμ•˜λ‹€.

preflight request

CORS 의 simple request 와 달리, λ¨Όμ € OPTIONS λ©”μ„œλ“œλ₯Ό 톡해 μ‹€μ œ μš”μ²­μ„ μ „μ†‘ν•˜κΈ°μ— μ•ˆμ „ν•œμ§€ ν™•μΈν•œλ‹€. 200λ²ˆλŒ€ 응닡을 λ°›μœΌλ©΄ λ‹€μ‹œ μ›λž˜μ˜ μš”μ²­μ„ 보낸닀.

μ™œ POSTMAN μ—μ„œλŠ” CORS 였λ₯˜κ°€ λ°œμƒν•˜μ§€ μ•Šμ„κΉŒ ?

CORS μ •μ±… μœ„λ°˜ νŒλ‹¨μ€ β€˜λΈŒλΌμš°μ €β€™ κ°€ μ‹€ν–‰ν•œλ‹€. λ”°λΌμ„œ λΈŒλΌμš°μ € 없이 μ„œλ²„ κ°„ 톡신을 μ§„ν–‰ν•œλ‹€λ©΄ λ‹Ήμ—°νžˆ CORS μ—λŸ¬λ₯Ό 확인할 수 μ—†λŠ” 것이닀.

Apache μ„œλ²„μ— λŒ€ν•œ 이해

μ• μ΄ˆμ— Apache λ₯Ό μ„€μΉ˜ν•œ μ΄μœ λŠ” SSL μΈμ¦μ„œκ°€ μ›Ή μ„œλ²„μ—μ„œ λ™μž‘ν•˜κΈ° λ•Œλ¬Έμ΄μ—ˆλ‹€.
κ·Έλ ‡λ‹€ λ³΄λ‹ˆ Apache μ„œλ²„λŠ” λ‹¨μˆœνžˆ SSL 섀정을 μœ„ν•œ 것이라고 생각이 κ΅³μ–΄μ§€κ²Œ λ˜μ—ˆλŠ”λ°,
싀상은 Load Balancer 의 역할도 κ²Έν•œλ‹€.
μ™ΈλΆ€μ—μ„œ https://api.ncbt.site 둜 μš”μ²­μ΄ λ“€μ–΄μ˜€λŠ” 경우 443 ν¬νŠΈμ—μ„œ λ™μž‘ν•˜λŠ” Apache κ°€ λ¨Όμ € μš”μ²­μ„ λ°›λŠ”λ‹€.
그리고 λ¦¬λ²„μŠ€ ν”„λ‘μ‹œλ‘œ ν•΄λ‹Ή 도메인에 λŒ€ν•œ μš”μ²­μ„ μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μœΌλ‘œ λ³΄λ‚΄μ£ΌλŠ” 것이닀.
λ§ν•˜μžλ©΄ λŒ€λ¬Έ 역할인 것인데, 이걸 깊이 μƒκ°ν•˜μ§€ λͺ»ν•˜κ³  ν•˜λ˜λŒ€λ‘œ μŠ€ν”„λ§μ—μ„œ CORS 섀정을 ν•˜λ €κ³  ν•˜λ‹ˆ λ¬Έμ œκ°€ 생긴 κ²ƒμ΄μ—ˆλ‹€.
Apache 와 μŠ€ν”„λ§μ€ localhost 둜 내뢀적인 톡신을 ν•˜κ³  μžˆμœΌλ‹ˆ μ„œλ‘œ κ°„μ˜ CORS 섀정은 ν•„μš”κ°€ μ—†λ‹€.
λ”°λΌμ„œ, λŒ€λ¬Έμ΄ λ˜λŠ” Apache μ—μ„œλ§Œ CORS 섀정을 ν•΄μ£Όλ©΄ λ˜λŠ” κ²ƒμ΄μ—ˆλ‹€.


πŸ”– 참고자료

Preflight Request λž€?