스프링 PSA(Portable Service Abstraction)

환경의 변화에 상관없이 일관된 방식의 기술 접근 환경을 제공하려는 추상화 구조

Spring은 Spring Web MVC, Spring Transaction, Spring Cache등의 다양한 기술에 Portable Service Abstraction을 제공하고 있다


Web MVC와 관련된 Service Abstraction

Spring MVC는 서블릿 어플리케이션임에도 불구하고 서블릿이 전혀 겉으로 들어나지 않았다.

단지 @Controller 어노테이션이 붙어잇는 클래스에서 @GetMapping, @PostMapping과 같은 @RequestMapping 애노테이션을 사용해서 요청을 매핑한다

실제롤는 내부적으로 서블릿 기반으로 코드가 동작하고 있지만 서블릿 기술은 추상화 계층에 숨겨져 있는 것이다.

이렇게 추상화 계층을 사용하여 어떤 기술을 내부에 숨기고 개발자에게 편의성을 제공해주는 것을 Service Abstraction이라 한다.

더하여 Service Abstraction으로 제공되는 기술을 다른 기술 스택으로 간편하게 바꿀 수 있는 확장성을 갖고 있는 것이 Portable Service Abstraction이다.


Spring Web MVC

Service Abstraction

일반 클래스에 @Controller 애노테이션을 사용하면 요청을 매핑할 수 있는 컨트롤러 역할을 수행하는 클래스가 된다.

그 클래스에서는 @GetMapping과 @PostMapping 애노테이션을 사용해서 요청을 매핑할 수 있다

요청을 처리하는 메소드들은 뷰를 리턴하며 각 메소드에서 model에 담은 객체가 모델에 해당한다

Spring Web MVC를 사용하면 이렇게 서블릿을 간편하게 개발할 수 있는데, 뒷단에 spring이 제공해주는 여러 기능들이 숨겨져 있기 때문이다.

즉 서블릿의 HttpServlet을 상속받고 doGet(), doPost() 작성하는 등의 작업을 직접하지 않아도 된다

Service Abstarction의 목적 중 하나가 이러한 편의성을 제공하는 것이다.

Portable

Spring Web MVC가 Tomcat기반을 동작하고 있을 때 코드를 거의 그대로 둔 상태에서 톰캣이 아닌 완전히 다른 기술 스택으로 실행하는 것이 가능하다.

spring-boot-starter-web 의존성 대신 프로젝트를 spring-boot-starter-webflux로 변경하기만 하면 톰캣이 아닌 netty기반을 실행되게 할 수 도 있다.

원래 기존에 톰캣으로 실행하던 프로젝트를 netty기반으로 실행하게 하려면 더 복잡한 과정이 필요하지만 spring이 제공해주는 Spring Web MVC 추상화 계층을 사용해서 간단히 netty로 실행할 수 있는 것이다.

이렇게 Spring Web MVC는 @Controller, @RequestMapping과 같은 애노테이션과 뒷단의 여러가지 복잡한 인터페이스들 그리고 기술들을 기반으로 하여 사용자가 웹 기술 스택을 간편하게 바꿀 수 있도록 해준다

중요한 것은 이런 것들이 기존 코드를 거의 변경하지 않고도 가능하다는 것이다.


Spring Transaction

트랜잭션을 처리하려면 setAutoCommit()과 commit(), rollback()을 명시적을 호출해야 한다

그러나 Spring이 제공하는 @Transactional 애노테이션을 사용하면 단순히 메소드에 애노테이션을 붙여줌으로써 트랜잭션 처리가 이루어진다.

이 또한 PSA로써 다양한 기술 스택으로 구현체를 바꿀 수 있다.

예를 들어 JDBC를 사용하면 DatasourceTransactionManager, JPA를 사용하는 JpaTransactionManager, Hibernate를 사용하는 HibernateTransactionManager를 유연하게 바꿔서 사용할 수 있다.

즉 기존 코드는 변경하지 않은 채로 트랜잭션을 실제로 처리하는 구현체를 사용 기술에 따라 바꿔 끼울 수 있다


Spring Cache

Cache도 마찬기지로 JCacheManagerm ConcurrentMapCacheManager, EhCacheManager와 같은 여러가지 구현체를 사용할 수 있다.

사용자는 @Cacheable 애노테이션을 붙여줌으로써 구현체를 크게 신경쓰지 않아도 필요에ㄷ 따라 바꿔 쓸 수 있는 것이다.

이렇게 spring이 제공해주는 다양한 PSA 기술 덕분에 코드는 더 견고해지고 기술이 바뀌어도 유연하게 대처할 수 있게 된다.

Comment and share


스프링 AOP(Aspect Oriented Programming)

AOP

AOP는 Asepect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.

관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고

부가적인 관점을 각각 따로 분리해서 모듈화하겠다는 것이다.

예로들어 핵심적인 관점은 우리가 적용하고자 하는 핵심 비지니스 로직이 된다.

또한 부가적인 관점은 핵심 로직외에 행해지는 데이터베이스 연결, 로깅, 파일 입출력등이 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화하겠다는 의미다.

이때, 소스 코드상에서 핵심 비지니스 로직(Core Concerns)외에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.

스크린샷 2020-11-27 오후 12 59 42

위와 같이 흩어진 관심사(Crosscutting Concerns)를 Aspect로 모듈화하고 핵심적인 비지니스 로직(Core concerns)에서 분리하여 재사용하겠다는 것이 AOP이다.

AOP 관련 용어

  • 조인 포인트(JoinPoint) : 클라이언트가 호출하는 모든 비지니스 메소드, 조인포인트 중에서 포인트컷이 되기때문에 포인트 컷의 후보로 생각할 수 있다.
  • 포인트 컷(Pointcut) : 특정 조건에 의해 필터링된 조인 포인트, 수 많은 조인 포인트 중에 특정 메소드에서만 Advice를 수행시키기 위해서 사용된다
  • 어드바이스(Advice) : Crosscutting Concern에 해당하는 공통 기능의 코드
  • 애스펙트(Aspect) : 포인트 컷과 어드바이스의 결합이다. 어떤 포인트 컷 메소드에 대해 어떤 어드바이스 메소드가 실행할 지 결정한다.
  • 위빙(Weaving) : 핵심 비지니스 로직과 Aspect를 연결하여 Advised Object를 만드는 과정
    1. complie time
    2. load time
    3. runtime

AOP 구현 방법

  1. 컴파일

    • A.java —— (AOP) —–> A.class
    • 컴파일을 할 때 중간에 공통 로직을 끼워 넣는다
    • 컴파일하기 전 코드에는 해당 로직이 없지만, 컴파일이 완료되면 해당 로직을 가지고
      있는 상태이다
    • AspectJ를 이용해서 만들어진다
  2. 바이트코드 조작

    • A.java —-> A.class —-> (AOP) —-> 메모리
    • load time에서 ClassLoader가 A.class를 읽어온다
    • 읽어와서 메모리에 올릴 때 공통 로직이 들어가도록 조작한다
    • AspectJ를 이용해서 만들어진다
  3. 프록시 패턴

    • Runtime에서 구현하는 방식
    • 프록시 패턴을 적용하여 실제 객체가 실행되는 것이 아닌 실제 객체를 감싸고 있는 프록시 객체를 Runtime에 생성
    • 프록시 객체는 Aspect가 적용되어있고 Core Concern이 들어있는 실제 객체를 reference하고 있다
    • AOP가 적용된 객체를 요청할 때 실제 객체가 아닌 프록시 객체가 실행된다.
    • AspectJ로 만들어져 있으며 SPRING AOP는 이 방식으로 구현되어있다.

Spring AOP

스프링 AOP 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있다
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, Spring IoC와 연동하여 어플리케이션의 문제에 대한 해결책을 제공하는 것이 목적

프록시 패턴

  • 접근 제어 또는 부가기능 추가

    image

    • 인터페이스에 대해서 핵심 비지니스 로직만으로 구성되어진 실제 객체를 만든다
    • 인터페이스에 대해서 또 하나의 Proxy 구현체를 만든다
    • 이 구현체에는 부가기능이 추가있으며 실제 객체를 참조하여 실제 객체의 비지니스 로직을 실행 시킬 수 있다
    • 클라이언트는 객체를 인터페이스를 통해서 접근하게 되며, 실제객체가 직접 실행되는 것이 아닌 Proxy객체를 통해서 부가기능을 수행하고 간접적으로 실행된다.

프록시 패턴을 적용 (Spring AOP를 사용하지 않음)

  • EventService Interface

    1
    2
    3
    4
    5
    6
    7
    public interface EventService {
    void createEvent();

    void publishEvent();

    void deleteEvent();
    }
  • EventService 구현체 (핵심 비지니스 로직 수행)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @Service
    public class SimpleEventService implements EventService{

    @Override
    public void createEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Created an event");
    }

    @Override
    public void publishEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }
  • EventService의 Proxy 구현체 (부가기능 + 핵심 로직 객체 참조)

    @Primary로 SimpleEventService가 아닌 ProxySimpleEventService이 실행됨

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Primary
    @Service
    public class ProxySimpleEventService implements EventService{

    @Autowired
    SimpleEventService simpleEventService;

    @Override
    public void createEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.createEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.publishEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
    simpleEventService.deleteEvent();
    }
    }

문제점

  • 매번 프록시 패턴을 적용해서 프록시 클래스를 작성해야한다
  • 여러 클래스 여러 메소드들에 적용할려면 반복적인 작업이 증가한다
  • 객체들관의 관계도 복잡하다

스프링 AOP 등장

  • 스프링 IoC컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제 해결
  • 동적 프록시 : 동적으로 프록시 객체를 생성하는 방법
    • 자바가 제공하는 방법은 인터페이스 기반 프록시 생성
    • CGlib은 클래스 기반 프록시도 지원
  • 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록해준다
    • 클라이언트 코드에 변경이 없다

스프링 AOP 적용해서 개선

스프링 AOP는 Spring Container에 등록된 Bean들만 적용된다

  • Pointcut을 Annotation 기반으로 작성

    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.METHOD)
    public @interface PerfLogging {
    }
  • Aspect 작성 (Around 방식을 적용한 Advice 메소드 작성)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class PerfAspect {

    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
    }

    @Before("bean(simpleEventService)")
    public void hello(){
    System.out.println("hello");
    }
    }
    • Around방식은 핵심 비지니스로직 실행 권한을 ProceedingJoinPoint로 받아온다 (Spring Container가 받아옴)
    • 받아온 객체를 실행하기 전 후에 작업을 진행한다
    • 받아온 객체가 return값을 가지고 있으면 Advice메소드에서 그 값을 return해 주어야한다
  • AOP를 적용할 Pointcut메소드를 지정한다(메소드 위에 어노테이션 설정)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Service
    public class SimpleEventService implements EventService{
    @PerfLogging
    @Override
    public void createEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Created an event");
    }

    @PerfLogging
    @Override
    public void publishEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    @Override
    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }

Pointcut 정의 방식

  1. execution 설정 방식

    • 사용 예

      1
      @Around("execution(* com.example..*.EventService.*(..))")
    • 적용 규칙

      스크린샷 2020-11-27 오후 2 51 06
  2. bean 설정 방식

    • 사용 예

      적용할 빈 이름을 직접 입력

      1
      @Around("bean(simpleEventService)")
  3. annotation 설정 방식 (위에서 설명)

Advice 실행 시점

  1. Before

    • 핵심 비지니스 로직 실행 전에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @Before("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사전 처리> " + method + "비지니스 로직 수행 전 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  2. After

    • 핵심 비지니스 로직 실행 후에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @After("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사후 처리> " + method + "비지니스 로직 수행 후 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  3. After Returning

    • 핵심 비지니스 로직 실행한 후 반환된 객체를 Advice에서 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @AfterReturning(pointcut = "!void com.rubypaper.biz..*Impl.*(..))", returning = "returnObj")
      public void afterLog(Object returnObj) {
      System.out.println("<사후 처리> 비지니스 로직 리턴 값 : " + returnObj.toString());

      if (returnObj instanceof UserVO) {
      UserVO user = (UserVO) returnObj;
      if(user.getRole().equals("ADMIN")) {
      System.out.println(user.getName() + "님은 관리자 화면으로 바로 이동........");
      }
      }
      }
    • 핵심 비지니스 로직에서 반환된 객체를 returnObj로 받아온다
  4. After Throwing

    • 핵심 비지니스 로직 실행헌 후 예외가 발생하면 예외객체를 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
       @AfterThrowing(pointcut = "execution(* com.rubypaper.biz..*Impl.*(..))", throwing = "exceptionObj")
      public void exceptionLog(JoinPoint jp, Exception exceptionObj) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름

      System.out.println("[ 예외처리 ]"+ method +" 메소드 수행 중 예외 발생");

      if(exceptionObj instanceof IllegalArgumentException){
      System.out.println("0 번 게시 글을 등록할 수 없습니다");
      } else if (exceptionObj instanceof ArithmeticException) {
      System.out.println("0 으로 숫자를 나눌 수 없습니다");
      } else if (exceptionObj instanceof SQLException) {
      System.out.println("SQL 구문에 오류가 있습니다");
      } else {
      System.out.println("문제 발생 !! 시스템을 잠시 종료합니다");
      }
      }
      • 예외객체를 exceptionObj로 받아온다
  5. Around

    • 위에서 설명

Advice에서 Pointcut에 해당되는 메소드에 대한 정보를 얻기 위해 JoinPoint라는 객체를 받아올 수 있다. Around만 예외적으로 ProceedJoinPoint라는 객체를 통해서 받아온다

Comment and share

  • page 1 of 1

Yechan Kim

author.bio


author.job