-
Notifications
You must be signed in to change notification settings - Fork 0
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
실습을 위한 레거시 코드 추가 #3
base: main
Are you sure you want to change the base?
Conversation
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
메서드
- refresh token으로
Authentication
객체를 생성한다. - 검증 후 로그인 토큰을 생성한다.
👉🏻
reissueToken
메서드와generateRefreshCookie
메서드 모두 모든 작업이 token 관련 객체에 의해 수행되고 있다.
2. 다른 클래스와 강한 결합
의존하는 모든 클래스가 인터페이스
가 아닌 구현
이기 때문에, UserLoginService
는 다른 클래스와 강한 결합을 형성하고 있다.
🔧 해결 방안
1. 역할 분리
토큰 관리 역할 분리
토큰을 생성 및 검증하는 역할을 별도의 클래스로 분리한다. 👉🏻 JwtTokenService
public class JwtTokenService {
private final JwtTokenProvider jwtTokenProvider;
private final JwtTokenValidator jwtTokenValidator;
private final JwtTokenResolver jwtTokenResolver;
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);
}
public ResponseCookie generateRefreshCookie(final TokenDto tokenDto) {
return jwtTokenProvider.generateRefreshCookie(
tokenDto.getRefreshToken(),
jwtTokenResolver.getAutoLogin(tokenDto.getAccessToken())
);
}
}
토큰 생성 및 검증에 필요한 세부사항들을
JwtTokenService
클래스로 캡슐화
👉🏻 토큰 관련 작업에 있어서, 외부에서는 관련 세부사항에 신경쓰지 않아도 된다. (= 메시지, 인터페이스만 알면 된다)
인증/인가 역할로 단순화: 유저 로그인은 협력
으로 해결한다.
login
메서드를 확인해보면 UserLoginService
클래스는 여러 클래스에 의존하여 로그인에 필요한 토큰을 생성하는 것을 확인할 수 있다.
👉🏻 주어진 단 하나의 역할만을 수행하고, 다른 객체와 협력할 수 있는 구조를 갖고 있다고는 보기 어렵다. 즉, 한 가지 문제에 대해서
login
메서드 내부에서 혼자 해결하고 있음.
앞서 토큰을 생성 및 검증하는 역할은 JwtTokenService
에 위임했으므로, UserLoginService
클래스의 역할은 아래와 같이 정의한다.
- 로그인 요청을 검증하고,
- 이를 기반으로
Authentication
객체를 생성해서 반환하는 역할
이러한 역할을 명확히 할 수 있도록 클래스 명을 UserAuthService
로 변경하고, 아래와 같이 수정한다.
public class UserAuthService {
private final UserService userService;
private final EmailManager emailManager;
private final PasswordEncoder passwordEncoder;
public Authentication generateAuthentication(ReqLoginDto reqLoginDto) {
final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(reqLoginDto.getEmail(), reqLoginDto.getPassword());
return authenticationManagerBuilder.getObject().authenticate(authenticationToken);
}
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);
}
}
코드를 작성했을 당시, 파사드 패턴을 적용하여 해당 클래스가 하위 서비스 계층인 UserService
에 의존하는 것을 볼 수 있다.
이 클래스에서는
userService
클래스의findByEmail
메소드만 활용하고 있으며, 파라미터를 다음과 같이 수정하여UserService
에 대한 의존성 또한 줄여줄 수 있을 것 같다.
public class UserAuthService {
private final EmailManager emailManager;
private final PasswordEncoder passwordEncoder;
public Authentication generateAuthentication(ReqLoginDto reqLoginDto) {
final UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(reqLoginDto.getEmail(), reqLoginDto.getPassword());
return authenticationManagerBuilder.getObject().authenticate(authenticationToken);
}
public void passwordCheck(User user, ReqPasswordDto reqPasswordDto){
if(passwordEncoder.matches(reqPasswordDto.getPassword(), user.getPassword())) {
return;
}
throw new UnAuthenticationException(ResponseMessage.WRONG_PASSWORD);
}
public void findPassword(User user){
String temporaryPassword = emailManager.temporaryPasswordEmail(user.getPassword());
changePassword(user, temporaryPassword);
}
}
👉🏻 즉,
UserDetails
를User
객체로 변환하는 과정은 전적으로UserService
에 맡긴다.
이렇게 되면, 아래와 같이 협력
으로 문제를 해결할 수 있을 것이다.
UserService
에UserDetails
를 넘긴다.UserService
는 이에 해당하는User
객체를 반환한다.- 이를
UserAuthService
에 넘겨주어 비밀번호 관련 작업을 처리한다.
2. 인터페이스로 느슨한 결합
이제 UserAuthService
(구: UserLoginService
) 클래스가 의존하는 구현체는 EmailManager
가 유일하다. (PasswordEncoder
는 인터페이스로 구현된 상태)
아래와 같이 EmailManager
의 인터페이스와 구현을 분리시켜서, 실제 이메일 전송 서비스가 연결되지 않아도 단위 테스트를 수행할 수 있도록 개선한다.
인터페이스 분리
public interface EmailManager {
public String joinEmail(String email);
public String temporaryPasswordEmail(String email);
public void checkCode(ReqCodeDto reqCodeDto) throws UnAuthenticationException;
}
구현
public class JavaEmailManager implements EmailManager {
private final JavaMailSender mailSender;
private final RedisManager redisManager;
@Override
public String joinEmail(String email) {
// ...
}
@Override
public String temporaryPasswordEmail(String email) {
// ...
}
@Override
public void checkCode(ReqCodeDto reqCodeDto) throws UnAuthenticationException {
// ...
}
}
마찬가지로 JwtTokenService
가 의존하는 Provider
, Resolver
, Validator
를 추상화해서 JwtTokenService
의 단위 테스트를 개선할 수 있을 것 같다.
인터페이스가 과도하게 많아지는 느낌이긴 한데 ,, 이 해결책에 대해서는 확신이 없음
👀 기대 효과
역할을 명확하게 분리하여 객체 간 협력을 통해 문제를 해결할 수 있도록 변경
👉🏻 각각의 문제 해결 구현에 변동이 생기더라도, 외부에는 영향을 전파시키지 않을 수 있을 것이다.
👉🏻 명확한 역할 분리로, 캡슐화의 이점을 얻을 수 있으며 동시에 서로의 의존성을 줄이는 효과를 얻을 수 있음.
인터페이스로 느슨한 결합 형성
👉🏻 이메일 같은 경우에는, 이메일 전송 서비스가 변경되는 등의 세부사항 변경에 외부 클래스가 영향을 받지 않을 것이다.
👉🏻 이메일 서비스와의 강한 결합으로 인해, 단위 테스트가 까다로웠으나 인터페이스로 느슨한 결합을 형성하여 테스트 객체를 직접 생성할 수 있음. 즉, 단위 테스트가 수월해짐
No description provided.