스프링을 사용하거나 공부하면서 굉장히 많은 듣게 되는 몇가지 단어가 있다.
DI, 스프링 컨테이너, 싱글톤, Bean, 프록시등등
이 중에서 오늘은 스프링에서 많이 사용되어 있는 프록시 패턴에 대해서 자세히 알아보도록 하겠다.
A가 B에게 요청한다고 가정해보자.
프록시 패턴은 A의 요청을 B가 바로 받는 것이 아니라, 프록시가 해당 요청을 대신 받아서 B에게 넘겨주는 중개자 역할을 하는 동작 방식을 의미한다.
'B에게 바로 요청하면 되는데, 왜 굳이 프록시를 거치지?'라고 생각할 수도 있는데, 프록시를 사용하는 이유는 사실 간단하다.
프록시는 가로챈 요청과 응답을 갖고 추가적인 처리를 해줄 수 있기 때문이다.
프록시 패턴이 사용되는 대표적인 예시가 로깅, 에러 트래킹등등이다.
이제, 다양한 프록시 패턴의 예시 중, @Configuration
과 proxyBeanMethod
에 대해서 좀 더 자세히 알아보자.
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";
boolean proxyBeanMethods() default true;
boolean enforceUniqueMethods() default true;
}
@Configuration
어노테이션을 열어보면, 이렇게 생겼다.
다른 스프링 어노테이션들과 마찬가지로, @Component
어노테이션을 달고 있어서 싱글톤 인스턴스로 생성된다.
또, 이름에서도 알 수 있듯이 Bean을 등록하거나 애플리케이션 전반에 걸친 설정을 하고자 할때 사용된다.
여기서 주목할 것이 있는데, 바로 proxyBeanMethod
이다.
proxyBeanMethod
는 프록시 기능을 활성화할 것인지를 선택할 수 있는 옵션이다.
결론 부터 이야기하자면, @Configuration
또는 @Configuration(proxyBeanMethod=true)
와 같이 어노테이션을 사용하면, 그 안에서 생성된 Bean들은 싱글톤을 보장받게 된다.
반대로, @Configuration(proxyBeanMethod=false)
안에서 생성된 Bean들은 싱글톤을 보장받지 못하게 된다. 즉, 동일한 클래스의 Bean을 여러 개 생성하면, 각각이 서로 다른 인스턴스로 만들어진다는 것을 의미한다.
proxyBeanMethod
는 기본 값이 true
이고, 스프링에서 Bean을 싱글톤으로 만들어주는 것은 자연스럽고 당연해서 그냥 사용하고 있었지만, 사실 이러한 동작은 Java의 기본 동작 방식을 벗어난 것이다.
글로만 읽어서는 이해하기 힘드니, @Configuration
의 동작을 직접 확인해보자.
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class MyProxyTest {
// 설정 Component
@Configuration
static class MyConfig {
@Bean
CommonBean commonBean() {
return new CommonBean();
}
@Bean
Bean1 bean1() {
return new Bean1(commonBean());
}
@Bean
Bean2 bean2() {
return new Bean2(commonBean());
}
}
// Bean으로 만들어줄 클래스 #1
@RequiredArgsConstructor
static class Bean1 {
private final CommonBean commonBean;
}
// Bean으로 만들어줄 클래스 #2
@RequiredArgsConstructor
static class Bean2 {
private final CommonBean commonBean;
}
// Bean으로 만들어줄 클래스 #3
static class CommonBean {}
}
먼저, 테스트 클래스를 하나 만들어주고, 그 안에 Bean으로 만들어줄 클래스들과 설정 컴포넌트를 하나 만들어주자.
위 형태는 아마 우리가 스프링을 사용할 때 가장 흔하게 볼 수 있는 @Configuration
사용법일 것이다.
여기서, 우리는 당연하게도 CommonBean
클래스로 생성된 하나의 인스턴스가 Bean1
과 Bean2
에 담겨졌을 것이라고 예상할 수 있다.
즉, CommonBean
클래스가 싱글톤을 보장 받게될 것이라고 예상할 수 있는 것이다.
자, 그럼 정말 싱글톤을 보장받고 있는지 검증해보자.
public class MyProxyTest {
@Test
@DisplayName("싱글톤 검증")
public void proxyCommonMethod() throws Exception {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
ac.register(MyConfig.class);
ac.refresh();
Bean1 bean1 = ac.getBean(Bean1.class);
Bean2 bean2 = ac.getBean(Bean2.class);
Assertions.assertThat(bean1.commonBean).isSameAs(bean2.commonBean);
}
}
이렇게 Bean1과 Bean2에서 꺼낸 CommonBean이 동일한지 비교해보면, 이 둘이 정확히 동일한 인스턴스인 것을 알 수 있고, 이로 인해서 싱글톤이 보장되는 것을 알 수 있다.
사실, Java의 동작방식으로 생각해보면 뭔가 이상한 점이 있다.
Java의 관점에서 봤을 때, commonBean()
메서드는 새로운 CommonBean
인스턴스를 생성해서 리턴하는 메서드이다.
즉, 원칙적으로는 2개의 서로 다른 인스턴스가 생성되어야하는 것이다.
하지만, proxyBeanMethod=true
때문에 Bean을 호출하는 메서드에 대해서는 이전에 생성된 인스턴스가 있다면, 새로운 인스턴스를 만들지 않고 기존의 인스턴스를 반환하도록 동작한다.
즉, Bean들이 모두 싱글톤을 보장받게 된다는 것을 의미한다.
그리고, 이런 역할을 하는 프록시는 빌드 과정에서 스프링이 @Configuration
이 붙은 클래스에 추가해준다.
그렇다면, 이번엔 proxyBeanMethod=false
옵션으로 위와 동일한 테스트 코드를 돌려보자.
그러면 아래와 같이 에러가 발생하게 된다.
메세지에도 나와 있듯이 2개의 인스턴스가 동일하지 않다는 것이다.
즉, 정상적으로 싱글톤이 보장되지 않았음을 의미한다.
그런데, 이 옵션은 왜 필요한 것일까?
엄청 특별한 이유가 있다기 보다는 그냥 서로 다른 인스턴스의 동일한 Bean을 사용할 수 있도록 한 것이다.
프록시를 만들고 사용하는 것 자체도 아무래도 일반 인스턴스를 사용하는 것보다는 비용이 더 나가는 행위여서, 이러한 동작을 개발자에게 선택할 수 있도록 한 것이다.
스프링 생태계는 공부를 해도해도 시간이 지나고 나면, 부족한 나의 모습이 많이 보인다.
그런데 얼마 전에 인프런에 토비 센세의 스프링 부트 강의가 나왔길래, 이 기회를 통해서 스프링 전반에 대해서 복습하는 시간을 가져보려고 한다.
이 글도, 강의에서 학습했던 내용을 다시 복습하며 작성해보았다. ㅎㅎ
완강할 때 까지 계속 부족했던 부분들이나 새로 알게된 지식들을 블로그 글로 남겨보도록 하겠다.
그럼 이만 오늘은 마치겠다! 😊