๊ณตํต ์ธ์ฆ, ๋ณด์, ์ด์ ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ ํฌํจํ 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.ymlapplication-dev.ymlapplication-prod.ymlapplication-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/AuthPublicControllerfunctions/auth/v1/AuthSessionControllerfunctions/auth/v1/AuthPasswordControllerfunctions/auth/v1/AuthMfaControllerfunctions/auth/v1/AuthAdminControllerfunctions/auth/AuthServicefunctions/auth/AuthFlowServicefunctions/auth/AuthAdminBootstrapcommon/provider/JwtProviderentities/AuthEntityentities/RefreshTokenEntity
- ํ์๊ฐ์
- ๋ก๊ทธ์ธ
- access token ๋ฐ๊ธ
- refresh token ๋ฐ๊ธ
- refresh token rotation
- ์ธ์ ๋ชฉ๋ก ์กฐํ
- ํน์ ์ธ์ ์ข ๋ฃ
- ํ์ฌ ์ธ์ ๋ก๊ทธ์์
- ์ ์ฒด ๋ก๊ทธ์์
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
- ๊ด๋ฆฌ์์ฉ ์ฌ์ฉ์ ์กฐํ/์ ๊ธ/์ ๊ธํด์ /์ธ์ ์กฐํ/์ ์ฒด๋ก๊ทธ์์
- ๊ด๋ฆฌ์์ฉ MFA ์ด๊ธฐํ
- FCM ๋๋ฐ์ด์ค ํ ํฐ ๋ฑ๋ก/์กฐํ/๋นํ์ฑํ
- ๊ด๋ฆฌ์์ฉ FCM ํ ํฐ ์กฐํ ๋ฐ ํธ์ ๋ฐ์ก
- ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ ์ด๊ธฐ ๊ด๋ฆฌ์ ๊ณ์ ์๋ ์์ฑ
POST /api/v1/auth/signupPOST /api/v1/auth/loginPOST /api/v1/auth/refreshPOST /api/v1/auth/logoutPOST /api/v1/auth/logout-allGET /api/v1/auth/sessionsDELETE /api/v1/auth/sessions/{tokenId}POST /api/v1/auth/password/changePOST /api/v1/auth/password/reset/requestPOST /api/v1/auth/password/reset/confirmGET /api/v1/auth/fcm/tokensPOST /api/v1/auth/fcm/tokensDELETE /api/v1/auth/fcm/tokens/{id}GET /api/v1/auth/@me
GET /api/v1/auth/admin/usersGET /api/v1/auth/admin/users/{uuid}/sessionsPOST /api/v1/auth/admin/users/{uuid}/logout-allPOST /api/v1/auth/admin/users/{uuid}/lockPOST /api/v1/auth/admin/users/{uuid}/unlockPOST /api/v1/auth/admin/users/{uuid}/mfa/resetGET /api/v1/auth/admin/users/{uuid}/fcm/tokensPOST /api/v1/auth/admin/users/{uuid}/fcm/push
๊ด๋ฆฌ์ ๊ถํ์ SYSTEM_ADMIN ๊ถํ ์ฝ๋๋ก ํ๋ณํฉ๋๋ค. ๊ถํ์ tb_auth_permission, tb_auth_permission_grant ๊ธฐ๋ฐ์ผ๋ก ๊ด๋ฆฌํ๋ฉฐ, ์์ ๊ถํ์ ๋ถ์ฌํ๋ฉด ํ์ ๊ถํ์ด ํจ๊ป ์ ํจ ๊ถํ์ผ๋ก ๊ณ์ฐ๋ฉ๋๋ค.
๋ํ ํด๋์ค:
common/properties/app/admin/AdminPropertiesfunctions/auth/AuthAdminBootstrapfunctions/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_ENABLEDADMIN_USERNAMEADMIN_PASSWORDADMIN_NICKNAMEADMIN_EMAILADMIN_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 + saltfallback ์์
์ฆ ํ์ฌ๋ ๋น๋ฐ๋ฒํธ ๊ฒ์ฆ์ด ์์ ํ BCrypt only ์
๋๋ค.
- ํ๋ฌธ ์ ์ฅํ์ง ์์
- ํด์ ๊ธฐ๋ฐ ์ ์ฅ
tokenId,familyId๊ธฐ๋ฐ ์ธ์ /rotation ๊ด๋ฆฌ- ์ฌ์ฌ์ฉ ๊ฐ์ง ์ family revoke ๊ฐ๋ฅ
- ์ธ์ ์ ์ ํ ์ค์ ๊ฐ๋ฅ
- ์ธ์
๋ณ
issuedAt,lastUsedAt,userAgent,deviceLabel,ipAddress๊ด๋ฆฌ
๊ด๋ จ ์ค์ :
app.token.storageapp.token.max-sessions-per-user
๋ํ ํด๋์ค:
common/properties/app/auth/AuthSecurityPropertiesfunctions/auth/AuthServicefunctions/auth/MfaService
- ์คํจ ํ์ ๋์
- ์ ํ ํ์ ์ด๊ณผ ์ ๊ณ์ ์ ๊ธ
- ์ฑ๊ณต ๋ก๊ทธ์ธ ์ ์คํจ ํ์ ์ด๊ธฐํ
- ์ ๊ธ ์๊ฐ ์ค์ ๊ฐ๋ฅ
- ์คํจ/์ ๊ธ ์ํ๋
auth_login_history์ต์ ๊ธฐ๋ก์ ๊ธฐ์ค์ผ๋ก ๊ณ์ฐ
๊ด๋ จ ์ค์ :
app:
auth:
login-failure:
enabled: true
max-attempts: 5
lock-minutes: 15์ฑ๊ณต ๋ก๊ทธ์ธ ๊ธฐ๋ก์ AuthLoginHistoryEntity์ ์ ์ฅ๋ฉ๋๋ค.
lastLoginAtlastLoginIp
์ค์ ์ ์ฅ ์ปฌ๋ผ:
successfailureReasonfailedLoginCountlockedUntilclientIpuserAgent
์ด ํ๋ก์ ํธ๋ ์ด๋ฉ์ผ ๊ธฐ๋ฅ on/off์ ๋ฐ๋ผ ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ ์ฑ ์ด ๋ฌ๋ผ์ง๋๋ค.
app.email.enabled=true- ์ฌ์ค์ ์์ฒญ ์ ์ธ์ฆ ์ฝ๋๋ฅผ ๋ฐ๊ธํ๊ณ ์ด๋ฉ์ผ๋ก ์ ์กํฉ๋๋ค.
- ์ฌ์ค์ ์๋ฃ ์ ์ด๋ฉ์ผ ์ธ์ฆ ์ฝ๋๋ฅผ ๊ฒ์ฆํ ๋ค ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ์ฅํฉ๋๋ค.
app.email.enabled=false- ์ฌ์ค์ ์๋ฃ ์ ํ์ฌ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ฒ์ฆํ ๋ค ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ์ฅํฉ๋๋ค.
๊ด๋ จ API:
POST /api/v1/auth/password/reset/requestPOST /api/v1/auth/password/reset/confirm
๊ด๋ จ ์ค์ :
app:
auth:
password-reset:
code-expire-minutes: 10๋ํ ํด๋์ค:
entities/AuthVerificationEntityfunctions/auth/AuthVerificationServicecommon/enums/AuthVerificationType
์ด ๊ตฌ์กฐ๋ "์ด ์ธ์ฆ ๋ฐ์ดํฐ๊ฐ ์ด๋ค ์ก์ ์ ์ํ ๊ฒ์ธ์ง"๋ฅผ ํ์ ์ผ๋ก ๊ตฌ๋ถํด์ ์ ์ฅํฉ๋๋ค.
ํ์ฌ ์ฌ์ฉ ์ค์ธ ํ์ :
PASSWORD_RESETEMAIL_VERIFICATIONLOGIN_CHALLENGE
ํฅํ ํ์ฅ ์:
- ์ด๋ฉ์ผ ์ธ์ฆ
- ๋ก๊ทธ์ธ ์ฑ๋ฆฐ์ง
- ์ถ๊ฐ ๋ณธ์ธํ์ธ ๋จ๊ณ
์ง์ ๊ธฐ๋ฅ:
- 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-ipapp.privacy.include-user-agentapp.privacy.audit-retention-days
์ฐธ๊ณ :
- ํ์ฌ ๊ฐ์ฌ ์ด๋ฒคํธ๋ DB ์ ์ฌ ๋์ ๊ตฌ์กฐํ๋ ์ฝ์ ๋ก๊ทธ๋ก ๋จ๊น๋๋ค.
- ์ธ์ ํ๋์ฒ๋ผ ๋น๋๊ฐ ๋์ ๋ก๊ทธ๋ DB์ ์ ์ฅํ์ง ์์ต๋๋ค.
- ๊ด๋ฆฌ์ ๊ด๋ จ ์์
์
ADMIN EVENT๋ก๊ทธ๋ก ๊ตฌ๋ถํด ์ด์ ๋ก๊ทธ ์์ง๊ธฐ์์ ๋ถ๋ฆฌํ ์ ์๊ฒ ํฉ๋๋ค.
๋ํ ํด๋์ค:
common/properties/app/email/EmailPropertiescommon/configs/EmailConfigcommon/utils/email/EmailService
๊ธฐ๋ฅ:
- SMTP ๋ฉ์ผ ๋ฐ์ก
- ํ ์คํธ ๋ฉ์ผ ๋ฐ์ก
- HTML ๋ฉ์ผ ๋ฐ์ก
- ๋น๋ฐ๋ฒํธ ์ฌ์ค์ ์ธ์ฆ ์ฝ๋ ๋ฐ์ก
- on/off ์ค์ ์ง์
- on ์ธ๋ฐ ํ์ ์ค์ ์ด ๋น์ด ์์ผ๋ฉด ์์ ์ ๊ฐ์ ์ข ๋ฃ
ํ์ ์ค์ :
app.email.hostapp.email.portapp.email.usernameapp.email.passwordapp.email.from-address
์ฌ์ฉ ์์:
emailService.sendText("[email protected]", "Subject", "Body");
emailService.sendHtml("[email protected]", "Subject", "<b>Body</b>");๋ํ ํด๋์ค:
common/properties/app/firebase/FirebasePropertiescommon/configs/FirebaseConfigcommon/utils/firebase/FirebaseUtils
๊ธฐ๋ฅ:
- Firebase Admin SDK ์ด๊ธฐํ
- Firebase ID ํ ํฐ ๊ฒ์ฆ
- FCM push ์ ์ก
- ์ฌ์ฉ์๋ณ ๋๋ฐ์ด์ค ํ ํฐ ๊ธฐ๋ฐ ํธ์ ๋ฐ์ก ์ง์
- on/off ์ค์ ์ง์
- on ์ธ๋ฐ ํ์ ์ค์ ์ด ๋น์ด ์์ผ๋ฉด ์์ ์ ๊ฐ์ ์ข ๋ฃ
ํ์ ์ค์ :
app.firebase.project-idapp.firebase.credentials-path๋๋app.firebase.credentials-json
์ฌ์ฉ ์์:
firebaseUtils.verifyIdToken(idToken);
firebaseUtils.sendNotification(targetToken, "title", "body", Map.of("type", "notice"));๋ํ ํด๋์ค:
entities/FcmDeviceTokenEntityfunctions/auth/FcmDeviceTokenServicerepositories/auth/FcmDeviceTokenRepository
๊ธฐ๋ฅ:
- ์ฌ์ฉ์ ๋๋ฐ์ด์ค ํ ํฐ ๋ฑ๋ก
- ๋ด ํ ํฐ ๋ชฉ๋ก ์กฐํ
- ํ ํฐ ๋นํ์ฑํ
- ๊ด๋ฆฌ์์ฉ ์ฌ์ฉ์ ํ ํฐ ์กฐํ
- ๊ด๋ฆฌ์์ฉ ์ฌ์ฉ์ ๋๋ฐ์ด์ค ํธ์ ๋ฐ์ก
์ ์ฅ ์ ๋ณด:
deviceTokendeviceLabeluserAgentlastIpAddresslastUsedAtenabled
๋ํ ํด๋์ค:
common/configs/WebMvcConfigcommon/interceptors/TokenInterceptorcommon/interceptors/ApiVersionInterceptorcommon/controller/ViewControllercommon/controller/CommonController
@ApiVersion(1) ์ด ๋ถ์ ์ปจํธ๋กค๋ฌ๋ ์๋์ผ๋ก /api/v1 prefix๊ฐ ๋ถ์ต๋๋ค.
์:
@RestController
@ApiVersion(1)
@RequestMapping("/sample")
public class SampleController {
}์ค์ ๊ฒฝ๋ก:
/api/v1/sample
TokenInterceptor๋ ๋ค์์ ์ฒ๋ฆฌํฉ๋๋ค.
Authorizationํค๋ ์กด์ฌ ์ฌ๋ถ ํ์ธBearerprefix ํ์ธ- JWT์์ ์ฌ์ฉ์ ์กฐํ
- ์์ฒญ attribute์ ์ฌ์ฉ์ ์ฃผ์
ViewController๋ /api/** ๊ฐ ์๋ ํ์ฅ์ ์๋ ์์ฒญ์ ๋ชจ๋ index.html๋ก forwardํฉ๋๋ค.
์ฆ React/Vue SPA ๋ผ์ฐํ ์ ์๋ฒ ๋จ์์๋ ์ง์ํฉ๋๋ค.
CommonController๋ ์์คํ
enum ๋ชฉ๋ก์ JSON์ผ๋ก ๋
ธ์ถํฉ๋๋ค.
์:
GET /api/common/enums
๋ํ ํด๋์ค:
common/configs/WebSecurityConfigcommon/handler/XssFiltercommon/handler/XssRequestWrappercommon/handler/XssRequestBodyAdvicecommon/handler/LoggingFiltercommon/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๊ธฐ๋ฅ:
- 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 ํจํด ๊ธฐ๋ฐ ๋ง์คํน
๊ธฐ๋ณธ ๋ฏผ๊ฐ ํค ์์:
passwordnewPasswordaccessTokenrefreshTokenauthorizationsecretapiKeyotpcookie
๋ํ ํด๋์ค:
common/utils/responses/BasicResponsecommon/utils/responses/PageResponsecommon/handler/GlobalExceptionHandlercommon/exception/ErrorCodecommon/exception/BusinessException
๊ธฐ๋ฅ:
- ์ฑ๊ณต/์คํจ ์๋ต ํ์ ํต์ผ
- ์๋ฌ ์ฝ๋ ํ์คํ
- validation field error ์๋ต
- path, timestamp, correlationId ํฌํจ
- ๋ฉ์์ง ๊ตญ์ ํ ์ฐ๋
์ธ์ฆ ๊ด๋ จ ์๋ต์ JPA ์ํฐํฐ๋ฅผ ๊ทธ๋๋ก ๋ฐํํ์ง ์๊ณ ์์ ํ DTO๋ก ์ ํํฉ๋๋ค.
- ํ์๊ฐ์
์๋ต:
AuthProfileDto - ๋ด ์ ๋ณด ์กฐํ ์๋ต:
AuthProfileDto
์ฆ ์๋ ๋ด๋ถ ์ธ์ฆ ํ๋๋ ์ธ๋ถ ์๋ต์์ ์ง์ ๋ ธ์ถํ์ง ์์ต๋๋ค.
password- refresh token ์๋ฌธ
- MFA secret
- ์ธ์ฆ ๊ฒ์ฆ hash
- ๋ด๋ถ ๋ก๊ทธ์ธ ์คํจ/์ ๊ธ ์ํ
๋ํ ํด๋์ค:
common/configs/CacheConfigcommon/properties/app/cache/CachePolicyPropertiescommon/properties/app/redis/RedisProperties
๊ธฐ๋ฅ:
- Redis ๊ธฐ๋ฐ ์บ์
- Redis ๋นํ์ฑ ์ ๋ก์ปฌ ์บ์ fallback
- cache name ์ ์ฑ
- TTL ์ค์
- cache prefix ์ค์
๋ํ ์บ์ ์ฌ์ฉ์ฒ:
- ์ฌ์ฉ์ ์กฐํ ์บ์
๋ํ ํด๋์ค:
common/properties/app/database/DatabasePropertiescommon/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๋
baseschema๋ฅผ ๋ด ๋๋ค. - Flyway
flyway_schema_history๋publicschema์ ๋ก๋๋ค. - Flyway migration SQL์
${appSchema}placeholder๋ฅผ ํตํดbaseschema์ ํ ์ด๋ธ์ ์์ฑ/๋ณ๊ฒฝํฉ๋๋ค.
๋ํ ํด๋์ค:
common/configs/RestTemplateConfigcommon/configs/RetryConfigcommon/handler/OutboundRequestInterceptorcommon/handler/RestTemplateLoggingInterceptorcommon/utils/RestServicecommon/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/StorageConfigcommon/utils/storage/StorageServicecommon/utils/storage/LocalStorageServicecommon/utils/storage/S3StorageServicecommon/utils/storage/FileUploadSecurityServicecommon/utils/storage/entity/FileEntitycommon/utils/storage/repository/FileRepositorycommon/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/filesDELETE /api/v1/files/{id}GET /api/v1/files/{id}/download-urlGET /api/v1/files/content/**
๋ํ ํด๋์ค:
common/configs/AsyncConfigcommon/configs/SchedulingConfigcommon/handler/MdcTaskDecoratorcommon/annotation/DistributedLockcommon/handler/DistributedLockAspect
๊ธฐ๋ฅ:
- ๋น๋๊ธฐ task executor
- MDC ์ ํ
- ์ค์ผ์ค ์์
- ๋ถ์ฐ ๋ฝ ๊ธฐ๋ฐ ์๊ณ์์ญ ๋ณดํธ
๋ํ ํด๋์ค:
common/configs/SwaggerConfigcommon/handler/BasePlatformHealthIndicator
๊ธฐ๋ฅ:
- Swagger UI
- OpenAPI ๋ฌธ์
- health / metrics / prometheus
- base ํ๋ซํผ health indicator
Swagger ๊ฒฝ๋ก:
/api/swagger
์ด ํ๋ก์ ํธ๋ 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());ํด๋์ค:
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/AesComponentcommon/convert/EncryptedStringConvertercommon/utils/crypto/Base64Componentcommon/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]");ํด๋์ค:
common/utils/QR/QRCodecommon/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/ExcelUtilscommon/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.htmlscript.jsstyle.css
๊ธฐ๋ณธ SPA ์ง์ ํ๋ฉด/์ํ ๋ฆฌ์์ค๋ก ์ฌ์ฉํ ์ ์์ต๋๋ค.
messages.properties
- ํ๋กํ ์ค์ ํ์ผ์
app.i18n.enabled๋ก ๋ค๊ตญ์ด ๊ธฐ๋ฅ์ ์ผ๊ณ ๋ ์ ์์ต๋๋ค. app.i18n.default-locale๋ก ์๋ฒ ๊ธฐ๋ณธ ์ธ์ด๋ฅผ ์ ํํฉ๋๋ค. ์:ko,enapp.i18n.supported-locales์ ์๋ ์ธ์ด๊ฐ ๋ค์ด์ค๋ฉด ๊ธฐ๋ณธ ์ธ์ด๋ก ์๋ fallback ๋ฉ๋๋ค.enabled=false์ด๋ฉดAccept-Languageํค๋๋ฅผ ๋ฌด์ํ๊ณ ํญ์ ๊ธฐ๋ณธ ์ธ์ด๋ง ์ฌ์ฉํฉ๋๋ค.- ๋ฉ์์ง ๋ฒ๋ค์ ์๋ ํ์ผ์ ์ฌ์ฉํฉ๋๋ค.
src/main/resources/messages.propertiessrc/main/resources/messages_ko.propertiessrc/main/resources/messages_en.properties
- ๋น์ฆ๋์ค ์์ธ์ ๊ฒ์ฆ ๋ฉ์์ง๋ ๊ฐ์ message source๋ฅผ ์ฌ์ฉํฉ๋๋ค.
- ๋ค๊ตญ์ด/๋ฉ์์ง ์ฝ๋ ๋ฆฌ์์ค
logback-spring.xml- ๋ก๊น ์ค์
src/main/resources/db/migration/V1__base_security_extensions.sql
๋ณด์/์ธ์ฆ ๊ด๋ จ ํ์ฅ ์คํค๋ง๋ฅผ Flyway๋ก ๊ด๋ฆฌํฉ๋๋ค.
ํ์ฌ ํฌํจ๋ ํ ์คํธ:
BaseApplicationTestsAuthVerificationServiceTestAuthSecurityPolicyTestAuthPermissionServiceTestAuthServiceRdbTestFcmDeviceTokenServiceTestMfaServiceTest
๊ฒ์ฆ ๋ช ๋ น:
./gradlew compileJava
./gradlew testSPRING_PROFILES_ACTIVE=local ./gradlew bootRunSPRING_PROFILES_ACTIVE=env \
JWT_SECRET_KEY=... \
AES_SECRET_KEY=... \
./gradlew bootRunhttp://localhost:8080/api/swagger
- ์ฌ์ฉ์ ์ธ์ฆ์ด ํ์ํ API๋
@ApiVersion+TokenInterceptorํ๋ฆ์ ๋ฐ๋ฆ ๋๋ค. - ๋ฏผ๊ฐ์ ๋ณด๋ ๋ก๊ทธ์ ๋จ๊ธฐ์ง ๋ง๊ณ
LoggingFilter๋ง์คํน ๊ท์น์ ๋ฐ๋ฆ ๋๋ค. - ์ ์ฅํ ๋ฏผ๊ฐ์ ๋ณด๋
EncryptedStringConverter๊ฐ์ ์ํธํ ๊ฒฝ๋ก๋ฅผ ์ฐ์ ๊ฒํ ํฉ๋๋ค. - HTML ํ์ฉ ํ๋๋ ๊ธฐ๋ณธ sanitize๋ฅผ ์ ์งํ ์ฑ
@AllowHtml๋ก ์์ธ ์ฒ๋ฆฌํฉ๋๋ค. - ์ธ๋ถ API ํธ์ถ์ ์ง์
RestTemplate๋ณด๋คRestService๋ฅผ ์ฐ์ ์ฌ์ฉํฉ๋๋ค. - ํ์ผ ์
๋ก๋๋ ๋ฐ๋์
StorageService๋ฅผ ํตํด ์ฒ๋ฆฌํฉ๋๋ค.
docs/BASE_TEMPLATE_CHECKLIST.mddocs/API_DEPRECATION_POLICY.md
์ด ๋ฌธ์๋ค๊ณผ ํจ๊ป ๋ณด๋ฉด ๋ฒ ์ด์ค ํ๋ก์ ํธ๋ฅผ ํ ํ์ค์ผ๋ก ๊ฐ์ ธ๊ฐ๊ธฐ๊ฐ ๋ ์ฝ์ต๋๋ค.