AOP 적용 대상 : 선언적 트랜잭션
6.1 트랜잭션 코드의 분리
6.1.1 메소드 분리
비즈니스 로직과 트랜잭션 경계설정은 완벽하게 독립적
트랙잭션의 시작과 종료 사이에 비즈니스 로직이 실행되는 조건만 지키면 됨
→ 비즈니스 로직을 담당하는 코드를 메소드로 추출해서 독립
6.1.2 DI를 사용한 클래스의 분리
비즈니스 로직 분리 but 트랜잭션 담당 코드가 여전히 서비스 안에 있음
DI 의 기본 아이디어 : 실제 사용할 오브젝트의 클래스 정체는 감추고 인터페이스 통해 간접 접근
→ 이로 인해 구현 클래스를 외부에서 자유롭게 변경 가능
인터페이스 이용해 구현 클래스 클라이언트에 노출하지 않고 런타임 시에 DI를 통해 적용하는 방법을 쓰는 이유 → 구현 클래스를 상황에 따라 바꿔가면서 사용하기 위해. (테스트 때는 테스트 구현, 정식 운영 시에는 정규 구현 클래스 등) 한 번에 한 가지 클래스를 선택해서 적용
한 번에 두 개의 인터페이스 구현 클래스를 동시에 이용한다면 ?
ex) 동시에 비즈니스 로직, 트랜잭션 적용
트랜잭션 담당 클래스는 비즈니스 로직 안 담고 있음 → 인터페이스 구현 클래스에 실제적인 로직 처리 작업은 위임하는 것. 그리고 그 위임을 위한 호출 작업 이전과 이후에 적절한 트랜잭션 경계 설정
트랜잭션 담당 클래스는 비즈니스 로직 담당하는 인터페이스 구현한 클래스를 DI 받는다.
→ DI 받은 오브젝트에 모든 기능 위임
트랜잭션 경계설정 코드 분리의 장점
- 비지니스 로직을 담당하고 있는 구현클래스 코드를 작성할 때 트랜잭션과 같은 기술적인 내용에 신경쓰지 않아도 됨. DI 통해 트랜잭션 담당 오브젝트가 먼저 실행되도록 만들기만 하면 됨
- 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있음
6.2 고립된 단위의 테스트
작은 단위 테스트가 좋은 이유
- 테스트 실패 시 원인을 찾기 쉬움
- 테스트 의도가 분명해짐
- 만들기 쉬움
6.2.1 복잡한 의존관계 속의 테스트
ex) UserService
가 테스트 대상 일 때, UserDao
, TracnsactionManager
, MailSender
라는 세 가지 의존관계 가지고 있음
6.2.2 테스트 대상 오브젝트 고립시키기
목 오브젝트 등의 테스트 대역 사용
ex) UserDao
목 오브젝트
- 목 오브젝트 사용하여 DB에 일일이 저장하는 대신,미리 준비된 테스트용 리스트를 메모리에 가지고 있다가 돌려주기만 하면 됨
- 테스트 작성 시 준비해준
MockUserDao
오브젝트를 사용하도록 수동 DI
인터페이스 구현하느라 테스트에 사용하지 않는 메소드까지 구현해 줘야 할 때
UnsupportedOperationException
던져서 지원하지 않는 기능이라는 예외 발생시킴 (실수로 사용될 위험 방지)
6.2.3 단위 테스트와 통합 테스트
단위 테스트
- 테스트 대상 클래스를 목 오브젝트 등의 테스트 대역을 이용해 의존 오브젝트나 외부의 리소스를 사용하지 않도록 고립시켜서 테스트 하는 것
통합 테스트
- 두 개 이상의, 성격이나 계층이 다른 오브젝트가 연동하도록 만들어 테스트하거나 또는 외부의 DB, 파일, 서비스 등의 리소스가 참여하는 테스트
- 두 개 이상의 단위가 결합해서 테스트가 수행되는 것
테스트 시 고려사항
- 항상 단위 테스트 먼저 고려
- 외부와의 의존관계 모두 차단하고 필요에 따라 스텁이나 목 오브젝트 등의 테스트 대역을 이용
- 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만든다.
- 단위 테스트로 만들기 어려운 코드 : DAO
- DAO는 그 자체로 로직을 담기보다는 DB를 통해 로직을 수행하는 인터페이스 같은 역할이라 고립된 테스트로 작성하기 힘듦
- 따라서 DB까지 연동하는 테스트로 만드는 게 효과적
- DAO 테스트는 외부 리소스 사용하기 때문에 통합 테스트. DAO 테스트 통과 후에는 DAO 이용하는 코드는 DAO 역할을 스텁이나 목 오브젝트로 대체해서 테스트 할 수 있음
- 단위 테스트 만들기가 너무 복잡한 코드는 처음부터 통합 테스트 고려, 다만 가능한 많은 부분을 단위 테스트 하기
- 스프링 테스트 컨텍스트 프레임워크를 이용하는 테스트는 통합 테스트. 스프링의 설정 자체도 테스트 대상
6.2.4 목프레임워크
- Mockito 프레임워크 사용하여 특정 인터페이스를 구현한 테스트 용 목 오브젝트 생성 가능
사용 단계
- 인터페이스 이용해 목 오브젝트 만듦
- 목 오브젝트가 리턴할 값 있으면 이를 지정함. 메소드가 호출되면 예외를 강제로 던지게 만들 수도 있음
- 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만듦
- 테스트 대상 오브젝트 사용 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증함
6.3 다이내믹 프록시와 팩토리 빈
6.3.1 프록시와 프록시 패턴, 데코레이터 패턴
Ex)
부가 기능(트랙잭션 기능)
핵심 기능(비즈니스 로직)
- 부가기능 외의 나머지 기능은 핵심 기능을 가진 클래스로 위임해야 함
→ 핵심 기능은 부가기능을 가진 클래스의 존재를 모름
→ 따라서 부가기능이 핵심기능을 사용하는 구조
- 위처럼 구성했을 시, 클라이언트가 핵심기능 클래스를 직접 사용하면 부가기능 적용 X
→ 부가기능은 마치 자신이 핵심 기능을 가진 클래스인 것처럼 꾸며서 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 함
→ 클라이언트는 인터페이스 통해서만 핵심 기능 사용, 부가기능 자신도 같은 인터페이스 구현 후 핵심 기능과 클라이언트 사이에 끼어야 함
프록시
- 자신이(부가기능) 클라이언트가 사용하려고 하는 실제 대상(핵심 기능) 인 것처럼 위장해서 클라이언트의 요청을 받아주는 것 → 대리인 대리자와 같은 역할
타깃 or 실체
프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트
→ 클라이언트는 프록시를 통해 타깃을 사용
프록시의 특징
- 타깃과 같은 인터페이스 구현
- 프록시가 타깃을 제어할 수 있는 위치에 존재
사용목적에 따른 구분
- 클라이언트가 타깃에 접근하는 방법 제어하기 위해
- 타깃에 부가적인 기능을 부여해주기 위해
→ 대리 오브젝트라는 개념의 프록시 두고 사용한다는 점 동일하지만, 목적에 따라서 디자인 패턴에서는 다른 패턴으로 구분함
데코레이터 패턴
- 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
- 즉, 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않음
- 내용물은 동일하지만 부가적인 효과를 줄 수 있다는 의미에서 <데코레이터>
- 프록시가 한 개로 제한 X
- 프록시가 직접 타깃 사용하도록 고정시킬 필요 X
→ 같은 인터페이스 구현한 타겟과 여러 개의 프록시 사용 가능, 순서를 정해서 단계적으로 위임
Ex)
클라이언트 → 라인넘버 데코레이터 → 컬러 데코레이터 → 페이징 데코레이터 → (타깃) 소스코드 출력 기능
런타임 시 조합함
각 데코레이터는 자신이 최종 타깃으로 위임하는지, 다음 단계의 데코레이터 프록시로 위임하는 지 알지 못함
→ 다음 위임 대상 인터페이스를 선언하고 생성자,수정자 메소드 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만듦
자바 IO 패키지의 대표적인 데코레이터 패턴
InputStream
// InputStream 인터페이스 구현한 타깃인 FileInputStream에
// 버퍼 읽기 기능을 제공해주는 BufferdInputStream 데코레이터 적용
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
OutputStream
스프링 DI 이용하여 데코레이터 정의 및 런타임 시 다이내믹 구성
- 데코레이터 빈의 프로퍼티로 같은 이너페이스를 구현한 다른 데코레이터 또는 타깃 빈 설정
Ex)
UserServiceTx
와 userService
: 데코레이터 패턴
필요하다면, 언제든지 트랜잭션 외에도 다른 기능 부여하는 데코레이터 만들어서 UserServiceTx
와 UserServiceImpl
사이에 추가 가능
언제 유용한가 ?
타깃의 코드 손대지 않고, 클라이언트가 호출하는 방법도 변경 하지 않은 채로 새로운 기능을 추가할 때
프록시 패턴
일반적인 프록시 용어
- 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트 총칭
디자인 패턴에서의 프록시 패턴
- 프록시를 사용하는 방법 중, 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우
프록시 패턴의 프록시
- 타깃의 기능 확장하거나 추가 X
- 클라이언트가 타깃에 접근하는 방식 변경해줌
언제 사용하는가?
- 타깃 오브젝트 생성 복잡하거나 당장 필요하지 않으면 꼭 필요한 시점까지는 생성하지 않는 게 좋음 → 그런데 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있음 → 클라이언트가 타깃에 대한 레퍼런스를 넘겨야 할 때, 실제 타깃 오브젝트 만드는 대신 프록시 만들어서 넘겨줌
- 프록시의 메소드를 통해 타깃 사용하려고 시도할 때, 프록시가 실제 타깃 오브젝트 생성 해서 요청을 위임해줌
- 레퍼런스 갖고 있지만 끝까지 사용하지 않거나 많은 작업이 진행된 후에 사용되는 경우, 프록시를 통해 실제 타깃 오브젝트 생성을 최대한 늦출 수 있음
- 원격 오브젝트 이용하는 경우에도 프록시 사용하면 편리함
- 다른 서버에 존재하는 오브젝트 사용하는 경우, 원격 오브젝트에 대한 프록시 만들어두고 클라이언트는 마치 로컬에 존재하는 오브젝트 쓰는 것처럼 프록시 사용하게 할 수 있음
- 프록시는 클라 요청 받으면 네트워크 통해 원격 오브젝트를 실행하고 결과 받아서 클라에게 반환함
- 클라이언트로 하여금 원격 오브젝트에 대한 접근 방법 제공해주는 프록시 패턴의 예
- 특별한 상황에서 타깃에 대한 접근 권한 제어하기 위해 사용
- 수정 가능한 오브젝트가 있을 때, 특정 레이어로 넘어가서는 읽기전용으로만 동작하게 강제해야 할 때 → 오브젝트의 프록시 만들어서 사용
- 프록시의 특정 메소드 사용하려고 할 지 접근 불가능 예외 발생시키면 됨
- Collections의
unmodifableCollection()
을 통해 만들어지는 오브젝트가 전형적인 접근권한 제어용 프록시 - 파라미터로 전달된 Collection 오브젝트의 프록시 만들어서
add()
,remove()
와 같이 정보를 수정하는 메소드 호출할 경우UnsupportedOperationException
예외 발생
- Collections의
프록시 패턴이란?
→ 타깃의 기능 자체에는 관여하지 않으면서 접근하는 방법을 제어해주는 프록시 이용하는 것
프록시와 데코레이터 구조적으로 유사 BUT 프록시는
- 생성을 지연하는 프록시라면 구체적인 생성 방법을 알아야 하기 때문에 코드에서 자신이 만들거나 접근할 타깃 클래스 정보 알고 있는 경우가 많음
- 프록시 패턴도 인터페이스 통해 위임하도록 만들 수 있음
- ex) 클라이언트 → (프록시 패턴) 접근제어 프록시 → 컬러 데코레이터 → 페이징 데코레이터 → (타깃) 소스코드 출력 기능
즉, 프록시란
→ 타깃과 동일한 인터페이스 구현, 클라이언트와 타깃 사이 존재, 기능의 부가 또는 접근 제어를 담당하는 오브젝트
→ 기능 부가인지, 접근제어인지에 따른 사용목적 구분으로 패턴 구분
6.3.2 다이내믹 프록시
프록시는 기존 코드에 영향 X 면서 타깃의 기능 확장, 접근 방법 제어할 수 있음.
→ 하지만 너무 번거로움
프록시 구성
- 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
- 지정된 요청에 대해서는 부가 기능을 수행한다.
프록시 작성의 문제점
- 타깃의 인터페이스 구현하고 위임하는 코드 작성이 번거로움. 부가기능 필요없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 하기 때문에 → 타깃 인터페이스 메소드가 추가,변경 시 함께 수정해줘야 함
- 부가 기능 코드가 중복될 가능성 많음. 트랜잭션의 경우 DB를 사용하는 대부분의 로지게 적용 → 트랜잭션 기능 제공하는 유사 코드가 여러 메소드에 중복돼서 나타날 것
→ JDK 다이내믹 프록시 통해 해결
리플렉션
다이내믹 프록시는 리플렉션 기능 이용해서 프록시 만듦
리플렉션 : 자바의 코드 자체를 추상화해서 접근하도록 만든 것
클라이언트가 프록시 팩토리를 통해 프록시 요청
→ 프록시 팩토리가 프록시 생성 (다이내믹 프록시)
- 프록시 팩토리에게 인터페이스 정보만 제공해주면 해당 인터에피스 구현한 클래스의 오브젝트를 자동으로 생성함
- 부가기능 제공 코드는 직접 작성 →
InvocationHandler
인터페이스의invoke()
메소드 통해 리플렉션의 Method 인터페이스 파라미터로 받음
6.3.4 다이내믹 프록시를 위한 팩토리 빈
스프링 빈은 클래스 이름, 프로퍼티로 정의됨
→ 다이내믹 프록시는 클래스 자체를 내부적으로 다이내믹하게 새로 정의 해서 사용함. 따라서 사전에 프록시 오브젝트의 클래스 정보를 미리 알아내서 스프링 빈에 정의할 방법이 없음
팩토리 빈
스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈
FactoryBean
인터페이스 구현해서 만듦
public interface FactoryBean<T> {
T getObject() throws Exception; //빈 오브젝트를 생성해서 돌려준다.
Class<? extends T> getObjectType(); //생성되는 오브젝트 타입을 알려준다.
boolean isSingleton(); //getObject가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지 알려준다.
}
→ 인터페이스 구현한 클래스 스프링 빈으로 등록하면 팩토리 빈으로 동작
6.3.5 프록시 팩토리 빈 방식의 장점과 한계
프록시 팩토리 빈의 재사용
부가 기능을 가진 프록시를 생성하는 팩토리 빈 만들면 타깃의 타입에 상관없이 재사용이 가능
프록시 팩토리 빈의 장점
- 다이내믹 프록시를 이용하여 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움 제거
- 하나의 핸들러 메소드 구현하는 것만으로도 수많은 메소드에 부가기능 부여 가능함으로 부가기능 코드의 중복 문제 해결
- 다이내믹 프록시에 팩토리 빈을 이용한 DI까지 더해주면 번거로운 다이내믹 프록시 생성 코드도 제거
- DI 설정을 통해 다양한 타깃 오브젝트에 적용 가능
프록시 팩토리 빈의 한계
- 타깃에 부가기능 제공하는 것은 메소드 단위 → 하나의 클래스 안에 여러개의 메소드 적용은 가능, but 여러 개의 클래스에 공통적인 부가기능을 제공하는 일은 불가능
- 하나의 타깃에 여러 개의 부가기능을 적용하려 할 때 문제 발생
- 트랜잭션뿐만 아니라 보안 기능, 기능 검사 등의 부가기능 담은 프록시를 추가하려 한다면 설정 코드가 끝없이 길어짐
TransactionHandler
오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐- 타깃 오브젝트가 달라지면 새로운
TracsactionHandler
오브젝트 만들어야 하면서 중복이 됨
- 타깃 오브젝트가 달라지면 새로운
6.4 스프링의 프록시 팩토리 빈
ProxyFactoryBean
- 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
- TxProxyFactoryBean과 달리, 순수하게 프록시 생성 작업만 담당, 프록시를 제공해줄 부가 기능은 별도의 빈에 둘 수 있음
- 부가기능은
MethodInterceptor
인터페이스 구현해서 만듦InvocationHandler
의invoke()
메소드가 타깃 오브젝트에 대한 정보 제공하지 않아서InvocationHandler
구현 클래스가 직접 알아야 했던 것과 달리,MethodInterceptor
invoke()
메소드는ProxyFactoryBean
으로부터 타깃 오브젝트에 대한 정보까지 함께 제공받음- 따라서 타깃 오브젝트에 상관없이 독립적으로 만들어서 타깃이 다른 여러 프록시에서 함께 사용 가능, 타깃에 대한 정보 알고 있지 않음으로 싱글톤 빈으로 등록 가능
어드바이스 : 타깃이 필요 없는 순수한 부가기능
MethodInvocation
처럼 타깃 오브젝트에 부가기능 제공하는 오브젝트
ProxyFactoryBean
적용 코드와 기존 JDK 다이내믹 프록시 사용 코드의 차이점
InvocationHandler
구현 했을 때와 달리MethodInterceptor
구현 클래스에는 타깃 오브젝트 등장 XMethodInterceptor
로는 메소드 정보와 함께 타깃 오브젝트가 담긴MethodInvocation
오브젝트가 전달됨MethodInvocation
타깃 오브젝트 메소드 실행할 수 있는 기능 있음 →MethodInterceptor
는 부가기능 제공에만 집중할 수 있음
MethodInvocation
은 일종의 콜백 오브젝트
proceed()
메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능MethodInvocation
구현 클래스는 일종의 공유 가능 템플릿처럼 동작
→ 스프링이 제공하는 프록시 추상화 기능인 ProxyFactoryBean
을 사용하는 코드의 가장 큰 차이점이자 장점
→ ProxyFactoryBean
은 작은 단위의 템플릿/콜백 구조를 응용하여 적용하였기에 템플릿 역할을 하는 MethodInvocation
을 싱글톤으로 두고 공유할 수 있다.
addAdvice()
메소드
ProxyFactoryBean
에MethodInterceptor
설정할 때 수정자 메소드 대신 사용ProxyFactoryBean
에 여러 개의MethodInterceptor
추가 가능
→ ProxyFactoryBean
하나만으로 여러 개 부가 기능 제공해주는 프록시 만들 수 있음
→ 새로운 부가 기능 추가시마다 프록시와 프록시 팩토리빈도 추가해야 하는 문제 해결
포인트컷 : 부가기능 적용 대상 메소드 선정 방법
- 메소드 선정 알고리즘을 담은 오브젝트
어드바이스와 포인트컷
- 프록시에 DI로 주입 돼서 사용
- 여러 프록시에서 공유가 가능하도록 만들어짐 → 스프링 싱글톤 빈으로 등록 가능
- 프록시는 포인트컷으로부터 부가기능 적용할 대상 메소드인지 확인 받으면,
MethodInteceptor
타입의 어드바이스 호출 → 어드바이스는 직접 타깃 호출 X, 자신이 공유돼야 하므로 타깃 정보라는 상태도 아님 ( 타깃에 직접 의존하지 않는 일종의 템플릿 구조 설계)
'토비의스프링vol.1' 카테고리의 다른 글
[토비의 스프링vol.1 8장] 스프링이란 무엇인가? (0) | 2023.04.03 |
---|---|
[토비의 스프링vol.1 6장] AOP - 2 (0) | 2023.04.03 |
[토비의 스프링vol.1 5장] 서비스 추상화 (0) | 2023.04.03 |
[토비의 스프링vol.1 4장] 예외처리 (0) | 2023.04.03 |
[토비의 스프링vol.1 3장] 템플릿 (1) | 2022.10.31 |