본문 바로가기

Backend/Spring

[Spring] AOP란?

본 포스팅은 

https://chanho0912.tistory.com/14

 

[Java] 프록시 패턴이란?

원래 Spring 카테고리에 AOP에 대한 글을 포스팅하고 있었는데, Spring에서 제공하는 AOP를 이해하기 위해서는 기본적인 프록시 패턴에 대한 이해가 필요하기 때문에 프록시 패턴에 대한 포스팅을 먼

chanho0912.tistory.com

 

해당 포스팅의 내용을 이해하고 있다고 가정하고 작성하겠습니다.

 

AOP란?

Aspect Oriented Programming의 약어로 한국어로 직역하면 관점 지향적인 프로그래밍으로 해석된다. 

 

여기서 말하는 관점 지향이란 비즈니스 로직을 기준으로 핵심적인 로직과 부과적인 로직을 분리하여 각각 분리하여 모듈화 하겠다는 의미입니다.

 

번역이 조금 어렵기 때문에 예제로 한번 살펴보겠습니다.

 

@RequiredArgsConstructor
@RestController
public class GoodsController {
    private final GoodsService goodsService;


    @PostMapping("/goods/save")
    public Long saveGoods(@RequestBody GoodsSaveDto goodsSaveDto, @AuthenticationPrincipal User user){
        return goodsService.save(goodsSaveDto, user);
    }

    @PostMapping("/goods/buy/{id}")
    public Long saveOrder(@RequestBody GoodsOrderSaveDto goodsOrderSaveDto, @AuthenticationPrincipal User user){
        return goodsService.saveOrder(goodsOrderSaveDto, user);
    }

    @PutMapping("/goods/update/{id}")
    public Long updateOrder(@RequestBody GoodsOrderUpdateDto goodsOrderUpdateDto, @AuthenticationPrincipal User user){
        return goodsService.updateOrder(goodsOrderUpdateDto, user);
    }

    @DeleteMapping("/goods/delete/{id}")
    public Long deleteOrder(@PathVariable Long id, @AuthenticationPrincipal User user){
        return goodsService.cancelOrder(id, user);
    }

    @GetMapping("/goods/user_order/{page}/{size}")
    public List<GoodsOrderListDto> getOrderList(@PathVariable("page") Integer page, @PathVariable("size") Integer size, @AuthenticationPrincipal User user){
        return goodsService.getOrderList(user, page, size);
    }


    @PutMapping("/goods/clubs/order_manage/{id}")
    public Long updateOrderState(@PathVariable Long id, @RequestParam String state, @AuthenticationPrincipal User user) {
        return goodsService.changeOrderState(state, id, user);
    }
}

 

해당 코드는 제가 학부 때 팀 프로젝트로 진행한 Spring RestAPI 코드의 일부입니다.

 

만약 제가 각 API의 성능을 확인하고 싶다면 어떻게 하면 될까요?

 

Controller는 기본적인 서비스로직을 수행하지 않습니다. 따라서 Service에서 로직을 처리할 때에 성능을 측정하는 것이 합리적인 평가 지표일 것입니다.

 

해당 프로젝트의 GoodsService는 다음과 같이 이루어져 있습니다.

 

@RequiredArgsConstructor
@Service
public class GoodsService {
    private final GoodsRepository goodsRepository;
    private final GoodsOrderRepository goodsOrderRepository;
    private final OptionRepository optionRepository;

    public static final String initialState = "상품 준비중";


    @Transactional
    public Long save(GoodsSaveDto goodsSaveDto, User user){
        if(!(user instanceof Club))
            return -3L;
        return goodsRepository.save(goodsSaveDto.toEntity((Club)user)).getId();
    }


    @Transactional
    public Long saveOrder(GoodsOrderSaveDto goodsOrderSaveDto, User user) {
        Goods goods = goodsRepository.findById(goodsOrderSaveDto.getGoods_id()).orElse(null);
        Option option = optionRepository.findById(goodsOrderSaveDto.getOption_id()).orElse(null);

        if(goods == null || option == null)
            return ErrorCodes.NOT_EXIST;

        return goodsOrderRepository.save(
                GoodsOrder.builder()
                .user(user)
                .goods(goods)
                .state(initialState)
                .address(goodsOrderSaveDto.getAddress())
                .shipped_date(null)
                .option(option)
                .build()).getId();
    }


    @Transactional
    public Long updateOrder(GoodsOrderUpdateDto goodsOrderUpdateDto, User user){

        GoodsOrder order = goodsOrderRepository.findById(goodsOrderUpdateDto.getOrderId()).orElse(null);
        Option option = optionRepository.findById(goodsOrderUpdateDto.getOptionId()).orElse(null);
        if(option == null || order == null)
            return ErrorCodes.NOT_EXIST;
        else if(!user.getId().equals(order.getUser().getId()))
            return ErrorCodes.NOT_SAME_USER;
        else if(!option.getGoods().getId().equals(order.getGoods().getId()))
            return ErrorCodes.INVALID_VAR;
        order.update(option, goodsOrderUpdateDto.getAddress());
        return goodsOrderUpdateDto.getOrderId();
    }

    @Transactional
    public Long cancelOrder(Long orderId, User user){
        GoodsOrder goodsOrder = goodsOrderRepository.findById(orderId).orElse(null);
        if(goodsOrder == null)
            return ErrorCodes.NOT_EXIST;
        else if(!goodsOrder.getUser().getId().equals(user.getId()))
            return ErrorCodes.NOT_SAME_USER;

        goodsOrderRepository.delete(goodsOrder);
        return orderId;
    }

    @Transactional
    public List<GoodsOrderListDto> getOrderList(User user, Integer page, Integer size){
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by("createdDate").descending());
        return goodsOrderRepository.findByUser(user, pageRequest).get()
                .map(o->GoodsOrderListDto.builder()
                        .id(o.getId())
                        .image(o.getGoods().getPictures().size() == 0? null : o.getGoods().getPictures().get(0))
                        .state(o.getState())
                        .address(o.getAddress())
                        .option_description(o.getOption().getName())
                        .cost(o.getGoods().getPrice() + o.getOption().getCosts())
                        .shipped_date(o.getShipped_date())
                        .created_date(o.getCreatedDate())
                        .build()
                ).collect(Collectors.toList());
    }

	...

}

 

이 중에서 save라는 메소드를 잠시 가져와 보겠습니다.

 

@Transactional
public Long save(GoodsSaveDto goodsSaveDto, User user){
	if(!(user instanceof Club))
		return -3L;
	Long ret = goodsRepository.save(goodsSaveDto.toEntity((Club)user)).getId();
	return ret;
}

 

그러면 저희는 stopWatch라는 utils를 활용하여 다음과 같이 성능을 평가해볼 수 있습니다.

 

@Transactional
public Long save(GoodsSaveDto goodsSaveDto, User user){
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
		
	if(!(user instanceof Club))
		return -3L;
	Long ret = goodsRepository.save(goodsSaveDto.toEntity((Club)user)).getId();
		
	stopWatch.stop();
	stopWatch.prettyPrint();
	return ret;
}

 

여기까지는 별로 문제가 없습니다. 하지만 만약에 모든 메서드에 대한 성능을 평가하려면 어떻게 해야 할까요?

 

public void methodA(){
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();

	DoSomething();

	stopWatch.stop();
	stopWatch.prettyPrint();

	return;
}

public void methodB(){
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();

	DoSomething();

	stopWatch.stop();
	stopWatch.prettyPrint();

	return;
}

public void methodC(){
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();

	DoSomething();

	stopWatch.stop();
	stopWatch.prettyPrint();

	return;
}

 

보기와 같이 모든 method에 StopWatch 성능평가를 앞 뒤로 삽입하여 구현할 수 있습니다.

 

여기서 문제가 발생합니다. 구현하는 것은 그렇다 하더라도 만약에 StopWatch라는 기능을 더 이상 제공하지 않거나, 혹은 메서드 명이 바뀌거나, 혹은 메서드 로직이 수정되는 등의 이벤트가 발생한다면 위의 코드는 StopWatch가 들어있는 모든 method에 대하여 수정을 해야 할 것입니다.

 

여기서 Spring framework는 Proxy패턴을 적용하여 AOP를 제공해 줍니다.

 

즉 앞서 보았던 핵심적인 로직 -> Service 로직과

부과적인 로직 -> 성능평가 로직

을 각각 모듈화하여 부과적인 로직은 Aspect로 분리하여 재사용이 가능하게 만들어 줍니다. 

 

다음과 같이 해당 코드를 Annotation을 활용하여 전 후로 필요한 코드들을 자동적으로 실행해줍니다.

 

@PerformanceCheck
public void methodA(){	
    DoSomething();
    ...
    return;
}

@PerformanceCheck
public void methodB(){	
    DoSomething();
    ...
    return;
}

@PerformanceCheck
public void methodC(){	
    DoSomething();
    ...
    return;
}

 

이렇게 되면 개발자는 해당 서비스의 로직에 집중하여 개발을 할 수 있고, 필요하지만 반복적인 업무 등은 Aspect 모듈 활용하여 Annotation 기법으로 처리할 수 있게 됩니다.

 

그러면 한번 실제로 @PerformanceCheck를 구현해보도록 하겠습니다.

 

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceCheck {

}

 

우선 Annotation 구현체를 하나 정의해 주겠습니다. 

 

Target의 인자로 Method에 적용하기 때문에 METHOD를 전달하여 주고, RetentionPolicy를 Runtime으로 가져감으로써 런타임 시까지 annotation 메모리를 유지하겠습니다. (실제 실행 중 성능을 측정하기 때문에 Runtime까지 메모리에 유지되어야 합니다)

 

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;

@Component
@Aspect
public class LogAspect {

	Logger logger = LoggerFactory.getLogger(LogAspect.class);
	@Around("@annotation(PerformanceCheck)")
	public Object PerformanceCheck(ProceedingJoinPoint joinPoint) throws Throwable {
		System.out.println("Aspect Logger Start");
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();

		System.out.println("Method running");
		Object proceed = joinPoint.proceed();

		stopWatch.stop();
		logger.info(stopWatch.prettyPrint());

		System.out.println("Aspect Logger Finish");
		return proceed;
	}
}

 

그다음 위의 내용처럼 Stopwatch를 활용할 Aspect를 하나 정의해 주겠습니다.

 

Bean으로 등록되어야 하기 때문에 @Component 어노테이션을 활용하고, Aspect로 사용되기 때문에 @Aspect 어노테이션을 붙여주었습니다.

 

그다음 실질적으로 PerformanceCheck가 수행할 로직을 구현합니다.

 

  • @Around : 메소드 실행 전, 후로 타깃 메서드를 감싸서 수행
  • @Before : 메소드 실행 전
  • @After : 예외 발생 유무에 관계없이 메서드 실행 후
  • @AfterReturning : 예외 없이 정상 완료된 후
  • @AfterThrowing : 예외가 발생했을 때만

사용할 수 있는 어노테이션은 다음과 같고 저는 메소드 전 후로 로직을 수행할 것이기 때문에 @Around 어노테이션을 활용하겠습니다.

 

JoinPoint라는 것은 저희가 어노테이션을 붙인 메서드를 인자로 받는 것을 의미합니다.

 

이를 순서대로 보면 

 

* Stopwatch 시작

* 메소드 수행 (joinPoint.proceed();)

* Stopwatch 종료

 

이렇게 수행됩니다.

 

 

PerformanceCheck라는 어노테이션이 정상적인 기능을 한 것을 볼 수 있습니다.

 

이처럼 Spring에서는 프록시 패턴 기반 AOP를 제공하고 있고, 저희가 흔히 쓰는 @Transactional, @Filter와 같은 어노테이션들도 AOP기반으로 구현된 어노테이션입니다.

 

*저의 글에 대한 피드백이나 지적은 언제나 환영합니다.