현재 프로젝트에서 인증/인가 로그인 맡으신 팀원 분이 Spring Security + JWT 토큰 인증을 적용하고 있다.
API 요청 시, 토큰을 통해 유저 인증을 이미 Filter 단계에서 완료했는데,Service 레이어에서 다시 userRepository.findById()를 호출해 유저 유효성 검사를 하고 있었다.
프로젝트 마무리 단계에서 문득 이런 생각이 들었다.
"아니 토큰으로 이미 인증했는데? 왜 또 귀찮게 조회해?? 불필요한 DB 호출도 되는거 잖아 성능적으로 안좋을 거 같은데.." 라는 의문이 들었다.
🛑 발생한 고민
1. JWT 인증은 이미 했는데, 또 DB 조회까지 해야 되나?
2. userId를 토큰에 담아놨는데, 이것만 믿으면 안 되는 걸까?
3. 혹시 보안적으로 위험할 수도 있나?
userId를 사용해서 굳이 유저 정보를 다시 조회할 필요가 없어 보였다.Spring Security에서는 인증된 유저 정보를 UserDetails라는 인터페이스로 관리한다.
내가 직접 구현한 CustomUserDetails 클래스에 유저의 필요한 정보를 담아서, 이후 @AuthenticationPrincipal을 통해 손쉽게 유저 정보를 가져올 수 있다.
이를 통해 Service나 Controller에서 userRepository.findById() 없이 유저 정보를 바로 가져올수 있다.
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
public Long getUserId() {
return user.getId();
}
// UserDetails 필수 메서드들 override
}
Spring Security가 Authentication 객체에 CustomUserDetails를 저장한다.@AuthenticationPrincipal 어노테이션을 사용하면, 인증된 유저의 정보를 바로 꺼낼 수 있다.@GetMapping("/profile")
public ApiResponseDto<MyProfileResponseDto> getProfile(@AuthenticationPrincipal CustomUserDetails userDetails) {
Long userId = userDetails.getUserId();
// 여기서부터 DB 조회 없이 userId 사용 가능
}
/**
* 장바구니 생성 API
*
* @param storeId 가게 ID (장바구니를 생성할 가게)
* @param httpServletRequest HttpServletRequest (요청 URI 정보)
* @return 장바구니 생성 결과
* @throws CustomException USER_NOT_FOUND, STORE_NOT_FOUND, CART_ALREADY_EXISTS
*/
@PostMapping("/cart/{storeId}")
public ResponseEntity<ApiResponseDto<Void>> createCart(
@PathVariable Long storeId,
HttpServletRequest httpServletRequest) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Long userId = Long.parseLong(auth.getName());
cartService.createCart(userId, storeId);
return ResponseEntity.ok(ApiResponseDto.success(SuccessCode.CREATE_CART_SUCCESS, null, httpServletRequest.getRequestURI()));
}
// 장바구니 생성 Service
@Transactional
public void createCart(Long userId, Long storeId) {
// CustomUserDetails에서 가져온 userId로 유저 정보 확인
User user = userRepository.findById(userId)
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
// 삭제된 유저는 장바구니 생성 불가
if (user.isDeleted()) {
throw new CustomException(ErrorCode.USER_DELETED);
}
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new CustomException(ErrorCode.STORE_NOT_FOUND));
// 이미 장바구니가 존재하는 경우 예외 발생
if (cartRepository.findByUserAndStore(user, store).isPresent()) {
throw new CustomException(ErrorCode.CART_ALREADY_EXISTS);
}
Cart cart = new Cart(user, store);
cartRepository.save(cart);
}
@PostMapping("/{storeId}")
public ResponseEntity<ApiResponseDto<Void>> createCart(
@PathVariable Long storeId,
HttpServletRequest httpServletRequest) {
// 인증된 사용자 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();
User user = customUserDetails.getUser(); // CustomUserDetails에서 바로 User 객체를 가져옵니다.
// 서비스 호출하여 장바구니 생성
cartService.createCart(user.getId(), storeId, user);
return ResponseEntity.ok(ApiResponseDto.success(SuccessCode.CREATE_CART_SUCCESS, null, httpServletRequest.getRequestURI()));
}
@Transactional
public void createCart(Long userId, Long storeId, User user) {
// 인증된 User 객체를 직접 받으므로, DB 조회 없이 사용 가능
if (user.isDeleted()) {
throw new CustomException(ErrorCode.USER_DELETED);
}
Store store = storeRepository.findById(storeId)
.orElseThrow(() -> new CustomException(ErrorCode.STORE_NOT_FOUND));
// 이미 장바구니가 존재하는 경우 예외 발생
if (cartRepository.findByUserAndStore(user, store).isPresent()) {
throw new CustomException(ErrorCode.CART_ALREADY_EXISTS);
}
Cart cart = new Cart(user, store);
cartRepository.save(cart);
}
JWT 토큰 인증 이후, 인증 정보를 CustomUserDetails에 담아 두고
Controller나 Service 레이어에서는
@AuthenticationPrincipal을 사용해 DB 조회 없이 인증된 유저 정보를 사용하자.
"토큰 인증 끝났으면, CustomUserDetails로 인증 정보를 들고 다녀라.
매번 DB 조회하지 말고, 필요한 곳에서만 딱 조회해!"