티스토리 뷰

웹/Spring

스프링 볶음밥 - 6장 - AOP

세댕댕이 2022. 7. 6. 17:50

 

 

 

 

[이전 글]

더보기

볶음밥 1장 - 1,2,3: 자바빈, 디자인 패턴(템플릿 메소드, 팩토리 메소드, 전략 패턴), 관심사의 분리, SOLID 및 객체지향 약간

볶음밥 1장 - 4,5,6: 제어의 역전, 프레임워크 vs 라이브러리, 스프링 IoC 및 용어 정리, 싱글톤, 동일성 vs 동등성, 빈의 스코프

볶음밥 1장 - 7: 의존관계 주입(DI), DL, IoC

볶음밥 2장: 테스트, TDD, jUnit

볶음밥 3장: 템플릿/콜백

볶음밥 4장: 예외

볶음밥 5장: 서비스 추상화, 테스트 대역

 

그 어렵다는 AOP.. 열심히 알아보자...

 

 

[6장] AOP

- AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반 기술이다.

 

 

[고립된 테스트]

- 테스트의 대상을 환경이나 외부 서버, 다른 클래스의 코드에 종속되고 영향을 받지 않도록 고립시킬 필요가 있다.

-- UserService와 같은 서비스 계층은 DI를 통해 여러 의존 오브젝트를 사용한다. 이를 테스트 하게되면 UserService에 대해서만 테스트 되는 것이 아니라 거기에 딸린 의존 오브젝트들 까지도 같이 실행되게 된다.. UserService만 테스트 하는 것처럼 보이지만, 그 뒤의 의존관계를 따라 등장하는 의존 오브젝트, 외부 환경 등이 모두 합쳐져 같이 테스트 되는 것이다

--> 이것은 진정한 단위 테스트가 아니야! 배보다 배꼽이 더 커질 수도 있다

==> 테스트 대역(테스트 스텁 or 목 객체)을 사용하자!

 

* 고립된 테스트에선 DI를 통한 외부 환경 의존이 필요없기 때문에 스프링 컨테이너에서 빈을 가져올 필요가 없다.

-> (테스트 대역으로 대체해 사용할 것이기 때문)

 

[고립된 테스트의 장점]

1. 수행시간이 빨라진다

- 테스트에 사용되는 테스트 대역을 제외하고는 불필요한 의존 오브젝트를 사용하지 않기 때문. 외부 환경의 영향도 없다.

2. 테스트가 다른 의존 대상에 영향을 받을 경우를 대비해 복잡하게 준비할 필요가 없다.

 

 

[단위 테스트]

단위 테스트의 단위는 정하기 나름이다. 중요한 것은 하나의 단위에 초점을 맞췄다는 것

= 테스트 대상 클래스를 목 객체 등의 테스트 대역을 이용해 의존 오브젝트나 외부 리소스를 사용하지 않도록 고립시켜서 테스트 하는 것

 

[통합 테스트]

두 개 이상의 성격이나 계층이 다른 객체가 연동하는 테스트, 혹은 외부 리소스가 참여하는 테스트

- 스프링 컨테이너를 이용해 DI된 객체를 테스트 하는 것 역시 통합 테스트이다

 

1. 항상 단위 테스트를 먼저 고려해라

2. 외부 리소스를 사용해야만 가능한 테스트는 통합 테스트로 만들어라

3. 단위 테스트로 만들기 어려운 객체(DAO 등...)는 DB까지 연동하는 통합테스트를 만드는 것이 효과적이다

- DB를 사용하는 테스트는 테스트 데이터, DB 확인 등 부가적인 작업이 필요하다

4. 단위 테스트를 만들기 너무 복잡하다고 판단되면 처음부터 통합 테스트를 만들어도 된다

- 다만 통합 테스트에 사용되는 코드 중 가능한 많은 부분은 미리 단위 테스트로 검증해 두는 것이 좋다

5. 여러 단위가 의존관계를 모여 동작할 때를 위한 통합 테스트 역시 필요하다

- 다만 단위 테스트를 꼼꼼히 해뒀다면 통합 테스트의 부담이 줄어든다

6. 스프링 테스트 컨텍스트 프레임워크(=스프링 컨테이너) 를 사용하는 테스트는 통합 테스트이다

- 가능한 한 스프링의 지원 없이 코드 레벨의 DI를 이용한 단위 테스트를 만들자

7. 코드를 작성하면서 테스트는 어떻게 만들 수 있을까를 고민해봐라

- 테스트는 코드가 작성되고 빠르게 진행되는 것이 좋다. 그래서 TDD가 좋다는 것이다.

 

 

[목(Mock) 프레임워크, Mockito]

단위 테스트를 만들기 위해서는 스텁이나 목 객체의 사용이 거의 필수적이다.

- 의존관계가 없는 단순한 클래스 or 메소드 단위 테스트가 아니라면 대부분 의존 객체를 필요로 하는 테스트를 진행하기 때문.

- 그런데, 매번 목 객체를 만든다는 것은 상당히 귀찮은 일이다 (인터페이스 구현 등...)

-> 이를 편리하게 작성할 수 있도록 도와주는 목 객체 지원 프레임워크를 사용하자 (=Mockito 프레임워크)

 

목 프레임워크의 특징은 목 클래스를 일일히 준비해 둘 필요가 없다는 것이다

- 간단한 메소드 호출만으로 동적으로 특정 인터페이스를 구현한 테스트용 목 객체를 만들 수 있다

 

Mockito의 목 객체는 아래의 단계를 거쳐 사용된다

1. 인터페이스를 이용해 목 객체를 만든다 (= 인터페이스가 없으면 사용할 수 없다!!!!!!)

2. 목 객체가 리턴할 값이 있다면 이를 지정해둔다

3. 테스트 대상 객체에 DI한 이후 목 객체를 사용하게 한다

4. 테스트 대상 객체를 사용한 이후 목 객체의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇번 호출됐는지를 검증한다.

 

어지간한 단위 테스트에서의 목 객체는 Mockito 프레임워크를 사용하는 것만으로도 충분하다

 

<예제>

        // update() 메소드가 2번 호출되는지 검증
        verify(mockUserDao, times(2)).update(any(User.class));

        // users.get(1)을 파라미터로 update() 메소드가 호출되었는지 검증
        verify(mockUserDao).update(users.get(1));
        assertThat(users.get(1).getLevel()).isEqualTo(Level.SILVER);

        // users.get(3)을 파라미터로 update() 메소드가 호출되었는지 검증
        verify(mockUserDao).update(users.get(3));
        assertThat(users.get(3).getLevel()).isEqualTo(Level.GOLD);

        ArgumentCaptor<SimpleMailMessage> mailMessageArg
                = ArgumentCaptor.forClass(SimpleMailMessage.class);
        // 목 객체에 전달된 파라미터를 가져와 내용 검증 - 파라미터 내부 정보 확인 시 사용
        verify(mockMailSender, times(2)).send(mailMessageArg.capture());
        List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
        assertThat(mailMessages.get(0).getTo()[0]).isEqualTo(users.get(1).getEmail());
        assertThat(mailMessages.get(1).getTo()[0]).isEqualTo(users.get(3).getEmail());

any() = 파라미터 내용은 무시한다

 

 

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

[프록시, Proxy]: 마치 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이이언트의 요청을 받아주는 역할을 하는 것.

- 프록시를 통해 최종적으로 요청을 위임받아 처리하는 실제 객체타깃(Target) or 실체(Real Object)라고 한다.

- 프록시는 타깃과 동일한 인터페이스를 구현하여 클라이언트와 실제 객체 사이에 끼어든다.

--> 프록시는 타깃을 제어할 수 있는 위치에 있다 (클라이언트 ---> 프록시 ---> 타깃)

 

1. 클라이언트가 타깃에 접근하는 방법을 제어하고자 할 때 사용

2. 타깃에 부가적인 기능을 부여해주기 위해 사용

 

 

[데코레이터 패턴]

타깃에 부가적인 기능을 런타임 중에 동적으로 부여해주기 위해 프록시를 사용하는 디자인 패턴

- 동적 부여 = 컴파일 시점에는 어떤 방법/순서로 프록시와 타깃이 연결되는지 정해져 있지 않다는 것 (= 코드상으로 드러나지 않는다)

- 프록시를 여러개 사용할 수도 있다. 프록시가 반드시 타깃을 사용하지 않아도 된다

(클라이언트 --> 데코레이터1(=프록시) --> 데코레이터2 --> 데코레이터3 --> 타깃).... "단계적 위임"

-- 단 프록시와 타깃은 동일한 인터페이스를 구현해야됨

--- 프록시로서 동작하는 데코레이터도 위임 대상을 인터페이스로 접근하기 때문에 자신이 최종 타겟으로 위임하는지, 다음 데코레이터로 위임하는지 알 수 없다!

(데코레이터의 다음 위임 대상은 인터페이스로 선언하고 위임 대상은 런타임 시에 주입받아 사용한다)

- 데코레이터 패턴은 타깃의 코드를 손대지 않고 + 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가하고자 할 때 유용한 방법이다

 

* 대표적인 데코레이터 패턴의 예시: InputStream / OutputStream 구현 클래스

InputStream is = new BufferedInputStream(new FileInputStream("test.txt"));

인터페이스: InputStream / 타깃: FileInputStream / 데코레이터: BufferedInputStream

 

 

[프록시와 프록시 패턴]

프록시: 클라이언트와 사용 대상 사이에 대리 역할을 맡은 객체를 두는 방법을 총칭

프록시 패턴: 타깃에 대한 접근 방법을 제어하려는 목적을 가진 경우 (기능 확장/추가 X)

- 타깃 객체를 생성하기 복잡하거나 당장 필요하지는 않은 경우에는 필요한 시점까지 생성을 미루는 것이 좋다. 하지만 타깃 객체에 대한 레퍼런스는 미리 필요할 때가 있는데 이때 적용하는 것이 프록시 패턴이다

- 실제 타깃 객체를 미리 만들지 말고 가짜 프록시 객체를 넘겨준다. 그리고 프록시의 메소드를 통해 타깃에 실제 접근하려고 한다면 그때 가서 타깃 객체를 생성하고 요청을 위임해준다.

--> 레퍼런스는 갖고 있지만 사용하지 않거나 한참 뒤에 사용하는 경우에 프록시를 통해 생성 시점을 최대한 늦추는 것

 

이외에도 타깃에 대한 접근권한 제어 등, 타깃의 기능 자체에는 관여하지 않으면서도 접근하는 방법을 제어할 수 있다.

=> 프록시 패턴이란 타깃의 기능 자체에는 관여하지 않으면서 접근 방법을 제어해주는 프록시를 이용하는 패턴이다.

 

* 프록시 패턴과 데코레이터 패턴은 거의 유사하나 의도의 차이로 보면 된다. 둘이 섞어 쓰는 것도 가능하다

프록시 패턴: 접근 제어 vs 데코레이터 패턴: 부가 기능 추가

+ 프록시는 코드 내에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고있는 경우가 많다 (자신이 생성지연 프록시라면 구체적인 생성 방법을 알아야 하기 때문)

 

 

[다이내믹 프록시]

프록시는 기존 코드에 영향을 주지 않고서도 타깃의 기능을 확장하거나 접근방법을 제어할 수 있다. 그러나 문제가 하나 있는데.. 바로 만들기 번거롭다는 것이다

1. 타깃의 인터페이스를 구현하고 위임하는 코드를 작성해야 하고

2. 부가기능 코드가 중복될 가능성 또한 많다 (메소드마다 중복 사용될 가능성)

-> 프록시 생성을 쉽게 도와주는 java.lang.reflect 패키지를 사용해보자

 

* 리플렉션: 자바의 코드 자체를 추상화해서 접근하도록 만든 것

* 다이내믹 프록시: 리플렉션 기능을 이용해서 프록시를 만들어 주는 것.

 

 

[리플렉션, reflection]

자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 Class 타입의 객체를 하나씩 갖고있다.

- 이는 '클래스명.class' 혹은 각 객체의 getClass() 메소드를 호출해서 클래스 정보를 담은 Class 타입의 객체를 가져올 수 있 b 다

- Class 타입 객체를 이용하면 클래스 코드에 대한 메타정보를 가져오거나 객체를 조작할 수 있다. (클래스 이름이 무엇인지 / 어떤 클래스를 상속했는지 / 어떤 인터페이스를 구현했는지 / 타입은 무엇인지 / 정의된 메소드, 파라미터와 리턴 타입은 무엇인지 알아낼 수  있다.

- 심지어는 메소드의 정보를 가져온 다음에 실행시킬 수도 있다 (Method 인터페이스에 정의된 invoke() 메소드 사용)

- 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API

    @Test
    public void invokeMethod() throws Exception {
        String name = "Winter";

        assertThat(name.length()).isEqualTo(6);

        Method lengthMethod = String.class.getMethod("length");
        assertThat((Integer)lengthMethod.invoke(name)).isEqualTo(6);

        assertThat(name.charAt(0)).isEqualTo('W');

        Method charAtMethod = String.class.getMethod("charAt", int.class);
        assertThat((Character)charAtMethod.invoke(name, 0)).isEqualTo('W');
    }

 

와 굉장히 신기하다. ㄷㄷ

자세한 리플렉션 사용은 링크 참조..

 

Java - Reflection 쉽고 빠르게 이해하기

자바의 리플렉션(Reflection)은 클래스, 인터페이스, 메소드들을 찾을 수 있고, 객체를 생성하거나 변수를 변경할 수 있고 메소드를 호출할 수도 있습니다. Reflection은 Class, Constructor, Method, Field와 같

codechacha.com

 

다이내믹 프록시프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트이다.

- 다이내믹 프록시 오브젝트는 타깃의 인터페이스와 같은 타입으로 만들어진다.

-- 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다.

--- 프록시 팩토리에게 인터페이스 정보를 제공해주면 해당 인터페이스를 구현한 클래스의 오브젝트를 자동으로 만들어준다!!

(인터페이스 구현 클래스 오브젝트는 만들어주지만, 프록시로서 필요한 부가기능 제공 코드는 직접 작성해야 함)

 

부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 작성한다.

public Object invoke(Object proxy, Method method, Object[] args);

- 메소드 하나만 가진 간단한 인터페이스

- 리플렉션의 Method 인터페이스 + 메소드 호출 시 필요한 파라미터를 받아온다

- 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환, InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘긴다. 

--> 타깃 인터페이스의 모든 메소드 요청이 하나의 메소드로 집중, 중복되는 기능을 효과적으로 제공할 수 있다.

 

p441.

 

앞서 살펴봤듯이 리플렉션을 통해 메소드/파라미터 정보를 모두 갖고있으면 타깃 오브젝트의 메소드를 호출할 수 있다.

- InvocationHandler 구현 오브젝트가 타깃 오브젝트의 레퍼런스를 갖고있기만 하면 리플렉션을 이용해 위임 코드를 만들어낼 수 있다

 

1. 인터페이스 정보와 함께 프록시 팩토리에게 다이내믹 프록시를 만들어 달라고 요청한다.

2. 해당 인터페이스의 모든 메소드를 구현한 오브젝트를 생성해준다

3. InvocationHandler 구현 오브젝트를 제공해주면 다이내믹 프록시가 받는 모든 요청을 InvocationHandler의 invoke() 메소드 한 곳으로 보낸다

-> 인터페이스의 메소드가 아무리 많더라도 invoke() 메소드 단 하나로 모두 처리할 수 있다!!!

4. invoke() 메소드를 통해 타깃 오브젝트를 호출 / 위임 / 부가 기능을 지지고 볶을 수 있다

엄청나다.. 진짜 뿅간다

 

p442.

 

[다이내믹 프록시 사용 시 장점]

1. 인터페이스의 메소드가 추가되더라도, 부가기능은 invoke() 메소드 하나로 처리할 수 있다.

- 단, 다른 리턴 타입을 갖는 메소드가 추가되는 경우에는 캐스팅 오류가 발생할 수 있으니 주의해야 한다.

2. 타깃의 종류에 상관없이 적용 가능하다

- 리플렉션의 Method 인터페이스를 이용해 타깃의 메소드를 호출하는 것이기 때문에 특정 타깃에 얽매이지 않는다.

public class UppercaseHandler implements InvocationHandler {
    private Object target; // 어떤 종류의 타깃도 적용 가능
    public UppercaseHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = method.invoke(target, args);
        if(ret instanceof String && method.getName().startsWith("say")) {
            return ((String)ret).toUpperCase();
        } else {
            return ret;
        }
    }
}

invoke() 메소드 하나로 모든 요청을 처리하기 때문에 어떤 메소드에 어떤 기능을 적용할 지 선택해야 할 수 있다.

- 호출 메소드 이름 / 파라미터 개수 및 타입 / 리턴 타입 등의 정보를 이용해서 적용 메소드를 선택할 수 있다.

(예제 - 메소드의 이름이 say로 시작하는 경우.)

 

 

[6.3.4] 다이내믹 프록시를 위한 팩토리 빈

다이내믹 프록시 오브젝트는 일반적인 스프링 빈으로 등록할 방법이 없다

- 스프링 빈은 기본적으로 클래스명과 프로퍼티로 정의된다. 스프링은 지정된 클래스명을 가지고 리플렉션을 이용해 해당 클래스의 오브젝트를 만들어 냄.

-- Class의 newInstance() 메소드는 해당 클래스의 NoArgsConstructor 호출하고, 그 결과로 생성되는 오브젝트를 돌려주는 리플렉션 API (그래서 파라미터 없는 생성자를 필요로 했던 것 ㄷㄷ)

- 스프링은 내부적으로 리플렉션 API를 이용, 빈 정의에 나오는 클래스명을 갖고 빈 오브젝트를 생성한다. 반면 다이내믹 프록시 오브젝트의 경우는 Proxy 클래스의 스태틱 메소드(Proxy.newProxyInstance())를 통해서만 만들 수 있다. 

-- 또한, 다이내믹 프록시 오브젝트의 클래스가 어떤 것인지도 알 수 없어 클래스 정보를 미리 알아내 빈에 정의할 수 없다.

 

하지만 스프링은 역시나 또 다른 방법을 갖고있다

 

스프링은 클래스 정보를 통한 오브젝트 생성 이외에도 빈을 만들어낼 수 있는 다양한 방법을 제공해준다.

- 대표적인 방법 중 하나가 "팩토리 빈"을 이용한 빈 생성.

 

[팩토리 빈]

- 스프링을 대신해서 오브젝트의 생성 로직을 담당하도록 만들어진 특별한 빈.

- 생성 방법은 여러가지가 있으나, 대표적으로는 스프링의 FactoryBean 인터페이스를 구현하는 것으로 만들어진다.

@Getter
public class Message {
    String text;

    // 외부에서 생성자를 통한 오브젝트 생성 불가(private)
    private Message(String text) {
        this.text = text;
    }

    public static Message newMessage(String text) {
        return new Message(text); // 생성자 대신 사용할 수 있는 스태틱 팩토리 메소드.
    }
}
@Setter // 오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해 DI 받는다.
public class MessageFactoryBean implements FactoryBean<Message> {
    String text;

    @Override
    public Message getObject() throws Exception {
        return Message.newMessage(text); // 실제 빈으로 사용될 오브젝트 생성.
    }

    @Override
    public Class<?> getObjectType() {
        return Message.class; // Message 빈의 타입은 getObjectType() 메소드가 리턴해주는 타입으로 결정.
    }

    @Override
    public boolean isSingleton() {
        return false; // getObject() 메소드마다 새로운 오브젝트가 생성됨으로 싱글톤이 아님.
        // 하지만 스프링은 이것조차 싱글톤으로 관리해줄 수 있다!
    }
}
/* 설정파일 */
<bean id="message" class="springbook.learningtest.factorybean.MessageFactoryBean">
        <property name="text" value="Factory Bean"/>
</bean>
//////////////

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/FactoryBeanTest-context.xml")
public class FactoryBeanTest {
    @Autowired
    ApplicationContext context;

    @Test
    public void getMessageFromFactoryBean() {
        Object message = context.getBean("message");
        assertThat(message).isInstanceOf(Message.class);
        assertThat(((Message)message).getText()).isEqualTo("Factory Bean");
    }

    @Test
    public void getFactoryBean() throws Exception {
        Object factory = context.getBean("&message"); // 팩토리 빈 자체를 가져오고자 할 때 사용.
        assertThat(factory).isInstanceOf(MessageFactoryBean.class);
    }
}

 

팩토리 빈을 사용하면 다이내믹 프록시 오브젝트를 스프링 빈으로 만들 수 있다!!!

- 팩토리 빈의 getObject() 메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 넣어주면 된다.

- 팩토리 빈이 만드는 다이내믹 프록시는 구현 인터페이스나 타깃 종류에 제한이 없다 = 재사용 가능

 

 

[프록시 팩토리 빈의 장점]

한번 부가기능을 가진 프록시 팩토리 빈을 만들어두면 타깃의 타입에 관계없이 재사용이 가능하다.

- 타깃 오브젝트에 맞는 프로퍼티 정보를 설정하고 빈으로 등록해주기만 하면 재사용이 가능하다.

- 하나 이상의 팩토리 빈을 동시에 빈으로 등록해도 상관없다 (빈의 타입은 타깃 인터페이스를 따라가기 때문)

 

다이내믹 프록시를 이용하면 타깃 인터페이스를 구현해야하는 번거러움을 제거할 수 있다. 기존 코드에는 손도 대지 않고도 부가적인 기능을 추가해줄 수 있고, 부가기능 코드의 중복도 사라진다.

- 다이내믹 프록시 오브젝트는 팩토리 빈을 통해 생성 및 사용되어진다

=> 프록시 기법을 편리하게 사용할 수 있도록 지원해준다.

 

 

[프록시 팩토리 빈의 한계점]

1. 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 것은 불가능하다

- 기존 방법으로는 팩토리 빈 설정 하나 당 하나의 클래스(타깃) 내 메소드 들에게만 부가기능 제공 가능.

--> 여러 클래스에 전부 부가기능을 부여하려면 팩토리 빈 설정에 중복이 불가피.

 

2. 하나의 타깃에 여러개의 부가기능을 적용하려고 할 때도 팩토리 빈 설정이 각 부가기능의 개수만큼 붙어야 한다.

- 설정 파일이 매우 복잡해짐.

- 새로운 부가기능이 추가될때 마다 프록시와 프록시 팩토리 빈이 추가되게 된다

 

3. 프록시 팩토리 빈의 개수만큼 TransactionHandler 오브젝트(InvocationHandler 구현체)가 만들어진다

- 동일한 부가기능을 제공하더라도 타깃 오브젝트가 달라지면 새로운 오브젝트를 또 만들어 내야 한다.

 

 

[중간 요약]

1. 인터페이스 정보와 함께 프록시 팩토리에게 다이내믹 프록시를 만들어 달라고 요청한다.

- 프록시 팩토리는 타깃 등 오브젝트를 생성할 때 필요한 정보를 프로퍼티로 설정해 DI 받는다

- 프록시 팩토리는 getObject() 메소드를 통해 다이내믹 프록시를 생성해준다.

@Setter // 오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해 DI 받는다.
public class TxProxyFactoryBean implements FactoryBean<Object> {
    Object target;
    PlatformTransactionManager transactionManager;
    String pattern;
    Class<?> serviceInterface; // 다이내믹 프록시 생성 시 사용.
    
    @Override  // 실제 빈으로 사용될 오브젝트 생성.
    public Object getObject() throws Exception {
        // 팩토리 빈의 개수만큼 중복 생성된다..
        TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTarget(target);
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern(pattern);
        // 다이내믹 프록시 생성
        return Proxy.newProxyInstance(getClass().getClassLoader(),
                new Class[]{serviceInterface}, txHandler);
    }

    @Override // 빈의 타입은 getObjectType() 메소드가 리턴해주는 타입으로 결정.
    public Class<?> getObjectType() {
        return serviceInterface;
    }

    @Override
    public boolean isSingleton() {
        return false; // getObject() 메소드마다 새로운 오브젝트가 생성됨으로 싱글톤이 아님.
        // 하지만 스프링은 이것조차 싱글톤으로 관리해줄 수 있다!
    }
}

2. 클라이언트가 타깃에게 하는 요청은 모두 다이내믹 프록시에게 간다

- 타깃과 다이내믹 프록시는 모두 동일한 인터페이스를 구현했기 때문에 클라이언트는 이를 알 도리가 없다

    <bean id="userService" class="springbook.user.service.TxProxyFactoryBean">
        <property name="target" ref="userServiceImpl"/>
        <property name="transactionManager" ref="transactionManager"/>
        <property name="pattern" value="upgradeLevels"/>
        <property name="serviceInterface" value="springbook.user.service.UserService"/>
    </bean>

 

3. 다이내믹 프록시가 받는 모든 요청은 전부 InvocationHandler 구현 오브젝트의 invoke() 메소드로 보낸다

// txHandler --> InvocationHander() 구현 오브젝트
return Proxy.newProxyInstance(getClass().getClassLoader(),
    new Class[]{serviceInterface}, 
    txHandler);

 

4. invoke() 메소드에서는 리플렉션을 이용해 타깃의 메소드를 실행(위임), 부가기능 혹은 접근 제어 기능 수행

public class TransactionHandler implements InvocationHandler {
    private Object target;
    private PlatformTransactionManager transactionManager;
    private String pattern;

    public void setTarget(Object target) {
        this.target = target;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().startsWith(pattern)) {
            return invokeInTransaction(method, args); // 트랜잭션 O
        } else {
            return method.invoke(target, args); // 트랜잭션 X
        }
    }

    private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
        TransactionStatus status = // 트랜잭션 시작
                transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object ret = method.invoke(target, args); // 타깃 메소드 호출
            transactionManager.commit(status);
            return ret;
        } catch (InvocationTargetException e) {
            transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

 

너무 길어져서 다음 글에 계속...

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
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 30
글 보관함