유저의 로그인 API 기능을 작성할 때 발생한 문제입니다. JWT 인터페이스, 쿠키 인터페이스, 캐시 인터페이스, HttpServletResponse 클래스를 사용하게 되면서 대부분의 로직이 컨트롤러 계층에서 작성되게 되었습니다.
이러한 코드 때문에 오류가 난 부분을 수정하거나 코드를 고칠 때 클래스 파일을 확인해서 일일이 코드를 수정해야 했고, mockito를 이용한 API 테스트 코드 작성 시에 어려움을 겪었습니다.
우선 로그인의 로직은 다음과 같습니다.
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid UserLoginRequestDTO userLoginRequestDTO,
HttpServletResponse httpServletResponse) {
User user = userService.findByEmail(userLoginRequestDTO.getEmail());
if (user == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
if (!passwordEncoder.matches(userLoginRequestDTO.getPassword(), user.getPassword())) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
String accessToken = jwtStrategy.createAccessToken(user.getId(), user.getRole(), user.getEmail());
String refreshToken = jwtStrategy.createRefreshToken(user.getId(), user.getRole(), user.getEmail());
AccessTokenResponseDTO accessTokenResponseDTO = new AccessTokenResponseDTO();
accessTokenResponseDTO.setAccessToken(accessToken);
Cookie cookie = cookieStrategy.createCookie(cookieRefreshTokenKey, refreshToken);
redisStrategy.setValueExpire(refreshToken, user.getEmail(), refreshTokenValidityInSeconds);
httpServletResponse.addCookie(cookie);
return new ResponseEntity(accessTokenResponseDTO, HttpStatus.OK);
}
이렇게 콘트롤러에서 대부분의 로직이 이루어집니다.
그림으로 보면 이러한 구조입니다.
이렇게 코드를 짜게 된 것의 원인은 Controller-Service-Repository 계층 구조의 역할에 혼동이 왔기 때문입니다.
JwtStrategy, CookieStrategy, RedisStrategy 인터페이스들 모두 비즈니스 로직을 담당하는 부분이니까 어떻게 보면 Service 계층이라고 생각했습니다. 그렇다면 굳이 UserService 클래스로 이동할 필요없이 Controller에서 바로 사용해도 된다고 생각했습니다.
하지만 이러한 구조는 코드 수정 시에 어려움이 많았고 클래스들에 대한 기준을 세워 문제를 해결했습니다.
Controller : API 요청과 응답을 다루는 클래스
- 단, 쿠키나 httpServlet 관련 코드는 Controller에서 이루어지도록합니다. (service 계층으로 로직을 옮기게 되면, 요청과 응답 시 어떤 것을 전달하는지 더욱 파악하기 힘들게 됩니다. 단순히 DTO를 응답시에 주는 body로 주는 구조가 아니라, 쿠키를 생성해서 servletResponse에 담기 때문에 컨트롤러 계층에 위치하게 했습니다.)
Service : 비즈니스 로직을 수행
- 모든 비즈니스 로직 코드는 Service에서 이루어지도록 합니다. Controller에서는 실제로 어떤 로직이 수행되는지 보이지 않게 되면서 테스트 코드 작성도 수월해지게 됩니다. 또, 이후 코드 수정은 Service 쪽만 확인하기만 하면합니다.
위 기준에 따라서 쿠키 헤더를 HttpServletResponse 응답에 넣어주는 부분은 Controller 클래스로,
DB를 조회해보고, 비밀번호를 확인하고,캐시에 토큰을 넣어주는 로직 부분은 Service 클래스에서 일을 수행하도록 합니다.( 그리고 controller와 service는 DTO 클래스를 통해서 소통하도록 합니다.)
그림으로 보면 다음과 같은 구조가 됩니다.
//Controller 클래스
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid UserLoginRequestDTO userLoginRequestDTO,
HttpServletResponse httpServletResponse) {
TokenResponseDTO tokenResponseDTO = userService.login(userLoginRequestDTO);
if (tokenResponseDTO == null) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
Cookie cookie = tokenResponseDTO.createTokenCookie(cookieStrategy, cookieRefreshTokenKey);
httpServletResponse.addCookie(cookie);
return new ResponseEntity(tokenResponseDTO.createAccessTokenResponseDTO(), HttpStatus.OK);
}
//Service 클래스
@Override
public TokenResponseDTO login(UserLoginRequestDTO userLoginRequestDTO) {
User user = userRepository.findByEmail(userLoginRequestDTO.getEmail());
if (user == null) {
return null;
}
if (!passwordEncoder.matches(userLoginRequestDTO.getPassword(), user.getPassword())) {
return null;
}
TokenResponseDTO tokenResponseDTO = user.createToken(tokenStrategy);
redisStrategy.setValueExpire(tokenResponseDTO.getRefreshToken(), user.getEmail(),
refreshTokenValidityInSeconds);
return tokenResponseDTO;
}
위와 같이 코드를 수정하게 되면서 이후 코드를 수정의 어려움을 줄이게 되었습니다. 하지만 코드에 정답은 없기 때문에 좀 더 나은 코드를 만들기 위해서 고민해야할 것 같습니다!