๋ง์ง๋ง ์๊ฐ์ ์จ๊ธฐ๋ฅผ ์ ํ๋ค - ์ ํํฐ๋ ธํธ๋ ์ฌํ์ ์ ๋ฌ๋๋ ํธ์ง ์๋น์ค์ ๋๋ค.
AfterNote ํ๋ก์ ํธ์ ๋ฐฑ์๋ ์๋ฒ์ ๋๋ค.
- ๊ธฐ์ ์คํ
- ์์คํ ์ํคํ ์ฒ
- ERD (Entity Relationship Diagram)
- ํ๋ก์ ํธ ๊ตฌ์กฐ
- ์์ํ๊ธฐ
- Backend: Java 17, Spring Boot 3.2.1
- ORM: Spring Data JPA, Hibernate 6.4.1
- Database: MySQL 8.4
- Authentication: JWT, Spring Security
- External APIs: Google Gemini, AWS S3
- Encryption: ChaCha20-Poly1305 (๋ชจ๋ฐ์ผ ์ต์ ํ)
- Build Tool: Gradle 8.12
- Containerization: Docker, Docker Compose
- Testing: JUnit 5, Mockito
| ์ปดํฌ๋ํธ | ์ ํ ์ด์ |
|---|---|
| VPC (Virtual Private Cloud) | ๋ณด์์ด ํ์ํ ๊ธ์ต/๊ฐ์ธ ์ ๋ณด๋ฅผ ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์ ๊ฒฉ๋ฆฌ๋ ๋คํธ์ํฌ ํ์, AWS ์์ ๋ก์ด ์ค์ ์ ๊ณต |
| NginX (๋ฆฌ๋ฒ์ค ํ๋ก์) | ํด๋ผ์ด์ธํธ ์์ฒญ์ Spring ์๋ฒ๋ก ๋ผ์ฐํ , SSL ์ธ์ฆ์ ๊ด๋ฆฌ(Certbot), ์์ฒญ ๋ถ์ฐ ์ฒ๋ฆฌ |
| Public Subnet | ์ธ๋ถ์์ ์ ๊ทผ ๊ฐ๋ฅํ ๋คํธ์ํฌ ์์ญ, ๋ชจ๋ฐ์ผ ํด๋ผ์ด์ธํธ์ HTTPS ํต์ ์ง์ |
| Spring Boot (EC2) | ์์ ์ ์ธ Java ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ , ๋น์ฆ๋์ค ๋ก์ง ์ฒ๋ฆฌ, ๋ค์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํ๊ณ |
| MySQL 8.4 | ACID ์ค์๋ก ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๋ณด์ฅ, ๋ณต์กํ ์ฟผ๋ฆฌ์ JOIN ์ฐ์ฐ ์ง์, ํ์๋ ํฐ/๊ฐ์ ๋ฐ์ดํฐ์ ์ ๋ขฐ์ฑ ํ์ |
| Redis | ํ ํฐ(JWT Refresh) ๋น ๋ฅธ ์กฐํ/๋ง๋ฃ ๊ด๋ฆฌ (TTL ์ค์ ), Gemini API ๊ฐ์ ๋ถ์ ๊ฒฐ๊ณผ ์บ์ฑ (1์ผ), ์ธ์ ๊ด๋ฆฌ |
| AWS S3 | ๋์ฉ๋ ๋ฏธ๋์ด ํ์ผ(์ฌ์ง, ์์ ) ์ ์ฅ์๋ก EC2 ์คํ ๋ฆฌ์ง ์๋ฐ ํด์, Presigned URL๋ก ๋ณด์์ฑ ๊ฐํ |
| ChaCha20-Poly1305 | ๋ชจ๋ฐ์ผ ํ๊ฒฝ ์ต์ ํ + ๋ณด์: SNS ๊ณ์ ์ ๋ณด ์ํธํ, ๊ฒฝ๋์ด๋ฉด์๋ ๋์ ๋ณด์์ฑ ์ ๊ณต |
| ๋ณ๊ฒฝ ์ฌํญ | ์ ํ ์ด์ |
|---|---|
| RDS (Managed Database) | EC2์์ ์ง์ DB ๊ด๋ฆฌ โ AWS ์๋ ๋ฐฑ์ /์ ๋ฐ์ดํธ/๋ณต๊ตฌ๋ก ๋ฐ์ดํฐ ์ ์ค ์ํ ์ ๊ฑฐ, ์ ์ธ์ฅ ๋ฐ์ดํฐ ์์ ์ฑ ํ๋ณด |
| ALB (Application Load Balancer) | ๊ณต๊ฐ๋๋ ๊ฒ์ ALB IP๋ง์ผ๋ก EC2 ์ค์ IP ์๋ โ ํด์ปค ์ง์ ๊ณต๊ฒฉ ์ด๋ ค์, DDoS ๊ณต๊ฒฉ ๋ฐฉ์ด |
| ALB๋ฅผ ํตํ SSL ๊ด๋ฆฌ | ์ธ์ฆ์ ๊ด๋ฆฌ๋ฅผ ALB ํ ๊ณณ์๋ง ์ง์ค โ ๋ง๋ฃ๋ก ์ธํ ๋ณด์ ๊ตฌ๋ฉ ์ํ ๊ฐ์ |
| WAF (Web Application Firewall) ์ฐ๋ | ALB ์๋จ์ WAF ์ ์ฉ ๊ฐ๋ฅ โ SQL Injection, ์ ์์ ๋ด ๊ณต๊ฒฉ ์ฐจ๋จ |
| ์ํ ํ์ฅ ์ค๋น | ALB๋ก ์ฌ๋ฌ EC2 ์ธ์คํด์ค์ ํธ๋ํฝ ๋ถ์ฐ ๊ฐ๋ฅ โ ์ถํ ํธ๋ํฝ ์ฆ๊ฐ ์ ํ์ฅ์ฑ ํ๋ณด |
| ํ ์ด๋ธ | ์ฃผ์ ํ๋ | ์ค๋ช |
|---|---|---|
| USERS | id, email, password, name, provider | ์ฌ์ฉ์ ๊ณ์ ์ ๋ณด |
| RECEIVER | id, name, phone, email, relationship | ์์ ์ ์ ๋ณด |
| AFTERNOON_NOTE | id, category, title, leave_message | ์ ํํฐ๋ ธํธ ๋ฉ์ธ |
| AFTERNOON_SECURE_CONTENT | id, key_name, encrypted_value | ์ํธํ๋ SNS ๊ณ์ ์ ๋ณด |
| AFTERNOON_RECEIVER | id, afternote_id, receiver_id | ์ ํํฐ๋ ธํธ-์์ ์ N:N |
| AFTERNOON_NOTE_PLAYLIST | id, atmosphere, memorial_photo_url | ํ๋ ์ด๋ฆฌ์คํธ ์ ๋ณด |
| AFTERNOON_PLAYLIST_ITEM | id, playlist_id, song_id | ํ๋ ์ด๋ฆฌ์คํธ ๋ด ๊ณก |
| TIMELETTER | id, title, content, send_at, status | ํ์๋ ํฐ ๋ฉ์ธ |
| TIMELETTER_MEDIA | id, s3_key | ํ์๋ ํฐ ๋ฏธ๋์ด |
| TIMELETTER_RECEIVER | id, timeletter_id, receiver_id, delivered_at | ํ์๋ ํฐ-์์ ์ N:N |
| MIND_RECORD | id, content, song_id | ๋ง์ธ๋๋ ์ฝ๋ |
| EMOTION | id, keyword | ๊ฐ์ ํค์๋ |
| DEEP_THOUGHT | id, content, category | ๊น์ ์๊ฐ |
| SONG | id, title, artist, cover_url | ๊ณก ์ ๋ณด |
| USER_RECEIVER | id, user_id, receiver_id | ์ฌ์ฉ์-์์ ์ N:N |
1:1 ๊ด๊ณ
- AFTERNOTE โ AFTERNOON_NOTE_PLAYLIST (1:1)
1:N ๊ด๊ณ
- USERS โ AFTERNOON_NOTE (1:N)
- USERS โ TIMELETTER (1:N)
- USERS โ MIND_RECORD (1:N)
- AFTERNOON_NOTE โ AFTERNOON_SECURE_CONTENT (1:N)
- AFTERNOON_NOTE โ AFTERNOON_PLAYLIST_ITEM (1:N)
- TIMELETTER โ TIMELETTER_MEDIA (1:N)
N:N ๊ด๊ณ (์ค๊ฐ ํ ์ด๋ธ ์ฌ์ฉ)
- AFTERNOON_NOTE โ RECEIVER (via AFTERNOON_RECEIVER)
- TIMELETTER โ RECEIVER (via TIMELETTER_RECEIVER)
- USERS โ RECEIVER (via USER_RECEIVER)
- AFTERNOON_NOTE_PLAYLIST โ SONG (via AFTERNOON_PLAYLIST_ITEM)
1๏ธโฃ ์ฌ์ฉ์ ๊ฐ์ USERS ํ ์ด๋ธ์ ์ ์ฅ RECEIVER ๋ฑ๋ก ๊ฐ๋ฅ
2๏ธโฃ ์ ํํฐ๋ ธํธ ์์ฑ AFTERNOON_NOTE ์์ฑ โโ SOCIAL: SECURE_CONTENT์ ์ํธํ๋ ๊ณ์ ์ ๋ณด โโ GALLERY: ๊ธฐ๋ณธ ์ ๋ณด โโ PLAYLIST: PLAYLIST + PLAYLIST_ITEM ์์ฑ
3๏ธโฃ ํ์๋ ํฐ ์์ฑ TIMELETTER ์์ฑ โโ TIMELETTER_MEDIA ์ ์ฅ โโ TIMELETTER_RECEIVER๋ก ์์ ์ ๋ฑ๋ก
4๏ธโฃ ๋ง์ธ๋๋ ์ฝ๋ ๊ธฐ๋ก MIND_RECORD ์์ฑ โโ EMOTION ๊ฐ์ ํค์๋ ์ ์ฅ โโ SONG ์ฐธ์กฐ (์ ํ์ฌํญ)
| ๋ชจ๋ | ์ค๋ช | ์ฃผ์ ํด๋์ค |
|---|---|---|
| auth/ | ํ์๊ฐ์ , ๋ก๊ทธ์ธ, ์์ ๋ก๊ทธ์ธ | AuthController, AuthService, SocialLoginFactory |
| user/ | ์ฌ์ฉ์ ๊ด๋ฆฌ (ํ๋กํ, ์ค์ , ํํด) | UserController, UserService, UserRepository |
| afternote/ | ์ ํํฐ๋ ธํธ (SOCIAL/GALLERY/PLAYLIST) | AfternoteController, AfternoteService, AfternoteValidator |
| timeletter/ | ํ์๋ ํฐ (์ฌํ ๋ฐ์ก ํธ์ง) | TimeLetterController, TimeLetterService |
| receiver/ | ์์ ์ ๊ด๋ฆฌ | ReceiverController, ReceiverService |
| mindrecord/ | ๋ง์ธ๋๋ ์ฝ๋ & ๊ฐ์ ๋ถ์ | MindRecordService, EmotionService, GeminiService |
| image/ | ์ด๋ฏธ์ง/ํ์ผ ์ฒ๋ฆฌ (S3) | ImageController, S3Service |
| ๋ชจ๋ | ์ค๋ช |
|---|---|
| jwt/ | JWT ํ ํฐ ์์ฑ/๊ฒ์ฆ (JwtTokenProvider, JwtAuthenticationFilter) |
| util/ | ์ ํธ ํด๋์ค (ChaChaEncryptionUtil: ChaCha20-Poly1305) |
| service/ | ๊ธ๋ก๋ฒ ์๋น์ค (GeminiService, EmailService, TokenService) |
| config/ | Spring ์ค์ (SecurityConfig, RestTemplateConfig, CacheConfig) |
| resolver/ | Custom Resolver (@UserId ์ด๋ ธํ ์ด์ ์ฒ๋ฆฌ) |
| exception/ | ์์ธ ์ฒ๋ฆฌ (CustomException, ErrorCode: 40+ ์๋ฌ ์ฝ๋) |
| common/ | ๊ณตํต ์๋ต ํฌ๋งท (ApiResponse) |
Strategy Pattern (์์ ๋ก๊ทธ์ธ)
auth/service/social/ โโโ SocialLoginService (Interface) โโโ KakaoLoginService (๊ตฌํ) โโโ GoogleLoginService (๊ตฌํ) โโโ NaverLoginService (๊ตฌํ)
โ SocialLoginFactory๊ฐ ๋ฐํ์์ ์ฌ๋ฐ๋ฅธ ๊ตฌํ์ฒด ์ ํ
Validator Pattern (๋ฐ์ดํฐ ๊ฒ์ฆ)
afternote/service/ โโโ AfternoteValidator - ์นดํ ๊ณ ๋ฆฌ๋ณ ํ๋ ๊ฒ์ฆ โโโ AfternoteService - ๋น์ฆ๋์ค ๋ก์ง โโโ AfternoteRelationService - ๊ด๊ณ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
Repository Pattern (๋ฐ์ดํฐ ์ ๊ทผ)
- Spring Data JPA๋ฅผ ์ฌ์ฉํ ์๋ ์ฟผ๋ฆฌ ์์ฑ
- JpaRepository ์์์ผ๋ก ๊ธฐ๋ณธ CRUD ์ ๊ณต
- DB/Redis ๋ก์ปฌ ์ธํ๋ผ๋ฅผ ๋จผ์ ์คํํฉ๋๋ค.
docker compose -f docker-compose.local.yml up -d- IntelliJ Run Configuration์ Environment variables์ ์๋ ๊ฐ์ ๋ฃ์ต๋๋ค.
SPRING_PROFILES_ACTIVE=local
DB_HOST=localhost
DB_PORT=3306
DB_NAME=afternote-db
DB_USERNAME=admin
DB_PASSWORD=password
REDIS_HOST=localhost
REDIS_PORT=6379
JWT_SECRET=...
CHACHA20_SECRET_KEY=...
SERVER_PORT=8080
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
ADMIN_EMAIL=your-admin@example.com
ADMIN_PASSWORD=your-secure-password
APP_ANDROID_STORE_URL=https://play.google.com/store/apps/details?id=your.package- IntelliJ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ง์ ์คํํฉ๋๋ค.
- ๋ก์ปฌ ํ๋กํ์ผ:
local - Swagger UI:
http://localhost:8080/swagger-ui.html
์ด์ EC2์ ~/deploy/.env ๋ ๋ ํฌ์ deploy/production.env(๋ก์ปฌ, gitignore) ๋ฅผ
GitHub Secret DEPLOY_PRODUCTION_ENV ๋ก ์ฌ๋ฆฐ ๋ค, main Deploy ์ํฌํ๋ก๊ฐ ๋ฎ์ด์ด๋ค.
cp deploy/production.env.example deploy/production.env # ๊ฐ ์ฑ์ฐ๊ธฐ (Sentry DSN ํฌํจ)
./scripts/push-deploy-env-secret.sh # Secret ๊ฐฑ์
# ์ดํ main push โ EC2 ~/deploy/.env ๋๊ธฐํ + ์ปจํ
์ด๋ ์ฌ๊ธฐ๋Afternote-BE๋ public ๋ ํฌ๋ผ DB/JWT ๋ฑ ์ํฌ๋ฆฟ์ git์ ์ปค๋ฐํ์ง ์๋๋ค.
๋ก์ปฌ์์deploy/production.env๋ง ์์ ํ๊ณ , ๋ฐฐํฌ ์ ์ ์ ์คํฌ๋ฆฝํธ๋ก Secret์ ๊ฐฑ์ ํ๋ฉด ๋๋ค.
๋ ๊ฑฐ์ ๋ก์ปฌ compose์ฉ:
cp .env.production.example .env.production
docker compose --env-file .env.production up -dVPC/EC2/RDS/EIP์ ๊ฐ๋ฐ off-hours(03:00~12:00 KST) Stop/Start๋ฅผ Terraform์ผ๋ก ์์ฑํฉ๋๋ค.
์ค์ ยท์ปท์ค๋ฒยท๊ตฌ ๋ฆฌ์์ค ํ๊ธฐ: infra/terraform/README.md
cd infra/terraform
cp terraform.tfvars.example terraform.tfvars # key pair, ssh CIDR, db_password, alert_email
terraform init && terraform apply
terraform output elastic_ip # DNS A + GitHub EC2_HOST
terraform output jdbc_url_hint # .env.production DB_URL๋ก์ปฌ ์ธํ๋ผ(DB/Redis) ์คํ:
docker compose -f docker-compose.local.yml up -d๋ก์ปฌ ์ฑ ์คํ:
IntelliJ Run๋ก์ปฌ ์ธํ๋ผ ์ค์ง:
docker compose -f docker-compose.local.yml down๋ฐฐํฌ ์คํ ์คํ:
docker compose --env-file .env.production up -dํ ์คํธ ์คํ:
./gradlew test์ด์ ๋ก๊ทธ ํ์ธ:
docker compose --env-file .env.production logs -f afternote-server- โ JWT ๊ธฐ๋ฐ ์ธ์ฆ
- โ ChaCha20-Poly1305 ์ํธํ (SNS ๊ณ์ ์ ๋ณด)
- โ ์์ ๋ก๊ทธ์ธ (์นด์นด์ค, ๊ตฌ๊ธ)
- โ ์์ฒด ์ธ์ฆ ์ฐ๋ (์ค๋น ์ค)
- โ SOCIAL: SNS ๊ณ์ ๊ด๋ฆฌ
- โ GALLERY: ์ฌ์ง์ฒฉ ๊ด๋ฆฌ
- โ PLAYLIST: ์์ ํ๋ ์ด๋ฆฌ์คํธ
- โ ์ฌํ ์๋ ๋ฐ์ก
- โ ์ค์ผ์ค ๊ด๋ฆฌ
- โ ๋ค์ค ์์ ์ ์ง์
- โ Gemini AI๋ฅผ ํตํ ๊ฐ์ ์์ฝ
- โ 7์ผ ๊ฐ์ ํต๊ณ
- โ Redis ์บ์ฑ
์ฅ์ ๋ฉ์ผ/์ด์ ์งํ๊ฐ ๋ณด์ด๋ฉด ์ด ์์๋ก ๋ณธ๋ค.
- CloudWatch ๋์๋ณด๋ โ EC2 / RDS / Lambda ์ค ์ด๋๊ฐ ์ด์ํ์ง
https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-2#dashboards:name=afternote-ops - Sentry Issues โ ์์ธ ์คํยทURLยทrelease
https://sentry.io โ ํ๋ก์ ํธjava-spring-bootโ Issues โ Feed - ์ฑ ๋ก๊ทธ โ ์ด์ ์งํ๊ฐ ๋ฐ์ํ ์๊ฐ ์ ํ์ stdout https://ap-northeast-2.console.aws.amazon.com/cloudwatch/home?region=ap-northeast-2#logsV2:log-groups/log-group/%2Fafternote%2Fapp
- ํฌ์ค โ ์ฑยทDBยทRedis ๋ถ๋ฆฌ
GET https://afternote.kro.kr/actuator/health
db/redis์ค DOWN์ด๋ฉด ๊ทธ์ชฝ๋ถํฐ ๋ณธ๋ค. (nginx-health๋ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ง ์ด์ ์๋์ง)
์๋ ๋ฉ์ผ: Terraform alert_email โ SNS afternote-scheduler-alerts
env ๋ณ๊ฒฝ: deploy/production.env ์์ โ ./scripts/push-deploy-env-secret.sh โ main ๋ฐฐํฌ
- Swagger UI (local ํ๋กํ์ผ): http://localhost:8080/swagger-ui.html
- ๋ชจ๋ API ์๋ํฌ์ธํธ๋ JWT ์ธ์ฆ ๋๋ OAuth ํ ํฐ ํ์
- ์ด๋ฉ์ผ ์๋ ์ ๊ท ๊ฐ์
์ #290 ์ ์ฑ
ํ์ ์ ๊น์ง ์ง์ํ์ง ์์ต๋๋ค. ์ด๋ฉ์ผ์ด ์๊ฑฐ๋ ๊ฒ์ฆ๋์ง ์์ ์นด์นด์ค ๊ณ์ ์ ์ ๊ท ๊ฐ์
/์ด๋ฉ์ผ ๊ธฐ๋ฐ ์ฐ๊ฒฐ ์
400 / 1222 SOCIAL_EMAIL_REQUIRED๋ก ์๋ตํฉ๋๋ค. ์นด์นด์ค ์ด๋ฉ์ผ ์ ๊ณต ๋์ ํ ์ฌ์๋ํด์ผ ํฉ๋๋ค. - ์ด๋ฏธ ์ฐ๊ฒฐ๋ ์์
๊ณ์ ์
provider + providerId๋ก ์ฐ์ ๋ก๊ทธ์ธํ๋ฏ๋ก ์นด์นด์ค ์ด๋ฉ์ผ์ด ๋ณ๊ฒฝ๋๊ฑฐ๋ ์ ๊ณต๋์ง ์์๋ ๊ธฐ์กด ์ฌ์ฉ์๋ก ๋ก๊ทธ์ธํฉ๋๋ค. - ์ด๋ฉ์ผ๋ก ์ฐพ์ ๊ธฐ์กด ์ฌ์ฉ์์ ๋ก๊ทธ์ธ ์๋จ์ ๋ณด์กดํ๊ณ ํด๋น ์์
์ ๊ณต์์ ID๋ฅผ ์ถ๊ฐํฉ๋๋ค. ๊ธฐ์กด ๋์ผ ์ ๊ณต์์ ๋ค๋ฅธ ID๊ฐ ์ฐ๊ฒฐ๋์ด ์์ผ๋ฉด
409 / 1212๋ก ๊ฑฐ์ ํฉ๋๋ค. ์ด์ ๋ฐ์ดํฐ์ ๋น providerId๋ ์ฒซ ์ ์ ๋ก๊ทธ์ธ ๋ ์ฑ์๋๋ค. - ์นด์นด์ค์ ์ ํจยท๊ฒ์ฆ ์ด๋ฉ์ผ ๋ฐ ๊ตฌ๊ธ์ ๊ฒ์ฆ ์ด๋ฉ์ผ๋ง ์ด๋ฉ์ผ ๊ธฐ๋ฐ ๊ฐ์ /์ฐ๊ฒฐ์ ์ฌ์ฉํฉ๋๋ค.


