Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
// JPA & DB
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.mycom.socket.auth.jwt;
package com.mycom.socket.auth.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/mycom/socket/auth/config/MailProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.mycom.socket.auth.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "spring.mail")
public class MailProperties {
private String host;
private int port;
private String protocol;
private String username;
private String password;
private String senderEmail;
private String senderName;
private String subject;
private String bodyTemplate;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mycom.socket.auth.config;

import com.mycom.socket.auth.jwt.JWTFilter;
import com.mycom.socket.auth.jwt.JWTProperties;
import com.mycom.socket.auth.jwt.JWTUtil;
import com.mycom.socket.auth.service.MemberDetailsService;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ public void logout(HttpServletResponse response) {
authService.logout(response);
}

@PostMapping("/verification")
@PostMapping("/verify-email")
public EmailVerificationResponse sendVerificationEmail(@Valid @RequestBody EmailRequest request) {
return mailService.sendMail(request.email());
}
Comment on lines +41 to 44
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


@PostMapping("/email/verify")
@PostMapping("/verification-code")
public EmailVerificationResponse verifyEmail(@Valid @RequestBody EmailVerificationRequest request) {
return mailService.verifyCode(request.email(), request.code());
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mycom.socket.auth.jwt;

import com.mycom.socket.auth.config.JWTProperties;
import com.mycom.socket.auth.service.MemberDetailsService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mycom.socket.auth.jwt;

import com.mycom.socket.auth.config.JWTProperties;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mycom.socket.auth.security;

import com.mycom.socket.auth.jwt.JWTProperties;
import com.mycom.socket.auth.config.JWTProperties;
import jakarta.servlet.http.Cookie;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
Expand Down
70 changes: 25 additions & 45 deletions src/main/java/com/mycom/socket/auth/service/MailService.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
package com.mycom.socket.auth.service;

import com.mycom.socket.auth.config.MailProperties;
import com.mycom.socket.auth.dto.response.EmailVerificationResponse;
import com.mycom.socket.auth.service.data.VerificationData;
import com.mycom.socket.global.exception.BaseException;
import com.mycom.socket.global.service.RedisService;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.security.SecureRandom;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
@RequiredArgsConstructor
public class MailService {

private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // ์ธ์ฆ ๋ฒˆํ˜ธ ์š”์ฒญ ์ œํ•œ

private final Map<String, VerificationData> verificationDataMap = new ConcurrentHashMap<>();

@Value("${spring.mail.username}")
private String senderEmail;
private final RedisService redisService;
private final MailProperties mailProperties;

/**
* 6์ž๋ฆฌ ๋‚œ์ˆ˜ ์ธ์ฆ๋ฒˆํ˜ธ ์ƒ์„ฑ
* SecureRandom ์‚ฌ์šฉํ•˜์—ฌ ๋ณด์•ˆ์„ฑ ํ–ฅ์ƒ
* @return 100000~999999 ๋ฒ”์œ„์˜ ์ธ์ฆ๋ฒˆํ˜ธ
* 6์ž๋ฆฌ ์ธ์ฆ๋ฒˆํ˜ธ ์ƒ์„ฑ (100000-999999)
*/
private String createVerificationCode() {
// Math.random()์€ ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์–ด ๋ณด์•ˆ์— ์ทจ์•ฝ
// SecureRandom์€ ์•”ํ˜ธํ•™์ ์œผ๋กœ ์•ˆ์ „ํ•œ ๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๋ฏ€๋กœ ์ธ์ฆ๋ฒˆํ˜ธ ์ƒ์„ฑ์— ๋” ์ ํ•ฉ
SecureRandom secureRandom = new SecureRandom();
return String.format("%06d", secureRandom.nextInt(1000000));
return String.format("%06d", new SecureRandom().nextInt(1000000));
}

public boolean isEmailVerified(String email) {
return redisService.isEmailVerified(email);
}

/**
Expand All @@ -48,14 +42,10 @@ private String createVerificationCode() {
public MimeMessage createMail(String email, String verificationCode) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
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.

message.setText(body, "UTF-8", "html");
} catch (MessagingException e) {
throw new BaseException("์ด๋ฉ”์ผ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage(),
Expand All @@ -70,15 +60,18 @@ public MimeMessage createMail(String email, String verificationCode) {
* @return ์ƒ์„ฑ๋œ ์ธ์ฆ๋ฒˆํ˜ธ
*/
public EmailVerificationResponse sendMail(String email) {
rateLimiter.checkRateLimit(email);
if (redisService.incrementCount(email) > 3) {
throw new BaseException("๋„ˆ๋ฌด ๋งŽ์€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค. 1๋ถ„ ํ›„์— ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
HttpStatus.TOO_MANY_REQUESTS);
}
Comment on lines +63 to +66
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(๋งŒ๋ฃŒ ์‹œ๊ฐ„)์„ ์„ค์ •ํ•˜์—ฌ ์ผ์ • ์‹œ๊ฐ„ ํ›„์— ์ž๋™์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


String verificationCode = createVerificationCode();
verificationDataMap.put(email, new VerificationData(verificationCode));
redisService.saveCode(verificationCode);

MimeMessage message = createMail(email, verificationCode);
try {
javaMailSender.send(message);
return EmailVerificationResponse.of("์ด๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต");
return EmailVerificationResponse.of("์ด๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต"); // ๋ฉ”์‹œ์ง€ ์ˆ˜์ •
} catch (Exception e) {
throw new BaseException("์ด๋ฉ”์ผ ๋ฐœ์†ก ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage(),
HttpStatus.INTERNAL_SERVER_ERROR);
Expand All @@ -93,30 +86,17 @@ public EmailVerificationResponse sendMail(String email) {
* @return ์ธ์ฆ๋ฒˆํ˜ธ ์ผ์น˜ ์—ฌ๋ถ€
*/
public EmailVerificationResponse verifyCode(String email, String code) {
validateVerificationCode(code);

VerificationData data = verificationDataMap.get(email);
if (data == null || data.isExpired()) {
throw new BaseException("์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST);
if (!code.matches("\\d{6}")) {
throw new BaseException("์œ ํšจํ•˜์ง€ ์•Š์€ ์ธ์ฆ ์ฝ”๋“œ ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST);
}

if (!data.code().equals(code)) {
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.

throw new BaseException("์ธ์ฆ ์ฝ”๋“œ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST);
}

verificationDataMap.put(email, data.withVerified());
return EmailVerificationResponse.of("์ด๋ฉ”์ผ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.");
}

private void validateVerificationCode(String code) {
if (!StringUtils.hasText(code) || !code.matches("\\d{6}")) {
throw new BaseException("์œ ํšจํ•˜์ง€ ์•Š์€ ์ธ์ฆ ์ฝ”๋“œ ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST);
}
}

public boolean isEmailVerified(String email) {
VerificationData data = verificationDataMap.get(email);
return data != null && !data.isExpired() && data.verified();
}

}
Expand Down
47 changes: 0 additions & 47 deletions src/main/java/com/mycom/socket/auth/service/RateLimiter.java

This file was deleted.

32 changes: 32 additions & 0 deletions src/main/java/com/mycom/socket/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.mycom.socket.global.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@RequiredArgsConstructor
@EnableRedisRepositories
public class RedisConfig {
private final RedisProperties redisProperties;

@Bean
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);
}


@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}

}
16 changes: 16 additions & 0 deletions src/main/java/com/mycom/socket/global/config/RedisProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mycom.socket.global.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisProperties {

private String host;
private int port;
}
87 changes: 87 additions & 0 deletions src/main/java/com/mycom/socket/global/service/RedisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.mycom.socket.global.service;

import com.mycom.socket.global.exception.BaseException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;

/**
* Redis Key Prefix ์ƒ์ˆ˜
*/
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:";


/**
* Redis TTL ์ƒ์ˆ˜
*/
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); // ์š”์ฒญ ์ œํ•œ ์‹œ๊ฐ„


/**
* ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ Redis์— ์ €์žฅ
* Key์™€ Value๋กœ ๋™์ผํ•œ ์ธ์ฆ๋ฒˆํ˜ธ๋ฅผ ์‚ฌ์šฉ
* 3๋ถ„ ํ›„ ์ž๋™ ์‚ญ์ œ
*/
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.


/**
* Redis์—์„œ ์ธ์ฆ๋ฒˆํ˜ธ ์กฐํšŒ
* ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ๋œ ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ
* @throws BaseException ์ธ์ฆ๋ฒˆํ˜ธ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ
*/
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();
}


/**
* ์ด๋ฉ”์ผ๋ณ„ ์š”์ฒญ ํšŸ์ˆ˜ ์ฆ๊ฐ€ (Rate Limiting)
* ์ฒซ ์š”์ฒญ์‹œ 1๋ถ„ ํ›„ ์ž๋™ ์‚ญ์ œ๋˜๋„๋ก ์„ค์ •
* @return ํ˜„์žฌ ์š”์ฒญ ํšŸ์ˆ˜
*/
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 ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™์‹œ์„ฑ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


/**
* ์ธ์ฆ๋œ ์ด๋ฉ”์ผ ์ •๋ณด ์ €์žฅ
* 30๋ถ„ ๋™์•ˆ ์œ ํšจ
*/
public void saveVerifiedEmail(String email) {
redisTemplate.opsForValue().set(
VERIFIED_EMAIL_PREFIX + email,
"true",
VERIFIED_EMAIL_TTL
);
}

/**
* ์ด๋ฉ”์ผ ์ธ์ฆ ์—ฌ๋ถ€ ํ™•์ธ
* @return ์ด๋ฉ”์ผ์ด ์ธ์ฆ๋˜์—ˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false
*/
public boolean isEmailVerified(String email) {
Object verified = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email);
return "true".equals(verified);
}

}
Loading
Loading