Goal
- 스프링 삼각형에 대해 알아본다
- 스프링이 구현한 AOP에 대해 알아보자
- 샘플코드를 통해 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{ //~ }
- 컴파일 (AspectJ)
- 스프링 AOP는 프록시를 사용한다. 하지만 호출하는 쪽에서나 호출 당하는 쪽에서나 그 누구도 프록시가 존재하는지 모르고 오직 프레임워크만 프록시의 존재를 안다 = 중간에서 가로챔
AOP를 사용하는 코드 - Transactional
class A {
method a () {
AAAA
오늘은 7월 4일 미국 독립 기념일이래요.
BBBB
}
}
Transaction 처리 과정
- AAAA
- Transaction manager를 가지고 auto commit false로 설정
- 어떤 작업 = ( ex. 오늘은 7월 4일 미국 독립 기념일이래요.) 수행 = 핵심 관심사
- transaction commit
- 어떤 작업을 try-catch-finally로 묶어 실행하기 때문에, catch구간에서 문제 발생 시 transaction을 rollback 시킴
- 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
코드샘플
'Spring > about spring' 카테고리의 다른 글
[IoC 컨테이너와 빈] @Autowired (0) | 2023.07.22 |
---|---|
[IoC 컨테이너와 빈] ApplicationContext (0) | 2023.07.19 |
[스프링 삼각형] PSA 잘 만든 인터페이스 (0) | 2023.07.17 |
[스프링 삼각형] Inversion of Control 제어의 역전 (0) | 2023.07.11 |
[Transaction] 트랜잭션 전파옵션, 격리수준 (0) | 2023.06.06 |