Runtime 시점에서 사용할 Instance를 선택하고 동작시킬 수 있는 유연성을 제공한다.
특정 객체를 감싸 추가적인 기능을 제공할 수 있다. RTW : JDK Dynamic Proxy
단점
Compile time에 Type, Exception 등의 검증을 진행할 수 없다. Runtime에서 가져오기때문
Runtime에서 Instance가 선택되기 때문에 해당 로직의 구체적인 동작 흐름을 파악하는 것에 대해 어려움을 가지게 된다.
Private 접근 제어자로 캡슐화된 필드, 메서드에 대해 접근 가능하기 때문에 기존 동작을 무시하고 깨트리는 행위가 가능해진다. Singleton 객체, Internal API 사용 등
Java 보안 관리자에게 Runtime 때 특정 권한을 지정받게 되는데, 이는 Linux의 Root 계정처럼 보안 취약점을 만들고, 제약 사항을 위반할 수 있다.
Reflection 성능 이슈?
"Java Reflection API가 느리고 높은 비용을 사용한다"라는 이야기는 흔히 듣게 되는 이야기이며, 이는 Reflection API의 Method Invoke() 실행 시간을 측정하는 ( 정적 메서드 디스패치와 비교하는 ) 많은 테스트들에서 나타나는 결과이다.
하지만 이는 Reflection만을 테스트하는 것이 아니라, 동적으로 Class를 Load 하고, Heap에 객체를 띄우는 선행 절차가 존재하기에 나타나는 결과이다.
그렇다고 Reflection API가 느리지 않고, 동일한 비용을 사용한다는 것은 아니다. 그러한 이유 중 하나로는 Reflection을 통한 초기 호출 시 JVM이 해당 정보를 미리 최적화할 수 없기 때문이다.
JIT Compiler의 Bytecode Caching, Opcode Optimization.. 등
즉 초기 호출 이후로는 캐싱을 통해서 Reflection API를 통한 메서드 호출도 최적화된다는 것을 의미한다.
초기 호출에서는 5배 이상의 차이를 보이더라도 이후 호출부터는 그러한 간격이 줄어들게 된다. 하지만 setAccessible과 같은 Class 정보 설정 기능을 사용하는 경우에는 그렇지 않을 수 있다.
2월 8일 추가
테스트 코드
public class ReflectionTest {
// get class name
// Reflection API 19~25ms
@Test
public void reflectionTest_getClassName() throws ClassNotFoundException {
// instance을 생성하지 않고 Metaspace 영역의 Type 정보를 가져온다.
for (int i = 0; i < 1000; i++) {
String name = Class.forName("notefive.oop.Animal").getSimpleName();
System.out.println(name);
}
}
// Non-Reflection API 17~22ms
@Test
public void reflectionTest_getName() {
// 생성된 instance를 기반 즉 Heap 영역의 Type 정보를 가져온다.
Animal animal = new Animal();
for (int i = 0; i < 1000; i++) {
String name = animal.getClass().getSimpleName();
System.out.println(name);
}
}
// method invoke
// Reflection API 34~46ms
@Test
public void reflectionTest_getClassAndGetMethod() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
for (int i = 0; i < 1000; i++) {
Object o = Class.forName("notefive.oop.Animal").newInstance();
Method getName = Class.forName("notefive.oop.Animal").getMethod("getName");
String res = (String) getName.invoke(o);
}
}
// Non-Reflection API 27~32ms
@Test
public void reflectionTest_getMethod() {
for (int i = 0; i < 1000; i++) {
String res = new Animal().getName();
}
}
}
Reflection API의 경우 초기 호출 시에 JVM의 Class Loader와 Excuter Engine을 통해 Class의 Metadata를 가져온 이후에는 Non-Reflection 방식과 동일하게 동작한다. 매번 인스턴스를 생성하고 메서드를 호출하는 절차가 진행하게 된다.
즉 초기 호출을 제외하고는 Reflection API를 사용하는 것이 별 차이가 없음을 알게 되었다.
특히 이미 Class Compile시 Loading 된 Class 정보를 Reflection API를 통해 가져오는 경우에는 기존 방식과 비교하여 오버헤드가 사실상 존재하지 않았다.
이러한 결과를 보았을 때 단순히 성능이 좋지 않다는 이야기 때문에 도입하지 못했던 것에 대해 Reflection API를 고려해볼 수 있는 어떠한 지표를 얻게 된 것 같다.
위에 작성된 getClassName 관련 테스트에선 Reflection API가 인스턴스를 생성하지 않고 정보를 가져오기 때문에, 인스턴스를 생성하여 정보를 가져오는 Non-Reflection API 방식이 일정 반복 횟수 이후에 GC가 발생하여 상대적으로 느린 실행시간을 보였었다.
서비스 메서드에 Transcational을 사용하였을 때와 사용하지 않았을 때, 흐름을 정리해보았습니다.
스프링 @Transaction 미적용 (JDBC API - Local Transaction)
기본적으로 JDBC의 트랜잭션은 하나의 Connection Instance를 생성하고 통신하며 종료하는 흐름과 같이 동작하게 된다.
즉 코드에 존재하는 DAO 로직들은 각각의 트랜잭션 안에서 연산을 진행하게 되는 것이다.
이때의 트랜잭션을 로컬 트랜잭션이라고 한다.
해당 메서드 내에서는 3개의 트랜잭션이 동작하며, 이는 내부에서 하나의 DAO 로직이 실패하더라도 다른 로직들은 성공하고 반영될 수 있는 상태임을 의미한다.
각각의 로직들은 Connection Pool에서 리소스 전달받아 새로운 Connection을 생성하며, Auto-Commit을 진행한다.
트랜잭션을 적용했다면?
단순하게 생각해본다면, 여러 질의를 포함하는 트랜잭션을 구성하기 위해서 하나의 커넥션을 생성하고 Auto-commit을 false 처리한 뒤 이 커넥션을 재사용을 하면 될 것 같다.
Spring에서는 이를 구현하는 방법을 Transaction Synchronization이라고 한다.
Transaction Synchronization?
개념 정립을 위하여 만든 이미지이므로 실제 내용과는 다를 수 있습니다.
트랜잭션을 시작하기 위해 사용할 Connection 객체를 저장소 역할을 하는 Connection Holder에 보관하고, 이후 호출 로직들에 대해서 매번 Connection을 생성하고 사용하는 것이 아니라 해당 Connection만을 꺼내어 재사용하게끔 한다.
트랜잭션 동기화를 적용한 JDBC Template work flow
DAO의 호출을 위한 connection을 트랜잭션 경계 상단에서 생성한다.
해당 connection 객체를 TransactionSynchornizationManager 내부의 참조 변수인 connectionHolder 객체에 저장한다.
connection의 Auto-commit 설정 값을 false로 설정한다.
DAO의 메서드가 호출되면 우선 Manager 내부의 Holder 객체에 connection이 있는지 확인한다.
저장되어 있는 Connection을 가져오고 Statement 객체를 생성하여 쿼리를 전송한다. 그리고 연산 종료 시 해당 connection을 종료시키지 않고 열어둔다
위와 같은 연산을 진행하며 Runtime Exception이 발생하면 connection 객체의 RollBack을 실행하고 그렇지 않은 경우 commit을 실행한다.
Spring Transcation Synchronization Interface
TransactionSynchronizationManager
스프링에서 제공하는 Transaction Synchronization 용 Manager Class이다.
선언적 트랜잭션을 이용하게 될 경우 트랜잭션 경계의 맨 첫 부분에서 initSynchronization()를 호출하여 트랜잭션 동기화 작업을 진행한다.
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
/**
* Return if transaction synchronization is active for the current thread.
* Can be called before register to avoid unnecessary instance creation.
* @see #registerSynchronization
*/
public static boolean isSynchronizationActive() {
return (synchronizations.get() != null);
}
/**
* Activate transaction synchronization for the current thread.
* Called by a transaction manager on transaction begin.
* @throws IllegalStateException if synchronization is already active
*/
public static void initSynchronization() throws IllegalStateException {
if (isSynchronizationActive()) {
throw new IllegalStateException("Cannot activate transaction synchronization - already active");
}
logger.trace("Initializing transaction synchronization");
synchronizations.set(new LinkedHashSet<>());
}
해당 작업은 해당 스레드 내부에서만 사용될 값을 저장하는 ThreadLocal 객체에 LinkedHashset 컬랙션 객체를 저장하고, 이 안에는 트랜잭션 동기화 설정과 관련된 필드를 가지는 TransactionSynchronization 타입의 객체를 저장한다.
내부에 정의된 클래스 패스들을 따라가게 되면 @ConditionalXXX를 통해 각각의 조건을 검증하여 통과하는 경우에 등록하는 방식으로 선언되어 있다.
ex) DispatcherServletAutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
/**
* The bean name for a DispatcherServlet that will be mapped to the root URL "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
/**
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
// DispatcherServletRegistrationConfiguration...
// DefaultDispatcherServletCondition...
// DispatcherServletRegistrationCondition...
}
EnableAutoConfiguration 흐름 요약
Context가 초기화되면 등록된 BeanPostProcessor들을 이용하여 BeanDefinition들을 읽어오게 됩니다. 이때 AutoConAutoConfigurationPackage와 연결된 AutoConfigurationPackage.class의 정적 클래스인 registrar가 Spring.factory파일의 클래스 패스 정보를 통해 Configuration Class들을 모두 가져와 저장하게 되고, 이것이 ConfigurationClassBeanDefinitionReader를 통해서 읽어져 온 뒤 ConfigurationClassParser에서 Condition 구현체를 통한 조건 검증을 진행하고 통과한 Bean Definition에 대해서만 ImportStack에 추가하여 BeanPostProcessor가 해당 정보를 싱글톤 형식의 Bean으로 등록하게 됩니다.
@Configuration
해당 어노테이션이 작성된 클래스가 Java Config 기반의 Bean Definition 으로 사용되는 것임을 나타낸다.
내부적으로 @Component라는 Meta-Annotation이 정의되어 있으므로 ComponentScan을 통해 탐색되어 읽어 들일 수 있으며, 이는
내부의 processConfigBeanDefinitions() 메서드를 호출한 뒤 ConfigurationClassParser 를 통해 읽어 들여진 Configuration Class들을 ConfigurationClass 타입으로 파싱 한다.
해당 정보를 ConfigurationClassBeanDefinitionReader의 loadBeanDefinitions() 메서드에 전달하여 순회한다.
순회되는 정보들을 loadBeanDefinitionsForConfigurationClass() 메서드를 통해 정보를 읽어 들어와 Bean Definition으로 등록한다.
이후 ConfigurationClass 객체들을 alreadyParsed에 저장하고 다음 로직을 진행한다.
의 흐름을 가지고 등록되게 된다.
@EnableAutoConfiguration에 포함된 AutoConfigurationImportSelector.class, @AutoConfigurationPackage의 흐름 따라가 보기
Import(AutoConfigurationImportSelector.class)
META-INF/spring.factories에 존재하는 정보들을 가져와 등록하는 클래스이다.
Main 메서드가 호출되고 run 메서드가 실행된다.
run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.
넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.
메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.
currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다.
내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행하는 중 postProcessBeanDefinitionRegistry → processConfigBeanDefinitions에 regisrar 정보를 넘긴다.
processConfigBeanDefinitions 내부에서 BeanDefinitionHolder 정보를 담는 ArrayList인 configCandidates 객체를 생성한 뒤 해당 객체의 요소를 추가하는 작업을 진행한다.
→ 전달받은 registry가 가지고 있는 DefinitionNames 들을 가져와 순회하면서 BeanDefinition 정보와 Bean Name을 꺼내오고 그것을 저장하는 BeanDefinitionHolder를 생성하여 저장한다.
configCandidates 객체를 정렬한 뒤 ConfigurationClassParser를 생성하고 정렬된 객체를 Set으로 변환한 다음 parse() 메서드의 인자로 넘긴다.
parse() 메서드 내부에서는 전달받은 Set를 순회하며 가져온 BeanDefinition 정보를 instanceof를 통해 Sub Class Type을 검증한 뒤 해당 정보에 맞게 캐스팅하여 BeanName과 함께 parse()를 호출하며 인자로 넘긴다.
deferredImportSelectorHandler의 process()가 호출된다.
해당 메서드는 deferredImportSelectors 정보를 가져와 정렬한 뒤 forEach를 통해 순회하며 DeferredImportSelectorGroupingHandler register()를 호출하여 해당 정보에 포함된 Annotation Meta 정보와 ConfigurationClass 정보, deferredImport 정보를 호출된 객체 내부에 존재하는 Map에 저장한다.
List<ConfigurationClassParser.DeferredImportSelectorHolder> deferredImports
= this.deferredImportSelectors;
ConfigurationClassParser.DeferredImportSelectorGroupingHandler handler
= ConfigurationClassParser.this.new DeferredImportSelectorGroupingHandler();
deferredImports.sort(ConfigurationClassParser.DEFERRED_IMPORT_COMPARATOR);
deferredImports.forEach(handler::register);
handler.processGroupImports();
private class DeferredImportSelectorGroupingHandler {
private final Map<Object, DeferredImportSelectorGrouping> groupings = new LinkedHashMap<>();
private final Map<AnnotationMetadata, ConfigurationClass> configurationClasses = new HashMap<>();
public void register(DeferredImportSelectorHolder deferredImport) {
Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
(group != null ? group : deferredImport),
key -> new DeferredImportSelectorGrouping(createGroup(group)));
grouping.add(deferredImport);
this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
deferredImport.getConfigurationClass());
}
저장된 정보를 가지고 processGroupImports() 메서드를 호출한다.
앞서 저장된 Map을 순회하면서 getImports() 메서드를 호출한다.
getImports() 메서드는 전달받은 DeferredImportSelectorHolder 타입 객체 정보를 순회한다.
AutoConfigurationImportSelector의 process()를 호출하면서 DeferredImportSelectorHolder의 ConfigurationClass의 Metadate 정보와 importSelector를 전달한다.
주어진 importSelector를 AutoConfigurationImportSelector로 캐스팅하여 getAutoConfigurationEntry() 메서드를 호출한 다음 AutoConfigurationEntry 타입 객체인 autoConfigurationEntry에 저장한다.
저장한 entry의 MetaData를 가져와 ConfigurationClass 타입의 객체를 선언하여 저장한다.
processImports() 메서드를 호출하고 주어진 ConfigurationClass 객체를 importStack이 상속받은 ArrayDeque에 해당 정보를 저장한다.
전달받은 Collection importCandidates 향상된 For문으로 순회한다.
해당 정보는 ImportSelector 또는 ImportBeanDefinitionRegistrar가 아닌 Configuration 정보이기에 해당 정보를 importStack의 LinkedMultiValueMap에 저장하게 된다.
→ 이때 저장되는 정보는 위에서 주어진 ConfigurationClass를 SourceClass로 필터를 거쳐 Converting 된 객체의 Metadata와 enrty의 포함된 Import Class 컬랙션이다.
importCandidates의 요소를 ConfigClass로 변환하고 grouping에 저장되어 있던 exclusionFilter를 전달한 뒤 SourceClass로 컨버팅하고 doProcessConfigurationClass()를 호출한다.
Bean, ComponentScan 등의 어노테이션들 정보가 포함되어 있는지 확인하여 각각의 어노테이션에 맞게 해당 정보를 로딩하거나 파싱 한 뒤 Bean을 생성한다.
@AutoConfigurationPackage?
자동 구성 패키지 정보를 저장하는 AutoConfigurationPackages 클래스의 정보를 가져오는 어노테이션이다.
AutoConfigurationPackages 클래스는 자동 구성 패키지 내의 정의된 Meta-annotation들을 기반으로 정보들을 가져온 뒤 별도의 리스트를 통해 (이후에 참조될) 패키지 이름들을 저장하는 로직을 가지고 있다.
해당 로직을 통해 저장된 패키지 정보들은 이후 beanDefinition으로 저장되게 되는데 이는 Import 된 AutoConfigurationPackages.Registrar.class를 통해 이루어진다.
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
beanDefinition.addBasePackages(packageNames);
}
else {
registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
}
}
PackageImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));
for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {
packageNames.add(basePackageClass.getPackage().getName());
}
if (packageNames.isEmpty()) {
packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
}
this.packageNames = Collections.unmodifiableList(packageNames);
}
EnableAutoConfiguration와 관련된 어노테이션들
@AutoConfigureOrder
ApplicationContext에 전달될 자동 구성 클래스 순서를 정의하는 어노테이션이다.
각 Bean의 의존 관계와 DependsOn 어노테이션을 통해 내부적으로 등록 순서가 결정되며 Bean이 만들어지는 것과 주입하는 순서에 대한 속성과는 별개의 어노테이션이다.
@AutoConfigureAfter
인자로 주어진 자동 구성 클래스가 적용된 뒤에 해당 어노테이션이 적용된 자동 구성 클래스를 적용하게끔 하는 어노테이션이다.
@Import
다른 @Configuration 이 적용된 클래스의 Bean Definition을 명시적으로 컨테이너나 구성 클래스에 적용할 수 있도록 한다.
ComponentScan 어노테이션과 유사한 동작 방식을 가지지만, 다수의 구성 클래스를 적용하기 위해서는 모두 명시적으로 작성하여야 된다.
기본적으로 사용하는 이유는 여러 설정을 하나의 클래스에 작성하지 않고 유사성을 따라 분리하여 정의하고 하나의 구성 클래스에서 정보를 집계하여 사용하기 위함이다.
@Configuration
@Import({ AConfig.class, BConfig.class })
public class GroupConfiguration {
}
Spring Boot가 설정해주는 것들 몇 가지 짚어보기
DataSource 자동 초기화
Main 메서드가 호출되고 run 메서드가 실행된다.
run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.
넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.
메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.
currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다
내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행한다. postProcessBeanDefinitionRegistry → processConfigBeanDefinitions → ConfigurationClassBeanDifinitionReader에 정보를 넘겨 생성한 뒤 loadBeanDefinitions 메서드를 호출한다.
넘겨받은 Set를 순회하며 loadBeanDefinitionsForConfigurationClass를 호출한다. 해당 메서드는 loadBeanDefinitionsFromRegistrars()를 호출하여 registrar 들의 registerBeanDefinitions() 메서드를 호출하는 절차를 진행한다.
DataSourceInitializationConfiguration 내부에 Regisrar의 registerBeanDefinitions()를 호출하고 해당 로직에서 dataSourceInitializerPostProcessor이라는 이름을 가진 PostProcessor 가 존재하지 않는 경우 DataSourceInitializerPostProcessor.class을 등록하는 로직을 실행한다.
이후 Bean 이 등록되어 초기화 절차를 진행하는데 이때 afterPropertiesSet() 메서드를 호출하여 createSchema()를 통해 DB의 Schema를 등록하는 절차를 진행한다.
위의 로직이 성공하면 initialize()를 호출하여 구성된 Schema에 data.sql 정보를 읽어 등록한다.
sql 파일들을 통해 정상적으로 DB가 구성되고 사용할 수 있게 된다.
내용을 계속 추가하고 있습니다.
Spring vs Spring Boot
[의존성 관리와 설정을 자동으로 해준다! - Starter]
스프링 부트는 스프링 프레임워크의 경량화를 위해 분리된 수많은 Jar 파일을 필요에 맞게 등록하고, 관련 설정을 선언하는 절차없이 기본 설정값으로 제공되는 @Configuration Class들을 통해 빠른 실행을 가능케 하고, Bean에 대한 Builder를 통해 사용자 정의도 쉽게 할 수 있게 하여 빠른 개발 환경 구성을 지원하기 위해 만들어진 것입니다.
내장 톰켓에 대해서도 요청을 관리하기 위해 필요했던 매핑 작업, 서블릿 등록, 컨텍스트 설정들을 구성해주고 있기에 바로 실행하여 결과를 확인할 수 있게 되는 것을 알 수 있습니다.
이것이 Spring과 Spring Boot 제일 큰 차이이며, Spring Boot를 사용해야하는 근본적인 이유임을 알 수 있습니다.
PreparedStatement는 Statement가 가지고 있던 불필요한 절차, 리소스 사용, Non-Caching 등의 요소들을 최적화한 하위 클래스이다. DB와의 쿼리 성능이 중요한 Server든 아니든간에 유의미한 성능차이를 보이므로, PreparedStatement를 사용하는 것이 좋다.
객체를 호출하는 지점에서 프록시 객체를 호출하여 부가적인 코드를 수행한 뒤 참조 변수를 통해 실제 서비스 객체를 호출한다.
클라이언트는 프록시를 통해 서비스 객체 기능을 사용하기에
접근 권한
부가기능
리턴 값 변경
로깅
트랜잭션 등
이러한 기능을 추가 할 수 있다.
즉 서비스 객체의 SRP를 해치지 않고 프록시를 통해 책임을 추가할 수 있다.
해당 Pattern의 특징
프록시 객체는 실제 서비스 객체와 같은 이름의 메서드를 구현한다.
프록시 객체는 합성을 통해 서비스 객체에 대한 참조 변수를 가진다.
프록시 객체는 실제 서비스의 메서드를 호출하고 그 결과를 클라이언트에게 반환한다.
프록시 객체는 실제 서비스 메서드의 호출 전, 후에 별도의 로직을 수행할 수 있다.
Pure Java Proxy
우선 실제로 동작할 서비스 객체와 프록시 객체의 타입을 정의한다.
TargetObject Interface
public interface TargetObject {
String someMethod(String name);
}
앞서 선언한 인터페이스를 구현하는 서비스 객체와 프록시 객체의 클래스를 정의한다.
TargetObjectImpl Class
public class TargetObjectImpl implements TargetObject {
@Override
public String someMethod(String name) {
return "Real Subject method "+ name +"\n";
}
}
TargetObjectProxy Class
public class TargetObjectProxy implements TargetObject {
TargetObjectImpl subject;
{
subject = new TargetObjectImpl();
}
@Override
public String someMethod(String name) {
System.out.println("Before proxy");
return subject.someMethod(name) + ("After proxy\n");
}
}
만들어낸 프록시 객체를 호출해보자.
PureProxyClient Class
public class PureProxyClient {
public static void main(String[] args) {
PureProxyClient client = new PureProxyClient();
client.run("Lob");
}
public void run(String name){
TargetObjectProxy proxy = new TargetObjectProxy();
System.out.println(proxy.someMethod(name));
}
}
Before proxy
Real Subject method Lob
After proxy
Dynamic Proxy?
런타임에 특정 인터페이스들을 구현하는 클래스나 인스턴스를 만드는 기술을 말한다.
Spring Data JPA
Spring AOP (Method Invocation을 사용하기에 메서드 래밸만 지원한다.)
Mockito
Hibernate - lazy initialzation
JDK Dynamic Proxy
JDK Dynamic Proxy?
Java 에서 지원하는 동적(런타임) Proxy 구현 방식이다. 특정 인터페이스들을 구현하는 클래스나 인스턴스를 만드는 기술이다.
Reflection API을 사용하여 Target Class의 method를 invoke()를 통해 동작시킨다.
invoke?
클래스의 이름과 인자 값을 넘겨서 객체의 메서드를 실행시키는 메서드이다.
인자 값을 이용하여 메서드를 실행시키게 된다.
제약
인터페이스 기반의 Proxy → 모든 Target Class 는 Interface를 implement 하고 있어야 한다.
JDK Dynamic Proxy 단점
Advise 대상이든 아니든 모든 Method Call 마다 reflection API의 invoke를 실시한다.
즉 Method invoke를 우선 진행하고 Advise 유무를 판단한다.
JDK Dynamic Proxy 를 사용하는 라이브러리
Spring AOP ProxyFactory ( JDK Dynamic Proxy 를 추상화하고 재정의한 Class)
Spring Boot 에서는 CGLIB를 기반으로 Spring AOP를 지원한다고 한다.
Spring Data JPA RepositoryFactorySupport (BeanClassLoaderAware, BeanFactoryAware)
JDK Dynamic Proxy 도 인터페이스 기반의 프록시이므로 이전 코드와 같은 구조를 지니는 인터페이스를 정의한다.
TargetObject Class
public interface TargetObject {
void someMethod(String name);
}
TargetObjectImpl Class
public class TargetObjectImpl implements TargetObject {
@Override
public void someMethod(String name) {
System.out.println("Real Subject Do something " + name);
}
}
리플랙션 API에 포함된 Proxy 클래스의 newProxyInstance 를 사용하여 프록시를 구현한다.
DynamicProxyClient Class
public class DynamicProxyClient {
public static void main(String[] args) {
DynamicProxyClient dynamicProxyClient = new DynamicProxyClient();
dynamicProxyClient.run("Lob");
}
public void run(String name) {
realObject.someMethod(name);
}
TargetObject realObject = (TargetObject) Proxy.newProxyInstance(TargetObject.class.getClassLoader(), new Class[]{TargetObject.class},
(InvocationHandler) (proxy, method, args) -> {
TargetObject targetObject = new TargetObjectImpl();
System.out.println("Before Proxy");
Object invoke = method.invoke(targetObject, args);
System.out.println("After Proxy");
return invoke;
});
}
Before Proxy
Real Subject Do something Lob
After Proxy
코드 생성 라이브러리(Code Generator Library), 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다.
장점
실제 바이트 코드를 조작하여 JDK Dynamic Proxy 보다 상대적으로 빠르다.
대상 객체가 인터페이스를 가지지 않았을 경우 사용한다.
인터페이스를 가져도 사용할 수 있다. aop:config의 proxy-target-class를 true로 설정.
대상 객체가 정의한 모든 메서드를 프록시 하여야하는 경우 사용한다.
제약
버전별 호환성이 좋지 않다. (사용 라이브러리 내부에 내장시켜 제공한다.)
final이나 private 같이 상속된 객체에 Overriding을 제공하지 않는다면 해당 행위에 대해서 Aspect를 적용할 수 없다.
CGLIB Dynamic Proxy 를 사용하는 라이브러리
Spring Boot AOP (Proxy)
Hibernate
TargetObject Class
public class TargetObject {
void someMethod(String name) {
System.out.println("Real Subject Do something " + name);
};
}
CglibDynamicProxyClient Class
public class CglibDynamicProxyClient {
public static void main(String[] args) {
CglibDynamicProxyClient client = new CglibDynamicProxyClient();
client.run("Lob");
}
public void run(String name) {
MethodInterceptor methodInterceptor = new MethodInterceptor() {
final TargetObject targetObject = new TargetObject();
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before Proxy");
Object invoke = method.invoke(targetObject, args);
System.out.println("After Proxy");
return invoke;
}
};
TargetObject targetObject = (TargetObject) Enhancer.create(TargetObject.class, methodInterceptor);
targetObject.someMethod(name);
}
}
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by net.sf.cglib.core.ReflectUtils$1 (file:/C:/Users/serrl/.gradle/caches/modules-2/files-2.1/cglib/cglib/3.3.0/c956b9f9708af5901e9cf05701e9b2b1c25027cc/cglib-3.3.0.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of net.sf.cglib.core.ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Before Proxy
Real Subject Do something Lob
After Proxy
외부, 다른 도메인의 API를 통해 데이터를 제공받기 위해 사용할 수 있는 인터페이스를 조사해보았다.
RestTemplate?
Spring에서 제공하는 HTTP API 호출 시 사용할 수 있는 인터페이스이다.
멀티스레드 환경에서 안전하며 여러 기능을 지원한다.
내부적으로 하나의 요청에 하나의 쓰레드를 생성하여 동작시키는 Java Servlet API를 사용한다.
이는 각각 스레드가 일정량의 메모리와 CPU주기를 사용하고, 응답을 받을 때까지 스레드가 차단되는 동기화 모델이다.
비동기 방식의 동작을 제공하는 AsysncRestTemplate 도 있지만, WebClient 라는 인터페이스가 생겨남으로써 최적화된, 최신의 기술을 제공하기에 필요성을 잃어 deprecated 되었고, RestTemplate 은 기존에 사용되었던 프로젝트가 많이 존재하기에 유지보수만을 제공하게 되었다.
→ 스프링 5.0 이상의 프로젝트에서는 WebClient를 사용함을 권장한다.
Spring Boot 에서 제공하는 자동 구성 빈 (dataSource, SqlSessionFactory 등의 설정들) 에는 포함되지 않지만, 해당 인터페이스를 인스턴스로 생성하여 제공해줄 수 있는 RestTemplateBuilder을 제공한다.
기본 사용 방식
@Component
public class ApiConfiguration{
private final RestTemplate restTemplate;
@Autowired
public RestExample(RestTemplateBuilder builder){
restTemplate = builder.build(); // 아무 설정없는 RestTemplate 설정
}
// returnClassType exam : String.class , User.class....
public String findOne() {
return restTemplate.getForObject(apiUrlPath, returnClassType))
}
}
RestTemplate 를 재정의하기
사용 목적에 따라서 재정의를 하는 방법은 3가지가 있다고 한다.
필요한 순간에 재정의하는 방법
자동 구성된 RestTemplateBuilder 빈을 주입받아 메서드가 호출되는 시점에 생성하는 방법이다. 이때 빌더를 이용해 추가적인 기능을 정의하여 새로운 인스턴스로 반환한다.
private final RestTemplate restTemplate;
public MyService(RestTemplateBuilder restTemplateBuilder) {
this.restTemplate = restTemplateBuilder.build();
}
public Post someMethod() {
return restTemplate.getForObject("/{name}/details", Post.class, name);
}
애플리케이션 전체에 적용되도록 하는 방법
RestTemplateCustomizer라는 빈을 재정의하여 추가하는 방법이다. RestTemplateBuilder 에도 변경된 내용이 반영되므로 인스턴스를 생성하여 반환하면 된다.
public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {
@Override
public void customize(RestTemplate restTemplate) {
// Do someThing
}
}
@Bean
public CustomRestTemplateCustomizer customRestTemplateCustomizer() {
return new CustomRestTemplateCustomizer();
}
RestTemplateBuilder 빈을 재정의하는 방법
구성의 우선 순위로 인하여 자동 구성으로 되는 RestTemplateBuilder 빈을 사용하지 않고 수동으로 재정의한 것을 사용함으로써 변경된 인스턴스를 반환하게 된다.
지정된 URL에 대한 GET을 수행하고 응답이 있는 경우에 해당 Resource를 반환한다.
// URL에 제공될 PathVarialbe에 대하여서 값을 전달할 수 있다.
String result = restTemplate.getForObject(
"http://example.com/Location/{locationId}/Posts/{postId}", String.class
, "42", "21");
add(String, String) : 주어진 헤더 이름과 그에 따른 값을 추가한다. (Map 형식)
set(String, String) : 특정 헤더가 가진 값을 해당 문자열 값으로 변경한다.
→ 그외에도 containsKey, addAll, equals, getAccept 등의 편의 메서드를 제공한다.
HTTP OPTIONS
optionsForAllow()
지정된 URL에 대한 Allow 헤더의 값을 반환한다.
Set<HttpMethod> allowedMethod = template.optionsForAllow(
new URI("http://example.com/Posts" + "/get"));
Allow Header?
해당 리소스가 지원하는 메서드의 집합을 나열해주는 헤더이다.
서버에서 405 Method Not Allowed를 응답할 경우 해당 헤더를 무조건 보내야 한다.
만약 해당 헤더가 비어있다면 어떠한 요청 메서드이든 간에 허용되지 않음을 의미한다.
HTTP POST
postForLocation()
주어진 HttpEntity를 URL에 요청하여 리소스를 만들고 Location 헤더 값을 반환받는다.
해당 결과 값은 일반적으로 리소스가 저장된 위치(URI)를 제공한다.
HttpEntity<String> requestEntity = new HttpEntity<>("Lob!");
URI location = restTemplate.postForLocation(url, request);
HttpEntity?
헤더 및 본문으로 구성된 HTTP 요청, 응답 엔티티를 나타내는 인터페이스이다.
RequestEntity, ResponseEntity 의 상위 클래스이다. (구현체 들)
postForObject()
주어진 객체를 URL에 요청하여 리소스를 만들고 응답을 해당 리소스로 받는다.
Post savedPost = restTemplate.postForObject("http://example.com/Posts",
post, Post.class);
postForEntity()
주어진 객체를 URL에 요청하여 리소스를 만들고 응답을 ResponseEntity로 받는다.
ResponseEntity<Post> responsePost = restTemplate.postForEntity(
"http://example.com/Posts", post, Post.class)
HTTP PUT
put()
주어진 요청 객체를 URL으로 PUT 요청을 보내어서 새로운 리소스를 생성한다.
restTemplate.put("http://example.com/Posts/{postId}",
new Post("ChangeTitle", "ChangeContent"), post.getId());
Any Method
exchange()
지정된 URI 템플릿에 대하여 HTTP 메서드를 실행한다.
RequestEntity를 사용하여 요청하며 반환 값은 ResponseEntity로 제공받는다.
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Accept", MediaType.APPLICATION_JSON_VALUE);
HttpEntity<String> requestEntity = new HttpEntity<>(requestHeaders);
ResponseEntity<Post> = restTemplate.exchange(
"http://example.com/Posts/{postId}", HttpMethod.GET,
requestEntity, Post.class, "10");
execute()
지정된 URI 템플릿에 대하여 HTTP 메서드를 실행한다.
Request Callback으로 요청을 받고, Response Extractor로 응답을 읽는다.
Post request = new Post("title","content");
RequestCallback requestCallback = httpEntityCallback(request);
execute(url, HttpMethod.PUT, requestCallback, null)
모든 메서드는 RestClientException를 던지게 된다.
Traverson
Spring HATEOAS 라이브러리에서 제공하는 하이퍼미디어 API이다.
HTTP Method를 지원하지 않고, 하이퍼 링크를 이용하여 다른 URL로 이동하게 된다.
기본적으로 Traverson 인스턴스에 API의 기본 URI와 Accept 헤더를 설정할 수 있다.
Traverson traverson = new Traverson(URI.create("http://example.com/")
, MediaTypes.HAL_JSON);
Traverson에서 제공하는 메서드
follow
Traverson에 Customizing 된 정보로 리소스 간의 단일 관계에 대하여 설정한다.
결과 값의 타입은 Traverson.TraversalBuilder이다.
Traverson.TraversalBuilder builder = traverson.follow("self");
// 추가적으로 사용할 수 있는 메서드
// Map<String, ?> 형식의 Key-Value를 전달할 수 있다.
Traverson.TraversalBuilder builder = traverson.follow("self")
.withTemplateParameters(parameters);
// 응답에 따른 결과를 지정하는 타입 형식으로 마샬링하는 메서드
String response = traverson.follow("self").withTemplateParameters(parameters)
.toObject(String.class);
// .toObject("$.title"); 속성 값 사용 가능
setRestOperations
사용할 RestOperations를 구성한다. null이 제공된 경우 RestTemplate를 사용한다.
traverson.setRestOperations(null);
RestOperations?
RESTful 작업의 기본적인 작업 단위를 지정한 인터페이스이다.
주 구현체로는 RestTemplate이 있으며 직접 사용되는 경우는 거의 없지만, 쉽게 모킹 하거나 스터빙을 할 수 있음으로써 테스트의 확장성을 향상하는데 유용하게 사용될 수 있다.
setLinkDiscoverers
사용할 링크 검색기를 설정한다. 기본적으로 단일 HalLink Discoveryer가 등록된다.
traverson.setLinkDiscoverers(null);
WebClient
Spring Flux 라이브러리에서 제공하는 API 호출 인터페이스.
내부적으로 이벤트 루프 방식을 구현한 Reactive Streams API를 사용한다.
WebClient를 재정의하기
재정의를 하는 방법은 크게 3가지가 있다고 한다.
필요한 순간에 재정의하기
WebClient.Builder를 주입받아서 메서드를 호출하는 시점에서 생성한다. 해당 변경사항은 해당 애플리케이션에서 생성되는 모든 클라이언트에 반영되게 된다.
private final WebClient webClient;
public MyService(WebClient.Builder webClientBuilder) {
webClient = webClientBuilder.baseUrl("http://example.com").build();
// 혹은
webClient = WebClient.builder()
.baseUrl("http://example.com/")
.defaultCookie(cookieKey, cookieValue)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultUriVariables(Collections.singletonMap("url", "http://example.com/"))
.build();
}
public Post someMethod() {
return webClient.get().url("/posts/{postId}", "1")
.retrieve().bodyToMono(Post.class);
}
동일 시점에서 하나의 빌더로 여러 클라이언트를 생성하려는 경우 builder.clone(); 을 통한 빌더 복제를 고려하여야 한다.
public MyService(WebClient.Builder webClientBuilder) {
WebClient.Builder clientBuilder2 = webClientBuilder.clone();
}
WebClientCustomizer 빈을 통한 커스텀
WebClient 인스턴스 생성 방식 이용하기
이 방식의 경우 Auto Configuration이나 WebClientCustomizer가 적용되지 않는다.
앞서 말한 것처럼 RestTemplate는 내부적으로 하나의 요청에 하나의 스레드를 생성하여 동작시키는 요청당 스레드 모델을 구현한 Java Servlet API를 사용한다.
각각 스레드가 일정량의 메모리와 CPU주기를 사용하고, 응답을 받을 때까지 스레드가 차단되게 되는데, 이는 요청마다 스레드를 생성하기에 많은 스레드를 생성하여 스레드 풀을 소모하거나 사용 가능한 모든 메모리를 차지할 수 있으며 빈번한 CPU 콘텍스트 (스레드) 전환으로 인해 성능 저하가 발생할 수 있다. (요청 수에 연관되어 성능 저하가 발생한다.)
WebClient
각 이벤트에 대해 Task를 만들고 대기열에 넣은 뒤 적절한 응답이 있는 경우에만 실행하게 된다. 즉 이벤트 루프 방식을 구현한 Reactive Streams API를 사용한다.
→ 응답이 있는 경우에만 실행 스레드가 동작하게 된다.
해당 방식은 RestTemplate에 비해 적은 스레드와 리소스를 사용하면서 동시에 더 많은 로직을 처리할 수 있다. (요청 수에 관계없이 일정한 성능을 제공하는 것에 의의를 둔다.)
선택?
상황에 따라 다를 수 있지만 들어오는 요청의 수가 많다고 가정하면 WebClient, 비동기 방식을 사용하는 것이 일반적으로 좋은 선택이다. (필요한 만큼만의 스레드, 리소스를 사용하기 때문이다.)