Spring

AOP의 정의와 프록시/데코레이션 패턴

쭈녁 2024. 1. 4. 00:32

 

AOP의 정의

 

AOP는 관점지향프로그래밍이라 불리며 어떤 로직을 기준으로 핵심적인 로직과 부가적인 기능의 관점을 나누어 보고 그 관점을 기준으로 모듈화 한다. 흩어진 관심사를 묶고 비즈니스 로직에선 분리하여 재사용 가능하게 한다.

 

AOP 적용 예시

 

AOP(관점지향 프로그래밍)을 구현하기 위해 다양한 패턴이 등장했다.

대표적으로 템플릿 / 메서드 콜백 패턴과 프록시 / 데코레이션 패턴이 있다.

템플릿 / 메서드 콜백 패턴은 대표적으로 JDBC 템플릿이 있다. 템플릿 메서드 패턴의 단점으로는 추상화된 메서드 (추상 객체, 인터페이스)를 사용 시 직접 구현해야 하는 불편함이 있다.

템플릿은 객체를 생성하고 그에 맞는 파라미터를 넣고 기능을 다시 정의해야 하며 수정에 매우 불리하다(재정의된 메인 로직을 모두 찾아 다시 수정해야 함)

 

예시) - JDBC템플릿

@Repository
public class JdbcTemplatePostRepository {

    private final JdbcTemplate jdbcTemplate;

    public JdbcTemplatePostRepository(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
    public Post save(Post post) {
        String sql = "INSERT INTO post(title, content) VALUE(?,?)";
        KeyHolder keyHolder = new GeneratedKeyHolder();

    // 1. JdbcTemplate을 사용시 실제 로직을 템플릿 메서드 안에 수행해야 하며 만일 JDBC가 아닌 임의로 생성된 탬플릿의 경우 해당 기능이 무엇을 하는지 알기 어렵고 로직 수정시 모두 찾아 수정해야한다..
        jdbcTemplate.update(connection -> {
            PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"});
            ps.setString(1, post.getTitle());
            ps.setString(2, post.getContent());
            return ps;
        }, keyHolder);
        
        Long postId = keyHolder.getKey().longValue();
        post.setId(postId);
        return post;
    }
}

 

 

때문에 객체 생성 시 실제 객체가 아닌 대체하는 객체를 통하여 부가적인 기능을 추가하는 패턴이 등장하게 되었다 이를 프록시 패턴과 데코레이션 패턴이라고 한다.

 

구성도는 다음과 같다.

 

 

서비스 흐름

 

 

 

예제 코드

 

구현체 예시)

@Slf4j
public class ConcreteLogic {
    public String operation() {
        log.info("concrete 로직 실행");
        return "data";
    }
}

 

@Slf4j
public class TimeProxy extends ConcreteLogic {
    private ConcreteLogic concreteLogic;

    public TimeProxy(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

    @Override
    public String operation() {
        log.info("TimeDeco 실행");
        long start = System.currentTimeMillis();
        String result = concreteLogic.operation();
        long end = System.currentTimeMillis();
        long resultTime = end - start;
        log.info("timeDeco 종료 resultTime ={}ms",resultTime);

        return result;
    }
}

 

public class ConcreteClient {
    private ConcreteLogic concreteLogic;

    public ConcreteClient(ConcreteLogic concreteLogic) {
        this.concreteLogic = concreteLogic;
    }

    public void execute() {
        concreteLogic.operation();
    }
}

 

public class ConcreteProxyTest {
    @Test
    void noProxy() {
        ConcreteLogic concreteLogic = new ConcreteLogic();
        ConcreteClient concreteClient = new ConcreteClient(concreteLogic);
        concreteClient.execute();
    }
    
    // noProxy 결과 -> 
    // concrete 로직 실행
   
    @Test
    void addProxy() {
        ConcreteLogic concreteLogic = new ConcreteLogic();
        TimeProxy timeProxy = new TimeProxy(concreteLogic);
        ConcreteClient concreteClient = new ConcreteClient(timeProxy);
        concreteClient.execute();
    }
    //addProxy 결과 ->
    /*
    TimeDeco 실행
    concrete 로직 실행
    timeDeco 종료 resultTime =1ms
    */
}

 

구현체의 경우 구현체 클래스를 직접 상속 (extends) 받아서 겉에 proxy를 감싸는 방법이다.

 

 

 

인터페이스 예시)

public interface Subject {
    public String operation();
}

 

@Slf4j
public class RealSubject implements Subject{
    @Override
    public String operation() {
        log.info("실제 객체 호출");
        sleep(1000);
        return "data";
    }

    private void sleep(int i) {
        try {
            Thread.sleep(i);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

@Slf4j
public class CacheProxy implements Subject {

    private Subject target;
    private String cacheValue;

    public CacheProxy(Subject target) {
        this.target = target;
    }

    @Override
    public String operation() {
        log.info("프록시 호출");
        if (cacheValue == null) {
            cacheValue = target.operation();
        }
        return cacheValue;
    }
}

 

public class ProxyPattenClient {
    private Subject subject;

    public ProxyPattenClient(Subject subject) {
        this.subject = subject;
    }

    public void execute() {
        subject.operation();
    }
}

 

public class ProxyPattenTest {
    @Test
    void noProxyTest() {
        RealSubject realSubject = new RealSubject();
        ProxyPattenClient client = new ProxyPattenClient(realSubject);
        client.execute();
        client.execute();
        client.execute();
    }
    //noProxy 결과 (캐시에 저장되지 않고 계속 호출)
    /*
    실제 객체 호출
    실제 객체 호출
    실제 객체 호출
    */

    @Test
    void proxyTest() {
        RealSubject realSubject = new RealSubject();
        CacheProxy cacheProxy = new CacheProxy(realSubject);
        ProxyPattenClient client = new ProxyPattenClient(cacheProxy);
        client.execute();
        client.execute();
        client.execute();
    }
    //Proxy 결과 (캐시에 저장되어 실제 객체까지 가지 않고 프록시에서 찾아옴)
    /*
    프록시 호출
    실제 객체 호출
    프록시 호출
    프록시 호출
    */
}

 

데코레이션 패턴

Interface 구성의 프록시 패턴에서 프록시를 겹겹이 쌓는 패턴으로

인터페이스를 구현한 구현체를 다시 Interface로 다른 프록시에 넣어 겹겹이 쌓는다.

 

 

데코레이션 예시)

@Slf4j
public class MessageDecorator implements Subject {
    private Subject subject;

    public MessageDecorator(Subject subject) {
        this.subject = subject;
    }


    @Override
    public String operation() {
        log.info("timeDeco 실행");
        long start = System.currentTimeMillis();
        String result = subject.operation();
        long end = System.currentTimeMillis();
        long resultTime = end - start;
        log.info("timeDeco 종료 resultTime ={}ms",resultTime);

        return result;
    }
}

 

@Test
void deco2() {
    RealSubject realSubject = new RealSubject();
    Subject cacheProxy = new CacheProxy(realSubject);
    Subject timeDeco = new TimeDecorator(cacheProxy);
    ProxyPattenClient client = new ProxyPattenClient(timeDeco);
    client.execute();

    // 데코레이션 패턴 결과
    /*
    timeDeco 실행
    CacheProxy 실행
    실제 객체 호출
    timeDeco 종료 resultTime =8ms
    */
}

 

AOP에서의 관점 분리는 이와 같은 패턴으로 되어 있으며 프록시 패턴으로 인해 관심사를 분리하고 역할을 나누었지만

많은 클래스를 작성해야 한다는 단점(만일 subject만 아니라 다른 클래스도 proxy를 적용하려면 그 인터페이스를 implents 하는 클래스를 그만큼 작성해야 함)이 있다.

 

이러한 단점을 동적 프록시 기술로 해결하였다.

동적 프록시 기술은 InvocationHandler 인터페이스를 상속하여 Object를 target으로 받고

target으로 받은 Object의 class 내부의 메서드를 호출하도록 하여 하나의 프록시 클래스에서 여러 타입의 객체(인터페이스를 상속받은 구현체)를 받을 수 있게 하였다.

 

더욱 상세한 내용과 Spring 프레임 워크의 자동 프록시 생성에 대해서는 다음 포스팅에서 더 자세하게 정리할 예정이다.

 

코드 출처

인프런 / 김영한 / 스프링 핵심원리, 고급편 

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8