@AuthUser 적용 배경(feat. SpEL 문법 수정 배경, EmptyUser)

HeoSeungYeon·2022년 1월 30일
0

Record the project

목록 보기
1/3
post-thumbnail

🗯️ 문제


@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을 사용하면서 발생한 문제점을 해결하고, 개선된 사항은 다음과 같습니다.

문제점

  • Service Layer 메서드에서 NullPointException 방지를 위해 User 객체의 null 체크를 항시 했어야 했음.
  • 때문에 repository 메서드도 많아짐.

개선 사항

  • 인증되지 않은 객체에 대해 EmptyUser() 객체를 생성하여 사용함.
  • user null 체크 if 조건문을 작성할 필요가 없음.
  • user.getId()를 해도 NullPointException이 발생하지 않고 null 값을 repository 메서드에 전달함.
  • repository 메서드도 하나로 사용할 수 있음.

회고


@AuthUser 메서드를 사용하면서 여러가지 고민들을 많이 하게 되었습니다.

  • @AuthenticationPrincipal 에서 Entity 객체인 User를 바로 받아도 될까
    • 서비스 메서드에 필요할 유저 정보가 아닌 이상, 불필요한 정보까지 넘겨줄 필요가 있을까? 라는 고민을 하게 되었습니다. 또 근본적으로 Entity 객체를 Controller 단에서 알고 있어도 될까? 에 대한 고민도 해본 것 같습니다. 해당 부분에 대해서는 더 공부해보고 리팩토링이나 포스팅을 한번 해보고 싶습니다.
  • @AuthenticationPrincipal 내부 원리에 대해서 깊이 공부해 보고 싶다.
    • 클라이언트로부터 전달받은 정보(Token)을 통해 인증 로직을 처리하는 것은 Spring Security의 Filter 단에서 수행하게 됩니다. @AuthenticationPrincipal 어노테이션도 인증 로직을 진행할 때, Principal 객체를 생성해주는 코드를 통해 인증 객체를 전달 받습니다.
  • @AuthUser로 전달받은 User를 서비스 메서드에 전달할 때, user 객체 그대로 전달할까 user.getId() 값을 전달할까
    • user 객체를 그대로 전달하는 방법을 선택하였습니다.
    • 그 이유로는 위 문제를 해결하기 전엔 user.getId()를 서비스 메서드에 전달하게 되면 controller 단에서 if문을 통해 user가 null 인지 체크해야 되는 문제가 발생하였기 때문입니다.

📖 참고문서


[Spring Security] @AuthenticationPrincipal 어노테이션은 어떻게 동작할까??

[Spring Security] @AuthenticationPrincipal 로그인한 사용자 정보 받아오기

@AuthenticationPrincipal - 현재 사용자 조회하기

[Spring-Security] @AuthenticationPrincipal에 대하여

kingsubin

Spring Security - @AuthenticationPrincipal

현재 인증된 사용자 정보 참조

SpEL

SpEL (스프링 Expression Language)

06. Controller와 View에서의 Spring Security

0개의 댓글