spring/게시판 api

Spring boot 게시판 API 서버 제작 (17) - Member <- Entity Graph 적용

얼킴 2023. 2. 27. 17:51

Authorization 헤더에 access token 정보를 주고 API 요청을 보낼 때, Member를 조회하는 과정에서 불필요한 쿼리 호출이 이루어지는 부분을 살펴보고 Entity Graph를 적용해서 해결해 보도록 하겠습니다.

 

JwtAuthenticationFilter

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

    private final TokenHelper accessTokenHelper;
    private final CustomUserDetailsService userDetailsService;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = extractToken(request);
        if (validateToken(token)) {
            setAuthentication(token);
        }
        chain.doFilter(request, response);
    }

    private String extractToken(ServletRequest request) {
        return ((HttpServletRequest) request).getHeader("Authorization");
    }

    private boolean validateToken(String token) {
        return token != null && accessTokenHelper.validate(token);
    }

    private void setAuthentication(String token) {
        String userId = accessTokenHelper.extractSubject(token);
        CustomUserDetails userDetails = userDetailsService.loadUserByUsername(userId); //<--
        SecurityContextHolder.getContext().setAuthentication(new CustomAuthenticationToken(userDetails, userDetails.getAuthorities()));
    }
}

Authorization 정보 검증을 위한 JwtAuthenticationFilter 클래스의 setAuthentication 메서드안에서 사용자 정보를 가져오기 위해  loadUserByUsernae()을 호출하고 있습니다.

 

CustomUseretailsService

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {
    private final MemberRepository memberRepository;
    @Override
    public CustomUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        Member member = memberRepository.findById(Long.valueOf(userId))
                .orElseGet(() -> new Member(null, null, null, null, List.of()));
        return new CustomUserDetails(
                String.valueOf(member.getId()),
                getAuthorities(member)
        );
    }

    private Set<GrantedAuthority> getAuthorities(Member member) {
        return member.getRoles().stream().map(memberRole -> memberRole.getRole())
                .map(role -> role.getRoleType())
                .map(roleType -> roleType.toString())
                .map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
    }
}

loadUserByUsername 메서드에서 문제가 되는 부분은 MemberRepositoy의 findById입니다.

 

먼저 loadUserByUsername()메서드를 실행했을 때의 결과를 먼저 보겠습니다.

위의 로그를 살펴보자면

1. Member 조회

2. MemberRole 조회

3. Role조회

 

불필요하게 쿼리를 많이 요청하는 케이스라고 볼 수 있습니다. 이는 Entity Graph를 적용해서 해결할 수 있습니다.

(Entity Graph에 대한 자세한 내용은 다음 글을 참고해주세요.)

https://coding-kim.tistory.com/51

 

[spring] Entity Graph에 대해 알아보자

Entity Graph란? Entity Graph는 JPA(Java Persistence API)에서 제공하는 기능으로, 엔티티 객체를 가져올 때 연관된 엔티티 객체들을 함께 가져오는 방법을 지정하는 기능입니다. 일반적으로 JPA에서는 지연

coding-kim.tistory.com

 

Entity Graph 적용

Member

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedEntityGraph(
        name = "Member.roles",
        attributeNodes = @NamedAttributeNode(value = "roles", subgraph = "Member.roles.role"),
        subgraphs = @NamedSubgraph(name = "Member.roles.role", attributeNodes = @NamedAttributeNode("role"))
)
public class Member extends EntityDate {}
  • 먼저 @NamedEntityGraph 어노테이션을 작성하여 Entity Graph를 정의 합니다.
  • attributeNodes는 Entity Graph에 포함될 Entity와 연관된 Entity를 지정합니다. 이 코드에는 roles라는 필드를 지정하고  subgraph roles.role을 지정하여 Role Entity를 가져옵니다.

 

MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email); //Optional은 조회 결과가 null인 경우를 대비해줌
    Optional<Member> findByNickname(String nickname);

    @EntityGraph("Member.roles")
    Optional<Member> findWithRolesById(Long id);

    boolean existsByEmail(String email);
    boolean existsByNickname(String nickname);
}
  • @EntityGraph를 사용하여 Entity Graph를 지정합니다.
  • Member Entity에 대한 Entity Graph를 Member.roles로 지정하였습니다. 이는 Member의 roles필드와 연관된 Role도 함께 조회하도록 지정한 것입니다.

CustomUserDetailsService

public CustomUserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
        //Member member = memberRepository.findById(Long.valueOf(userId))
        Member member = memberRepository.findWithRolesById(Long.valueOf(userId))
                .orElseGet(() -> new Member(null, null, null, null, List.of()));
        return new CustomUserDetails(
                String.valueOf(member.getId()),
                getAuthorities(member)
        );
    }

Entity Graph 를 적용한 finWithRolesById를 적용합니다.

결과

Entity Graph 적용 후에는 다음과 같이 쿼리가 호출됩니다.

 


궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.

github : https://github.com/jaeyeon423/spring_board_api