
이번 글에서는 MyBoard 프로젝트에서 로그인한 사용자의 정보를 간편하게 가져오는 방법으로 Spring Security의 @AuthenticationPrincipal 어노테이션을 사용하는 방법과 과정을 기록하려고 합니다.
게시글 작성 및 수정, 댓글 작성, 좋아요 등의 기능은 로그인한 사용자만 이용할 수 있도록 설계되었습니다.
따라서 로그인한 사용자의 정보를 간단하고 효과적으로 가져오는 방법이 필요했습니다.
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
이 방법은 직접적으로 Spring Security가 관리하는 Principal 또는 UserDetails 객체를 가져옵니다. 다만, 이 방법을 자주 사용할 경우 코드 중복이 많아지고 가독성이 떨어지는 단점이 있습니다.
@PostMapping("/articles")
public ResponseEntity<ArticleResponse> createArticle(
@RequestBody CreateArticleRequest request,
@AuthenticationPrincipal CustomUserDetails principal
) {
ArticleResponse response = articleFacade.save(request, principal.getId());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
두 번째 방법은 보다 깔끔하고 유지보수성이 뛰어나며 코드의 중복을 방지할 수 있습니다.
이번 프로젝트에서는 두 번째 방법을 사용했으며, 이 과정에서 Spring MVC의 ArgumentResolver 역할 및 구현체와 Spring Security의 인증 구조를 함께 정리하겠습니다.
@AuthenticationPrincipal을 처리하는 핵심은 바로 AuthenticationPrincipalArgumentResolver 입니다.

Spring Security 4.0부터 제공되며, 최근에는 org.springframework.security.web.method.annotation 패키지에서 제공됩니다.

AuthenticationPrincipalArgumentResolver는 컨트롤러의 파라미터에서 @AuthenticationPrincipal 어노테이션이 붙어있는지를 확인하고, Spring Security의 SecurityContextHolder로부터 현재 로그인한 사용자의 정보를 가져와 자동으로 주입하는 역할을 하는 HandlerMethodArgumentResolver 구현체입니다.
ArgumentResolver 라는 네이밍이 낯이 있어서 이전에 공부했던 강의를 다시 찾아봤습니다.
ArgumentResolver라고 부르던 것이 바로 HandlerMethodArgumentResolver였다…HandlerMethodArgumentResolver 를 상속받은 구현체는 대략 30개가 넘는것 같습니다.
그 중에서도 오늘 정리할 AuthenticationPrincipalArgumentResolver가 있는것을 확인할 수 있었습니다.
ArgumentResolver가 처리할 수 있는 어노테이션 기반의 요청인 파라미터는 아래 스프링 공식 레퍼런스에 나와있습니다.
Method Arguments :: Spring Framework
위 공식 레퍼런스의 @AuthenticationPrincipal 에 대한 정보입니다.

Currently authenticated user — possibly a specific
Principalimplementation class if known.
"현재 인증된 사용자(Principal)”를 가리키며, 상황에 따라 특정 Principal 구현 클래스로 주입될 수도 있다.
Note that this argument is not resolved eagerly, if it is annotated in order to allow a custom resolver to resolve it before falling back on default resolution via
HttpServletRequest#getUserPrincipal.For example, the Spring Security
AuthenticationimplementsPrincipaland would be injected as such viaHttpServletRequest#getUserPrincipal, unless it is also annotated with@AuthenticationPrincipalin which case it is resolved by a custom Spring Security resolver throughAuthentication#getPrincipal.
이 매개변수(컨트롤러 파라미터)는 지연(lazy) 방식으로 값이 결정된다.
즉, 파라미터에 특정 어노테이션이 붙어 있으면 먼저 커스텀 ArgumentResolver가 처리할 기회를 갖고, 만약 커스텀 처리기가 없으면 기본 동작( HttpServletRequest#getUserPrincipal)으로 넘어간다.
예를 들어 Spring Security의 Authentication 객체는 Principal을 구현하므로, 일반적으로는 HttpServletRequest#getUserPrincipal을 통해 그대로 주입된다.
하지만 파라미터에 @AuthenticationPrincipal이 붙어 있으면 Spring Security 전용 ArgumentResolver가 동작하여 Authentication#getPrincipal 값을 주입한다.
Spring Security 전용 ArgumentResolver가 바로 AuthenticationPrincipalArgumentResolver인 것 같습니다.

supprotsParameter(): 파라미터에 @AuthenticationPrincipal이 붙어 있는지 확인합니다
주어진 메소드의 파라미터가 ArgumentResolver에서 지원하는 타입인지 검사합니다. 지원하면 true, 그렇지 않으면 false를 반환합니다.
resolverArgument(): SecurityContext에서 principal을 꺼내 타입 캐스팅 후 반환합니다

Authentication 또는 principal 자체가 null → null 반환null 반환,@AuthenticationPrincipal(errorOnInvalidType = true) 라면 ClassCastException 발생그렇다면, HandlerMethodArgumentResolver의 resolvedArguement를 오버라이딩한 AuthenticationPrincipalArgumentResolver 의 메서드를 확인해보겠습니다.
빨간 네모박스에서, SecuriyContext 에서 꺼내온 객체를 기반으로 null이 아니라면 Principal을 꺼내오는 코드를 확인할 수 있습니다.
또한 해당 어노테이션이 붙은 파라미터가 존재하는지 확인한 후, Principal객체를 반환합니다.
자주 사용된다면, 아래와 같이 별도의 메타-애너테이션을 정의해서 더욱 간결하게 사용할 수 있습니다.
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal
public @interface CurrentUser {
}
이를 적용하면 컨트롤러에서 더욱 직관적으로 로그인한 사용자 객체를 받을 수 있습니다.
@Controller
public class MyController {
@GetMapping("/profile")
public String profile(@CurrentUser CustomUserDetails userDetails) {
// 로그인한 사용자 정보 사용
}
}
MyBoard 프로젝트에서는 로그인 사용자 정보를 CustomUserDetails로 관리합니다.
초기에는 User JPA 엔티티가 직접 UserDetails를 구현하도록 하였으나, 이 방법은 JPA 엔티티가 프레젠테이션 계층까지 직접 노출되어 트랜잭션 문제나 지연 로딩 문제, 프록시 객체 문제 등의 여러 가지 부작용을 유발할 수 있었습니다.
이러한 문제를 해결하고자 별도의 객체로 책임을 명확히 나눈 것이 바로 CustomUserDetails입니다.
@Getter
@EqualsAndHashCode
@RequiredArgsConstructor
@Builder
public class CustomUserDetails implements UserDetails {
private final Long id;
private final String email;
private final String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean enabled;
public static CustomUserDetails from(User user) {
return CustomUserDetails.builder()
.id(user.getId())
.email(user.getEmail())
.password(user.getPassword())
.username(user.getUsername())
.authorities(List.of(new SimpleGrantedAuthority("ROLE_USER")))
.enabled(true)
.build();
}
}
이 클래스를 통해 인증 계층과 데이터베이스 계층을 명확히 분리하며, UserDetails를 효과적으로 구현하여 Spring Security가 제공하는 인증·인가 기능을 최대한 활용할 수 있게 되었습니다.