Spring/about spring

[스프링 삼각형] AOP 관점 지향 프로그래밍

dev_rosieposie 2023. 7. 11. 21:43

Goal 

  1. 스프링 삼각형에 대해 알아본다
  2. 스프링이 구현한 AOP에 대해 알아보자
  3. 샘플코드를 통해 AOP를 이해해본다

스프링 삼각형 

스프링을 이해하는 데는 POJO(Plain Old Java Obejct)를 기반으로 스프링 삼각형이라는 애칭을 가진 Ioc/DI, AOP, PSA라고 하는 스프링의 3대 프로그래밍 모델에 대한 이해가 필수다. 

 

이번 포스팅에서는 3대 프로그래밍 중 하나인 AOP에 관해 살펴보자

 

AOP (Aspect - Oriented Programming) 관점 지향 프로그래밍

흩어진 코드를 한 곳으로 모으고, 다른 기타 클래스들은 자신이 해야할 일만 하도록 돕는 코딩 기볍 = > SRP 단일 책임의 원칙

흩어진 AAAA 와 BBBB

class A {
    method a () {
        AAAA
        오늘은 7월 4일 미국 독립 기념일이래요.
        BBBB
        }
    
    method b () {
        AAAA
        저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
        BBBB
        }
    }
class B {
    method c() {
        AAAA
        점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
        BBBB
    }
}

문제 : 변경이 될 경우 일일이 수정해야하는 번거로움

모아 놓은 AAAA 와 BBBB

class A {
    method a () {
    	오늘은 7월 4일 미국 독립 기념일이래요.
    }
    method b () {
    	저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
    }
}
class B {
    method c() {
    점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
    }
}
class AAAABBBB {
    method aaaabbb(JoinPoint point) {
        AAAA
        point.execute()
        BBBB
    }
}
  • 여러가지 다른 방법으로 구현할 수 있다
  • 대표적인 AOP
    • 컴파일 (AspectJ)
      • A.java ---(AOP)---> a.class
    • byte code 조작하는 방법 (AspectJ)
      • .class 파일을 조작
      •  A.java -> a.class, 런타임시 클래스로더가 a.class를 읽어와서 메모리에 올릴 때 조작
      • 메모리에 올라온 클래스가 실제 클래스와 다름
    • 프록시 패턴을 사용하는 방법 
      • 스프링 AOP
      • class AProxy extends A{ //~ }
  • 스프링 AOP는 프록시를 사용한다. 하지만 호출하는 쪽에서나 호출 당하는 쪽에서나 그 누구도 프록시가 존재하는지 모르고 오직 프레임워크만 프록시의 존재를 안다  = 중간에서 가로챔

AOP를 사용하는 코드  - Transactional 

class A {
	method a () {
        AAAA
        오늘은 7월 4일 미국 독립 기념일이래요.
        BBBB
    }
}

Transaction 처리 과정

  1. AAAA
    1. Transaction manager를 가지고 auto commit false로 설정
    2. 어떤 작업  = ( ex. 오늘은 7월 4일 미국 독립 기념일이래요.) 수행 = 핵심 관심사
    3. transaction commit 
    4. 어떤 작업을 try-catch-finally로 묶어 실행하기 때문에, catch구간에서 문제 발생 시 transaction을 rollback 시킴
  2. BBBB

핵심 관심사 = 어떤 작업

 

횡단 관심사 

  • 다수의 모듈에 공통적으로 or 반복적으로 나타나는 부분
  • 위 코드에서는 AAAA, BBBB가 그에 해당 

참고

Transaction은 사방에서 일어남 ex) Repository - 스프링 JPA가 제공하는 모든 메서드에 적용되어 있음

AOP 적용 예제 

프록시 패턴 - 기존 코드 건드리지 않고 새 기능 추가하기

Payment 인터페이스

public interface Payment {
	void pay(int amount);
}

Cash 클래스

public class Cash implements Payment{
	@Override
	public void pay(int amount) {
		System.out.println(amount + " 현금 결제");
	}
}

CreditCard 클래스

public class CreditCard implements Payment{
	Payment cash = new Cash();
	@Override
	public void pay(int amount) {
		if(amount > 100){
			System.out.println(amount +" 신용 카드");
		}else{
			cash.pay(amount);
		}
	}
}

Store 클래스 - 클라이언트 코드

public class Store {
	Payment payment;
	public Store(Payment payment) {
		this.payment = payment;
	}
	public void buySomthing(int amount){
		payment.pay(amount);
	}
}
  • 클라이언트 코드는 변경되지 않음

CashPerf 클래스 - 프록시 코드

public class CashPerf implements Payment{

	Payment cash = new Cash();
	@Override
	public void pay(int amount) {
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();

		cash.pay(amount);

		stopWatch.stop();
		System.out.println(stopWatch.prettyPrint());
	}
}

 

StoreTest 클래스

class StoreTest {

	@Test
	public void testPay(){
		Payment cash = new CashPerf();
		Store store = new Store(cash);
		store.buySomthing(200);
	}
}

결과

200 현금 결제
StopWatch '': running time = 4008284 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
004008284  100%  

  • 원래는 cash가 bean으로 등록되어야 하지만, 내가 만든 프록시 - CashPerf가 자동으로 bean으로 등록됨
  • 그래서 클라이언트가 원래 bean으로 등록해야하는 cash가 아니라 cashperf를 대신 쓰게 되는 일이 스프링 내부에서 발생하게 됨

정리

aop를 proxy구현하는 방법 - 새로운 코드를 추가했지만 기존의 코드를 건드리지 않는다 

 

특정 메소드(어노테이션이 있는)가 호출되었을 때 그 메소드 처리 시간 로깅하기

LogExcutionTime - 어디에 적용할지 표시해두는 용도

@Target(ElementType.METHOD)				// 메서드에 붙힐 것이므로
@Retention(RetentionPolicy.RUNTIME)		
public @interface LogExcutionTime {

}
  • 어노테이션을 사용한 코드를 언제까지 유지할 것인가 - runtime까지 유지해야만 스프링이 찾아서 bean 등록

LogAspect - 실제 Aspect로, @LogExcutionTime 어노테이션 달린곳에 적용

@Component
@Aspect
public class LogAspect {
   Logger logger = LoggerFactory.getLogger(LogAspect.class);

   @Around("@annotation(LogExcutionTime)")
   public Object logExcutionTime(ProceedingJoinPoint joinPoint) throws Throwable{

      StopWatch stopWatch = new StopWatch();
      stopWatch.start();

      Object ret = joinPoint.proceed();

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

      return ret;
   }
}
  • spring bean만 aspect가 될 수 있음

SampleController에 @LogExcutionTime 추가

@LogExcutionTime
@GetMapping("/context")
public String context(){
    return "hello "+rosie;
}

결과

2023-07-11T21:22:12.916+09:00  WARN 12641 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=3m40s628ms).
2023-07-11T21:26:11.432+09:00  INFO 12641 --- [nio-8080-exec-3] o.s.samples.petclinic.aspect.LogAspect   : StopWatch '': running time = 661282 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
000661282  100%  

 

OwnerController에 @LogExcutionTime 추가

@LogExcutionTime
@GetMapping("/owners/new")
public String initCreationForm(Map<String, Object> model) {
    Owner owner = new Owner();
    model.put("owner", owner);
    return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

@LogExcutionTime
@PostMapping("/owners/new")
public String processCreationForm(@Valid Owner owner, BindingResult result) {
    if (result.hasErrors()) {
        return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
    }

    this.owners.save(owner);
    return "redirect:/owners/" + owner.getId();
}

@LogExcutionTime
@GetMapping("/owners/find")
public String initFindForm() {
    return "owners/findOwners";
}

2023-07-11T21:30:36.286+09:00  INFO 12782 --- [nio-8080-exec-3] o.s.samples.petclinic.aspect.LogAspect   : StopWatch '': running time = 98784045 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
098784045  100%  

 

 

정리

  • 스프링 AOP는 인터페이스 기반이다
  • 스프링 AOP는 프록시 기반이다 = 위임
  • 스프링 AOP는 런타임 기반이다

 

 

참고

스프링 입문을 위한 자바 객체지향의 원리와 이해

스프링입문 강의 by 백기선

https://www.inflearn.com/course/lecture?courseSlug=spring&unitId=15538

코드샘플

https://github.com/spring-projects/spring-petclinic