[성능개선/Spring Security]

아빠는 외계연·2022년 11월 2일
2

Spring Boot

목록 보기
4/4
post-thumbnail

SecurityContext 저장 로직

정상적으로 jwt토큰이 인증되고 난 뒤 securitycontext에 유저 정보를 저장한다.

private void setAuthToSecurityContextHolder(String token) {
        Authentication auth = tokenProvider.getAuthentication(token);
        SecurityContextHolder.getContext().setAuthentication(auth);
}
public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserIdentifier(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

UsernamePasswordAuthenticationToken이란?

UsernamePasswordAuthenticationToken.java

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
              Collection<? extends GrantedAuthority> authorities) {
          super(authorities);
          this.principal = principal;
          this.credentials = credentials;
          super.setAuthenticated(true); // must use super, as we override
      }
    ...
}

AbstractAuthenticationToken을 상속받는다.

AbstractAuthenticationToken.java

public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {

	private final Collection<GrantedAuthority> authorities;
    ...
}

AbstractAuthenticationToken은 Authentication을 상속받는다.
따라서 UsernamePasswordAuthenticationToken은 SecurityContextHolder에 저장될 Authentication 객체이다.


SecurityContext에 저장할 유저 정보를 가져오기 위해 loadUserByUsername함수를 이용한다. 필자는 User 엔티티와 필수적인 컬럼들값만 저장하는 엔티티를 분리하였는데, 해당 엔티티의 이름은 CustomUserDetails이다.

public UserDetails loadUserByUsername(String usernameOrEmail) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(usernameOrEmail)
                .orElseThrow(()-> new UsernameNotFoundException("User not found with email : " + usernameOrEmail));
        return CustomUserDetails.create(user);
}

CustomUserDetails를 반환해주기 위해 User 안의 데이터를 불러와야 하는데, 이때 findUserById SQL문이 날라간다.

이후 SecurityContext내부안에 저장된 CustomUserDetails정보를 사용하기 위해서는 @AuthenticationPrincipal 어노테이션을 이용하는데, 필자는 그동안 유저 정보를 받아오기 위해 해당 어노테이션을 통해 받은 CustomUserDetails의 id값을 통해 findUserById 함수를 다시 사용하고 있었다.

[해결방법]

public class CustomUserDetails implements UserDetails, OAuth2User {
    private User user;
    private Long id;
    private String email;
    private String name;
    private Collection<? extends GrantedAuthority> authorities;
    private Map<String, Object> attributes;
    ...
}

이와 같이 CustomUserDetails 엔티티 내부에 User 컬럼을 추가하여 CustomUserDetails부터 User 엔티티의 정보를 쉽게 꺼내올 수 있도록 변경하였다.

[문제점]

@Override
@Transactional
public User signUp(User user, UserSignUpDto dto) {
        return user.signup(dto);
}
public User signup(UserSignUpDto dto) {
        this.birthday = dto.getBirthday();
        this.phone = dto.getPhone();
        this.transactionType = TransactionType.of(dto.getTransactionType());
        this.estateType = EstateType.of(dto.getEstateType());
        this.borough = dto.getBorough();
        return this;
}

필자는 이와같이 영속성 컨텍스트의 변경감지 기능을 이용하여 User 엔티티의 컬럼값을 변경해왔는데,

@PostMapping("/signup")
public ResponseEntity<UserInfoDto> signup(@Parameter(hidden = true) @AuthenticationPrincipal CustomUserDetails customUserDetails, @RequestBody UserSignUpDto userSignUpDto) {
    UserInfoDto userInfoDto = userService.signUp(customUserDetails.getUser(), userSignUpDto);
    return ResponseEntity.status(HttpStatus.OK).body(userInfoDto);
}

변경감지 기능은 프리젠테이션 계층에서는 동작하지 않기 때문에 자동으로 flush가 날라가지 않는 문제가 발생하였다. -> User entity가 영속성 컨텍스트를 벗어난다.

이에 따라

@Transactional
public User signUp(User user, UserSignUpDto dto) {
        return userRepository.save(user.signup(dto));
}

Spring Data JPA의 save함수를 통해 값을 직접 저장하였다.

정리

해당 방식을 통해 불필요하게 날아가던 User 데이터를 가져오는 SQL문을 날리지 않을 수 있게 되었다.
다시한번 Spring Security 구조를 되짚어 보는 시간이 되었다.

profile
Backend Developer

2개의 댓글

comment-user-thumbnail
2022년 11월 2일

깔끔한 정리네요~!

답글 달기
comment-user-thumbnail
약 11시간 전

DB 접근이 아닌 영속화가 끊겨서 발생하는 문제가 핵심이라고 생각합니다.

OSIV=true 를 사용하신다면 아래와 같이 Filter Chain 의 순서를 변경하여 해결 가능하지 않을까 싶습니다!

@Bean
public FilterRegistrationBean<OpenEntityManagerInViewFilter> openEntityManagerInViewFilter() {
	FilterRegistrationBean<OpenEntityManagerInViewFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>();
	filterFilterRegistrationBean.setFilter(new OpenEntityManagerInViewFilter());
	filterFilterRegistrationBean.setOrder(Integer.MIN_VALUE); // 예시를 위해 최우선 순위로 Filter 등록
	return filterFilterRegistrationBean;
}

혹시 다르게 적용가능한 부분이 있다면 대댓글 부탁드립니다.

https://tecoble.techcourse.co.kr/post/2020-11-03-osiv_with_interceptor/

답글 달기