토비의스프링vol.1

[토비의 스프링vol.1 6장] AOP - 1

ohyujeong 2023. 4. 3. 09:57

AOP-1 정리 노션 페이지

AOP 적용 대상 : 선언적 트랜잭션

6.1 트랜잭션 코드의 분리

6.1.1 메소드 분리

비즈니스 로직과 트랜잭션 경계설정은 완벽하게 독립적

트랙잭션의 시작과 종료 사이에 비즈니스 로직이 실행되는 조건만 지키면 됨

→ 비즈니스 로직을 담당하는 코드를 메소드로 추출해서 독립

6.1.2 DI를 사용한 클래스의 분리

비즈니스 로직 분리 but 트랜잭션 담당 코드가 여전히 서비스 안에 있음

DI 의 기본 아이디어 : 실제 사용할 오브젝트의 클래스 정체는 감추고 인터페이스 통해 간접 접근

→ 이로 인해 구현 클래스를 외부에서 자유롭게 변경 가능

인터페이스 이용해 구현 클래스 클라이언트에 노출하지 않고 런타임 시에 DI를 통해 적용하는 방법을 쓰는 이유 → 구현 클래스를 상황에 따라 바꿔가면서 사용하기 위해. (테스트 때는 테스트 구현, 정식 운영 시에는 정규 구현 클래스 등) 한 번에 한 가지 클래스를 선택해서 적용

한 번에 두 개의 인터페이스 구현 클래스를 동시에 이용한다면 ?

ex) 동시에 비즈니스 로직, 트랜잭션 적용

트랜잭션 담당 클래스는 비즈니스 로직 안 담고 있음 → 인터페이스 구현 클래스에 실제적인 로직 처리 작업은 위임하는 것. 그리고 그 위임을 위한 호출 작업 이전과 이후에 적절한 트랜잭션 경계 설정

트랜잭션 담당 클래스는 비즈니스 로직 담당하는 인터페이스 구현한 클래스를 DI 받는다.

→ DI 받은 오브젝트에 모든 기능 위임

트랜잭션 경계설정 코드 분리의 장점

  1. 비지니스 로직을 담당하고 있는 구현클래스 코드를 작성할 때 트랜잭션과 같은 기술적인 내용에 신경쓰지 않아도 됨. DI 통해 트랜잭션 담당 오브젝트가 먼저 실행되도록 만들기만 하면 됨
  2. 비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있음

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 프레임워크 사용하여 특정 인터페이스를 구현한 테스트 용 목 오브젝트 생성 가능

사용 단계

  1. 인터페이스 이용해 목 오브젝트 만듦
  2. 목 오브젝트가 리턴할 값 있으면 이를 지정함. 메소드가 호출되면 예외를 강제로 던지게 만들 수도 있음
  3. 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만듦
  4. 테스트 대상 오브젝트 사용 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증함

6.3 다이내믹 프록시와 팩토리 빈

6.3.1 프록시와 프록시 패턴, 데코레이터 패턴

Ex)

부가 기능(트랙잭션 기능)

핵심 기능(비즈니스 로직)

  • 부가기능 외의 나머지 기능은 핵심 기능을 가진 클래스로 위임해야 함

→ 핵심 기능은 부가기능을 가진 클래스의 존재를 모름

→ 따라서 부가기능이 핵심기능을 사용하는 구조

  • 위처럼 구성했을 시, 클라이언트가 핵심기능 클래스를 직접 사용하면 부가기능 적용 X

→ 부가기능은 마치 자신이 핵심 기능을 가진 클래스인 것처럼 꾸며서 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 함

→ 클라이언트는 인터페이스 통해서만 핵심 기능 사용, 부가기능 자신도 같은 인터페이스 구현 후 핵심 기능과 클라이언트 사이에 끼어야 함

프록시

  • 자신이(부가기능) 클라이언트가 사용하려고 하는 실제 대상(핵심 기능) 인 것처럼 위장해서 클라이언트의 요청을 받아주는 것 → 대리인 대리자와 같은 역할

타깃 or 실체

프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 오브젝트

→ 클라이언트는 프록시를 통해 타깃을 사용

프록시의 특징

  • 타깃과 같은 인터페이스 구현
  • 프록시가 타깃을 제어할 수 있는 위치에 존재

사용목적에 따른 구분

  1. 클라이언트가 타깃에 접근하는 방법 제어하기 위해
  2. 타깃에 부가적인 기능을 부여해주기 위해

→ 대리 오브젝트라는 개념의 프록시 두고 사용한다는 점 동일하지만, 목적에 따라서 디자인 패턴에서는 다른 패턴으로 구분함

데코레이터 패턴

  • 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴
  • 즉, 코드상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 사용되는지 정해져 있지 않음
  • 내용물은 동일하지만 부가적인 효과를 줄 수 있다는 의미에서 <데코레이터>
  • 프록시가 한 개로 제한 X
  • 프록시가 직접 타깃 사용하도록 고정시킬 필요 X

→ 같은 인터페이스 구현한 타겟과 여러 개의 프록시 사용 가능, 순서를 정해서 단계적으로 위임

Ex)

클라이언트 → 라인넘버 데코레이터 → 컬러 데코레이터 → 페이징 데코레이터 → (타깃) 소스코드 출력 기능

런타임 시 조합함

각 데코레이터는 자신이 최종 타깃으로 위임하는지, 다음 단계의 데코레이터 프록시로 위임하는 지 알지 못함

→ 다음 위임 대상 인터페이스를 선언하고 생성자,수정자 메소드 통해 위임 대상을 외부에서 런타임 시에 주입받을 수 있도록 만듦

자바 IO 패키지의 대표적인 데코레이터 패턴

  • InputStream
// InputStream 인터페이스 구현한 타깃인 FileInputStream에 
// 버퍼 읽기 기능을 제공해주는 BufferdInputStream 데코레이터 적용
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
  • OutputStream

스프링 DI 이용하여 데코레이터 정의 및 런타임 시 다이내믹 구성

  • 데코레이터 빈의 프로퍼티로 같은 이너페이스를 구현한 다른 데코레이터 또는 타깃 빈 설정

Ex)

UserServiceTxuserService : 데코레이터 패턴

필요하다면, 언제든지 트랜잭션 외에도 다른 기능 부여하는 데코레이터 만들어서 UserServiceTxUserServiceImpl 사이에 추가 가능

언제 유용한가 ?

타깃의 코드 손대지 않고, 클라이언트가 호출하는 방법도 변경 하지 않은 채로 새로운 기능을 추가할 때

프록시 패턴

일반적인 프록시 용어

  • 클라이언트와 사용 대상 사이에 대리 역할을 맡은 오브젝트 총칭

디자인 패턴에서의 프록시 패턴

  • 프록시를 사용하는 방법 중, 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우

프록시 패턴의 프록시

  • 타깃의 기능 확장하거나 추가 X
  • 클라이언트가 타깃에 접근하는 방식 변경해줌

언제 사용하는가?

  • 타깃 오브젝트 생성 복잡하거나 당장 필요하지 않으면 꼭 필요한 시점까지는 생성하지 않는 게 좋음 → 그런데 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있음 → 클라이언트가 타깃에 대한 레퍼런스를 넘겨야 할 때, 실제 타깃 오브젝트 만드는 대신 프록시 만들어서 넘겨줌
    • 프록시의 메소드를 통해 타깃 사용하려고 시도할 때, 프록시가 실제 타깃 오브젝트 생성 해서 요청을 위임해줌
    • 레퍼런스 갖고 있지만 끝까지 사용하지 않거나 많은 작업이 진행된 후에 사용되는 경우, 프록시를 통해 실제 타깃 오브젝트 생성을 최대한 늦출 수 있음
  • 원격 오브젝트 이용하는 경우에도 프록시 사용하면 편리함
    • 다른 서버에 존재하는 오브젝트 사용하는 경우, 원격 오브젝트에 대한 프록시 만들어두고 클라이언트는 마치 로컬에 존재하는 오브젝트 쓰는 것처럼 프록시 사용하게 할 수 있음
    • 프록시는 클라 요청 받으면 네트워크 통해 원격 오브젝트를 실행하고 결과 받아서 클라에게 반환함
    • 클라이언트로 하여금 원격 오브젝트에 대한 접근 방법 제공해주는 프록시 패턴의 예
  • 특별한 상황에서 타깃에 대한 접근 권한 제어하기 위해 사용
    • 수정 가능한 오브젝트가 있을 때, 특정 레이어로 넘어가서는 읽기전용으로만 동작하게 강제해야 할 때 → 오브젝트의 프록시 만들어서 사용
    • 프록시의 특정 메소드 사용하려고 할 지 접근 불가능 예외 발생시키면 됨
      • Collections의 unmodifableCollection()을 통해 만들어지는 오브젝트가 전형적인 접근권한 제어용 프록시
      • 파라미터로 전달된 Collection 오브젝트의 프록시 만들어서 add() , remove()와 같이 정보를 수정하는 메소드 호출할 경우 UnsupportedOperationException 예외 발생

프록시 패턴이란?

→ 타깃의 기능 자체에는 관여하지 않으면서 접근하는 방법을 제어해주는 프록시 이용하는 것

프록시와 데코레이터 구조적으로 유사 BUT 프록시는

  • 생성을 지연하는 프록시라면 구체적인 생성 방법을 알아야 하기 때문에 코드에서 자신이 만들거나 접근할 타깃 클래스 정보 알고 있는 경우가 많음
  • 프록시 패턴도 인터페이스 통해 위임하도록 만들 수 있음
    • ex) 클라이언트 → (프록시 패턴) 접근제어 프록시 → 컬러 데코레이터 → 페이징 데코레이터 → (타깃) 소스코드 출력 기능

즉, 프록시란

→ 타깃과 동일한 인터페이스 구현, 클라이언트와 타깃 사이 존재, 기능의 부가 또는 접근 제어를 담당하는 오브젝트

→ 기능 부가인지, 접근제어인지에 따른 사용목적 구분으로 패턴 구분

6.3.2 다이내믹 프록시

프록시는 기존 코드에 영향 X 면서 타깃의 기능 확장, 접근 방법 제어할 수 있음.

→ 하지만 너무 번거로움

프록시 구성

  • 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임한다.
  • 지정된 요청에 대해서는 부가 기능을 수행한다.

프록시 작성의 문제점

  1. 타깃의 인터페이스 구현하고 위임하는 코드 작성이 번거로움. 부가기능 필요없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 하기 때문에 → 타깃 인터페이스 메소드가 추가,변경 시 함께 수정해줘야 함
  2. 부가 기능 코드가 중복될 가능성 많음. 트랜잭션의 경우 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 인터페이스 구현해서 만듦
    • InvocationHandlerinvoke() 메소드가 타깃 오브젝트에 대한 정보 제공하지 않아서 InvocationHandler 구현 클래스가 직접 알아야 했던 것과 달리, MethodInterceptor invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지 함께 제공받음
    • 따라서 타깃 오브젝트에 상관없이 독립적으로 만들어서 타깃이 다른 여러 프록시에서 함께 사용 가능, 타깃에 대한 정보 알고 있지 않음으로 싱글톤 빈으로 등록 가능

어드바이스 : 타깃이 필요 없는 순수한 부가기능

  • MethodInvocation처럼 타깃 오브젝트에 부가기능 제공하는 오브젝트

ProxyFactoryBean 적용 코드와 기존 JDK 다이내믹 프록시 사용 코드의 차이점

  • InvocationHandler 구현 했을 때와 달리 MethodInterceptor 구현 클래스에는 타깃 오브젝트 등장 X
  • MethodInterceptor 로는 메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달됨
  • MethodInvocation 타깃 오브젝트 메소드 실행할 수 있는 기능 있음 → MethodInterceptor 는 부가기능 제공에만 집중할 수 있음

MethodInvocation은 일종의 콜백 오브젝트

  • proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능
  • MethodInvocation 구현 클래스는 일종의 공유 가능 템플릿처럼 동작

→ 스프링이 제공하는 프록시 추상화 기능인 ProxyFactoryBean을 사용하는 코드의 가장 큰 차이점이자 장점

ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용하여 적용하였기에 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 두고 공유할 수 있다.

addAdvice() 메소드

  • ProxyFactoryBeanMethodInterceptor 설정할 때 수정자 메소드 대신 사용
  • ProxyFactoryBean에 여러 개의 MethodInterceptor 추가 가능

ProxyFactoryBean 하나만으로 여러 개 부가 기능 제공해주는 프록시 만들 수 있음

→ 새로운 부가 기능 추가시마다 프록시와 프록시 팩토리빈도 추가해야 하는 문제 해결

포인트컷 : 부가기능 적용 대상 메소드 선정 방법

  • 메소드 선정 알고리즘을 담은 오브젝트

어드바이스와 포인트컷

  • 프록시에 DI로 주입 돼서 사용
  • 여러 프록시에서 공유가 가능하도록 만들어짐 → 스프링 싱글톤 빈으로 등록 가능
  • 프록시는 포인트컷으로부터 부가기능 적용할 대상 메소드인지 확인 받으면, MethodInteceptor 타입의 어드바이스 호출 → 어드바이스는 직접 타깃 호출 X, 자신이 공유돼야 하므로 타깃 정보라는 상태도 아님 ( 타깃에 직접 의존하지 않는 일종의 템플릿 구조 설계)