Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.whereyouad.WhereYouAd.domains.user.application.dto.response;

import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider;

public record EmailSentResponse(
String message, //인증코드를 이메일로 전송했습니다.
String email, //전송한 이메일
long expireIn //만료시간 (500L -> 500초)
Long expireIn, //만료시간 (500L -> 500초)
boolean isProviderLinked,
Provider providerType
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.whereyouad.WhereYouAd.domains.user.application.dto.response;

public record PasswordResetResponse(
String message, //인증코드를 이메일로 전송했습니다.
String email, //전송한 이메일
Long expireIn //만료시간 (500L -> 500초)
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.whereyouad.WhereYouAd.domains.user.application.mapper;

import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.PasswordResetResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import com.whereyouad.WhereYouAd.global.security.oauth2.dto.OAuth2Response;
Expand Down Expand Up @@ -32,4 +35,27 @@ public static OAuth2UserInfo toOAuth2UserInfo(User user, OAuth2Response oAuth2Re
.provider(oAuth2Response.getProvider())
.build();
}

public static EmailSentResponse toEmailSentResponseSuccess(String email) {
return new EmailSentResponse("인증 코드를 이메일로 전송했습니다",
email,
180L,
false,
null);
}

public static EmailSentResponse toEmailSentResponseFail(String email, Provider provider) {
return new EmailSentResponse("이미 소셜 계정으로 가입된 이메일 입니다.",
email,
null,
true,
provider);
}

public static PasswordResetResponse toPasswordResetResponse(String email) {

return new PasswordResetResponse("인증 코드를 이메일로 전송했습니다.",
email,
180L);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package com.whereyouad.WhereYouAd.domains.user.domain.service;

import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.PasswordResetResponse;
import com.whereyouad.WhereYouAd.domains.user.application.mapper.UserConverter;
import com.whereyouad.WhereYouAd.domains.user.domain.constant.Provider;
import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler;
import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.AuthProviderAccount;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import com.whereyouad.WhereYouAd.domains.user.persistence.repository.AuthProviderAccountRepository;
import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository;
import com.whereyouad.WhereYouAd.global.utils.RedisUtil;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,6 +20,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

@Transactional
@Service
@RequiredArgsConstructor
Expand All @@ -23,6 +31,7 @@ public class EmailService {
private final JavaMailSender emailSender;
private final RedisUtil redisUtil;
private final UserRepository userRepository;
private final AuthProviderAccountRepository authProviderAccountRepository;

// application.yml 적용 필요
@Value("${spring.mail.username}")
Expand All @@ -34,19 +43,36 @@ public class EmailService {
// 인증코드 이메일 발송 로직 (최초 회원가입 시)
public EmailSentResponse sendEmail(String toEmail) {
if (userRepository.existsByEmail(toEmail)) { // 이미 해당 이메일로 생성한 계정이 있으면
throw new UserHandler(UserErrorCode.USER_EMAIL_DUPLICATE); // 이메일 중복 예외(회원가입 시 사용했던 예외)
//해당 사용자 정보 조회
User user = userRepository.findUserByEmail(toEmail)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));

//해당 사용자에 연관된 AuthProviderAccount 조회
Optional<AuthProviderAccount> authProviderAccount = authProviderAccountRepository.findByUser(user);

//만약 AuthProviderAccount 가 없으면
if (authProviderAccount.isEmpty()) {
//단순 이메일 회원가입에서 이메일 값이 중복인 것이므로 예외(기존 예외처리 로직)
throw new UserHandler(UserErrorCode.USER_EMAIL_DUPLICATE);
} else { //만약 AuthProviderAccount 가 있으면
//해당 소셜 로그인 플랫폼 타입을 추출해서 반환
Provider provider = authProviderAccount.get().getProvider();

return UserConverter.toEmailSentResponseFail(toEmail, provider);
}
}
Comment on lines 44 to 63
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

불필요한 DB 쿼리 중복: existsByEmail + findUserByEmail

Line 45에서 existsByEmail로 존재 여부를 확인한 후, Line 47에서 다시 findUserByEmail로 같은 이메일을 조회합니다. 이는 동일 데이터에 대해 DB를 두 번 조회하는 것입니다.

findUserByEmail만 사용하여 OptionalisPresent()로 판단하면 쿼리 1회로 줄일 수 있습니다.

♻️ 쿼리 최적화 제안
     public EmailSentResponse sendEmail(String toEmail) {
-        if (userRepository.existsByEmail(toEmail)) {
-            User user = userRepository.findUserByEmail(toEmail)
-                    .orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));
+        Optional<User> existingUser = userRepository.findUserByEmail(toEmail);
+        if (existingUser.isPresent()) {
+            User user = existingUser.get();
 
             Optional<AuthProviderAccount> authProviderAccount = authProviderAccountRepository.findByUser(user);

As per coding guidelines, "JPA 사용 시 N+1 문제나 불필요한 쿼리가 발생하지 않는지 ... 체크하라."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/user/domain/service/EmailService.java`
around lines 44 - 63, Replace the redundant existsByEmail + findUserByEmail
pattern in sendEmail by calling userRepository.findUserByEmail once and using
the returned Optional to branch; i.e., remove the existsByEmail call, call
findUserByEmail(toEmail) into an Optional<User>, if empty proceed with the "no
user" path (or throw USER_NOT_FOUND where appropriate), and if present retrieve
the user, query authProviderAccountRepository.findByUser(user) and preserve the
existing logic that throws USER_EMAIL_DUPLICATE when authProviderAccount is
empty or returns the social provider via UserConverter.toEmailSentResponseFail
when present.


//해당 이메일로 이미 생성된 계정 없으면 이메일 전송 진행
String type = "회원가입";

return emailSendTemplate(toEmail, type);
//템플릿 호출
return (EmailSentResponse) emailSendTemplate(toEmail, type);
}

// 비밀번호 재설정을 위한 인증코드 이메일 발송 로직 (이미 회원가입 된 상태에서 비밀번호 재설정)
public EmailSentResponse sendEmailForPwd(String toEmail) {
public PasswordResetResponse sendEmailForPwd(String toEmail) {
if (userRepository.existsByEmail(toEmail)) { // 이미 회원가입 되어있는 것이 확인되면
String type = "비밀번호 재설정";
return emailSendTemplate(toEmail, type); // 정상적으로 이메일 발송
return (PasswordResetResponse) emailSendTemplate(toEmail, type); // 정상적으로 이메일 발송
} else { // 만약 회원가입 되어있지 않다면
throw new UserHandler(UserErrorCode.USER_NOT_FOUND); // 예외발생
}
Expand All @@ -69,8 +95,9 @@ public void sendEmailForOrgInvitation(String token, String toEmail, String orgNa
}
}

// 기존 이메일 발송 로직 템플릿 화
private EmailSentResponse emailSendTemplate(String toEmail, String type) {
//기존 이메일 발송 로직 템플릿 화
//"회원가입 시 인증 이메일 발송" 과 "비밀번호 재설정 시 인증 이메일 발송" 에 대한 Response 분리 위해 반환값 Object로 변경 (fix/#39)
private Object emailSendTemplate(String toEmail, String type) {

// 인증코드 재전송 로직 -> 이미 Redis 에 해당 이메일 인증코드가 있을시 삭제
String redisKey = "CODE:" + toEmail;
Expand Down Expand Up @@ -111,7 +138,13 @@ private EmailSentResponse emailSendTemplate(String toEmail, String type) {
// 테스트 계정의 인증은 서버 로그를 통해 인증코드를 얻어 입력.
redisUtil.setDataExpire("CODE:" + toEmail, authCode, 60 * 3L);

return new EmailSentResponse("인증코드를 이메일로 전송했습니다.", toEmail, 180L);
if (type.equals("회원가입")) { //해당 템플릿 메서드를 "회원가입을 위한 이메일 인증" 에서 호출한 경우,
//해당 DTO 형식에 맞춰 반환
return UserConverter.toEmailSentResponseSuccess(toEmail);
} else { //"비밀번호 재설정" 에서 호출한 경우,
//해당 DTO 형식에 맞춰 반환
return UserConverter.toPasswordResetResponse(toEmail);
}
}

// 인증코드 검증 메서드
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
package com.whereyouad.WhereYouAd.domains.user.persistence.repository;

import com.whereyouad.WhereYouAd.domains.user.persistence.entity.AuthProviderAccount;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.Optional;

public interface AuthProviderAccountRepository extends JpaRepository<AuthProviderAccount, Long> {
AuthProviderAccount findByProviderId(String username);

@Query("select apa from AuthProviderAccount apa where apa.user = :user")
Optional<AuthProviderAccount> findByUser(@Param(value = "user") User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SmsRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.PwdResetRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.PasswordResetResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SmsResponse;
import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService;
import com.whereyouad.WhereYouAd.domains.user.domain.service.SmsService;
Expand Down Expand Up @@ -68,11 +69,11 @@ public ResponseEntity<DataResponse<SmsResponse.SmsVerifiedResponse>> verifySms(
}

@PostMapping("/password-reset/request")
public ResponseEntity<DataResponse<EmailSentResponse>> sendEmailForPwdReset(@RequestBody @Valid EmailRequest.Send request) {
EmailSentResponse emailSentResponse = emailService.sendEmailForPwd(request.email());
public ResponseEntity<DataResponse<PasswordResetResponse>> sendEmailForPwdReset(@RequestBody @Valid EmailRequest.Send request) {
PasswordResetResponse response = emailService.sendEmailForPwd(request.email());

return ResponseEntity.ok(
DataResponse.from(emailSentResponse)
DataResponse.from(response)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.PwdResetRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.request.SignUpRequest;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.EmailSentResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.PasswordResetResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SmsResponse;
import com.whereyouad.WhereYouAd.domains.user.application.dto.response.SignUpResponse;
import com.whereyouad.WhereYouAd.global.response.DataResponse;
Expand All @@ -29,7 +30,8 @@ public interface UserControllerDocs {
@Operation(
summary = "이메일 인증코드 전송 API",
description = "입력받은 이메일로 인증코드를 전송합니다. 인증코드 재전송도 해당 API 를 호출합니다.\n\n" +
"테스트용 이메일은 'test' 로 시작하거나 'example.com' 으로 끝나야합니다. 테스트용 이메일의 인증코드는 서버 로그로 확인 가능합니다."
"테스트용 이메일은 'test' 로 시작하거나 'example.com' 으로 끝나야합니다. 테스트용 이메일의 인증코드는 서버 로그로 확인 가능합니다.\n\n" +
"이미 소셜 로그인으로 가입된 이메일 값이 요청으로 들어올 경우, isProviderLinked = true, providerType = KAKAO 와 같이 값이 나오며 이메일은 전송되지 않습니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
Expand All @@ -56,7 +58,7 @@ public interface UserControllerDocs {
@ApiResponse(responseCode = "400_2", description = "이메일 전송실패(이메일 오타 등)"),
@ApiResponse(responseCode = "404_1", description = "해당 이메일로 가입한 회원 존재하지 않음")
})
public ResponseEntity<DataResponse<EmailSentResponse>> sendEmailForPwdReset(@RequestBody @Valid EmailRequest.Send request);
public ResponseEntity<DataResponse<PasswordResetResponse>> sendEmailForPwdReset(@RequestBody @Valid EmailRequest.Send request);

@Operation(
summary = "사용자 비밀번호 재설정 API",
Expand Down