Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[임시 비밀번호 발급 및 전송] 임시 비밀번호 발급 및 전송 비즈니스 로직/API 구성(#35) #40

Merged
merged 56 commits into from
Mar 23, 2024

Conversation

Minjae-An
Copy link
Contributor

@Minjae-An Minjae-An commented Mar 14, 2024

작업 대상

  • 서비스 사용 정규식(ServiceRegex )
  • 임시 비밀번호 발급 기능(TemporaryPasswordGenerator)
  • spring mail 사용을 위한 설정(MailConfig )
  • 이메일 전송 로직(EmailService )
  • 임시 비밀번호 전송 비즈니스 로직(LoginService )
  • 임시 비밀번호 전송 API(LoginController )

📄 작업 내용

  • 서비스에 사용하는 정규식들을 한 눈에 관리하기 위한 enum 정의
  • 비밀번호 정규식 조건을 충족하는 랜덤한 임시 비밀번호 생성 로직 구현
  • spring mail에서 제공하는 이메일 전송 기능(JavaMailSender ) 사용을 위한 설정 정의
  • 임시 비밀번호 발급 및 전송 비즈니스 로직
    1. 요청 이메일 기반 사용자 조회
    2. 임시 비밀번호 생성
    3. 사용자 비밀번호, 임시 비밀번호로 변경
    4. 임시 비밀번호 발급 메일 전송
  • 임시 비밀번호 발급 및 전송 API 구현

임시 비밀번호를 암호화하는 로직, 비동기 처리 로직은 추후에 구현 예정

🙋🏻 주의 사항

  • build.gradle 에 spring mail 모듈에 대한 의존성을 추가하였습니다.
  • 이메일 전송 기능은 Gmail SMTP 서버를 통해 구현했습니다. 이에 따라 application.propertiesapplication.yml 에 해당 서버를 이용하는데 필요한 속성들을 정의했으니 참고해주세요.

📎 관련 이슈

  • springframwork.util의 Javadoc에서 해당 패키지에서 제공하는 StringUtils보다 복합적인 유틸 기능을 필요로 할 시 apache.commons.lang에서 제공하는 유틸 클래스를 사용할 것을 언급하고 있음

  • 이에 임시 비밀번호 생성 로직에 spring boot에서 기본적으로 제공하는 apache.commons.lang3.RandomStringUtils를 사용

  • 이메일을 발송하는 로직이 동기 방식으로 동작할 경우 SMTP 서버에서 서비스 서버가 응답을 받은 후 클라이언트에 응답

  • 이로 인해 응답 시간이 17~20초로 매우 길게 측정됨

  • 비동기 처리를 하면 시간을 훨씬 단축되나 해당 로직이 별도의 쓰레드에 실행됨(컨텍스트가 다르다)

  • 이에 따라 기존 ExceptionAdvice에서 비동기 로직에서 발생한 예외를 커스텀 처리할 수 없음

  • 추후에 현재의 일관성 있는 예외 응답 형식을 비동기 예외에 대해서도 적용할 방법을 찾는다면 비동기 처리 예정

레퍼런스

Minjae-An added 30 commits March 9, 2024 17:16
보낼 화살 수를 담은 요청 DTO 정의
- 화살 송수신 내역 repository 정의
- 보낸 사용자, 받은 사용자를 통해 내역 존재 여부를 확인하는 로직 구현
보낸 사용자, 받은 사용자를 통해 송수신 내역 존재 여부를 확인하는
로직 구현
다음 상황들에 대한 예외 enum을 정의하였다.
- 자기 자신에게 화살을 보내는 경우
- 이미 화살을 보낸 사용자인 경우
- 가진 화살 수가 부족해 화살을 보낼 수 없는 경우
감소하려는 화살 수가 사용자가 보유한 화살 수보다 클 경우 예외 발생
화살 보내기 비즈니스 로직은 다음 과정을 거친다.
1. 자기 자신에게 화살을 보내는 상황 검증
2. 화살을 보낸 사용자에게 또 보내는 상황 검증
3. 화살 내역 저장
4. 보내는 사용자 화살 감소, 화살이 부족할 경우 예외 발생(과정 3 롤백)
5. 받는 사용자 화살 증가
- query, command를 service, repository를 중개하는 별도의 계층을 통해 구분
- 이에 따라 기존 command repository 삭제
- 기존 command repository 사용 위치, 화살 command로 대체
# Conflicts:
#	be/src/main/java/yeonba/be/arrow/repository/ArrowCommand.java
#	be/src/main/java/yeonba/be/arrow/service/ArrowService.java
#	be/src/main/java/yeonba/be/exception/CommonException.java
#	be/src/main/java/yeonba/be/user/entity/User.java
서로 대치되는 작업을 수행하는 상황을 잘 표현할 수 있도록 메서드 이름 수정
- 이미 화살을 보낸 사용자 예외
- 화살이 부족하여 화살을 보낼 수 없는 예외
가독성 향상을 위해 간단한 이름을 수정
dev 브랜치 작업 내역 병합에 따른 코드 배치 수정
좀 더 명확하게 대치되는 의미를 나타낼 수 있도록 화살을 받는
사용자 ID 파라미터 이름을 receiverId로 수정
- 이름 칼럼 추가
- 테스트시 활용 가능한 생성자 추가
- 래퍼 타입 필드들 기본 타입으로 변경
- 프로필 사진 URL 리스트 필드 추가
- 사용자가 가진 총 화살 수 필드 추가
- 음주 성향, 흡연 성향 필드 추가
- 코드 정렬
- 레퍼런스 타입 not null 필드들, Column 어노테이션 사용 명시
- salt 필드 추가
- 생성 일시, 최종 수정 일시 필드 추가
- JpaAuditing 활용, 생성/최종 수정 일시 필드들 엔티티 저장시 자동 설정
- 새로운 생성자 구성
프로필을 조회하는 사용자 ID의 경우 상대방이 화살을 보넀던 사용자인지
확인하기 위해 파라미터로 받는다.
- 요구사항 변경에 따라 음주 성향, 흡연 성향 필드 삭제
- 성별 정보 제공 메서드 추가
- 음주 성향, 흡연 성향 필드 삭제
- 성별 필드 추가
- 음주 성향, 흡연 성향 필드 삭제
- 생성자 성별 필드 설정 로직 수정
- 코드 정렬
- 음주 성향, 흡연 성향 삭제
- 성별 정보 응답에 포함토록 수정
- 코드 정렬
# Conflicts:
#	be/src/main/java/yeonba/be/arrow/service/ArrowService.java
#	be/src/main/java/yeonba/be/user/controller/UserController.java
#	be/src/main/java/yeonba/be/user/entity/User.java
@Minjae-An Minjae-An added the ✨ feat 새로운 기능 추가 label Mar 14, 2024
@Minjae-An Minjae-An requested a review from acceptor-gyu March 14, 2024 09:35
@Minjae-An Minjae-An self-assigned this Mar 14, 2024
# Conflicts:
#	be/src/main/java/yeonba/be/user/entity/User.java
@Minjae-An Minjae-An closed this Mar 14, 2024
@Minjae-An Minjae-An reopened this Mar 14, 2024
dev 브랜치 작업 내역 병합에 따른 코드 재배치
dev 브랜치 작업 내역 병합에 따른 코드 재배치
Copy link
Contributor

@acceptor-gyu acceptor-gyu left a comment

Choose a reason for hiding this comment

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

코드, 팀 컨벤션 생각해주세요. 테스트 코드가 없기 때문에 급하게 구현만 하는 것보다 postman으로 테스트도 성공/실패 케이스 모두 해보시고 PR 올려주시기 바랍니다.

Comment on lines 14 to 18
mail:
host: smtp.gmail.com
port: 587
username: ${GOOGLE_SMTP_USERNAME}
password: ${GOOGLE_SMTP_PASSWORD}
Copy link
Contributor

Choose a reason for hiding this comment

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

secret의 경우 properties에서 관리하기로 했는데 yml에 작성하신 이유는 무엇인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

secret이라는 게 노출되었을 경우 보안적으로 위험할 수 있는 대상들을 지칭한다고 생각했습니다. smtp 서비스 제공 호스트와 그에 따른 포트의 경우 지극히 공개적인 값이라 굳이 application.properties로 관리하지 않고 YAML 파일에 바로 명시하였습니다. 하지만 말씀해주신 부분을 생각해보면 application.properties에서 일관성 있게 관리하는 것이 맞다고 생각됩니다. 수정하겠습니다.

Comment on lines 47 to 52
if(result.matches(pattern)){

return result;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

return 문이 없습니다.

Comment on lines 16 to 52
public static String generatePassword(){
String pattern = ServiceRegex.PASSWORD.getPattern();
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder();

int length = MIN_LENGTH + random.nextInt(MAX_LENGTH - MIN_LENGTH+1);

while(true){
sb.setLength(0);
for(int i=0; i<length; i++){
int choice = random.nextInt(4);

if(choice == 0){
sb.append(getRandomCharacterFromSrc(LOWER, random));
continue;
}

if(choice==1){
sb.append(getRandomCharacterFromSrc(UPPER, random));
continue;
}

if(choice==2){
sb.append(getRandomCharacterFromSrc(DIGITS, random));
continue;
}

sb.append(getRandomCharacterFromSrc(SPECIAL, random));
}

String result = sb.toString();
if(result.matches(pattern)){

return result;
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

depth가 너무 깊어 어디부터 어디까지 하나의 블록인지 구분하기가 어려운데 아래와 같은 방식은 어떤가요?

Suggested change
public static String generatePassword(){
String pattern = ServiceRegex.PASSWORD.getPattern();
SecureRandom random = new SecureRandom();
StringBuilder sb = new StringBuilder();
int length = MIN_LENGTH + random.nextInt(MAX_LENGTH - MIN_LENGTH+1);
while(true){
sb.setLength(0);
for(int i=0; i<length; i++){
int choice = random.nextInt(4);
if(choice == 0){
sb.append(getRandomCharacterFromSrc(LOWER, random));
continue;
}
if(choice==1){
sb.append(getRandomCharacterFromSrc(UPPER, random));
continue;
}
if(choice==2){
sb.append(getRandomCharacterFromSrc(DIGITS, random));
continue;
}
sb.append(getRandomCharacterFromSrc(SPECIAL, random));
}
String result = sb.toString();
if(result.matches(pattern)){
return result;
}
}
}
public static String generatePassword() {
StringBuilder password = new StringBuilder(MIN_LENGTH);
Random random = new Random();
// 각 카테고리에서 최소 하나의 문자를 선택
password.append(LOWER_CASES.charAt(random.nextInt(LOWER_CASES.length())));
password.append(UPPER_CASES.charAt(random.nextInt(UPPER_CASES.length())));
password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
password.append(SPECIAL_CHARS.charAt(random.nextInt(SPECIAL_CHARS.length())));
// 모든 가능한 문자를 포함하는 문자열
String allPossibleChars = LOWER_CASES + UPPER_CASES + DIGITS + SPECIAL_CHARS;
// 나머지 길이를 채우기
for (int i = 4; i < MIN_LENGTH; i++) {
password.append(allPossibleChars.charAt(random.nextInt(allPossibleChars.length())));
}
// 문자열 섞기
ArrayList<Character> pwdChars = new ArrayList<>();
for (char c : password.toString().toCharArray()) {
pwdChars.add(c);
}
Collections.shuffle(pwdChars);
StringBuilder finalPassword = new StringBuilder();
for (char c : pwdChars) {
finalPassword.append(c);
}
return finalPassword.toString();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

위 구성 형태가 더 깔끔하고 가독성도 좋은 것 같습니다. 앞서 언급했던 return문 관련 코멘트도 고려하여 수정하겠습니다.

Comment on lines 23 to 38
while(true){
sb.setLength(0);
for(int i=0; i<length; i++){
int choice = random.nextInt(4);

if(choice == 0){
sb.append(getRandomCharacterFromSrc(LOWER, random));
continue;
}

if(choice==1){
sb.append(getRandomCharacterFromSrc(UPPER, random));
continue;
}

if(choice==2){
Copy link
Contributor

Choose a reason for hiding this comment

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

코드 컨벤션 생각해주세요.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

수정하겠습니다! 앞으로 유의하겠습니다.

임시 비밀번호는 다음 과정을 거쳐 생성된다.
1. 각 카테고리(영어 대소문자, 숫자, 특수문자)에서 한 글자씩 랜덤 선택
2. 모든 가능한 문자를 포함하는 문자열 형성
3. 앞서 생성한 문자열에서 나머지 길이를 채울 문자 랜덤 선택
4. 선택된 문자들 섞기
5. 최종 임시 비밀번호 생성

- 랜덤한 문자를 선택하는 부분은 spring boot에서 기본 제공되는 RandomStringUtils를 활용
- 문자열을 섞는 부분에서 shuffle을 위해 가변 컬렉션을 생성하려 새로운 List 할당
메일 서비스 제공 호스트와 포트 번호도 application.properties에서
관리하는 형태로 수정
Copy link
Contributor

@acceptor-gyu acceptor-gyu left a comment

Choose a reason for hiding this comment

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

제가 잘 몰라서 그러는데 spring mail을 사용하려면 application.yml 파일에
spring:
mail:
host:
...
과 같은 형식이 들어가야 하는 건가요?

Comment on lines 20 to 21
String subject,
String text){
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
String subject,
String text){
String subject,
String text) {

Comment on lines 3 to 6
public enum ServiceRegex {

EMAIL("[a-zA-Z0-9_!#$%&’*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"),
PASSWORD("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[~#@!]).{8,20}$");
Copy link
Contributor

Choose a reason for hiding this comment

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

조금 더 직관적인 이름을 사용하는 것은 어떤가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

클래스명 수정하고 반영하겠습니다!

Comment on lines 14 to 18
mail:
host: ${GOOGLE_SMTP_HOST}
port: ${GOOGLE_SMTP_PORT}
username: ${GOOGLE_SMTP_USERNAME}
password: ${GOOGLE_SMTP_PASSWORD}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
mail:
host: ${GOOGLE_SMTP_HOST}
port: ${GOOGLE_SMTP_PORT}
username: ${GOOGLE_SMTP_USERNAME}
password: ${GOOGLE_SMTP_PASSWORD}

Comment on lines 13 to 23
@Value("${spring.mail.host}")
private String serverHost;

@Value("${spring.mail.port}")
private int serverPort;

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

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

Choose a reason for hiding this comment

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

Suggested change
@Value("${spring.mail.host}")
private String serverHost;
@Value("${spring.mail.port}")
private int serverPort;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${GOOGLE_SMTP_HOST}")
private String serverHost;
@Value("${GOOGLE_SMTP_PORT}")
private int serverPort;
@Value("${GOOGLE_SMTP_USERNAME}")
private String username;
@Value("${GOOGLE_SMTP_PASSWORD}")
private String password;

Comment on lines 82 to 83
@RequestBody @Valid UserPasswordInquiryRequest request) {

Copy link
Contributor

Choose a reason for hiding this comment

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

제가 생각할 때는 @RequestBody UserPasswordInquiryRequest를 하나로 보고 이것을 @Valid로 검증한다라는 의미로 생각하는게 자연스러운데 어떻게 생각하시나요?

Suggested change
@RequestBody @Valid UserPasswordInquiryRequest request) {
@Valid @RequestBody UserPasswordInquiryRequest request) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

로직 흐름상 더 자연스러운 것 같습니다. 반영하겠습니다!

# Conflicts:
#	be/src/main/java/yeonba/be/mypage/dto/response/UserProfileDetailResponse.java
#	be/src/main/java/yeonba/be/user/dto/response/UserProfileResponse.java
#	be/src/main/java/yeonba/be/user/entity/User.java
@Minjae-An
Copy link
Contributor Author

application.yml에서 메일 관련 프로퍼티들을 따로 관리하지 않으면, 메일 전송과 관련된 코드에서 해당 설정들이 하드 코딩된 형태로 작성되게 됩니다. 따라서 application.yml에 값을 명시하고 가져다 쓰는 방식이 유지보수에 용이하다고 판단하여 이런 형태로 구성하였습니다.

어플리케이션 전역에서 검증을 위해 사용되는 정규식들을 모아놓는 용도를 잘 표현하기 위해
클래스명 수정
사용자 지정 속성을 사용한다는 맥락을 잘 나타낼 수 있도록 수정
요청 바디에서 읽어온 데이터를 검증한다는 흐름에서 @Valid 어노테이션이
@RequestBody보다 먼저 위치하도록 코드 수정
@acceptor-gyu
Copy link
Contributor

application.yml에서 메일 관련 프로퍼티들을 따로 관리하지 않으면, 메일 전송과 관련된 코드에서 해당 설정들이 하드 코딩된 형태로 작성되게 됩니다. 따라서 application.yml에 값을 명시하고 가져다 쓰는 방식이 유지보수에 용이하다고 판단하여 이런 형태로 구성하였습니다.

  • application.properties에서 관리하는데 application.yml에서도 관리해야 하는 이유가 있는지 궁금합니다.

@Minjae-An
Copy link
Contributor Author

application.propertiesapplication.yml의 우선순위에 관해 제가 잘못 인식하고 있는 부분이 있어 보완하였습니다. 감사합니다!

Comment on lines +21 to +27
/*
임시 비밀번호는 다음 과정을 거친다.
1. 요청 이메일 기반 사용자 조회
2. 임시 비밀번호 생성
3. 사용자 비밀번호, 임시 비밀번호로 변경
4. 임시 비밀번호 발급 메일 전송
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

?

@acceptor-gyu acceptor-gyu merged commit afa1330 into dev Mar 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨ feat 새로운 기능 추가
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[BE] 임시 비밀번호 발급 및 전송 비즈니스 로직 및 API 구성
2 participants