실습을 위한 레거시 코드 추가 - #3
Conversation
There was a problem hiding this comment.
🔍 문제점
해당 클래스로 단위 테스트를 한다고 가정했을 때, 테스트 과정이 매우 복잡해진다는 것을 짐작할 수 있다.
그리고 테스트가 어려운 이유로 아래 두 가지를 꼽았다.
- 너무 많은 역할을 담당
- 다른 클래스와 강한 결합 (👉🏻
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.