Proxy 패턴

임준영·2021년 4월 10일
0

Proxy 패턴

스프링 AOP에 대해서 공부했던 과정들을 숙지하기 위해서 다시한번 Proxy에 대해서 알아보았습니다. 프록시에 대해서 검색을 하는 도중에 Proxy 패턴에 대한 포스팅을 접하게 되었고, 이 내용은 실제 AOP가 어떻게 동작하는지에 대한 메커니즘을 이해하는데 좋은 글이였다고 생각하여 다시 반복적인 코드 작성을 통하여 알아보는 시간이 였습니다.

1. Proxy란?

Proxy는 실제로 액션을 취하는 객체를 대신해서 대리자 역할을 합니다. 한마디로 실제 호출해야하는 메서드를 가지고 있는 실제 대상을 감싸고 있는 Wrapping 클래스라고 생각하시면 됩니다.

Proxy 패턴을 사용하게 되면 Proxy 단계에서 권한을 부여할 수 있는 이점이 생기고 필요에 따라 객체를 생성시키거나 사용하기 때문에 메모리를 절약할 수 있는 이점도 생깁니다. Proxy 패턴이 하는 일은 한마디로 자신이 보호하고 있는 객체에 대한 엑세스 권한을 제어하는 것입니다.

이렇게 함으로써 실제 인스턴스 사용 과정을 관여하고 메모리를 절약하는 방법을 코드로 살펴보겠습니다.

가장 먼저 실질적으로 사용되는 핵심 코드를 만들어보겠습니다. 하나는 인터페이스이고 하나는 인터페이스를 상속받는 클래스입니다.

인터페이스

public interface CommandExcutor {

    public void runCommand(String cmd) throws Exception;

}

인터페이스를 구현한 클래스

public class CommandExcutorImpl implements CommandExcutor {

    @Override
    public void runCommand(String cmd) throws Exception {
        Runtime.getRuntime().exec(cmd);
        System.out.println("cmd: " + cmd);
    }
}

위의 코드를 보면 인터페이스와 구현을 분리하여 작성하였고, 실질적으로 CommandExcutorImple를 호출하여 객체를 생성시키고 높은 cost의 runCommand() 메서드 작업으로 인해 메모리 낭비가 예상될 수 있습니다.

Runtime.getRuntime().exec() 메서드는 process를 실행시키거나 os를 제어할때 사용하는 클래스로만 알고 있습니다.

proxy class

위의 단점을 보완하기 위해 프록시 클래스를 작성하였습니다. 일단 똑같은 인터페이스를 상속받음으로 인터페이스의 일관성을 유지합니다.
그리고 생성자에서 CommandExcutorImpl 클래스를 인스턴스화 시키고, 인스턴스화 된 excutor 객체의 runCommand() 메서드를 프록시 클래스의 runCommand 메서드에서 엑세스를 결정합니다.

public class CommandExcutorProxy implements CommandExcutor {

    // isAdmin 값에 따라서 객체에 대한 엑세스 권한을 제어합니다.    
    private boolean isAdmin;
    private CommandExcutor excutor;


    public CommandExcutorProxy(String user, String pwd) {
        if ("sa1341".equals(user) && "1234".equals(pwd)) {
            isAdmin = true;
        }
        excutor = new CommandExcutorImpl();
    }

    @Override
    public void runCommand(String cmd) throws Exception {
    
        if (isAdmin) {
            excutor.runCommand(cmd);
        } else {
            if (cmd.trim().startsWith("rm")) {
                throw new Exception("rm is only admin");
            } else {
                excutor.runCommand(cmd);
            }
        }
    }
}

실행 클래스

public class ProxyPatternTest {

    public static void main(String[] args) {

       CommandExcutor excutor = new CommandExcutorProxy("sa1341","1111");

       try {
           excutor.runCommand("ls -al");
           excutor.runCommand("rm -rf *");
       } catch (Exception e){
           System.out.println(e.getMessage());
       }
    }
}

위에서는 일부로 패스워드를 다르게 주어서 특정 명렁어에 대해서 예외를 던지도록 작성하였습니다. rm -rf * 같이 터미널에서 디렉토리를 전부 강제 삭제하는 명령어이기 때문에 리스크가 존재하기 때문에 인증된 사용자만이 수행할 수 있도록 테스트 케이스에 넣어봤습니다.

실행 결과

위와 같이 프록시를 작성하면 인터페이스를 일관성있게 유지시키면서 엑세스 권한을 부여할 수 있고 더불어 메모리 절약을 할 수 있게 됩니다.

2. Spring AOP Proxy

Aspect: 부가 기능 모듈로써 객체지향의 객체처럼 AOP의 한 기능을 가지는 모듈입니다. 스프링에서는 @Aspect 어노테이션을 통해 구현할 수 있습니다.

스프링에서는 위빙(Weaving)을 통해서 Aspect가 지정된 객체를 새로운 프록시 객체를 생성합니다.

스프링 AOP 프록시 생성방식
1. JDK Dynamic Proxy
2. CGLIB Proxy

@Transactional
public void deleteById(final Long id){
    boardRepository.deleteById(id);
}
@Service
@RequiredArgsConstructor
@Slf4j
public class BoardService {

    private final BoardRepository boardRepository;

    @Transactional
    public void deleteById(final Long id){
        boardRepository.deleteById(id);
    }
}

위의 Service를 호출했을 때, BoardService로 바로 접근하는 것이 아니라

CGLIB 프록시

$$EnhancerBySpringCGLIB라고 적힌 weaving으로 생성된 CGLIB 프록시를 통해 간접적으로 접근하게 됩니다. 저 프록시 객체를 통해서 트랜잭션이나 로깅 등 AOP와 관련된 처리가 동작하게 됩니다.

내부적으로 프록시의 코드가 어떻게 되어있는지는 모르겠지만 아래코드는 트램님이 올리신 블로그 포스팅을 참조하였습니다.

public class BoardServiceEnhancerBySpringCGLIB{

    private BoardService boardService;


    public BoardServiceEnhancerBySpringCGLIB(BoardService boardService){
        this.boardService = boardService;
    }

    public void save(final Long id){

        TransactionManager transactionManager = new TransactionManager();

        try{
            this.boardService.save(id);
            //BoardService를 호출해 로직 실행 후, 에러가 없으면 comit 실행
            transactionManager.comit();

        } catch(Exception e){
            transactionManager.rollBack();
            throw new Exception("에러발생");           
        }
    }
}

과거에는 기본적으로 인터페이스가 있고, 그 인터페이스를 구현한 클래스의 경우 JDK Dynamic Proxy을 사용하고 인터페이스가 없는 경우 CGLIB Proxy를 사용했습니다.

CGLIB Proxy는 상속을 통해 Proxy를 구현하기 때문에 final 클래스인 경우 Proxy를 생성할 수 없습니다.

스프링 부트에서는 2.0 이후로는 기본적으로 디폴트로 CGLIB Proxy를 생성합니다. 스프링 부트 개발 팀장도 CGLIB Proxy는 내부적으로 JVM이 바이트 코드를 조작하기 때문에 성능도 JDK 방식보다 더 빠르다고 합니다. 그렇기 때문에 CGLIB 방식을 더 권장됩니다.

하지만 아래 코드처럼 예외적으로 spring-data-jpa에서는 JDK Dynamic Proxy를 사용해 Repository를 생성합니다.

public interface BoardRepository extends JpaRepository<Board, Long>, QuerydslPredicateExecutor<Board>{
}

JpaRepository 인터페이스를 구현한 클래스인 SimpleJpaRepository가 존재하는데 왜 CGLIB 방식을 사용하지 않을까.. 궁금하긴 하지만 더 알아보면 머리가 터질지도 몰라서.. 나중에 기회가 된다면 포스팅해보겠습니다.

참조 사이트: https://tram-devlog.tistory.com/entry/Spring-AOP-weaving-proxy, https://blog.seotory.com/post/2017/09/java-proxy-pattern

0개의 댓글