Skip to content

th-release/spring-boot-base

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

171 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Spring Boot Base

๊ณตํ†ต ์ธ์ฆ, ๋ณด์•ˆ, ์šด์˜ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ ํฌํ•จํ•œ Spring Boot base ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

์ด ํ”„๋กœ์ ํŠธ๋Š” ๋‹จ์ˆœํ•œ ์ƒ˜ํ”Œ API๊ฐ€ ์•„๋‹ˆ๋ผ, ์‹ค์ œ ์„œ๋น„์Šค ์‹œ์ž‘ ์‹œ ๋ฐ”๋กœ ๊ฐ€์ ธ๋‹ค ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ณตํ†ต ๊ธฐ๋ฐ˜์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. ์ธ์ฆ, ํ† ํฐ, ๊ฐ์‚ฌ ๋กœ๊ทธ, ํŒŒ์ผ ์—…๋กœ๋“œ, ์™ธ๋ถ€ ์—ฐ๋™, ์บ์‹œ, ํ‘œ์ค€ ์‘๋‹ต, ์Šค์ผ€์ค„๋ง, ์šด์˜ ํ—ฌ์Šค์ฒดํฌ, ๋ฌธ์„œํ™”๊นŒ์ง€ ํ•œ ๋ฒˆ์— ๊ฐ–์ถ˜ ํ˜•ํƒœ์ž…๋‹ˆ๋‹ค.

ํ•ต์‹ฌ ํŠน์ง•

  • JWT access/refresh token ์ธ์ฆ
  • refresh token rotation, token family, ์„ธ์…˜๋ณ„ ๊ด€๋ฆฌ
  • ์ธ์ฆ ๋ชฉ์ ๋ณ„ ๊ณตํ†ต ๊ฒ€์ฆ ๋ฐ์ดํ„ฐ ์ €์žฅ
  • BCrypt ๊ธฐ๋ฐ˜ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ €์žฅ/๊ฒ€์ฆ
  • ๋กœ๊ทธ์ธ ์‹คํŒจ ํšŸ์ˆ˜ ์ œํ•œ ๋ฐ ๊ณ„์ • ์ž ๊ธˆ
  • ์ตœ๊ทผ ๋กœ๊ทธ์ธ ์‹œ๊ฐ„/IP ๊ธฐ๋ก
  • ์„ ํƒ์  MFA(TOTP)
  • ์ด๋ฉ”์ผ ์œ ํ‹ธ ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •
  • Firebase ์œ ํ‹ธ
  • FCM ๋””๋ฐ”์ด์Šค ํ† ํฐ ๊ด€๋ฆฌ
  • ๊ฐ์‚ฌ ๋กœ๊ทธ
  • ๋ฏผ๊ฐ์ •๋ณด ๋กœ๊ทธ ๋งˆ์Šคํ‚น
  • XSS ๋ฐฉ์–ด ๋ฐ HTML ํ—ˆ์šฉ ์˜ˆ์™ธ ์ •์ฑ…
  • ํŒŒ์ผ ์—…๋กœ๋“œ ๋ณด์•ˆ ๊ฒ€์ฆ
  • Redis/๋กœ์ปฌ ์บ์‹œ ์ „๋žต ํ‘œ์ค€ํ™”
  • ์™ธ๋ถ€ ์—ฐ๋™ timeout/retry/idempotency ๊ธฐ๋ณธ๊ธฐ
  • ์šด์˜/๊ด€๋ฆฌ์ž API
  • ์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ž๋™ ์ƒ์„ฑ
  • Flyway ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • ํ‘œ์ค€ ์—๋Ÿฌ ์‘๋‹ต
  • Actuator ๊ธฐ๋ฐ˜ ์šด์˜ ํ—ฌ์Šค์ฒดํฌ
  • SPA ๋ผ์šฐํŒ… ์ง€์›

ํŒจํ‚ค์ง€ ๊ตฌ์กฐ

src/main/java/com/threlease/base
โ”œโ”€ common
โ”‚  โ”œโ”€ annotation
โ”‚  โ”œโ”€ configs
โ”‚  โ”œโ”€ controller
โ”‚  โ”œโ”€ convert
โ”‚  โ”œโ”€ entity
โ”‚  โ”œโ”€ enums
โ”‚  โ”œโ”€ exception
โ”‚  โ”œโ”€ handler
โ”‚  โ”œโ”€ interceptors
โ”‚  โ”œโ”€ properties
โ”‚  โ”œโ”€ provider
โ”‚  โ”œโ”€ utils
โ”‚  โ””โ”€ validation
โ”œโ”€ entities
โ”œโ”€ functions
โ”‚  โ””โ”€ auth
โ””โ”€ repositories

์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘์ 

  • BaseApplication
    • ์บ์‹œ ํ™œ์„ฑํ™”
    • ์Šค์ผ€์ค„๋ง ํ™œ์„ฑํ™”
    • JPA Auditing ํ™œ์„ฑํ™”
    • JPA Repository ํ™œ์„ฑํ™”
    • Redis/Redisson ์ž๋™ ์„ค์ • ์ œ์™ธ

์ด ํ”„๋กœ์ ํŠธ๋Š” Redis๋ฅผ Spring Boot ๊ธฐ๋ณธ ์ž๋™์„ค์ •์— ๋งก๊ธฐ์ง€ ์•Š๊ณ , ๋‚ด๋ถ€ CacheConfig์—์„œ ์ง์ ‘ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

์„ค์ • ๊ตฌ์กฐ

๊ณตํ†ต ์„ค์ •

src/main/resources/application.yml

๊ณตํ†ต์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ์„ค์ •์€ ์•„๋ž˜์ž…๋‹ˆ๋‹ค.

  • ๋กœ๊น… ๊ด€๋ จ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ จ
  • CORS ๊ด€๋ จ
  • Swagger ๊ด€๋ จ
  • ๊ธฐ๋ณธ ํ™œ์„ฑ ํ”„๋กœํ•„ ์„ ํƒ

๊ธฐ๋ณธ ํ™œ์„ฑ ํ”„๋กœํ•„:

spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:local}

ํ”„๋กœํ•„๋ณ„ ์„ค์ •

  • application-local.yml
  • application-dev.yml
  • application-prod.yml
  • application-env.yml

์—ญํ• ์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • local, dev, prod
    • ๋ฒ ์ด์Šค ํ”„๋กœ์ ํŠธ ์ปค์Šคํ…€ ๊ธฐ๋Šฅ์„ ๊ณ ์ •๊ฐ’์œผ๋กœ ๊ด€๋ฆฌ
  • env
    • ๋ฒ ์ด์Šค ํ”„๋กœ์ ํŠธ ์ปค์Šคํ…€ ๊ธฐ๋Šฅ์„ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ด€๋ฆฌ

์ฃผ์š” ์ปค์Šคํ…€ ์„ค์ • ๋ฒ”์œ„:

  • app.redis.*
  • app.cache.*
  • app.jwt.*
  • app.token.*
  • app.auth.*
  • app.admin.*
  • app.outbound.*
  • app.privacy.*
  • app.security.*
  • app.qr.*
  • storage.*
  • crypto.*
  • spring.cloud.aws.*

์ธ์ฆ/์ธ๊ฐ€ ๊ธฐ๋Šฅ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • functions/auth/v1/AuthPublicController
  • functions/auth/v1/AuthSessionController
  • functions/auth/v1/AuthPasswordController
  • functions/auth/v1/AuthMfaController
  • functions/auth/v1/AuthAdminController
  • functions/auth/AuthService
  • functions/auth/AuthFlowService
  • functions/auth/AuthAdminBootstrap
  • common/provider/JwtProvider
  • entities/AuthEntity
  • entities/RefreshTokenEntity

์ง€์› ๊ธฐ๋Šฅ

  • ํšŒ์›๊ฐ€์ž…
  • ๋กœ๊ทธ์ธ
  • access token ๋ฐœ๊ธ‰
  • refresh token ๋ฐœ๊ธ‰
  • refresh token rotation
  • ์„ธ์…˜ ๋ชฉ๋ก ์กฐํšŒ
  • ํŠน์ • ์„ธ์…˜ ์ข…๋ฃŒ
  • ํ˜„์žฌ ์„ธ์…˜ ๋กœ๊ทธ์•„์›ƒ
  • ์ „์ฒด ๋กœ๊ทธ์•„์›ƒ
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
  • ๊ด€๋ฆฌ์ž์šฉ ์‚ฌ์šฉ์ž ์กฐํšŒ/์ž ๊ธˆ/์ž ๊ธˆํ•ด์ œ/์„ธ์…˜์กฐํšŒ/์ „์ฒด๋กœ๊ทธ์•„์›ƒ
  • ๊ด€๋ฆฌ์ž์šฉ MFA ์ดˆ๊ธฐํ™”
  • FCM ๋””๋ฐ”์ด์Šค ํ† ํฐ ๋“ฑ๋ก/์กฐํšŒ/๋น„ํ™œ์„ฑํ™”
  • ๊ด€๋ฆฌ์ž์šฉ FCM ํ† ํฐ ์กฐํšŒ ๋ฐ ํ‘ธ์‹œ ๋ฐœ์†ก
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ž๋™ ์ƒ์„ฑ

์ธ์ฆ API

  • POST /api/v1/auth/signup
  • POST /api/v1/auth/login
  • POST /api/v1/auth/refresh
  • POST /api/v1/auth/logout
  • POST /api/v1/auth/logout-all
  • GET /api/v1/auth/sessions
  • DELETE /api/v1/auth/sessions/{tokenId}
  • POST /api/v1/auth/password/change
  • POST /api/v1/auth/password/reset/request
  • POST /api/v1/auth/password/reset/confirm
  • GET /api/v1/auth/fcm/tokens
  • POST /api/v1/auth/fcm/tokens
  • DELETE /api/v1/auth/fcm/tokens/{id}
  • GET /api/v1/auth/@me

๊ด€๋ฆฌ์ž API

  • GET /api/v1/auth/admin/users
  • GET /api/v1/auth/admin/users/{uuid}/sessions
  • POST /api/v1/auth/admin/users/{uuid}/logout-all
  • POST /api/v1/auth/admin/users/{uuid}/lock
  • POST /api/v1/auth/admin/users/{uuid}/unlock
  • POST /api/v1/auth/admin/users/{uuid}/mfa/reset
  • GET /api/v1/auth/admin/users/{uuid}/fcm/tokens
  • POST /api/v1/auth/admin/users/{uuid}/fcm/push

๊ด€๋ฆฌ์ž ๊ถŒํ•œ์€ SYSTEM_ADMIN ๊ถŒํ•œ ์ฝ”๋“œ๋กœ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. ๊ถŒํ•œ์€ tb_auth_permission, tb_auth_permission_grant ๊ธฐ๋ฐ˜์œผ๋กœ ๊ด€๋ฆฌํ•˜๋ฉฐ, ์ƒ์œ„ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๋ฉด ํ•˜์œ„ ๊ถŒํ•œ์ด ํ•จ๊ป˜ ์œ ํšจ ๊ถŒํ•œ์œผ๋กœ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.

์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ์ž๋™ ์ƒ์„ฑ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/properties/app/admin/AdminProperties
  • functions/auth/AuthAdminBootstrap
  • functions/auth/AuthPermissionService

app.admin.enabled=true์ด๋ฉด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ค€๋น„ ์™„๋ฃŒ ์‹œ์ ์— ์ดˆ๊ธฐ ๊ด€๋ฆฌ์ž ๊ณ„์ •์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ๊ฐ™์€ username์˜ ๊ณ„์ •์ด ์žˆ์œผ๋ฉด ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š๊ณ  SYSTEM_ADMIN ๊ถŒํ•œ๋งŒ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

SYSTEM_ADMIN ๊ถŒํ•œ row๊ฐ€ ์—†๋Š” ํ™˜๊ฒฝ์—์„œ๋„ bootstrap ๊ณผ์ •์—์„œ ์ž๋™ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

๊ด€๋ จ ์„ค์ •:

app:
  admin:
    enabled: true
    username: admin
    password: "Admin1234!"
    nickname: ๊ด€๋ฆฌ์ž
    email: [email protected]
    reset-password-on-startup: false

์šด์˜์—์„œ๋Š” application-env.yml ํ”„๋กœํ•„์„ ์‚ฌ์šฉํ•ด ์•„๋ž˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜๋กœ ์ฃผ์ž…ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

  • ADMIN_ENABLED
  • ADMIN_USERNAME
  • ADMIN_PASSWORD
  • ADMIN_NICKNAME
  • ADMIN_EMAIL
  • ADMIN_RESET_PASSWORD_ON_STARTUP

์ฃผ์˜:

  • enabled=true์ธ๋ฐ username ๋˜๋Š” password๊ฐ€ ๋น„์–ด ์žˆ์œผ๋ฉด ๋ถ€ํŒ…์„ ์‹คํŒจ์‹œํ‚ต๋‹ˆ๋‹ค.
  • ์ด๋ฏธ ๊ฐ™์€ username์˜ ๊ณ„์ •์ด ์žˆ์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๊ธฐ์กด ๊ด€๋ฆฌ์ž ๋น„๋ฐ€๋ฒˆํ˜ธ๊นŒ์ง€ ์„ค์ •๊ฐ’์œผ๋กœ ๋งž์ถฐ์•ผ ํ•˜๋ฉด reset-password-on-startup=true๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๊ธฐ๋ณธ ์˜ˆ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์šด์˜ ๋ฐฐํฌ ์งํ›„ ๋ฐ˜๋“œ์‹œ ๋ณ€๊ฒฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋น„๋ฐ€๋ฒˆํ˜ธ ์ •์ฑ…

  • ์ €์žฅ: BCrypt
  • ๊ฒ€์ฆ: BCrypt
  • salt: ์‚ฌ์šฉ์ž๋ณ„ ๋ณ„๋„ salt ์ปฌ๋Ÿผ์„ password pre-hash์— ์‚ฌ์šฉํ•˜๊ณ , BCrypt hash ๋‚ด๋ถ€ salt๋„ ํ•จ๊ป˜ ์‚ฌ์šฉ
  • ๋ ˆ๊ฑฐ์‹œ SHA-512 + salt fallback ์—†์Œ

์ฆ‰ ํ˜„์žฌ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ์ด ์™„์ „ํžˆ BCrypt only ์ž…๋‹ˆ๋‹ค.

refresh token ์ •์ฑ…

  • ํ‰๋ฌธ ์ €์žฅํ•˜์ง€ ์•Š์Œ
  • ํ•ด์‹œ ๊ธฐ๋ฐ˜ ์ €์žฅ
  • tokenId, familyId ๊ธฐ๋ฐ˜ ์„ธ์…˜/rotation ๊ด€๋ฆฌ
  • ์žฌ์‚ฌ์šฉ ๊ฐ์ง€ ์‹œ family revoke ๊ฐ€๋Šฅ
  • ์„ธ์…˜ ์ˆ˜ ์ œํ•œ ์„ค์ • ๊ฐ€๋Šฅ
  • ์„ธ์…˜๋ณ„ issuedAt, lastUsedAt, userAgent, deviceLabel, ipAddress ๊ด€๋ฆฌ

๊ด€๋ จ ์„ค์ •:

  • app.token.storage
  • app.token.max-sessions-per-user

๋กœ๊ทธ์ธ ๋ณด์•ˆ ๊ธฐ๋Šฅ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/properties/app/auth/AuthSecurityProperties
  • functions/auth/AuthService
  • functions/auth/MfaService

๋กœ๊ทธ์ธ ์‹คํŒจ ์ œํ•œ

  • ์‹คํŒจ ํšŸ์ˆ˜ ๋ˆ„์ 
  • ์ œํ•œ ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ ๊ณ„์ • ์ž ๊ธˆ
  • ์„ฑ๊ณต ๋กœ๊ทธ์ธ ์‹œ ์‹คํŒจ ํšŸ์ˆ˜ ์ดˆ๊ธฐํ™”
  • ์ž ๊ธˆ ์‹œ๊ฐ„ ์„ค์ • ๊ฐ€๋Šฅ
  • ์‹คํŒจ/์ž ๊ธˆ ์ƒํƒœ๋Š” auth_login_history ์ตœ์‹  ๊ธฐ๋ก์„ ๊ธฐ์ค€์œผ๋กœ ๊ณ„์‚ฐ

๊ด€๋ จ ์„ค์ •:

app:
  auth:
    login-failure:
      enabled: true
      max-attempts: 5
      lock-minutes: 15

์ตœ๊ทผ ๋กœ๊ทธ์ธ ๊ธฐ๋ก

์„ฑ๊ณต ๋กœ๊ทธ์ธ ๊ธฐ๋ก์€ AuthLoginHistoryEntity์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.

  • lastLoginAt
  • lastLoginIp

์‹ค์ œ ์ €์žฅ ์ปฌ๋Ÿผ:

  • success
  • failureReason
  • failedLoginCount
  • lockedUntil
  • clientIp
  • userAgent

๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ •

์ด ํ”„๋กœ์ ํŠธ๋Š” ์ด๋ฉ”์ผ ๊ธฐ๋Šฅ on/off์— ๋”ฐ๋ผ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ •์ฑ…์ด ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

  • app.email.enabled=true
    • ์žฌ์„ค์ • ์š”์ฒญ ์‹œ ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๋ฐœ๊ธ‰ํ•˜๊ณ  ์ด๋ฉ”์ผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.
    • ์žฌ์„ค์ • ์™„๋ฃŒ ์‹œ ์ด๋ฉ”์ผ ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ๊ฒ€์ฆํ•œ ๋’ค ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.
  • app.email.enabled=false
    • ์žฌ์„ค์ • ์™„๋ฃŒ ์‹œ ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๊ฒ€์ฆํ•œ ๋’ค ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

๊ด€๋ จ API:

  • POST /api/v1/auth/password/reset/request
  • POST /api/v1/auth/password/reset/confirm

๊ด€๋ จ ์„ค์ •:

app:
  auth:
    password-reset:
      code-expire-minutes: 10

๊ณตํ†ต ์ธ์ฆ ๊ฒ€์ฆ ๋ฐ์ดํ„ฐ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • entities/AuthVerificationEntity
  • functions/auth/AuthVerificationService
  • common/enums/AuthVerificationType

์ด ๊ตฌ์กฐ๋Š” "์ด ์ธ์ฆ ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋–ค ์•ก์…˜์„ ์œ„ํ•œ ๊ฒƒ์ธ์ง€"๋ฅผ ํƒ€์ž…์œผ๋กœ ๊ตฌ๋ถ„ํ•ด์„œ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค.

ํ˜„์žฌ ์‚ฌ์šฉ ์ค‘์ธ ํƒ€์ž…:

  • PASSWORD_RESET
  • EMAIL_VERIFICATION
  • LOGIN_CHALLENGE

ํ–ฅํ›„ ํ™•์žฅ ์˜ˆ:

  • ์ด๋ฉ”์ผ ์ธ์ฆ
  • ๋กœ๊ทธ์ธ ์ฑŒ๋ฆฐ์ง€
  • ์ถ”๊ฐ€ ๋ณธ์ธํ™•์ธ ๋‹จ๊ณ„

MFA

์ง€์› ๊ธฐ๋Šฅ:

  • TOTP secret ๋ฐœ๊ธ‰
  • OTP Auth URI ์ œ๊ณต
  • QR code ๊ธฐ๋ฐ˜ MFA setup
  • MFA enable
  • MFA disable
  • ๋กœ๊ทธ์ธ ์‹œ OTP ๊ฒ€์ฆ

๊ด€๋ จ ์„ค์ •:

app:
  auth:
    mfa:
      enabled: false
      issuer: spring-boot-base
      code-digits: 6
      time-step-seconds: 30
      allowed-windows: 1
      required-types: []

๊ฐ์‚ฌ ๋กœ๊ทธ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • functions/auth/AuditLogService

๊ธฐ๋ก ๋Œ€์ƒ ์˜ˆ์‹œ:

  • ๊ด€๋ฆฌ์ž ์ž‘์—…
  • ๊ด€๋ฆฌ์ž ์ž ๊ธˆ/์ž ๊ธˆ ํ•ด์ œ
  • ๊ด€๋ฆฌ์ž MFA ์ดˆ๊ธฐํ™”
  • ๊ด€๋ฆฌ์ž ์ „์ฒด ๋กœ๊ทธ์•„์›ƒ
  • ๊ด€๋ฆฌ์ž FCM ํ‘ธ์‹œ ๋ฐœ์†ก

๊ด€๋ จ ์„ค์ •:

app:
  auth:
    audit:
      enabled: true
      include-user-agent: true

๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ์™€ ํ•จ๊ป˜ ์—ฐ๊ฒฐ๋˜๋Š” ์„ค์ •:

  • app.privacy.mask-audit-ip
  • app.privacy.include-user-agent
  • app.privacy.audit-retention-days

์ฐธ๊ณ :

  • ํ˜„์žฌ ๊ฐ์‚ฌ ์ด๋ฒคํŠธ๋Š” DB ์ ์žฌ ๋Œ€์‹  ๊ตฌ์กฐํ™”๋œ ์ฝ˜์†” ๋กœ๊ทธ๋กœ ๋‚จ๊น๋‹ˆ๋‹ค.
  • ์„ธ์…˜ ํ™œ๋™์ฒ˜๋Ÿผ ๋นˆ๋„๊ฐ€ ๋†’์€ ๋กœ๊ทธ๋Š” DB์— ์ €์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ๊ด€๋ฆฌ์ž ๊ด€๋ จ ์ž‘์—…์€ ADMIN EVENT ๋กœ๊ทธ๋กœ ๊ตฌ๋ถ„ํ•ด ์šด์˜ ๋กœ๊ทธ ์ˆ˜์ง‘๊ธฐ์—์„œ ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฉ”์ผ ์œ ํ‹ธ๋ฆฌํ‹ฐ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/properties/app/email/EmailProperties
  • common/configs/EmailConfig
  • common/utils/email/EmailService

๊ธฐ๋Šฅ:

  • SMTP ๋ฉ”์ผ ๋ฐœ์†ก
  • ํ…์ŠคํŠธ ๋ฉ”์ผ ๋ฐœ์†ก
  • HTML ๋ฉ”์ผ ๋ฐœ์†ก
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ์ธ์ฆ ์ฝ”๋“œ ๋ฐœ์†ก
  • on/off ์„ค์ • ์ง€์›
  • on ์ธ๋ฐ ํ•„์ˆ˜ ์„ค์ •์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์‹œ์ž‘ ์‹œ ๊ฐ•์ œ ์ข…๋ฃŒ

ํ•„์ˆ˜ ์„ค์ •:

  • app.email.host
  • app.email.port
  • app.email.username
  • app.email.password
  • app.email.from-address

์‚ฌ์šฉ ์˜ˆ์‹œ:

emailService.sendText("[email protected]", "Subject", "Body");
emailService.sendHtml("[email protected]", "Subject", "<b>Body</b>");

Firebase ์œ ํ‹ธ๋ฆฌํ‹ฐ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/properties/app/firebase/FirebaseProperties
  • common/configs/FirebaseConfig
  • common/utils/firebase/FirebaseUtils

๊ธฐ๋Šฅ:

  • Firebase Admin SDK ์ดˆ๊ธฐํ™”
  • Firebase ID ํ† ํฐ ๊ฒ€์ฆ
  • FCM push ์ „์†ก
  • ์‚ฌ์šฉ์ž๋ณ„ ๋””๋ฐ”์ด์Šค ํ† ํฐ ๊ธฐ๋ฐ˜ ํ‘ธ์‹œ ๋ฐœ์†ก ์ง€์›
  • on/off ์„ค์ • ์ง€์›
  • on ์ธ๋ฐ ํ•„์ˆ˜ ์„ค์ •์ด ๋น„์–ด ์žˆ์œผ๋ฉด ์‹œ์ž‘ ์‹œ ๊ฐ•์ œ ์ข…๋ฃŒ

ํ•„์ˆ˜ ์„ค์ •:

  • app.firebase.project-id
  • app.firebase.credentials-path ๋˜๋Š” app.firebase.credentials-json

์‚ฌ์šฉ ์˜ˆ์‹œ:

firebaseUtils.verifyIdToken(idToken);
firebaseUtils.sendNotification(targetToken, "title", "body", Map.of("type", "notice"));

FCM ๋””๋ฐ”์ด์Šค ํ† ํฐ ๊ด€๋ฆฌ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • entities/FcmDeviceTokenEntity
  • functions/auth/FcmDeviceTokenService
  • repositories/auth/FcmDeviceTokenRepository

๊ธฐ๋Šฅ:

  • ์‚ฌ์šฉ์ž ๋””๋ฐ”์ด์Šค ํ† ํฐ ๋“ฑ๋ก
  • ๋‚ด ํ† ํฐ ๋ชฉ๋ก ์กฐํšŒ
  • ํ† ํฐ ๋น„ํ™œ์„ฑํ™”
  • ๊ด€๋ฆฌ์ž์šฉ ์‚ฌ์šฉ์ž ํ† ํฐ ์กฐํšŒ
  • ๊ด€๋ฆฌ์ž์šฉ ์‚ฌ์šฉ์ž ๋””๋ฐ”์ด์Šค ํ‘ธ์‹œ ๋ฐœ์†ก

์ €์žฅ ์ •๋ณด:

  • deviceToken
  • deviceLabel
  • userAgent
  • lastIpAddress
  • lastUsedAt
  • enabled

์›น/MVC ๊ธฐ๋Šฅ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/WebMvcConfig
  • common/interceptors/TokenInterceptor
  • common/interceptors/ApiVersionInterceptor
  • common/controller/ViewController
  • common/controller/CommonController

API ๋ฒ„์ €๋‹

@ApiVersion(1) ์ด ๋ถ™์€ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ์ž๋™์œผ๋กœ /api/v1 prefix๊ฐ€ ๋ถ™์Šต๋‹ˆ๋‹ค.

์˜ˆ:

@RestController
@ApiVersion(1)
@RequestMapping("/sample")
public class SampleController {
}

์‹ค์ œ ๊ฒฝ๋กœ:

/api/v1/sample

ํ† ํฐ ์ธํ„ฐ์…‰ํ„ฐ

TokenInterceptor๋Š” ๋‹ค์Œ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

  • Authorization ํ—ค๋” ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ
  • Bearer prefix ํ™•์ธ
  • JWT์—์„œ ์‚ฌ์šฉ์ž ์กฐํšŒ
  • ์š”์ฒญ attribute์— ์‚ฌ์šฉ์ž ์ฃผ์ž…

SPA ๋ผ์šฐํŒ… ์ง€์›

ViewController๋Š” /api/** ๊ฐ€ ์•„๋‹Œ ํ™•์žฅ์ž ์—†๋Š” ์š”์ฒญ์„ ๋ชจ๋‘ index.html๋กœ forwardํ•ฉ๋‹ˆ๋‹ค.

์ฆ‰ React/Vue SPA ๋ผ์šฐํŒ…์„ ์„œ๋ฒ„ ๋‹จ์—์„œ๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๊ณตํ†ต enum API

CommonController๋Š” ์‹œ์Šคํ…œ enum ๋ชฉ๋ก์„ JSON์œผ๋กœ ๋…ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ:

  • GET /api/common/enums

๋ณด์•ˆ ํ—ค๋”/XSS/๋กœ๊ทธ ๋งˆ์Šคํ‚น

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/WebSecurityConfig
  • common/handler/XssFilter
  • common/handler/XssRequestWrapper
  • common/handler/XssRequestBodyAdvice
  • common/handler/LoggingFilter
  • common/utils/XssUtils

๋ณด์•ˆ ํ—ค๋”

๊ธฐ๋ณธ ์ œ๊ณต:

  • CSP
  • Frame deny
  • HSTS
  • Referrer-Policy
  • Content-Type-Options

์„ค์ •:

app:
  security:
    headers:
      hsts-enabled: true
      content-security-policy: "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
      referrer-policy: strict-origin-when-cross-origin

XSS ๋ฐฉ์–ด

๊ธฐ๋Šฅ:

  • request parameter sanitize
  • request body string sanitize
  • JSON/Map/List ์žฌ๊ท€ sanitize
  • @AllowHtml ์˜ˆ์™ธ ์ง€์›

@AllowHtml ์˜ˆ์‹œ:

public class NoticeCreateDto {
    @AllowHtml(AllowHtml.Policy.BASIC)
    private String content;
}

๊ธฐ๋ณธ์€ sanitize, HTML์„ ํ—ˆ์šฉํ•ด์•ผ ํ•˜๋Š” ํ•„๋“œ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋กœ๊ทธ ๋งˆ์Šคํ‚น

LoggingFilter๋Š” ๋‹ค์Œ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

  • request/response payload ๋กœ๊น…
  • correlation id ์ƒ์„ฑ ๋ฐ ์‘๋‹ต ํ—ค๋” ์ถ”๊ฐ€
  • JSON/form/query string ๋งˆ์Šคํ‚น
  • ๋ฏผ๊ฐ ํ‚ค ์ด๋ฆ„ ๊ธฐ๋ฐ˜ ๋งˆ์Šคํ‚น
  • JWT/Bearer/API key ํŒจํ„ด ๊ธฐ๋ฐ˜ ๋งˆ์Šคํ‚น

๊ธฐ๋ณธ ๋ฏผ๊ฐ ํ‚ค ์˜ˆ์‹œ:

  • password
  • newPassword
  • accessToken
  • refreshToken
  • authorization
  • secret
  • apiKey
  • otp
  • cookie

ํ‘œ์ค€ ์‘๋‹ต/์˜ˆ์™ธ ์ฒ˜๋ฆฌ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/utils/responses/BasicResponse
  • common/utils/responses/PageResponse
  • common/handler/GlobalExceptionHandler
  • common/exception/ErrorCode
  • common/exception/BusinessException

๊ธฐ๋Šฅ:

  • ์„ฑ๊ณต/์‹คํŒจ ์‘๋‹ต ํ˜•์‹ ํ†ต์ผ
  • ์—๋Ÿฌ ์ฝ”๋“œ ํ‘œ์ค€ํ™”
  • validation field error ์‘๋‹ต
  • path, timestamp, correlationId ํฌํ•จ
  • ๋ฉ”์‹œ์ง€ ๊ตญ์ œํ™” ์—ฐ๋™

์ธ์ฆ ์‘๋‹ต ๋…ธ์ถœ ์ •์ฑ…

์ธ์ฆ ๊ด€๋ จ ์‘๋‹ต์€ JPA ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€ ์•Š๊ณ  ์•ˆ์ „ํ•œ DTO๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค.

  • ํšŒ์›๊ฐ€์ž… ์‘๋‹ต: AuthProfileDto
  • ๋‚ด ์ •๋ณด ์กฐํšŒ ์‘๋‹ต: AuthProfileDto

์ฆ‰ ์•„๋ž˜ ๋‚ด๋ถ€ ์ธ์ฆ ํ•„๋“œ๋Š” ์™ธ๋ถ€ ์‘๋‹ต์—์„œ ์ง์ ‘ ๋…ธ์ถœํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

  • password
  • refresh token ์›๋ฌธ
  • MFA secret
  • ์ธ์ฆ ๊ฒ€์ฆ hash
  • ๋‚ด๋ถ€ ๋กœ๊ทธ์ธ ์‹คํŒจ/์ž ๊ธˆ ์ƒํƒœ

์บ์‹œ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/CacheConfig
  • common/properties/app/cache/CachePolicyProperties
  • common/properties/app/redis/RedisProperties

๊ธฐ๋Šฅ:

  • Redis ๊ธฐ๋ฐ˜ ์บ์‹œ
  • Redis ๋น„ํ™œ์„ฑ ์‹œ ๋กœ์ปฌ ์บ์‹œ fallback
  • cache name ์ •์ฑ…
  • TTL ์„ค์ •
  • cache prefix ์„ค์ •

๋Œ€ํ‘œ ์บ์‹œ ์‚ฌ์šฉ์ฒ˜:

  • ์‚ฌ์šฉ์ž ์กฐํšŒ ์บ์‹œ

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค/Flyway/JPA

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/properties/app/database/DatabaseProperties
  • common/configs/CustomPhysicalNamingStrategy

๊ธฐ๋Šฅ:

  • JPA auditing
  • ํ…Œ์ด๋ธ”/์ปฌ๋Ÿผ naming ์ „๋žต
  • Flyway migration
  • JPA schema, Flyway migration target schema, Flyway history schema ๋ถ„๋ฆฌ ์„ค์ •
  • DB schema ๋ณ€๊ฒฝ์€ Flyway migration์œผ๋กœ ๊ฒ€์ฆ/๊ด€๋ฆฌ
  • Hibernate๋Š” ddl-auto=validate๋กœ ์—”ํ‹ฐํ‹ฐ์™€ DB schema ๋ถˆ์ผ์น˜๋งŒ ๊ฒ€์ฆ

์„ค์ • ์˜ˆ์‹œ:

spring:
  jpa:
    hibernate:
      ddl-auto: validate
  flyway:
    enabled: true
    default-schema: public
    schemas:
      - public
      - base
    placeholders:
      appSchema: base

app:
  database:
    jpa-schema: base
    flyway-schema: base
    flyway-history-schema: public

ํ˜„์žฌ ๊ธฐ๋ณธ ์ •์ฑ…:

  • JPA/Hibernate๋Š” base schema๋ฅผ ๋ด…๋‹ˆ๋‹ค.
  • Flyway flyway_schema_history๋Š” public schema์— ๋‘ก๋‹ˆ๋‹ค.
  • Flyway migration SQL์€ ${appSchema} placeholder๋ฅผ ํ†ตํ•ด base schema์— ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑ/๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

์™ธ๋ถ€ ์—ฐ๋™

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/RestTemplateConfig
  • common/configs/RetryConfig
  • common/handler/OutboundRequestInterceptor
  • common/handler/RestTemplateLoggingInterceptor
  • common/utils/RestService
  • common/properties/app/outbound/OutboundProperties

๊ธฐ๋Šฅ:

  • connect/read timeout
  • retry
  • correlation-id ์ „๋‹ฌ
  • idempotency key ์ถ”๊ฐ€
  • ์™ธ๋ถ€ ์š”์ฒญ ๋กœ๊น…

RestService ์‚ฌ์šฉ ์˜ˆ์‹œ:

String response = restService.get(
    "https://example.com/api/users",
    Map.of("page", 0, "size", 20),
    String.class
);

ํ—ค๋” ํฌํ•จ ํ˜ธ์ถœ ์˜ˆ์‹œ:

ResponseEntity<String> response = restService.exchange(
    "https://example.com/api/orders",
    HttpMethod.POST,
    Map.of("name", "sample"),
    Map.of("Authorization", "Bearer xxx"),
    String.class
);

ํŒŒ์ผ ์—…๋กœ๋“œ/์Šคํ† ๋ฆฌ์ง€

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/StorageConfig
  • common/utils/storage/StorageService
  • common/utils/storage/LocalStorageService
  • common/utils/storage/S3StorageService
  • common/utils/storage/FileUploadSecurityService
  • common/utils/storage/entity/FileEntity
  • common/utils/storage/repository/FileRepository
  • common/utils/storage/batch/OrphanFileCleanupJob

๊ธฐ๋Šฅ:

  • ๋กœ์ปฌ ์ €์žฅ
  • S3 ์ €์žฅ
  • ์Šคํ† ๋ฆฌ์ง€ ๊ตฌํ˜„ ์ž๋™ ์„ ํƒ
  • ์—…๋กœ๋“œ ํŒŒ์ผ๋ช… sanitize
  • ์ด์ค‘ ํ™•์žฅ์ž ์ฐจ๋‹จ
  • ํŒŒ์ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ €์žฅ
  • DB ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ํŒŒ์ผ ์กฐํšŒ/์‚ญ์ œ
  • ํ† ํฐ ๊ธฐ๋ฐ˜ ๋‹ค์šด๋กœ๋“œ URL ๋ฐœ๊ธ‰
  • ๋กœ์ปฌ ํŒŒ์ผ ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต
  • S3 ํŒŒ์ผ presigned/redirect ๊ธฐ๋ฐ˜ ๋‹ค์šด๋กœ๋“œ
  • ๊ณ ์•„ ํŒŒ์ผ ์ •๋ฆฌ ์Šค์ผ€์ค„

์ฐธ๊ณ :

  • ํ™•์žฅ์ž/content-type allowlist๋Š” ๋ฒ ์ด์Šค ํ”„๋กœ์ ํŠธ์—์„œ ๊ฐ•์ œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์„œ๋น„์Šค๋ณ„ ์ •์ฑ…์ด ๋‹ค๋ฅด๋ฏ€๋กœ ์‹ค์ œ ๋„๋ฉ”์ธ์—์„œ ๋ณ„๋„ ๊ฒ€์ฆ ๋กœ์ง์„ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ์ „์ œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

์—…๋กœ๋“œ ๋ณด์•ˆ ์„ค์ • ์˜ˆ์‹œ:

storage:
  upload:
    enabled: true
    max-file-size: 10MB
    block-double-extension: true

ํŒŒ์ผ API:

  • POST /api/v1/files
  • DELETE /api/v1/files/{id}
  • GET /api/v1/files/{id}/download-url
  • GET /api/v1/files/content/**

์Šค์ผ€์ค„๋ง/๋น„๋™๊ธฐ/๋ฝ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/AsyncConfig
  • common/configs/SchedulingConfig
  • common/handler/MdcTaskDecorator
  • common/annotation/DistributedLock
  • common/handler/DistributedLockAspect

๊ธฐ๋Šฅ:

  • ๋น„๋™๊ธฐ task executor
  • MDC ์ „ํŒŒ
  • ์Šค์ผ€์ค„ ์ž‘์—…
  • ๋ถ„์‚ฐ ๋ฝ ๊ธฐ๋ฐ˜ ์ž„๊ณ„์˜์—ญ ๋ณดํ˜ธ

Swagger / Actuator / ์šด์˜์„ฑ

๋Œ€ํ‘œ ํด๋ž˜์Šค:

  • common/configs/SwaggerConfig
  • common/handler/BasePlatformHealthIndicator

๊ธฐ๋Šฅ:

  • Swagger UI
  • OpenAPI ๋ฌธ์„œ
  • health / metrics / prometheus
  • base ํ”Œ๋žซํผ health indicator

Swagger ๊ฒฝ๋กœ:

  • /api/swagger

Repository/Query ์ž‘์„ฑ ๊ทœ์น™

์ด ํ”„๋กœ์ ํŠธ๋Š” repository ์กฐํšŒ ์˜๋„๋ฅผ ๋ช…์‹œํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.

  • ๋‹จ์ˆœ ์กฐํšŒ๋„ ๊ฐ€๊ธ‰์  @Query๋กœ JPQL์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค.
  • default ๋ฉ”์„œ๋“œ์—์„œ UUID๋ฅผ entity reference๋กœ ๊ฐ์‹ธ๊ฑฐ๋‚˜ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ ํ›„ ์ฒซ ๊ฑด์„ ๊บผ๋‚ด๋Š” ํŒจํ„ด์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ์ฐธ์กฐ ์กฐ๊ฑด์€ userUuid ๊ฐ™์€ ์ค‘๋ณต FK ํ•„๋“œ ๋Œ€์‹  entity.user = :user ํ˜•ํƒœ๋กœ ๊ด€๊ณ„ ํ•„๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
  • update๋Š” repository bulk update๋ณด๋‹ค entity๋ฅผ ์กฐํšŒํ•œ ๋’ค ๊ฐ’์„ ๋ฐ”๊พธ๊ณ  repository.save()๋กœ ์ €์žฅํ•˜๋Š” ๋ฐฉ์‹์„ ๊ธฐ๋ณธ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.
  • search์ฒ˜๋Ÿผ ์กฐ๊ฑด์ด ๋ณต์žกํ•œ ์กฐํšŒ๋Š” QueryDSL custom repository๋กœ ๋ถ„๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ

์ด ํ”„๋กœ์ ํŠธ๋Š” ์‹ค๋ฌด์—์„œ ์ž์ฃผ ๋ฐ˜๋ณต๋˜๋Š” ์ž‘์—…์„ ์ค„์ด๊ธฐ ์œ„ํ•œ ์œ ํ‹ธ๋ฆฌํ‹ฐ๋ฅผ ๋งŽ์ด ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

๋‚ ์งœ/์‹œ๊ฐ„

ํด๋ž˜์Šค:

  • common/utils/DateTimeUtils

๊ธฐ๋Šฅ:

  • ๋ฌธ์ž์—ด yyyy-MM-dd โ†’ LocalDateTime
  • LocalDateTime โ†’ ๋‚ ์งœ ๋ฌธ์ž์—ด
  • LocalDateTime โ†’ ๋‚ ์งœ์‹œ๊ฐ„ ๋ฌธ์ž์—ด
  • ํ˜„์žฌ ์‹œ๊ฐ ์กฐํšŒ

์‚ฌ์šฉ ์˜ˆ์‹œ:

Failable<LocalDateTime, String> result = dateTimeUtils.parseDateStringToLocalDateTime("2026-04-08");
if (result.isSuccess()) {
    LocalDateTime value = result.getSuccess();
}

String date = dateTimeUtils.formatLocalDateTimeToDateString(LocalDateTime.now());
String dateTime = dateTimeUtils.formatLocalDateTimeToDateTimeString(LocalDateTime.now());

JSON

ํด๋ž˜์Šค:

  • common/utils/JsonUtils

๊ธฐ๋Šฅ:

  • ๊ฐ์ฒด โ†’ pretty JSON
  • ๊ฐ์ฒด โ†’ compact JSON
  • JSON ๋ฌธ์ž์—ด โ†’ ๊ฐ์ฒด
  • JSON pretty print

์‚ฌ์šฉ ์˜ˆ์‹œ:

String json = JsonUtils.toJson(Map.of("name", "base"));
String compact = JsonUtils.toJsonCompact(Map.of("id", 1));
MyDto dto = JsonUtils.fromJson("{\"name\":\"sample\"}", MyDto.class);

๊ตญ์ œํ™” ๋ฉ”์‹œ์ง€

ํด๋ž˜์Šค:

  • common/utils/MessageUtils

๊ธฐ๋Šฅ:

  • ํ˜„์žฌ locale ๊ธฐ์ค€ ๋ฉ”์‹œ์ง€ ์กฐํšŒ
  • ํŒŒ๋ผ๋ฏธํ„ฐ ์น˜ํ™˜ ๋ฉ”์‹œ์ง€ ์กฐํšŒ
  • locale ์ง€์ • ์กฐํšŒ

์‚ฌ์šฉ ์˜ˆ์‹œ:

String message = MessageUtils.getMessage("common.error");
String formatted = MessageUtils.getMessage("USER_NOT_FOUND", new Object[]{"tester"});

์•”ํ˜ธํ™”

ํด๋ž˜์Šค:

  • common/utils/crypto/AesComponent
  • common/convert/EncryptedStringConverter
  • common/utils/crypto/Base64Component
  • common/utils/crypto/HashComponent

๊ธฐ๋Šฅ:

  • AES-256-GCM ์–‘๋ฐฉํ–ฅ ์•”๋ณตํ˜ธํ™”
  • JPA entity ํ•„๋“œ ์ž๋™ ์•”๋ณตํ˜ธํ™”
  • Base64 encode/decode
  • SHA-512 ํ•ด์‹œ ์œ ํ‹ธ

AesComponent ์‚ฌ์šฉ ์˜ˆ์‹œ:

String encrypted = aesComponent.encrypt("01012345678");
String plain = aesComponent.decrypt(encrypted);
boolean same = aesComponent.matches("01012345678", encrypted);

EncryptedStringConverter ์‚ฌ์šฉ ์˜ˆ์‹œ:

@Convert(converter = EncryptedStringConverter.class)
private String phoneNumber;

๋‚œ์ˆ˜/์ฝ”๋“œ ์ƒ์„ฑ

ํด๋ž˜์Šค:

  • common/utils/random/RandomComponent

๊ธฐ๋Šฅ:

  • OTP ์ƒ์„ฑ
  • ์ž„์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ƒ์„ฑ
  • ์ดˆ๋Œ€์ฝ”๋“œ ์ƒ์„ฑ
  • ์•ŒํŒŒ๋‰ด๋ฉ”๋ฆญ ๋ฌธ์ž์—ด ์ƒ์„ฑ

์‚ฌ์šฉ ์˜ˆ์‹œ:

String otp = randomComponent.generateOtp();
String tempPassword = randomComponent.generateTempPassword();
String inviteCode = randomComponent.generateInviteCode();
String random = randomComponent.generateAlphanumeric(32);

ํŒŒ์ผ ์ฒ˜๋ฆฌ

ํด๋ž˜์Šค:

  • common/utils/FileUtils

๊ธฐ๋Šฅ:

  • ํŒŒ์ผ/๋””๋ ‰ํ† ๋ฆฌ ์กด์žฌ ํ™•์ธ
  • ํŒŒ์ผ/๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
  • ํŒŒ์ผ ์‚ญ์ œ
  • ๋””๋ ‰ํ† ๋ฆฌ ์žฌ๊ท€ ์‚ญ์ œ
  • ๋ฌธ์ž์—ด ์ฝ๊ธฐ/์“ฐ๊ธฐ
  • ํŒŒ์ผ ๋ณต์‚ฌ
  • ํ™•์žฅ์ž ์ถ”์ถœ

์‚ฌ์šฉ ์˜ˆ์‹œ:

if (!FileUtils.exists("./uploads/test.txt")) {
    FileUtils.createDirectory("./uploads");
    FileUtils.writeStringToFile("./uploads/test.txt", "hello");
}

String ext = FileUtils.getFileExtension("sample.pdf");

ํŽ˜์ด์ง€๋„ค์ด์…˜

ํด๋ž˜์Šค:

  • common/utils/PageRequestHelper

๊ธฐ๋Šฅ:

  • ๊ธฐ๋ณธ pageable ์ƒ์„ฑ
  • ์ •๋ ฌ ํฌํ•จ pageable ์ƒ์„ฑ
  • ์ตœ์‹ ์ˆœ pageable ์ƒ์„ฑ

์‚ฌ์šฉ ์˜ˆ์‹œ:

Pageable pageable = PageRequestHelper.of(page, size);
Pageable latest = PageRequestHelper.ofLatest(page, size);

๊ฐœ์ธ์ •๋ณด ๋งˆ์Šคํ‚น

ํด๋ž˜์Šค:

  • common/utils/masking/MaskingComponent

๊ธฐ๋Šฅ:

  • ์ด๋ฆ„ ๋งˆ์Šคํ‚น
  • ์ „ํ™”๋ฒˆํ˜ธ ๋งˆ์Šคํ‚น
  • ์ด๋ฉ”์ผ ๋งˆ์Šคํ‚น

์‚ฌ์šฉ ์˜ˆ์‹œ:

String maskedName = maskingComponent.maskName("ํ™๊ธธ๋™");
String maskedPhone = maskingComponent.maskPhone("+821012345678");
String maskedEmail = maskingComponent.maskEmail("[email protected]");

QR ์ฝ”๋“œ

ํด๋ž˜์Šค:

  • common/utils/QR/QRCode
  • common/utils/QR/QRCodeOption

๊ธฐ๋Šฅ:

  • QR ์ƒ์„ฑ byte[]
  • QR ์ƒ์„ฑ Base64
  • QR ํŒŒ์ผ ์ €์žฅ
  • ๋กœ๊ณ  ํฌํ•จ QR ์ƒ์„ฑ
  • QR decode

์‚ฌ์šฉ ์˜ˆ์‹œ:

byte[] qr = qrCode.generateQrCode("otpauth://totp/sample");
String base64 = qrCode.generateQrCodeBase64("hello");
qrCode.generateQrCodeToFile("hello", Path.of("./tmp/hello.png"));

์—‘์…€

ํด๋ž˜์Šค:

  • common/utils/ExcelUtils
  • common/annotation/ExcelColumn

๊ธฐ๋Šฅ:

  • ๋ฆฌ์ŠคํŠธ โ†’ ์—‘์…€ export
  • ์—‘์…€ โ†’ ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ import

์‚ฌ์šฉ ์˜ˆ์‹œ:

Workbook workbook = ExcelUtils.export(users, UserExcelDto.class);
List<UserExcelDto> rows = ExcelUtils.importExcel(inputStream, UserExcelDto.class);

DTO ์˜ˆ์‹œ:

public class UserExcelDto {
    @ExcelColumn(headerName = "์•„์ด๋””", order = 1)
    private String username;

    @ExcelColumn(headerName = "๋‹‰๋„ค์ž„", order = 2)
    private String nickname;
}

๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ

  • CollectionUtils
    • ์ปฌ๋ ‰์…˜ ๊ด€๋ จ ํ—ฌํผ
  • ObjectUtils
    • null-safe ๊ฐ์ฒด ์ฒ˜๋ฆฌ
  • ReflectionUtils
    • reflection ํ—ฌํผ
  • IpUtils
    • ํด๋ผ์ด์–ธํŠธ IP ์ถ”์ถœ
  • QuerydslUtils
    • Querydsl ๊ด€๋ จ ๋ณด์กฐ ๋กœ์ง
  • EnumMapperValue, EnumValueValidator
    • enum ์ฝ”๋“œ ๋…ธ์ถœ/๊ฒ€์ฆ

๊ณตํ†ต ๊ฒ€์ฆ ์–ด๋…ธํ…Œ์ด์…˜

์ง€์› ์–ด๋…ธํ…Œ์ด์…˜:

  • @ValidEmail
  • @ValidPhoneNumber
  • @ValidEnum

์˜ˆ์‹œ:

public class SignUpDto {
    @ValidEmail
    private String email;
}

์ •์  ๋ฆฌ์†Œ์Šค

src/main/resources/static

  • index.html
  • script.js
  • style.css

๊ธฐ๋ณธ SPA ์ง„์ž… ํ™”๋ฉด/์ƒ˜ํ”Œ ๋ฆฌ์†Œ์Šค๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฉ”์‹œ์ง€/๋กœ๊น… ๋ฆฌ์†Œ์Šค

  • messages.properties

I18N

  • ํ”„๋กœํ•„ ์„ค์ • ํŒŒ์ผ์˜ app.i18n.enabled๋กœ ๋‹ค๊ตญ์–ด ๊ธฐ๋Šฅ์„ ์ผœ๊ณ  ๋Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • app.i18n.default-locale๋กœ ์„œ๋ฒ„ ๊ธฐ๋ณธ ์–ธ์–ด๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ: ko, en
  • app.i18n.supported-locales์— ์—†๋Š” ์–ธ์–ด๊ฐ€ ๋“ค์–ด์˜ค๋ฉด ๊ธฐ๋ณธ ์–ธ์–ด๋กœ ์ž๋™ fallback ๋ฉ๋‹ˆ๋‹ค.
  • enabled=false์ด๋ฉด Accept-Language ํ—ค๋”๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ํ•ญ์ƒ ๊ธฐ๋ณธ ์–ธ์–ด๋งŒ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฉ”์‹œ์ง€ ๋ฒˆ๋“ค์€ ์•„๋ž˜ ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • src/main/resources/messages.properties
    • src/main/resources/messages_ko.properties
    • src/main/resources/messages_en.properties
  • ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ์™€ ๊ฒ€์ฆ ๋ฉ”์‹œ์ง€๋Š” ๊ฐ™์€ message source๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
    • ๋‹ค๊ตญ์–ด/๋ฉ”์‹œ์ง€ ์ฝ”๋“œ ๋ฆฌ์†Œ์Šค
  • logback-spring.xml
    • ๋กœ๊น… ์„ค์ •

DB ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜

  • src/main/resources/db/migration/V1__base_security_extensions.sql

๋ณด์•ˆ/์ธ์ฆ ๊ด€๋ จ ํ™•์žฅ ์Šคํ‚ค๋งˆ๋ฅผ Flyway๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ

ํ˜„์žฌ ํฌํ•จ๋œ ํ…Œ์ŠคํŠธ:

  • BaseApplicationTests
  • AuthVerificationServiceTest
  • AuthSecurityPolicyTest
  • AuthPermissionServiceTest
  • AuthServiceRdbTest
  • FcmDeviceTokenServiceTest
  • MfaServiceTest

๊ฒ€์ฆ ๋ช…๋ น:

./gradlew compileJava
./gradlew test

๋น ๋ฅธ ์‹œ์ž‘

1. ๋กœ์ปฌ ์‹คํ–‰

SPRING_PROFILES_ACTIVE=local ./gradlew bootRun

2. ํ™˜๊ฒฝ๋ณ€์ˆ˜ ๊ธฐ๋ฐ˜ ์‹คํ–‰

SPRING_PROFILES_ACTIVE=env \
JWT_SECRET_KEY=... \
AES_SECRET_KEY=... \
./gradlew bootRun

3. Swagger ํ™•์ธ

http://localhost:8080/api/swagger

๊ถŒ์žฅ ์‚ฌ์šฉ ๋ฐฉ์‹

  • ์‚ฌ์šฉ์ž ์ธ์ฆ์ด ํ•„์š”ํ•œ API๋Š” @ApiVersion + TokenInterceptor ํ๋ฆ„์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.
  • ๋ฏผ๊ฐ์ •๋ณด๋Š” ๋กœ๊ทธ์— ๋‚จ๊ธฐ์ง€ ๋ง๊ณ  LoggingFilter ๋งˆ์Šคํ‚น ๊ทœ์น™์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค.
  • ์ €์žฅํ˜• ๋ฏผ๊ฐ์ •๋ณด๋Š” EncryptedStringConverter ๊ฐ™์€ ์•”ํ˜ธํ™” ๊ฒฝ๋กœ๋ฅผ ์šฐ์„  ๊ฒ€ํ† ํ•ฉ๋‹ˆ๋‹ค.
  • HTML ํ—ˆ์šฉ ํ•„๋“œ๋Š” ๊ธฐ๋ณธ sanitize๋ฅผ ์œ ์ง€ํ•œ ์ฑ„ @AllowHtml๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  • ์™ธ๋ถ€ API ํ˜ธ์ถœ์€ ์ง์ ‘ RestTemplate๋ณด๋‹ค RestService๋ฅผ ์šฐ์„  ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
  • ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” ๋ฐ˜๋“œ์‹œ StorageService๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๋ฌธ์„œ

  • docs/BASE_TEMPLATE_CHECKLIST.md
  • docs/API_DEPRECATION_POLICY.md

์ด ๋ฌธ์„œ๋“ค๊ณผ ํ•จ๊ป˜ ๋ณด๋ฉด ๋ฒ ์ด์Šค ํ”„๋กœ์ ํŠธ๋ฅผ ํŒ€ ํ‘œ์ค€์œผ๋กœ ๊ฐ€์ ธ๊ฐ€๊ธฐ๊ฐ€ ๋” ์‰ฝ์Šต๋‹ˆ๋‹ค.

About

spring boot base

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages