[Spring 3.1] Aspect Oriented Programming
Service 계층에서 비즈니스 로직과 트랜잭션 설정 관련 로직을 분리했지만, 여전히 트랜잭션 경계 설정을 위해 비즈니스 로직의 앞뒤로 관련된 코드가 붙어 있는 상황이다.
두 부분을 분리할 수는 없을까? Service 계층에는 비즈니스 로직만 포함하면 좋을 텐데..
Service 계층을 인터페이스로 설계하고 두 가지 클래스를 구현 클래스로 설정해보자.
하나는 비즈니스 관련 로직만을 다루고, 나머지 하나는 트랜잭션 관련 설정 로직만을 다룬다.
ServiceTx 구현체에서는 ServiceImpl에게 작업을 위임한다.
트랜잭션 관련 설정은 알아서 처리하고, 비즈니스 관련 로직을 처리해야 하는 경우 ServiceImpl을 DI 받아서 해당 구현체가 처리하도록 설계한다.
작업을 거듭할수록 Service 계층이 점점 무거워지고 있다.
운영 환경에서는 문제되지 않지만, 계층의 의존관계가 복잡해질수록 작은 단위로 쪼개서 테스트하는 작업이 힘들어진다.
실제로 이메일을 발송하거나, 데이터베이스에 접근하는 작업들은 오버헤드가 많이 발생하니 테스트 전용 목 오브젝트를 생성해 외부 환경에 상관없이 테스트를 진행하자.
Mock 클래스를 생성해 의존관계에 상관 없이 단위 테스트를 진행할 수 있도록 설계하고, 사용자 로직 검증에 필요하지 않은 의존 오브젝트들을 모두 제거해 가볍게 테스트 할 수 있도록 하자. (Mockito 프레임워크를 사용하면 편리하다)
비즈니스 로직과 트랜잭션 설정 관련 로직이 있다면, 비즈니스 로직은 핵심 기능이고 트랜잭션 설정 관련 로직은 부가 기능이라고 할 수 있다.
두 가지를 분리할 때 부가 기능이 핵심 기능을 호출하는 방식을 사용했고, 여기서 부가 기능을 프록시라고 부른다.
프록시는 클라이언트가 핵심 기능에 접근하는 방법을 제어하거나 핵심 기능에 부가적인 기능을 부여하기 위해서 사용된다.
데코레이터 패턴은 핵심 기능에 부가적인 기능을 다이나믹하게 부여하기 위해 프록시를 사용하는 패턴을 의미한다.
실행하기 전에는 어떤 관계를 가지는지 알 수 없지만, DI를 사용해 다이나믹하게 부가적인 기능을 구현한다.
프록시 패턴은 핵심 기능에 접근하는 방법을 제어할 때 사용된다.
클라이언트에게 타겟에 대한 레퍼런스를 넘길 때 타겟 오브젝트의 생성을 늦추기 위해 우선 프록시를 넘기고, 프록시의 메서드를 통해 타겟을 사용하려 할 때 타겟 오브젝트를 생성하고 요청을 위임하는 방식으로 동작한다. (타겟은 부가기능을 부여할 대상을 의미한다)
타겟 오브젝트의 생성을 늦추는 작업 외에도, 다른 서버에 존재하는 오브젝트를 사용해야 하는 경우 원격 오브젝트에 대한 프록시를 만들어두고 클라이언트는 프록시를 사용하고 프록시는 원격의 오브젝트를 실행하는 방식으로 클라이언트가 원격 오브젝트에 접근하는 방식을 조작할 때도 사용된다.
다이나믹 프록시는 프록시 팩토리가 만드는 런타임 시 만들어지는 오브젝트이고, 타겟의 인터페이스와 같은 타입으로 만들어진다. (자바 리플랙션을 사용한다)
프록시 팩토리에게 인터페이스 정보를 제공해 인터페이스의 구현체를 만들고, 부가기능은 InvocationHandler 구현체에 담는다.
전반적인 흐름은 위와 같다.
다이나믹 프록시로 구현체를 만들고, 구현체의 메서드를 InvocationHandler로 한 번에 관리한다.
이렇게 만들어지는 다이나믹 프록시 오브젝트는 팩토리 빈을 사용해 스프링 빈으로 등록한 후 사용한다. (FactoryBean<?> 을 구현한다)
팩토리 빈은 타겟 오브젝트를 전달하기 위해 다이나믹 프록시와 연관된 타겟 오브젝트에 대한 레퍼런스를 프로퍼티를 통해 DI 받아야 한다.
프록시 팩토리 빈을 만들어두면 추후 동일한 작업을 진행해야 하는 경우 재사용할 수 있어 편리하지만, TransactionHandler 오브젝트가 팩토리 빈의 개수만큼 늘어나고, 여러 부가기능을 설정하기 힘들다는 단점이 있다.
스프링이 제공하는 ProxyFactoryBean 클래스를 사용하면 문제를 해결할 수 있다.
해당 클래스는 프록시를 생성하는 작업만 담당하고 부가기능은 MethodInterceptor 인터페이스를 구현해서 제공한다.
InvocationHandler와 비슷한 역할을 수행하지만, MethodInterceptor의 inovke 메서드는 ProxyFactoryBean에게 타겟 오브젝트에 관련된 정보도 함께 제공받아 타겟 오브젝트에 독립적으로 만들어질 수 있다. (MethodInterceptor는 타겟 정보를 가지지 않고, 싱글톤 빈으로 등록할 수 있다)
ProxyFactoryBean 클래스의 addAdvice 메서드로 MethodInterceptor가 구현하고 있는 부가기능을 몇 개가 됐든 추가할 수 있다.
여기서 Advice는 타겟 오브젝트에 적용하는 부가기능을 담은 오브젝트를 Advice라고 부른다.
ProxyFactoryBean 클래스는 인터페이스 자동검출 기능이 있어 타겟 오브젝트가 구현하는 인터페이스 정보를 알아내고 해당 오브젝트를 구현하는 프록시를 알아서 만들어준다.
트랜잭션 적용 대상 메서드 이름 패턴을 넣어주는 작업도 Pointcut을 사용하면 간편하게 구현할 수 있다.
전반적인 흐름은 위와 같다.
포인트컷은 어떤 메서드에 어드바이스(부가기능) 을 구현할 지 결정하는 알고리즘을 포함하는 오브젝트이다.
프록시, 포인트컷, 어드바이스는 서로 분리되어 있고 DI를 통해 OCP원칙을 지키고 있다.
포인트컷과 어드바이스 기능을 합쳐서 제공하는 요소를 어드바이저 라고 부른다.
근본은 코드의 분리와 중복의 제거이다.
트랜잭션 경계 설정 부분을 비즈니스 로직과 분리하기 위해서... 어드바이저가 도입됐다.
변하는 부분인 부가기능 코드는 별도로 만들어 DI 하고, 변하지 않는 타겟으로의 위임과 부가기능의 적용 여부 판단은 프록시 기술이 처리하도록 설계했다.
그리고 그 과정에서 DI, 다양한 디자인 패턴, 서비스 추상화 등이 모두 적용됐고, 덕분에 OCP 원칙을 지키면서 개발을 진행할 수 있게 됐다.
이렇게 트랜잭션 설정 관련 부분을 어드바이저로 분리했으니 앞으로 어떤 서비스 계층이 도입되든 어드바이저로 비즈니스 로직을 분리할 수 있다.
빈 오브젝트로 등록되는 요소는 ProxyFactoryBean에서 생성되는 프록시이다.
빈 후처리기를 사용하면 프록시가 자동으로 빈으로 등록되게 할 수 있다.
빈 후처리기가 등록되어 있으면 빈 오브젝트가 만들어질 때 마다 후처리기에게 빈을 보낸다.
후처리기는 어드바이저를 사용해 해당 빈이 프록시 적용 대상인지 확인하고, 대상이라면 프록시를 만든다.
만들어진 프록시 오브젝트를 스프링 컨테이너에게 돌려줘 프록시가 빈으로 된다. (포인트컷은 프록시를 적용한 클래스인지도 판단한다)
객체지향 설계 방식으로는 독립적인 불가능한 부가기능을 DI, 프록시, 포인트컷 등 여러 방법을 도입해서 깔끔하게 분리했다.
이 부분을 객체지향에서 사용하는 용어와 분리해서 사용하기 위해 부가기능 모듈을 Aspect로 부른다.
Aspect는 부가기능이 정의된 어드바이스와 어드바이스를 적용할지 결정하는 포인트컷을 함께 가진다.
부가기능을 핵심기능에 도입하게 되면서 깔끔하게 설계한 핵심기능을 난잡하게 보일 수 있다는 문제점을 Aspect를 도입해서 해결했다.
이렇게 부가기능을 분리해서 Aspect 모듈로 설계하고 개발하는 방법론을 Aspect Oriented Programming라고 부른다.
지금은 프록시를 사용해서 AOP를 구현했지만, 클래스가 JVM에 로딩되는 시점에 바이트코드를 조작하는 방식으로 AOP를 구현하는 방법도 있다. (AOP 프레임워크인 AspectJ 가 사용하는 방법이다)
트랜잭션은 최소의 작업 단위이다.
트랜잭션을 시작할 때 고려해야 하는 부분이 여럿 있다.
1. 트랜잭션 전파
트랜잭션을 새로 시작할 때 이미 시작된 트랜잭션이 있다면 어떻게 해야 할까?
기존 트랜잭션에 참여할 수 있고, 별도의 트랜잭션으로 설정할 수 있다.
PROPAGATION_REQUIRED : 트랜잭션이 진행 중이라면 참여하고, 아니라면 새로 시작한다. 가장 많이 사용된다.
PROPAGATION_REQUIRES_NEW : 항상 새 트랜잭션을 시작한다.
PROPAGATION_NOT_SUPPORTED : 트랜잭션 없이 시작한다. AOP로 트랜잭션을 설정할 때 예외 케이스로 사용한다,
2. 격리수준
적절하게 격리수준을 조정해 최대한 많은 트랜잭션을 동시에 진행시켜야 한다.
데이터베이스에 설정된 격리수준을 사용해도 되고, 트랜잭션 단위로 조정해도 된다.
동시성 처리의 기준이 된다.
3. 제한시간
말 그대로이다.
4. 읽기전용
데이터 조작을 막을 수 있다.
데이터 액세스 기술에 따라서 성능이 향상될 수도 있다.
TransactionInterceptor 클래스를 사용해서 위의 네 가지 요소를 설정하고 사용한다.
쓰기 작업은 물론, 읽기 전용 메서드에도 트랜잭션을 걸어 네 가지 요소를 적용하자.
트랜잭션을 효과적으로 관리하기 위해 Service 계층에서만 트랜잭션을 시작하고 데이터베이스에 접근할 수 있도록 설계하는 편이 합리적이다.
위와 같이 트랜잭션을 적용할 클래스와 메서드를 포인트컷과 트랜잭션 속성을 사용해 지정한 후 트랜잭션 부가기능을 적용하는 방법도 있고, 트랜잭션 애너테이션을 사용해서 메서드별로 부가기능을 적용하는 방법도 있다.
@Target : 애너테이션을 사용할 대상을 지정한다. 메서드와 타입에 사용할 수 있다.
@Retention : 애너테이션 정보가 유지되는 기간을 설정한다. 런타임에도 리플렉션을 통해 정보를 얻는다.
@Inherited : 상속을 통해서 애너테이션 정보를 얻을 수 있다.
속성의 모든 항목들에 대해서는 디폴트 값이 설정되어있어 생략해도 괜찮다.
애너테이션에서 트랜잭션 격리 수준을 설정해 데이터베이스 설정에 접근할 수 있고, 동시성 제어를 수행할 수 있다. (기본값으로 데이터베이스의 설정을 따라간다)
@Transactional 애너테이션은 AnnotationTransactionAttrubuteSource를 통해 트랜잭션 속성을 가져오고, 역시 프록시가 사용된다.
적용 순서는 아래와 같다.
1. 타겟 메서드에 @Transactional 애너테이션이 있다 -> 바로 적용한다.
2. 타겟 클래스에 @Transactional 애너테이션이 있다. -> 바로 적용한다.
3. 선언 메서드에 @Transactional 애너테이션이 있다. -> 바로 적용한다.
4. 선언 클래스에 @Transactional 애너테이션이 있다. -> 바로 적용한다.
@Transactional 애너테이션은 간단하고 메서드 단위로 설정할 수 있어 트랜잭션의 단위를 작게 잡을 수 있다.
간단한 트랜잭션이 필요한 경우 애너테이션을 사용하고, 복잡한 트랜잭션 설정이 필요하거나 트랜잭션 설정을 공통으로 지정해야 하는 경우 별도의 클래스에 어드바이저를 정의하는 방식을 사용해 중복을 줄이고 관리를 간편하게 가져가자.
즉, 상황에 맞게 두 가지 방법을 적절하게 사용하면 된다.
'Spring > Spring 3.1' 카테고리의 다른 글
[Spring 3.1] IoC 컨테이너와 스프링의 동작 원리 (0) | 2023.05.10 |
---|---|
[Spring 3.1] Annotation (0) | 2023.05.04 |
[Spring 3.1] 트랜잭션과 서비스 추상화 (0) | 2023.04.23 |
[Spring 3.1] 스프링과 예외처리 (0) | 2023.04.20 |
[Spring 3.1] 템플릿과 콜백 (0) | 2023.04.19 |
댓글
이 글 공유하기
다른 글
-
[Spring 3.1] IoC 컨테이너와 스프링의 동작 원리
[Spring 3.1] IoC 컨테이너와 스프링의 동작 원리
2023.05.10 -
[Spring 3.1] Annotation
[Spring 3.1] Annotation
2023.05.04 -
[Spring 3.1] 트랜잭션과 서비스 추상화
[Spring 3.1] 트랜잭션과 서비스 추상화
2023.04.23 -
[Spring 3.1] 스프링과 예외처리
[Spring 3.1] 스프링과 예외처리
2023.04.20