[클린코드 적용기] 1. 관심사 분리 (HandlerMethodArgumentResolver)

전종원·2024년 8월 6일
0
post-custom-banner

클린코드 11장 시스템에서 다룬 관심사 분리를 적용합니다.

🤔 관심사 분리를 해야 하는 이유?

/**
 * 알림함 조회
 */
@GetMapping("/messages")
public ResponseEntity<ReadFcmMessagesResponse> readFcmMessages(@Nullable @RequestParam Long lastMessageId) {
    UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    long userId = Long.parseLong(userDetails.getUsername());

    return new ResponseEntity<>(fcmService.readMessages(userId, DEFAULT_ONE_PAGE_SIZE, lastMessageId), HttpStatus.OK);
}

/**
 * 닉네임 변경
 */
@PutMapping("")
public ResponseEntity<String> updateUser(@RequestBody UpdateUserRequest request) {
    UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    long userId = Long.parseLong(userDetails.getUsername());

    userService.updateUser(request.getNickname(), userId);

    return new ResponseEntity<>("유저 업데이트 성공", HttpStatus.OK);
}

/**
 * 회원 탈퇴
 */
@DeleteMapping("")
public ResponseEntity<String> deleteUser() {
    UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    long userId = Long.parseLong(userDetails.getUsername());

    userService.deleteUser(userId);

    return new ResponseEntity<>("회원 탈퇴 성공", HttpStatus.OK);
}
  • 유저 컨트롤러 코드의 일부입니다.
  • 중복되는 코드를 확인할 수 있습니다.
    UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    long userId = Long.parseLong(userDetails.getUsername());

문제점

  1. 유지보수 용이성 ↓
    • 인증된 사용자 정보 추출 코드가 중복되어 사소한 변경에도 여러 군데에 퍼져있는 중복을 모두 수정해야 합니다.
  2. 개발 비용 ↑
    • 비즈니스 로직 상에 userId가 사용되는 경우 컨트롤러에서 매번 추출해서 전달해 주어야 합니다.
  3. 컨트롤러의 책임 ↑
    • 서비스 메서드를 호출하고, 결과를 반환하는 컨트롤러의 책임에 사용자 정보를 추출하는 책임까지 더해집니다.

기대 효과

“인증된 사용자 정보 추출” 이라는 관심사를 분리한다면?
→ 중복이 제거되어 변경 범위가 줄어들며, 유지보수하기 용이해 질 것입니다.
→ 책임이 명확히 분리될 것입니다.

💡 리팩토링 계획

  1. @AuthenticationPrincipal

    • Spring Security가 제공하는 Argument Resolver 어노테이션입니다.
    • 하지만 UserDetails의 username에 userId를 저장하고 있어 String → Long으로 매번 형변환을 해야 한다는 불편함이 존재합니다.
  2. implements HandlerMethodArgumentResolver

    • Spring에서 제공하는 Argument Resolver 인터페이스입니다.
    • Parameter 대상의 어노테이션을 정의하고 HandlerMethodArgumentResolver를 구현하는 방식을 적용한다면 보다 디테일하게 인증된 사용자 정보를 추출할 수 있을 것 같습니다.
    • 따라서 이 방식으로 관심사 분리를 구현해보겠습니다.

관심사 분리

AuthUserDTO

@Getter
@AllArgsConstructor
public class AuthUserDTO {
    private Long userId;
}

AuthUser

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthUser {
}

AuthenticationArgumentResolver

@Component
public class AuthenticationArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasAuthUserAnnotation = parameter.getParameterAnnotation(AuthUser.class) != null;
        boolean isAuthUserInfoType = parameter.getParameterType().equals(AuthUserDTO.class);

        return hasAuthUserAnnotation && isAuthUserInfoType;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return new AuthUserDTO(Long.parseLong(userDetails.getUsername()));
    }
}
  • SecurityContextHolder에서 인증된 사용자 정보를 가져와 userId를 가지고 AuthUserDTO를 생성하여 전달합니다.

WebConfig

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final AuthenticationArgumentResolver authenticationArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(authenticationArgumentResolver);
    }
}
  • 위에서 작성한 AuthenticationArgumentResolver를 등록합니다.

적용

/**
 * 알림함 조회
 */
@GetMapping("/messages")
public ResponseEntity<ReadFcmMessagesResponse> readFcmMessages(@AuthUser AuthUserDTO authUserDTO,
                                                               @Nullable @RequestParam Long lastMessageId) {
    return new ResponseEntity<>(
            fcmService.readMessages(authUserDTO.getUserId(), DEFAULT_ONE_PAGE_SIZE, lastMessageId), HttpStatus.OK);
}

/**
 * 닉네임 변경
 */
@PutMapping("")
public ResponseEntity<String> updateUser(@AuthUser AuthUserDTO authUserDTO,
                                         @RequestBody UpdateUserRequest request) {
    userService.updateUser(request.getNickname(), authUserDTO.getUserId());
    return new ResponseEntity<>("유저 업데이트 성공", HttpStatus.OK);
}

/**
 * 회원 탈퇴
 */
@DeleteMapping("")
public ResponseEntity<String> deleteUser(@AuthUser AuthUserDTO authUserDTO) {
    userService.deleteUser(authUserDTO.getUserId());
    return new ResponseEntity<>("회원 탈퇴 성공", HttpStatus.OK);
}

참고
https://gaemi606.tistory.com/entry/Spring-Boot-HandlerMethodArgumentResolver로-Authentication-정보-간단하게-받기

post-custom-banner

0개의 댓글