๋ณธ ํ๋ก์ ํธ๋ MSA(Microservice Architecture) ํ๊ฒฝ์์ ๋ณด์๊ณผ ํ์ฅ์ ์ฑ
์์ง๋ JWT ๊ธฐ๋ฐ ํตํฉ ์ธ์ฆ ๋ฐ ํ์ ๊ด๋ฆฌ ์๋ฒ์
๋๋ค.
๊ณ ์ฑ๋ฅ ํ์ ์กฐํ, ์์ ํ ์ธ์ฆ ํ๋ฆ, ๊ทธ๋ฆฌ๊ณ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ํคํ
์ฒ๋ฅผ ์ง์ํ๊ธฐ ์ํ ๊ธฐ์ ์ ์ฅ์น๋ค์ด ํฌํจ๋์ด ์์ต๋๋ค.
src/main/java/org/pgsg/user_service/
โโโ auth/ # ์ธ์ฆ ๋๋ฉ์ธ
โ โโโ application/ # ์๋น์ค(AuthService) ๋ฐ ํ ํฐ ๋ฐํ ๋ก์ง
โ โโโ domain/ # ์ธ์ฆ ์ ์ฑ
์ธํฐํ์ด์ค ๋ฐ ๊ฐ ๊ฐ์ฒด
โ โโโ infrastructure/ # RedisTokenRepository, JwtTokenProvider, Security ํํฐ
โ โโโ presentation/ # AuthController (๋ก๊ทธ์ธ, ํ์๊ฐ์
API)
โ
โโโ user/ # ํ์ ๋๋ฉ์ธ
โ โโโ application/ # Facade(์กฐ์จ) ๋ฐ ๋๋ฉ์ธ ์๋น์ค
โ โโโ domain/ # User ์ํฐํฐ, UserRole, UserRepository ์ธํฐํ์ด์ค
โ โโโ infrastructure/ # JpaUserRepository, UserQueryRepository(QueryDSL)
โ โโโ presentation/ # UserController (ํ์ ์ ๋ณด ๊ด๋ฆฌ API)
โ
โโโ UserServiceApplication.java # Application Bootstrapping
| ๊ตฌ๋ถ | ๊ธฐ์ | ์์ธ |
|---|---|---|
| Framework | Spring Boot 3.5.13 | Java 21 LTS ๊ธฐ๋ฐ |
| Common Lib | org.pgsg:common | ์ ์ฌ ๊ณตํต ์์ธ ์ฒ๋ฆฌ ๋ฐ ์ ํธ๋ฆฌํฐ |
| Database | PostgreSQL 17 | UUID ๋ฐ JSONB ์ง์ |
| Cache | Redis 7.2 | RefreshToken ์ ์ฅ ๋ฐ ํ ํฐ ๋ธ๋๋ฆฌ์คํธ ๊ด๋ฆฌ |
| Persistence | Spring Data JPA / QueryDSL | Type-safeํ ์ฟผ๋ฆฌ ์์ฑ ๋ฐ ์ฑ๋ฅ ์ต์ ํ |
| Security | Spring Security / JWT | Stateless ์ธ์ฆ ๋ฐ ์ธ๊ฐ |
| Observability | Micrometer / Zipkin | ๋ถ์ฐ ํธ๋ ์ด์ฑ ๋ฐ ๋ฉํธ๋ฆญ ์์ง |
| Infrastructure | Spring Cloud | Eureka(Discovery), Config(Centralized Config) |
์ค์ ์ค์ ์๋ฒ(Config Server)๋ฅผ ํตํด ๊ณตํต ์ ์ฑ ์ ๊ด๋ฆฌํ๋, ๋ก์ปฌ ๊ฐ๋ฐ ๋ฐ Docker ํ๊ฒฝ์ ์ ์ฐ์ฑ์ ์ํด ๋ก์ปฌ ํ๊ฒฝ ๋ณ์๊ฐ ์๊ฒฉ ์ค์ ์ ๋ฎ์ด์ฐ๋๋ก(Override) ๊ตฌ์ฑํ์์ต๋๋ค.
๐ application.yml ๋ก์ปฌ ์ค์ ์ ๋ฌธ (ํผ์น๊ธฐ)
server:
port: ${SERVER_PORT:8081}
spring:
application:
name: user-service
profiles:
active: dev, kafka
include: error, user-error
config:
import:
- "optional:file:.env[.properties]"
- "optional:configserver:"
cloud:
config:
allow-override: true
override-none: true
override-system-properties: false
discovery:
service-id: config-server
enabled: true
main:
allow-bean-definition-overriding: true
jwt:
secret: ${JWT_SECRET}
access-token-expiration: ${JWT_ACCESS_EXPIRATION:1800000}
refresh-token-expiration: ${JWT_REFRESH_EXPIRATION:604800000}
eureka:
instance:
prefer-ip-address: true
ip-address: ${HOSTNAME:localhost}
hostname: ${HOSTNAME:localhost}
instance-id: "${HOSTNAME:${spring.application.name}}:${spring.application.name}:${server.port}"
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: ${EUREKA_SERVER_URL:http://localhost:8761/eureka/}
management:
tracing:
sampling:
probability: 0.1
zipkin:
tracing:
endpoint: ${ZIPKIN_ENDPOINT:http://localhost:9411/api/v2/spans}Java ๋ณด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ ์ฝ์ผ๋ก ์ธํด SSL ํธ๋ฌ์คํธ์คํ ์ด(jks)๋ JAR ๋ด๋ถ๊ฐ ์๋ ๋ฌผ๋ฆฌ ํ์ผ ์์คํ
์ ์์นํด์ผ ํฉ๋๋ค.
- ํด๊ฒฐ์ฑ
:
docker-compose.yml๋ณผ๋ฅจ ์ค์ ์ ํตํด ํธ์คํธ์./sslํด๋๋ฅผ ์ปจํ ์ด๋/app/ssl๋ก ๋ง์ดํธํ์ฌ ๊ธฐ๋ ์ ์ฆ์ ๋ก๋ ๊ฐ๋ฅํ๊ฒ ์ฒ๋ฆฌํ์ต๋๋ค.
๋ณธ ํ๋ก์ ํธ๋ ๋ณด์์ ์ํด ๋ฏผ๊ฐ ์ ๋ณด(๋น๋ฐ๋ฒํธ, ํ ํฐ ๋ฑ)๋ฅผ ํ์ผ์ ์ง์ ๊ธฐ๋กํ์ง ์๊ณ , Docker Secrets ๋ฐ ํ๊ฒฝ ๋ณ์ ์์คํ ์ ํ์ฉํฉ๋๋ค.
๐ณ Dockerfile (๋ฉํฐ ์คํ ์ด์ง ๋น๋)
# ๋น๋ ์คํ
์ด์ง: GitHub ํจํค์ง ์ ๊ทผ์ ์ํด ์ํฌ๋ฆฟ ๋ง์ดํธ ์ฌ์ฉ
FROM gradle:8.7-jdk21 AS build
WORKDIR /app
COPY . .
RUN --mount=type=secret,id=GPR_USER \
--mount=type=secret,id=GPR_TOKEN \
export GPR_USER=$(cat /run/secrets/GPR_USER) && \
export GPR_TOKEN=$(cat /run/secrets/GPR_TOKEN) && \
gradle bootJar --no-daemon
# ์คํ ์คํ
์ด์ง: ๊ฒฝ๋ํ๋ JRE ํ๊ฒฝ
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]๐ docker-compose.yml (์๋น์ค ์ค์ผ์คํธ๋ ์ด์ )
services:
user-service:
container_name: user-service
build:
context: .
secrets:
- GPR_USER
- GPR_TOKEN
environment:
- "SERVER_PORT=${SERVER_PORT:-8081}"
ports:
- "${SERVER_PORT:-8081}:${SERVER_PORT:-8081}"
env_file:
- .env
volumes:
- ./src/main/resources/ssl:/app/ssl
depends_on:
db: { condition: service_healthy }
redis: { condition: service_healthy }
networks:
- pgsg-network
db:
image: postgres:17
environment:
POSTGRES_DB: ${DB_NAME:-userdb}
POSTGRES_USER: ${DB_USERNAME:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres}"]
networks:
- pgsg-network
redis:
image: redis:7.2
healthcheck:
test: ["CMD", "redis-cli", "ping"]
networks:
- pgsg-network
networks:
pgsg-network:
name: pgsg-network
external: true
volumes:
postgres-data:
secrets:
GPR_USER: { environment: GPR_USER }
GPR_TOKEN: { environment: GPR_TOKEN }ํ๋ก์ ํธ ๋ฃจํธ์ .env ํ์ผ์ ์์ฑํ๊ณ , ์ต์ ํ๋ .env.example ๋ด์ฉ์ ์ฐธ๊ณ ํ์ฌ ์ค์ ํ๊ฒฝ์ ๋ง๋ ๊ฐ์ ์์ฑํฉ๋๋ค.
# [Database] PostgreSQL ์ค์
DB_USERNAME=postgres
DB_PASSWORD=your_secure_password
DB_URL=jdbc:postgresql://db:5432/userdb
# [Redis] ํ ํฐ ์ ์ฅ์ ์ค์
REDIS_HOST=redis
REDIS_PORT=6379
# [JWT] ์ธ์ฆ ๋ณด์ ์ค์
JWT_SECRET=YOUR_SECURE_BASE64_ENCODED_SECRET
JWT_ACCESS_EXPIRATION=1800000
JWT_REFRESH_EXPIRATION=604800000
# [MSA Infrastructure] ์ค์ ์ค์ ๋ฐ ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ
CONFIG_SERVER=http://your-config-server-ip:13100
EUREKA_SERVER_URL=http://eureka-server:8761/eureka/
ZIPKIN_ENDPOINT=http://zipkin:9411/api/v2/spans
# [Application] ์๋ฒ ๊ตฌ๋ ์ค์
SERVER_PORT=8081
HOSTNAME=user-service
# [GitHub Packages] ๊ณตํต ๋ชจ๋ ๋น๋ ์๊ฒฉ ์ฆ๋ช
(PAT)
GPR_USER=your_github_id
GPR_TOKEN=your_personal_access_token# ์ธ๋ถ ํต์ ์ ์ํ ๋คํธ์ํฌ ์์ฑ
docker network create pgsg-network
# ๋น๋ ๋ฐ ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํ
docker compose up -d --build| ๊ธฐ๋ฅ | ๋ฉ์๋ | ๊ฒฝ๋ก | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ๋ก๊ทธ์ธ | POST |
/api/v1/auth/login |
๋๊ตฌ๋ | ์ฌ์ฉ์ ์ธ์ฆ ๋ฐ ํ ํฐ ์(Access/Refresh) ๋ฐ๊ธ |
| ํ์๊ฐ์ | POST |
/api/v1/auth/signup |
๋๊ตฌ๋ | ์ ๊ท ์ฌ์ฉ์ ๋ฑ๋ก |
| ๋ก๊ทธ์์ | POST |
/api/v1/auth/logout |
์ธ์ฆ ์ฌ์ฉ์ | Access Token ๋ธ๋๋ฆฌ์คํธ ์ฒ๋ฆฌ ๋ฐ ๋ก๊ทธ์์ |
| ํ ํฐ ์ฌ๋ฐ๊ธ | POST |
/api/v1/auth/reissue |
๋๊ตฌ๋ | Refresh Token์ ์ด์ฉํ Access Token ๊ฐฑ์ |
| ๊ธฐ๋ฅ | ๋ฉ์๋ | ๊ฒฝ๋ก | ๊ถํ | ์ค๋ช |
|---|---|---|---|---|
| ๋ด ์ ๋ณด ์กฐํ | GET |
/api/v1/users/me |
์ธ์ฆ ์ฌ์ฉ์ | ํ์ฌ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์์ ์์ธ ํ๋กํ ์กฐํ |
| ํ์ ์์ธ ์กฐํ | GET |
/api/v1/users/{userId} |
์ธ์ฆ ์ฌ์ฉ์ | ํน์ ์ฌ์ฉ์์ ์์ธ ์ ๋ณด ์กฐํ (๊ถํ ํ์ธ ํฌํจ) |
| ํ์ ๋ชฉ๋ก ๊ฒ์ | GET |
/api/v1/users |
๊ด๋ฆฌ์ | QueryDSL ๊ธฐ๋ฐ ๋์ ๊ฒ์ ๋ฐ ํ์ด์ง ์กฐํ |
| ๋ด ํ๋กํ ์์ | PATCH |
/api/v1/users/me |
์ธ์ฆ ์ฌ์ฉ์ | ๋ณธ์ธ์ ๋๋ค์, ์ด๋ฆ ๋ฑ ํ๋กํ ์ ๋ณด ์์ |
| ํ์ ์ ๋ณด ์์ | PATCH |
/api/v1/users/{userId} |
๊ด๋ฆฌ์ | ๊ด๋ฆฌ์ ๊ถํ์ผ๋ก ํน์ ํ์ ์ ๋ณด(์ญํ ๋ฑ) ์์ |
| ํ์ ํํด/์ญ์ | DELETE |
/api/v1/users/{userId} |
์ธ์ฆ/๊ด๋ฆฌ์ | ํ์ ๊ณ์ ์ญ์ (Soft Delete) |
| ๊ธฐ๋ฅ | ๋ฉ์๋ | ๊ฒฝ๋ก | ์ค๋ช |
|---|---|---|---|
| ํ ํฐ ๊ฒ์ฆ | POST |
/internal/v1/auth/verify |
๊ฒ์ดํธ์จ์ด ๋ฑ์์ ํ ํฐ ์ ํจ์ฑ ๋ฐ ๋ธ๋๋ฆฌ์คํธ ์ฌ๋ถ ํ์ธ |
| ์ธ์ฆ์ฉ ์ ๋ณด ์กฐํ | GET |
/internal/v1/users |
username์ผ๋ก ์ธ์ฆ์ ํ์ํ ์ต์ ์ ๋ณด ์กฐํ |
| ํ์ ์์ธ ์ฐ๋ | GET |
/internal/v1/users/{userId} |
ํ ์๋น์ค์์ UUID๋ก ํ์ ๊ธฐ๋ณธ ์ ๋ณด ์ฐ๋ ์ ์ฌ์ฉ |
| ์ฑํ ๊ฐ๋ฅ ์๊ฐ ์กฐํ | GET |
/internal/v1/users/{userId}/chat-availability |
ํน์ ์ฌ์ฉ์์ ์์ผ/์๊ฐ๋ณ ์ฑํ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ |
- Actuator:
/actuator/health,/actuator/info,/actuator/prometheus๋ฅผ ํตํด ์ค์๊ฐ ์ํ ๋ฐ ๋ฉํธ๋ฆญ ํ์ธ ๊ฐ๋ฅ. - Tracing: Micrometer๋ฅผ ํตํด ํธ๋ ์ด์ฑ ๋ฐ์ดํฐ๋ฅผ ์์งํ๊ณ Zipkin๊ณผ ์ฐ๋ํ์ฌ ์๋น์ค ๊ฐ ์์ฒญ ํ๋ฆ์ ์ถ์ ํฉ๋๋ค.
- Log ์์ง (Loki/Promtail): ์ด์ ์๋ฒ์ ๋ก๊ทธ๋ฅผ Promtail์ด ์ค์๊ฐ์ผ๋ก ์์งํ์ฌ Loki ์๋ฒ๋ก ์ ์กํ๋ฉฐ, Grafana๋ฅผ ํตํด ํตํฉ ์กฐํ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
- ๋ถ์ฐ ๋ก๊ทธ ๊ด๋ฆฌ: MDC ํํฐ๋ฅผ ์ ์ฉํ์ฌ ๋ก๊ทธ์ Trace ID๋ฅผ ํฌํจํจ์ผ๋ก์จ ๋ถ์ฐ ํ๊ฒฝ์์์ ๋๋ฒ๊น ๊ณผ ์๊ด๊ด๊ณ ๋ถ์์ ์ง์ํฉ๋๋ค.