๐Ÿš€ Spring Boot ํ”„๋กœ์ ํŠธ์˜ Render ๋ฐฐํฌ ์‹คํŒจ ์‚ฌ๋ก€ ์ •๋ฆฌ: gradlew, JAVA_HOME, Dockerfile ๋ฌธ์ œ ํ•ด๊ฒฐ

@leekh8 ยท May 13, 2025 ยท 19 min read

Code N Solve ๐Ÿ“˜: Spring Boot ํ”„๋กœ์ ํŠธ์˜ Render ๋ฐฐํฌ ์‹คํŒจ ์‚ฌ๋ก€ ์ •๋ฆฌ

์ง€๋‚œ๋ฒˆ Vite ๋ฐฐํฌ ์˜ค๋ฅ˜ ๊ธ€๊ณผ๋Š” ๋‹ฌ๋ฆฌ, Java + Spring Boot + Gradle1 ํ™˜๊ฒฝ์˜ ๋ฐฑ์—”๋“œ ํ”„๋กœ์ ํŠธ๋ฅผ Render2์— Docker๋กœ ๋ฐฐํฌํ•  ๋•Œ ๊ฒช์€ ๋นŒ๋“œ ์‹คํŒจ ์‚ฌ๋ก€์™€ ํ•ด๊ฒฐ ๊ณผ์ •์„ ์ •๋ฆฌํ•˜์˜€๋‹ค.

๊ฐ๊ฐ์˜ ์˜ค๋ฅ˜๋ฅผ ๋ถ„์„ํ•˜๊ณ  ํ•ด๊ฒฐํ•œ ๊ณผ์ •์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์ž.

Vite๋‚˜ Node.js ํ™˜๊ฒฝ์ด ์•„๋‹Œ, Java ๊ธฐ๋ฐ˜ ์„œ๋ฒ„๋ฅผ Docker ํ™˜๊ฒฝ์—์„œ Render๋กœ ๋ฐฐํฌํ•˜๋ ค๊ณ  ํ•˜๋Š” ๋‹ค๋ฅธ ์‚ฌ๋žŒ์—๊ฒŒ ๋„์›€์ด ๋˜๋ฉด ์ข‹๊ฒ ๋‹ค.


๐Ÿ“‹ ๋ชฉ์ฐจ

  1. Render ๋ฌด๋ฃŒ ํ”Œ๋žœ ํŠน์„ฑ ์ดํ•ด
  2. Render ๋ฐฐํฌ ์ „์ฒด ํ๋ฆ„
  3. ์˜ค๋ฅ˜ 1 - ./gradlew Permission denied
  4. ์˜ค๋ฅ˜ 2 - Gradle Wrapper ์—†์Œ
  5. ์˜ค๋ฅ˜ 3 - JAVA_HOME ์„ค์ • ์˜ค๋ฅ˜
  6. ์˜ค๋ฅ˜ 4 - Gradle Toolchain ์—๋Ÿฌ
  7. ์˜ค๋ฅ˜ 5 - render.yaml ์„ค์ •
  8. ์˜ค๋ฅ˜ 6 - DATABASE_URL ์ž๋™ ์ œ๊ณต vs H2
  9. ์ถ”๊ฐ€ ์˜ค๋ฅ˜ ์ผ€์ด์Šค
  10. ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ Dockerfile ์ตœ์ ํ™”
  11. .dockerignore ์„ค์ •
  12. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ
  13. Health Check ์„ค์ •
  14. ๋กœ์ปฌ Docker ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•
  15. ๊ฒฐ๋ก 

Render ๋ฌด๋ฃŒ ํ”Œ๋žœ ํŠน์„ฑ ์ดํ•ด

Spring Boot ํ”„๋กœ์ ํŠธ๋ฅผ Render์— ์˜ฌ๋ฆฌ๊ธฐ ์ „์—, ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ ํŠน์„ฑ์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค. ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ ์ œ์•ฝ์„ ๋ชจ๋ฅด๊ณ  ๋ฐฐํฌํ•˜๋ฉด, ๋ฐฐํฌ ์ž์ฒด๋Š” ์„ฑ๊ณตํ–ˆ๋Š”๋ฐ ์„œ๋น„์Šค๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์„ ๊ฒช๊ฒŒ ๋œ๋‹ค.

์ฝœ๋“œ ์Šคํƒ€ํŠธ(Cold Start)์™€ ์Šฌ๋ฆฝ ๋ชจ๋“œ(Sleep Mode)

Render ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ ์›น ์„œ๋น„์Šค๋Š” 15๋ถ„ ๋™์•ˆ ํŠธ๋ž˜ํ”ฝ์ด ์—†์œผ๋ฉด ์Šฌ๋ฆฝ(sleep) ์ƒํƒœ๋กœ ์ „ํ™˜๋œ๋‹ค. ์ดํ›„ ์ƒˆ๋กœ์šด ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ๋‹ค์‹œ ๊นจ์–ด๋‚˜๋Š”๋ฐ, ์ด ๊ณผ์ •์—์„œ 30์ดˆ~1๋ถ„ ์ด์ƒ์˜ ์ฝœ๋“œ ์Šคํƒ€ํŠธ ์ง€์—ฐ์ด ๋ฐœ์ƒํ•œ๋‹ค.

Spring Boot๋Š” JVM ๊ธฐ๋ฐ˜์ด๋ผ ์ผ๋ฐ˜์ ์ธ Node.js ์„œ๋ฒ„๋ณด๋‹ค ๊ธฐ๋™ ์‹œ๊ฐ„์ด ๊ธธ๋‹ค. ์ผ๋ฐ˜ Spring Boot ์•ฑ์˜ ๊ฒฝ์šฐ JVM ์›œ์—… ํฌํ•จ 30~60์ดˆ, ๋Œ€ํ˜• ์•ฑ์€ ๊ทธ ์ด์ƒ์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

์ฝœ๋“œ ์Šคํƒ€ํŠธ๋ฅผ ์™„ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•:

# render.yaml
services:
  - type: web
    name: my-spring-app
    env: docker
    plan: free
    healthCheckPath: /actuator/health

healthCheckPath๋ฅผ ์„ค์ •ํ•ด๋‘๋ฉด Render๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ๋ณด๋‚ด ์Šฌ๋ฆฝ ์ƒํƒœ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ์•ฝ๊ฐ„ ๋„์›€์ด ๋œ๋‹ค. ํ•˜์ง€๋งŒ ์™„๋ฒฝํ•œ ํ•ด๊ฒฐ์ฑ…์€ ์•„๋‹ˆ๋ฉฐ, ๊ทผ๋ณธ์ ์œผ๋กœ๋Š” ์œ ๋ฃŒ ํ”Œ๋žœ์œผ๋กœ ์ „ํ™˜ํ•˜๊ฑฐ๋‚˜ ์™ธ๋ถ€ ์—…ํƒ€์ž„ ๋ชจ๋‹ˆํ„ฐ๋ง ์„œ๋น„์Šค(UptimeRobot ๋“ฑ)๋ฅผ ์ด์šฉํ•ด ์ฃผ๊ธฐ์ ์œผ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” ๋ฐฉ๋ฒ•์„ ์“ฐ๊ธฐ๋„ ํ•œ๋‹ค.

๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ

Render ๋ฌด๋ฃŒ ํ”Œ๋žœ์€ 512MB RAM ์ œํ•œ์ด ์žˆ๋‹ค. Spring Boot ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ ๊ธฐ๋™ ์‹œ๋ถ€ํ„ฐ ์ƒ๋‹นํ•œ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, JVM ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ๋Š” ์ด ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ•˜๊ธฐ ์‰ฝ๋‹ค.

ํ•ญ๋ชฉ ๋ฌด๋ฃŒ ํ”Œ๋žœ ์Šคํƒ€ํ„ฐ ํ”Œ๋žœ ($7/์›”) ์Šคํƒ ๋‹ค๋“œ ํ”Œ๋žœ ($25/์›”)
RAM 512MB 512MB 2GB
CPU ๊ณต์œ  0.5 CPU 1 CPU
์Šฌ๋ฆฝ ๋ชจ๋“œ ์žˆ์Œ (15๋ถ„) ์—†์Œ ์—†์Œ
๋นŒ๋“œ ์‹œ๊ฐ„ ์›” 400๋ถ„ ์›” 400๋ถ„ ์›” 400๋ถ„
๋Œ€์—ญํญ 100GB/์›” 100GB/์›” 100GB/์›”

๋ฌด๋ฃŒ ํ”Œ๋žœ์—์„œ Spring Boot๋ฅผ ์“ธ ๋•Œ ์ฃผ์˜ํ•  ์ 

  1. JVM ํž™ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ œํ•œํ•ด์•ผ ํ•œ๋‹ค. ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด JVM์ด ์ปจํ…Œ์ด๋„ˆ ๋ฉ”๋ชจ๋ฆฌ์˜ 25%๋ฅผ ์ž๋™์œผ๋กœ ์ตœ๋Œ€ ํž™์œผ๋กœ ์„ค์ •ํ•˜์ง€๋งŒ, ์ปจํ…Œ์ด๋„ˆ ํ™˜๊ฒฝ์—์„œ ์ด ๊ฐ์ง€๊ฐ€ ๋ถ€์ •ํ™•ํ•  ์ˆ˜ ์žˆ๋‹ค.

  2. ๋ถˆํ•„์š”ํ•œ Spring Boot ์Šคํƒ€ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•ด ๊ธฐ๋™ ์‹œ๊ฐ„๊ณผ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ค„์ธ๋‹ค.

  3. Spring Native(GraalVM) ๋˜๋Š” Spring AOT๋ฅผ ๊ณ ๋ คํ•˜๋ฉด ๊ธฐ๋™ ์‹œ๊ฐ„์„ ์ˆ˜ ์ดˆ ์ดํ•˜๋กœ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค. ๋‹จ, ๋นŒ๋“œ ๋ณต์žก๋„๊ฐ€ ๋†’์•„์ง„๋‹ค.

  4. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ํ’€ ํฌ๊ธฐ๋ฅผ ์ค„์ธ๋‹ค. HikariCP ๊ธฐ๋ณธ ํ’€ ํฌ๊ธฐ(10)๋ฅผ 2~3์œผ๋กœ ์ค„์ด๋ฉด ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ์— ๋„์›€์ด ๋œ๋‹ค.

# application.properties
spring.datasource.hikari.maximum-pool-size=3
spring.datasource.hikari.minimum-idle=1

Render ๋ฐฐํฌ ์ „์ฒด ํ๋ฆ„

์‹ค์ œ ๋ฐฐํฌ๊ฐ€ ์–ด๋–ค ๊ณผ์ •์„ ๊ฑฐ์น˜๋Š”์ง€ ๋จผ์ € ์ „์ฒด ๊ทธ๋ฆผ์„ ํŒŒ์•…ํ•ด๋‘๋ฉด ๊ฐ ๋‹จ๊ณ„์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ดํ•ดํ•˜๊ธฐ ํ›จ์”ฌ ์‰ฝ๋‹ค.

์‹คํŒจ
์„ฑ๊ณต
์‹คํŒจ
์„ฑ๊ณต
๊ฐœ๋ฐœ์ž: ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐ ์ปค๋ฐ‹
GitHub์— git push
Render Webhook ๊ฐ์ง€
Render ๋นŒ๋“œ ์„œ๋ฒ„ ์‹œ์ž‘
GitHub ์ €์žฅ์†Œ ํด๋ก 
render.yaml ํ™•์ธ
env: docker ์„ค์ • ํ™•์ธ
Dockerfile ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
Docker ๋นŒ๋“œ ์‹œ์ž‘
๋นŒ๋“œ ์Šคํ…Œ์ด์ง€: JDK ์ด๋ฏธ์ง€ ์‚ฌ์šฉ
./gradlew build ์‹คํ–‰
๋นŒ๋“œ ์„ฑ๊ณต ์—ฌ๋ถ€
๋นŒ๋“œ ๋กœ๊ทธ ์ถœ๋ ฅ ๋ฐ ์ข…๋ฃŒ
๋Ÿฐํƒ€์ž„ ์Šคํ…Œ์ด์ง€: JRE ์ด๋ฏธ์ง€๋กœ ๋ณต์‚ฌ
Docker ์ด๋ฏธ์ง€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ํ‘ธ์‹œ
๊ธฐ์กด ์ปจํ…Œ์ด๋„ˆ ์ข…๋ฃŒ
์ƒˆ ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋™
Health Check ํ†ต๊ณผ ์—ฌ๋ถ€
์ด์ „ ๋ฐฐํฌ๋กœ ๋กค๋ฐฑ
๋ฐฐํฌ ์™„๋ฃŒ - ํŠธ๋ž˜ํ”ฝ ์ „ํ™˜

์ด ํ๋ฆ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์ ์€ ํฌ๊ฒŒ ์„ธ ๊ตฐ๋ฐ๋‹ค:

  • ํด๋ก  ์งํ›„: gradlew ํŒŒ์ผ ๋ˆ„๋ฝ, ๊ถŒํ•œ ๋ฌธ์ œ
  • Docker ๋นŒ๋“œ ์ค‘: JAVA_HOME, Toolchain ๋ถˆ์ผ์น˜, ๋ฉ”๋ชจ๋ฆฌ ๋ถ€์กฑ
  • ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋™ ์ค‘: ํฌํŠธ ์„ค์ •, ํ™˜๊ฒฝ๋ณ€์ˆ˜, Health Check ์‹คํŒจ

๐Ÿšจ 1. ./gradlew ์‹คํ–‰ ์˜ค๋ฅ˜ - Permission denied

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

./gradlew: Permission denied

๐Ÿง ์›์ธ ๋ถ„์„

  • Git์—์„œ ํŒŒ์ผ ๊ถŒํ•œ์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ถ”์ ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ๋กœ์ปฌ์—์„œ๋Š” ์‹คํ–‰๋˜๋˜ ./gradlew ํŒŒ์ผ์ด ๋ฆฌ๋ˆ…์Šค ๋ฐฐํฌ ํ™˜๊ฒฝ(Render ๋“ฑ)์—์„œ๋Š” ์‹คํ–‰ ๊ถŒํ•œ(x) ์—†์ด ๋ณต์ œ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Œ.

  • ํŠนํžˆ gradlew๋Š” .gitignore๋กœ ์ œ์™ธ๋˜์ง€ ์•Š๋”๋ผ๋„ chmod +x๋กœ ๋ถ€์—ฌ๋œ ์‹คํ–‰ ๊ถŒํ•œ ์ž์ฒด๊ฐ€ Git ์ปค๋ฐ‹์— ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์œผ๋ฉฐ, ์ด๋Š” Linux ํ™˜๊ฒฝ์—์„œ Permission denied ์˜ค๋ฅ˜๋ฅผ ์œ ๋ฐœํ•จ.

  • Windows๋‚˜ macOS ํ™˜๊ฒฝ์—์„œ๋Š” ์‹คํ–‰ ๊ถŒํ•œ์ด ๋ฌธ์ œ ์—†์ด ์ ์šฉ๋  ์ˆ˜ ์žˆ์œผ๋‚˜, Render์˜ Docker ๋นŒ๋“œ๋Š” Linux ๊ธฐ๋ฐ˜์ด๋ฏ€๋กœ ์ด ๊ถŒํ•œ์ด ์—„๊ฒฉํžˆ ์š”๊ตฌ๋จ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฐฉ๋ฒ• 1. ๋กœ์ปฌ์—์„œ ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ ํ›„ ์ปค๋ฐ‹

chmod +x ./gradlew
git add gradlew
git commit -m "fix: gradlew ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ"
git push

Git์€ ์‹คํ–‰ ๋น„ํŠธ(executable bit)๋ฅผ ์ถ”์ ํ•œ๋‹ค. chmod +x ํ›„ git addํ•˜๋ฉด Git์ด ํŒŒ์ผ ๋ชจ๋“œ ๋ณ€๊ฒฝ์„ ๊ธฐ๋กํ•˜๊ณ , ์ดํ›„ ํด๋ก  ์‹œ Linux ํ™˜๊ฒฝ์—์„œ๋„ ์‹คํ–‰ ๊ถŒํ•œ์ด ์œ ์ง€๋œ๋‹ค.

๋ฐฉ๋ฒ• 2. Dockerfile์—์„œ ๊ถŒํ•œ ๋ถ€์—ฌ (๊ถŒ์žฅ)

๋กœ์ปฌ ์ปค๋ฐ‹ ์ƒํƒœ์™€ ๋ฌด๊ด€ํ•˜๊ฒŒ, Dockerfile ์•ˆ์—์„œ ๊ถŒํ•œ์„ ๋ช…์‹œ์ ์œผ๋กœ ๋ถ€์—ฌํ•˜๋ฉด ๊ฐ€์žฅ ์•ˆ์ „ํ•˜๋‹ค.

COPY . .
RUN chmod +x ./gradlew
RUN ./gradlew build --no-daemon

์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉด ์–ด๋–ค ํ™˜๊ฒฝ์—์„œ ํด๋ก ํ•˜๋”๋ผ๋„ ๋นŒ๋“œ๊ฐ€ ์ผ๊ด€๋˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.

๋ฐฉ๋ฒ• 3. Git ์„ค์ •์œผ๋กœ ๊ถŒํ•œ ์ถ”์  ๊ฐ•์ œ

git config core.fileMode true

์ด ์„ค์ • ํ›„ chmod +x gradlew๋ฅผ ๋‹ค์‹œ ์‹คํ–‰ํ•˜๊ณ  ์ปค๋ฐ‹ํ•˜๋ฉด ํŒŒ์ผ ๋ชจ๋“œ ๋ณ€๊ฒฝ์ด ์ถ”์ ๋œ๋‹ค.


๐Ÿšจ 2. Gradle Wrapper ์—†์Œ - GradleWrapperMain ์˜ค๋ฅ˜

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain

๐Ÿง ์›์ธ ๋ถ„์„

  • Gradle ํ”„๋กœ์ ํŠธ๋ฅผ Git์œผ๋กœ ๊ด€๋ฆฌํ•  ๋•Œ, ์ข…์ข… .gitignore์— ์˜ํ•ด ๋ฌด์‹ฌ์ฝ” ์ œ์™ธ๋˜๊ฑฐ๋‚˜ ์ปค๋ฐ‹ ๋ˆ„๋ฝ๋˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Œ.

  • ํŠนํžˆ ๋‹ค์Œ ํŒŒ์ผ๋“ค์€ Gradle์˜ Wrapper3 ๊ธฐ๋Šฅ์„ ๊ตฌ์„ฑํ•˜๋Š” ํ•ต์‹ฌ ํŒŒ์ผ๋กœ, Wrapper ๋ฐฉ์‹์˜ ๋นŒ๋“œ๋Š” Gradle์ด ์„ค์น˜๋˜์ง€ ์•Š์€ ํ™˜๊ฒฝ์—์„œ๋„ ๋นŒ๋“œ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์คŒ.

    • gradlew
    • gradlew.bat
    • gradle/wrapper/gradle-wrapper.jar
    • gradle/wrapper/gradle-wrapper.properties
  • ๋”ฐ๋ผ์„œ ์œ„ ํŒŒ์ผ๋“ค์ด ๋ˆ„๋ฝ๋˜๋ฉด ./gradlew ์‹คํ–‰ ์‹œ ๋‚ด๋ถ€์ ์œผ๋กœ org.gradle.wrapper.GradleWrapperMain ํด๋ž˜์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. .gitignore์— ์˜ํ•ด ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ ๋‹ค์Œ ํŒŒ์ผ๋“ค์„ ๋ฐ˜๋“œ์‹œ ์ปค๋ฐ‹

gradlew
gradlew.bat
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties

์ด ํŒŒ์ผ๋“ค์€ Gradle ๋นŒ๋“œ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ์š”์†Œ๋กœ, Gradle ์„ค์น˜ ์—†์ด๋„ ./gradlew ๋ช…๋ น์–ด๋กœ ๋นŒ๋“œ๊ฐ€ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•œ๋‹ค.

2. .gitignore ํ™•์ธ ๋ฐ ์ˆ˜์ •

ํ”ํžˆ ์‚ฌ์šฉํ•˜๋Š” .gitignore ํ…œํ”Œ๋ฆฟ์—์„œ๋Š” *.jar๋ฅผ ์ „๋ถ€ ์ œ์™ธํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด ๋•Œ gradle-wrapper.jar๋„ ํ•จ๊ป˜ ์ œ์™ธ๋  ์ˆ˜ ์žˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

# Gradle
.gradle/
build/

# Gradle Wrapper๋Š” ์ปค๋ฐ‹ ๋Œ€์ƒ (์•„๋ž˜ ์ค„๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ)
!gradle/wrapper/gradle-wrapper.jar

3. Wrapper ํŒŒ์ผ ์žฌ์ƒ์„ฑ

์ด๋ฏธ ํŒŒ์ผ์ด ์—†์–ด์กŒ๋‹ค๋ฉด Gradle์ด ์„ค์น˜๋œ ํ™˜๊ฒฝ์—์„œ ์ƒˆ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

gradle wrapper --gradle-version 8.5

๐Ÿšจ 3. JAVA_HOME ๊ด€๋ จ ์˜ค๋ฅ˜

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

Docker ๋นŒ๋“œ ๊ณผ์ •์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ.

ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

๐Ÿง ์›์ธ ๋ถ„์„

  • Render์˜ ๊ธฐ๋ณธ ๋ฐฐํฌ ํ™˜๊ฒฝ์€ Node.js๋‚˜ Python๊ณผ ๊ฐ™์€ ์–ธ์–ด์—๋Š” ์นœํ™”์ ์ด์ง€๋งŒ, Java๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ํ™˜๊ฒฝ์ด ๊ธฐ๋ณธ์ ์œผ๋กœ ์ œ๊ณต๋˜์ง€ ์•Š์Œ.

  • ํŠนํžˆ env: docker ์„ค์ •์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ, ๋ณ„๋„์˜ Docker ์ด๋ฏธ์ง€์—์„œ Java๊ฐ€ ์„ค์น˜๋˜์ง€ ์•Š์œผ๋ฉด Gradle ๋นŒ๋“œ ๊ณผ์ •์—์„œ JAVA_HOME ์„ค์ •์ด ์—†๋‹ค๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ฒŒ ๋จ.

  • Java ๊ธฐ๋ฐ˜์˜ ๋นŒ๋“œ๋ฅผ ์œ„ํ•œ ํ™˜๊ฒฝ์€ ์ง์ ‘ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•˜๋ฉฐ, Gradle ๋นŒ๋“œ ์‹œ ๋‚ด๋ถ€์ ์œผ๋กœ Java compiler๋ฅผ ์ฐพ์ง€ ๋ชปํ•˜๋ฉด ์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. Dockerfile์„ ์‚ฌ์šฉํ•ด Java๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์น˜

# Dockerfile (๊ธฐ๋ณธ ๋‹จ์ผ ์Šคํ…Œ์ด์ง€ - ๋น ๋ฅธ ์‹œ์ž‘์šฉ)
FROM eclipse-temurin:21-jdk

WORKDIR /app
COPY . .

RUN chmod +x ./gradlew
RUN ./gradlew build --no-daemon

CMD find ./build/libs -name "*.jar" ! -name "*plain*" | xargs java -jar
  • Render์—์„œ Java๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด Dockerfile์—์„œ Java๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์น˜ํ•จ.
  • eclipse-temurin4์€ OpenJDK๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ณต์‹ Docker ์ด๋ฏธ์ง€.
  • ! -name "*plain*" ์กฐ๊ฑด์„ ์ถ”๊ฐ€ํ•ด Spring Boot๊ฐ€ ์ƒ์„ฑํ•˜๋Š” -plain.jar(์˜์กด์„ฑ ์—†๋Š” jar)๋ฅผ ์ œ์™ธํ•˜๊ณ  ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ fat jar๋งŒ ์„ ํƒํ•œ๋‹ค.

๐Ÿšจ 4. Gradle Toolchain ์—๋Ÿฌ (Java 17 ์š”๊ตฌ)

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

Failed to calculate the value of task ':compileJava' property 'javaCompiler'.
Cannot find a Java installation matching: {languageVersion=17...}

๐Ÿง ์›์ธ ๋ถ„์„

  • Gradle Toolchain5 ๊ธฐ๋Šฅ์€ ํŠน์ • Java ๋ฒ„์ „์œผ๋กœ์˜ ์ผ๊ด€๋œ ๋นŒ๋“œ๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋จ.

    • ์˜ˆ: JavaLanguageVersion.of(17).
  • ๊ทธ๋Ÿฌ๋‚˜ Render์˜ Docker ๋นŒ๋“œ ํ™˜๊ฒฝ์—์„œ๋Š” ์ž๋™์œผ๋กœ ํ•„์š”ํ•œ Java ๋ฒ„์ „์„ ๋‹ค์šด๋กœ๋“œํ•˜์ง€ ์•Š์Œ.

  • ๋”ฐ๋ผ์„œ toolchain์— ๋ช…์‹œ๋œ ๋ฒ„์ „๊ณผ Docker ์ด๋ฏธ์ง€์˜ JDK ๋ฒ„์ „์ด ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ๋นŒ๋“œ ์‹œ Gradle์ด ํ•ด๋‹น ๋ฒ„์ „์„ ์ฐพ์ง€ ๋ชปํ•ด ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ด.

  • Java toolchain ์„ค์ •์ด ์ ์šฉ๋œ ์ƒํƒœ์—์„œ eclipse-temurin:21-jdk ๋“ฑ ์ƒ์œ„ ๋ฒ„์ „์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์—๋„ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

๋ฐฉ๋ฒ• 1. build.gradle์—์„œ toolchain ์ œ๊ฑฐ

// toolchain ์„ค์ •์„ ์ œ๊ฑฐํ•˜๊ณ  ๋‹จ์ˆœํ™”
tasks.withType(JavaCompile) {
  options.encoding = 'UTF-8'
}

build.gradle์—์„œ toolchain ์ œ๊ฑฐ ๋˜๋Š” Java ๋ฒ„์ „ ์ž๋™ ๋งค์นญ ์ œ๊ฑฐ.

๋ฐฉ๋ฒ• 2. Java ๋ฒ„์ „ Docker์™€ ๋งž์ถ”๊ธฐ

FROM eclipse-temurin:17-jdk

Dockerfile์˜ JDK ๋ฒ„์ „์„ Gradle toolchain๊ณผ ์ผ์น˜์‹œํ‚ด.

  • ์ถ”๊ฐ€๋กœ, Gradle์˜ toolchain ์„ค์ •์ด ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋Š” Dockerfile์—์„œ ๋™์ผํ•œ JDK ๋ฒ„์ „์„ ๋ช…์‹œ์ ์œผ๋กœ ํฌํ•จ์‹œ์ผœ์•ผ ํ•จ.

    java {
      toolchain {
        languageVersion = JavaLanguageVersion.of(17)
      }
    }

    ์ด ๊ฒฝ์šฐ, ๋นŒ๋“œ ํ™˜๊ฒฝ์—์„œ๋„ ์ •ํ™•ํžˆ JDK 17์ด ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ์ •์ƒ ์ž‘๋™ํ•จ.


๐Ÿšจ 5. Render ๋ฐฐํฌ ์„ค์ • - render.yaml

๐Ÿง ์›์ธ ๋ถ„์„

  • Github ์ €์žฅ์†Œ ๋ฃจํŠธ์— render.yaml์ด ์žˆ์–ด์•ผ ํ•จ.

  • Dockerfile ๊ธฐ์ค€์œผ๋กœ ๋นŒ๋“œ๋˜๋„๋ก ์„ค์ • ํ•„์š”ํ•จ.

  • Dockerfile์ด ์กด์žฌํ•˜๊ณ  CMD ๊ตฌ๋ฌธ์—์„œ .jar๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•จ.

  • ์—ฌ๋Ÿฌ ์„œ๋น„์Šค๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ healthCheckPath, buildCommand ๋“ฑ ์ถ”๊ฐ€ ์„ค์ • ๊ฐ€๋Šฅ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. render.yaml ๊ธฐ๋ณธ ์ž‘์„ฑ

services:
  - type: web
    name: my-spring-app
    env: docker
    plan: free

2. render.yaml ๊ถŒ์žฅ ์ž‘์„ฑ (์ „์ฒด ์˜ต์…˜ ํฌํ•จ)

services:
  - type: web
    name: my-spring-app
    env: docker
    plan: free
    region: singapore   # ์ง€์—ญ ์„ค์ • (asia์—์„œ๋Š” singapore ์ถ”์ฒœ)
    healthCheckPath: /actuator/health
    envVars:
      - key: SPRING_PROFILES_ACTIVE
        value: prod
      - key: DATABASE_URL
        fromDatabase:
          name: my-postgres-db
          property: connectionString
      - key: JWT_SECRET
        sync: false   # Render ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ง์ ‘ ์ž…๋ ฅํ•˜๋Š” ์‹œํฌ๋ฆฟ

databases:
  - name: my-postgres-db
    plan: free
    databaseName: myapp
    user: myapp_user
  • fromDatabase๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด PostgreSQL ์ธ์Šคํ„ด์Šค์˜ ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด์„ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ž๋™ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • sync: false๋Š” ํ•ด๋‹น ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ Render ๋Œ€์‹œ๋ณด๋“œ์—์„œ ์ง์ ‘ ์ž…๋ ฅํ•ด์•ผ ํ•˜๋Š” ์‹œํฌ๋ฆฟ์œผ๋กœ ํ‘œ์‹œํ•œ๋‹ค. render.yaml์„ Git์— ์˜ฌ๋ ค๋„ ๊ฐ’์ด ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.

๐Ÿšจ 6. DATABASE_URL ์ž๋™ ์ œ๊ณต vs H2

  • Render๋Š” PostgreSQL ์‚ฌ์šฉ ์‹œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ DATABASE_URL์„ ์ž๋™ ์ œ๊ณตํ•จ.6

  • postgres://<user>:<password>@<host>:<port>/<dbname> ํ˜•์‹์˜ ์ ‘์† ๋ฌธ์ž์—ด๋กœ, Spring Boot์—์„œ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์ •๋ณด๋ฅผ ํ•œ ์ค„๋กœ ์ œ๊ณตํ•จ.

  • ๊ธฐ์กด์— H2 (in-memory)๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด application.properties์— ๋‹ค์Œ์„ ๋ฐ˜์˜ํ•ด์•ผ ํ•จ.

๐Ÿง ์›์ธ ๋ถ„์„

  • ๋กœ์ปฌ ๊ฐœ๋ฐœ์—์„œ H2 ์ธ๋ฉ”๋ชจ๋ฆฌ DB๋ฅผ ์‚ฌ์šฉํ•˜๋‹ค๊ฐ€, Render์™€ ๊ฐ™์€ ์šด์˜ ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ๋Š” PostgreSQL ๊ฐ™์€ ์˜์†์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ํ•„์š”ํ•จ.

  • Render๋Š” PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๋ฉด ์ž๋™์œผ๋กœ DATABASE_URL ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ด์ฃผ์ง€๋งŒ, Spring Boot์˜ application.properties์—์„œ ์ด ๊ฐ’์„ ์ฝ๋„๋ก ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ H2๋ฅผ ๊ณ„์† ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•จ.

  • ๋˜ํ•œ PostgreSQL์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด JDBC ๋“œ๋ผ์ด๋ฒ„ ์˜์กด์„ฑ์„ ๋ณ„๋„๋กœ ์ถ”๊ฐ€ํ•ด์•ผ ํ•˜๋ฉฐ, ๋กœ์ปฌ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ๋˜ ์„ค์ •์ด Render์—์„œ๋Š” ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. PostgreSQL ์‚ฌ์šฉ ์‹œ ์„ค์ •

# application.properties
spring.datasource.url=${DATABASE_URL}
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
// build.gradle
dependencies {
  implementation 'org.postgresql:postgresql:42.7.3'
}

PostgreSQL ๋“œ๋ผ์ด๋ฒ„ ์˜์กด์„ฑ๋„ build.gradle์— ์ถ”๊ฐ€ ํ•„์š”ํ•จ.

โ›” H2์—์„œ ์ „ํ™˜ ์‹œ ์ฃผ์˜

  • ๋กœ์ปฌ ๊ฐœ๋ฐœ๊ณผ ๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ DB๊ฐ€ ๋‹ค๋ฅผ ๊ฒฝ์šฐ, ๋ฐ์ดํ„ฐ ์Šคํ‚ค๋งˆ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Flyway ๋˜๋Š” Liquibase ๋“ฑ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํˆด์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Œ.

  • Render์—์„œ ์ œ๊ณตํ•˜๋Š” DATABASE_URL ํ™˜๊ฒฝ๋ณ€์ˆ˜๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ํ˜•์‹.

    • postgres://username:password@hostname:port/dbname
  • ํ•ด๋‹น URL์€ spring.datasource.url์— ๊ทธ๋Œ€๋กœ ๋„ฃ์œผ๋ฉด Spring Boot์—์„œ ์ž๋™์œผ๋กœ ๋ถ„๋ฆฌ ํŒŒ์‹ฑํ•˜์—ฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•จ.


๐Ÿ”ฅ ์ถ”๊ฐ€ ์˜ค๋ฅ˜ ์ผ€์ด์Šค

์‹ค์ œ ์šด์˜์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜์ง€๋งŒ ์•ž์„  ์ ˆ์—์„œ ๋‹ค๋ฃจ์ง€ ์•Š์€ ์˜ค๋ฅ˜ ์ผ€์ด์Šค๋“ค์ด๋‹ค. ์ด ๋ถ€๋ถ„์„ ๋†“์น˜๋ฉด ๋ฐฐํฌ ์„ฑ๊ณต ํ›„์—๋„ ์„œ๋น„์Šค๊ฐ€ ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ๋‹ค.

Out of Memory (OOM) ์˜ค๋ฅ˜

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

java.lang.OutOfMemoryError: Java heap space

๋˜๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ OOM Killer์— ์˜ํ•ด ๊ฐ‘์ž๊ธฐ ์ข…๋ฃŒ๋˜์–ด ๋กœ๊ทธ ์—†์ด ์žฌ์‹œ์ž‘๋˜๋Š” ํ˜„์ƒ.

๐Ÿง ์›์ธ ๋ถ„์„

Render ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ 512MB ์ œํ•œ ์•ˆ์—์„œ JVM์ด ๋™์ž‘ํ•˜๋ ค๋ฉด ํž™ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๋ฐ˜๋“œ์‹œ ์ œํ•œํ•ด์•ผ ํ•œ๋‹ค. JVM์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ปจํ…Œ์ด๋„ˆ ์ „์ฒด ๋ฉ”๋ชจ๋ฆฌ์˜ ์ƒ๋‹น ๋ถ€๋ถ„์„ ํž™์œผ๋กœ ์žก์œผ๋ ค ํ•˜๋Š”๋ฐ, ์ด๋กœ ์ธํ•ด OS ์˜ˆ์•ฝ ๋ฉ”๋ชจ๋ฆฌ, JVM ์ž์ฒด ์˜ค๋ฒ„ํ—ค๋“œ, ์Šค๋ ˆ๋“œ ์Šคํƒ ๋“ฑ๊ณผ ํ•ฉ์‚ฐํ•˜๋ฉด ์ œํ•œ์„ ์ดˆ๊ณผํ•œ๋‹ค.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

Dockerfile์˜ CMD์—์„œ JVM ์˜ต์…˜์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•œ๋‹ค.

CMD ["java", "-Xms128m", "-Xmx256m", "-jar", "/app/app.jar"]
์˜ต์…˜ ์˜๋ฏธ
-Xms128m ์ดˆ๊ธฐ ํž™ ํฌ๊ธฐ 128MB
-Xmx256m ์ตœ๋Œ€ ํž™ ํฌ๊ธฐ 256MB
-XX:+UseContainerSupport ์ปจํ…Œ์ด๋„ˆ ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ ์ž๋™ ์ธ์‹ (Java 11+)
-XX:MaxRAMPercentage=50.0 ์ปจํ…Œ์ด๋„ˆ ๋ฉ”๋ชจ๋ฆฌ์˜ 50%๋ฅผ ์ตœ๋Œ€ ํž™์œผ๋กœ ์‚ฌ์šฉ

์ปจํ…Œ์ด๋„ˆ ๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜ ์ž๋™ ์„ค์ • ๋ฐฉ์‹(๋” ๊ถŒ์žฅ):

CMD ["java", \
  "-XX:+UseContainerSupport", \
  "-XX:MaxRAMPercentage=50.0", \
  "-jar", "/app/app.jar"]

์ด ๋ฐฉ์‹์„ ์“ฐ๋ฉด ๋‚˜์ค‘์— ํ”Œ๋žœ์„ ์˜ฌ๋ ค ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋Š˜์–ด๋‚ฌ์„ ๋•Œ Dockerfile์„ ์ˆ˜์ •ํ•˜์ง€ ์•Š์•„๋„ ์ž๋™์œผ๋กœ ์ตœ๋Œ€ ํž™์ด ์กฐ์ •๋œ๋‹ค.


๋นŒ๋“œ ์‹œ๊ฐ„ ์ดˆ๊ณผ

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

Build timed out after 30 minutes

๐Ÿง ์›์ธ ๋ถ„์„

Render ๋ฌด๋ฃŒ ํ”Œ๋žœ์€ ๋นŒ๋“œ ์‹œ๊ฐ„์ด ์›” 400๋ถ„์œผ๋กœ ์ œํ•œ๋˜๋ฉฐ, ๋‹จ์ผ ๋นŒ๋“œ๋„ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด ๊ฐ•์ œ ์ข…๋ฃŒ๋œ๋‹ค. Spring Boot ํ”„๋กœ์ ํŠธ๋Š” Gradle ์˜์กด์„ฑ ๋‹ค์šด๋กœ๋“œ + ๋นŒ๋“œ + Docker ์ด๋ฏธ์ง€ ์ƒ์„ฑ ๊ณผ์ •์„ ํฌํ•จํ•˜๋ฉด ์ดˆ๊ธฐ ๋นŒ๋“œ์— 10~20๋ถ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ๋‹ค.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

  1. Gradle ๋ฐ๋ชฌ ๋น„ํ™œ์„ฑํ™”: Docker ๋นŒ๋“œ์—์„œ๋Š” ๋ฐ๋ชฌ์ด ์˜คํžˆ๋ ค ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋œ๋‹ค.

    RUN ./gradlew build --no-daemon
  2. ์บ์‹œ ํ™œ์šฉ: Docker ๋ ˆ์ด์–ด ์บ์‹ฑ์„ ํ™œ์šฉํ•ด ์˜์กด์„ฑ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์žฌ์‚ฌ์šฉํ•œ๋‹ค.

    # ์˜์กด์„ฑ ํŒŒ์ผ๋งŒ ๋จผ์ € ๋ณต์‚ฌํ•ด์„œ ๋ ˆ์ด์–ด ์บ์‹œ ํ™œ์šฉ
    COPY build.gradle settings.gradle ./
    COPY gradle/ gradle/
    RUN ./gradlew dependencies --no-daemon
    
    # ์†Œ์Šค์ฝ”๋“œ ๋ณ€๊ฒฝ ์‹œ ์œ„ ๋ ˆ์ด์–ด๋Š” ์บ์‹œ ์žฌ์‚ฌ์šฉ
    COPY src/ src/
    RUN ./gradlew build --no-daemon -x test
  3. ํ…Œ์ŠคํŠธ ์Šคํ‚ต: CI ํ™˜๊ฒฝ์—์„œ ๋ฐฐํฌ์šฉ ๋นŒ๋“œ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์Šคํ‚ตํ•˜๊ฑฐ๋‚˜ ๋ณ„๋„ ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ์‹คํ–‰ํ•œ๋‹ค.

    RUN ./gradlew build --no-daemon -x test

ํฌํŠธ ์„ค์ • ์˜ค๋ฅ˜

โŒ ๋ฌธ์ œ ์ƒํ™ฉ

๋ฐฐํฌ๋Š” ์„ฑ๊ณตํ–ˆ์ง€๋งŒ ์ ‘์†์ด ์•ˆ ๋˜๊ฑฐ๋‚˜, Health Check๊ฐ€ ๊ณ„์† ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ.

๐Ÿง ์›์ธ ๋ถ„์„

Render๋Š” ์ปจํ…Œ์ด๋„ˆ๊ฐ€ $PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ์ง€์ •๋œ ํฌํŠธ์—์„œ ์ˆ˜์‹  ๋Œ€๊ธฐํ•  ๊ฒƒ์„ ์š”๊ตฌํ•œ๋‹ค. Spring Boot๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 8080 ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, Render๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ํ• ๋‹นํ•˜๋Š” ํฌํŠธ ๋ฒˆํ˜ธ์™€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ๋‹ค.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

application.properties์—์„œ $PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ฝ๋„๋ก ์„ค์ •ํ•œ๋‹ค.

# application.properties
server.port=${PORT:8080}

${PORT:8080}์€ PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ ๊ฐ’์„, ์—†์œผ๋ฉด(๋กœ์ปฌ ์‹คํ–‰ ์‹œ) 8080์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์˜๋ฏธ๋‹ค.


application.properties vs application.yml ํ™˜๊ฒฝ๋ณ„ ๋ถ„๋ฆฌ

๋กœ์ปฌ๊ณผ ์šด์˜ ํ™˜๊ฒฝ์˜ ์„ค์ •์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ์ข‹์€ ๊ฐœ๋ฐœ ์Šต๊ด€์ด๋‹ค. Spring Boot๋Š” spring.profiles.active ๊ฐ’์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์„ค์ • ํŒŒ์ผ์„ ๋กœ๋“œํ•œ๋‹ค.

ํ”„๋กœํŒŒ์ผ๋ณ„ ํŒŒ์ผ ๋ถ„๋ฆฌ ๊ตฌ์กฐ

src/main/resources/
โ”œโ”€โ”€ application.yml          โ† ๊ณตํ†ต ์„ค์ •
โ”œโ”€โ”€ application-local.yml    โ† ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
โ””โ”€โ”€ application-prod.yml     โ† ์šด์˜ ํ™˜๊ฒฝ (Render)
# application.yml (๊ณตํ†ต)
spring:
  application:
    name: my-spring-app
  jpa:
    open-in-view: false

server:
  port: ${PORT:8080}

management:
  endpoints:
    web:
      exposure:
        include: health,info
# application-local.yml (๋กœ์ปฌ ๊ฐœ๋ฐœ)
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true
# application-prod.yml (์šด์˜ - Render)
spring:
  datasource:
    url: ${DATABASE_URL}
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: validate

logging:
  level:
    root: WARN
    com.myapp: INFO

๋กœ์ปฌ ์‹คํ–‰ ์‹œ:

./gradlew bootRun --args='--spring.profiles.active=local'

Render์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ • (render.yaml ๋˜๋Š” ๋Œ€์‹œ๋ณด๋“œ):

envVars:
  - key: SPRING_PROFILES_ACTIVE
    value: prod

๐Ÿณ ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ Dockerfile ์ตœ์ ํ™”

์•ž์„œ ์†Œ๊ฐœํ•œ ๋‹จ์ผ ์Šคํ…Œ์ด์ง€ Dockerfile์€ ๋™์ž‘ํ•˜์ง€๋งŒ, JDK ์ด๋ฏธ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐฐํฌ ์ด๋ฏธ์ง€๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋ถˆํ•„์š”ํ•˜๊ฒŒ ํฌ๋‹ค. ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋นŒ๋“œ ๋„๊ตฌ(JDK, Gradle)๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์€ ๊ฐ€๋ฒผ์šด ์ด๋ฏธ์ง€๋กœ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋‹ค.

๋นŒ๋“œ ์Šคํ…Œ์ด์ง€์™€ ๋Ÿฐํƒ€์ž„ ์Šคํ…Œ์ด์ง€ ๋ถ„๋ฆฌ

๋‹จ์ผ ์Šคํ…Œ์ด์ง€: eclipse-temurin:21-jdk (~600MB) โ†’ ๋ฐฐํฌ ์ด๋ฏธ์ง€
๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€: eclipse-temurin:21-jdk (๋นŒ๋“œ) โ†’ eclipse-temurin:21-jre (~200MB) โ†’ ๋ฐฐํฌ ์ด๋ฏธ์ง€

JDK์—๋Š” ์ปดํŒŒ์ผ๋Ÿฌ(javac), ๋””๋ฒ„๊ฑฐ ๋“ฑ ๊ฐœ๋ฐœ ๋„๊ตฌ๊ฐ€ ํฌํ•จ๋œ๋‹ค. ๋Ÿฐํƒ€์ž„์—์„œ๋Š” JRE(Java Runtime Environment)๋งŒ ์žˆ์œผ๋ฉด ์ถฉ๋ถ„ํ•˜๋‹ค.

ํ”„๋กœ๋•์…˜์šฉ Dockerfile ์ „์ฒด ์˜ˆ์‹œ

# =====================
# 1๋‹จ๊ณ„: ๋นŒ๋“œ ์Šคํ…Œ์ด์ง€
# =====================
FROM eclipse-temurin:21-jdk AS builder

WORKDIR /app

# ์˜์กด์„ฑ ์บ์‹œ ๋ ˆ์ด์–ด ๋ถ„๋ฆฌ (์†Œ์Šค ๋ณ€๊ฒฝ ์‹œ ์ด ๋ ˆ์ด์–ด ์žฌ์‚ฌ์šฉ)
COPY gradlew .
COPY gradle/ gradle/
COPY build.gradle .
COPY settings.gradle .

RUN chmod +x ./gradlew
# ์˜์กด์„ฑ๋งŒ ๋จผ์ € ๋‹ค์šด๋กœ๋“œ
RUN ./gradlew dependencies --no-daemon --quiet

# ์†Œ์Šค ์ฝ”๋“œ ๋ณต์‚ฌ ๋ฐ ๋นŒ๋“œ
COPY src/ src/
RUN ./gradlew build --no-daemon -x test

# ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ fat jar ์œ„์น˜ ํ™•์ธ ๋ฐ ์ด๋ฆ„ ์ •๊ทœํ™”
RUN find ./build/libs -name "*.jar" ! -name "*plain*" -exec cp {} app.jar \;

# =====================
# 2๋‹จ๊ณ„: ๋Ÿฐํƒ€์ž„ ์Šคํ…Œ์ด์ง€
# =====================
FROM eclipse-temurin:21-jre AS runtime

# ๋ณด์•ˆ: ๋ฃจํŠธ๊ฐ€ ์•„๋‹Œ ์ „์šฉ ์‚ฌ์šฉ์ž๋กœ ์‹คํ–‰
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app

# ๋นŒ๋“œ ์Šคํ…Œ์ด์ง€์—์„œ jar๋งŒ ๋ณต์‚ฌ (๋นŒ๋“œ ๋„๊ตฌ ์ œ์™ธ)
COPY --from=builder /app/app.jar app.jar

# ํŒŒ์ผ ์†Œ์œ ๊ถŒ ์„ค์ •
RUN chown appuser:appgroup app.jar

USER appuser

# ํฌํŠธ ๋ฌธ์„œํ™” (์‹ค์ œ ๋ฐ”์ธ๋”ฉ์€ server.port์—์„œ ๊ฒฐ์ •)
EXPOSE 8080

# ๋ฉ”๋ชจ๋ฆฌ ์„ค์ • ํฌํ•จํ•œ ์‹คํ–‰ ๋ช…๋ น
ENTRYPOINT ["java", \
  "-XX:+UseContainerSupport", \
  "-XX:MaxRAMPercentage=50.0", \
  "-Djava.security.egd=file:/dev/./urandom", \
  "-jar", "app.jar"]

์ด๋ฏธ์ง€ ํฌ๊ธฐ ๋น„๊ต

๋ฐฉ์‹ ๋ฒ ์ด์Šค ์ด๋ฏธ์ง€ ์˜ˆ์ƒ ์ตœ์ข… ์ด๋ฏธ์ง€ ํฌ๊ธฐ
๋‹จ์ผ ์Šคํ…Œ์ด์ง€ (JDK) eclipse-temurin:21-jdk ~600MB
๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ (JRE) eclipse-temurin:21-jre ~250MB
๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ (Alpine JRE) eclipse-temurin:21-jre-alpine ~150MB

Alpine ๊ธฐ๋ฐ˜ ์ด๋ฏธ์ง€๋Š” ๋” ์ž‘์ง€๋งŒ, glibc ๋Œ€์‹  musl libc๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ผ๋ถ€ Java ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ํ˜ธํ™˜์„ฑ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌธ์ œ๊ฐ€ ์—†๋‹ค๋ฉด ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•œ๋‹ค.

FROM eclipse-temurin:21-jre-alpine AS runtime

๐Ÿ“ .dockerignore ์„ค์ •

.gitignore์ฒ˜๋Ÿผ, .dockerignore๋Š” Docker ๋นŒ๋“œ ์ปจํ…์ŠคํŠธ์—์„œ ์ œ์™ธํ•  ํŒŒ์ผ์„ ์ง€์ •ํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋นŒ๋“œ ์†๋„๋ฅผ ๋†’์ด๊ณ  ์ด๋ฏธ์ง€์— ๋ถˆํ•„์š”ํ•œ ํŒŒ์ผ์ด ํฌํ•จ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

.dockerignore๊ฐ€ ์—†์œผ๋ฉด docker build ๋ช…๋ น ์‹œ ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ์˜ ๋ชจ๋“  ํŒŒ์ผ์ด Docker ๋ฐ๋ชฌ์œผ๋กœ ์ „์†ก๋œ๋‹ค. node_modules, build/, .git/ ๋“ฑ ๋Œ€์šฉ๋Ÿ‰ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ํฌํ•จ๋˜๋ฉด ๋นŒ๋“œ ์‹œ์ž‘ ์ž์ฒด๊ฐ€ ๋А๋ ค์ง„๋‹ค.

.dockerignore ์˜ˆ์‹œ

# Git ๊ด€๋ จ
.git/
.gitignore
.gitattributes

# Gradle ๋นŒ๋“œ ์‚ฐ์ถœ๋ฌผ (Docker ๋นŒ๋“œ ๋‚ด์—์„œ ์ƒˆ๋กœ ์ƒ์„ฑ)
.gradle/
build/

# IDE ์„ค์ •
.idea/
*.iml
.vscode/
*.code-workspace

# OS ๊ด€๋ จ
.DS_Store
Thumbs.db

# ํ…Œ์ŠคํŠธ ๊ด€๋ จ (๋นŒ๋“œ ์ด๋ฏธ์ง€์— ํฌํ•จ ๋ถˆํ•„์š”)
src/test/

# ๋ฌธ์„œ
*.md
docs/

# Docker ํŒŒ์ผ ์ž์ฒด (๋ถˆํ•„์š”)
Dockerfile*
docker-compose*.yml
.dockerignore

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํŒŒ์ผ (์ ˆ๋Œ€ ํฌํ•จํ•˜๋ฉด ์•ˆ ๋จ)
.env
.env.*
*.env

# ๋กœ๊ทธ ํŒŒ์ผ
*.log
logs/

ํŠนํžˆ .env ํŒŒ์ผ์„ .dockerignore์— ๋ฐ˜๋“œ์‹œ ์ถ”๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค. ์‹ค์ˆ˜๋กœ ์ด๋ฏธ์ง€์— ํฌํ•จ๋˜๋ฉด Docker Hub ๋“ฑ ํผ๋ธ”๋ฆญ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์˜ฌ๋ฆด ๋•Œ ์‹œํฌ๋ฆฟ์ด ๋…ธ์ถœ๋œ๋‹ค.


๐Ÿ”‘ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ด€๋ฆฌ

Render ๋Œ€์‹œ๋ณด๋“œ์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ค์ •

  1. Render ๋Œ€์‹œ๋ณด๋“œ ์ ‘์† โ†’ ์„œ๋น„์Šค ์„ ํƒ
  2. ์ขŒ์ธก ๋ฉ”๋‰ด Environment ํด๋ฆญ
  3. Add Environment Variable ๋ฒ„ํŠผ์œผ๋กœ ํ‚ค-๊ฐ’ ์ถ”๊ฐ€
  4. Save Changes ํ›„ ์ž๋™ ์žฌ๋ฐฐํฌ ํŠธ๋ฆฌ๊ฑฐ

๋ฏผ๊ฐํ•œ ์ •๋ณด(JWT_SECRET, API_KEY ๋“ฑ)๋Š” render.yaml์— ๊ฐ’์„ ์ง์ ‘ ์“ฐ์ง€ ์•Š๊ณ , ๋Œ€์‹œ๋ณด๋“œ์—์„œ๋งŒ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

render.yaml์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์„ ์–ธ

services:
  - type: web
    name: my-spring-app
    env: docker
    envVars:
      # ์ผ๋ฐ˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜ (๊ฐ’ ์ง์ ‘ ์ง€์ •)
      - key: SPRING_PROFILES_ACTIVE
        value: prod

      # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฌธ์ž์—ด ์ž๋™ ์ฃผ์ž…
      - key: DATABASE_URL
        fromDatabase:
          name: my-postgres-db
          property: connectionString

      # ์‹œํฌ๋ฆฟ (๋Œ€์‹œ๋ณด๋“œ์—์„œ ๋ณ„๋„ ์ž…๋ ฅ ํ•„์š”)
      - key: JWT_SECRET
        sync: false

      - key: SMTP_PASSWORD
        sync: false

Spring Boot์—์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฝ๊ธฐ

@Value ์–ด๋…ธํ…Œ์ด์…˜ ๋ฐฉ์‹ (๋‹จ์ˆœํ•œ ๊ฒฝ์šฐ):

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class AppConfig {

    @Value("${jwt.secret}")
    private String jwtSecret;

    @Value("${server.port:8080}")
    private int serverPort;
}

@ConfigurationProperties ๋ฐฉ์‹ (๋ณต์žกํ•œ ์„ค์ • ๊ทธ๋ฃน์˜ ๊ฒฝ์šฐ, ๋” ๊ถŒ์žฅ):

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "app")
public class AppProperties {

    private String jwtSecret;
    private long jwtExpirationMs;
    private Database database = new Database();

    // getter, setter ์ƒ๋žต...

    public static class Database {
        private int maxPoolSize = 3;
        // getter, setter ์ƒ๋žต...
    }
}
# application-prod.yml
app:
  jwt-secret: ${JWT_SECRET}
  jwt-expiration-ms: 86400000
  database:
    max-pool-size: 3

ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ด๋ฆ„ ๊ทœ์น™: Spring Boot๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜์˜ ์–ธ๋”์Šค์ฝ”์–ด(_)๋ฅผ ์ (.)๊ณผ ํ•˜์ดํ”ˆ(-)์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ํ™˜๊ฒฝ๋ณ€์ˆ˜ SPRING_DATASOURCE_URL์€ spring.datasource.url๋กœ ๋งคํ•‘๋œ๋‹ค.

SPRING_PROFILES_ACTIVE=prod
โ†’ spring.profiles.active=prod

JWT_SECRET=mySecret
โ†’ jwt.secret=mySecret (application.yml์—์„œ ${jwt.secret}์œผ๋กœ ์ฝ๊ธฐ)

๐Ÿ’“ Health Check ์„ค์ •

๋ฐฐํฌ ํ›„ ์„œ๋น„์Šค๊ฐ€ ์ •์ƒ์ธ์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด Health Check๋ฅผ ์„ค์ •ํ•˜๋ฉด, Render๊ฐ€ ์ž๋™์œผ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ๋ฅผ ๊ฐ์‹œํ•˜๊ณ  ๋น„์ •์ƒ์ผ ๋•Œ ์ด์ „ ๋ฒ„์ „์œผ๋กœ ๋กค๋ฐฑํ•œ๋‹ค.

Spring Boot Actuator ์ถ”๊ฐ€

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,info
      base-path: /actuator
  endpoint:
    health:
      show-details: when-authorized   # ์šด์˜: ์ธ์ฆ๋œ ์š”์ฒญ์—๋งŒ ์ƒ์„ธ ์ •๋ณด ๋…ธ์ถœ

๊ธฐ๋ณธ /actuator/health ์—”๋“œํฌ์ธํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

{
  "status": "UP"
}

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์ƒํƒœ๊นŒ์ง€ ํฌํ•จํ•˜๋ ค๋ฉด:

management:
  endpoint:
    health:
      show-details: always   # ๋˜๋Š” when-authorized
{
  "status": "UP",
  "components": {
    "db": {
      "status": "UP",
      "details": {
        "database": "PostgreSQL",
        "validationQuery": "isValid()"
      }
    },
    "diskSpace": {
      "status": "UP"
    }
  }
}

render.yaml์— healthCheckPath ์„ค์ •

services:
  - type: web
    name: my-spring-app
    env: docker
    plan: free
    healthCheckPath: /actuator/health

Render๋Š” ๋ฐฐํฌ ํ›„ ์ด ๊ฒฝ๋กœ์— HTTP GET ์š”์ฒญ์„ ๋ณด๋‚ด HTTP 200 ์‘๋‹ต์ด ์˜ค๋ฉด ๋ฐฐํฌ ์„ฑ๊ณต์œผ๋กœ ํŒ๋‹จํ•œ๋‹ค. Health Check๊ฐ€ ์‹คํŒจํ•˜๋ฉด ๋ฐฐํฌ๋ฅผ ์ทจ์†Œํ•˜๊ณ  ์ด์ „ ์ •์ƒ ์ƒํƒœ๋กœ ๋กค๋ฐฑํ•œ๋‹ค.

์ปค์Šคํ…€ Health Indicator ์ž‘์„ฑ

์™ธ๋ถ€ API ์—ฐ๊ฒฐ ์—ฌ๋ถ€, ํ•„์ˆ˜ ํŒŒ์ผ ์กด์žฌ ์—ฌ๋ถ€ ๋“ฑ ์ปค์Šคํ…€ ์ƒํƒœ๋ฅผ Health Check์— ํฌํ•จํ•  ์ˆ˜ ์žˆ๋‹ค.

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final ExternalApiClient apiClient;

    public ExternalApiHealthIndicator(ExternalApiClient apiClient) {
        this.apiClient = apiClient;
    }

    @Override
    public Health health() {
        try {
            boolean isAvailable = apiClient.ping();
            if (isAvailable) {
                return Health.up()
                    .withDetail("externalApi", "Available")
                    .build();
            }
            return Health.down()
                .withDetail("externalApi", "Unreachable")
                .build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}

๐Ÿงช ๋กœ์ปฌ Docker ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

Render์— ๋ฐฐํฌํ•˜๊ธฐ ์ „์— ๋กœ์ปฌ์—์„œ Docker ๋นŒ๋“œ์™€ ์‹คํ–‰์„ ๊ฒ€์ฆํ•˜๋ฉด ๋นŒ๋“œ ์˜ค๋ฅ˜๋ฅผ ๋ฏธ๋ฆฌ ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ๋‹ค. Render์˜ ๋นŒ๋“œ ์‹œ๊ฐ„(์›” 400๋ถ„)์„ ์•„๋‚„ ์ˆ˜๋„ ์žˆ๋‹ค.

Docker ๋นŒ๋“œ

# ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ์—์„œ ์‹คํ–‰
docker build -t my-spring-app:local .

# ๋นŒ๋“œ ์บ์‹œ ์—†์ด ์ฒ˜์Œ๋ถ€ํ„ฐ ๋นŒ๋“œ (ํด๋ฆฐ ๋นŒ๋“œ ๊ฒ€์ฆ)
docker build --no-cache -t my-spring-app:local .

ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๋ฉฐ ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰

# ๊ธฐ๋ณธ ์‹คํ–‰
docker run -p 8080:8080 my-spring-app:local

# ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ง์ ‘ ์ „๋‹ฌ
docker run -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=local \
  -e DATABASE_URL=jdbc:h2:mem:testdb \
  -e PORT=8080 \
  my-spring-app:local

.env ํŒŒ์ผ๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ด€๋ฆฌ

๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋ฅผ .env ํŒŒ์ผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉด ํŽธ๋ฆฌํ•˜๋‹ค.

# .env (์ ˆ๋Œ€ Git์— ์ปค๋ฐ‹ํ•˜์ง€ ๋ง ๊ฒƒ)
SPRING_PROFILES_ACTIVE=local
PORT=8080
DATABASE_URL=jdbc:postgresql://localhost:5432/mydb
JWT_SECRET=local-dev-secret-not-for-production
# --env-file๋กœ ์ „๋‹ฌ
docker run -p 8080:8080 --env-file .env my-spring-app:local

๋กœ์ปฌ์—์„œ PostgreSQL๊ณผ ํ•จ๊ป˜ ํ…Œ์ŠคํŠธ (docker-compose)

Render ์šด์˜ ํ™˜๊ฒฝ๊ณผ ๋™์ผํ•˜๊ฒŒ PostgreSQL์„ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ปฌ ํ™˜๊ฒฝ์„ ๋งŒ๋“ค๋ ค๋ฉด docker-compose๋ฅผ ํ™œ์šฉํ•œ๋‹ค.

# docker-compose.yml (๋กœ์ปฌ ๊ฐœ๋ฐœ์šฉ, Git์— ์ปค๋ฐ‹ํ•ด๋„ ๋จ)
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - SPRING_PROFILES_ACTIVE=prod
      - PORT=8080
      - DATABASE_URL=jdbc:postgresql://db:5432/mydb
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: myuser
      POSTGRES_PASSWORD: mypassword
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
      interval: 5s
      timeout: 5s
      retries: 5
# ์ „์ฒด ์Šคํƒ ์‹คํ–‰
docker-compose up --build

# ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์‹คํ–‰
docker-compose up -d --build

# ๋กœ๊ทธ ํ™•์ธ
docker-compose logs -f app

# ์ข…๋ฃŒ
docker-compose down

๋ฐฐํฌ ์ „ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

# 1. ๋กœ์ปฌ Docker ๋นŒ๋“œ ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
docker build -t my-spring-app:local .

# 2. ์ปจํ…Œ์ด๋„ˆ ์ •์ƒ ๊ธฐ๋™ ํ™•์ธ
docker run -d -p 8080:8080 -e PORT=8080 my-spring-app:local
docker ps   # ์ปจํ…Œ์ด๋„ˆ ์ƒํƒœ ํ™•์ธ

# 3. Health Check ์—”๋“œํฌ์ธํŠธ ์‘๋‹ต ํ™•์ธ
curl http://localhost:8080/actuator/health

# 4. ์ด๋ฏธ์ง€ ํฌ๊ธฐ ํ™•์ธ
docker images my-spring-app:local

# 5. ์ปจํ…Œ์ด๋„ˆ ์ •๋ฆฌ
docker stop $(docker ps -q --filter ancestor=my-spring-app:local)
docker rmi my-spring-app:local

๊ฒฐ๋ก 

Spring Boot ํ”„๋กœ์ ํŠธ๋ฅผ Render์— Docker๋กœ ๋ฐฐํฌํ•  ๋•Œ ๋งˆ์ฃผ์น˜๋Š” ์˜ค๋ฅ˜๋“ค์€ ๋Œ€๋ถ€๋ถ„ ์„ธ ๊ฐ€์ง€ ๋ฒ”์ฃผ๋กœ ๋‚˜๋‰œ๋‹ค.

ํ™˜๊ฒฝ ๋ถˆ์ผ์น˜: ./gradlew ๊ถŒํ•œ, Gradle Wrapper ํŒŒ์ผ ๋ˆ„๋ฝ, Java ๋ฒ„์ „ ๋ถˆ์ผ์น˜. Dockerfile์— ๋ช…์‹œ์ ์œผ๋กœ ํ™˜๊ฒฝ์„ ์„ ์–ธํ•˜๋Š” ๊ฒƒ์œผ๋กœ ํ•ด๊ฒฐ๋œ๋‹ค.

ํ”Œ๋žซํผ ํŠน์„ฑ ๋ฏธ์ˆ™์ง€: Render ๋ฌด๋ฃŒ ํ”Œ๋žœ์˜ ์Šฌ๋ฆฝ ๋ชจ๋“œ, 512MB ๋ฉ”๋ชจ๋ฆฌ ์ œํ•œ, $PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์š”๊ตฌ. ์ด ํŠน์„ฑ์„ ์•Œ๊ณ  ์ฒ˜์Œ๋ถ€ํ„ฐ ์„ค๊ณ„์— ๋ฐ˜์˜ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค.

์šด์˜ ํ™˜๊ฒฝ ์„ค์ • ๋ถ€์žฌ: ํ”„๋กœํŒŒ์ผ ๋ถ„๋ฆฌ, ์‹œํฌ๋ฆฟ ๊ด€๋ฆฌ, Health Check. ๋กœ์ปฌ์—์„œ ์ž˜ ๋˜๋˜ ๊ฒƒ์ด ์šด์˜์—์„œ ์•ˆ ๋˜๋Š” ์›์ธ์˜ ๋Œ€๋ถ€๋ถ„์ด๋‹ค.

์•„๋ž˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

  1. ./gradlew์— ์‹คํ–‰ ๊ถŒํ•œ ๋ถ€์—ฌ (chmod +x ./gradlew) ํ›„ ์ปค๋ฐ‹

  2. gradlew, gradle-wrapper.jar, .properties ๋“ฑ Wrapper ๊ด€๋ จ ํŒŒ์ผ์„ Git์— ๋ฐ˜๋“œ์‹œ ์ปค๋ฐ‹

  3. Dockerfile์—์„œ eclipse-temurin ์ด๋ฏธ์ง€๋กœ Java ํ™˜๊ฒฝ ๋ช…์‹œ, ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋กœ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ตœ์†Œํ™”

  4. Gradle toolchain์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ Docker JDK ๋ฒ„์ „๊ณผ ์ผ์น˜์‹œํ‚ฌ ๊ฒƒ

  5. Render ๋ฃจํŠธ์— render.yaml ์กด์žฌ ํ™•์ธ, healthCheckPath ์„ค์ •

  6. server.port=${PORT:8080}์œผ๋กœ Render์˜ PORT ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๋Œ€์‘

  7. JVM ๋ฉ”๋ชจ๋ฆฌ ์˜ต์…˜ -XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0 ์„ค์ •

  8. PostgreSQL ์‚ฌ์šฉ ์‹œ DATABASE_URL๊ณผ JDBC ์„ค์ • ์ถ”๊ฐ€

  9. ๋กœ์ปฌ H2, ์šด์˜ PostgreSQL ๋ถ„๋ฆฌ ์‹œ spring.profiles.active๋กœ ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ํŒŒ์ผ ๋ถ„๋ฆฌ

  10. Spring Boot Actuator๋กœ Health Check ์—”๋“œํฌ์ธํŠธ ๊ตฌ์„ฑ

  11. .dockerignore ์ž‘์„ฑ์œผ๋กœ ๋นŒ๋“œ ์ปจํ…์ŠคํŠธ ์ตœ์ ํ™”

  12. Render ๋ฐฐํฌ ์ „ ๋กœ์ปฌ์—์„œ docker build + docker run์œผ๋กœ ๊ฒ€์ฆ

Render๋Š” ํŽธ๋ฆฌํ•˜๊ณ  ๋ฌด๋ฃŒ๋กœ ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ์ด์ง€๋งŒ Java ํ”„๋กœ์ ํŠธ๋ฅผ ์œ„ํ•œ ๋ณ„๋„ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค๋Š” ์ ์„ ๊ผญ ๊ธฐ์–ตํ•ด์•ผ๊ฒ ๋‹ค!

@leekh8
๋ณด์•ˆ, ์›น ๊ฐœ๋ฐœ, Python์„ ๋‹ค๋ฃจ๋Š” ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ