ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 토비의 스프링 - 1권 6장 AOP
    BackEnd/SpringBoot 2023. 10. 8. 00:54

    1. 관심사의 분리

    - upgradeLevels 메서드 기능

    회원 등급 업그레이드 코드

    public void upgradeLevels() {
    	// 	트랜잭션 경계 시작
    	TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    
    	try {
    		List<User> users = userDao.getAll();
    		for (User user : users) {
    			if (canUpgradeLevel(user)) {
    			upgradeLevel();
    			}
            	}
    	}
        
    	// 트랜잭션 경계 끝
    	this.transactionManager.commit(status);
        
    	} catch (Exception e) {
    		this.transactionManager.rollback(status);
    	}
    }

    - upgradeLevels 메서드의 문제점

    비지니스 로직과 트랜잭션 설정 로직이 한 메서드에 섞여 있다.

    지금은 쉽게 구분이 되지만 추후 코드가 길어진다면 관심사가 분리 되지않아 보기가 불편하며 수정이 복잡해진다.

    - upgradeLevels 메서드의 기능들

    핵심 기능 : 회원 등급 업그레이드

    부가 기능 : 트랜잭션 경계 설정

    - 핵심 기능과 부가기능을 분리하는 방법 3가지

    1. 전략 패턴

    위 코드에서 트랜잭션 기능을 제거 한후 트랜잭션 인터페이스와 클래스를 만들어 DI로 주입하여 사용하는 방법.

    장점 : 회원 등급 메서드에서 트랜잭션 기능을 제거하여 핵심기능에 집중할 수 있고 부가기능의 존재를 몰라도 된다.

    단점 :  핵심기능이 부가기능을 거치지않고 직접사용하면 원하는 방법으로 적용되지않는다.

     

    2. 프록시 패턴

    프록시는 타깃의 기능을 확장하거나 추가하지 않고 접근방법을 바꾼다. 정확히 타깃의 레퍼런스를 알 필요가 있는경우 적용한다.

    실제 타깃 오브젝트를 만드는 대신 프록시를 넘긴다음 프록시를 통해서 실제 오브젝트를 사용하려 할때 생성한다.

    장점 : 원하는 시점에 타깃 객체를 생성 할 수 있도록 지연한다. 예를 들어 서버의 캐시 기능처럼 초기 요청시 생성한 내용의 레퍼런스를 물고 있어서 추후에 같은 요청이 온 경우 다시 객체를 생성하지않고 레퍼런스를 보여 준다.

     

    3. 데코레이터 패턴

    부가기능들을 핵심기능과 같은 인터페이스로 구현하는 프록시를 설정한 후 각 부가기능들에 원하는 기능을 추가한다.

    장점 : 서로 하나의 인터페이스로 추상화 되었기때문에 순서변경이나 타깃의 타입에 큰 부담이 없다.

     

    프록시 패턴과 데코레이터 패턴은 매우 유사하지만 타깃 객체의 접근 제어가 우선인지 타깃의 기능이 확장인지에 따라서 구별할 수 있다.

    2. 프록시의 단점

    1. 매번 타깃과 같은 인터페이스를 구현하는 클래스를 만들고 코드를 작성해야하기 때문에 번거롭고 코드량이 많아진다.

    2. 부가기능 코드가 중복될 가능성이 높다. 현재는 회원의 업그레이드 메서드를 구현하고 있지만 추후에 회원가입 등 트랙잰션이 필요한 경우가 많은 경우도

    매번 프록시를 사용시 부가기능 코드가 중복된다.

    3. 프록시의 단점 해결 방안

    1. 리플렉션

    일일이 프록시 클래스를 구현하지 않아도 몇가지 API를 이용해 프록시 처럼 동작 하는 오브젝트를 다이나믹하게 생성 할 수 있다.

     

    예시)

    Method를 통해서 리플렉션 방식으로 호출하는 방법

    String name = "Spring";
    
    asserThat(name.length(), is(6))
    
    Method lengthMethod = String.class.getMethod("length");
    
    assertThat((Integer) lengthMethod.invoke(name), is(6));

    인터페이스인 InvocationHandler에 invoke 메서드를 구현함으로 부가기능을 적용 할 수 있다.

    public interface InvocationHandler {
    
    	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    
    }

    대문자 변환 구현 클래스

    public class UppercaseHandler implements InvocationHandler {
    
    	Hello target;
    
    	public UppercaseHandler(Hello target) {
    		this.target = target;
    	}
    
    	@Override
    	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    	String ret = (String) method.invoke(target, args);
    
    	return ret.toUpperCase();
        
    	}
    }

    프록시 생성

    Hello proxiedHello = (Hello) Proxy.newProxyInstance(
    
    getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용되는 클래스로더
    
    new Class[]{Hello.class}, // 구현 할 인터페이스
    
    new UppercaseHandler(new HelloTarget()) // 부가기능과 위임 코드를 담은 invocationHandler
    
    );

    위와 같이 InvocationHandler을 통해서 얻는 이점

     

    장점

    1.부가기능이 추가되어도 구현된 다른 부가기능 인터페이스를 수정 할 필요가 없다.

    예를 들어 3개 정도의 부가기능이 있었던 인터페이스에 100개 정도의 부가기능이 추가된다면 모든 부가기능에 100개의 메서드를 구현해줘야한다는 의미인데

    이 같은 중복 코드를 작성하지않아도 InvocationHandler를 구현함으로 해결할 수 있다.

     

    2. 프록시 생성 코드와 같이 타깃에 종류에 상관없이 적용이 가능하다.

     

    3. 호출하는 메서드의 이름, 파라미터 개수와 타입, 리턴 타입 등의 정보를 가지고 부가적인 기능을 적용할 메서드를 선택할 수 있다.

     

    단점

    1. 다이나믹 프록시 오브젝트는 일반 스프링 빈으로 등록할 수 없기 때문에 일반적인 DI를 위해서는 FactoryBean<T> 인터페이스를 구현하여 하는 번거로움이 있다.

     

    2. 타깃에 부여하는 부가기능은 메서드 단위로 일어나는 일이다. 하나의 클래스 안에 존재하는 여러개의 부가기능은 한번에 적용 할 수 있지만 다수의 클래스를 대상으로 공통적인 기능을 부여하기는 어렵다.

    2. ProxyFactoryBean

    포인트 컷과 어드바이스 활용

    포인트 컷 : 메서드 선정 방법 ( 메서드 선정 알고리즘 )

    어드바이스 : 타깃이 필요 없는 부가기능

     

    어드바이스 설정

    static class newUppercaseAdvise implements MethodInterceptor {
    
    	@Override
    	public Object invoke(MethodInvocation invocation) throws Throwable {
    		
            String ret = (String) invocation.proceed(); // 리플렉션 자바와 달리 메서드 실행시 타깃 오브젝트를 전달할 필요가 없다.
    
    		return ret.toUpperCase();
        }
    
    }

    포인트 컷 설정과 검증

    public void proxyFactoryBean() {
    
    	ProxyFactoryBean pfBean = new ProxyFactoryBean();
    
    	NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    
    	pointcut.setMappedName("sayH*");
    
    	// 메서드 선정 알고리즘 과 부가기능을 담은 어드바이스 추가
    	pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseHandler.newUppercaseAdvise()));
    
    	pfBean.setTarget(new HelloTarget()); // 타깃 설정
    
    	Hello proxiedHello = (Hello) pfBean.getObject();
    
    	// 메서드 선정 알고리즘과 선정 되기 때문에 대문자 적용
    	assertThat(proxiedHello.sayHello("TORY"), is("HELLO TORY"));
    
    	// 메서드 선정 알고리즘에 선정 되지 않기 때문에 미적용
    	assertThat(proxiedHello.sayThankYou("TORY"), is("Thank You Tory"));
    
    }

     

    정리 

    AOP 에스펙트 지향 프로그래밍은 기존에 객체지향 기술의 설계방법으로 독립적인 모듈화가 불가능한 트랜잭션 경계설정과 같은 부가기능을 어떻게 모듈화 할지 연구해온 사람들이 객체지향 프로그래밍 설계 패러다임과 다르기때문에 에스펙트라는 새로운 이름으로 부르기 시작했다.

    AOP는 기능을 각 부가기능을 독립적으로 모듈화하여 핵심기능에 집중할 수 있도록 설계하는데 중점을 두고있다.

     

     

     

    댓글

Designed by Tistory.