@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : user")
public @interface AuthUser {
Controller layer 에서 매개변수에 어노테이션을 사용하여 편리하게 인증 유저 정보를 받아오기 위해 @AuthenticationPrincipal
어노테이션을 사용하여 Custom 어노테이션(@AuthUser
)을 만들어 사용했습니다.
위 코드 덕분에 쉽게 유저 객체를 얻어올 수 있었는데요. 구현 중에 발생한 문제점 및 개선 방안에 대해서 설명해보도록 하겠습니다.
먼저 @AuthenticationPrincipal
의 expression 매개변수에 작성한 SpEL 표현식에 대해서 설명하겠습니다.
SpEL 이란? (Spring Expression Language)
SpEL은 Spring Expression Language의 약자로 스프링 표현식입니다.
객체 그래프를 조회하고 조작하는 기능을 제공합니다. Unified EL 과 비슷하지만, 메소드 호출을 지원하며, 문자열 템플릿 기능도 제공합니다.
OGNL, MVEL, JBOss EL 등 자바에서 사용할 수 있는 여러 EL이 있지만, SpEL은 모든 스프링 프로젝트 전반에 걸쳐 사용할 EL로 만들었습니다.(스프링 3.0 부터 지원하는 표현식입니다.)
스프링 시큐리티에서도 SpEL을 많이 사용합니다. 문자열로 구성된 표현식을 통해 자바 코드의 객체나 메서드를 사용하고, and나 or 연산을 지원하여 권한에 따른 조건문 작성도 쉽게 할 수 있습니다.
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : user")
SpEL 표현식에선 # 을 앞에 붙여 변수를 참조할 수 있는데, #this는 항상 현재 평가하고 있는 객체를 참조합니다.
@AuthenticationPrincipal 에선 인증되지 않은 객체에 대해 ‘anonymousUser’ 문자열을 return 하는데,
이를 이용하여 현재 전달된 객체가 인증되지 않았으면 null을 파라미터에 담고, 인증된 객체라면 user 객체로 담는 식으로 표현을 하였습니다.
인증되지 않은 객체에 대해 null을 User 객체에 담으니 개발 중에 한가지 문제점을 발견하였습니다.
해당 문제점은 프로젝트 요구사항에 따른 API 명세서를 참고하였을 때, anonymous 유저(인증하지 않은 유저)와 인증한 유저에 대해서 접근을 허용하는 API가 존재하였기 때문에 발생하였는데요,
때문에 Controller 로 부터 User 객체를 전달받은 Service 메서드에서 user가 null 인지 아닌지에 대한 체크를 하고 null 이 아닌 경우, user.getId()
를 Repository 메서드에 전달해주는 코드를 모든 메서드에서 해줘야 했습니다.
if(user == null){
XXXRepository.XXX();
}
else{
XXXRepository.XXX(user.getId(),...);
}
위 코드에서 동일한 Repository 메서드를 사용하여 if 조건문을 없앨 수 있는 방법이 무엇일까 고민을 하게 되었습니다.
위에서 발생한 문제점을 해결하기 위해 User 객체를 상속받는 EmptyUser 클래스를 구현하였습니다.
그리고 @AuthenticationPrincipal
어노테이션의 Custom 어노테이션인 @AuthUser
에서 SpEL 표현식을 다음과 같이 수정하였습니다.
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? new kdt.prgrms.kazedon.everevent.domain.user.EmptyUser() : user")
public @interface AuthUser {
위와 같이 수정하면 anonymous 유저일 경우 User 객체엔 EmptyUser 객체가 담기게 됩니다.
이렇게 수정하고 다시 서비스 메서드들을 살펴보도록 하겠습니다.
public void serviceMethod(User user, ... ) {
XXXRepository.XXX(user.getId(),...);
}
EmptyUser는 모든 필드 값을 null로 갖고 있기 때문에, user.getId()를 하더라도 NullPointException이 발생하지 않고 repository 메서드에 null이 전달되게 됩니다. 또한 코드도 간결해진 것도 확인할 수 있는데요. repository 메서드도 2개로 따로 분리하지 않고 하나에서 관리할 수 있다는 점도 개선점으로 확인할 수 있습니다.
@AuthUser을 사용하면서 발생한 문제점을 해결하고, 개선된 사항은 다음과 같습니다.
문제점
개선 사항
@AuthUser 메서드를 사용하면서 여러가지 고민들을 많이 하게 되었습니다.
[Spring Security] @AuthenticationPrincipal 어노테이션은 어떻게 동작할까??
[Spring Security] @AuthenticationPrincipal 로그인한 사용자 정보 받아오기
@AuthenticationPrincipal - 현재 사용자 조회하기
[Spring-Security] @AuthenticationPrincipal에 대하여
Spring Security - @AuthenticationPrincipal
SpEL