-
Notifications
You must be signed in to change notification settings - Fork 0
실습을 위한 레거시 코드 추가 #3
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
Open
minSsan
wants to merge
2
commits into
main
Choose a base branch
from
ch02-minsan-legacy
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package com.hanyang.dataportal.user.infrastructure; | ||
|
|
||
| import com.hanyang.dataportal.core.exception.UnAuthenticationException; | ||
| import com.hanyang.dataportal.core.response.ResponseMessage; | ||
| import com.hanyang.dataportal.user.dto.req.ReqCodeDto; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.mail.javamail.MimeMessageHelper; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import javax.mail.MessagingException; | ||
| import javax.mail.internet.MimeMessage; | ||
| import java.util.Objects; | ||
| import java.util.Random; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class EmailManager { | ||
| private final JavaMailSender mailSender; | ||
| private final RedisManager redisManager; | ||
| @Value("${email.setFrom}") | ||
| private String setFrom; | ||
| private static final String EMAIL_TITLE = "한양대 에리카 DATA 포털 - 회원 가입을 위한 인증 이메일"; | ||
| private static final String EMAIL_CONTENT_TEMPLATE = "한양대 에리카 DATA 포털 사이트에 가입해주셔서 감사합니다! " + | ||
| "아래의 인증번호를 입력하여 회원가입을 완료해주세요."+ | ||
| "<br><br>" + | ||
| "인증번호 %s"; | ||
| private static final String EMAIL_TITLE_TEMPORARY_PASSWORD = "한양대 에리카 DATA 포털 - 임시 비밀번호 발급"; | ||
| private static final String EMAIL_CONTENT_TEMPLATE_TEMPORARY_PASSWORD = "한양대 에리카 DATA 포털 임시 비밀번호 입니다. " + | ||
| "아래의 임시번호를 입력하여 로그인 해주세요."+ | ||
| "<br><br>" + | ||
| "임시 비밀번호 %s"; | ||
| private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||
|
|
||
| public String joinEmail(String email) { | ||
| String code = Integer.toString(makeRandomNumber()); | ||
| String content = String.format(EMAIL_CONTENT_TEMPLATE, code); | ||
| mailSend(setFrom, email, EMAIL_TITLE,content); | ||
| redisManager.setCode(email,code); | ||
| return code; | ||
| } | ||
| public String temporaryPasswordEmail(String email) { | ||
| String temporaryPassword = generateRandomString(); | ||
| String content = String.format(EMAIL_TITLE_TEMPORARY_PASSWORD, temporaryPassword); | ||
| mailSend(setFrom, email, EMAIL_CONTENT_TEMPLATE_TEMPORARY_PASSWORD,content); | ||
| redisManager.setCode(email,temporaryPassword); | ||
| return temporaryPassword; | ||
| } | ||
|
|
||
| public void checkCode(ReqCodeDto reqCodeDto) { | ||
| String code = redisManager.getCode(reqCodeDto.getEmail()); | ||
| if(!Objects.equals(code, reqCodeDto.getCode())){ | ||
| throw new UnAuthenticationException(ResponseMessage.AUTHENTICATION_FAILED); | ||
| } | ||
| } | ||
|
|
||
| private void mailSend(String setFrom, String toMail, String title, String content) { | ||
| MimeMessage message = mailSender.createMimeMessage(); | ||
| try { | ||
| MimeMessageHelper helper = new MimeMessageHelper(message,true,"utf-8"); | ||
| helper.setFrom(setFrom); | ||
| helper.setTo(toMail); | ||
| helper.setSubject(title); | ||
| helper.setText(content,true); | ||
| mailSender.send(message); | ||
| } catch (MessagingException e) { | ||
| e.printStackTrace(); | ||
| } | ||
| } | ||
|
|
||
| private int makeRandomNumber() { | ||
| Random r = new Random(); | ||
| StringBuilder randomNumber = new StringBuilder(); | ||
| for(int i = 0; i < 6; i++) { | ||
| randomNumber.append(r.nextInt(10)); | ||
| } | ||
| return Integer.parseInt(randomNumber.toString()); | ||
| } | ||
|
|
||
| private String generateRandomString() { | ||
| Random r = new Random(); | ||
| StringBuilder stringBuilder = new StringBuilder(10); | ||
| for (int i = 0; i < 10; i++) { | ||
| stringBuilder.append(CHARACTERS.charAt(r.nextInt(10))); | ||
| } | ||
| return stringBuilder.toString(); | ||
| } | ||
|
|
||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package com.hanyang.dataportal.user.service; | ||
|
|
||
| import com.hanyang.dataportal.core.exception.TokenExpiredException; | ||
| import com.hanyang.dataportal.core.exception.UnAuthenticationException; | ||
| import com.hanyang.dataportal.core.jwt.component.JwtTokenProvider; | ||
| import com.hanyang.dataportal.core.jwt.component.JwtTokenResolver; | ||
| import com.hanyang.dataportal.core.jwt.component.JwtTokenValidator; | ||
| import com.hanyang.dataportal.core.jwt.dto.TokenDto; | ||
| import com.hanyang.dataportal.core.response.ResponseMessage; | ||
| import com.hanyang.dataportal.user.domain.User; | ||
| import com.hanyang.dataportal.user.dto.req.ReqLoginDto; | ||
| import com.hanyang.dataportal.user.dto.req.ReqPasswordDto; | ||
| import com.hanyang.dataportal.user.infrastructure.EmailManager; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; | ||
| import org.springframework.security.core.Authentication; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.security.crypto.password.PasswordEncoder; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| @Transactional | ||
| public class UserLoginService { | ||
| private final AuthenticationManagerBuilder authenticationManagerBuilder; | ||
| private final JwtTokenProvider jwtTokenProvider; | ||
| private final JwtTokenValidator jwtTokenValidator; | ||
| private final JwtTokenResolver jwtTokenResolver; | ||
| private final UserService userService; | ||
| private final EmailManager emailManager; | ||
| private final PasswordEncoder passwordEncoder; | ||
|
|
||
| public TokenDto login(ReqLoginDto reqLoginDto) { | ||
| // 1. Login ID/PW 를 기반으로 Authentication 객체 생성 | ||
| // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false | ||
| final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(reqLoginDto.getEmail(), reqLoginDto.getPassword()); | ||
|
|
||
| // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분 | ||
| // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행 | ||
| final Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); | ||
|
|
||
| // JWT 토큰 생성 | ||
| return jwtTokenProvider.generateLoginToken(authentication, reqLoginDto.getAutoLogin()); | ||
| } | ||
|
|
||
| /** | ||
| * 액세스 토큰(+ 리프레시 토큰)을 재발급하는 메서드 | ||
| * @param refreshToken | ||
| * @return | ||
| * @throws TokenExpiredException | ||
| */ | ||
| public TokenDto reissueToken(final String refreshToken) throws TokenExpiredException { | ||
| final Authentication authentication = jwtTokenResolver.getAuthentication(refreshToken); | ||
| final boolean autoLogin = jwtTokenResolver.getAutoLogin(refreshToken); | ||
| if (jwtTokenValidator.validateToken(refreshToken)) { | ||
| return jwtTokenProvider.generateLoginToken(authentication, autoLogin); | ||
| } | ||
| throw new TokenExpiredException(ResponseMessage.REFRESH_EXPIRED); | ||
| } | ||
|
|
||
| /** | ||
| * refresh token 쿠키를 리턴하는 메서드 | ||
| * @param tokenDto | ||
| * @return | ||
| */ | ||
| public ResponseCookie generateRefreshCookie(final TokenDto tokenDto) { | ||
| return jwtTokenProvider.generateRefreshCookie( | ||
| tokenDto.getRefreshToken(), | ||
| jwtTokenResolver.getAutoLogin(tokenDto.getAccessToken()) | ||
| ); | ||
| } | ||
|
|
||
| public void passwordCheck(UserDetails userDetails, ReqPasswordDto reqPasswordDto){ | ||
| User user = userService.findByEmail(userDetails.getUsername()); | ||
| //일치하면 | ||
| if(passwordEncoder.matches(reqPasswordDto.getPassword(),user.getPassword())) { | ||
| return; | ||
| } | ||
| throw new UnAuthenticationException(ResponseMessage.WRONG_PASSWORD); | ||
| } | ||
| public void changePassword(UserDetails userDetails,String newPassword){ | ||
| User user = userService.findByEmail(userDetails.getUsername()); | ||
| user.updatePassword(passwordEncoder.encode(newPassword)); | ||
| } | ||
|
|
||
| public void findPassword(UserDetails userDetails){ | ||
| User user = userService.findByEmail(userDetails.getUsername()); | ||
| String temporaryPassword = emailManager.temporaryPasswordEmail(user.getPassword()); | ||
| changePassword(userDetails,temporaryPassword); | ||
| } | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 문제점
해당 클래스로 단위 테스트를 한다고 가정했을 때, 테스트 과정이 매우 복잡해진다는 것을 짐작할 수 있다.
그리고 테스트가 어려운 이유로 아래 두 가지를 꼽았다.
PasswordEncoder인터페이스를 제외한 나머지는 모두 클래스로 정의되어 있음)1. 너무 많은 역할을 담당
현재
UserLoginService클래스의 역할을 나열하면 다음과 같다.login메서드reissueToken메서드Authentication객체를 생성한다.2. 다른 클래스와 강한 결합
의존하는 모든 클래스가
인터페이스가 아닌구현이기 때문에,UserLoginService는 다른 클래스와 강한 결합을 형성하고 있다.🔧 해결 방안
1. 역할 분리
토큰 관리 역할 분리
토큰을 생성 및 검증하는 역할을 별도의 클래스로 분리한다. 👉🏻
JwtTokenService인증/인가 역할로 단순화: 유저 로그인은
협력으로 해결한다.login메서드를 확인해보면UserLoginService클래스는 여러 클래스에 의존하여 로그인에 필요한 토큰을 생성하는 것을 확인할 수 있다.앞서 토큰을 생성 및 검증하는 역할은
JwtTokenService에 위임했으므로,UserLoginService클래스의 역할은 아래와 같이 정의한다.Authentication객체를 생성해서 반환하는 역할이러한 역할을 명확히 할 수 있도록 클래스 명을
UserAuthService로 변경하고, 아래와 같이 수정한다.코드를 작성했을 당시, 파사드 패턴을 적용하여 해당 클래스가 하위 서비스 계층인
UserService에 의존하는 것을 볼 수 있다.이렇게 되면, 아래와 같이
협력으로 문제를 해결할 수 있을 것이다.UserService에UserDetails를 넘긴다.UserService는 이에 해당하는User객체를 반환한다.UserAuthService에 넘겨주어 비밀번호 관련 작업을 처리한다.2. 인터페이스로 느슨한 결합
이제
UserAuthService(구:UserLoginService) 클래스가 의존하는 구현체는EmailManager가 유일하다. (PasswordEncoder는 인터페이스로 구현된 상태)아래와 같이
EmailManager의 인터페이스와 구현을 분리시켜서, 실제 이메일 전송 서비스가 연결되지 않아도 단위 테스트를 수행할 수 있도록 개선한다.인터페이스 분리
구현
마찬가지로
JwtTokenService가 의존하는Provider,Resolver,Validator를 추상화해서JwtTokenService의 단위 테스트를 개선할 수 있을 것 같다.👀 기대 효과
역할을 명확하게 분리하여 객체 간 협력을 통해 문제를 해결할 수 있도록 변경
👉🏻 각각의 문제 해결 구현에 변동이 생기더라도, 외부에는 영향을 전파시키지 않을 수 있을 것이다.
👉🏻 명확한 역할 분리로, 캡슐화의 이점을 얻을 수 있으며 동시에 서로의 의존성을 줄이는 효과를 얻을 수 있음.
인터페이스로 느슨한 결합 형성
👉🏻 이메일 같은 경우에는, 이메일 전송 서비스가 변경되는 등의 세부사항 변경에 외부 클래스가 영향을 받지 않을 것이다.
👉🏻 이메일 서비스와의 강한 결합으로 인해, 단위 테스트가 까다로웠으나 인터페이스로 느슨한 결합을 형성하여 테스트 객체를 직접 생성할 수 있음. 즉, 단위 테스트가 수월해짐