[Spring] 반복되는 회원 확인을 AOP로 리팩토링

오형상·2023년 7월 4일
0

Spring

목록 보기
3/9
post-thumbnail

현재 상황

토이 프로젝트를 진행하다 보니 품목 등록, 수정, 삭제 등 로그인된 상태에서 요청해야는 API가 다수 있었다.
매번 Authentication에서 email을 꺼내와 이메일을 통해 회원을 찾고 없으면 에러를 반환하는 부분이 계속해서 반복되고 있음을 느껴 반복을 줄이고자 AOP를 적용하고자 한다.

ItemController

인수로 매번 Authentication 받아 email를 꺼내고 있다.

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/items")
public class ItemController {

    private final ItemService itemService;

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 단건 조회")
    @GetMapping("/{itemId}")
    public Response<ItemDto> findItem(@PathVariable Long itemId) {

        ItemDto response = itemService.selectOne(itemId);

        return Response.success(response);
    }

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 검색")
    @GetMapping
    public Response<Page<ItemDto>> createItem(ItemSearchCond itemSearchCond, Pageable pageable) {

        Page<ItemDto> response = itemService.searchItem(itemSearchCond, pageable);

        return Response.success(response);
    }

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 추가")
    @PostMapping
    public Response<ItemDto> createItem(@RequestPart ItemCreateRequest request, @RequestPart MultipartFile multipartFile, Authentication authentication) {
    
        String email = authentication.getName();
        
        log.info("현재 로그인된 유저의 이메일:{}", email);
        
        ItemDto response = itemService.saveItem(request, multipartFile, email);

        return Response.success(response);
    }

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 수정")
    @PatchMapping("/{itemId}")
    public Response<ItemDto> changeItem(@PathVariable Long itemId, @RequestPart ItemUpdateRequest request, @RequestPart MultipartFile multipartFile, Authentication authentication) {
    
        String email = authentication.getName();
        
        log.info("현재 로그인된 유저의 이메일:{}", email);
        
        ItemDto response = itemService.updateItem(itemId, request, multipartFile, email);

        return Response.success(response);
    }

    @Tag(name = "Item", description = "품목 API")
    @Operation(summary = "품목 삭제")
    @DeleteMapping("/{itemId}")
    public Response<MessageResponse> removeItem(@PathVariable Long itemId, Authentication authentication) {
    
        String email = authentication.getName();
        
        log.info("현재 로그인된 유저의 이메일:{}", email);
        
        MessageResponse response = itemService.deleteItem(itemId, email);

        return Response.success(response);
    }
}

ItemService

이메일을 통해 해당 유저가 존재하는 지 체크하는 코드가 반복 되고 있다.

customerRepository.findByEmail(email)
                .orElseThrow(() -> new AppException(CUSTOMER_NOT_FOUND, CUSTOMER_NOT_FOUND.getMessage()));
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class ItemService {
    private final CustomerRepository customerRepository;

    private final ItemRepository itemRepository;

    private final BrandRepository brandRepository;

    private final AwsS3Service awsS3Service;

    @Transactional(readOnly = true)
    @Cacheable(value = "items", key = "#id")
    public ItemDto selectOne(Long id) {
        return getItem(id).toItemDto();
    }

    @Transactional(readOnly = true)
    public Page<ItemDto> searchItem(ItemSearchCond itemSearchCond, Pageable pageable) {
        return itemRepository.search(itemSearchCond, pageable);
    }

    public ItemDto saveItem(ItemCreateRequest request, MultipartFile multipartFile, String email) {

        customerRepository.findByEmail(email)
                .orElseThrow(() -> new AppException(CUSTOMER_NOT_FOUND, CUSTOMER_NOT_FOUND.getMessage()));

        itemRepository.findItemByItemName(request.getItemName())
                .ifPresent((item -> {
                    throw new AppException(DUPLICATE_ITEM, DUPLICATE_ITEM.getMessage());
                }));

        Brand findBrand = getBrand(request.getBrandName());

        String originImageUrl = awsS3Service.uploadBrandOriginImage(multipartFile);

        request.setItemPhotoUrl(originImageUrl);

        Item savedItem = itemRepository.save(request.toEntity(findBrand));

        return savedItem.toItemDto();

    }

    @CacheEvict(value = "items", allEntries = true)
    public ItemDto updateItem(Long id, ItemUpdateRequest request, MultipartFile multipartFile, String email) {

        customerRepository.findByEmail(email)
                .orElseThrow(() -> new AppException(CUSTOMER_NOT_FOUND, CUSTOMER_NOT_FOUND.getMessage()));

        Item findItem = getItem(id);

        Brand findBrand = getBrand(request.getBrandName());

        if (!multipartFile.isEmpty()) {
            String extractFileName = FileUtils.extractFileName(findItem.getItemPhotoUrl());

            awsS3Service.deleteBrandImage(extractFileName);

            String newUrl = awsS3Service.uploadBrandOriginImage(multipartFile);

            request.setItemPhotoUrl(newUrl);
        } else {
            request.setItemPhotoUrl(findItem.getItemPhotoUrl());
        }

        findItem.updateItem(request, findBrand);

        return findItem.toItemDto();

    }

    @CacheEvict(value = "items", allEntries = true)
    public MessageResponse deleteItem(Long id, String email) {

        customerRepository.findByEmail(email)
                .orElseThrow(() -> new AppException(CUSTOMER_NOT_FOUND, CUSTOMER_NOT_FOUND.getMessage()));

        Item findItem = getItem(id);

        String extractFileName = FileUtils.extractFileName(findItem.getItemPhotoUrl());

        awsS3Service.deleteBrandImage(extractFileName);

        itemRepository.deleteById(findItem.getId());

        return new MessageResponse(findItem.getId(), "해당 품목이 삭제되었습니다.");
    }

    private Item getItem(Long id) {
        Item findItem = itemRepository.findById(id)
                .orElseThrow(() -> new AppException(ITEM_NOT_FOUND, ITEM_NOT_FOUND.getMessage()));
        return findItem;
    }

    private Brand getBrand(String brandName) {
        Brand findBrand = brandRepository.findBrandByName(brandName)
                .orElseThrow(() -> new AppException(BRAND_NOT_FOUND, BRAND_NOT_FOUND.getMessage()));
        return findBrand;
    }
}

AOP 적용

간단한 용어 설명

  • 조인포인트(Joinpoint) ➡ 클라이언트가 호출하는 모든 비즈니스 메소드, 조인포인트 중에서 포인트컷되기 때문에 포인트컷의 후보로 생각할 수 있다.
  • 포인트컷(Pointcut) ➡ 특정 조건에 의해 필터링된 조인포인트, 수많은 조인포인트 중에 특정 메소드에서만 횡단 공통기능을 수행시키기 위해서 사용한다.
    표현식 : 리턴타입 패키지경로 클래스명 메소드명(매개변수)
  • 어드바이스(Advice) ➡ 횡단 관심에 해당하는 공통 기능의 코드, 독립된 클래스의 메소드로 작성한다
  • 애스팩트(Aspect) ➡ 포인트컷과 어드바이스의 결합이다. 어떤 포인트컷 메소드에 대해 어떤 어드바이스 메소드를 실행할지 결정한다.
@Aspect
@Component
@RequiredArgsConstructor
public class CustomerCheck {

    private final CustomerRepository customerRepository;
	

	/**
     *
     * @Around ➡ 스프링에서 구현 가능한 Advice의 동작 시점 중 하나로 메소드 실행 전, 후 또는 익셉션 발생 시점을 뜻한다.
     * value = "execution(* store.myproject.onlineshop.controller..*.*(..))") ➡ 포인트컷 정의 (패키지와 하위 패키지의 모든 메소드 실행)
     * joinPoint.getArgs() ➡ 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object 배열 로 리턴
     *
     */
    @Around(value = "execution(* store.myproject.onlineshop.controller..*.*(..))") 
    public Object customerCheckAdviceHandler(ProceedingJoinPoint joinPoint) throws Throwable {  
    
    	// 인수가 Authentication인 필터 후 존재하면 Authentication에서 email를 뽑아 회원을 찾고 없으면 에러를 반환
        Stream.of(joinPoint.getArgs())
                .filter(arg -> arg instanceof Authentication)
                .map(arg -> (Authentication) arg)
                .findAny()
                .ifPresent((authentication) ->
                        customerRepository.findByEmail(authentication.getName())
                                .orElseThrow(() -> new AppException(CUSTOMER_NOT_FOUND))
                );

        return joinPoint.proceed();
    }
}

최종적으로 Controller에서 Authentication에서 email를 가져오는 부분과
Service에서 email로 유저를 찾아 없으면 오류를 반환하는 부분을 제거하여 중복을 제거하였다.


전체 코드 보기

0개의 댓글