Skip to content

🔀 Email Send#27

Merged
ohyuchan123 merged 17 commits intodevelopfrom
refactor/email-send
Jan 8, 2025
Merged

🔀 Email Send#27
ohyuchan123 merged 17 commits intodevelopfrom
refactor/email-send

Conversation

@ohyuchan123
Copy link
Member

@ohyuchan123 ohyuchan123 commented Jan 7, 2025

📌 관련 이슈

#23 이메일 인증 기능

✨ 과제 내용

이메일 인증 번호를 통해 이메일을 인증하는 기능을 구현하였습니다.

📸 스크린샷(선택)

image image

Summary by CodeRabbit

Summary by CodeRabbit

  • 새로운 기능

    • 이메일 인증 기능 추가
    • 이메일로 6자리 인증 번호 발송 기능 구현
    • 이메일 인증 번호 확인 엔드포인트 추가
    • 요청 빈도를 제한하는 레이트 리미터 기능 추가
  • 종속성 변경

    • Spring Boot Mail Starter 의존성 추가
  • 서비스 개선

    • 사용자 인증 프로세스에 이메일 인증 단계 도입
    • 안전한 사용자 등록을 위한 검증 메커니즘 강화
    • 요청 빈도 관리 기능 강화
    • 스케줄링 지원 추가
  • 데이터 모델 추가

    • 인증 코드 및 만료 시간을 포함하는 VerificationData 레코드 추가
    • 이메일 요청 및 검증 요청을 위한 DTO 추가
    • 이메일 인증 응답 DTO 추가
  • 예외 처리 개선

    • 공통 예외 처리 메서드 추가 및 기존 메서드 수정
  • 응답 처리 개선

    • 응답 처리 로직 간소화

@ohyuchan123 ohyuchan123 added 🔨 Refactor 코드 리팩토링 ✨ Feature 기능 개발 labels Jan 7, 2025
@ohyuchan123 ohyuchan123 self-assigned this Jan 7, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2025

Walkthrough

이 변경 사항은 Spring Boot 프로젝트에 이메일 인증 기능을 추가합니다. build.gradle에 Spring Boot Mail Starter 의존성을 추가하고, MailService를 통해 이메일 인증 번호를 생성하고 전송하는 새로운 서비스를 구현했습니다. AuthController에는 이메일 발송과 인증 번호 확인을 위한 새로운 POST 엔드포인트가 도입되었습니다. 또한, 요청 빈도를 제한하기 위한 RateLimiter 클래스가 추가되었습니다. GoSocketBeApplication 클래스에 스케줄링 지원을 활성화하는 주석이 추가되었습니다.

Changes

파일 변경 내용
build.gradle Spring Boot Mail Starter 의존성 추가
AuthController.java - MailServiceRateLimiter 필드 추가
- /verification POST 엔드포인트 생성
- /email/verify POST 엔드포인트 생성
MailService.java - 이메일 인증 번호 생성 및 발송 기능 구현
- 랜덤 6자리 인증 번호 생성 메서드 추가
- 이메일 작성 및 전송 메서드 구현
- 인증 번호 확인 메서드 추가
RateLimiter.java - 요청 빈도 제한 기능 구현
- 요청 제한 확인 메서드 추가
AuthService.java 이메일 관련 주석 제거
GoSocketBeApplication.java 스케줄링 지원 활성화 주석 추가
VerificationData.java 인증 코드 및 만료 시간을 포함하는 새로운 레코드 추가
EmailRequestDto.java 이메일 요청을 위한 새로운 레코드 추가
EmailVerificationRequestDto.java 이메일 인증 요청을 위한 새로운 레코드 추가
EmailVerificationCheckResponseDto.java 이메일 인증 확인 응답을 위한 새로운 레코드 추가
EmailVerificationResponseDto.java 이메일 인증 응답을 위한 새로운 레코드 추가
ApiResponse.java 성공 상태를 나타내는 새로운 필드 추가 및 메서드 수정
GlobalExceptionHandler.java 새로운 예외 처리 메서드 추가 및 기존 메서드 이름 변경
ResponseHandler.java 응답 처리 로직 수정

Sequence Diagram

sequenceDiagram
    participant Client
    participant AuthController
    participant MailService
    participant JavaMailSender
    participant RateLimiter

    Client->>AuthController: Request email verification
    AuthController->>RateLimiter: checkRateLimit(email)
    RateLimiter-->>AuthController: Rate limit check
    AuthController->>MailService: sendMail(email)
    MailService->>MailService: createVerificationCode()
    MailService->>JavaMailSender: Send verification email
    MailService-->>AuthController: Return verification code
    AuthController-->>Client: Return verification code
    Client->>AuthController: Verify code
    AuthController->>MailService: verifyCode(email, code)
    MailService-->>AuthController: Return verification result
    AuthController-->>Client: Return verification result
Loading

Possibly related PRs

  • 🔀 Member register #21: 이 PR은 build.gradle에 이메일 기능을 위한 새로운 의존성을 추가하며, 사용자 등록 및 이메일 인증 프로세스와 관련된 RegisterControllerAuthController 변경과 관련이 있습니다.
  • 🔀 Jwt Login #24: 이 PR의 Spring Boot Mail Starter 추가는 이메일 처리와 관련된 인증 프로세스 개선을 포함하여 JWT 로그인 기능과 관련이 있습니다.

Suggested labels

✅ Test

Poem

🐰 코드 토끼의 이메일 마법 🌈
랜덤 숫자 춤추고
메일 날개 펼치니
인증의 문이 열리네
보안은 토끼의 미소 🐇


🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
build.gradle (1)

62-62: 메일 의존성 버전 관리 개선 필요

Spring Boot Mail Starter의 버전이 명시되어 있지 않습니다. 의존성 관리를 위해 다음과 같이 수정하는 것을 권장합니다:

-    implementation 'org.springframework.boot:spring-boot-starter-mail'
+    implementation 'org.springframework.boot:spring-boot-starter-mail:3.3.4'
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32785e8 and 8a35e5b.

📒 Files selected for processing (4)
  • build.gradle (1 hunks)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (2 hunks)
  • src/main/java/com/mycom/socket/auth/service/AuthService.java (0 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/com/mycom/socket/auth/service/AuthService.java

Comment on lines 22 to 40
public MimeMessage CreateMail(String mail) {
createNumber();
MimeMessage message = javaMailSender.createMimeMessage();

try {
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, mail);
message.setSubject("이메일 인증");
String body = "";
body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
body += "<h1>" + number + "</h1>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
} catch (MessagingException e) {
e.printStackTrace();
}

return message;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 생성 로직 개선 필요

다음과 같은 개선이 필요합니다:

  1. 예외 처리가 미흡합니다.
  2. 이메일 주소 유효성 검증이 없습니다.
  3. HTML 템플릿 분리가 필요합니다.
     public MimeMessage CreateMail(String mail) {
+        if (!isValidEmail(mail)) {
+            throw new InvalidEmailException("유효하지 않은 이메일 주소입니다.");
+        }
-        createNumber();
+        int verificationCode = createVerificationCode();
+        verificationCodes.put(mail, verificationCode);
         MimeMessage message = javaMailSender.createMimeMessage();
         try {
             message.setFrom(senderEmail);
             message.setRecipients(MimeMessage.RecipientType.TO, mail);
             message.setSubject("이메일 인증");
-            String body = "";
-            body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
-            body += "<h1>" + number + "</h1>";
-            body += "<h3>" + "감사합니다." + "</h3>";
+            String body = emailTemplateService.getVerificationEmailTemplate(verificationCode);
             message.setText(body,"UTF-8", "html");
         } catch (MessagingException e) {
-            e.printStackTrace();
+            throw new EmailSendException("이메일 생성 중 오류가 발생했습니다.", e);
         }
         return message;
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 17 to 20
// 랜덤으로 숫자 생성
public static void createNumber() {
number = (int)(Math.random() * (90000)) + 100000; //(int) Math.random() * (최댓값-최소값+1) + 최소값
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

인증 번호 생성 로직 개선 필요

인증 번호 생성 메서드에 다음과 같은 문제가 있습니다:

  1. 정적 메서드로 구현되어 테스트가 어렵습니다.
  2. 보안성이 낮은 난수 생성 방식을 사용합니다.

다음과 같이 개선하는 것을 권장합니다:

-    public static void createNumber() {
-        number = (int)(Math.random() * (90000)) + 100000;
+    private int createVerificationCode() {
+        SecureRandom secureRandom = new SecureRandom();
+        return 100000 + secureRandom.nextInt(900000);

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 14 to 15
private static final String senderEmail= "oyuchan50@gmail.com";
private static int number;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

정적 변수 사용 및 이메일 주소 하드코딩 문제

다음과 같은 심각한 문제점들이 있습니다:

  1. number를 정적 변수로 사용하면 동시성 문제가 발생할 수 있습니다.
  2. 이메일 주소가 코드에 하드코딩되어 있습니다.

다음과 같이 수정하는 것을 권장합니다:

-    private static final String senderEmail= "oyuchan50@gmail.com";
-    private static int number;
+    @Value("${mail.sender.address}")
+    private String senderEmail;
+    private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>();

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 42 to 47
public int sendMail(String mail) {
MimeMessage message = CreateMail(mail);
javaMailSender.send(message);

return number;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 전송 메서드 개선 필요

다음과 같은 보안 및 안정성 개선이 필요합니다:

  1. 속도 제한(rate limiting) 구현이 필요합니다.
  2. 이메일 전송 실패 시 예외 처리가 미흡합니다.
     public int sendMail(String mail) {
+        rateLimiter.checkRateLimit(mail);
         MimeMessage message = CreateMail(mail);
-        javaMailSender.send(message);
+        try {
+            javaMailSender.send(message);
+            return verificationCodes.get(mail);
+        } catch (MailException e) {
+            throw new EmailSendException("이메일 전송 중 오류가 발생했습니다.", e);
+        }
-        return number;
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 19 to 20
private final MailService mailService;
private int number;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

컨트롤러의 상태 저장 방식 개선 필요

인증 번호를 컨트롤러 인스턴스 변수로 저장하는 것은 다음과 같은 문제가 있습니다:

  1. 여러 사용자의 인증 요청을 처리할 수 없습니다.
  2. 서버가 여러 인스턴스로 실행될 경우 동작하지 않습니다.

인증 번호 저장소를 서비스 계층으로 이동하고 Redis와 같은 분산 캐시를 사용하는 것을 권장합니다.

-    private int number;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final MailService mailService;
private int number;
private final MailService mailService;

Comment on lines 48 to 51
@GetMapping("/mail-check")
public Boolean mailCheck(@RequestParam(name = "userNumber") String userNumber) {
return userNumber.equals(String.valueOf(number));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

인증 번호 확인 엔드포인트 개선 필요

다음과 같은 보안 및 기능 개선이 필요합니다:

  1. 응답 형식이 표준화되어 있지 않습니다.
  2. 인증 시도 횟수 제한이 없습니다.
  3. 인증 만료 시간 설정이 없습니다.
     @GetMapping("/mail-check")
-    public Boolean mailCheck(@RequestParam(name = "userNumber") String userNumber) {
-        return userNumber.equals(String.valueOf(number));
+    public ApiResponse<VerificationResult> verifyCode(
+            @RequestParam(name = "mail") @Email String mail,
+            @RequestParam(name = "code") @Pattern(regexp = "^[0-9]{6}$") String code) {
+        boolean isValid = mailService.verifyCode(mail, code);
+        return ApiResponse.success(new VerificationResult(isValid));
     }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 38 to 46
@PostMapping("/mail-send")
public Integer mailSend(@RequestParam(name = "mail") String mail) {
try {
number = mailService.sendMail(mail);
return number;
} catch (Exception e) {
throw new RuntimeException("Failed to send mail: " + e.getMessage());
}
}
Copy link

@coderabbitai coderabbitai bot Jan 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 전송 엔드포인트 개선 필요

다음과 같은 개선이 필요합니다:

  1. 응답 형식이 표준화되어 있지 않습니다.
  2. 입력값 검증이 부족합니다.
  3. 예외 처리가 구체적이지 않습니다.
     @PostMapping("/mail-send")
-    public Integer mailSend(@RequestParam(name = "mail") String mail) {
+    public ApiResponse<VerificationResponse> mailSend(
+            @RequestParam(name = "mail") @Email(message = "유효한 이메일 주소를 입력해주세요") String mail) {
         try {
-            number = mailService.sendMail(mail);
-            return number;
+            int verificationCode = mailService.sendMail(mail);
+            return ApiResponse.success(new VerificationResponse(verificationCode));
         } catch (Exception e) {
-            throw new RuntimeException("Failed to send mail: " + e.getMessage());
+            throw new EmailServiceException("이메일 전송에 실패했습니다", e);
         }
     }

Committable suggestion skipped: line range outside the PR's diff.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1)

22-34: 예외 처리 개선 필요

현재 구현은 일반적인 BaseException을 사용하고 있습니다. 더 구체적인 예외 클래스를 사용하면 클라이언트가 더 적절하게 대응할 수 있습니다.

다음과 같은 개선을 제안합니다:

+public class RateLimitExceededException extends BaseException {
+    public RateLimitExceededException(String message) {
+        super(message, HttpStatus.TOO_MANY_REQUESTS);
+    }
+}

 if (requests.size() >= MAX_REQUESTS) {
-    throw new BaseException("너무 많은 요청입니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
+    throw new RateLimitExceededException("너무 많은 요청입니다. 잠시 후 다시 시도해주세요.");
 }
src/main/java/com/mycom/socket/auth/service/MailService.java (1)

44-49: 이메일 템플릿 외부화 필요

HTML 이메일 템플릿이 코드에 하드코딩되어 있습니다. 유지보수와 다국어 지원을 위해 외부 파일로 분리해야 합니다.

resources/templates/email/verification.html 파일을 생성하고 Thymeleaf 템플릿 엔진을 사용하는 것을 제안합니다:

-String body = String.format("""
-    <h3>요청하신 인증 번호입니다.</h3>
-    <h1>%d</h1>
-    <h3>감사합니다.</h3>
-    """, verificationCode);
-message.setText(body, "UTF-8", "html");
+Context context = new Context();
+context.setVariable("code", verificationCode);
+String body = templateEngine.process("email/verification", context);
+message.setText(body, "UTF-8", "html");
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8a35e5b and 92944df.

📒 Files selected for processing (3)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (2 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1 hunks)
🔇 Additional comments (2)
src/main/java/com/mycom/socket/auth/controller/AuthController.java (2)

41-49: 🛠️ Refactor suggestion

입력 유효성 검증 및 예외 처리 개선 필요

이메일 검증 엔드포인트에 다음과 같은 개선이 필요합니다:

  1. 이메일 주소 형식 검증
  2. 구체적인 예외 처리

다음과 같은 개선을 제안합니다:

 @PostMapping("/email/verification")
-public Integer mailSend(@RequestParam(name = "mail") String mail) {
+public Integer mailSend(
+    @RequestParam(name = "mail") 
+    @Email(message = "유효한 이메일 주소를 입력해주세요") 
+    String mail) {
     try {
         rateLimiter.checkRateLimit(mail);  // 요청 제한 체크
         return mailService.sendMail(mail);
-    } catch (Exception e) {
-        throw new BaseException("이메일 전송에 실패했습니다.", HttpStatus.BAD_REQUEST);
+    } catch (RateLimitExceededException e) {
+        throw e;
+    } catch (MessagingException e) {
+        throw new BaseException("이메일 전송에 실패했습니다: " + e.getMessage(), 
+            HttpStatus.BAD_REQUEST);
     }
 }

Likely invalid or redundant comment.


51-59: 🛠️ Refactor suggestion

인증 코드 검증 로직 개선 필요

인증 코드 검증 엔드포인트에 다음과 같은 개선이 필요합니다:

  1. 인증 코드 형식 검증
  2. 응답 형식 표준화

다음과 같은 개선을 제안합니다:

 @GetMapping("/email/verify")
-public Boolean mailCheck(@RequestParam(name = "mail") String mail,
-                          @RequestParam(name = "code") String code) {
+public ApiResponse<VerificationResult> mailCheck(
+    @RequestParam(name = "mail") @Email String mail,
+    @RequestParam(name = "code") @Pattern(regexp = "^[0-9]{6}$") String code) {
     try {
-        return mailService.verifyCode(mail, code);
+        boolean isValid = mailService.verifyCode(mail, code);
+        return ApiResponse.success(new VerificationResult(isValid));
     } catch (Exception e) {
         throw new BaseException("인증코드 검증에 실패했습니다.", HttpStatus.BAD_REQUEST);
     }
 }

Likely invalid or redundant comment.

Comment on lines +18 to +20
private final Map<String, List<LocalDateTime>> requestMap = new ConcurrentHashMap<>();
private static final int MAX_REQUESTS = 3; // 1분당 최대 3번
private static final Duration WINDOW_SIZE = Duration.ofMinutes(1); // 1분의 시간 간격
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

메모리 누수 가능성 개선 필요

requestMap에 저장된 이메일 주소와 요청 시간이 지속적으로 누적되어 메모리 누수가 발생할 수 있습니다. 주기적으로 오래된 데이터를 정리하는 메커니즘이 필요합니다.

다음과 같은 개선을 제안합니다:

 private final Map<String, List<LocalDateTime>> requestMap = new ConcurrentHashMap<>();
 private static final int MAX_REQUESTS = 3;  // 1분당 최대 3번
 private static final Duration WINDOW_SIZE = Duration.ofMinutes(1);  // 1분의 시간 간격
+private static final Duration CLEANUP_INTERVAL = Duration.ofHours(1);
+
+@Scheduled(fixedRate = 3600000) // 1시간마다 실행
+public void cleanup() {
+    LocalDateTime threshold = LocalDateTime.now().minus(WINDOW_SIZE);
+    requestMap.entrySet().removeIf(entry ->
+        entry.getValue().stream().allMatch(time -> time.isBefore(threshold)));
+}

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines 62 to 65
public boolean verifyCode(String email, String code) {
Integer savedCode = verificationCodes.get(email);
return savedCode != null && String.valueOf(savedCode).equals(code);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

인증 코드 검증 로직 개선 필요

현재 구현에서는 인증 코드 만료 여부를 확인하지 않습니다. 또한, 검증 후에도 코드가 계속 유효한 상태로 남아있습니다.

다음과 같은 개선을 제안합니다:

 public boolean verifyCode(String email, String code) {
-    Integer savedCode = verificationCodes.get(email);
-    return savedCode != null && String.valueOf(savedCode).equals(code);
+    VerificationData data = verificationCodes.get(email);
+    if (data == null || LocalDateTime.now().isAfter(data.expiryTime)) {
+        return false;
+    }
+    boolean isValid = String.valueOf(data.code).equals(code);
+    if (isValid) {
+        verificationCodes.remove(email);  // 사용된 코드 제거
+    }
+    return isValid;
 }

Committable suggestion skipped: line range outside the PR's diff.

public class MailService {

private final JavaMailSender javaMailSender;
private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

인증 코드 만료 시간 설정 필요

현재 구현에서는 인증 코드가 만료되지 않습니다. 보안을 위해 만료 시간을 설정해야 합니다.

다음과 같은 개선을 제안합니다:

-private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>();
+private final Map<String, VerificationData> verificationCodes = new ConcurrentHashMap<>();
+
+@Value("${verification.code.expiry-minutes:5}")
+private int codeExpiryMinutes;
+
+private static class VerificationData {
+    private final int code;
+    private final LocalDateTime expiryTime;
+    
+    public VerificationData(int code, LocalDateTime expiryTime) {
+        this.code = code;
+        this.expiryTime = expiryTime;
+    }
+}

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1)

23-28: cleanup 메서드의 실행 주기 설정 개선 필요

현재 하드코딩된 실행 주기(3600000)를 설정 파일로 분리하는 것이 좋습니다.

-    @Scheduled(fixedRate = 3600000)  // 1시간마다 실행
+    @Value("${rate.limiter.cleanup.interval:3600000}")
+    private long cleanupInterval;
+    @Scheduled(fixedRateString = "${rate.limiter.cleanup.interval:3600000}")
     public void cleanup() {
src/main/java/com/mycom/socket/auth/service/MailService.java (1)

37-42: 난수 생성 최적화 필요

SecureRandom 인스턴스를 매번 생성하지 말고 클래스 필드로 선언하여 재사용하는 것이 좋습니다.

+    private final SecureRandom secureRandom = new SecureRandom();
+
     private int createVerificationCode() {
-        SecureRandom secureRandom = new SecureRandom();
         return 100000 + secureRandom.nextInt(900000);
     }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92944df and d6f20ad.

📒 Files selected for processing (3)
  • src/main/java/com/mycom/socket/GoSocketBeApplication.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1 hunks)
🔇 Additional comments (4)
src/main/java/com/mycom/socket/GoSocketBeApplication.java (1)

7-8: 스케줄링 기능 활성화 적절히 구현됨

RateLimiter의 cleanup 작업을 위한 @EnableScheduling 어노테이션이 올바르게 추가되었습니다.

src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1)

19-21: 동시성 처리가 잘 구현되었으나 메모리 관리 개선 필요

ConcurrentHashMap을 사용한 동시성 처리는 적절하나, 이전 리뷰에서 지적된 메모리 누수 문제가 여전히 존재합니다.

src/main/java/com/mycom/socket/auth/service/MailService.java (2)

92-112: 🛠️ Refactor suggestion

검증 로직 최적화 필요

검증 로직이 두 개의 Map을 조회하고 있어 비효율적입니다. 제안된 VerificationData 구조를 사용하여 개선해야 합니다.

     public boolean verifyCode(String email, String code) {
-        Integer savedCode = verificationCodes.get(email);
-        LocalDateTime expiryTime = expiryTimes.get(email);
+        VerificationData data = verificationData.get(email);
 
-        if (savedCode == null ||
-                expiryTime == null ||
-                LocalDateTime.now().isAfter(expiryTime)) {
+        if (data == null || LocalDateTime.now().isAfter(data.expiryTime())) {
             return false;
         }
 
-        boolean isValid = String.valueOf(savedCode).equals(code);
+        boolean isValid = String.valueOf(data.code()).equals(code);
         if (isValid) {
-            verificationCodes.remove(email);
-            expiryTimes.remove(email);
+            verificationData.remove(email);
         }
         return isValid;
     }

Likely invalid or redundant comment.


75-84: ⚠️ Potential issue

이메일 전송 실패 처리 누락

JavaMailSender.send() 메서드의 예외 처리가 필요합니다.

     public int sendMail(String mail) {
         rateLimiter.checkRateLimit(mail);
         MimeMessage message = createMail(mail);
-        javaMailSender.send(message);
+        try {
+            javaMailSender.send(message);
+        } catch (MailException e) {
+            throw new BaseException("이메일 전송 중 오류가 발생했습니다: " + e.getMessage(),
+                HttpStatus.INTERNAL_SERVER_ERROR, e);
+        }

Likely invalid or redundant comment.

Comment on lines 30 to 42
public void checkRateLimit(String email) {
List<LocalDateTime> requests = requestMap.computeIfAbsent(email, k -> new ArrayList<>());
LocalDateTime now = LocalDateTime.now();

requests.removeIf(requestTime ->
requestTime.plus(WINDOW_SIZE).isBefore(now));

if (requests.size() >= MAX_REQUESTS) {
throw new BaseException("너무 많은 요청입니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
}

requests.add(now);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

요청 제한 로직 보완 필요

다음 사항들을 고려해야 합니다:

  1. 예외 메시지 국제화
  2. 남은 대기 시간 정보 제공
     if (requests.size() >= MAX_REQUESTS) {
-        throw new BaseException("너무 많은 요청입니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
+        LocalDateTime oldestRequest = requests.get(0);
+        Duration waitTime = WINDOW_SIZE.minus(Duration.between(oldestRequest, now));
+        throw new BaseException(
+            String.format("너무 많은 요청입니다. %d초 후에 다시 시도해주세요.", 
+                waitTime.getSeconds()),
+            HttpStatus.TOO_MANY_REQUESTS);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void checkRateLimit(String email) {
List<LocalDateTime> requests = requestMap.computeIfAbsent(email, k -> new ArrayList<>());
LocalDateTime now = LocalDateTime.now();
requests.removeIf(requestTime ->
requestTime.plus(WINDOW_SIZE).isBefore(now));
if (requests.size() >= MAX_REQUESTS) {
throw new BaseException("너무 많은 요청입니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
}
requests.add(now);
}
public void checkRateLimit(String email) {
List<LocalDateTime> requests = requestMap.computeIfAbsent(email, k -> new ArrayList<>());
LocalDateTime now = LocalDateTime.now();
requests.removeIf(requestTime ->
requestTime.plus(WINDOW_SIZE).isBefore(now));
if (requests.size() >= MAX_REQUESTS) {
LocalDateTime oldestRequest = requests.get(0);
Duration waitTime = WINDOW_SIZE.minus(Duration.between(oldestRequest, now));
throw new BaseException(
String.format("너무 많은 요청입니다. %d초 후에 다시 시도해주세요.",
waitTime.getSeconds()),
HttpStatus.TOO_MANY_REQUESTS);
}
requests.add(now);
}

Comment on lines 64 to 66
} catch (MessagingException e) {
throw new BaseException("이메일 생성 중 오류가 발생했습니다.", HttpStatus.BAD_REQUEST);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리 개선 필요

원인 예외를 포함하여 더 자세한 디버깅이 가능하도록 해야 합니다.

-            throw new BaseException("이메일 생성 중 오류가 발생했습니다.", HttpStatus.BAD_REQUEST);
+            throw new BaseException("이메일 생성 중 오류가 발생했습니다: " + e.getMessage(), 
+                HttpStatus.BAD_REQUEST, e);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (MessagingException e) {
throw new BaseException("이메일 생성 중 오류가 발생했습니다.", HttpStatus.BAD_REQUEST);
}
} catch (MessagingException e) {
throw new BaseException("이메일 생성 중 오류가 발생했습니다: " + e.getMessage(),
HttpStatus.BAD_REQUEST, e);
}

Comment on lines 22 to 30
private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // 인증 번호 요청 제한
private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>(); // 이메일별 인증번호 저장
private final Map<String, LocalDateTime> expiryTimes = new ConcurrentHashMap<>(); // 이메일별 인증번호 만료 시간 저장

private static final Duration CODE_VALID_DURATION = Duration.ofMinutes(5);

@Value("${spring.mail.username}")
private String senderEmail;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

필드 구조 개선 필요

두 개의 Map을 사용하는 대신 단일 Map과 복합 객체를 사용하는 것이 더 효율적입니다.

-    private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>();
-    private final Map<String, LocalDateTime> expiryTimes = new ConcurrentHashMap<>();
+    private final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
+
+    @Value("${mail.verification.expiry-minutes:5}")
+    private int expiryMinutes;
+
+    private record VerificationData(
+        int code,
+        LocalDateTime expiryTime
+    ) {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // 인증 번호 요청 제한
private final Map<String, Integer> verificationCodes = new ConcurrentHashMap<>(); // 이메일별 인증번호 저장
private final Map<String, LocalDateTime> expiryTimes = new ConcurrentHashMap<>(); // 이메일별 인증번호 만료 시간 저장
private static final Duration CODE_VALID_DURATION = Duration.ofMinutes(5);
@Value("${spring.mail.username}")
private String senderEmail;
private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // 인증 번호 요청 제한
private final Map<String, VerificationData> verificationData = new ConcurrentHashMap<>();
@Value("${mail.verification.expiry-minutes:5}")
private int expiryMinutes;
private record VerificationData(
int code,
LocalDateTime expiryTime
) {}
@Value("${spring.mail.username}")
private String senderEmail;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/com/mycom/socket/auth/service/MailService.java (1)

61-66: HTML 템플릿 분리 필요

HTML 내용이 코드에 직접 포함되어 있어 유지보수가 어렵습니다. 별도의 템플릿 파일로 분리하는 것이 좋습니다.

Thymeleaf나 다른 템플릿 엔진을 사용하여 HTML 내용을 분리하는 것을 권장합니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6f20ad and b40a9e5.

📒 Files selected for processing (3)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/data/VerificationData.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/com/mycom/socket/auth/service/data/VerificationData.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java
🔇 Additional comments (3)
src/main/java/com/mycom/socket/auth/service/MailService.java (3)

37-42: 구현이 적절합니다!

SecureRandom을 사용한 안전한 난수 생성과 명확한 주석 처리가 잘 되어있습니다.


67-70: 🛠️ Refactor suggestion

예외 처리 개선 필요

BaseException에 원인 예외를 포함시키지 않아 디버깅이 어려울 수 있습니다.

다음과 같이 수정하는 것을 권장합니다:

-            throw new BaseException("이메일 생성 중 오류가 발생했습니다: " + e.getMessage(),
-                    HttpStatus.BAD_REQUEST);
+            throw new BaseException("이메일 생성 중 오류가 발생했습니다: " + e.getMessage(),
+                    HttpStatus.BAD_REQUEST, e);

Likely invalid or redundant comment.


79-85: ⚠️ Potential issue

메일 전송 예외 처리 필요

JavaMailSender.send() 메서드의 예외 처리가 누락되었습니다. 메일 서버 연결 실패 등의 상황에 대한 처리가 필요합니다.

다음과 같이 수정하는 것을 권장합니다:

     public int sendMail(String mail) {
         rateLimiter.checkRateLimit(mail);
         MimeMessage message = createMail(mail);
-        javaMailSender.send(message);
+        try {
+            javaMailSender.send(message);
+        } catch (MailException e) {
+            throw new BaseException("이메일 전송 중 오류가 발생했습니다", 
+                HttpStatus.INTERNAL_SERVER_ERROR, e);
+        }
         return verificationDataMap.get(mail).code();
     }

Likely invalid or redundant comment.

Comment on lines 23 to 30
private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // 인증 번호 요청 제한

private final Map<String, VerificationData> verificationDataMap = new ConcurrentHashMap<>();
private static final Duration CODE_VALID_DURATION = Duration.ofMinutes(5);

@Value("${spring.mail.username}")
private String senderEmail;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

이메일 유효성 검증 로직 추가 필요

이메일 주소의 유효성을 검증하는 로직이 없습니다. 잘못된 이메일 주소로 인한 오류를 방지하기 위해 유효성 검증이 필요합니다.

다음과 같은 유틸리티 메서드를 추가하는 것을 권장합니다:

private boolean isValidEmail(String email) {
    String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
    return email != null && email.matches(emailRegex);
}

Comment on lines 93 to 105
public boolean verifyCode(String email, String code) {
VerificationData data = verificationDataMap.get(email);

if (data == null || LocalDateTime.now().isAfter(data.expiryTime())) {
return false;
}

boolean isValid = String.valueOf(data.code()).equals(code);
if (isValid) {
verificationDataMap.remove(email);
}
return isValid;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

입력값 검증 강화 필요

code 파라미터에 대한 null 체크가 누락되었습니다. 또한 코드 형식(숫자 6자리)에 대한 검증도 필요합니다.

다음과 같이 수정하는 것을 권장합니다:

     public boolean verifyCode(String email, String code) {
+        if (code == null || !code.matches("\\d{6}")) {
+            return false;
+        }
         VerificationData data = verificationDataMap.get(email);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public boolean verifyCode(String email, String code) {
VerificationData data = verificationDataMap.get(email);
if (data == null || LocalDateTime.now().isAfter(data.expiryTime())) {
return false;
}
boolean isValid = String.valueOf(data.code()).equals(code);
if (isValid) {
verificationDataMap.remove(email);
}
return isValid;
public boolean verifyCode(String email, String code) {
if (code == null || !code.matches("\\d{6}")) {
return false;
}
VerificationData data = verificationDataMap.get(email);
if (data == null || LocalDateTime.now().isAfter(data.expiryTime())) {
return false;
}
boolean isValid = String.valueOf(data.code()).equals(code);
if (isValid) {
verificationDataMap.remove(email);
}
return isValid;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/main/java/com/mycom/socket/auth/service/data/VerificationData.java (1)

9-9: 만료 시간을 구성 가능하도록 개선 필요

만료 시간이 하드코딩되어 있습니다. 환경에 따라 유연하게 조정할 수 있도록 구성 파일에서 설정할 수 있게 개선하는 것이 좋습니다.

-    private static final Duration CODE_VALID_DURATION = Duration.ofMinutes(5);
+    @Value("${verification.code.expiry-minutes:5}")
+    private static final int CODE_EXPIRY_MINUTES;
+    private static final Duration CODE_VALID_DURATION = Duration.ofMinutes(CODE_EXPIRY_MINUTES);
src/main/java/com/mycom/socket/auth/service/MailService.java (3)

55-60: 이메일 템플릿 외부화 필요

HTML 템플릿이 코드에 하드코딩되어 있습니다. 다음과 같은 개선이 필요합니다:

  1. 템플릿을 외부 파일로 분리
  2. 다국어 지원을 위한 메시지 외부화
-            String body = String.format("""
-                   <h3>요청하신 인증 번호입니다.</h3>
-                   <h1>%s</h1>
-                   <h3>감사합니다.</h3>
-                   """, verificationCode);
-            message.setText(body, "UTF-8", "html");
+            Context context = new Context();
+            context.setVariable("code", verificationCode);
+            String body = templateEngine.process("mail/verification-template", context);
+            message.setText(body, "UTF-8", "html");

49-66: MimeMessage 생성 로직 개선 필요

MimeMessage 생성 로직을 빌더 패턴을 사용하여 개선하면 좋을 것 같습니다:

  1. 가독성 향상
  2. 유지보수성 개선
  3. 테스트 용이성 향상
+    @Builder
+    private static class EmailBuilder {
+        private String from;
+        private String to;
+        private String subject;
+        private String content;
+        
+        public MimeMessage build(JavaMailSender mailSender) throws MessagingException {
+            MimeMessage message = mailSender.createMimeMessage();
+            message.setFrom(from);
+            message.setRecipients(MimeMessage.RecipientType.TO, to);
+            message.setSubject(subject);
+            message.setText(content, "UTF-8", "html");
+            return message;
+        }
+    }

83-99: 이메일 발송 실패 시 재시도 로직 추가 고려

현재 구현에서는 이메일 발송 실패 시 즉시 예외를 던지고 있습니다. 다음과 같은 개선을 고려해보세요:

  1. 재시도 로직 추가
  2. 실패 로그 기록
  3. 비동기 처리 고려
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b40a9e5 and 0bb17dc.

📒 Files selected for processing (3)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (2 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/data/VerificationData.java (1 hunks)
🔇 Additional comments (2)
src/main/java/com/mycom/socket/auth/controller/AuthController.java (1)

51-59: 🛠️ Refactor suggestion

인증 코드 검증 엔드포인트 개선 필요

검증 엔드포인트에 다음 개선사항이 필요합니다:

  1. 코드 형식 검증 (@Pattern 어노테이션 사용)
  2. 표준화된 응답 형식 사용
     @GetMapping("/email/verify")
-    public Boolean mailCheck(@RequestParam(name = "mail") String mail,
-                             @RequestParam(name = "code") String code) {
+    public ApiResponse<EmailVerificationResponse> mailCheck(
+            @RequestParam(name = "mail") @Email(message = "유효한 이메일 주소를 입력해주세요") String mail,
+            @RequestParam(name = "code") @Pattern(regexp = "^\\d{6}$", message = "6자리 숫자를 입력해주세요") String code) {
         try {
-            return mailService.verifyCode(mail, code);
+            boolean verified = mailService.verifyCode(mail, code);
+            return ApiResponse.success(new EmailVerificationResponse(verified));
         } catch (Exception e) {
             throw new BaseException("인증코드 검증에 실패했습니다.", HttpStatus.BAD_REQUEST);
         }
     }

Likely invalid or redundant comment.

src/main/java/com/mycom/socket/auth/service/MailService.java (1)

32-42: 인증번호 생성 로직에 대한 단위 테스트 필요

SecureRandom을 사용한 인증번호 생성 로직에 대한 단위 테스트가 필요합니다:

  1. 생성된 코드가 6자리인지 확인
  2. 생성된 코드가 숫자로만 구성되어 있는지 확인
  3. 중복 코드 생성 확률 테스트

단위 테스트 코드를 생성해드릴까요?

Comment on lines 41 to 49
@PostMapping("/email/verification")
public Boolean mailSend(@RequestParam(name = "mail") String mail) {
try {
rateLimiter.checkRateLimit(mail); // 요청 제한 체크
return mailService.sendMail(mail);
} catch (Exception e) {
throw new BaseException("이메일 전송에 실패했습니다.", HttpStatus.BAD_REQUEST);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

입력값 검증 및 응답 형식 표준화 필요

이메일 검증 엔드포인트에 다음 개선사항이 필요합니다:

  1. @Email 어노테이션으로 이메일 형식 검증
  2. 표준화된 응답 형식 사용
     @PostMapping("/email/verification")
-    public Boolean mailSend(@RequestParam(name = "mail") String mail) {
+    public ApiResponse<EmailVerificationResponse> mailSend(
+            @RequestParam(name = "mail") @Email(message = "유효한 이메일 주소를 입력해주세요") String mail) {
         try {
             rateLimiter.checkRateLimit(mail);  // 요청 제한 체크
-            return mailService.sendMail(mail);
+            boolean sent = mailService.sendMail(mail);
+            return ApiResponse.success(new EmailVerificationResponse(sent));
         } catch (Exception e) {
             throw new BaseException("이메일 전송에 실패했습니다.", HttpStatus.BAD_REQUEST);
         }
     }

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/main/java/com/mycom/socket/global/handler/GlobalExceptionHandler.java (1)

11-13: 불필요하거나 잘못된 예외 클래스 임포트

NoResourceFoundException은 주로 리소스 핸들링 시 사용되는 예외로, 해당 클래스를 임포트하는 것이 필요한지 확인해 주시기 바랍니다. 만약 사용되지 않는다면 불필요한 임포트를 제거하는 것이 좋습니다.

src/main/java/com/mycom/socket/auth/dto/request/EmailVerificationRequestDto.java (1)

7-14: 코드 구현이 잘 되었습니다!

이메일 검증을 위한 DTO 구현이 깔끔하고 적절합니다. 유효성 검사 애노테이션과 한글 에러 메시지가 잘 구성되어 있습니다.

인증 코드 정규식 패턴에 대한 제안

현재 정규식 \\d{6}은 정확히 6자리 숫자만을 허용합니다. 보안성을 더욱 강화하기 위해 다음과 같은 패턴을 고려해보세요:

-@Pattern(regexp = "\\d{6}", message = "인증 코드는 6자리 숫자여야 합니다.")
+@Pattern(regexp = "^[0-9]{6}$", message = "인증 코드는 6자리 숫자여야 합니다.")
src/main/java/com/mycom/socket/auth/dto/response/EmailVerificationResponseDto.java (1)

5-14: 응답 DTO 구현이 명확합니다.

성공과 실패 케이스를 위한 정적 팩토리 메서드가 잘 구현되어 있습니다.

문서화 개선 제안

메서드에 Javadoc을 추가하여 사용 방법과 반환값을 명확히 설명하면 좋을 것 같습니다:

/**
 * 이메일 전송 성공 응답을 생성합니다.
 * @return 성공 메시지가 포함된 응답 객체
 */
public static EmailVerificationResponseDto createSuccessResponse() {
    // ... existing code
}

/**
 * 이메일 전송 실패 응답을 생성합니다.
 * @param errorMessage 실패 원인을 설명하는 에러 메시지
 * @return 에러 메시지가 포함된 실패 응답 객체
 */
public static EmailVerificationResponseDto createFailureResponse(String errorMessage) {
    // ... existing code
}
src/main/java/com/mycom/socket/auth/dto/response/EmailVerificationCheckResponseDto.java (1)

5-14: 응답 구조가 일관성 있게 구현되었습니다.

성공/실패 응답 생성 로직이 잘 구현되어 있으며, Boolean 타입 사용이 적절합니다.

메시지 상수화 제안

성공/실패 메시지를 상수로 분리하면 재사용성과 유지보수성이 향상될 것 같습니다:

public record EmailVerificationCheckResponseDto(ApiResponse<Boolean> apiResponse) {
    private static final String SUCCESS_MESSAGE = "이메일 인증 성공";
    
    public static EmailVerificationCheckResponseDto createSuccessResponse() {
        return new EmailVerificationCheckResponseDto(
            ApiResponse.success(SUCCESS_MESSAGE, true));
    }
    
    // ... rest of the code
}
src/main/java/com/mycom/socket/auth/controller/AuthController.java (1)

41-61: 아키텍처 개선 제안

현재 구현의 확장성과 보안을 위해 다음과 같은 아키텍처 개선을 제안합니다:

  1. 인증 코드 저장소를 Redis와 같은 분산 캐시로 이동
  2. 이메일 발송을 비동기로 처리하여 응답 시간 개선
  3. 보안을 위한 이메일 템플릿 사용
  4. 실패한 인증 시도에 대한 로깅 추가

이러한 개선사항들을 구현하시겠습니까? 필요한 경우 상세한 구현 가이드를 제공해드리겠습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0bb17dc and 8aa701d.

📒 Files selected for processing (9)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (2 hunks)
  • src/main/java/com/mycom/socket/auth/dto/request/EmailRequestDto.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/dto/request/EmailVerificationRequestDto.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/dto/response/EmailVerificationCheckResponseDto.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/dto/response/EmailVerificationResponseDto.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (1 hunks)
  • src/main/java/com/mycom/socket/global/dto/ApiResponse.java (1 hunks)
  • src/main/java/com/mycom/socket/global/handler/GlobalExceptionHandler.java (2 hunks)
  • src/main/java/com/mycom/socket/global/handler/ResponseHandler.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/mycom/socket/auth/service/MailService.java
🔇 Additional comments (6)
src/main/java/com/mycom/socket/global/handler/ResponseHandler.java (1)

26-26: API 응답 형식의 변경으로 인한 영향 확인 필요

beforeBodyWrite 메서드에서 응답 바디를 그대로 반환하도록 변경되었습니다. 이전에는 모든 응답이 ApiResponse로 래핑되어 일관된 응답 형식을 제공했을 수 있습니다. 이 변경으로 인해 클라이언트 측에서 응답 형식의 변화가 발생할 수 있으므로, 기존 API를 사용하는 클라이언트에 미치는 영향과 응답의 일관성을 확인해 주시기 바랍니다.

src/main/java/com/mycom/socket/auth/dto/request/EmailRequestDto.java (1)

6-11: Java Record 사용에 따른 호환성 확인 필요

EmailRequestDtorecord로 정의하셨습니다. record 키워드는 Java 16부터 지원되므로, 프로젝트의 Java 버전이 16 이상인지 확인해 주시기 바랍니다. 만약 Java 16 미만을 사용하고 있다면, 일반 클래스로 변경하는 것이 필요합니다.

src/main/java/com/mycom/socket/global/dto/ApiResponse.java (2)

11-15: 응답 구조체의 필드 구성이 적절합니다.

success 필드 추가로 응답 상태를 명확히 표현할 수 있게 되었습니다.


17-19: Builder 패턴 구현이 깔끔합니다.

Record와 Builder 패턴의 조합이 불변성과 유연성을 잘 제공합니다.

src/main/java/com/mycom/socket/auth/controller/AuthController.java (2)

24-24: 의존성 주입 패턴 승인

@RequiredArgsConstructor와 함께 final 필드로 MailService를 주입한 것이 좋은 방식입니다.


42-50: ⚠️ Potential issue

이메일 검증 엔드포인트 보완 필요

다음 사항들의 개선이 필요합니다:

  1. PR 목표에서 언급된 분당 3회 요청 제한이 구현되지 않았습니다.
  2. 이메일 형식 검증을 위한 @Email 어노테이션이 누락되었습니다.
  3. 응답 형식이 일관되지 않습니다.

다음과 같이 수정하는 것을 제안합니다:

     @PostMapping("/verification")
-    public EmailVerificationResponseDto mailSend(@Valid @RequestBody EmailRequestDto emailRequestDto) {
+    public EmailVerificationResponseDto mailSend(
+            @Valid @Email(message = "유효한 이메일 형식이 아닙니다") 
+            @RequestBody EmailRequestDto emailRequestDto) {
         try {
+            rateLimiter.checkRateLimit(emailRequestDto.email());  // 요청 제한 확인
             boolean isSuccess = mailService.sendMail(emailRequestDto.email());
             return isSuccess ? EmailVerificationResponseDto.createSuccessResponse() : 
                     EmailVerificationResponseDto.createFailureResponse("이메일 전송에 실패했습니다.");
         } catch (BaseException e) {
             return EmailVerificationResponseDto.createFailureResponse(e.getMessage());
         }
     }

Likely invalid or redundant comment.

Comment on lines 47 to 55
// 일반적인 예외 처리 (IllegalArgumentException, NoSuchElementException 등)
@ExceptionHandler({IllegalArgumentException.class, NoSuchElementException.class, NoResourceFoundException.class})
protected ResponseEntity<ApiResponse<?>> handleCommonException(Exception e) {
HttpStatus status = (e instanceof NoResourceFoundException) ? HttpStatus.NOT_FOUND : HttpStatus.BAD_REQUEST;
log.error("Common Exception : {}", e.getMessage());
return ResponseEntity
.status(status)
.body(ApiResponse.error(e.getMessage()));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

공통 예외 처리에 대한 적절성 검토 필요

handleCommonException 메서드에서 IllegalArgumentException, NoSuchElementException, NoResourceFoundException을 한꺼번에 처리하고 있습니다. 이러한 예외들을 전역적으로 처리하는 것이 적절한지 검토해 주시기 바랍니다. 특히, NoSuchElementExceptionNoResourceFoundException은 구체적인 상황에 따라 다른 HTTP 상태 코드를 반환해야 할 수 있습니다.

Comment on lines 58 to 62
protected ResponseEntity<ApiResponse<?>> handleAllException(Exception e) {
log.error("Internal Server Error", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("서버 내부 오류가 발생했습니다"));
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage() : "서버 내부 오류가 발생했습니다."));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

예외 메시지의 직접 노출로 인한 보안 위험

handleAllException 메서드에서 예외의 상세 메시지를 클라이언트에게 직접 반환하고 있습니다. 이는 내부 정보가 노출되어 보안 취약점이 될 수 있습니다. 클라이언트에게는 일반적인 오류 메시지를 제공하고, 상세한 예외 정보는 로그로만 남기는 것을 권장합니다.

수정 제안:

- .body(ApiResponse.error(e.getMessage() != null ? e.getMessage() : "서버 내부 오류가 발생했습니다."));
+ .body(ApiResponse.error("서버 내부 오류가 발생했습니다."));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected ResponseEntity<ApiResponse<?>> handleAllException(Exception e) {
log.error("Internal Server Error", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("서버 내부 오류가 발생했습니다"));
.body(ApiResponse.error(e.getMessage() != null ? e.getMessage() : "서버 내부 오류가 발생했습니다."));
protected ResponseEntity<ApiResponse<?>> handleAllException(Exception e) {
log.error("Internal Server Error", e);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("서버 내부 오류가 발생했습니다."));

Comment on lines 20 to 51
public static <T> ApiResponse<T> success(String message) {
return new ApiResponse<>(message, null, LocalDateTime.now());
return ApiResponse.<T>builder()
.success(true)
.message(message)
.timestamp(LocalDateTime.now())
.build();
}

public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(message, data, LocalDateTime.now());
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(LocalDateTime.now())
.build();
}

public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(message, null, LocalDateTime.now());
return ApiResponse.<T>builder()
.success(false)
.message(message)
.timestamp(LocalDateTime.now())
.build();
}

public static <T> ApiResponse<T> error(String message, T data) {
return new ApiResponse<>(message, data, LocalDateTime.now());
return ApiResponse.<T>builder()
.success(false)
.message(message)
.data(data)
.timestamp(LocalDateTime.now())
.build();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

정적 팩토리 메서드의 Null 안전성 개선 필요

메시지와 데이터의 null 처리를 개선하면 좋을 것 같습니다:

 public static <T> ApiResponse<T> success(String message) {
+    Objects.requireNonNull(message, "메시지는 null일 수 없습니다.");
     return ApiResponse.<T>builder()
             .success(true)
             .message(message)
             .timestamp(LocalDateTime.now())
             .build();
 }

 public static <T> ApiResponse<T> success(String message, T data) {
+    Objects.requireNonNull(message, "메시지는 null일 수 없습니다.");
     return ApiResponse.<T>builder()
             .success(true)
             .message(message)
             .data(data)
             .timestamp(LocalDateTime.now())
             .build();
 }

또한 timestamp 생성을 중앙화하면 일관성 있는 시간 처리가 가능합니다:

private static LocalDateTime getCurrentTimestamp() {
    return LocalDateTime.now();
}

Comment on lines 52 to 61
@GetMapping("/email/verify")
public EmailVerificationCheckResponseDto mailCheck(@Valid @RequestBody EmailVerificationRequestDto emailRequestDto) {
try {
boolean isVerified = mailService.verifyCode(emailRequestDto.email(), emailRequestDto.code());
return isVerified ? EmailVerificationCheckResponseDto.createSuccessResponse() :
EmailVerificationCheckResponseDto.createFailureResponse("이메일 인증에 실패했습니다.");
} catch (BaseException e) {
return EmailVerificationCheckResponseDto.createFailureResponse(e.getMessage());
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

이메일 인증 확인 엔드포인트 개선 필요

현재 구현에 다음과 같은 문제가 있습니다:

  1. GET 메소드에 @RequestBody를 사용하는 것은 REST 규약에 맞지 않습니다.
  2. 인증 코드 길이 검증이 없습니다.
  3. 인증 시도 횟수 제한이 없습니다.

다음과 같이 수정하는 것을 제안합니다:

-    @GetMapping("/email/verify")
-    public EmailVerificationCheckResponseDto mailCheck(@Valid @RequestBody EmailVerificationRequestDto emailRequestDto) {
+    @PostMapping("/email/verify")
+    public EmailVerificationCheckResponseDto mailCheck(
+            @Valid @Email(message = "유효한 이메일 형식이 아닙니다")
+            @Pattern(regexp = "^[0-9]{6}$", message = "인증번호는 6자리 숫자여야 합니다")
+            @RequestBody EmailVerificationRequestDto emailRequestDto) {
         try {
+            verificationAttemptLimiter.checkAttemptLimit(emailRequestDto.email());  // 시도 횟수 제한 확인
             boolean isVerified = mailService.verifyCode(emailRequestDto.email(), emailRequestDto.code());
             return isVerified ? EmailVerificationCheckResponseDto.createSuccessResponse() :
                     EmailVerificationCheckResponseDto.createFailureResponse("이메일 인증에 실패했습니다.");
         } catch (BaseException e) {
             return EmailVerificationCheckResponseDto.createFailureResponse(e.getMessage());
         }
     }

Committable suggestion skipped: line range outside the PR's diff.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/main/java/com/mycom/socket/global/handler/GlobalExceptionHandler.java (2)

47-54: ⚠️ Potential issue

예외 메시지를 직접 노출하면 보안 취약점이 발생할 수 있습니다

IllegalArgumentException의 메시지를 클라이언트에게 직접 반환하고 있습니다. 이는 내부 정보가 노출되어 보안 취약점을 초래할 수 있습니다. 클라이언트에게는 일반적인 오류 메시지를 제공하고 상세한 예외 정보는 로그로만 기록하는 것이 좋습니다.

코드 수정 제안:

-                    .body(ApiResponse.error(e.getMessage()));
+                    .body(ApiResponse.error("잘못된 요청입니다."));

56-63: ⚠️ Potential issue

예외 메시지를 직접 노출하면 보안 취약점이 발생할 수 있습니다

NoSuchElementException의 메시지를 클라이언트에게 직접 반환하고 있습니다. 이는 내부 정보가 노출되어 보안 취약점을 초래할 수 있습니다. 클라이언트에게는 일반적인 오류 메시지를 제공하고 상세한 예외 정보는 로그로만 기록하는 것이 좋습니다.

코드 수정 제안:

-                    .body(ApiResponse.error(e.getMessage()));
+                    .body(ApiResponse.error("요청하신 자원을 찾을 수 없습니다."));
🧹 Nitpick comments (2)
src/main/java/com/mycom/socket/auth/controller/AuthController.java (2)

44-52: 예외 처리 로직의 중복을 줄이고 코드의 재사용성을 높일 수 있습니다

mailSend 메서드에서 예외 처리 로직이 반복되고 있습니다. 예외 처리를 공통 메서드로 추출하여 코드 중복을 줄이고 일관된 예외 처리를 구현하는 것을 권장합니다.

코드 수정 제안:

// 공통 예외 처리 메서드 추가
private <T> T handleException(Supplier<T> supplier, Function<String, T> failureResponseCreator) {
    try {
        return supplier.get();
    } catch (BaseException e) {
        return failureResponseCreator.apply(e.getMessage());
    }
}

// 기존 메서드 수정
@PostMapping("/verification")
public EmailVerificationResponseDto mailSend(@Valid @RequestBody EmailRequestDto emailRequestDto) {
    return handleException(() -> {
        boolean isSuccess = mailService.sendMail(emailRequestDto.email());
        return isSuccess ? EmailVerificationResponseDto.createSuccessResponse()
                         : EmailVerificationResponseDto.createFailureResponse("이메일 전송에 실패했습니다.");
    }, EmailVerificationResponseDto::createFailureResponse);
}

54-64: 예외 처리 로직의 중복을 줄이고 코드의 재사용성을 높일 수 있습니다

mailCheck 메서드에서도 예외 처리 로직이 반복되고 있습니다. 앞서 제안한 공통 예외 처리 메서드를 활용하여 코드 중복을 줄일 수 있습니다.

코드 수정 제안:

@PostMapping("/email/verify")
public EmailVerificationCheckResponseDto mailCheck(@Valid @RequestBody EmailVerificationRequestDto emailRequestDto) {
    return handleException(() -> {
        rateLimiter.checkRateLimit(emailRequestDto.email()); // 시도 횟수 제한
        boolean isVerified = mailService.verifyCode(emailRequestDto.email(), emailRequestDto.code());
        return isVerified ? EmailVerificationCheckResponseDto.createSuccessResponse()
                          : EmailVerificationCheckResponseDto.createFailureResponse("이메일 인증에 실패했습니다.");
    }, EmailVerificationCheckResponseDto::createFailureResponse);
}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa701d and 89a7f63.

📒 Files selected for processing (4)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (2 hunks)
  • src/main/java/com/mycom/socket/auth/dto/request/EmailVerificationRequestDto.java (1 hunks)
  • src/main/java/com/mycom/socket/global/dto/ApiResponse.java (1 hunks)
  • src/main/java/com/mycom/socket/global/handler/GlobalExceptionHandler.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/mycom/socket/auth/dto/request/EmailVerificationRequestDto.java
🔇 Additional comments (1)
src/main/java/com/mycom/socket/global/dto/ApiResponse.java (1)

19-20: LGTM!

ApiResponse 클래스에 success 필드를 추가하고 빌더 패턴을 사용하여 생성자를 정리한 부분이 잘 구현되었습니다.

@ohyuchan123 ohyuchan123 merged commit a86e950 into develop Jan 8, 2025
2 checks passed
@ohyuchan123 ohyuchan123 changed the title Email Send 🔀 Email Send Jan 8, 2025
@ohyuchan123 ohyuchan123 deleted the refactor/email-send branch January 8, 2025 15:46
This was referenced Jan 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant