스프링 프레임워크 기본개념 2

김주언·2022년 11월 5일
0

Spring

목록 보기
2/15
post-thumbnail

5. AOP

스프링의 3대 기반 기술 중 하나.
Aspect Oriented Programming의 약자이다.

🤷🏻‍♀️ Aspect Oriented Programming의 의미?

  • Aspect를 만드는 프로그래밍 방법
  • Aspect 지향 프로그램

관점 지향 프로그래밍

AOP는 메인 프로그램의 비즈니스 로직으로부터 2차적 또는 보조 기능들을 고립시키는 프로그램 패러다임(관점 지향 프로그래밍 패러다임)이며, 애플리케이션의 핵심적인 기능에서 부가적인 기능을 분리해서 애스펙트(Aspect)로 정의하고 설계하여 개발하는 방식.

좋은 개발환경의 중요 원칙 → "개발자가 비즈니스 로직에만 집중할 수 있게 하는 것"
이를 위하여 가장 쉽게 생각 할 수 있는 것은 반복적인 코드의 제거

대부분의 시스템이 공통으로 가지는 보안이나 로그, 트랜잭션과 같이 비즈니스 로직은 아니지만, 반드시 처리가 필요한 부분을 횡단 관심사 라고 한다. 스프링은 이러한 횡단 관심사를 분리해서 제작하는 것이 가능하게 지원한다.

즉, 전통적인 객체지향기술의 설계방법으로는 독립적인 모듈화가 불가능한 (보안, 로그, 트랜잭션 설정 등) 부가적인 기능들을 모듈화해주는 것. (= 핵심기능과 부가 기능의 분리)

이러한 모듈로의 분리를 통해 횡단관심사와 이에 영향을 받는 객체 간의 결합도를 낮추는 것이 AOP 프로그래밍의 패러다임!

쉽게 말하자면 클래스들이 공통으로 갖는 기능이나 절차 등을 하나의 모듈로 묶어서 빼내어 이를 별도로 관리하는 것이 목적이다.

위에서도 언급된 바와 같이, 스프링에서의 Aspect란

  • 주업무가 아닌 보조업무
    • 보조 업무 : 로그, 트랜잭션, 보안처리 등


5.1 AOP의 구현이란

: 주 업무가 아닌 보조적인 업무를 주 업무를 처리하는 코드에서 분리하는 것



5.2 AOP의 장점

  1. 전체 코드 곳곳에 흩어져 았는 다양한 관심 사항이 (트랜잭션, 로그 등) 하나의 장소로 응집된다
  2. 여타 서비스 모듈이 자신의 주요 관심 사항 (핵심 기능)에 대한 코드만 포함하고, 그 외의 관심 사항은 모든 Aspect로 옮겨지므로 코드가 깔끔해지고 가독성이 높아진다.
  3. 개발자는 핵심 비즈니스 로직에만 집중해서 코드를 개발할 수 있게 된다.
  4. 각 프로젝트마다 다른 관심사 적용 시 코드 수정 최소화 가능


5.3 AOP 시각화

위와 같은 그림은 핵심 관심사와 주변 관심사가 섞여 있는 모습. 이를 독립 Aspect를 이용하여 부가기능을 분리하고 모듈화한다.

Aspect를 모듈로 분리하고, 핵심기능들은 부가기능이 필요할때마다 가져다 쓰는 형식이다.



5.4 실제 코드에서의 AOP 처리

 기존의 개발 방식은 보조 업무를 담당하는 코드가 주 업무 코드 사이사이에 포함되어 있음.

 보조 업무가 주 업무 코드에 포함될 때에는 다음과 같은 일이 발생.  

  • 동일한 작업 반복. 
  • 보조 업무의 작업 코드가 변경될 시, 해당 보조 업무를 사용하는 모든 주 업무 코드의 소스 수정 필요. 
  • 주 업무 코드보다 더 많은 양의 보조 업무 코드 – 특히 DB 객체 생성 및 접속, 예외 처리, DB 닫기 등.

 AOP는 이러한 보조 업무 코드를 주 업무 코드에서 별도로 분리하여 작성하고, 필요할 때에만 도킹(Docking)하여 사용하는 것은 어떨까? 하는 발상에서 나온 개념.

  여기서 핵심 코드(핵심 관심사)는 Core(Primary) Concern.
부가/보조 업무 코드(횡단 관심사)는 Cross-Cutting Concern.

 위의 오른쪽 그림에서 보듯이 주 업무에서 보조 업무를 횡단으로 잘라내었다는 의미로 Cross-Cutting 이라 불림.



개발자 입장에서 AOP를 적용한다는 것 = 기존 코드의 수정 없이 원하는 관심사(cross-concern)들을 엮을 수 있다는 것. Target이 개발자가 작성한 핵심 비즈니스 로직을 가지는 객체이다.

  • Target
    Target은 순수 비즈니스 로직을 의미하고, 어떠한 관심사들과도 관계를 맺지 않는 순수한 코어 ( 주업무 )

  • Proxy
    Target을 전체적으로 감싸는 부분을 Proxy라고 한다.
    Proxy는 내부적으로 Target을 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 또는 수동으로 작성된다.
    대부분의 경우 Proxy는 스프링 AOP 기능을 이용하여 자동 생성되는 방식을 사용한다. (auto-proxy)

  • JoinPoint
    JoinPoint는 Target 객체가 가진 메서드. 외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식이다.



AOP Proxy

프록시란, 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장하여 클라이언트의 요청을 받아주어 처리하는 대리자 역할

프록시라는 단어 자체는 '대리인'이라는 의미. 스프링 AOP에서 프록시란 말 그대로 업무를 대리로 처리하는 것. 함수 호출자는 주요 업무가 아닌 보조 업무를 프록시에게 맡기고, 프록시는 내부적으로 이러한 보조 업무를 처리한다.

이를 통해 주 업무 코드는 보조 업무가 필요할 경우 해당 프록시만 추가하면 되고, 필요 없게 되면 프록시를 제거하면 된다.
보조업무의 탈부착이 쉬워짐에 따라 주 업무 코드는 보조 업무 코드의 변경으로 인해 발생하는 코드 수정 작업이 필요없게 된다.

프록시의 호출 및 처리 순서

빨간 원은 Target이 가진 메서드. 즉 JoinPoint

  1. 프록시 호출
  2. 보조업무 처리
  3. 프록시 처리함수가 실제 구현함수에서 호출 및 주 업무 처리
  4. 제어권이  다시 Proxy 함수(메서드)로 넘어오고 나머지 보조 업무 처리
  5. 처리 작업 완료 후, 호출 함수(메서드)로 반환.

프록시의 사용목적

  • 클라이언트가 타깃(Target)에 접근하는 방법을 제어.
  • 타깃에 부가적인 기능을 부여.


프록시 구현 예제

  1. 사칙연산을 위한 인터페이스와 실제 기능을 구현한 클래스 정의
// 인터페이스
public interface Calculator {
    public int add(int x, int y);
    public int subtract(int x, int y);
    public int multiply(int x, int y);
    public int divide(int x, int y);
}
// 사칙 연산을 구현하는 클래스 
public class myCalculator implements Calculator {

    @Override
    public int add(int x, int y) {
        return x + y;
    }
   
    @Override
    public int subtract(int x, int y) {
        return x – y;
    }
   
    @Override
    public int multiply(int x, int y) {
        return x * y;
    }
 
    @Override
    public int divide(int x, int y) {
        return x / y;
    }
}

  1. 실제로 사칙연산 클래스를 사용하는 예제 코드
public static void main(String[] args) {
    Calculator cal = new myCalculator(); // 다형성 
    System.out.println(cal.add(3, 4)); // add 메서드를 호출하여 3 + 4 결과를 출력 
}

2.1. 만약 이 때 실제 사칙연산을 하는데에 소요되는 시간을 측정하는 기능이 필요하다면?
add() 메서드는 아래와 같은 형태로 변경되어야 한다.

    @Override
    public int add(int x, int y) {

       // 보조 업무 (시간 측정 시작 & 로그 출력)
      Log log = LogFactory.getLog(this.getClass());
      StopWatch sw = new StopWatch();
      sw.start();
      log.info(Timer Begin);


      int sum = x + y; // 주 업무 (덧셈 연산)


       // 보조 업무 (시간 측정 끝 & 측정 시간 로그 출력)
      sw.stop();
      log.info(Timer StopElapsed Time :+ sw.getTotalTimeMillis());


      return sum;
    }

위와 같이 시간을 측정하기 위한 업무(실제 연산 업무가 아니기 때문에 보조 업무)가 add 메서드에 추가됨.
하지만, 시간 측정을 위한 이러한 코드를 add 메서드 뿐만 아니라, subtract, multiply, divide 메서드에도 추가해 주어야 하고, 설령 측정 방식이 달라지거나 로그 출력 내용이 변경 되기라도 한다면 모든 메서드를 수정해야 한다. 게다가 연산을 위한 주 업무 코드를 수정하는 것도 아니다.

그렇다면 좀 더 코드를 깔끔하게 하고, 관리도 쉽게 하고, 직관적인 프로그래밍을 위해서는 어떻게 변화를 줄 수 있을까?
주 업무와 보조 업무를 분리(크로스 컷팅, Cross Cutting)하고 보조 업무를 프록시(Proxy)에게 넘긴다!


AOP Proxy 구현 코드

// 보조 업무를 처리할 프록시 클래스 정의 (여기서는 LogPrintHandler 라 이름 지었음) 
public class LogPrintHandler implements InvocationHandler { // 프록시 클래스 (핸들러)
    private Object target; // 객체에 대한 정보 

 
    public LogPrintHandler(Object target) { // 생성자 
        this.target = target;
    }
 

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable   {
        Log log = LogFactory.getLog(this.getClass());
        StopWatch sw = new StopWatch();
        sw.start();
        log.info(Timer Begin);

        int result = (int) method.invoke(target, args); // (3) 주업무를 invoke 함수를 통해 호출

        sw.stop();
        log.info(Timer StopElapsed Time :+ sw.getTotalTimeMillis());

        return result;
} 
 
 
 
public static void main(String[] args) {
    Calculator cal = new myCalculator();
 
    Calculator proxy_cal = (Calculator) Proxy.newProxyInstance( // (1)
        cal.getClass().getClassLoader(),  // Loader
        cal.getClass().getInterfaces(), // Interface
        new LogPrintHandler(cal)); // Handler (보조 업무를 구현하고 있는 실제 클래스)

 
    System.out.println(proxy_cal.add(3, 4));    // (2) 주 업무 처리 클래스의 add 메서드를 호출
} 
 
  1. InvocationHandler 인터페이스를 구현한 객체는 invoke 메소드를 구현해야 한다.
    이 객체는 실제 타겟이 되는 객체의 메소드를 호출해준다. 메인 메서드에서 Proxy.newProxyInstance를 통해 주 업무를 처리할 클래스와 보조 업무를 처리할 프록시 클래스를 결합한다.
  • cal(변수) 실제 객체를 proxy_cal(변수) 객체에 핸들러를 통해서 전달. (LogPrintHandler())
  • getClassLoader() : 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더. 
  • getInterfaces() : 구현할 인터페이스. 
  • LogPrintHandler(cal) : 부가 기능과 위임 코드를 담은 핸들러.
  1. 이후 주 업무 클래스의 메서드를 호출하게 되면 프록시 클래스의 invoke() 메서드가 호출되어 보조 업무를 처리하고, 주 업무의 메서드를 본인이 대신 호출한다.
  • invoke() : 메서드를 실행시킬 대상 객체와 파라미터 목록을 받아서 메소드를 호출한 후 그 결과를 Object 타입으로 반환한다.

실제 프록시 및 주 업무의 처리 순서
클라이언트 → 프록시 → 타겟(주 업무)

AOP 개념은 스프링에 한정된 개념이 아니다. (위의 코드는 AOP Proxy의 구현을 스프링 개념없이 순수 JAVA 코드단에서 구현한 것)

다만 스프링을 통해서라면 AOP는 XML과 어노테이션을 통해 더 쉽게 구현이 가능 한 것.

profile
학생 점심을 좀 차리시길 바랍니다

0개의 댓글