본문 바로가기

카테고리 없음

AOP를 사용한 로그인 확인 구현

A. AOP란

  • AOP(Aspect Oriented Programming)이란 Aspect라는 종단 관심사(Crosscutting Concerns)를 담은 모듈을 기반으로 프로그래밍을 하는 것입니다
  • OOP에서 모듈의 관심사가 Class이듯이 AOP에서는 Aspect 입니다.
  • Aspect는 기본적으로 어드바이스와 포인트 컷으로 이루어 집니다.
  • 포인트컷은 적용 위치를 선별하고 어드바이스는 부가 기능을 추가합니다.
  • Aspect를 통해 핵심 관심사(핵심 비즈니스)와 부가 기능을 분리할 수 있고 반복되는 부가기능은 재사용 가능합니다.
    • 예) 트랜잭션, 로깅등

B. AOP의 원리 및 발전

1.  Proxy 패턴

  • 부가 기능을 추가하는 AOP의 Aspect는 프록시 패턴에서 시작합니다.
  • 기능
    • 타깃과 같은 메소드를 구현하고 있다가 메소드 호출시 타깃 오브젝트로 위임
    • 지정된 요청에 대해 부가기능 수행 가능
  • 단점
    • 타깃 인터페이스를 구현하고 위임하는 코드를 작성하기 번거롭다.
      • 타깃 인터페이스의 메소드가 추가 변경 될때마다 함께 수정 해주어야 한다.
    • 부가 기능 코드가 중복될 가능성이 많습니다
public class ProxyAction implements Action {

    private OriginalAction action;
    
    @Override
    public void displayImage() {
    	if (action == null) {
        	action = new OriginalAction();
        }
        action.displayImage();
        //대리 실행.
    }
}

 

2.다이나믹 프록시

  • 프록시에서 프록시 타겟에게 의존된 방식 → 다이나믹 프록시는 InvocationHandler로 어떤 클래스든 리플랙션을 이용하여 코드 변경 없이 프록시로 만들 수 있고 FactoryBean을 통해서 빈 등록이 가능하다. + 필요시 부가 기능 등록도 가능
  • 한계: 여러 클래스에 부가기능 추가는 어렵다./ 구현된 Handler가 팩토리 빈 개수만큼 생김.
  • 리플랙션 이용
  • 오브젝트는 만들어 주지만 부가기능은 직접 작성
    • 부가 기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담음.
  • 프록시 클래스는 클래스의 메타정보를 바탕으로 InvocationHandler의 리플렉션을 이용하는 invoke을 동작시켜서 메서드를 동작.
public class Test implements InvocationHandler {
    Hello hi;

    public Test(Hello hi) {
        this.hi = hi;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        String ret = (String) method.invoke(hi, objects);
        return ret;
    }
}
// ret은 return이 있는 경우. 이렇게 메서드 단위로 만들어서

Hello proxiedCheckIn = (Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(), new Class[]{Hello.class}, new Test(new Hi()));
proxiedCheckIn.printName();

 

  • 프록시를 빈으로 등록 시키려면 FactoryBean을 만들어야 합니다.
public interface FactoryBean<T>{
	T getObject() throws Exception;
	Class<? extends T> getObjectType();
	boolean isSingleton();
}

public class MessageFactoryBean implements FactoryBean<Message>{
	
	String text; //Message의 필드
	
	public void setText(String text){
		this.text = text;
	}
	
	public Message getObject() throws Exception {
		return Message.newMessage(this.text); 
		//Message가 정적 팩토리 메서드로 생성. 다른 방법도 가능
		// --> 이때 프록시인 경우 프록시를 넣어 준다.
		// ... set등 생성후
		return Proxy.newProxyInstance(getClass().getClassLoader(), 
						new Class[]{theInterface}, txHandler);
			
	}
	
	public Class<? extends Message> getObjectType() {
		return Message.class;
	}
	
	public boolean isSingleton(){
		return false;
	}
	
}

@Test
public void test(){
	Object message = context.getBean("message"); 
	//XML등록 후 받아 올 수 있다. 
}

 

3. 다이나믹 프록시 - 스프링의 방법

  • ProxyFactoryBean
    • 여러개의 어드바이저(Advisor)와 관련 포인트컷을 등록할 수 있기에 동시에 등록시에는 addAdvisor()를 이용해서 Advisor에 담아서 등록.
    • MethodInterceptor 인터페이스를 통해 생성 → ProxyFactoryBean에게서 타깃 오브젝트의 정보도 받기에 타깃을 알 필요없이 독립적으로 만들어 진다.
  • 관심사의 분리
    • 프록시로 부터 포인트컷(PointCut)과 어드바이스(Advice) 분리.
    • 부가 기능을 제공하는 Advice
    • 메소드 선정 알고리즘인 PointCut → 타깃 메서드인지 확인 한다.
    ⇒ 재사용한 기능을 만들어 놓고 외부에서 바뀌는 부분을 주입하는 탬플릿/콜백 구조. (어드바이스가 템플릿이 되고 Invocation을 통해 타깃 메서드를 콜백한다.)
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
pfBean.addAdvice(new UppercaseAdvice()); // 부가 기능 추가.


static class UppercaseAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable{
	String ret = (String)invocation.proceed(); //타깃 오브젝트 전달하지 않음.
	return ret.toUpperCase();
}

}
  • 빈 후처리기(Bean PostProcessor)
    • 스프링에서는 프록시 생성 기능을 IoC 적용하여 구현한 “빈 후처리기”를 통해서 자동 생성 할 수 있다.
    • ProxyFactoryBean에 포인트컷과 어드바이저를 등록해 놓으면 포인트 컷이
      • 포인트컷에는 클래스 필터(Class Filter)와 메소드 매처(Method Matcher)가 있다.
      → 수동 제작시에는 메소드 매처만 이용할 수 있지만 빈 후처리기 사용시 일단 클래스 필터로 어드바이스를 적용할 클래스인지 확인후에 매소드에만 적용된 경우 특정 메소드를 찾아서 적용해 준다.
  • 포인트컷 표현식(Pointcut Expression)
    • AspectJ 포인트컷 표현식을 사용한다.
    • AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 를 통해 등록 가능
    • 표현식
      • [ , ] 대괄호는 옵션항목이라 생략 가능/ ; 는 Or 조건
      • … 여러개의 패키지/ * 모든 현 폴더의 패키지
      • execution([접근 제한자 패턴] 타입 패턴 [타입패턴]. 이름패턴 (타입패턴; “..”, …)
        • 타입 패턴 첫번째 : 리턴 타입
        • 타입 패턴 두번째(옵션): 패키지와 타입이름을 포함 클래스의 타입 (생략시 모든 클래스에 적용)
        • 이름 패턴: 메소드의 이름
        • 타입 패턴 세번쨰: 메소드의 파라미터 타입 패턴 ‘,’ 로 구분하며 순서대로/ 없으면 (). 타입과 개수 상관없이 모두 다 혀용하는 패턴 = “…”
        • 마지막은 예외에 대한 이름 타입 패턴

4. AOP (Aspect Oriented Programming, 관점 지향 프로그래밍)

  • 종류
    • 스프링 AOP
      • 스프링 컨테이너를 이용하여 Proxy를 생성하는 방식
    • AspectJ
      • 컴파일된 타깃 클래스 파일 자체를 수정하거나 JVM이 클래스 로딩하는 시점에 가로채서 바이트코드를 조작
  • 스프링 AOP 종류
    • 동적 프록시 (JDK Dynamic Proxy)
      • 하나 이상의 인터페이스를 구현하였을떄.
      • Spring Framework의 기본 방식
      • 리플렉션을 통해 생성하여 성능 떨어짐
    • CGLIB (Code General Library)
      • 인터페이스를 구현하지 않았을때 사용
      • SpringBoot 2.0부터 기본 방식
      • final 클래스나 매서드에 적용 불가 → 예외가 아니라 단순히 동작하지 않는다.
      • 바이트 코드를 조작해서 만들기에 성능이 빠르다.
      • JDK Proxy에 비해 unexpected class casting exception 나올 가능성 적음.
  • 추가 용어
    • 조인 포인트(Join Point): 어드바이스가 적용될 수 있는 위치 = 포인트 컷을 통해서 조인 포인트를 선별.
    • Aspect: OOP의 클래스 처럼 AOP의 기본 모듈. 포인트컷과 어드바이스의 조합으로 만ㄴ들어 지며 보통 싱글톤 형태의 오브젝트로 존재. 클래스와 다르게 템플릿과 실체의 차이가 없다.
  • 등록법: 4가지를 등록 빈으로 등록해야 한다.
    • 자동 프록시 생성기
      • @EnableAspectJAutoProxy : DefaultAdvisorAutoProxyCreator를 상속해 발전 시킨 AnnotationAwareAdvisorAutoProxyCreator를 등록. 이건 @Aspect어노테이션을 읽어서 프록시를 만든다.
    • 어드바이스: 에스팩트 클래스의 부가 메서드
    • 포인트컷: @Before, @After..etc
    • 어드바이저 : @Aspect을 통해서 에스팩트 클래스를 정의하고 스프링이 자동으로 어드바이저 생성
  • *주의점
    • AOP는 자기 자신이 호출하는 경우에는 일어나지 않는다.(내부 메소드 문제)
      • 왜냐하면 AOP의 적용은 스프링 컨테이너가 빈후처리기를 통해 런타임에 프록시를 적용하기 때문에.

C.AOP 적용

  • Authentication은 항상 받도록 하고 Authentication이 필요하지 않는 곳에 적용을 풀기 위해 사용하였습니다. 

1. 자동 프록시 생성기

@SpringBootApplication
@EnableAspectJAutoProxy
public class  MemberContextApplication {
    public static void main(String[] args) {
        SpringApplication.run(MemberContextApplication.class, args);
    }
}

 

2. 어드바이스, 포인트 컷, 어드바이저

@Aspect
@Component
public class AuthenticationAspect {

    @Autowired
    private HttpServletRequest request;

    @Before("execution(public * com.membercontext.memberAPI.web.controller.*.*(..)) " +
            "&& !@annotation(com.membercontext.memberAPI.application.aop.authentication.NoAuthentication)")
    public void authenticate() {
        String cookieValue = checkCookie();
        checkSession(cookieValue);
    }

 

3. 어노테이션

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

 

출처

토비의 스프링 3.1 Vol 1.