Skip to content

🔀 Redis Email Send#34

Merged
ohyuchan123 merged 14 commits intodevelopfrom
refactor/redis-send
Jan 14, 2025
Merged

🔀 Redis Email Send#34
ohyuchan123 merged 14 commits intodevelopfrom
refactor/redis-send

Conversation

@ohyuchan123
Copy link
Member

@ohyuchan123 ohyuchan123 commented Jan 14, 2025

📌 관련 이슈

#26 이메일 인증 기능 Redis 도입

✨ 과제 내용

  1. 메모리 관리 개선
  • ConcurrentHashMap 대신 Redis 사용
  • TTL 기능으로 만료된 데이터 자동 삭제
  1. Rate Limiting 구현
  • 1분당 최대 3회로 인증 요청 제한
  • Redis increment와 TTL 활용
  1. 인증 프로세스 개선
  • 인증번호 유효시간: 3분
  • Rate Limit: 1분당 3회
  • 인증된 이메일 유효시간: 30분

📸 스크린샷(선택)

  • 이메일 인증 전
image
  • 이메일 인증 후
image
  • 인증번호의 유효시간이 지난 후 (로직 필요 없이 자동으로 삭제)
image

📚 레퍼런스 (또는 새로 알게 된 내용) 혹은 궁금한 사항들

Summary by CodeRabbit

Summary by CodeRabbit

  • 새로운 기능

    • Redis 지원을 위한 의존성 추가
    • 이메일 인증을 위한 새로운 서비스 도입
    • 이메일 검증 엔드포인트 경로 업데이트
    • Redis 기반의 이메일 검증 및 요청 카운트 관리 기능 추가
    • Redis 구성 및 속성 관리 클래스 추가
  • 버그 수정

    • 이메일 인증 프로세스 개선
    • 요청 속도 제한 메커니즘 최적화
  • 리팩토링

    • JWT 속성 및 구성 관리 개선
    • 인메모리 데이터 구조에서 Redis 기반 접근 방식으로 전환
    • 보안 구성에서 불필요한 JWT 속성 제거
    • 패키지 구조 변경에 따른 클래스 위치 조정
    • RateLimiter 클래스 제거

@ohyuchan123 ohyuchan123 added ⚙ Setting 개발 환경 세팅 🔨 Refactor 코드 리팩토링 ✨ Feature 기능 개발 labels Jan 14, 2025
@ohyuchan123 ohyuchan123 self-assigned this Jan 14, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2025

"""

워크스루

이 풀 리퀘스트는 애플리케이션의 이메일 검증 및 인증 시스템을 Redis 기반으로 재설계하는 주요 변경 사항을 포함합니다. 기존의 메모리 내 접근 방식에서 Redis를 활용한 확장 가능하고 영구적인 솔루션으로 전환하여 이메일 검증, 속도 제한, 토큰 관리 등의 기능을 개선했습니다.

변경 사항

파일 변경 요약
build.gradle Redis 지원을 위한 새로운 의존성 추가
JWTProperties.java 패키지 위치를 jwt에서 config로 변경
AuthController.java 이메일 검증 엔드포인트 경로 업데이트
MailService.java Redis 기반 이메일 검증 로직으로 리팩토링
RedisConfig.java, RedisProperties.java, RedisService.java Redis 통합을 위한 새로운 구성 및 서비스 클래스 추가
SecurityConfig.java JWTProperties의 import 문 제거
JWTFilter.java, JWTUtil.java, CookieUtil.java JWTProperties import 경로 변경

시퀀스 다이어그램

sequenceDiagram
    participant Client
    participant AuthController
    participant MailService
    participant RedisService
    
    Client->>AuthController: 이메일 검증 요청
    AuthController->>MailService: 검증 코드 생성 및 전송
    MailService->>RedisService: 검증 코드 저장
    RedisService-->>MailService: 코드 저장 확인
    MailService->>Client: 검증 코드 전송
    
    Client->>AuthController: 검증 코드 확인
    AuthController->>MailService: 코드 검증 요청
    MailService->>RedisService: 코드 유효성 확인
    RedisService-->>MailService: 코드 유효성 결과
    MailService-->>AuthController: 검증 결과
    AuthController-->>Client: 검증 응답
Loading

관련 가능성 있는 이슈

관련 가능성 있는 PR

토끼의 시

🐰 Redis의 마법, 코드의 춤
이메일 검증 반짝이네
속도와 보안, 한 번에 품어
토큰은 춤추고 데이터는 노래해
개발의 즐거움, 토끼가 노래해! 🌈
"""

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 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: 8

🔭 Outside diff range comments (3)
src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java (1)

Line range hint 66-74: 이메일 추출 시 예외 처리가 필요합니다.

토큰이 유효하지 않을 경우 발생할 수 있는 예외에 대한 처리가 필요합니다.

 public String getEmail(String token) {
+    try {
         return Jwts.parser()
                 .verifyWith(secretKey)
                 .requireIssuer(jwtProperties.getIssuer())
                 .build()
                 .parseSignedClaims(token)
                 .getPayload()
                 .getSubject();
+    } catch (Exception e) {
+        log.error("토큰에서 이메일을 추출하는 중 오류 발생", e);
+        throw new IllegalArgumentException("유효하지 않은 토큰입니다.", e);
+    }
 }
src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java (2)

Line range hint 52-65: 사용자 상태 검증 로직 추가 필요

현재 구현에서는 사용자의 활성 상태나 계정 만료 여부 등을 확인하지 않고 있습니다. 다음 검증들을 추가하는 것이 좋습니다:

  • 계정 활성화 상태 확인
  • 계정 잠금 상태 확인
  • 비밀번호 만료 여부 확인
     private void setAuthentication(String token) {
         String email = jwtUtil.getEmail(token);
         UserDetails userDetails = memberDetailsService.loadUserByUsername(email);
 
+        if (!userDetails.isEnabled() || !userDetails.isAccountNonLocked() 
+            || !userDetails.isAccountNonExpired() || !userDetails.isCredentialsNonExpired()) {
+            throw new IllegalStateException("계정 상태가 유효하지 않습니다");
+        }
+
         UsernamePasswordAuthenticationToken authentication =
                 new UsernamePasswordAuthenticationToken(
                         userDetails,

쿠키 보안 설정이 일부 누락되어 있습니다

현재 구현된 보안 설정:

  • ✅ HttpOnly 플래그 설정됨
  • ✅ Secure 플래그 설정됨 (properties 통해 구성 가능)
  • ✅ Path가 "/" 로 설정됨

보완이 필요한 설정:

  • SameSite 속성이 설정되어 있지 않습니다 (CSRF 공격 방지를 위해 'Strict' 또는 'Lax' 설정 권장)
  • 쿠키의 도메인 제한이 설정되어 있지 않습니다 (의도하지 않은 도메인과의 쿠키 공유 방지 필요)
🔗 Analysis chain

Line range hint 41-50: 쿠키 보안 설정을 확인해주세요

JWT 토큰을 쿠키에 저장할 때 다음 보안 설정들을 고려해야 합니다:

  • HttpOnly 플래그 설정
  • Secure 플래그 설정 (HTTPS 환경)
  • SameSite 속성 설정
  • 적절한 도메인 및 경로 설정

쿠키 생성 부분의 보안 설정을 확인하기 위해 다음 스크립트를 실행해주세요:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check cookie security settings in the codebase

# Search for cookie creation/configuration
rg -l 'new Cookie|cookie\.set' 

# Search for specific security settings
rg 'setHttpOnly|setSecure|setSameSite'

Length of output: 793


Script:

#!/bin/bash
# Check complete cookie configuration
rg -A 5 'new Cookie' src/main/java/com/mycom/socket/auth/security/CookieUtil.java

# Check JWT properties configuration
fd -e java -e yml -e properties -e yaml | xargs rg -l 'cookie|Cookie'

Length of output: 1121

🧹 Nitpick comments (8)
src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java (3)

Line range hint 20-25: 생성자에 유효성 검사를 추가하는 것이 좋겠습니다.

JWTProperties와 secret 값이 null이 아닌지 확인하는 검증 로직을 추가하면 좋겠습니다.

 public JWTUtil(JWTProperties jwtProperties) {
+    if (jwtProperties == null || jwtProperties.getSecret() == null) {
+        throw new IllegalArgumentException("JWTProperties와 secret은 null이 될 수 없습니다.");
+    }
     this.jwtProperties = jwtProperties;
     this.secretKey = Keys.hmacShaKeyFor(
             jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)
     );
 }

Line range hint 29-42: 토큰 생성 로깅을 추가하면 좋겠습니다.

디버깅과 모니터링을 위해 토큰 생성 시점에 로그를 추가하는 것이 좋겠습니다.

 public String createToken(String email) {
+    log.debug("JWT 토큰 생성 시작 - 이메일: {}", email);
     Date now = new Date();
     Date validity = new Date(now.getTime() +
             (jwtProperties.getAccessTokenValidityInSeconds() * 1000));

-    return Jwts.builder()
+    String token = Jwts.builder()
             .issuer(jwtProperties.getIssuer())
             .subject(email)
             .issuedAt(now)
             .expiration(validity)
             .signWith(secretKey)
             .compact();
+    log.debug("JWT 토큰 생성 완료 - 만료시간: {}", validity);
+    return token;
 }

Line range hint 47-61: 예외 처리를 더 구체화하면 좋겠습니다.

현재는 모든 예외를 하나로 처리하고 있는데, 구체적인 예외 유형별로 처리하면 디버깅에 도움이 될 것 같습니다.

 public boolean validateToken(String token) {
     try {
         if (!StringUtils.hasText(token)) {
+            log.warn("토큰이 비어있거나 null입니다.");
             return false;
         }

         Jwts.parser()
                 .verifyWith(secretKey)
                 .requireIssuer(jwtProperties.getIssuer())
                 .build()
                 .parseSignedClaims(token);
         return true;
-    } catch (Exception e) {
-        log.warn("JWT 토큰 검증 실패", e);
+    } catch (ExpiredJwtException e) {
+        log.warn("만료된 JWT 토큰입니다.", e);
+        return false;
+    } catch (MalformedJwtException e) {
+        log.warn("잘못된 형식의 JWT 토큰입니다.", e);
+        return false;
+    } catch (Exception e) {
+        log.warn("JWT 토큰 검증 중 예상치 못한 오류 발생", e);
         return false;
     }
 }
src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java (1)

Line range hint 33-37: 로깅 개선이 필요합니다

현재 로그 메시지("인증 처리 실패")는 너무 간단합니다. 다음 정보들을 포함하여 로깅을 개선해주세요:

  • 실패한 요청의 URI
  • 실패 원인 구분 (토큰 누락, 토큰 만료, 검증 실패 등)
  • 요청 식별자 (request ID)
-            log.warn("인증 처리 실패", e);
+            log.warn("인증 처리 실패 - URI: {}, 원인: {}, Request ID: {}",
+                    request.getRequestURI(),
+                    e.getMessage(),
+                    request.getAttribute("requestId"));
src/main/java/com/mycom/socket/auth/service/MailService.java (1)

30-30: SecureRandom 인스턴스 재사용 권장

createVerificationCode() 메서드에서 new SecureRandom()를 매번 생성하고 있습니다. 성능 향상을 위해 SecureRandom 인스턴스를 클래스 변수로 선언하여 재사용하는 것이 좋습니다.

+private static final SecureRandom secureRandom = new SecureRandom();

private String createVerificationCode() {
-    return String.format("%06d", new SecureRandom().nextInt(1000000));
+    return String.format("%06d", secureRandom.nextInt(1000000));
}
src/main/java/com/mycom/socket/global/config/RedisConfig.java (1)

23-30: RedisTemplate 직렬화 설정 개선 필요

현재 값 직렬화가 StringRedisSerializer로 설정되어 있어 문자열만 저장 가능합니다. 객체 저장을 위해 Jackson2JsonRedisSerializer 사용을 고려해보세요.

 @Bean
 public RedisTemplate<?, ?> redisTemplate() {
     RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
     redisTemplate.setConnectionFactory(redisConnectionFactory());
 
     redisTemplate.setKeySerializer(new StringRedisSerializer());
-    redisTemplate.setValueSerializer(new StringRedisSerializer());
+    redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
     return redisTemplate;
 }
src/main/java/com/mycom/socket/global/service/RedisService.java (1)

82-85: 이메일 인증 확인 로직 개선 필요

문자열 비교 대신 Boolean 타입을 사용하는 것이 더 적절합니다.

 public boolean isEmailVerified(String email) {
     Object verified = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email);
-    return "true".equals(verified);
+    return Boolean.TRUE.equals(verified);
 }
build.gradle (1)

49-49: Redis 의존성 버전 명시 권장

Spring Boot parent에서 상속받은 버전을 사용하고 있습니다. 명시적인 버전 관리를 위해 버전을 지정하는 것이 좋습니다.

-    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
+    implementation 'org.springframework.boot:spring-boot-starter-data-redis: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 16bf7a5 and 81dbc82.

📒 Files selected for processing (15)
  • build.gradle (1 hunks)
  • src/main/java/com/mycom/socket/auth/config/JWTProperties.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/config/MailProperties.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/config/SecurityConfig.java (0 hunks)
  • src/main/java/com/mycom/socket/auth/controller/AuthController.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/security/CookieUtil.java (1 hunks)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (4 hunks)
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java (0 hunks)
  • src/main/java/com/mycom/socket/global/config/RedisConfig.java (1 hunks)
  • src/main/java/com/mycom/socket/global/config/RedisProperties.java (1 hunks)
  • src/main/java/com/mycom/socket/global/service/RedisService.java (1 hunks)
  • src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java (1 hunks)
  • src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java (1 hunks)
💤 Files with no reviewable changes (2)
  • src/main/java/com/mycom/socket/auth/config/SecurityConfig.java
  • src/main/java/com/mycom/socket/auth/service/RateLimiter.java
✅ Files skipped from review due to trivial changes (4)
  • src/main/java/com/mycom/socket/auth/config/JWTProperties.java
  • src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java
  • src/main/java/com/mycom/socket/auth/security/CookieUtil.java
  • src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build-test
🔇 Additional comments (5)
src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java (1)

3-3: 구성이 잘 정리되었습니다!

JWTProperties를 config 패키지로 이동하고 final 필드로 선언한 것이 좋은 설계 결정입니다.

Also applies to: 17-18

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

46-49: Redis 통합 검증이 필요합니다.

엔드포인트 이름이 /verification-code로 변경된 것은 좋은 개선입니다. 하지만 Redis 통합과 관련된 부분을 검증해야 합니다.

다음 사항들을 확인해주세요:

  1. Redis 연동이 올바르게 구현되었는지
  2. 이메일 인증 코드의 만료 시간이 설정되었는지
  3. 동시성 처리가 적절히 되어있는지
src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java (1)

3-3: 패키지 구조 개선이 잘 되었습니다!

JWTProperties를 config 패키지로 이동한 것은 관심사의 분리 원칙을 잘 따른 좋은 변경입니다.

src/main/java/com/mycom/socket/global/config/RedisProperties.java (1)

1-16: RedisProperties 구성 확인

RedisProperties 클래스가 Redis 설정을 위한 필요한 속성을 적절히 매핑하고 있습니다. @ConfigurationProperties@Component 애너테이션을 사용하여 설정 파일로부터 값을 주입받는 방식이 올바르게 적용되었습니다.

src/main/java/com/mycom/socket/auth/config/MailProperties.java (1)

1-22: MailProperties 구성 확인

MailProperties 클래스는 메일 전송에 필요한 다양한 속성들을 포함하고 있으며, @ConfigurationProperties를 통해 spring.mail 프리픽스의 설정을 올바르게 매핑하고 있습니다. 메일 발송에 필요한 정보를 효율적으로 관리할 수 있을 것으로 보입니다.

Comment on lines +41 to 44
@PostMapping("/verify-email")
public EmailVerificationResponse sendVerificationEmail(@Valid @RequestBody EmailRequest request) {
return mailService.sendMail(request.email());
}
Copy link

Choose a reason for hiding this comment

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

💡 Codebase verification

API 문서화가 필요합니다

엔드포인트 변경사항(/verify-email)이 문서화되어 있지 않은 것으로 보입니다. 다음 사항들을 추가해주시기 바랍니다:

  • API 엔드포인트에 대한 명세 문서 작성
  • 클라이언트 코드에서 새로운 엔드포인트 사용 여부 확인
  • README.md에 API 사용 방법 추가
🔗 Analysis chain

엔드포인트 이름이 개선되었습니다!

새로운 엔드포인트 이름 /verify-email이 기능을 더 명확하게 설명합니다.

API 문서가 업데이트되었는지 확인하기 위해 다음 스크립트를 실행하세요:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# API 문서에서 이전 엔드포인트 참조 확인
rg -i "/verification" --type md
rg -i "/verify-email" --type md

Length of output: 63


Script:

#!/bin/bash
# Search for API documentation files
echo "=== Documentation files ==="
fd -e yaml -e yml -e json . | grep -i "api\|swagger\|openapi"

echo -e "\n=== Searching for endpoint references ==="
# Search in all text files for both old and new endpoints
rg -t txt -t yaml -t yml -t json -t java "/verification|/verify-email"

echo -e "\n=== README files ==="
fd README

Length of output: 407

Comment on lines +63 to +66
if (redisService.incrementCount(email) > 3) {
throw new BaseException("너무 많은 요청입니다. 1분 후에 다시 시도해주세요.",
HttpStatus.TOO_MANY_REQUESTS);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

요청 횟수 제한에 대한 만료 시간 설정 필요

redisService.incrementCount(email)를 통해 이메일별 요청 횟수를 증가시키지만, 현재 요청 횟수를 초기화하거나 만료시키는 로직이 없습니다. 이로 인해 사용자가 영구적으로 이메일 전송을 할 수 없게 될 수 있습니다. 요청 횟수에 대한 Redis 키에 TTL(만료 시간)을 설정하여 일정 시간 후에 자동으로 초기화되도록 수정해야 합니다.

Comment on lines 45 to 48
message.setFrom(mailProperties.getUsername());
message.setRecipients(MimeMessage.RecipientType.TO, email);
message.setSubject("이메일 인증");
String body = String.format("""
<h3>요청하신 인증 번호입니다.</h3>
<h1>%s</h1>
<h3>감사합니다.</h3>
""", verificationCode);
String body = String.format(mailProperties.getBodyTemplate(), verificationCode);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

발신 이메일 주소 설정 수정 필요

현재 message.setFrom(mailProperties.getUsername());로 설정되어 있는데, 이는 인증에 사용되는 사용자 이름입니다. 실제 발신자 이메일 주소를 설정하려면 mailProperties.getSenderEmail()을 사용하는 것이 적절합니다.

-message.setFrom(mailProperties.getUsername());
+message.setFrom(mailProperties.getSenderEmail());

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

Comment on lines 93 to 97
try {
redisService.getCode(code); // 인증코드 검증
redisService.saveVerifiedEmail(email); // 인증된 이메일 저장
return EmailVerificationResponse.of("이메일 인증이 완료되었습니다.");
} catch (Exception e) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

인증 코드 검증 로직 보완 필요

인증 코드 검증 시 redisService.getCode(code);로 코드만을 이용하여 검증하고 있습니다. 이는 인증 코드와 이메일이 매핑되지 않아, 다른 사용자의 인증 코드를 악용할 수 있는 보안 문제가 발생할 수 있습니다. 인증 코드를 저장할 때 이메일과 함께 저장하고, 검증 시에도 해당 이메일에 저장된 코드를 가져와 비교하도록 수정해야 합니다.

-public EmailVerificationResponse verifyCode(String email, String code) {
     if (!code.matches("\\d{6}")) {
         throw new BaseException("유효하지 않은 인증 코드 형식입니다.", HttpStatus.BAD_REQUEST);
     }

     try {
-        redisService.getCode(code);  // 인증코드 검증
+        String savedCode = redisService.getCode(email);  // 이메일로부터 코드 조회
+        if (!code.equals(savedCode)) {
+            throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
+        }
         redisService.saveVerifiedEmail(email);  // 인증된 이메일 저장
         return EmailVerificationResponse.of("이메일 인증이 완료되었습니다.");
     } catch (Exception e) {
         throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
     }
 }

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

Comment on lines 18 to 20
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redis 연결 설정 개선 필요

연결 시간 초과 및 풀 설정이 누락되어 있습니다. 프로덕션 환경에서는 다음 설정들을 추가하는 것이 좋습니다:

  • 연결 시간 초과 설정
  • 연결 풀 설정
 @Bean
 public LettuceConnectionFactory redisConnectionFactory() {
-    return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
+    LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
+        .commandTimeout(Duration.ofSeconds(1))
+        .build();
+    RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(
+        redisProperties.getHost(),
+        redisProperties.getPort()
+    );
+    return new LettuceConnectionFactory(serverConfig, clientConfig);
 }
📝 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 LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(1))
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(
redisProperties.getHost(),
redisProperties.getPort()
);
return new LettuceConnectionFactory(serverConfig, clientConfig);
}

Comment on lines 19 to 20
private static final String VERIFIED_EMAIL_PREFIX = "verified:email:";
private static final String RATE_LIMIT_PREFIX = "rate-limit:";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Redis 키 접두사 체계 개선 필요

인증 코드에 대한 키 접두사가 누락되어 있습니다. 키 충돌을 방지하기 위해 모든 키에 접두사를 추가하는 것이 좋습니다.

 private static final String VERIFIED_EMAIL_PREFIX = "verified:email:";
 private static final String RATE_LIMIT_PREFIX = "rate-limit:";
+private static final String VERIFICATION_CODE_PREFIX = "verification:code:";
📝 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 static final String VERIFIED_EMAIL_PREFIX = "verified:email:";
private static final String RATE_LIMIT_PREFIX = "rate-limit:";
private static final String VERIFIED_EMAIL_PREFIX = "verified:email:";
private static final String RATE_LIMIT_PREFIX = "rate-limit:";
private static final String VERIFICATION_CODE_PREFIX = "verification:code:";

Comment on lines 35 to 37
public void saveCode(String code) {
redisTemplate.opsForValue().set(code, code, VERIFICATION_TTL);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

인증 코드 저장 로직 개선 필요

인증 코드 저장 시 접두사를 사용하지 않아 키 충돌이 발생할 수 있습니다.

 public void saveCode(String code) {
-    redisTemplate.opsForValue().set(code, code, VERIFICATION_TTL);
+    redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + code, code, VERIFICATION_TTL);
 }

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

@ohyuchan123 ohyuchan123 added the 🐞 BugFix Something isn't working label Jan 14, 2025
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 (3)
src/main/java/com/mycom/socket/global/service/RedisService.java (2)

19-28: Redis 관련 상수값들의 구성 개선 필요

상수값들을 코드에서 직접 정의하는 대신 설정 파일로 분리하는 것이 좋습니다. 이를 통해 환경별로 다른 값을 적용하거나 운영 중에 값을 변경하기 쉬워집니다.

+@Value("${redis.ttl.verification:180}")  // 3분
+private int verificationTtlSeconds;
+@Value("${redis.ttl.verified-email:1800}")  // 30분
+private int verifiedEmailTtlSeconds;
+@Value("${redis.ttl.rate-limit:60}")  // 1분
+private int rateLimitTtlSeconds;

-private static final Duration VERIFICATION_TTL = Duration.ofMinutes(3);
-private static final Duration VERIFIED_EMAIL_TTL = Duration.ofMinutes(30);
-private static final Duration RATE_LIMIT_TTL = Duration.ofMinutes(1);

+private Duration getVerificationTtl() {
+    return Duration.ofSeconds(verificationTtlSeconds);
+}

71-86: 이메일 인증 상태 저장 방식 개선 필요

Redis에 문자열로 boolean 값을 저장하는 것보다 Redis의 기능을 활용하는 것이 좋습니다.

 public void saveVerifiedEmail(String email) {
-    redisTemplate.opsForValue().set(
-            VERIFIED_EMAIL_PREFIX + email,
-            "true",
-            VERIFIED_EMAIL_TTL
-    );
+    redisTemplate.opsForValue().set(VERIFIED_EMAIL_PREFIX + email, "", VERIFIED_EMAIL_TTL);
 }

 public boolean isEmailVerified(String email) {
-    Object verified = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email);
-    return "true".equals(verified);
+    return Boolean.TRUE.equals(redisTemplate.hasKey(VERIFIED_EMAIL_PREFIX + email));
 }
src/main/java/com/mycom/socket/auth/service/MailService.java (1)

28-31: 인증 코드 생성 로직 개선 필요

SecureRandom 인스턴스를 매번 생성하는 것은 비효율적입니다. 또한 현재 구현은 1,000,000으로 나누지 않아 7자리 숫자가 생성될 수 있습니다.

+private static final SecureRandom secureRandom = new SecureRandom();

 private String createVerificationCode() {
-    return String.format("%06d", new SecureRandom().nextInt(1000000));
+    return String.format("%06d", secureRandom.nextInt(900000) + 100000);
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 81dbc82 and 4121cf6.

📒 Files selected for processing (3)
  • src/main/java/com/mycom/socket/auth/service/MailService.java (4 hunks)
  • src/main/java/com/mycom/socket/global/config/RedisConfig.java (1 hunks)
  • src/main/java/com/mycom/socket/global/service/RedisService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/mycom/socket/global/config/RedisConfig.java
🔇 Additional comments (3)
src/main/java/com/mycom/socket/global/service/RedisService.java (1)

58-65: 요청 횟수 제한 로직 보완 필요

최대 요청 횟수 제한이 설정되어 있지 않아 무한정 요청이 가능합니다. 또한 race condition이 발생할 수 있습니다.

 public Long incrementCount(String email) {
     String key = RATE_LIMIT_PREFIX + email;
     Long count = redisTemplate.opsForValue().increment(key);
     if (count == 1) {
         redisTemplate.expire(key, RATE_LIMIT_TTL);
     }
+    if (count > 3) {  // PR 목표: 1분당 3회로 제한
+        throw new BaseException("너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
+    }
     return count;
 }
src/main/java/com/mycom/socket/auth/service/MailService.java (2)

63-70: ⚠️ Potential issue

이메일 발송 로직 개선 필요

인증 코드 저장 시 이메일 정보가 누락되어 있어 보안 문제가 발생할 수 있습니다.

 if (redisService.incrementCount(email) > 3) {
     throw new BaseException("너무 많은 요청입니다. 1분 후에 다시 시도해주세요.",
             HttpStatus.TOO_MANY_REQUESTS);
 }

 String verificationCode = createVerificationCode();
-redisService.saveCode(verificationCode);
+redisService.saveCode(email, verificationCode);

Likely invalid or redundant comment.


89-99: ⚠️ Potential issue

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

이메일과 인증 코드의 매핑이 누락되어 있어 보안 문제가 발생할 수 있습니다. 또한 인증 성공 시 이메일 인증 상태를 저장하는 로직이 누락되어 있습니다.

 try {
-    String saveCode = redisService.getCode(code);  // 인증코드 검증
-    if(!saveCode.equals(code)) {
+    String savedCode = redisService.getCode(email);  // 이메일로 인증코드 조회
+    if(!savedCode.equals(code)) {
         throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
     }
+    redisService.saveVerifiedEmail(email);  // 이메일 인증 완료 처리
     return EmailVerificationResponse.of("이메일 인증이 완료되었습니다.");
 } catch (Exception e) {
     throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
 }

Likely invalid or redundant comment.

Comment on lines 36 to 38
public void saveCode(String code) {
redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX +code, code, VERIFICATION_TTL);
}
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 void saveCode(String code) {
-    redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + code, code, VERIFICATION_TTL);
+public void saveCode(String email, String code) {
+    redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + email, code, VERIFICATION_TTL);
}
📝 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 saveCode(String code) {
redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX +code, code, VERIFICATION_TTL);
}
public void saveCode(String email, String code) {
redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + email, code, VERIFICATION_TTL);
}

Comment on lines 45 to 51
public String getCode(String code) {
Object savedCode = redisTemplate.opsForValue().get(code);
if (savedCode == null) {
throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
}
return savedCode.toString();
}
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 String getCode(String code) {
-    Object savedCode = redisTemplate.opsForValue().get(code);
+public String getCode(String email) {
+    Object savedCode = redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + email);
     if (savedCode == null) {
         throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
     }
     return savedCode.toString();
}
📝 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 String getCode(String code) {
Object savedCode = redisTemplate.opsForValue().get(code);
if (savedCode == null) {
throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
}
return savedCode.toString();
}
public String getCode(String email) {
Object savedCode = redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + email);
if (savedCode == null) {
throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
}
return savedCode.toString();
}

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 (1)
src/main/java/com/mycom/socket/global/service/RedisService.java (1)

36-38: 이미 인증된 이메일에 대한 인증 코드 발송 방지

saveCode 메서드에서 이미 인증된 이메일에 대해 인증 코드를 재발송하지 않도록 이메일의 인증 상태를 확인하고, 인증된 경우 예외를 발생시키는 로직을 추가하는 것이 좋습니다.

아래와 같이 코드를 수정할 수 있습니다:

public void saveCode(String email, String code) {
    if (isEmailVerified(email)) {
        throw new BaseException("이미 인증된 이메일입니다.", HttpStatus.BAD_REQUEST);
    }
    redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + email, code, VERIFICATION_TTL);
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4121cf6 and c3ea46e.

📒 Files selected for processing (1)
  • src/main/java/com/mycom/socket/global/service/RedisService.java (1 hunks)
🔇 Additional comments (1)
src/main/java/com/mycom/socket/global/service/RedisService.java (1)

58-65: 요청 횟수 제한 로직 보완 필요

incrementCount 메서드에서 최대 요청 횟수 제한이 적용되지 않아 사용자가 무제한으로 요청을 보낼 수 있습니다. PR 목표에 명시된 "1분당 최대 3회의 요청 제한"을 구현하기 위해, 요청 횟수가 제한을 초과하면 예외를 발생시키는 로직을 추가해야 합니다.

이전 리뷰에서 제안한 코드를 참고하여 아래와 같이 수정할 수 있습니다:

public Long incrementCount(String email) {
    String key = RATE_LIMIT_PREFIX + email;
    Long count = redisTemplate.opsForValue().increment(key);
    if (count == 1) {
        redisTemplate.expire(key, RATE_LIMIT_TTL);
    }
    if (count > 3) {
        throw new BaseException("요청 가능 횟수를 초과했습니다. 잠시 후 다시 시도해주세요.", HttpStatus.TOO_MANY_REQUESTS);
    }
    return count;
}

Comment on lines 58 to 65
public Long incrementCount(String email) {
String key = RATE_LIMIT_PREFIX + email;
Long count = redisTemplate.opsForValue().increment(key);
if (count == 1) {
redisTemplate.expire(key, RATE_LIMIT_TTL);
}
return count;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

동시성 문제 해결을 위한 원자적 연산 사용 권장

incrementCount 메서드에서 여러 스레드가 동시에 접근할 경우, race condition이 발생하여 요청 횟수 제한 로직이 올바르게 작동하지 않을 수 있습니다. Redis의 원자적 명령어 또는 Lua 스크립트를 활용하여 동시성 문제를 해결하는 것이 좋습니다.

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

🧹 Nitpick comments (2)
src/main/java/com/mycom/socket/global/service/RedisService.java (2)

54-73: Rate Limiting 로직 개선이 필요합니다.

현재 구현은 기본적인 동시성 처리를 포함하고 있지만, Lua 스크립트를 사용하면 더 안전한 원자적 연산이 가능합니다.

다음과 같이 Lua 스크립트를 사용하여 개선할 수 있습니다:

 public Long incrementCount(String email) {
     String key = RATE_LIMIT_PREFIX + email;
-    return redisTemplate.execute(
-            (RedisConnection connection) -> {
-                Long count = connection.incr(key.getBytes());
-                if (count == 1) {
-                    connection.expire(key.getBytes(), RATE_LIMIT_TTL.getSeconds());
-                }
-                return count;
-            }
-    );
+    String script = "local count = redis.call('incr', KEYS[1]) " +
+                    "if count == 1 then " +
+                    "  redis.call('expire', KEYS[1], ARGV[1]) " +
+                    "end " +
+                    "return count";
+    return redisTemplate.execute(
+        new DefaultRedisScript<>(script, Long.class),
+        Collections.singletonList(key),
+        RATE_LIMIT_TTL.getSeconds()
+    );
 }

75-95: 이메일 인증 상태 관리 기능 확장이 필요합니다.

현재 구현은 기본적인 기능을 잘 수행하고 있지만, 인증 상태를 제거하는 기능이 누락되어 있습니다.

다음 메소드 추가를 제안합니다:

/**
 * 이메일 인증 상태 제거
 * 사용자가 재인증을 요청하거나 인증을 취소할 때 사용
 */
public void removeVerifiedEmail(String email) {
    redisTemplate.delete(VERIFIED_EMAIL_PREFIX + email);
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between f7ec6d2 and 371a66b.

📒 Files selected for processing (1)
  • src/main/java/com/mycom/socket/global/service/RedisService.java (1 hunks)
🔇 Additional comments (3)
src/main/java/com/mycom/socket/global/service/RedisService.java (3)

1-16: 클래스 구조가 적절합니다!

Spring과 Lombok 어노테이션을 올바르게 사용하고 있으며, 생성자 주입 패턴이 잘 적용되어 있습니다.


17-30: 상수 정의가 개선되었습니다!

이전 리뷰에서 지적된 키 접두사 문제가 해결되었으며, TTL 설정도 적절합니다.


31-52: 인증 코드 저장 및 조회 로직이 개선되었습니다!

이전 리뷰에서 지적된 이메일-코드 매핑 문제가 해결되었으며, 예외 처리도 적절합니다.

@ohyuchan123 ohyuchan123 merged commit 5ef4d93 into develop Jan 14, 2025
2 checks passed
@ohyuchan123 ohyuchan123 changed the title Redis Email Send 🔀 Redis Email Send Jan 14, 2025
@coderabbitai coderabbitai bot mentioned this pull request Jan 14, 2025
@ohyuchan123 ohyuchan123 deleted the refactor/redis-send branch January 15, 2025 09:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 BugFix Something isn't working ✨ Feature 기능 개발 🔨 Refactor 코드 리팩토링 ⚙ Setting 개발 환경 세팅

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant