๐Ÿ“ง ์ด๋ฉ”์ผ ์ธ์ฆ ์‹œ์Šคํ…œ ๊ตฌํ˜„ํ•˜๊ธฐ

๋ฐ•์ค€ํ˜•ยท2025๋…„ 8์›” 17์ผ

์Šคํ”„๋ง ๊ฐœ๋ฐœ

๋ชฉ๋ก ๋ณด๊ธฐ
16/20
post-thumbnail

Dataracy ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํšŒ์›๊ฐ€์ž…, ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๊ณผ์ •์—์„œ ์ด๋ฉ”์ผ ์ธ์ฆ์„ ํ•„์ˆ˜๋กœ ์ ์šฉํ•œ๋‹ค.
ํšŒ์›๊ฐ€์ž… ์‹œ ์‚ฌ์šฉ์ž๋Š” 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์ด๋ฉ”์ผ๋กœ ์ˆ˜์‹ ํ•˜๊ณ , ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ž…๋ ฅํ•ด ๊ฒ€์ฆ์„ ํ†ต๊ณผํ•ด์•ผ ๊ฐ€์ž…์ด ์™„๋ฃŒ๋œ๋‹ค.
๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ถ„์‹คํ–ˆ์„ ๊ฒฝ์šฐ์—๋„ ๋™์ผํ•˜๊ฒŒ ์ด๋ฉ”์ผ ์ธ์ฆ ๋‹จ๊ณ„๋ฅผ ๊ฑฐ์ณ์•ผ ํ•œ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ์‹ค์ œ ์ ์šฉํ•œ ์ฝ”๋“œ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ด๋ฉ”์ผ ์ธ์ฆ ๊ตฌํ˜„(SendGrid) โ†’ ์ด๋ฉ”์ผ ๋ฐœ์†ก โ†’ Redis ๊ธฐ๋ฐ˜ OTP ๊ด€๋ฆฌ โ†’ ๊ฒ€์ฆ API๊นŒ์ง€ ํ๋ฆ„์„ ์ •๋ฆฌํ•œ๋‹ค.
๋˜ํ•œ ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ ๊ธฐ๋ฐ˜์œผ๋กœ Port / Adapter / Application / Domain ๋ ˆ์ด์–ด๋ฅผ ๋ช…ํ™•ํžˆ ๋ถ„๋ฆฌํ•œ๋‹ค.


1. ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”

[Web Controller] โ†’ [UseCase (SendEmailUseCase / VerifyEmailUseCase)]
        โ†“
[Application Service] (EmailCommandService / EmailVerifyService)
        โ†“
[Port Out] (SendEmailPort, VerifyEmailPort)
        โ†“
[Adapter] (SendGrid Adapter, Redis Adapter)
        โ†“
์™ธ๋ถ€ ์„œ๋น„์Šค (SendGrid / Redis)
  • Controller: HTTP ์š”์ฒญ ์ฒ˜๋ฆฌ (/email/send, /email/verify)
  • Application Service: ์ธ์ฆ๋ฒˆํ˜ธ ์ƒ์„ฑ, ์ „์†ก, ๊ฒ€์ฆ ๋กœ์ง ์ฒ˜๋ฆฌ
  • Port: ์™ธ๋ถ€ I/O ์ถ”์ƒํ™” (SendEmailPort, VerifyEmailPort)
  • Adapter: SendGrid API ํ˜ธ์ถœ, Redis ์ €์žฅ/์กฐํšŒ/์‚ญ์ œ

2. ์ธ์ฆ ์ฝ”๋“œ ๋ฐœ์†ก (SendEmailUseCase)

EmailCommandService๊ฐ€ ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  1. SecureRandom์œผ๋กœ 6์ž๋ฆฌ ์ˆซ์ž ์ฝ”๋“œ ์ƒ์„ฑํ•œ๋‹ค.
  2. EmailContentFactory๋กœ ์ด๋ฉ”์ผ ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์„ ์ƒ์„ฑํ•œ๋‹ค.
  3. SendEmailPort๋ฅผ ํ†ตํ•ด SendGrid API๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์ด๋ฉ”์ผ์„ ์ „์†กํ•œ๋‹ค.
  4. VerifyEmailPort.saveCode๋ฅผ ํ†ตํ•ด Redis์— ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ์ €์žฅํ•œ๋‹ค.
@Override
public void sendEmailVerificationCode(String email, EmailVerificationType type) {
    String code = generateCode(); // 6์ž๋ฆฌ ์ˆซ์ž
    EmailContent content = EmailContentFactory.generate(type, code);

    try {
        sendEmailPort.send(email, content.subject(), content.body());
    } catch (Exception e) {
        throw new EmailException(EmailErrorStatus.FAIL_SEND_EMAIL_CODE);
    }

    verifyEmailPort.saveCode(email, code, type);
}

3. ์ด๋ฉ”์ผ ๋ฐœ์†ก ์–ด๋Œ‘ํ„ฐ (SendGrid)

SendEmailSendGridAdapter๊ฐ€ SendGrid API๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

  • from, to, subject, body๋ฅผ ๊ตฌ์„ฑํ•ด ๋ฉ”์ผ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ ๋‹ค.
  • ์‹คํŒจ ์‹œ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ณ  ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธด๋‹ค.
@Override
public void send(String email, String subject, String body) {
    Email from = new Email(sender);
    Email to = new Email(email);
    Content content = new Content("text/plain", body);

    Mail mail = new Mail(from, subject, to, content);
    Request request = new Request();
    request.setMethod(Method.POST);
    request.setEndpoint("mail/send");
    request.setBody(mail.build());

    Response response = sendGrid.api(request);
    if (response.getStatusCode() >= 400) {
        throw new RuntimeException("SendGrid ์ „์†ก ์‹คํŒจ");
    }
}

4. Redis๋ฅผ ํ†ตํ•œ ์ธ์ฆ ์ฝ”๋“œ ์ €์žฅ

EmailRedisAdapter๊ฐ€ Redis์— ์ธ์ฆ ์ฝ”๋“œ๋ฅผ ์ €์žฅํ•œ๋‹ค.

  • Key ๊ตฌ์กฐ: email:verification:{purpose}:{email}
  • ๋งŒ๋ฃŒ ์‹œ๊ฐ„: ๊ธฐ๋ณธ 5๋ถ„ (TTL)
  • Redis ์žฅ์•  ์‹œ CommonException์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
@Override
public void saveCode(String email, String code, EmailVerificationType type) {
    String key = getEmailKey(email, type);
    redisTemplate.opsForValue().set(key, code, EXPIRE_MINUTES, TimeUnit.MINUTES);
}

5. ์ธ์ฆ ์ฝ”๋“œ ๊ฒ€์ฆ (VerifyEmailUseCase)

EmailVerifyService๊ฐ€ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  1. Redis์—์„œ ์ €์žฅ๋œ ์ฝ”๋“œ๋ฅผ ์กฐํšŒํ•œ๋‹ค.
  2. ์ž…๋ ฅ๊ฐ’๊ณผ ๋น„๊ตํ•œ๋‹ค.
    • ๋ถˆ์ผ์น˜ / ๋งŒ๋ฃŒ ์‹œ EmailException์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
  3. ์„ฑ๊ณต ์‹œ Redis์—์„œ ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
  4. ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ(PASSWORD_SEARCH) ๋ชฉ์ ์ผ ๊ฒฝ์šฐ ๋น„๋กœ๊ทธ์ธ ์œ ์ €์ด๊ธฐ ๋•Œ๋ฌธ์— email์„ ๋‹ด์€ JWT ๊ธฐ๋ฐ˜ Reset Token์„ ๋ฐœ๊ธ‰ํ•œ๋‹ค.
@Override
public GetResetTokenResponse verifyCode(String email, String code, EmailVerificationType type) {
    String savedCode = cacheEmailPort.verifyCode(email, code, type);
    if (savedCode == null) throw new EmailException(EmailErrorStatus.EXPIRED_EMAIL_CODE);
    if (!savedCode.equals(code)) throw new EmailException(EmailErrorStatus.FAIL_VERIFY_EMAIL_CODE);

    cacheEmailPort.deleteCode(email, type);

    if (type.equals(EmailVerificationType.PASSWORD_SEARCH)) {
        String resetToken = jwtGenerateUseCase.generateResetPasswordToken(email);
        cacheResetTokenUseCase.saveResetToken(resetToken);
        return new GetResetTokenResponse(resetToken);
    }
    return new GetResetTokenResponse(null);
}

6. ์ปจํŠธ๋กค๋Ÿฌ (API ๋ ˆ์ด์–ด)

EmailCommandController๋Š” API ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

  • ํšŒ์›๊ฐ€์ž… โ†’ OK_SEND_EMAIL_CODE_SIGN_UP
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ โ†’ OK_SEND_EMAIL_CODE_PASSWORD_SEARCH
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • โ†’ OK_SEND_EMAIL_CODE_PASSWORD_RESET

๊ฐ์ž ๋ชฉ์ ์— ๋”ฐ๋ผ ์ด๋ฉ”์ผ ์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ enum์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๋ชฉ์ ์— ๋งž๋Š” ์ด๋ฉ”์ผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ฒŒ ์„ค์ •ํ•˜์˜€๋‹ค.

@Override
public ResponseEntity<SuccessResponse<Void>> sendCode(SendEmailWebRequest webRequest) {
    EmailVerificationType type = EmailVerificationType.of(webRequest.purpose());
    sendEmailUseCase.sendEmailVerificationCode(webRequest.email(), type);

    return switch (type) {
        case SIGN_UP -> ok(EmailSuccessStatus.OK_SEND_EMAIL_CODE_SIGN_UP);
        case PASSWORD_SEARCH -> ok(EmailSuccessStatus.OK_SEND_EMAIL_CODE_PASSWORD_SEARCH);
        case PASSWORD_RESET -> ok(EmailSuccessStatus.OK_SEND_EMAIL_CODE_PASSWORD_RESET);
    };
}

7. ํ•ต์‹ฌ ํฌ์ธํŠธ

  • ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ ์ ์šฉ
    ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ์™ธ๋ถ€ ์˜์กด์„ฑ(SendGrid, Redis)์„ ๋ถ„๋ฆฌํ•œ๋‹ค.

  • ๋ณด์•ˆ ๊ณ ๋ ค
    ์ธ์ฆ๋ฒˆํ˜ธ๋Š” 6์ž๋ฆฌ ์ˆซ์ž๋กœ ์ƒ์„ฑํ•˜๋ฉฐ TTL 5๋ถ„์„ ๋‘๊ณ  Redis์— ์ €์žฅํ•œ๋‹ค.
    ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ ์‹œ JWT Reset Token์„ ์ถ”๊ฐ€ ๋ฐœ๊ธ‰ํ•œ๋‹ค.

  • ๋กœ๊ทธ ์ฒด๊ณ„
    LoggerFactory ๊ธฐ๋ฐ˜์œผ๋กœ API / Service / Redis / SendGrid ํ๋ฆ„์„ ๊ตฌ๋ถ„ํ•ด ๊ธฐ๋กํ•œ๋‹ค.


๐ŸŽฏ ๊ฒฐ๋ก 

Dataracy ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ํšŒ์›๊ฐ€์ž… / ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ / ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ๊ณผ์ •์— ์ด๋ฉ”์ผ ์ธ์ฆ์„ ์ ์šฉํ•˜์—ฌ ๊ณ„์ • ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•œ๋‹ค.

  • SendGrid๋กœ ์•ˆ์ •์ ์ธ ์ด๋ฉ”์ผ ๋ฐœ์†ก์„ ๊ตฌํ˜„ํ•œ๋‹ค.
  • Redis๋กœ OTP ์ €์žฅยท๊ฒ€์ฆยท์‚ญ์ œ๋ฅผ ๊ด€๋ฆฌํ•œ๋‹ค.
  • JWT Reset Token์œผ๋กœ ๋น„๋ฐ€๋ฒˆํ˜ธ ์žฌ์„ค์ • ํ”Œ๋กœ์šฐ๋ฅผ ์™„์„ฑํ•œ๋‹ค.

โœ… ์ด ๊ตฌ์กฐ๋ฅผ ํ†ตํ•ด ์ŠคํŒธ๋ฅ ์„ ์ค„์ด๊ณ , ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๋ฉฐ, ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค. ๋˜ํ•œ, ํ•œ API ๊ธฐ๋Šฅ์œผ๋กœ ์—ฌ๋Ÿฌ ๋ชฉ์ ์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ด๋ฉ”์ผ์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๊ธฐ์— ํšจ์œจ์ด ์ข‹๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ ๋‹ค.

profile
๋งค์ผ ๋งค์ผ ์„ฑ์žฅํ•˜๊ธฐ

0๊ฐœ์˜ ๋Œ“๊ธ€