
@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 객체를 연동하는 것이다.
@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를 생성하였다.