Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ work/
outputs/

/.env
babi-backup-before-schema-restore.sql
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-mail'

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
5 changes: 4 additions & 1 deletion compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ services:
MYSQL_PASSWORD: ${MYSQL_PASSWORD?required}
volumes:
- babi_mysql_data:/var/lib/mysql

redis:
image: redis:7.4-alpine
ports:
- "127.0.0.1:6379:6379"
volumes:
babi_mysql_data:
100 changes: 100 additions & 0 deletions docs/2026-10-25-email-signup-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# ์ด๋ฉ”์ผ ์ธ์ฆ๊ณผ ํšŒ์›๊ฐ€์ž… ์—ฐ๋™ ๊ฒฐ๊ณผ

> ์ด ๋ฌธ์„œ๋Š” verificationToken ๋„์ž… ์ „ ๊ตฌํ˜„๊ณผ ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋ก์ž…๋‹ˆ๋‹ค. ์ด๋ฉ”์ผ ์ „์—ญ ๊ฐ’ `1`์— ๊ธฐ๋ฐ˜ํ•œ ์•„๋ž˜ ๊ฐ€์ž… ๊ณ„์•ฝ์€ ํ๊ธฐ๋์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์„œ๋น„์Šค๋Š” ์ผํšŒ์„ฑ ํ† ํฐ์„ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•˜๋ฉฐ, ์ตœ์‹  ์š”์ฒญยท์‘๋‹ต ๋ฐ Redis ์ฒ˜๋ฆฌ ๋ฐฉ์‹์€ [์ด๋ฉ”์ผ ์ธ์ฆ ํ† ํฐ API ๊ณ„์•ฝ](api/email-verification.md)์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. ๊ณผ๊ฑฐ์˜ 165๊ฐœ ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฅผ ํ˜„์žฌ ํ† ํฐ ๋ณด์•ˆ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋กœ ํ•ด์„ํ•˜์ง€ ๋งˆ์„ธ์š”.

2026-10-25 KST. ๋Œ€์ƒ: `/Users/dgsw49/Desktop/Development/BABI-server`, ๋ธŒ๋žœ์น˜ `feature/email-verification`.

## ํ™•์ธ ๋ฐ ๋ณ€๊ฒฝ ๋ฒ”์œ„

ํ˜„์žฌ ์ฒดํฌ์•„์›ƒ๊ณผ ์ƒ์œ„ ๊ฒฝ๋กœ์—๋Š” AGENTS.md/CLAUDE.md๊ฐ€ ์—†์—ˆ๋‹ค. ๋™์ผ ์ €์žฅ์†Œ์˜ ๋‹ค๋ฅธ ๋กœ์ปฌ ์ฒดํฌ์•„์›ƒ์— ์žˆ๋Š” SELECT/BABI AGENTS.md์™€ CLAUDE.md๋ฅผ ์ฐธ๊ณ ํ•ด ๊ธฐ์กด ๋ ˆ์ด์–ด, record DTO, ์ƒ์„ฑ์ž ์ฃผ์ž…, ApiResponse/ErrorCode ๋ฐ Gradle Wrapper ๊ด€๋ก€๋ฅผ ์œ ์ง€ํ–ˆ๋‹ค. ๋ธŒ๋žœ์น˜์™€ origin/status๋ฅผ ๋จผ์ € ํ™•์ธํ–ˆ๊ณ  ๊ธฐ์กด ์ด๋ฉ”์ผ ์ธ์ฆ/DB ์ž‘์—…์„ ๋ฎ์–ด์“ฐ์ง€ ์•Š์•˜๋‹ค.

์ˆ˜์ • ํŒŒ์ผ:

- `src/main/java/com/select/domain/auth/service/EmailVerificationService.java`: ์ด๋ฉ”์ผ ์ •๊ทœํ™”, ์œ ํ˜•๋ณ„ ์˜ˆ์™ธ, ๊ฐ€์ž…์šฉ ์ธ์ฆ ์˜ˆ์•ฝ ๋ฐ ํŠธ๋žœ์žญ์…˜ ์™„๋ฃŒ ์ฒ˜๋ฆฌ
- `src/main/java/com/select/domain/user/service/UserService.java`: ๊ธฐ๋ณธ ๊ฐ€์ž… ๊ฒ€์ฆ ํ›„ ์ธ์ฆ ์˜ˆ์•ฝ, ๋กœ๊ทธ์ธ๊ณผ ๊ณตํ†ต ์ด๋ฉ”์ผ ์ •๊ทœํ™”
- `src/main/java/com/select/domain/auth/dto/request/EmailSendRequest.java`
- `src/main/java/com/select/domain/auth/dto/request/EmailVerifyRequest.java`
- `src/main/java/com/select/domain/user/dto/request/SignupRequest.java`
- `src/main/java/com/select/domain/user/dto/request/LoginRequest.java`: ์œ„ ๋„ค record DTO ๋ชจ๋‘ ๊ฐ™์€ ์ •๊ทœํ™” ์‚ฌ์šฉ. ์ธ์ฆ DTO ์ด๋ฉ”์ผ ๊ธธ์ด๋„ 320์ž๋กœ ์ œํ•œ
- `src/main/java/com/select/global/exception/ErrorCode.java`
- `src/main/java/com/select/global/exception/GlobalExceptionHandler.java`: ์ธ์ฆ ์‹คํŒจ์˜ ๊ตฌ์กฐํ™”๋œ ์ƒํƒœ/์˜ค๋ฅ˜ ์‘๋‹ต
- ๊ธฐ์กด ํ…Œ์ŠคํŠธ 4๊ฐœ: UserServiceTest, UserEmailConstraintTest, UserControllerTest, JwtAuthenticationIntegrationTest. ์ƒ์„ฑ์ž ์˜์กด์„ฑ์„ ๋ฐ˜์˜ํ•˜๊ณ  ๊ธฐ์กด ํ…Œ์ŠคํŠธ ๋ชฉ์ ์„ ์œ ์ง€

์ถ”๊ฐ€ ํŒŒ์ผ:

- `src/main/java/com/select/global/util/EmailNormalizer.java`
- `src/main/java/com/select/domain/auth/exception/EmailVerificationException.java`
- `src/test/java/com/select/domain/auth/service/EmailVerificationServiceTest.java`
- `src/test/java/com/select/domain/auth/service/EmailSignupIntegrationTest.java`
- `src/test/java/com/select/domain/auth/controller/EmailVerificationControllerTest.java`
- ์ด ๋ฌธ์„œ

## ์ด๋ฉ”์ผ๊ณผ API ๊ณ„์•ฝ

๋ฐœ์†กยท๊ฒ€์ฆยทํšŒ์›๊ฐ€์ž…ยท๋กœ๊ทธ์ธ ๋ชจ๋‘ `trim().toLowerCase(Locale.ROOT)`์„ ์‚ฌ์šฉํ•œ๋‹ค. DTO ์ƒ์„ฑ ์‹œ ๊ฒ€์ฆ ์ „์— ์ ์šฉํ•˜๋ฉฐ ์„œ๋น„์Šค ์ง์ ‘ ํ˜ธ์ถœ๋„ ๊ฐ™์€ ๊ณตํ†ต ํ•จ์ˆ˜๋ฅผ ํ†ต๊ณผํ•œ๋‹ค. ์ด๋ฉ”์ผ ๋ณ„์นญ/์  ์ œ๊ฑฐ ๋“ฑ์˜ ์ถ”๊ฐ€ ๊ทœ์น™์€ ๋„ฃ์ง€ ์•Š์•˜๋‹ค. ์ธ์ฆ๋ฒˆํ˜ธ๋Š” ๊ทธ๋Œ€๋กœ ๊ฒ€์ฆํ•˜๋ฉฐ ๊ธฐ์กด HMAC-SHA256 ์ €์žฅ ๋ฐฉ์‹๊ณผ ์ธ์ฆ/์‹œ๋„/์žฌ๋ฐœ์†ก TTL์€ ์œ ์ง€ํ–ˆ๋‹ค.

ํšŒ์›๊ฐ€์ž… ์š”์ฒญ/์‘๋‹ต record์™€ ์„ฑ๊ณต 201, ๊ธฐ์กด ๋กœ๊ทธ์ธ/JWT/์ค‘๋ณต ์ด๋ฉ”์ผ 409/๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ์ฒ˜๋ฆฌ๋Š” ์œ ์ง€ํ•œ๋‹ค. ๊ฐ€์ž…์— ์œ ํšจํ•œ ์ด๋ฉ”์ผ ์ธ์ฆ์ด ์ถ”๊ฐ€๋กœ ํ•„์š”ํ•˜๋‹ค. ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ ๋ฐ ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์€ ์˜ˆ์•ฝ ์ „์— ๊ฑฐ๋ถ€ํ•˜๋ฏ€๋กœ ์ธ์ฆ์„ ์†Œ๋น„ํ•˜์ง€ ์•Š๋Š”๋‹ค.

| ์˜ค๋ฅ˜ ์ฝ”๋“œ | HTTP | ์˜๋ฏธ |
|---|---|---|
| EMAIL_CODE_MISMATCH | 400 | ์ธ์ฆ๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ |
| EMAIL_CODE_EXPIRED | 400 | ์ธ์ฆ๋ฒˆํ˜ธ ์—†์Œ/๋งŒ๋ฃŒ/์ด๋ฏธ ์‚ฌ์šฉ |
| EMAIL_ATTEMPTS_EXCEEDED | 429 | ์ตœ๋Œ€ ์ž…๋ ฅ ํšŸ์ˆ˜ ์ดˆ๊ณผ |
| EMAIL_RESEND_TOO_SOON | 429 | ์žฌ๋ฐœ์†ก ์ œํ•œ |
| EMAIL_NOT_VERIFIED | 400 | ์ธ์ฆ ์™„๋ฃŒ ์ •๋ณด ์—†์Œ/๋งŒ๋ฃŒ |
| EMAIL_VERIFICATION_IN_PROGRESS | 409 | ๊ฐ™์€ ์ธ์ฆ์„ ๋‹ค๋ฅธ ๊ฐ€์ž… ์š”์ฒญ์ด ์˜ˆ์•ฝ ์ค‘ |
| EMAIL_VERIFICATION_UNAVAILABLE | 503 | Redis ์žฅ์• /์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ์ธ์ฆ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ |
| EMAIL_SEND_FAILED | 503 | ์ธ์ฆ ๋ฉ”์ผ ์ „์†ก ์‹คํŒจ |

์‘๋‹ต์€ ๊ธฐ์กด ApiResponse์˜ success/message/data/error.code/error.details ํ˜•์‹์„ ๋”ฐ๋ฅธ๋‹ค. ๋‚ด๋ถ€ Redis ์˜ค๋ฅ˜๋‚˜ ์ธ์ฆ๋ฒˆํ˜ธ/secret์„ ์‘๋‹ต์— ๋‹ด์ง€ ์•Š๋Š”๋‹ค.

## Redis ์ผํšŒ์„ฑ ์‚ฌ์šฉ๊ณผ ์‹ค์ œ DB ์ปค๋ฐ‹

๊ธฐ์กด ์ธ์ฆ ์™„๋ฃŒ ํ‚ค `email:verified:{์ •๊ทœํ™” ์ด๋ฉ”์ผ}`, ๊ฐ’ `1`, TTL 10๋ถ„์„ ์œ ์ง€ํ•œ๋‹ค. ์กฐํšŒ ํ›„ ์‚ญ์ œํ•˜๋Š” ๊ตฌํ˜„ ๋Œ€์‹  Lua ๋‘ ๊ฐœ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ๊ธฐ์กด ๋ฐœ์†ก/๋ฒˆํ˜ธ๊ฒ€์ฆ Lua ๋ณธ๋ฌธ์€ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•˜๋‹ค.

1. `UserService.signup()`์˜ ์‹ค์ œ Spring ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ ๊ธฐ๋ณธ ๊ฐ€์ž… ๊ฒ€์ฆ ํ›„ ์˜ˆ์•ฝํ•œ๋‹ค.
2. ์˜ˆ์•ฝ Lua๋Š” ๊ฐ’์ด `1`์ด๋ฉฐ PTTL์ด ์–‘์ˆ˜์ผ ๋•Œ์—๋งŒ `signup:{์ž„์˜ UUID}`๋กœ ์›์ž์ ์œผ๋กœ ๋ณ€๊ฒฝํ•œ๋‹ค. Redis KEEPTTL๋กœ ์›๋ž˜ ๋งŒ๋ฃŒ ์‹œ๊ฐ์„ ์œ ์ง€ํ•œ๋‹ค. ๋‹ค๋ฅธ ์š”์ฒญ์€ ์˜ˆ์•ฝ ์ค‘ ์˜ค๋ฅ˜๋ฅผ ๋ฐ›๋Š”๋‹ค. ๋งŒ๋ฃŒ/TTL ์—†๋Š” ํ‚ค/์ž„์˜ ๊ฐ’์€ ์ธ์ฆ์œผ๋กœ ์ธ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค.
3. ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•ด์‹œ ๋ฐ saveAndFlush๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. flush๋Š” ์ปค๋ฐ‹์ด ์•„๋‹ˆ๋ฏ€๋กœ ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ์ธ์ฆ์„ ์‚ญ์ œํ•˜์ง€ ์•Š๋Š”๋‹ค.
4. Spring TransactionSynchronization.afterCompletion์˜ ์‹ค์ œ ๊ฒฐ๊ณผ๊ฐ€ COMMITTED์ผ ๋•Œ๋งŒ ๋‚ด ์˜ˆ์•ฝ ํ† ํฐ์„ ์‚ญ์ œํ•œ๋‹ค.
5. ํ™•์ •๋œ ROLLED_BACK์ด๋ฉด ๋‚ด ํ† ํฐ์ด ์•„์ง ์œ ํšจํ•œ ๊ฒฝ์šฐ `1`๋กœ ๋ณต์›ํ•œ๋‹ค. KEEPTTL์ด๋ฏ€๋กœ ๋‚จ์€ TTL๋งŒ ์œ ์ง€ํ•œ๋‹ค. ์ด๋ฏธ ๋งŒ๋ฃŒ๋๊ฑฐ๋‚˜ ์ƒˆ ์ธ์ฆ/๋‹ค๋ฅธ ์š”์ฒญ์œผ๋กœ ๋ฐ”๋€ ํ‚ค๋Š” ๋ณต์›ํ•˜๊ฑฐ๋‚˜ ์ง€์šฐ์ง€ ์•Š๋Š”๋‹ค.
6. UNKNOWN์€ ๋ณต์›ํ•˜์ง€ ์•Š๊ณ  ๋‚ด ์˜ˆ์•ฝ์„ ์†Œ๋น„ํ•œ๋‹ค. ์„ฑ๊ณต ์—ฌ๋ถ€๊ฐ€ ๋ถˆ๋ช…ํ™•ํ•œ DB ์ž‘์—…์— ๋Œ€ํ•ด ์ธ์ฆ ์žฌ์‚ฌ์šฉ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

ํ•œ ์ธ์ฆ์„ ๋™์‹œ์— ๋‘ ๊ฐ€์ž… ์š”์ฒญ์ด ์˜ˆ์•ฝํ•  ์ˆ˜ ์—†๋‹ค. MySQL ์ด๋ฉ”์ผ UNIQUE๋„ ์ค‘๋ณต ๊ณ„์ • ์ƒ์„ฑ์„ ์ตœ์ข… ์ฐจ๋‹จํ•œ๋‹ค. ์ƒˆ ์ธ์ฆ์ด ์ง„ํ–‰๋˜์–ด ํ‚ค ๊ฐ’์ด ๋ฐ”๋€Œ์–ด๋„ ์ด์ „ ์š”์ฒญ์˜ ์™„๋ฃŒ ์ฒ˜๋ฆฌ๊ฐ€ ์ƒˆ ์ธ์ฆ์„ ์ œ๊ฑฐํ•˜์ง€ ์•Š๋„๋ก ํ† ํฐ์„ ๋น„๊ตํ•œ๋‹ค.

## ์‹คํŒจ ์ฒ˜๋ฆฌ์™€ ๋‚จ์€ ์œ„ํ—˜

MySQL๊ณผ Redis ์‚ฌ์ด์— ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜/์ž๋™ ๊ณต๋™ ๋กค๋ฐฑ์€ ์—†๋‹ค. ์ด ๊ตฌํ˜„์€ ์›์ž์  ์˜ˆ์•ฝ๊ณผ ์‹ค์ œ ํŠธ๋žœ์žญ์…˜ ๊ฒฐ๊ณผ์— ๋”ฐ๋ฅธ ์กฐ๊ฑด๋ถ€ ๋ณด์ƒ ๋ฐฉ์‹์ด๋‹ค.

- DB ์ €์žฅ/flush/์ปค๋ฐ‹ ์ „ ๋‹จ๊ณ„ ์‹คํŒจ๊ฐ€ ๋ช…ํ™•ํžˆ ๋กค๋ฐฑ๋˜๋ฉด ์ธ์ฆ์€ ์›๋ž˜ ๋‚จ์€ ์‹œ๊ฐ„ ์•ˆ์—์„œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋‹ค.
- DB ์ปค๋ฐ‹ ํ›„ Redis ์‚ญ์ œ ์‹คํŒจ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ฐ€์ž…์„ ์‹คํŒจ ์‘๋‹ต์œผ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ๊ฒฝ๊ณ ๋ฅผ ๋‚จ๊ธฐ๋ฉฐ ์˜ˆ์•ฝ ๊ฐ’์€ `1`์ด ์•„๋‹ˆ๋ฏ€๋กœ ๊ฐ€์ž… ์ธ์ฆ์œผ๋กœ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค. ์›๋ž˜ TTL ๋˜๋Š” ์ƒˆ ์ธ์ฆ์œผ๋กœ ์ •๋ฆฌ๋œ๋‹ค.
- ๋กค๋ฐฑ ํ›„ Redis ๋ณต๊ตฌ ์‹คํŒจ๋‚˜ ํ”„๋กœ์„ธ์Šค ๊ฐ•์ œ ์ข…๋ฃŒ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด ์˜ˆ์•ฝ์ด TTL๊นŒ์ง€ ๋‚จ์„ ์ˆ˜ ์žˆ์–ด ์žฌ์ธ์ฆ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ณด์•ˆ์„ ์œ„ํ•ด ์ž๋™์œผ๋กœ `1`๋กœ ๊ฐ„์ฃผํ•˜์ง€ ์•Š๋Š”๋‹ค.
- Redis ์žฅ์• ๋กœ ์˜ˆ์•ฝ ์„ฑ๊ณต ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์œผ๋ฉด ํšŒ์› ์ €์žฅ ์ „์— ๊ฐ€์ž…์„ ๊ฑฐ๋ถ€ํ•œ๋‹ค. Redis๊ฐ€ ์˜ˆ์•ฝ์„ ๋ฐ˜์˜ํ•œ ๋’ค ์‘๋‹ต์ด ์œ ์‹ค๋˜๋ฉด ์—ญ์‹œ TTL๊นŒ์ง€ ๋Œ€๊ธฐ/์žฌ์ธ์ฆ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค.
- Redis ์žฌ์‹œ์ž‘ยท๋น„๋™๊ธฐ ๋ณต์ œ failover๊นŒ์ง€ ํฌํ•จํ•˜๋Š” ์—„๋ฐ€ํ•œ ์ „์—ญ exactly-once๋ฅผ ๋ณด์žฅํ•˜๋Š” ์„ค๊ณ„๋Š” ์•„๋‹ˆ๋‹ค. ์˜์† outbox/์˜ˆ์•ฝ ํ…Œ์ด๋ธ”/์žฌ์ฒ˜๋ฆฌ ์ž‘์—…์€ DB ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ๊ธˆ์ง€์™€ ํ˜„์žฌ ๋ฒ”์œ„๋ฅผ ๊ณ ๋ คํ•ด ๋„์ž…ํ•˜์ง€ ์•Š์•˜๋‹ค.
- ๊ฐ€์ž… ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์ธ์ฆ ์œ ํšจ๊ธฐ๊ฐ„์ด ๋๋‚˜๋„, ๋งŒ๋ฃŒ ์ „์— ์˜ˆ์•ฝํ•œ ์š”์ฒญ์€ ์™„๋ฃŒ๋  ์ˆ˜ ์žˆ๋‹ค. ์ดํ›„ ์š”์ฒญ์—๋Š” ๋งŒ๋ฃŒ๋œ ํ‚ค๋ฅผ ์ƒˆ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค.
- ์ด์ „ ๋ฒ„์ „์ด ๋งŒ๋“  ๋Œ€๋ฌธ์ž/๊ณต๋ฐฑ ์ด๋ฉ”์ผ Redis ํ‚ค๋Š” ์ƒˆ ์ •๊ทœํ™” ํ‚ค์™€ ์—ฐ๊ฒฐํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ธฐ์กด TTL๋กœ ๋งŒ๋ฃŒ๋˜๋ฉฐ ํ•„์š”ํ•œ ์‚ฌ์šฉ์ž๋Š” ์žฌ์ธ์ฆํ•œ๋‹ค.

## ๊ฒ€์ฆ

์‹คํ–‰ ๋ช…๋ น(Java 21 ๊ฒฝ๋กœ๋งŒ ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ๋ณ€๊ฒฝ):

```bash
JAVA_HOME=/Users/dgsw49/Library/Java/JavaVirtualMachines/ms-21.0.10/Contents/Home BABI_REDIS_TEST_PORT=6379 ./gradlew compileJava test --offline --console=plain
```

๊ฒฐ๊ณผ: ์ปดํŒŒ์ผ ์„ฑ๊ณต, ์ด **165๊ฑด ํ†ต๊ณผ**, ์‹คํŒจ/์˜ค๋ฅ˜/๊ฑด๋„ˆ๋œ€ 0. ๊ธฐ์กด 130๊ฑด๊ณผ ์‹ ๊ทœ 35๊ฑด์ด๋‹ค. ์‹ค์ œ Redis ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ 13๊ฑด์ด ํฌํ•จ๋๋‹ค.

- ๋ฏธ์ธ์ฆ/๋งŒ๋ฃŒ ๊ฐ€์ž… ๊ฑฐ๋ถ€, ์ธ์ฆ ํ›„ ๊ฐ€์ž…ยท๋กœ๊ทธ์ธยทJWT ๋ฐœ๊ธ‰, ์ค‘๋ณต ์ด๋ฉ”์ผ, ์ธ์ฆ ์žฌ์‚ฌ์šฉ ๊ฑฐ๋ถ€
- ์ด๋ฉ”์ผ ๋ฐœ์†ก/๊ฒ€์ฆ API์˜ ๋Œ€์†Œ๋ฌธ์žยท๊ณต๋ฐฑ ์ผ์น˜, ์žฌ๋ฐœ์†ก ์ œํ•œ, ๋ถˆ์ผ์น˜/์‹œ๋„ ์ดˆ๊ณผ/๋งŒ๋ฃŒ, ์ „์†ก ์‹คํŒจ ์ •๋ฆฌ
- ์‹ค์ œ HTTP ๋™์‹œ ๊ฐ€์ž… ๋‘ ์š”์ฒญ ์ค‘ 1๊ฑด๋งŒ ์„ฑ๊ณต, ์ฒซ ํŠธ๋žœ์žญ์…˜์ด ์—ด๋ฆฐ ๋™์•ˆ ๋‹ค๋ฅธ ์š”์ฒญ ์˜ˆ์•ฝ ์ฐจ๋‹จ
- saveAndFlush ํ›„ ์™ธ๋ถ€ ํŠธ๋žœ์žญ์…˜ ๋กค๋ฐฑ ๋ฐ beforeCommit ์‹คํŒจ ๋ณด์ƒ, ์‹ค์ œ ์ €์žฅ ์‹คํŒจ ํ›„ ์ธ์ฆ ๋ณต๊ตฌ
- ๋งŒ๋ฃŒ ์ธ์ฆ ๋ณต๊ตฌ ๋ฐฉ์ง€, ์˜ค๋ž˜๋œ ์™„๋ฃŒ ์ž‘์—…์ด ์ƒˆ ์ธ์ฆ์„ ๋ฎ์–ด์“ฐ๊ฑฐ๋‚˜ ์ง€์šฐ์ง€ ์•Š์Œ
- UNKNOWN ๊ฒฐ๊ณผ ์†Œ๋น„, Redis ์žฅ์•  ๊ฑฐ๋ถ€, ์ปค๋ฐ‹ ํ›„ Redis ์ •๋ฆฌ ์‹คํŒจ๊ฐ€ ์„ฑ๊ณต ์‘๋‹ต์„ ๋’ค์ง‘์ง€ ์•Š์Œ
- ๊ธฐ์กด ๊ฐ€์ž…/๋กœ๊ทธ์ธ/ํƒˆํ‡ด ์ƒํƒœ/JWT ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์œ ์ง€. ๊ธฐ์กด JWT ํ…Œ์ŠคํŠธ๋Š” ์ธ์ฆ ์„œ๋น„์Šค mock์„ ์‚ฌ์šฉํ•˜๊ณ  ์ƒˆ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” ์‹ค์ œ ์ธ์ฆ ์„œ๋น„์Šค/Lua๋ฅผ ์‚ฌ์šฉ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” H2์™€ ์‹ค์ œ 127.0.0.1 Redis๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ์ด๋ฉ”์ผ์€ UUID ๊ธฐ๋ฐ˜ `babi-test-โ€ฆ@example.invalid`์ด๋‹ค. ํ•ด๋‹น ํ…Œ์ŠคํŠธ๊ฐ€ ๋งŒ๋“  ๋„ค ์ข…๋ฅ˜์˜ ์ •ํ™•ํ•œ Redis ํ‚ค๋งŒ ์ •๋ฆฌํ•˜๊ณ  FLUSHDB/FLUSHALL์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. SMTP EmailSender๋Š” MockitoBean์œผ๋กœ ๊ต์ฒดํ•ด ์™ธ๋ถ€ Gmail ๋ฐœ์†ก์„ ์ฐจ๋‹จํ•œ๋‹ค. BABI_REDIS_TEST_PORT๊ฐ€ ์—†์œผ๋ฉด ์‹ค์ œ Redis ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋งŒ ๊ฑด๋„ˆ๋›ฐ๋ฉฐ ๋‚˜๋จธ์ง€ ๋‹จ์œ„/ํšŒ๊ท€ ํ…Œ์ŠคํŠธ๋Š” ์‹คํ–‰ ๊ฐ€๋Šฅํ•˜๋‹ค. ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ๋ฐ”๊ฟ” ๋‹ค์‹œ ๊ฒ€์ฆํ•  ๋•Œ์—๋Š” Gradle ์บ์‹œ ์˜ํ–ฅ์„ ํ”ผํ•˜๋ ค๊ณ  `--rerun-tasks`๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

Spring Boot๋Š” ํ…Œ์ŠคํŠธ์˜ RANDOM_PORT์—์„œ ์‹ค์ œ HTTP๋ฅผ ์ฒ˜๋ฆฌํ–ˆ๋‹ค. ์‹ค์ œ ๊ฐœ๋ฐœ MySQL์—๋Š” ์—ฐ๊ฒฐ/๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•˜๋‹ค. ๋”ฐ๋ผ์„œ ์ด๋ฒˆ ์—ฐ๋™์˜ MySQL ์ง์ ‘ ํ†ตํ•ฉ ๊ฒ€์ฆ๊ณผ ์‹ค Gmail ์ „์†ก์€ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์•˜์œผ๋ฉฐ, ๊ธฐ์กด MySQL UNIQUE ๋ฐ ํŠธ๋žœ์žญ์…˜ ๋™์ž‘์„ ์ „์ œ๋กœ ํ•œ๋‹ค.

GmailEmailSender, Entity, build.gradle, compose.yml, application ์„ค์ • 3๊ฐœ์˜ ์ด 7๊ฐœ ๋ณดํ˜ธ ํŒŒ์ผ์€ ์ตœ์ดˆ์™€ ํ•ด์‹œ๊ฐ€ ์ผ์น˜ํ•œ๋‹ค. `.env`๋Š” ์ฝ๊ฑฐ๋‚˜ ์ถœ๋ ฅํ•˜์ง€ ์•Š์•˜๋‹ค. ์‹ค์ œ ํšŒ์› ๋ฐ์ดํ„ฐ, DB ์Šคํ‚ค๋งˆ, Docker ๋ณผ๋ฅจ์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์•˜๊ณ  commit/push/merge/stage๋„ ์ˆ˜ํ–‰ํ•˜์ง€ ์•Š์•˜๋‹ค.

`git diff --check` ๋ฐ ์‹ ๊ทœ ํŒŒ์ผ ๋ณ„๋„ ๊ณต๋ฐฑ ๊ฒ€์‚ฌ๋ฅผ ํ†ต๊ณผํ–ˆ๋‹ค. ๊ธฐ์กด Git ๋ณ€๊ฒฝ์€ ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์žˆ์œผ๋ฏ€๋กœ ์ „์ฒด git diff์—๋Š” ์ด์ „ ์ด๋ฉ”์ผ/DB ์ž‘์—…๋„ ํ•จ๊ป˜ ๋ณด์ธ๋‹ค. ์ด ๋ฌธ์„œ์˜ ํŒŒ์ผ ๋ชฉ๋ก์ด ์ด๋ฒˆ ์—ฐ๋™์˜ ๋ณ€๊ฒฝ ๋ฒ”์œ„๋‹ค.
133 changes: 133 additions & 0 deletions docs/api/email-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# ์ด๋ฉ”์ผ ์ธ์ฆ ํ† ํฐ๊ณผ ํšŒ์›๊ฐ€์ž… API ๊ณ„์•ฝ

2026-10-25. `feature/email-verification`์˜ ํด๋ผ์ด์–ธํŠธ ๊ฒฐํ•ฉ ์ธ์ฆ ํ† ํฐ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด๋‹ค. ๊ณตํ†ต `ApiResponse`์™€ ๊ธฐ์กด ์˜ค๋ฅ˜ ์ฝ”๋“œ๋Š” ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค. ์›๋ณธ API ๋ช…์„ธ ์ „์ฒด๋Š” ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ/๋ฌธ์„œ์—์„œ ํ™•์ธ๋˜์ง€ ์•Š์•˜์œผ๋ฏ€๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ์ง€์ •ํ•œ ์„ฑ๊ณต ์‘๋‹ต๊ณผ ํ˜„์žฌ `ErrorCode`๋ฅผ ์ด ๋ณ€๊ฒฝ์˜ ๊ธฐ์ค€์œผ๋กœ ์‚ผ์•˜๋‹ค.

## ํ”„๋ก ํŠธ์—”๋“œ ํ๋ฆ„

1. ์ด๋ฉ”์ผยท๋น„๋ฐ€๋ฒˆํ˜ธยท๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธยท๋‹‰๋„ค์ž„์„ ์ž…๋ ฅ ์ƒํƒœ์— ๋ณด๊ด€ํ•œ๋‹ค.
2. `POST /auth/email/send`๋กœ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์š”์ฒญํ•œ๋‹ค.
3. `POST /auth/email/verify`์— ์ด๋ฉ”์ผ๊ณผ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ๋ณด๋‚ธ๋‹ค.
4. ์„ฑ๊ณต ์‘๋‹ต์˜ `data.verificationToken`์„ ๋ฐ›์ž๋งˆ์ž ๋ณด๊ด€ ์ค‘์ธ ๊ฐ€์ž… ์ •๋ณด์™€ ํ•จ๊ป˜ `POST /auth/signup`์„ ํ˜ธ์ถœํ•œ๋‹ค.
5. ๊ฐ€์ž… 201 ์‘๋‹ต์„ ๋ฐ›์€ ๊ฒฝ์šฐ์—๋งŒ ์™„๋ฃŒ ํ™”๋ฉด์„ ํ‘œ์‹œํ•œ๋‹ค. ์ธ์ฆ ์„ฑ๊ณต๋งŒ์œผ๋กœ ๊ฐ€์ž… ์™„๋ฃŒ๋กœ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค.

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

## ์ธ์ฆ๋ฒˆํ˜ธ ๊ฒ€์ฆ

`POST /auth/email/verify`, ์ธ์ฆ ๋ถˆํ•„์š”.

```json
{
"email": "user@example.com",
"code": "123456"
}
```

200 ์„ฑ๊ณต ์‘๋‹ต:

```json
{
"success": true,
"message": "ํšŒ์›๊ฐ€์ž… ์ด๋ฉ”์ผ ์ธ์ฆ๋ฒˆํ˜ธ ํ™•์ธ ์š”์ฒญ์ด ์ฒ˜๋ฆฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"data": {
"email": "user@example.com",
"verified": true,
"expiresIn": 600,
"verificationToken": "๋ฐœ๊ธ‰๋œ-43์ž๋ฆฌ-Base64URL-ํ† ํฐ"
}
}
```

์œ„ ํ† ํฐ ๋ฌธ์ž์—ด์€ ์„ค๋ช…์šฉ ์ž๋ฆฌํ‘œ์‹œ์ž๋‹ค. ์‹ค์ œ ๊ฐ’์€ `SecureRandom` 32๋ฐ”์ดํŠธ(256๋น„ํŠธ)๋ฅผ ํŒจ๋”ฉ ์—†๋Š” Base64URL๋กœ ์ธ์ฝ”๋”ฉํ•œ ASCII 43์ž๋ฆฌ๋‹ค. ์ด ๊ฐ’์€ ๊ฒ€์ฆ์— ์„ฑ๊ณตํ•œ ์š”์ฒญ์˜ ์‘๋‹ต์—๋งŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค. `expiresIn`์€ ์ดˆ ๋‹จ์œ„์ด๊ณ  Redis ์„ค์ •์— ์‚ฌ์šฉํ•˜๋Š” ๋™์ผํ•œ 600์ดˆ Duration์—์„œ ๊ณ„์‚ฐํ•œ๋‹ค. ์‹ค์ œ ๋งŒ๋ฃŒ ๊ธฐ์ค€์€ ๋ฐœ๊ธ‰ ์‹œ์ ์˜ ์„œ๋ฒ„ Redis TTL์ด๋‹ค. ๋„คํŠธ์›Œํฌ ์ง€์—ฐ ๋™์•ˆ์—๋„ ์‹œ๊ฐ„์ด ํ๋ฅด๋ฉฐ ๊ฐ€์ž… ์š”์ฒญ์€ ๋งŒ๋ฃŒ ์ „์— ํ† ํฐ์„ ์˜ˆ์•ฝํ•ด์•ผ ํ•œ๋‹ค.

๊ธฐ์กด `email`, `verified`, `expiresIn` ์‘๋‹ต ํ•„๋“œ๋ฅผ ํฌํ•จํ•˜๊ณ  `verificationToken`์„ ์ถ”๊ฐ€ํ–ˆ๋‹ค. DTO๋Š” `EmailVerificationResponse` record๋‹ค. ํ˜„์žฌ ์ž‘์—… ์ „ ๊ตฌํ˜„์€ data=null์ด์—ˆ์œผ๋ฉฐ, ์ด๋ฒˆ ๋ณ€๊ฒฝ์—์„œ ์š”์ฒญ๋œ ๋ช…์„ธ ํ•„๋“œ๋ฅผ ๊ตฌํ˜„ํ–ˆ๋‹ค.

## ํšŒ์›๊ฐ€์ž…

`POST /auth/signup`, ์ธ์ฆ ๋ถˆํ•„์š”. ์š”์ฒญ์— `verificationToken`์ด ์ƒˆ ํ•„์ˆ˜ ํ•„๋“œ๋กœ ์ถ”๊ฐ€๋œ๋‹ค.

```json
{
"email": "user@example.com",
"password": "Test1234!",
"passwordConfirm": "Test1234!",
"nickname": "ํ…Œ์ŠคํŠธ",
"verificationToken": "์ด๋ฉ”์ผ-๊ฒ€์ฆ-์‘๋‹ต์—์„œ-๋ฐ›์€-43์ž๋ฆฌ-๊ฐ’"
}
```

ํ† ํฐ ์ž๋ฆฌํ‘œ์‹œ์ž๋Š” ์‹ค์ œ ๊ฒ€์ฆ ์‘๋‹ต ๊ฐ’์œผ๋กœ ๋ฐ”๊ฟ”์•ผ ํ•œ๋‹ค. ํ† ํฐ์€ trimยท์†Œ๋ฌธ์ž ๋ณ€ํ™˜ํ•˜์ง€ ์•Š์œผ๋ฉฐ ์ •ํ™•ํžˆ ์ „๋‹ฌํ•œ๋‹ค. ์ด๋ฉ”์ผ๋งŒ ๊ธฐ์กด ๊ทœ์น™๋Œ€๋กœ trim + Locale.ROOT ์†Œ๋ฌธ์ž ๋ณ€ํ™˜ํ•œ๋‹ค.

201 ์„ฑ๊ณต ์‘๋‹ต์€ ๊ธฐ์กด ๊ณ„์•ฝ์„ ์œ ์ง€ํ•œ๋‹ค.

```json
{
"success": true,
"message": "ํšŒ์›๊ฐ€์ž…์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
"data": {
"userId": 123,
"email": "user@example.com",
"nickname": "ํ…Œ์ŠคํŠธ"
}
}
```

์ธ์ฆ๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ, ๋‹ค๋ฅธ ์ด๋ฉ”์ผ์˜ ํ† ํฐ, ์ž˜๋ชป๋œ ํ† ํฐ, ๋งŒ๋ฃŒยท์†Œ๋น„๋œ ํ† ํฐ์€ ๊ฐ€์ž…ํ•  ์ˆ˜ ์—†๋‹ค. ์ƒˆ ์ธ์ฆ๋ฒˆํ˜ธ ๋ฐœ์†ก์€ ๊ธฐ์กด ์ธ์ฆ์„ ํ๊ธฐํ•˜๊ณ , ์ƒˆ ๋ฒˆํ˜ธ ๊ฒ€์ฆ ์™„๋ฃŒ ์‹œ ์ƒˆ ํ† ํฐ ํ•ด์‹œ๊ฐ€ ์ €์žฅ๋˜์–ด ์ด์ „ ํ† ํฐ์œผ๋กœ ์ƒˆ ์˜ˆ์•ฝ์„ ๋งŒ๋“ค ์ˆ˜ ์—†๋‹ค. ์ด๋ฏธ ๋งŒ๋ฃŒ ์ „์— ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์œผ๋กœ ์˜ˆ์•ฝํ•œ DB ํŠธ๋žœ์žญ์…˜์€ ๊ธฐ์กด ๋™์ž‘๋Œ€๋กœ ์™„๋ฃŒ๋  ์ˆ˜ ์žˆ๋‹ค. ์žฌ์ธ์ฆ์ด ์ด๋ฏธ ์ง„ํ–‰ ์ค‘์ธ DB ํŠธ๋žœ์žญ์…˜์„ ์ทจ์†Œํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋‹ค.

## ์‹คํŒจ ์‘๋‹ต๊ณผ ์ƒํƒœ ์ฝ”๋“œ

๋ชจ๋“  ์‹คํŒจ๋Š” ๊ธฐ์กด `ApiResponse`์˜ `success=false`, `message`, `data=null`, `error.code`, `error.details`๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ƒˆ `TOO_MANY_ATTEMPTS` ์ฝ”๋“œ๋ฅผ ๋งŒ๋“ค์ง€ ์•Š์•˜๋‹ค.

| ์ƒํ™ฉ | HTTP | ๊ธฐ์กด ErrorCode |
|---|---|---|
| ์ธ์ฆ๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜ | 400 | EMAIL_CODE_MISMATCH |
| ์ธ์ฆ๋ฒˆํ˜ธ ์—†์Œ/๋งŒ๋ฃŒ/์ด๋ฏธ ์†Œ๋น„๋จ | 400 | EMAIL_CODE_EXPIRED |
| ์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ ํšŸ์ˆ˜ ์ดˆ๊ณผ | 429 | EMAIL_ATTEMPTS_EXCEEDED |
| 60์ดˆ ์žฌ๋ฐœ์†ก ์ œํ•œ | 429 | EMAIL_RESEND_TOO_SOON |
| ํ† ํฐ ๋ˆ„๋ฝยท๋นˆ ๊ฐ’ยท43์ž๋ฆฌ Base64URL ๊ทœ์น™ ๋ถˆ์ผ์น˜ | 400 | VALIDATION_ERROR |
| ํ˜•ํƒœ๋Š” ๋งž์œผ๋‚˜ ํ† ํฐ/์ด๋ฉ”์ผ ๋ถˆ์ผ์น˜ยท๋งŒ๋ฃŒยท์†Œ๋น„, ๊ณผ๊ฑฐ ๊ฐ’ 1 | 400 | EMAIL_NOT_VERIFIED |
| ๊ฐ™์€ ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์˜ ๋‹ค๋ฅธ ๊ฐ€์ž… ์š”์ฒญ์ด ์˜ˆ์•ฝ ์ค‘ | 409 | EMAIL_VERIFICATION_IN_PROGRESS |
| ์ด๋ฏธ ๊ฐ€์ž…ํ•œ ์ด๋ฉ”์ผ | 409 | DUPLICATE_EMAIL |
| ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ๋ถˆ์ผ์น˜ | 400 | PASSWORD_MISMATCH |
| Redis ์žฅ์• /์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ๊ฒฐ๊ณผ | 503 | EMAIL_VERIFICATION_UNAVAILABLE |
| ๋ฉ”์ผ ๋ฐœ์†ก ์‹คํŒจ | 503 | EMAIL_SEND_FAILED |

๊ฐ€์ž…์˜ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ/์ค‘๋ณต ์ด๋ฉ”์ผ ์„ ํ–‰ ๊ฒ€์‚ฌ ์ˆœ์„œ๋Š” ์œ ์ง€ํ•œ๋‹ค. ์ž˜๋ชป๋œ ํ† ํฐ์„ ์ œ์ถœํ•œ ์š”์ฒญ์—๋Š” ์˜ˆ์•ฝ ์ค‘ ์ƒํƒœ๋ฅผ ๋…ธ์ถœํ•˜์ง€ ์•Š๋Š”๋‹ค. DTO๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๋Š” ์„œ๋น„์Šค ํ˜ธ์ถœ๋„ ํ† ํฐ ํ˜•์‹๊ณผ ํ•ด์‹œ๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

์ž…๋ ฅ ํšŸ์ˆ˜ ์ดˆ๊ณผ์˜ ์‹ค์ œ ์‘๋‹ต:

```json
{
"success": false,
"message": "์ธ์ฆ๋ฒˆํ˜ธ ์ž…๋ ฅ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค.",
"data": null,
"error": {
"code": "EMAIL_ATTEMPTS_EXCEEDED",
"details": []
}
}
```

`VALIDATION_ERROR`์˜ details์—๋Š” ํ•ด๋‹น ํ•„๋“œ์˜ ๊ฒ€์ฆ ์ด์œ ๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉฐ ํ† ํฐ ์›๋ฌธ์€ ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค. ์‹คํŒจ ์‘๋‹ต์—์„œ ์ƒˆ ์ธ์ฆ ํ† ํฐ์„ ๋ฐœ๊ธ‰ํ•˜์ง€ ์•Š๋Š”๋‹ค.

## Redis ์ผํšŒ์„ฑ๊ณผ DB ์ •ํ•ฉ์„ฑ

- `email:verified:{์ •๊ทœํ™” ์ด๋ฉ”์ผ}`์—๋Š” ์›๋ฌธ ํ† ํฐ ๋Œ€์‹  `proof:<์ด๋ฉ”์ผ๊ณผ ํ† ํฐ์— ๊ฒฐํ•ฉ๋œ SHA-256 ํ•ด์‹œ>`๋ฅผ ์ €์žฅํ•œ๋‹ค. ๋„๋ฉ”์ธ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๊ณผ ๊ตฌ๋ถ„์ž๋ฅผ ํฌํ•จํ•œ๋‹ค. ๋žœ๋ค ํ† ํฐ ์›๋ฌธ์€ Redis ํ‚คยท๊ฐ’ยทLua ์ธ์ž์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.
- ๋ฒˆํ˜ธ ๊ฒ€์ฆ ์„ฑ๊ณตยท๋ฒˆํ˜ธ ์†Œ๋น„ยท๊ธฐ์กด ํ† ํฐ ๊ต์ฒดยท600์ดˆ TTL ์„ค์ •์€ ํ•˜๋‚˜์˜ Lua ์Šคํฌ๋ฆฝํŠธ๋กœ ์ˆ˜ํ–‰ํ•œ๋‹ค.
- ๊ฐ€์ž… Lua๋Š” ํ•ด์‹œ๊ฐ€ ์ผ์น˜ํ•˜๊ณ  PTTL์ด ์–‘์ˆ˜์ผ ๋•Œ๋งŒ ๊ฐ’์„ `signup:<๊ฐ™์€ ํ•ด์‹œ>:<์š”์ฒญ๋ณ„ UUID>`๋กœ ๋ฐ”๊พธ๋ฉฐ KEEPTTL์„ ์‚ฌ์šฉํ•œ๋‹ค. ๊ฐ™์€ ์˜ฌ๋ฐ”๋ฅธ ํ† ํฐ์˜ ๋‘ ์š”์ฒญ์€ ๋™์‹œ์— ์˜ˆ์•ฝํ•  ์ˆ˜ ์—†๋‹ค.
- `saveAndFlush()`๋Š” ์ปค๋ฐ‹์ด ์•„๋‹ˆ๋‹ค. `afterCompletion(COMMITTED)`์—์„œ ๋‚ด ์˜ˆ์•ฝ ๊ฐ’์ด ์ผ์น˜ํ•  ๋•Œ๋งŒ ์‚ญ์ œํ•œ๋‹ค.
- ํ™•์ • ๋กค๋ฐฑ์ด๋ฉด ๋‚ด ์˜ˆ์•ฝ์ด ์•„์ง ์‚ด์•„ ์žˆ์„ ๋•Œ ๊ฐ™์€ proof ํ•ด์‹œ๋ฅผ KEEPTTL๋กœ ๋ณต๊ตฌํ•œ๋‹ค. ๋งŒ๋ฃŒ๋œ ํ† ํฐ์ด๋‚˜ ๋‹ค๋ฅธ ์ƒˆ ํ† ํฐ์„ ๋ณต๊ตฌ/๋ฎ์–ด์“ฐ๊ธฐํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ฒฐ๊ณผ ๋ถˆ๋ช…(UNKNOWN)์€ ํ† ํฐ์„ ๋ณต๊ตฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
- ์ปค๋ฐ‹ ํ›„ Redis ์ •๋ฆฌ ์˜ค๋ฅ˜๋ฅผ ์ด์œ ๋กœ ์ด๋ฏธ ์™„๋ฃŒ๋œ ๊ฐ€์ž…์„ ์‹คํŒจ๋กœ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค. ์˜ˆ์•ฝ ๊ฐ’์€ ์ธ์ฆ ๊ฐ€๋Šฅ proof๊ฐ€ ์•„๋‹ˆ๋ฏ€๋กœ ์›๋ž˜ TTL๊นŒ์ง€ ์žฌ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
- ๊ธฐ์กด Redis ๊ฐ’ `1`์„ ํ—ˆ์šฉํ•˜๋Š” ํ˜ธํ™˜ ๊ฒฝ๋กœ๋‚˜ ํ† ํฐ ์—†๋Š” ๊ฐ€์ž… ์˜ค๋ฒ„๋กœ๋“œ๋Š” ์—†๋‹ค. ์ด์ „ ์ธ์ฆ ์‚ฌ์šฉ์ž๋Š” ์žฌ์ธ์ฆํ•ด์•ผ ํ•œ๋‹ค. ์‹ค์ œ Redis ๋ฐ์ดํ„ฐ๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๊ฑฐ๋‚˜ ์ „์ฒด ์‚ญ์ œํ•˜์ง€ ์•Š์•˜๋‹ค.

ํ† ํฐ์€ bearer ์ฆ๋ช…์ด๋ฏ€๋กœ ํƒˆ์ทจํ•œ ์‚ฌ๋žŒ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์‹ค์ œ ๋ฐฐํฌ๋Š” HTTPS๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋ฉฐ ํด๋ผ์ด์–ธํŠธ/ํ”„๋ก์‹œ ๋กœ๊ทธ์˜ body ์ˆ˜์ง‘์„ ํ”ผํ•ด์•ผ ํ•œ๋‹ค. MySQL๊ณผ Redis๋Š” ๋ถ„์‚ฐ ํŠธ๋žœ์žญ์…˜์ด ์•„๋‹ˆ๋ฏ€๋กœ ํ”„๋กœ์„ธ์Šค ์ข…๋ฃŒยทRedis ์žฅ์• ยทfailover ์‹œ ์žฌ์ธ์ฆ์ด ํ•„์š”ํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•œ ํ† ํฐ์œผ๋กœ ๋™์‹œ์— ๋งŒ๋“  ๊ณ„์ •์˜ ์ค‘๋ณต์€ ๊ธฐ์กด DB UNIQUE๊ฐ€ ๋งˆ์ง€๋ง‰์œผ๋กœ ์ฐจ๋‹จํ•œ๋‹ค.

## ํ…Œ์ŠคํŠธ

์‹ค์ œ Redis ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” `BABI_REDIS_TEST_PORT`๋ฅผ ์ง€์ •ํ•ด ์‹คํ–‰ํ•œ๋‹ค. H2 + ์‹ค์ œ ๋กœ์ปฌ Redis + mock EmailSender๋ฅผ ์‚ฌ์šฉํ•˜๋ฉฐ ๊ฐœ๋ฐœ MySQL/Gmail์€ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

```bash
JAVA_HOME=/path/to/java21 BABI_REDIS_TEST_PORT=6379 ./gradlew clean test --offline --console=plain
```

๊ฒ€์ฆ ํ•ญ๋ชฉ: ์ •์ƒ ๊ฒ€์ฆโ†’์ž๋™ ๊ฐ€์ž…โ†’๋กœ๊ทธ์ธ, ๋ˆ„๋ฝ/์ž˜๋ชป๋œ/๋‹ค๋ฅธ ์ด๋ฉ”์ผ/๋งŒ๋ฃŒ/์†Œ๋น„ ํ† ํฐ ๊ฑฐ๋ถ€, ๋™์ผ ํ† ํฐ ๋™์‹œ ์š”์ฒญ, ์‹ค์ œ flush ๋ฐ ์ปค๋ฐ‹ ์ „ ์‹คํŒจ ๋ณด์ƒ, ์›๋ž˜ TTL ์œ ์ง€, ์žฌ์ธ์ฆ ์‹œ ์ด์ „ ํ† ํฐ ๋ฌดํšจํ™”, ๊ณผ๊ฑฐ ์ „์—ญ ๊ฐ’ 1 ๊ฑฐ๋ถ€, ๋‹ค๋ฅธ HTTP ํด๋ผ์ด์–ธํŠธ์˜ ๊ณ„์ • ์„ ์  ์ฐจ๋‹จ, Redis ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๊ธฐ์กด JWT ํšŒ๊ท€ ํ…Œ์ŠคํŠธ.
Loading
Loading