Code N Solve ๐: Spring Boot ํ๋ก์ ํธ์ Render ๋ฐฐํฌ ์คํจ ์ฌ๋ก ์ ๋ฆฌ
์ง๋๋ฒ Vite ๋ฐฐํฌ ์ค๋ฅ ๊ธ๊ณผ๋ ๋ฌ๋ฆฌ, Java + Spring Boot + Gradle1 ํ๊ฒฝ์ ๋ฐฑ์๋ ํ๋ก์ ํธ๋ฅผ Render2์ Docker๋ก ๋ฐฐํฌํ ๋ ๊ฒช์ ๋น๋ ์คํจ ์ฌ๋ก์ ํด๊ฒฐ ๊ณผ์ ์ ์ ๋ฆฌํ์๋ค.
๊ฐ๊ฐ์ ์ค๋ฅ๋ฅผ ๋ถ์ํ๊ณ ํด๊ฒฐํ ๊ณผ์ ์ ๋ํด ์์๋ณด์.
Vite๋ Node.js ํ๊ฒฝ์ด ์๋, Java ๊ธฐ๋ฐ ์๋ฒ๋ฅผ Docker ํ๊ฒฝ์์ Render๋ก ๋ฐฐํฌํ๋ ค๊ณ ํ๋ ๋ค๋ฅธ ์ฌ๋์๊ฒ ๋์์ด ๋๋ฉด ์ข๊ฒ ๋ค.
๐ ๋ชฉ์ฐจ
- Render ๋ฌด๋ฃ ํ๋ ํน์ฑ ์ดํด
- Render ๋ฐฐํฌ ์ ์ฒด ํ๋ฆ
- ์ค๋ฅ 1 -
./gradlewPermission denied - ์ค๋ฅ 2 - Gradle Wrapper ์์
- ์ค๋ฅ 3 - JAVA_HOME ์ค์ ์ค๋ฅ
- ์ค๋ฅ 4 - Gradle Toolchain ์๋ฌ
- ์ค๋ฅ 5 - render.yaml ์ค์
- ์ค๋ฅ 6 - DATABASE_URL ์๋ ์ ๊ณต vs H2
- ์ถ๊ฐ ์ค๋ฅ ์ผ์ด์ค
- ๋ฉํฐ ์คํ ์ด์ง Dockerfile ์ต์ ํ
- .dockerignore ์ค์
- ํ๊ฒฝ ๋ณ์ ๊ด๋ฆฌ
- Health Check ์ค์
- ๋ก์ปฌ Docker ํ ์คํธ ๋ฐฉ๋ฒ
- ๊ฒฐ๋ก
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/healthhealthCheckPath๋ฅผ ์ค์ ํด๋๋ฉด 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๋ฅผ ์ธ ๋ ์ฃผ์ํ ์
-
JVM ํ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ๋ฐ๋์ ์ ํํด์ผ ํ๋ค. ์ค์ ํ์ง ์์ผ๋ฉด JVM์ด ์ปจํ ์ด๋ ๋ฉ๋ชจ๋ฆฌ์ 25%๋ฅผ ์๋์ผ๋ก ์ต๋ ํ์ผ๋ก ์ค์ ํ์ง๋ง, ์ปจํ ์ด๋ ํ๊ฒฝ์์ ์ด ๊ฐ์ง๊ฐ ๋ถ์ ํํ ์ ์๋ค.
-
๋ถํ์ํ Spring Boot ์คํํฐ๋ฅผ ์ ๊ฑฐํด ๊ธฐ๋ ์๊ฐ๊ณผ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ค์ธ๋ค.
-
Spring Native(GraalVM) ๋๋ Spring AOT๋ฅผ ๊ณ ๋ คํ๋ฉด ๊ธฐ๋ ์๊ฐ์ ์ ์ด ์ดํ๋ก ์ค์ผ ์ ์๋ค. ๋จ, ๋น๋ ๋ณต์ก๋๊ฐ ๋์์ง๋ค.
-
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ ํ ํฌ๊ธฐ๋ฅผ ์ค์ธ๋ค. HikariCP ๊ธฐ๋ณธ ํ ํฌ๊ธฐ(10)๋ฅผ 2~3์ผ๋ก ์ค์ด๋ฉด ๋ฉ๋ชจ๋ฆฌ ์ ์ฝ์ ๋์์ด ๋๋ค.
# application.properties
spring.datasource.hikari.maximum-pool-size=3
spring.datasource.hikari.minimum-idle=1Render ๋ฐฐํฌ ์ ์ฒด ํ๋ฆ
์ค์ ๋ฐฐํฌ๊ฐ ์ด๋ค ๊ณผ์ ์ ๊ฑฐ์น๋์ง ๋จผ์ ์ ์ฒด ๊ทธ๋ฆผ์ ํ์ ํด๋๋ฉด ๊ฐ ๋จ๊ณ์์ ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ดํดํ๊ธฐ ํจ์ฌ ์ฝ๋ค.
์ด ํ๋ฆ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์๋ ์ง์ ์ ํฌ๊ฒ ์ธ ๊ตฐ๋ฐ๋ค:
- ํด๋ก ์งํ:
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 pushGit์ ์คํ ๋นํธ(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์ด ์ค์น๋์ง ์์ ํ๊ฒฝ์์๋ ๋น๋๋ฅผ ์ํํ ์ ์๊ฒ ๋์์ค.
gradlewgradlew.batgradle/wrapper/gradle-wrapper.jargradle/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.jar3. 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-jdkDockerfile์ 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: free2. 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_userfromDatabase๋ฅผ ์ฌ์ฉํ๋ฉด 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๋ถ์ด ๊ฑธ๋ฆด ์ ์๋ค.
โ ํด๊ฒฐ ๋ฐฉ๋ฒ
-
Gradle ๋ฐ๋ชฌ ๋นํ์ฑํ: Docker ๋น๋์์๋ ๋ฐ๋ชฌ์ด ์คํ๋ ค ์ค๋ฒํค๋๊ฐ ๋๋ค.
RUN ./gradlew build --no-daemon -
์บ์ ํ์ฉ: Docker ๋ ์ด์ด ์บ์ฑ์ ํ์ฉํด ์์กด์ฑ ๋ค์ด๋ก๋๋ฅผ ์ฌ์ฌ์ฉํ๋ค.
# ์์กด์ฑ ํ์ผ๋ง ๋จผ์ ๋ณต์ฌํด์ ๋ ์ด์ด ์บ์ ํ์ฉ COPY build.gradle settings.gradle ./ COPY gradle/ gradle/ RUN ./gradlew dependencies --no-daemon # ์์ค์ฝ๋ ๋ณ๊ฒฝ ์ ์ ๋ ์ด์ด๋ ์บ์ ์ฌ์ฌ์ฉ COPY src/ src/ RUN ./gradlew build --no-daemon -x test -
ํ ์คํธ ์คํต: 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 /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 ๋์๋ณด๋์์ ํ๊ฒฝ๋ณ์ ์ค์
- Render ๋์๋ณด๋ ์ ์ โ ์๋น์ค ์ ํ
- ์ข์ธก ๋ฉ๋ด Environment ํด๋ฆญ
- Add Environment Variable ๋ฒํผ์ผ๋ก ํค-๊ฐ ์ถ๊ฐ
- 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: falseSpring 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/healthRender๋ ๋ฐฐํฌ ํ ์ด ๊ฒฝ๋ก์ 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. ๋ก์ปฌ์์ ์ ๋๋ ๊ฒ์ด ์ด์์์ ์ ๋๋ ์์ธ์ ๋๋ถ๋ถ์ด๋ค.
์๋ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ์ฐธ๊ณ ํ์.
-
./gradlew์ ์คํ ๊ถํ ๋ถ์ฌ (chmod +x ./gradlew) ํ ์ปค๋ฐ -
gradlew,gradle-wrapper.jar,.properties๋ฑ Wrapper ๊ด๋ จ ํ์ผ์ Git์ ๋ฐ๋์ ์ปค๋ฐ -
Dockerfile์์
eclipse-temurin์ด๋ฏธ์ง๋ก Java ํ๊ฒฝ ๋ช ์, ๋ฉํฐ ์คํ ์ด์ง ๋น๋๋ก ์ด๋ฏธ์ง ํฌ๊ธฐ ์ต์ํ -
Gradle
toolchain์ ์ฌ์ฉํ ๊ฒฝ์ฐ Docker JDK ๋ฒ์ ๊ณผ ์ผ์น์ํฌ ๊ฒ -
Render ๋ฃจํธ์
render.yaml์กด์ฌ ํ์ธ,healthCheckPath์ค์ -
server.port=${PORT:8080}์ผ๋ก Render์ PORT ํ๊ฒฝ๋ณ์ ๋์ -
JVM ๋ฉ๋ชจ๋ฆฌ ์ต์
-XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0์ค์ -
PostgreSQL ์ฌ์ฉ ์
DATABASE_URL๊ณผ JDBC ์ค์ ์ถ๊ฐ -
๋ก์ปฌ H2, ์ด์ PostgreSQL ๋ถ๋ฆฌ ์
spring.profiles.active๋ก ํ๊ฒฝ๋ณ ์ค์ ํ์ผ ๋ถ๋ฆฌ -
Spring Boot Actuator๋ก Health Check ์๋ํฌ์ธํธ ๊ตฌ์ฑ
-
.dockerignore์์ฑ์ผ๋ก ๋น๋ ์ปจํ ์คํธ ์ต์ ํ -
Render ๋ฐฐํฌ ์ ๋ก์ปฌ์์
docker build+docker run์ผ๋ก ๊ฒ์ฆ
Render๋ ํธ๋ฆฌํ๊ณ ๋ฌด๋ฃ๋ก ๋ฐฐํฌํ ์ ์๋ ๋๊ตฌ์ด์ง๋ง Java ํ๋ก์ ํธ๋ฅผ ์ํ ๋ณ๋ ์ค์ ์ด ํ์ํ๋ค๋ ์ ์ ๊ผญ ๊ธฐ์ตํด์ผ๊ฒ ๋ค!
