커스텀 어노테이션을 사용해 컨트롤러에서 유저 확인하기(@UserInfo)

김코·2025년 7월 12일

User 어노테이션

목록 보기
1/2
post-thumbnail

현재 상황

@GetMapping("/{userId}")
    @Operation(summary = "프로필 조회")
    public CustomResponse<?> getProfile(@PathVariable("userId") Long userId, @AuthenticationPrincipal PrincipalDetails userDetails) {

        User getUser = profileQueryService.getProfile(userId);

        if (!getUser.getEmail().equals(userDetails.getUsername())) {
            // 현재 로그인한 사용자의 이메일과 조회하려는 사용자의 이메일이 다를 경우
            return CustomResponse.onFail(UserErrorCode.UNAUTHORIZED_USER);
        }
     ...

로그인된 유저의 정보를 담고 있는 객체인 PrincipalDetails와 userId의 이메일을 이용해 두 사용자가 같은지 확인하고 있다.



이에 따른 응답값은 다음과 같다.

{
  "isSuccess": false,
  "status": "UNAUTHORIZED",
  "code": "USER401",
  "message": "인증되지 않은 사용자입니다."
}

크게 문제는 없지만 service를 이용해 프로필을 얻어오고 로그인한 사용자와 비교하는 과정은 번거롭다고 느꼈고 따라서 사용자의 정보를 컨트롤러에서 쉽게 파악하기 위해, 커스텀 어노테이션을 만들기로 했다.

사용자 정보를 인지하는 커스텀 어노테이션 만들기

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Parameter(hidden = true)
public @interface UserInfo {
}

먼저 User 정보를 가져온다는 의미로 커스텀 어노테이션인 UserInfo를 만든다.

  • @Target(ElementType.PARAMETER): 어노테이션이 적용될 수 있는 요소의 종류를 선택하는 것으로써, ElementType.PARAMTEER을 사용해 파라미터에만 사용할 수 있도록 지정.

  • @Retention(RetentionPolicy.RUNTIME): 어노테이션 정보를 언제까지 보관할지를 결정하는 것으로써, RententionPolicy.RUNTIME울 사용해 런타임에도 어노테이션 정보가 남아 리플렉션 즉, 실행 중에 어노테이션 정보를 읽고 사용할 수 있게 하는 것.

  • @Documented: javaDoc에 포함될 수 있도록하여, API 문서 관련해서 이 어노테이션이 붙은 파라미터임을 문서에서 확인할 수 있도록 한다.

  • @Paramter(hidden=true): api 관련해서 스웨거를 사용하고 있기에, UserInfo의 파라미터를 API 명세에서 숨기고자 사용. 이는 인증처리가 내부에서 처리하기에 파라미터를 노출시킬 필요가 없었다.


@Component
@RequiredArgsConstructor
public class UserInfoResolver implements HandlerMethodArgumentResolver {

    private final UserRepository userRepository;


    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UserInfo.class) &&
                parameter.getParameterType().equals(User.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && authentication.isAuthenticated()) {
                String email = authentication.getName();
                return userRepository.findByEmail(email)
                        .orElseThrow(() -> new UserCustomException(UserErrorCode.USER_NOT_FOUND));
            }

        throw new UserCustomException(UserErrorCode.UNAUTHORIZED_USER);
    }
}

두 번째로 @UserInfo 어노테이션이 붙은 곳에 로그인한 사용자의 정보를 주입할 수 있게 HandlerMethodArgumentResolver를 구현하도록 한다. 즉, @UserInfo와 User 객체를 연동하는 것이다.

  • HandlerMethodArgumentResolver를 implements하는: UserInfoResolver가 컨트롤러의 파라미터를 원하는 값으로 바인딩해줄 수 있도록 한다.
  • supportsParamemter: 파라미터를 처리할 수 있는지 판단하도록 한다. 여기서는 파라미터 타입이 User일 때 true를 리턴.
  • resolveArgument: 위에서 true를 반환하게 되면 어떤 값을 주입할지 결정하게 된다. SecurityContextHolder를 통해 인증 정보를 가져오고, 사용자 이메일을 통해 UserRepository를 조회한다. 여기서 유저 정보가 있으면 반환하지만 아니라면 예외를 발생시키도록 하였다.


@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final UserInfoResolver userInfoResolver;

    @Bean
    public WebClient webClient() { // 기존 코드
        return WebClient.builder().build();
    }

    @Bean
    public RestTemplate restTemplate() { // 기존 코드
        return new RestTemplate();
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userInfoResolver);
    }
}

UserInfoResolver 클래스가 작동되도록 하기 위해 스프링 mvc의 동작을 제어하는 WebConfig에서 WebMvcConfigurer을 구현함으로써, addArgumentResolver를 오버라이딩하여 userInfoResolver를 add 해준다.


	@GetMapping("")
    @Operation(summary = "프로필 조회")
    public CustomResponse<?> getProfile(@UserInfo User user) {

        User getUser = profileQueryService.getProfile(user.getId());

        return CustomResponse.onSuccess(GeneralSuccessCode.OK, ProfileConverter.from(getUser));

    }

따라서 위와 같이 @UserInfo를 사용해 유저를 굳이 서비스 쪽에서 찾지 않고 로직을 실행할 수 있도록 한다. 그런데 여기서 user.getId()를 조회한다. 이렇게 되면 getProfile에서 불필요한 쿼리가 발생되기에 @UserInfo를 사용했으므로 사용자의 정보를 바로 반환될 수 있도록 힌다.

 @GetMapping("")
    @Operation(summary = "프로필 조회")
    public CustomResponse<?> getProfile(@UserInfo User user) {

        return CustomResponse.onSuccess(GeneralSuccessCode.OK, ProfileConverter.from(user));

    }

프로젝트를 진행하면서 userId 사용이 빈번해 @UserInfo 뿐만 아니라 @UserId가 있으면 더 편리하겠다고 생각하였고 @UserInfo와 유사한 방식으로 @UserId를 생성하였다.

profile
백엔드 공부하는 코린이입니다

0개의 댓글