Spring Security 인증 객체를 어떻게 가져올래?

Kevin·2023년 11월 27일
0

? 내 생각에는!

목록 보기
4/5
post-thumbnail

이번 카카오 테크 캠퍼스 활동간에 나는 아래와 같이 인증된 유저 객체를 Presentation layer로 받아오기 위해서 아래와 같은 방식을 사용했다.

 		/**
     * 산책 허락하기 메서드
     */
    @PostMapping("walk/{walkerId}/{matchingId}")
    public ApiResponse<ApiResponse.CustomBody<Void>> acceptWalk(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable("walkerId") Long userId, @PathVariable("matchingId") Long matchingId)
            throws MatchNotExistException, DuplicateNotificationWithWalkException, MemberNotExistException {
        walkService.saveWalk(customUserDetails, userId, matchingId);
        return ApiResponseGenerator.success(HttpStatus.OK);
    }

그 때 카카오 테크 캠퍼스의 같은 팀원분이 아래와 같이 Bean을 통해서 Service Layer에서 사용자 정보를 가져오는 방법에 대해서 이야기를 해주었다.

private String getEmail(){
        Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
        return loggedInUser.getName();
    }

이 때 어느 방식이 우리 프로젝트에 더 적합한 방식이었는지에 대해서 각 방식의 장, 단점을 알아보면서 비교해보고자 한다.

1️⃣ @AuthenticationPrincipal 을 이용한 방식

내가 생각한 장점을 뽑는다면 아래와 같다.

  1. 코드의 간결화와 간편성이다.

     		/**
         * 산책 허락하기 메서드
         */
        @PostMapping("walk/{walkerId}/{matchingId}")
        public ApiResponse<ApiResponse.CustomBody<Void>> acceptWalk(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable("walkerId") Long userId, @PathVariable("matchingId") Long matchingId)
                throws MatchNotExistException, DuplicateNotificationWithWalkException, MemberNotExistException {
            walkService.saveWalk(customUserDetails, userId, matchingId);
            return ApiResponseGenerator.success(HttpStatus.OK);
        }

    인증된 유저 객체를 가져오는 2번 방식과 비교해서, 직접 SecurityContextHolder에 접근해서 유저객체를 가져오지 않고 메서드 파라미터로 주입을 받으니 코드의 간결화 및 간편성 또한 증대된다.

  2. Presentation Layer에서의 직접적인 접근이 편리하다.

    Presentation Layer에서부터 메서드 파라미터로 인증된 유저 객체를 받을 수 있기 때문에 Presentation Layer에서 직접 사용하거나, Service 계층에 유저 객체를 내려줄 때 특정 값으로 연산 후 내려줄 수도 있다.

반면 내가 생각한 단점들을 이야기 해보자.

  1. @AuthenticationPrincipal은 내부적으로 UserDetailsService를 이용해서 DB로부터 유저 객체를 조회 후에 가져오기 때문에 조회 쿼리가 발생한다.

    @RequiredArgsConstructor
    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
        private final MemberRepository memberRepository;
    
        @Override
        public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    
            Member member = memberRepository.findByEmail(email).orElseThrow(
                    () -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")
            );
            return new CustomUserDetails(member);
        }
    
    }

  1. 단위 테스트, 통합 테스트시 유저 인증 객체를 넣어주는 방식이 불편하다.

    		@WithUserDetails(value = "yardyard@likelion.org", userDetailsServiceBeanName = "customUserDetailsService", setupBefore = TestExecutionEvent.TEST_EXECUTION
    	  @Test
         void get_payment_test() throws Exception {
             // given
             int matchingId = 1;
    
             // when
             ResultActions resultActions = mvc.perform(
                     get(String.format("/api/payment/%d", matchingId))
             );
    
             // console
             String responseBody = resultActions.andReturn().getResponse().getContentAsString();
             System.out.println("테스트 : " + responseBody);
    
             // verify
             resultActions.andExpect(jsonPath("$.success").value("true"));
         }

@WithUserDetails을 이용해서 테스트시 @AuthenticationPrinciple에 인증 객체를 넣어줄 수 있다. 허나 이 때 반드시 value에 해당하는 유저가 존재해야하기에, BeforeEach등으로 유저를 미리 저장해야하는 불편함이 존재한다.

  1. @AuthenticationPrincipal을 사용하면 해당 어노테이션에 의존성을 가지게 되어 특정 스프링 기능에 의존하게 된다.

해당 방식을 사용할 경우 추후 Spring에서 해당 어노테이션을 내부적으로 변경하거나, Deprecate할 경우에 높은 Spring 기능 의존으로 인해서 변경에 취약하게 된다.



2️⃣ 직접 SecurityContextHolder에서 가져오는 방법

이 방식의 경우 내가 생각한 장점을 뽑는다면 아래와 같다.

  1. 내가 User 객체가 필요한 로직에서 유연하게 가져올 수 있다.

    private String getEmail(){
            Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
            return loggedInUser.getName();
        }
    
    public List<ChatListResDTO> getChatList() {
    
            Member member = memberRepository.findByEmail(getEmail())
                    .orElseThrow(() -> new InvalidMemberException(ChatRoomMessageCode.INVALID_MEMBER));
    
    				// ...
    
       }

위 코드와 같이 내가 사용하고 싶은 코드에서 직접 SecurityContextHolder에서 인증 유저 객체를 가져오는 메서드를 호출함으로써 특정 메소드에서 필요한 시점에 유연하게 사용자 정보에 접근할 수 있으며, 이를 통해 로직에 따라 다르게 동작하거나 특별한 처리를 할 수 있다.

  1. 테스트시 목 객체를 넣기 용이하다.

    사전에 SecurityContextHolder에 목 객체를 저장 시켜놓으면 되기 때문에 실제 DB를 거치지 않고, 테스팅을 할 수 있다.

    @Test
    public void testSomeMethod() {
        SecurityTestUtils.setAuthentication("testUser", "ROLE_USER");
    
        // Test logic
    
        SecurityTestUtils.clearAuthentication();
    }

이 방식또한 내가 생각한 단점들을 이야기 해보겠다.

  1. 1번 방식에 비해서 상대적으로 코드가 지저분해진다.

    매번 인증 객체가 필요할 때마다 메서드를 호출해야하며, 인증이 필요한 Service마다 해당 메서드를 구현해야 하기에 코드의 중복이 일어난다.

    // ChatService
    private String getEmail(){
            Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
            return loggedInUser.getName();
        }
    
    public List<ChatListResDTO> getChatList() {
    
            Member member = memberRepository.findByEmail(getEmail())
                    .orElseThrow(() -> new InvalidMemberException(ChatRoomMessageCode.INVALID_MEMBER));
    
    				// ...
    
       }

그러면 static으로 선언하면 되는거 아니야??

Spring Security의 SecurityContextHolderThreadLocal을 사용하여 현재 스레드에 대한 정보를 유지하기 때문에, 정적 메서드를 사용하면 여러 스레드 간에 공유되는 메서드가 되므로 위험하다.

  1. Presentation layer와 Service Layer 간의 결합도가 증가한다.

    유저의 정보는 클라이언트와 맞닿아 있는 Presentation Layer에서 접근해 가져오는 것이 결합도를 줄이는 방법이다.

    Service Layer에서 유저의 정보를 가져오면, 3 layers의 각 역할을 넘어선 것이라고도 생각한다.



내 생각

이번 프로젝트에서 내가 구현하고자 한 목표는 성능 최적화적인 부분보다 코드의 간결함이었기 때문에 1번 방식을 택하는게 맞을 것 같다는 생각을 하게 되었다. 반면 만약 극도의 성능 최적화를 하고자하면 직접 Bean에서 유저 객체를 가져오는 2번 방식을 택하는게 맞을 것 같다는 생각을 했다.

profile
Hello, World! \n

0개의 댓글