커스텀 에너테이션으로 중복되는 코드 제거하기

Regular Kim·2025년 6월 6일
0

Reelvy

목록 보기
11/11

@LoginUser + HandlerMethodArgumentResolver로 반복되는 유저 조회 로직 제거하기

서비스 메서드마다 반복적으로 등장하는 아래와 같은 유저 조회 로직이 있다.

User user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);

service 메서드마다 요청한 유저의 엔티티를 찾는 로직이 계속 반복되기 때문에 수정을 진행했다.

개선 전

public class VideoService {  

    public VideoResponse uploadVideo(VideoUploadRequest videoUploadRequest, UserDetails userDetails) {  
        User user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);  
        // .. logic
    }  
  
    public VideoResponse changeVideoStatus(Long videoId, VideoStatusChangeRequest videoStatusChangeRequest, UserDetails userDetails) {  
        User user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);  
        // .. logic
    }  
  
    public void changeVideosStatus(VideosStatusChangeRequest videosStatusChangeRequest,  
                                   UserDetails userDetails) {  
  
        User user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);  
		// .. logic
    }  
  
    public VideoResponse updateVideoInfo(Long videoId, VideoUpdateRequest videoUpdateRequestDto, UserDetails userDetails) {   
        User user = userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);  
        // .. logic
    }  
}

위에서도 설명했지만 메서드마다 유저 조회하는 로직이 계속해서 반복된다. 줄일 수 없을까...? 개선할 수 있다. 스프링의 HandlerMethodArgumentResolver 를 사용하면 된다. 구현부를 살펴보기 전에 어떻게 개선되었는지 결과부터 살펴보자.

개선 후

VideoService

public class VideoService {  

	public VideoResponse uploadVideo(VideoUploadRequest videoUploadRequest, User user) {  
	    // .. logic
	}
  
    public VideoResponse changeVideoStatus(Long videoId, VideoStatusChangeRequest videoStatusChangeRequest, User user) {  
        // .. logic
    }  
  
    public void changeVideosStatus(VideosStatusChangeRequest videosStatusChangeRequest,  
                                   User user) {  
		// .. logic
    }  
  
    public VideoResponse updateVideoInfo(Long videoId, VideoUpdateRequest videoUpdateRequestDto, User user) {   
        // .. logic
    }  
}

서비스 메서드들이 파라미터로 User 엔티티를 그대로 받도록 변경되었다. 유저가 null 만 아니라면 로직이 잘 동작한다. 그럼 이런 의문이 든다. 서비스에서 유저를 찾는게 아니라 컨트롤러 단에서 유저를 찾고 서비스로 유저 엔티티를 넘겨주는거 아님? 컨트롤러를 살펴보자.

VideoPermittedController

@RestController  
@RequiredArgsConstructor  
@RequestMapping("/v1/videos")  
@PreAuthorize("hasRole('USER')")  
public class VideoPermittedController {  
  
    private final VideoService videoService;  
  
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)  
    public ResponseEntity<VideoResponse> uploadVideo(@ModelAttribute @Valid VideoUploadRequest videoUploadRequest,  
                                         @LoginUser User user) {  
       return ResponseEntity.status(HttpStatus.CREATED).body(videoService.uploadVideo(videoUploadRequest, user));  
    }  
  
    @PatchMapping("/{videoId}/info")  
    public ResponseEntity<VideoResponse> updateVideo(@PathVariable Long videoId,  
                                                     @Valid @RequestBody VideoUpdateRequest videoUpdateRequestDto,  
                                         @LoginUser User user) {  
       return ResponseEntity.ok(videoService.updateVideoInfo(videoId, videoUpdateRequestDto, user));  
    }  
  
    @PatchMapping("/{videoId}/status")  
    public ResponseEntity<VideoResponse> changeVideoStatus(@PathVariable Long videoId,  
                                                           @Valid @RequestBody VideoStatusChangeRequest videoStatusChangeRequest,  
                                              @LoginUser User user) {  
       return ResponseEntity.ok(videoService.changeVideoStatus(videoId, videoStatusChangeRequest, user));  
    }  
  
    @PatchMapping("/status")  
    public ResponseEntity<Void> changeVideosStatus(@RequestBody VideosStatusChangeRequest videosStatusChangeRequest,  
                                        @LoginUser User user) {  
       videoService.changeVideosStatus(videosStatusChangeRequest, user);  
       return ResponseEntity.ok().build();  
    }  
}

컨트롤러에서도 별다른 처리 없이 User를 바로 파라미터로 입력받고 이를 서비스로 전달하고있다. @LoginUser 어노테이션이 눈에 띄는데 살펴보자.

@LoginUser

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

엥? 이게 다야?

그렇다. @LoginUser 의 내용은 이게 전부다. 별다른 로직이 없다.

@LoginUser는 마커 역할만 수행하고, 실제 인증된 유저를 찾아서 주입하는 역할은 LoginUserArgumentResolver가 담당한다. LoginUserArgumentResolverHandlerMethodArgumentResolver를 구현하는데 이 인터페이스가 중요한 부분이다. Spring MVC는 컨트롤러 메서드의 파라미터를 커스터마이징할 수 있도록 HandlerMethodArgumentResolver 기능을 제공한다. 즉, 애노테이션은 신호만 보내고, 실제 동작은 Resolver가 처리하는 구조다. 해당 인터페이스까지 구현이 된다면 반복되는 사용자 조회 코드를 제거할 수 있다. 살펴보자.

HandlerMethodArgumentResolver

@Component
@RequiredArgsConstructor
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {

    private final UserRepository userRepository;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(LoginUser.class) != null && parameter.getParameterType().equals(User.class);
        // 처리하려는 파라미터가 LoginUser 에너테이션을 갖고 있고 && 에너테이션이 수식하는 파라미터가 User 클래스라면 
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 시큐리티 컨텍스트에서 인증 정보 획득

        if (authentication == null || !(authentication.getPrincipal() instanceof UserDetails userDetails)) {
	        // 인증 정보가 없거나 위조됐다면 Spring Security의 ExceptionTranslationFilter가 401 응답으로 처리
            throw new AuthenticationCredentialsNotFoundException("No Authentication"); // 401 처리
        }

        return userRepository.findByUsername(userDetails.getUsername()).orElseThrow(NoUserFoundException::new);
        // 인증 정보대로 유저를 찾는데 해당하는 유저가 없다면 예외처리
        // 유저를 찾았다면 유저를 응답
    }
}

@LoginUser 에너테이션이 수식하는 파라미터가 있을 경우 위 리졸버가 동작하여 User를 찾고 주입해준다. 엄청나다! @LoginUser 에너테이션이 동작하기 위해서는 설정이 하나 더 필요하다.

WebConfig

@Configuration  
@RequiredArgsConstructor  
public class WebConfig implements WebMvcConfigurer {  
  
    private final LoginUserArgumentResolver loginUserArgumentResolver;  
  
    @Override  
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {  
       resolvers.add(loginUserArgumentResolver);  
    }  
}

애플리케이션에서 동작하는 리졸버 리스트에 제작한 LoginUserArgumentResolver를 등록해줘야한다.

정리

구성 요소역할
@LoginUser컨트롤러 메서드에서 유저를 파라미터로 받는 마커 역할
LoginUserArgumentResolver인증된 User를 조회하여 메서드 파라미터에 주입
WebConfigArgumentResolver를 Spring MVC에 등록
profile
What doesn't kill you, makes you stronger

0개의 댓글