프록시

반응형
반응형

프록시는 고기방패 (아 미안해)

프록시는 무엇을 뜻하는 걸까?

대안이라는 뜻을 가지고 있다. 

가장 대표적인 예로 JPA를 들 수 있다.
간단하게 JPA코드를 작성해보겠다.

public interface BookJpaRepository extends JpaRepository<Book,Long> {

}
@SpringBootTest
class BookJpaRepositoryTest {

    @Autowired
    BookJpaRepository bookJpaRepository;

    @Test
    void test() {
        System.out.println(bookJpaRepository);
    }
}

인터페이스로 만들었지만..

다양한 메소드를이 나오는 것을 확인 할 수 있다.
물론, 다른 인터페이스를 상속한다음 실행해도 위와 같은 결과는 나온다.
하지만..

특정 메소드를 실행한다고 한다면 어떨까?

System.out.println(bookJpaRepository.count());

놀랍게도 구현이 되지 않았지만,

값이 나오는 것을 확인할 수 있다. 
그런데 이것이 어떻게 가능한 걸까?

나는 분명히 구현을 하지 않았는데...
이것이 바로 "프록시"다.
프록시에 대해 자세히 알아보자.

UML로 프록시를 그리면 다음과 같이 그릴 수 있다.

여기서 주목해야될 점은 Proxy와 RealSubject모두 Subject로 구현되있는 모습이다.(상속되있는 모습이다.)

코드로 구현해보자.

public interface CovidSubject {
    void spread();
}
public class CovidProxy implements CovidSubject{
    @Override
    public void spread() {

    }
}
public class CovidRealSubject implements CovidSubject{
    @Override
    public void spread() {

    }
}

이런식으로 구현할 수 있다. 근데 프록시를 사용한다고 했으니

public class CovidProxy implements CovidSubject{
    private CovidSubject covid;
    public CovidProxy(CovidSubject covid) {
        this.covid = covid;
    }

    @Override
    public void spread() {
        System.out.println("바이러스가 퍼져나갑니다.");
    }
}

이런식으로 작성하면 사용만 하면 된다.

public class Client {
    public static void main(String[] args) {
        CovidSubject subject = new CovidProxy(new CovidRealSubject());
        subject.spread();
    }
}

이것이 프록시 패턴인데,
프록시 패턴이 우리에게 어떤 것을 줄 수 있을까?
프록시는 왜 사용하는 걸까?
그냥 new CovidRealSubject()로 불러오면 더 편할 것 같은데,
번거롭게 프록시를 사용하는 이유가 뭘까? 

두 가지이유를 생각해 볼 수 있다.

   1. 원래 객체(CovidRealSubject)를 건들지 않고 수정이 가능하다.
   2. 코드의 양이 방대하여 무겁다면 그것을 "프록시"를 통해 완화가 가능하다.

 이것이 프록시를 사용하는 이유가 아닐까?
도중에 프록시가 껴있기 때문에 성능적으로 문제가 발생할 여지가 분명히 있다.

다이나믹 프록시

자바에서는 리플렉션 기능을 이용해서 "프록시"기능을 제공하고 있다.
어떻게 동작하는지 알아보자.

CovidSubject instance = (CovidSubject) Proxy.newProxyInstance(CovidSubject.class.getClassLoader(), new Class[]{CovidSubject.class},
                new InvocationHandler() {
                    CovidRealSubject real = new CovidRealSubject();
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object invoke = null;
                        if (method.getName().equals("spread")) {
                            System.out.println("hello");
                            invoke = method.invoke(real, args);
                            System.out.println("코로나 끝났으면...");
                        }
                        return invoke;
                    }
                });

instance.spread();

이런식으로 작성할 수 있다고 한다. 
여기서 한 층더 업그레이드 된것이 스프링 AOP다. 따라서 스프링 AOP는 프록시 기반 이라고 한다.
토비의 스프링에 자세히 나와있으니 그 책을 읽은뒤에 다시 작성해보자.

근데 현재는 인터페이스로 class를 사용하고 있다.
하지만 이것을 클래스로 바꾼다면?
CovidSubject => CovidRealSubject

이런 에러를 확인 할 수 있다.

그렇다면 클래스로 하는 것은 불가능한것일까?

순수한 자바의 리플렉션 기능을 활용해서 만드는 것은 불가능하고
라이브러리의 힘을 빌려야 한다.

지금은 간단하게 코드 작성하려고 한다.

  CGLIB

public class Client {
    public static void main(String[] args) {
        MethodInterceptor handler = new MethodInterceptor() {
            BookServiceImpl bookService = new BookServiceImpl();
            @Override
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("hello");
                Object invoke = methodProxy.invoke(bookService, objects);
                System.out.println("who");
                return invoke;
            }
        };
        BookServiceImpl bookService = (BookServiceImpl) Enhancer.create(BookServiceImpl.class, handler);
        bookService.action();
    }
}

이런식으로 만들면 되는데 handler를 만드는 방식은 여러종류가 존재한다고 한다.
또한 mehtod혹은 methodProxy아무거나 사용해도 무방하다고 한다.(그래도 methodProxy방법을 추천한다고 함)

ByteBuddy

public class Client {
    public static void main(String[] args) throws Exception{
        Class<? extends BookServiceImpl> proxy = new ByteBuddy().subclass(BookServiceImpl.class)
                .method(named("action")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
                    BookServiceImpl bookService = new BookServiceImpl();
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("hello");
                        Object invoke = method.invoke(bookService, args);
                        System.out.println("this is byte buddy");
                        return invoke;
                    }
                }))
                .make().load(BookServiceImpl.class.getClassLoader()).getLoaded();
        
        BookServiceImpl bookService = proxy.getConstructor(null).newInstance();
        bookService.action();
    }
}

이 방법들의 치명적인 단점은
변경이 불가능한 클래스에는 사용이 불가능하다.(final class, private 생성자)

클래스를 굳이 다이나믹 프록시로 만들어야 된다면, 그래야 되겠지만,
그렇지 않을 경우에는 인터페이스로 만드는 것이 더 좋다고 한다.

이는 다양한 곳에서 활용되어지는데,
Spring Data JPA
Spring AOP
Mockito

등 여러곳에서 사용이 된다고 한다.

반응형

'프로그래밍 언어 > 자바' 카테고리의 다른 글

ENUM  (0) 2021.01.24
예외 처리  (0) 2021.01.14
인터페이스  (0) 2021.01.04
나만의 DI프로그램 만들기  (0) 2021.01.03
리플렉션  (0) 2021.01.01

댓글

Designed by JB FACTORY