6 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

구체 클래스 기반 프록시 - 예제1

해당 위치에 ConcreteLogic 클래스를 만든다.

1
2
3
4
5
6
7
@Slf4j  
public class ConcreteLogic {  
    public String operation() {  
        log.info("ConcreteLogic 실행");  
        return "data";  
    }  
}

다음에 보이는 ConcreteLogic은 인터페이스가 없고 구체 클래스만 있다.

이렇게 인터페이스가 없어도 프록시를 적용할 수 있을까?

지금 사진에서는 클라이언트가 직접 로직 클래스를 실행한다.

이제 클라이언트 코드를 작성하자.

hello.proxy.pureproxy.concreteproxy.code.ConcreteClient

1
2
3
4
5
6
7
8
9
10
11
12
public class ConcreteClient {  
  
    private ConcreteLogic concreteLogic;  
  
    public ConcreteClient(ConcreteLogic concreteLogic) {  
        this.concreteLogic = concreteLogic;  
    }  
  
    public void execute() {  
        concreteLogic.operation();  
    }  
}

생성자로 ConcreteLogic을 받아서 실행하는 아주 간단한 코드이다.

테스트 코드를 작성해 보자.

hello.proxy.pureproxy.concreteproxy.ConcreteClientTest

1
2
3
4
5
6
7
8
9
class ConcreteClientTest {  
  
    @Test  
    void noProxy() {  
        ConcreteLogic concreteLogic = new ConcreteLogic();  
        ConcreteClient client = new ConcreteClient(concreteLogic);  
        client.execute();  
    }  
}

자 로직을 만들고 클라이언트를 만들 때 넘겨 줬고 실행하는 코드이다.

이제 여기 사이에 프록시를 어떻게 넣을지 고민을 해보자.

구체 클래스 기반 프록시 - 예제2

클래스 기반 프록시 도입

지금까지 인터페이스를 기반으로 프록시를 도입했다. 그런데 자바의 다형성은 인터페이스를 구현하든, 아니면 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다. 쉽게 이야기 해서 인터페이스가 없어도 프록시를 만들 수 있다는 뜻이다. 그래서 이번에는 클래스 기반으로 상속을 받아서 프록시를 만들어 보자.

클라이언트가 타임 프록시를 사용하고, 타임 프록시는 로직 클래스를 호출한다.

TimeProxy

TimeProxy프록시는 시간을 측정하는 부가 기능을 제공한다.

그리고 인터페이스가 아니라 클래스인 ConcreteLogic을 상속 받아서 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Slf4j  
public class TimeProxy extends ConcreteLogic{  
    private ConcreteLogic concreteLogic;  
  
    public TimeProxy(ConcreteLogic concreteLogic) {  
        this.concreteLogic = concreteLogic;  
    }  
  
    @Override  
    public String operation() {  
        log.info("TimeDecorator 실행");  
        long startTime = System.currentTimeMillis();  
  
        String result = concreteLogic.operation();  
  
        long endTime = System.currentTimeMillis();  
        long resultTime = endTime - startTime;  
        log.info("TimeDecorator 종료 realTime = {}ms", resultTime);  
  
        return result;  
    }  
}
1
2
3
4
5
6
7
@Test  
void addProxy() {  
    ConcreteLogic concreteLogic = new ConcreteLogic();  
    TimeProxy timeProxy = new TimeProxy(concreteLogic);  
    ConcreteClient client = new ConcreteClient(timeProxy);  
    client.execute();  
}

다음과 같이 ConcreteClient의 생성자에 concreteLogic이 아니라 timeProxy를 주입하는 부분이다.

ConcreteClientConcreteLogic을 의존하는데, 다형성에 의해 ConcreteLogic의 자식인 timeProxy도 넣을 수 있는 것이다.

실행 결과

1
2
3
TimeDecorator 실행 
ConcreteLogic 실행 
TimeDecorator 종료 resultTime=1

참고: 자바 언어에서 다형성은 인터페이스나 클래스를 구분하지 않고 모두 적용된다. 해당 타입과 그 타입의 하위 타입은 모두 다형성의 대상이 된다. 자바 언어의 너무 기본적인 내용이지만, 인터페이스가 없어도 프록시가 가능하다는 것을 확실하게 집고 넘어갈 필요가 있다.

구체 클래스 기반 프록시 - 적용

이번에는 앞서 학습한 내용을 기반으로 구체 클래스만 있는 V2 애플리케이션에 프록시 기능을 적용해보자.

해당 위치에 concrete_proxy패키지를 만들어서 상속으로 프록시를 만들어 보자.

OrderRepositoryConcreteProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OrderRepositoryConcreteProxy extends OrderRepositoryV2 {  
  
    private final OrderRepositoryV2 target;  
    private final LogTrace logTrace;  
  
    public OrderRepositoryConcreteProxy(OrderRepositoryV2 target, LogTrace logTrace) {  
        this.target = target;  
        this.logTrace = logTrace;  
    }  
  
  
    @Override  
    public void save(String itemId) {  
        TraceStatus status = null;  
        try {  
            status = logTrace.begin("OrderRepositoryV2.request()");  
            target.save(itemId);  
            logTrace.end(status);  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
}

인터페이스가 아닌 OrderRepositoryV2클래스를 상속 받아서 프록시를 만든다.

OrderServiceConcreteProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OrderServiceConcreteProxy extends OrderServiceV2 {  
  
    private final OrderServiceV2 target;  
    private final LogTrace logTrace;  
  
    public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {  
        super(null);  
        this.target = target;  
        this.logTrace = logTrace;  
    }  
  
    @Override  
    public void orderItem(String itemId) {  
        TraceStatus status = null;  
        try {  
            status = logTrace.begin("OrderServiceV2.orderItem()");  
            target.orderItem(itemId);  
            logTrace.end(status);  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
}

자 여기서 중요한 점이 있다.

1
2
3
4
5
public OrderServiceConcreteProxy(OrderServiceV2 target, LogTrace logTrace) {  
        super(null);  // <------- 이놈
        this.target = target;  
        this.logTrace = logTrace;  
    }

클래스 기반 프록시의 단점

super(null) : OrderServiceV2 자바 기본 문법에 의해 자식 클래스를 생성할 때는 항상 super()로부모 클래스의 생성자를 호출해야 한다. 이 부분을 생략하면 기본 생성자가 호출된다.

그런데 부모 클래스인 OrderServiceV2는 기본 생성자가 없고, 생성자에서 파라미터 1개를 필수로 받는다. 따라서 파라미터를 넣어서 super(??)를 호출해야 한다.

하지만 프록시는 부모 객체를 사용하는게 목적이 아니기 때문에 부모 객체(OrderRepositoryV2)가 필요가 없다. 그렇기 때문에 super(null)을 넣어줘도 상관이 없다.

인터페이스 기반 프록시는 이런 고민을 하지 않아도 된다.

OrderControllerConcreteProxy

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
public class OrderControllerConcreteProxy extends OrderControllerV2 {  
  
    private final OrderControllerV2 target;  
    private final LogTrace logTrace;  
  
    public OrderControllerConcreteProxy(OrderControllerV2 target, LogTrace logTrace) {  
        super(null);  
        this.target = target;  
        this.logTrace = logTrace;  
    }  
  
    @Override  
    public String request(String itemId) {  
        TraceStatus status = null;  
        try {  
            status = logTrace.begin("OrderControllerV2.request()");  
            String request = target.request(itemId);  
            logTrace.end(status);  
            return request;  
        } catch (Exception e) {  
            logTrace.exception(status, e);  
            throw e;  
        }  
    }  
  
    @Override  
    public String noLog() {  
        return target.noLog();  
    }  
}

컨트롤러도 서비스와 마찬가지로 super(null)을 해줬다.

이제 이 프록시들을 bean 으로 등록해보자.

ConcreteProxyConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration  
public class ConcreteProxyConfig {  
  
    @Bean  
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {  
        OrderControllerV2 controllerImpl = new OrderControllerV2(orderServiceV2(logTrace));  
        return new OrderControllerConcreteProxy(controllerImpl, logTrace);  
    }  
  
    @Bean  
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {  
        OrderServiceV2 serviceImpl = new OrderServiceV2(orderRepositoryV2(logTrace));  
        return new OrderServiceConcreteProxy(serviceImpl, logTrace);  
    }  
  
    @Bean  
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {  
        OrderRepositoryV2 repositoryImpl = new OrderRepositoryV2();  
        return new OrderRepositoryConcreteProxy(repositoryImpl, logTrace);  
    }  
}

인터페이스 대신에 구체 클래스를 기반으로 프록시를 만든다는 것을 제외하고는 기존과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//@Import(AppV1Config.class)  
//@Import({AppV1Config.class, AppV2Config.class})  
//@Import(InterfaceProxyConfig.class)  
@Import(ConcreteProxyConfig.class)  
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의  
public class ProxyApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(ProxyApplication.class, args);  
    }  
  
    @Bean  
    public LogTrace logTrace() {  
       return new ThreadLocalLogTrace();  
    }  
}

마지막으로 @Import(ConcreteProxyConfig.class)해서 등록하면 된다.

실행해보면 동일하게 작동하는 것을 볼 수 있다.

인터페이스 기반 프록시와 클래스 기반 프록시

프록시를 사용한 덕분에 원본 코드를 전혀 변경하지 않고, V1, V2 애플리케이션에 LogTrace기능을 적용할 수 있었다.

인터페이스 기반 프록시 vs 클래스 기반 프록시

  • 인터페이스가 없어도 클래스 기반으로 프록시를 생성할 수 있다.
  • 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다. 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇 가지 제약이 있다.
    • 부모 클래스의 생성자를 호출해야 한다(super(null))
    • 클래스에 final 키워드가 붙으면 상속이 불가능하다.
    • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. (final은 자바 에서 수정할 수 없는 .. 마지막)

이렇게 보면 인터페이스 기반의 프록시가 더 좋아 보이고 실제로도 맞다. 인터페이스 기반의 프록시는 상속이라는 제약에서 자유롭다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다.

인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 그 자체이다. 인터페이스가 없으면 인터페이스 기반 프록시를 만들 수 없다.


이론적으로는 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 이렇게 하면 역할과 구현을 나누어서 구현체를 매우 편리하게 변경할 수 있다. 하지만 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.

인터페이스를 도입하는 것은 구현을 변경할 가능성이 있을 때 효과적인데, 구현을 변경할 가능성이 거의 없는 코드에 무작정 인터페이스를 사용하는 것은 번거롭고 그다지 실용적이지 않다.

이런 곳에는 실용적인 관점에서 인터페이스를 사용하지 않고 구체 클래스를 바로 사용하는 것이 좋다고 생각한다. (물론 인터페이스를 도입하는 다양한 이유가 있다. 여기서 핵심은 인터페이스가 항상 필요하지는 않다는 것이다.)

실무에서는 프록시를 적용할 때 V1처럼 인터페이스도 있고, V2처럼 구체 클래스도 있다. 따라서 두 가지 상황 모두 대처 방법을 알아 두는 것이 좋다.

너무 많은 프록시 클래스

지금까지 프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다.

그런데 문제는 프록시 클래스를 너무 많이 만들어야 한다는 점이다.

잘 보면 프록시 클래스가 하는 일은 LogTrace를 사용하는 것인데, 그 로직이 모두 똑같다.

대상 클래스만 다를 뿐이다. 만약 적용해야 하는 대상 클래스가 100개라면 프록시 클래스도 100개를 만들어야한다.

프록시 클래스를 하나만 만들어서 모든 곳에 적용하는 방법은 없을까? 바로 다음에 설명할 동적 프록시 기술이 이 문제를 해결해준다.

댓글남기기