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 적용 후에는 다음과 같이 쿼리가 호출됩니다.
궁금한신점이나 잘못된 부분이 있으면 자유롭게 댓글 달아주세요.
'spring > 게시판 api' 카테고리의 다른 글
Spring boot 게시판 API 서버 제작 (19) - 게시글 - 생성 (0) | 2023.03.03 |
---|---|
Spring boot 게시판 API 서버 제작 (18) - 게시글 - Entity 설계 (0) | 2023.02.28 |
Spring boot 게시판 API 서버 제작 (16) - 중간 정리 & 카테고리 API (0) | 2023.02.27 |
Spring boot 게시판 API 서버 제작 (15) - 게시판 - 카테고리 - 웹 계층 구현 (0) | 2023.02.26 |
Spring boot 게시판 API 서버 제작 (14) - 게시판 - 카테고리 - 2 (0) | 2023.02.25 |