http://jessezhuang.github.io/article/java-reflection-test/

 

Reflection이란?


Compile Time에 Class나 Method 명을 알지 못하더라도 Runtime에 Type, Classpath를 이용하여 인스턴스화, 객체의 상태, 메서드 정보 등을 가져올 수 있도록 지원하는 API이다.

 

 

사용하는 Library, Framework, API, Feature

  • Jackson, GSON 등의 JSON Serialization Library

  • Log4 j2, Logback 등의 Logging Framework

  • Apache Commons BeanUtils 등의 Class Verification API

  • Spring의 @Autorwired와 같은 DL, DI 기능 (: processInject(), inject() Method )

    내부적으로 Spring의 ReflectionUtils라는 Abstraction Library를 사용한다.

  • Eclipse, Intellij 등의 IDE, Junit, Hamcrest와 같은 Test Framework

등이 있다.

 

 

 

Refliection Flow?


  1. JVM의 ClassLoader는. class를 Perm Gen (8+ : Metaspace)에 Load 한다.

  2. Class 형식의 Instance가 생성된다. Type.class 형식으로 Heap에 저장된다.

  3. Animal.class, animal.getClass(), = Class.forName(Classpath) 등으로 접근 가능하다.

     // 1
     Class<Animal> clazz = Animal.class;
    
     // 2
     Animal animal = new Animal();
     Class<? extends Animal> aClass = animal.getClass();
    
     // 3
     try {
             Class<?> aClass1 = Class.forName("...Animal");
     } catch (ClassNotFoundException e) {
             e.printStackTrace();
     }

 

 

 

Reflection API 사용 시 장단점


장점

  • 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가 발생하여 상대적으로 느린 실행시간을 보였었다.

 

하지만 이 부분은 잘못된 테스트 방식일 수도 있기에 수정을 고려해볼 것이다.

 

 

참고 자료

G1 GC 구조

개념 구조 (모든 영역을 블록으로 표기하지 않음)

G1 GC는 여러 Background Thread를 이용하여 Heap 크기에 따라 1MB~32MB로 분할된 Region들을 지정된 Pointer를 통해 Scan 하고 제일 많은 수집 대상이 존재하는 Region에 대해 이름을 지정한다.

이름이 지정된 영역은 GC의 대상이 된다.

 

 

 

Young Generation Region (Non-Contiguous Region)

Heap 메모리 공간에서 비연속적으로 존재하는 Young Object의 주거지를 의미한다.

 

해당 영역의 크기는 Heap 전체 크기의 5~60% 까지를 차지할 수 있다.

 

 

 

G1 GC 특징

  • G1 GC는 Scan을 하는 도중에 해당 Region에 대한 Compacting도 수행한다.
  • Region은 모두 같은 크기의 영역을 할당받으며, Region에 부여된 역할에 따라 그에 맞는 Object가 존재하게 한다. (Default Region Size = Heap Size / 2048 {Region count})
    • XX : G1HeapRegionSize를 통해 Size를 명시적으로 설정할 수 있다.
  • JDK 8부터는 Heap 영역에 존재하는 중복된 문자열에 대해 식별하고 여러 참조에 대해 하나의 문자열을 가리키도록 수정하는 최적화 방식이 포함되었다.

 

 

G1 GC Flow

Evacuation Pauses (Young Generation GC, Minor GC, Young 공간 부족시 발생)

Young Generation Region에서 발생하는 GC를 말하며 Live Object가 하나 이상의 Survivor Region으로 이동하는 상황을 의미한다. (이때 S-T-W가 발생, Multi-Threading 동작)

 

Ageing을 통해 임계값을 만족하는 Object들은 Old Region으로 이동한다.

 

 

 

G1 Collection Phases (= Old GC)

  1. Initial Mark (S-T-W)

    Evacuation Pauses가 실행되는 시점에서 수행되는 동작이다. Survivor Region을 Mark

    Old GC가 필요한 경우에 동시 수행된다.

  2. Concurrent Marking (Multi-Threading, Concurrent)

    Heap 내의 모든 Live Object와 빈 Region 들을 Mark 하고 Region의 Live Object 비율 계산

    해당 동작은 Evacuation Pauses와 같이 수행될 수 있으며 Application과 같이 동작한다.

  3. Remark (S-T-W)

    Heap 내의 Live Object Mark 완료 단계. 빈 Region은 Free Region으로 등록

  4. Copying, Cleanup Phase (S-T-W)

    앞서 2단계에서 계산했던 비율을 가지고 빨리 청소할 수 있는 Region들을 선택하여 Cleanup, Copying을 진행한다.

    not-live Object 제거, 빈 Region은 Free Region으로 등록, Survivor → Old Copy 등

  5. After Copying, Cleanup Phase (S-T-W)

    선택된 Region들의 Phase가 종료된 시점, 남은 Region 들에 대해 Cleanup, Copying 진행

 

 

G1 GC의 장점

  • 별도의 STW 없이도 여유 메모리 공간을 압축하는 기능을 제공한다.

    Compecting으로 인한 STW 발생 시간을 최소화하였다.

  • Heap 크기가 클수록 잘 동작하게 된다.

  • 특별한 최적화 기능을 제공한다. (String Deduplication Optimize)

    해당 기능은 Evacuation Pauses 중에 발생하며, String의 Hashcode와 값을 비교하게 되는데, 

    최소 수명 즉 AgeThreshold의 값이 3 이상인 중복 String Object 들을 정리한다.

  • 많은 튜닝 가능성이 존재한다. (Size, Count, GC 최대 시간, 동작 Thread 수와 실행 빈도 등)

 

 

G1 GC의 유의점, 단점

  • 공간 부족 상태를 조심하여야 한다.

    이때 Full GC가 수행되게 되는데, 이 GC는 Single Thread로 동작한다.

    이를 해결하기 위한 간단한 방법으론 Old Generation Region를 크게 만드는 방법이 있지만 이 경우에는 Young Generation Region 영역이 줄어드는 트레이드 오프가 발생한다.

  • 해당 GC 방식은 6GB 이상의 Heap Management를 목적으로 개발된 모듈이다. 즉 작은 Heap 공간을 가지는 Application에서는 성능을 제대로 발휘하지 못하고 빈번한 Full GC가 발생할 수 있다.

 

 

참고 자료

잘못된 내용에 대해서는 댓글 부탁드립니다.

 

 

 

Concurrency


하나의 코어가 여러 프로세스를 번갈아가며 실행하는 것을 의미한다. 이는 사용자에게 동시에 실행되는 것처럼 보이게 만드는 효과를 가지며, 단위 시간 내에 더 많은 일을 처리한다.

 

프로세스 간의 컨텍스트 스위칭이 발생한다.

 

 

Concurrency의 장단점


장점

  • CPU의 처리량이 증가한다.
  • 자원의 활용도가 증가한다.
  • 프로세스 간의 대기시간이 감소된다.

단점

  • Context Switching에 대한 Overhead가 발생한다.

 

 

 

Parallelism


하나의 프로세스를 분할하여 처리

 

여러 개의 코어가 하나의 프로세스의 작업을 분할하여 처리하는 것을 의미할 수 있다. 이는 내부적으로 동작하는 스레드의 개수만큼 CPU에 할당할 수 있음을 의미한다.

 

화면을 랜더링 하는 스레드, 계산을 진행하는 스레드, 서버와 통신하는 스레드 등...

 

 

한 번에 여러 프로세스를 실행

 

이와 같이 각 코어가 별개의 프로세스를 동작시킴으로써 단위 시간 내에 여러 프로세서를 동작시키는 것을 의미할 수도 있다.

 

 

Parallelism의 장단점


장점

  • 하나의 프로세스를 분할하여 여러 작업으로 처리할 수 있다.
  • 하나의 작업에 대해 가용 자원을 더 많이 할당할 수 있다.
  • 여러 프로세스에 대해서도 동시에 수행할 수 있다.

단점

  • 단일 코어 방식보다 어려운 작성 방식을 가진다.
  • 프로세스 분할 처리시 발생하는 추가 비용이 더 크다. 데이터 전송, 동기화, 통신, 전환 등
  • 각각의 시스템 아키텍처에 맞게 알고리즘 로직의 조정이 필요하다.

 

 

 

Parallel and Concurrency


여러 개의 코어에서 여러 프로세스들을 번갈아 실행하는 상황을 의미한다.

 

물리적인 개념(Parallel)과 논리적인 개념(Context Switching)이 연계된 것이다.

 

 

참고 자료

'Programming' 카테고리의 다른 글

Java의 Reflection API와 성능 이슈?  (0) 2021.02.02
G1 GC  (0) 2021.01.26
Service에 @Transactional 을 적용한다면?  (0) 2021.01.18
Spring Auto-Configuration Condition Annotations  (0) 2021.01.12
Spring Boot의 Auto Configuration!  (0) 2021.01.12

서비스 메서드에 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 타입의 객체를 저장한다.

 

 

참고 자료

 

Auto-Configuration Conditions

주어지는 여러가지 조건들을 이용하여 Spring Application 실행시 사용할 Bean Definition을 선택할 수 있도록하는 Annotation들의 집합이다.

 

  • @ConditionalOnClass

    어노테이션의 인자로 받은 클래스가 존재할 때, 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnMissingClass

    어노테이션의 인자로 받은 클래스가 존재하지 않을 때, 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnBean

    어노테이션의 인자로 받은 Bean ID가 존재할 때 해당 주석이 적용된 Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnMissingBean

    어노테이션의 인자로 받은 Bean ID가 존재하지 않을 때 해당 주석이 적용된Configuration class의 Java기반 Bean Definition들을 등록한다.

 

  • @ConditionalOnProperty

    application.properties 구성 정보와 값에 따라 특정한 Bean을 등록하는데 사용된다.

    ex) Prod 용 DataSource, Test 용 DataSource 등

    • value

      속성의 이름을 나타내는 필드이다. (ex) lob)

    • prefix

      각 속성에 적용되는 접두사를 정의한다. 기본 값이 지정되지 않으면 .을 포함한다.

    • name

      검증될 속성의 이름을 나타낸다. (prifix + value ex) : .lob)

    • havingValue

      해당 속성에 대한 예상 값을 나타내며, 값이 지정된 경우 다른 값이여야 한다. 기본적으로 빈 문자열 로 정의된다.

    • matchIfMissing

      해당 속성이 설정되지 않은 경우 정의한 조건을 사용할지에 대해 정의한다. 기본적으로 false로 정의되어 있다.

 

  • @ConditionalOnResource

    특정한 리소스가 존재할 때 해당 Class의 Definition을 사용하게 한다.

    ex) (resources = "classpath:application.properties")

 

  • @ConditionalOnWebApplication

    현재 Application이 Web Application이라면 해당 Definition을 사용한다.

 

  • @ConditionalOnNotWebApplication

    현재 Application이 Web Application이 아니라면 해당 Definition을 사용한다.

 

  • @ConditionalExpression

    SpEL 표현식을 통해 작성된 검증로직이 true인 경우 해당 Definition을 사용한다.

 

  • @Conditional

    Custom된 지정 조건 Class를 만든 뒤 해당 어노테이션에 인자로 넘긴 뒤 true인 경우 해당 Definition을 사용한다.

 

 

Conditional의 동작

해당 어노테이션들의 동작도 ConfigurationClassParser를 통해 이루어진다.

 

  1. ConfigurationClassParser 의 process 메서드 호출

  2. 추가가 될 수 있는 모든 그룹(AutoConfiguration.class)에 대한 정보를 저장해온다.

  3. processGroupImports()가 호출되고 앞서 저장된 정보들을 순회하면서 각각의 ConfigurationClass 정보를 가져온 뒤 processImports() 메서드를 호출한다.

  4. 저장된 정보들은 Configuration Class들 이기 때문에 processConfigurationClass() 를 호출한다.

  5. 최상단에 shouldSkip 메서드를 호출한 뒤 몇몇 조건을 검증한다.

    1. MetaData가 Null 이거나 Conditional class 유형의 어노테이션이 없는 경우

      → 아무것도 하지않고 false를 Return 한다.

    2. ConfigurationPhase phase가 Null 인 경우

      해당 MetaData가 AnnotationMetadata이고 Annotation이라면, 혹은 Method 단위에 Annotation이 붙어있다면 shouldSkip 메서드를 호출하며 ConfigurationPhase를 PARSE_CONFIGURATION로 설정한다.

       

      위의 조건이 아니라 Interface이거나 메서드 단위의 Annotation이 작성되지 않았다면 shouldSkip 메서드를 호출하며 REGISTER_BEAN로 설정한다.

       

       

      ConfigurationPhase 의 속성 정보

      • PARSE_CONFIGURATION

        @Configuration Annotation을 파싱할 때 작성된 조건식을 평가한다.

      • REGISTER_BEAN

        ConfigurationClassBeanDefinitionReader가 Bean을 등록할 때 조건식을 평가한다.

  6. 이후 MetaData 정보에서 Condition 정보가 존재하는지 확인하고 리스트에 저장한다.

  7. 저장된 정보를 순회하며, ConfigurationPhase requiredPhase 만든 뒤 조건을 검증한다.

    1. requiredPhase 가 Null 이거나 (||) 메서드를 호출될 때 넘겨받은 phase 속성 값과 같고 condition 검증 조건이 false인 경우 true를 반환한다. true를 전달받은 경우 5번 절차에 조건식을 만족함으로 메서드 실행이 종료된다.

    이때 호출되는 matches 메서드가 Condition 인터페이스를 구현한 SpringBootCondition의 matches 메서드이다.

    해당 메서드는 ConditionContext 객체와 AnnotationType의 Metadata를 넘기면서 하위 어노테이션들의 getMatchOutcome()을 호출한다. 해당 메서드는 정해진 조건식을 검증하고 그에 따른 결과로 boolean 값을 전달한다.

 

 

 

참고자료

주의 : 틀린 내용이 있을 수 있습니다..! 내용을 보완 중에 있습니다.

 

SpringBootConfiguration?

개발자의 편의를 위해 EnableConfiguration과 SpringBootConfiguration, 그리고 Component Scan을 합쳐 하나로 제공한다.

// 기존에 작성하던 방식을 좀더 간편하게 하였다.
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(DbunitApplication.class, args);
    }
}

// 동일하게 동작한다.
@SpringBootApplication
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(DbunitApplication.class, args);
    }
}

 

EnableAutoConfiguration?

Spring Boot Application에 존재하는 특정한 패키지에 포함된 jar 파일과 Bean Metadata들을 CLASSPATH를 통해 접근하여 Bean들을 자동 구성하는 애노테이션이다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

}

Spring-Boot-Stater Dependency들을 추가하게 되면 해당 정보들이

 

org.springframework.boot:spring-boot-autoconfigure > META-INF > spring.factories

 

에 포함되며 별도로 제외할 basePackage, class 정보를 적용하지 않는 이상 모두 자동으로 등록하게 된다.

 

 

spring.factories

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
.....

내부에 정의된 클래스 패스들을 따라가게 되면 @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을 통해 탐색되어 읽어 들일 수 있으며, 이는

  1. ConfigurationClassPostProcessor의 postProcessBeanDefinitionRegistry()메서드 호출
  2. 내부의 processConfigBeanDefinitions() 메서드를 호출한 뒤 ConfigurationClassParser 를 통해 읽어 들여진 Configuration Class들을 ConfigurationClass 타입으로 파싱 한다.
  3. 해당 정보를 ConfigurationClassBeanDefinitionReader의 loadBeanDefinitions() 메서드에 전달하여 순회한다.
  4. 순회되는 정보들을 loadBeanDefinitionsForConfigurationClass() 메서드를 통해 정보를 읽어 들어와 Bean Definition으로 등록한다.
  5. 이후 ConfigurationClass 객체들을 alreadyParsed에 저장하고 다음 로직을 진행한다.

의 흐름을 가지고 등록되게 된다.

 

 

@EnableAutoConfiguration에 포함된 AutoConfigurationImportSelector.class, @AutoConfigurationPackage의 흐름 따라가 보기

 

Import(AutoConfigurationImportSelector.class)

META-INF/spring.factories에 존재하는 정보들을 가져와 등록하는 클래스이다.

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다.

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행하는 중 postProcessBeanDefinitionRegistry → processConfigBeanDefinitions에 regisrar 정보를 넘긴다.

  7. processConfigBeanDefinitions 내부에서 BeanDefinitionHolder 정보를 담는 ArrayList인 configCandidates 객체를 생성한 뒤 해당 객체의 요소를 추가하는 작업을 진행한다.

  8. → 전달받은 registry가 가지고 있는 DefinitionNames 들을 가져와 순회하면서 BeanDefinition 정보와 Bean Name을 꺼내오고 그것을 저장하는 BeanDefinitionHolder를 생성하여 저장한다.

  9. configCandidates 객체를 정렬한 뒤 ConfigurationClassParser를 생성하고 정렬된 객체를 Set으로 변환한 다음 parse() 메서드의 인자로 넘긴다.

  10. parse() 메서드 내부에서는 전달받은 Set를 순회하며 가져온 BeanDefinition 정보를 instanceof를 통해 Sub Class Type을 검증한 뒤 해당 정보에 맞게 캐스팅하여 BeanName과 함께 parse()를 호출하며 인자로 넘긴다.

  11. deferredImportSelectorHandler의 process()가 호출된다.

  12. 해당 메서드는 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());
            }
  13. 저장된 정보를 가지고 processGroupImports() 메서드를 호출한다.

  14. 앞서 저장된 Map을 순회하면서 getImports() 메서드를 호출한다.

  15. getImports() 메서드는 전달받은 DeferredImportSelectorHolder 타입 객체 정보를 순회한다.

  16. AutoConfigurationImportSelector의 process()를 호출하면서 DeferredImportSelectorHolder의 ConfigurationClass의 Metadate 정보와 importSelector를 전달한다.

  17. 주어진 importSelector를 AutoConfigurationImportSelector로 캐스팅하여 getAutoConfigurationEntry() 메서드를 호출한 다음 AutoConfigurationEntry 타입 객체인 autoConfigurationEntry에 저장한다.

  18. 저장한 entry의 MetaData를 가져와 ConfigurationClass 타입의 객체를 선언하여 저장한다.

  19. processImports() 메서드를 호출하고 주어진 ConfigurationClass 객체를 importStack이 상속받은 ArrayDeque에 해당 정보를 저장한다.

  20. 전달받은 Collection importCandidates 향상된 For문으로 순회한다.

  21. 해당 정보는 ImportSelector 또는 ImportBeanDefinitionRegistrar가 아닌 Configuration 정보이기에 해당 정보를 importStack의 LinkedMultiValueMap에 저장하게 된다.

    → 이때 저장되는 정보는 위에서 주어진 ConfigurationClass를 SourceClass로 필터를 거쳐 Converting 된 객체의 Metadata와 enrty의 포함된 Import Class 컬랙션이다.

  22. importCandidates의 요소를 ConfigClass로 변환하고 grouping에 저장되어 있던 exclusionFilter를 전달한 뒤 SourceClass로 컨버팅하고 doProcessConfigurationClass()를 호출한다.

  23. 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 자동 초기화

  1. Main 메서드가 호출되고 run 메서드가 실행된다.

  2. run 메서드 실행 중 refreshContext() 메서드가 ConfigurableApplicationContext 타입의 객체를 전달받으며 호출된다.

  3. 넘겨받은 객체 타입을 검증하는 refresh(ApplicationContext) 메서드에 전달하여 확인하고 해당 객체 타입의 인자가 전달되지 않는 refresh() 메서드를 호출한다.

  4. 메서드 내부적으로 invokeBeanFactoryPostProcessors(BeanFactory); 를 호출한다. 인자로 전달받은 beanFactory에서 PostProcessor 개수를 전달받은 뒤 순회하면서 해당 정보를 ArrayList인 currentRegistryProcessors에 저장한다.

  5. currentRegistryProcessors를 정렬하고 해당 리스트와 beanFactory를 BeanDefinitionRegistry로 Casting 한 다음 invokeBeanDefinitionRegistryPostProcessors() 메서드에 전달한다

  6. 내부적으로 전달받은 ArrayList를 순회하며 각각의 PostProcessor 들을 등록하는 절차를 진행한다. postProcessBeanDefinitionRegistry → processConfigBeanDefinitions → ConfigurationClassBeanDifinitionReader에 정보를 넘겨 생성한 뒤 loadBeanDefinitions 메서드를 호출한다.

  7. 넘겨받은 Set를 순회하며 loadBeanDefinitionsForConfigurationClass를 호출한다. 해당 메서드는 loadBeanDefinitionsFromRegistrars()를 호출하여 registrar 들의 registerBeanDefinitions() 메서드를 호출하는 절차를 진행한다.

  8. DataSourceInitializationConfiguration 내부에 Regisrar의 registerBeanDefinitions()를 호출하고 해당 로직에서 dataSourceInitializerPostProcessor이라는 이름을 가진 PostProcessor 가 존재하지 않는 경우 DataSourceInitializerPostProcessor.class을 등록하는 로직을 실행한다.

  9. 이후 Bean 이 등록되어 초기화 절차를 진행하는데 이때 afterPropertiesSet() 메서드를 호출하여 createSchema()를 통해 DB의 Schema를 등록하는 절차를 진행한다.

  10. 위의 로직이 성공하면 initialize()를 호출하여 구성된 Schema에 data.sql 정보를 읽어 등록한다.

  11. sql 파일들을 통해 정상적으로 DB가 구성되고 사용할 수 있게 된다.

 

내용을 계속 추가하고 있습니다.

 

 

 

Spring vs Spring Boot

[의존성 관리와 설정을 자동으로 해준다! - Starter]

 

스프링 부트는 스프링 프레임워크의 경량화를 위해 분리된 수많은 Jar 파일을 필요에 맞게 등록하고, 관련 설정을 선언하는 절차없이 기본 설정값으로 제공되는 @Configuration Class들을 통해 빠른 실행을 가능케 하고, Bean에 대한 Builder를 통해 사용자 정의도 쉽게 할 수 있게 하여 빠른 개발 환경 구성을 지원하기 위해 만들어진 것입니다.

 

내장 톰켓에 대해서도 요청을 관리하기 위해 필요했던 매핑 작업, 서블릿 등록, 컨텍스트 설정들을 구성해주고 있기에 바로 실행하여 결과를 확인할 수 있게 되는 것을 알 수 있습니다.

 

이것이 Spring과 Spring Boot 제일 큰 차이이며, Spring Boot를 사용해야하는 근본적인 이유임을 알 수 있습니다. 

 

 

참고 자료

Logging?

서비스 동작 시 시스템 상태, 작동 정보를 시간의 경과에 따라 기록하는 것을 말한다.

 

 

로깅을 사용하는 이유?

서비스 동작 상태를 파악하고, 발생한 장애를 알려주거나, 파악하기 위해 사용한다. 그러기 위해서는 Log Message에 Context를 담아주어야 한다.

 

어떤 위치에서 어떤 Param을 사용하였고 어떤 것이 실패하였다는 느낌으로 작성하자.

 

 

로깅을 사용할 때 주의할 점?

  • Log 파일 / DB 생명 주기 & 저장소 용량
  • 개인 정보
  • 시스템 주요 정보 (시스템 보안, 계정 정보)

 

 

Logging을 사용하는 방법?

  • Linux System API(sysout)
  • Java API(sysout)
  • Java API(util.Logging)
  • Logging Framework

 

 

Spring Boot에서 제공하는 Logging Framework?

  • 기본적으로 내부 로깅 동작에 대해서는 common-logging api를 사용한다.
    • 별도의 구현체인 Logback, Java API(util.Logging) 등을 활용할 수 있다.
  • boot stater의 logging 의존성을 제외함으로써 별도의 Framework를 적용 가능하다.

 

 

 

Slf4j Library?

Slf4j는 여러 Logging Framework를 하나의 통일된 방식으로 사용하게끔 지원하는 추상 API이다.

  • Spring Boot Module이 제공하는 AutoConfiguration과 의존성을 그대로 사용할 경우 실제 Binding되는 Module은 Logback이다.

 

 

Slf4j Module Structure

API Module

  • slf4j의 Logging Interface Module이다.
  • 해당 모듈은 추상화된 인터페이스만을 제공하여 Binding될 다양한 로깅 프레임워크를 실행, 배포 시에 선택적으로 주입하도록 지원한다. 이를 통해 추상화 수준을 높이고 일관적인 사용이 가능해진다.

 

Bridge Module

  • 기존에 개발된 레거시 코드를 위해 사용하는 Module이다.
  • 기존에 작성된 로거 호출을 가로채 slf4j API로 전달하고 Binding Module과 연결된 Logger를 작동시킨다.
  • 여러 개의 Bridge Module 들을 가질 수 있다.
  • Binding Library와 같이 사용해서는 안된다.

 

 

Binding Module

  • 여러가지 Logger Framework 를 일관성 있는 API Module의 Interface와 연결해주는 역할을 한다.
  • API Interface가 호출하는 Logger API는 Binding을 통해 연결된 실제 Logger Framework이다.
    • Logback, Log4j (v 1.x, 2.x) 등

 

 

이것을 사용함으로써

  • Facade Pattern을 이용하여 구현부를 숨기고 추상화된 인터페이스를 제공한다.
  • 다른 Logging 구현체를 사용하더라도 Application Code가 변경되지 않는다.
    • 이를 통해 Application 배포 시에는 원하는 Library로 변경할 수 있게끔 한다.
    • 개발 시에는 logback을 사용한다면, 최종 배포 등에는 Log4j2 를 사용하는 등

 

 

Slf4j Logging Level

Logging Level Description
FATAL Application이 종료될 정도의 심각한 에러를 의미한다.
ERROR 에러가 발생하였으나, Application이 종료될 정도는 아님을 의미한다.
WARN 에러가 될 수 있는 잠재적 가능성이 존재함을 의미한다.
INFO Application의 상태를 간단하게 확인할 때 사용한다.
DEBUG INFO Level보다 더 자세한 정보가 필요한 경우 사용한다.
TRACE DEBUG보다 더 자세한 정보를 제공하며, 개발 환경에서 버그를 해결하기 위해 사용한다.
  • FATAL
    • 에러로 인한 비정상 종료 시에 사용되는데, 해당 상황에서는 Log가 남지 않을 수 있음을 고려하여야 한다. (상대적으로 많이 사용되지는 않는다.)

 

  • ERROR
    • FATAL, ERROR Level은 의도하지 않은 Exception 정보를 파악하는 데 사용한다.
    • 외부 API 호출 시 ERROR가 반환된다거나, 시스템 내부 에러가 발생하였을 때 사용한다.

 

  • WARN
    • 인메모리 캐시, DB 커넥션 같은 Resource 가 소진되어갈 때 해당 Level을 통해 정보를 수집하여, 알림을 받아 해결하거나 에러 발생 시 이유를 파악하는 용도로 사용한다.

 

  • INFO
    • 명확한 의도를 가지는 Exception 정보를 Logging 하는 데 사용한다.
    • Application의 Service Flow가 정상 동작하는지 확인하는 수준의 간단한 Level이다.

 

  • DEBUG
    • 권한이 없어 디버깅을 진행할 수 없을 경우, 필요한 level이다.

 

 

 

Logback?

log4j의 여러 문제점을 개선한 Logging Framework이며, 개념적으로 매우 유사하다. ( 이는 동일한 개발자가 프로젝트를 리드하였기에 그렇다고 한다. )

 

 

Logback vs Log4j 

 

Logback은 Log4j에 비하여

  • 더 빠른 구현 = Logging이 빠르게 수행되며, 메모리 공간을 상대적으로 적게 사용한다.
  • 상대적으로 견고하고 신뢰성 있는 내부 테스트들을 수행한다. (테스트 케이스 ^)
  • 다른 Logging Library 전환 시에 관련된 작업을 최대한 줄일 수 있다.
  • 구성 파일 수정 시 자동으로 해당 내용들을 적용시킨다.
  • I/O 장애로부터 원활한 복구가 가능하다.
    • Logging을 다시 시작하기 위해 Application 재구동이 필요 없다.
  • Log File의 용량, 기간 등을 설정 값을 통해 관리 가능하다.
  • Log File에 대한 비동기 방식의 자동 압축을 지원한다.
  • 구성 파일에서 조건 식을 이용한 분기 처리가 가능하다.
  • 많은 필터 기능을 제공한다.
  • Servlet Container와 통합 됨으로써 HTTP-Access Log 기능을 제공한다.

등을 추가하고 개선하였다.

 

 

Logback Module Structure

core

  • classic, access Module의 기반이 되는 Module이다.

 

classic

  • core를 확장하는 Module 로써 log4 j의 기존 기능을 개선하여 구현하였다.
  • 기본적으로 slf4j API를 이용함으로써 log4j, jul, jcl 와 같은 여러 로깅 Framework들과 쉽게 전환을 할 수 있다.

 

access

  • Tomcat, Jetty와 같은 Servlet Container와 통합되어 HTTP-Access Log 기능을 제공한다.
  • classic Module과는 독립적으로 구성되며, Container 수준에서 설치되어 사용돼야 한다.

 

 

Logback Architecture

Logger, Appender, Layout Interface들이 모여서 Logback Architecture를 구성한다.

 

 

Logger

  • Application이 Log Message를 생성하는 역할을 수행한다.
  • Logger에는 Log Level이 지정될 수 있다.
    • Logger의 Level이 지정되지 않는다면 가장 가까운 조상(상위 노드)에게 상속받는다.
    • 최상위 Root Logger는 기본 값으로 DEBUG Level이 지정되어 있다.
    • log 요청은 Level을 기준으로 같거나 상위 Level 일 때 활성화된다.
      • TRACE < DEBUG < INFO < WARN < ERROR
  • Package 단위로도 Logging Level을 지정할 수 있으며, 중복적으로 선언될 경우 최하위 Level이 적용된다.

 

Logger Context

  • 각 Logger는 로거 계층 구조에 각각의 Logger를 배치하는 logger Context와 연결된다.
  • 계층에 존재하는 모든 로거들은 LoggerFactory의 getLogger를 통해 가져올 수 있다. 
  • // 최상위 Logger // getLogger의 인자는 가져올 Logger의 이름이다. Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER+NAME);
  • 이때 가져오는 Logger들은 싱글톤처럼 Application에서 하나만 존재하며 여러 Ref Variable은 같은 위치를 참조하게 된다.

Logger는 classic Module의 일부이다. 즉 해당 Module에서만 사용되는 개념이다.

 

 

Appender

  • 생성된 Log Message를 저장(출력)하는 역할을 수행한다.
  • Logger에 2개 이상의 Appender가 존재 가능하다.
    • Console Appender
    • File Appender
    • External Socket Server Appender
    • RDBMS Appender
    • JMS Appender
    • UNIX Syslog Daemon Appender
  • addAppender를 통해 주어진 Logger에 Appender를 추가할 수 있다.
    • 상위 계층 Logger에서 정의된 Appender는 하위 Logger에도 적용된다.해당 부분을 비활성화하기 위해서는 additivity flag를 false로 설정하여야 한다.
    • 이는 요청이 하위 계층에서 상위 계층으로 올라가기에 발생한다.

 

Layout

  • 출력할 Log Message 형식을 지정하는 역할을 수행한다.
  • Logback은 기본적으로 PatterLayout을 제공하며 이것을 통해 printf()와 유사한 형식을 사용할 수 있다.

Appender와 Layout은 core Module의 일부이다.

 

 

Logger Configuration

Logback의 환경 구성은 일반적으로 Application 초기화 시에 수행된다고 한다.

  • 선호되는 방식은 Configuration File을 읽는 것이다.

 

 

 

Log4j2 vs Logback

Log4j2 는 Logback 를 사용하는 것에 대해 여러 이점이 있다고 한다.

https://logging.apache.org/log4j/2.x/performance.html

  • 멀티 스레드 환경에서의 Logging 성능 차이 (최대 처리량)
    • 이는 Log4j2의 Logger가 낙관적 락을 사용하여 구현되는 반면에 Logback에서는 ArrayBlockingQueue를 사용하기 때문이다.
    • Appender 관련해 Queue에 Log Event를 추가할 때 잠금 경합이 발생하기도 한다.

 

https://logging.apache.org/log4j/2.x/performance.html

  • (동기성) 파일 Logging 지속 처리량
    • 모든 시스템에서 최대 지속 처리량은 가장 느린 구성요소에 의해 결정된다.
    • Logger에서는 Appender의 Massage Format, Disk I/O를 의미한다.

 

https://logging.apache.org/log4j/2.x/performance.html

  • 비동기 Logging 시 Parameterized Message Formating
    • 멀티 스레드 환경에서 성능 차이가 많이 벌어지지만(높지만), 매개 변수 수에 따라서 Formating 비용이 급격히 증가한다고 한다.

 

간단하게 살펴보니 Log4j2는 대부분의 상황에서, 특히 멀티 스레드 환경에서 성능의 이점이 큰 것을 알 수 있다.

 

 

 

하지만 왜 Log4j2를 Spring Boot의 기본 Logger로 사용하지 않을까?

내용 출처 : https://github.com/spring-projects/spring-boot/issues/16864

 

Log4j2 의 성능이 멀티 스레드 환경에서 우세함을 보이지만 이것이 개발자들에 있어 일반적으로 선택할만한 이유가 되지는 않는다고 생각한다.

  • 잘 추상화된 Interface와 Spring Boot 설정을 통해 쉽게 Logger 구현체를 변경 가능함으로 필요한 경우에 log4j2를 사용하면 된다.
  • 이것을 기본 Logger로 등록하게 된다면, 기존에 Logback을 사용하던 프로젝트들이 변경(마이그레이션)하는데 큰 어려움을 주게 된다. (Boot 1.X → 2.X ...)
  •  

이러한 내용들을 통해 성능만이 어떤 기술을 도입하고, 변경하는데 의미를 주지 않는다는 것을 알게 되었다. 특히나 Application에서 제일 비중 있는 DB (Query 성능 등)이 아닌 이상에는 더욱 그런 느낌을 받게 된다.

 

 

 

System.out.println() vs Logger

 

로그 출력, 저장 방식

Logger

  • Logger는 Log 내역을 별도의 파일에 저장할 수 있다.
  • Logger는 Layout 을 이용해 메시지에 대한 일관적인 형식을 지정할 수 있다.
  • Log의 유지 기간, 파일 용량 등을 설정하여 자동화된 관리를 제공한다. Framework 지원 여부
  • Log를 압축하여 관리할 수 있다. Framework 지원 여부
  • Log를 출력하는 데 있어서 기본적인 설정이 필요하고 인스턴스 사용을 해야 한다.
  • 다양한 저장 방식을 제공하고 일관적인 형식 지정이 가능하며, 자동화된 관리 방식을 제공한다.

System.out.println()

  • println()은 Log를 콘솔에만 출력할 수 있다.
  • 설정하지 않아도 메서드 하나로 로그를 출력할 수 있다.
  • 상황에 따라 필요하지 않은 log들도 Console에 모두 출력된다.
  • 각각의 Println() 마다 출력 형식을 지정하여야 한다.
  • 당시 날짜, 시간을 출력하지 않는 한 언제 발생한 로그인지 확인조차 어렵다.
  • 형식을 지정하는데 상대적으로 많은 노력이 필요하며, 저장이 되지 않는다.

 

 

로그 분류, 제어

Logger

  • Logger는 Log 내역을 별도의 파일, 서버, DB 등에 저장할 수 있다. (Appender)
    • Logging 레벨을 통해 log 정보들을 분리할 수 있다.
  • Logging 래벨 설정을 통해 필요한 log만 출력할 수 있다.
  • 로그를 세분화하여 관리할 수 있고 필요한 데이터만을 모니터링할 수 있다.

System.out.println()

  • 출력되는 메시지를 제어할 수 없다.
  • Application env (Was)의 로그와 섞여서 출력된다.
  • 로그가 남지 않으며, 제어를 할 수 없고 상황에 따라 정확한 모니터링이 불가능하다.

 

 

사용하는 리소스 량, 동기화 문제

Logger

  • Logger Framework는 일반적으로 Blocking Queue나 낙관적 락이 적용된 자료구조를 통해 Log를 저장하며, 별도의 출력이 없는 경우에 기록을 진행한다.
  • 서버의 실시간 처리량에 최대한 영향을 미치지 않는다.
  • FIFO 자료구조로 Log가 삽입되기 때문에 로그를 순차적으로 저장할 수 있다.

System.out.println()

  • println()은 매 실행시마다 스트림을 생성하고 I/O 작업(System.call)을 진행하는 Blocking 방식이다. 이는 로컬 개발 PC에서는 큰 성능 차이와 리소스 문제를 나타내지 않을 수 있지만, 실제 운영 중인 서버에서는 큰 차이가 발생하게 된다.
  • 서버의 실시간 처리량에 큰 영향을 미친다.
  • 로그가 순차적으로 저장되지 않기에 별도의 동기화 코드를 구현하여 사용하여야 한다.
  • 멀티 스레드에 안전하게 구현되지 않았다.
  • 요청 처리에 따라 로그가 소실될 수 있다.

 

 

System.out.println() vs Logger 결론

위에서 다룬 모든 경우 (간편한 사용 방식을 제외한)에서 Logger가 Println() 보다 우세함을 알 수 있다. 요즘 개발을 공부하고, 여러 자료를 찾아보면서 느끼는 SW의 중요 요소는 유지보수라는 것이다.

 

결국 Logger를 사용하는 것도 그러한 이유를 위해서라고 생각한다. 개발만이 그 SW의 전부가 아니기에, 사실상 운영하고 유지보수되는 기간이 더욱더 길 것은 자명하기 때문에 더 중요하게 생각하는 것은 아닐까?

 

 

 

 

[ 추가된 내용 ]

  • 2021-12-09일 쯤 공유되었던 Log4j 2.x 취약점은 2.0.0 에 추가된 JNDI Lookup 기능 때문이다.
    • 해당 취약점은 2.15.0 버전에서 해결하였으나 미흡한(?) 처리로 인해 DDOS 공격이 가능한 상태가 되어 다시 패치한 2.16.0 버전에서 해결 되었다.
    • 1.x 버전의 경우 JMSAppender를 사용하지 않아야 안전하다.
  • 2021-12-15일 쯤 공유되었던 Logback 1.2.9 이전 버전의 취약점은 그보다 치명적인 취약점은 아니나, JNDI Lookup과 관련된 취약점임은 동일하다.
    • 해당 취약점을 이용하기 위해선 3가지의 조건을 만족하여야 하는데 이게 가능한 수준이면 이미 서버가 털린 상태나 다름없지 않나 싶다.
      • 1. 공격자가 서버에 접속하여 Logback config 파일에 쓰기 권한을 가질 것
      • 2. 공격자가 쓰기 권한을 이용해 config 파일을 변조한(Scan = true) 다음 Application을 재기동 시킬 것 
      • 3. 1.2.9 버전 밑의 Logback을 사용하고 있을 것

 

 

출처

알게 모르게 한 번이라도 들어본 JDBC를 정리해보았습니다.

 

Java Database Connectivity?

  • Java와 여러 가지 데이터베이스 간의 연결을 위하여 제공되는 표준 인터페이스이다.
  • DB 벤더나, 써드파티 관련 라이브러리에서 JDBC를 구현한 드라이버를 제공한다.
  • DB 데이터 접근을 위해 계층 처리 모델을 제공한다. (기본 2계층 사용, 3계층 지원)

데이터베이스 벤더마다 각각의 SQL 문, 작성 방식을 사용함으로써 발생했던 문제점을 해결하였다.

  • 달랐던 메서드, 구조, 전역 변수 등의 모든 문법을 통일시켜 API로 명세한 것이다.

 

JDBC 구성 요소

Untitled (34)

 

JDBC DriverManager

  • DB의 드라이버 목록을 관리하는 클래스
  • Java Application의 연결 요청을 적절한 DB Driver와 매핑시킨다.
  • 이는 Driver가 가지고 있는 고유한 명칭 즉 ClassName을 이용하여 선택하게 된다.

 

DatabaseDriver

  • DB 서버와의 통신을 처리한다.

 

Connection

  • DB와 연결하기 위한 모든 메시지가 포함된 인터페이스
  • DB와의 통신은 연결(Connection) 객체를 통해서만 이루어진다.

 

Statement

  • 해당 인터페이스의 구현체가 SQL 문을 DB에 전달한다.
  • 일부 파생 인터페이스는 저장 프로시저를 실행하는 것 외에도 매개 변수를 허용한다.

 

PreparedStatement

  • Statement의 하위 인터페이스.
  • Statement의 실행 절차, 자원 사용 부분에 대하여 최적화를 진행하였다.

 

ResultSet

  • Statement 객체를 사용하여 SQL 쿼리를 실행한 뒤 데이터베이스에서 검색, 반환된 데이터를 저장한다. (쿼리에 대한 결과를 나타낸다.)
  • 데이터를 이동시키는 일종의 이터레이터 역할을 한다.

 

SQLException

  • 해당 클래스는 DB에서 발생하는 모든 오류를 나타낸다.
  • 각 벤더(Database)에서 제공하는 오류 코드를 Application에 그대로 전달한다.

 

JDBC의 처리 흐름

  • JDBC 클래스가 포함된 패키지를 로드하고 JDBC 드라이버를 등록하여야 한다.
  • DriverManager.getConnection() 혹은 datasource.getConnection() 를 이용하여 Connection 객체를 얻는다.
  • Statement 타입의 객체를 사용하여 쿼리를 실행한다.
  • 결과가 있는 경우 반환된 ResultSet.getXXX() 메서드를 통하여 데이터를 추출한다.
  • close() 를 통하여 사용한 객체들에 대한 자원을 풀에 반환한다.

Untitled (35)

Connection과 PreparedStatement 객체는 보통 Pool 방식으로 운영되며, 미리 정의된 제한된 수의 자원을 만들어두고 필요할 때 할당하고 반환하면 다시 풀에 넣는 방식으로 동작한다.

 

close()가 동작하지 않으면 풀에 저장된 자원은 계속해서 줄어들게 되고, 고갈됨으로써 문제를 일으키게 된다.

코드를 작성할 때에는 예외처리를 하여야 하며, 어떤 상황이 발생하더라도 자원이 반환되게 코드를 작성하여야 한다.

 

 

 

모든 상황에서 리소스를 반환하기 위한 예외처리 방법

 

try-catch-finally 방식

복잡한 흐름을 가짐으로써 가독성을 떨어트리게 된다

Connection con = null; 
PreparedStatement ps = null; 
try { 
	con = dataSource.getConnection(); 
    ps = con.prepareStatement("SQL"); 
    ps.executeUpdate(); 
} catch (SQLException exception) { 
	exception.printStackTrace(); 
} finally { 
	// 리소스 반납 코드 
   	try { 
    	if (ps != null) { 
        	ps.close(); 
        } 
        if (con != null) { 
        	con.close(); 
        } 
    } catch (SQLException exception) { 
    	exception.printStackTrace(); 
}

 

 

try-with-resources 방식

해당 방식은 내부적으로 close를 호출함으로써 자원을 반환한다.

try (Connection con = dataSource.getConnection(); PreparedStatement ps = con.prepareStatement("SELECT * FROM USER")){ 
	ResultSet resultSet = ps1.executeQuery(); 
} catch (SQLException exception) { 
	exception.printStackTrace(); 
}

 

 

JDBC Template

Spring은 JDBC의 반복되는 보일러 플레이트 코드를 템플릿, 콜백 패턴을 적용한 라이브러리인 Spring JDBC Template 을 제공한다. JDBC의 복잡했던 사용 방식을 저수준의 모듈, 메서드에 모아놓고 호출, 사용한다.

 

 

JDBC Template는

  • DB 연결, SQL 질의에 대한 사용한 자원을 내부적으로 정리하고 Pool에 반환한다.
  • JDBC SQLException (checked)을 RuntimeException으로 변환하여 제공함으로써, 유연한 대응을 할 수 있게끔 지원한다. (예외 처리 지원)
  • 다양한 SQL 질의 방식과 결과 추출을 제공한다.
    • ResultSetExtractor : 단일 객체에 대한 데이터 바인딩 제공 (Post, Member ....)
    • RowMapper : 객체 목록에 대한 데이터 바인딩 제공 (List, Map ....)
  • 멀티 스레드 환경에서 안전한 객체를 제공한다.
    • 객체의 행위에 대해 내부 상태변수 이용 없이 전달받은 것들을 사용하고 정리함을 의미한다.

 

템플릿 패턴?

객체의 행위(로직)에 대한 구조(템플릿)을 정의하고 일부 단계를 추상적으로 만들어 하위 클래스들에서 정의되게끔 한다.

 

이렇게 구현한 메서드는 사용자가 필요에 따라 재정의(선택)한 객체, 메서드를 호출하게끔하여 상호작용을 이용해 유연함을 제공한다.

  • 행위의 구조를 미리 구현한다.
  • 행위의 구조를 수정하지 않고도 하위 클래스를 통해 유연하게 변경하게끔 한다. (추상화)
  • 행위에 대한 유연한 구현 부를 제공함으로써 코드 재사용, 중복을 최소화한다.
  • 이를 통해 구현된 템플릿을 여러 지점에서 활용할 수 있다.

 

콜백 패턴?

호출된 객체(템플릿)가 호출한 객체(클라이언트)를 호출하는 흐름을 구현하는 패턴이다.

  • 객체가 다른 객체를 호출하면서 그 인자 값 중에 콜백 객체를 전달한다.
  • 해당 콜백 객체는 호출된 객체의 행위 중에 필요한 데이터가 있을 때 사용되어 호출한 객체에 정보를 요청하고 반환 데이터를 호출된 객체에 제공한다.
  • 구현된 방식에 따라서 콜백 객체가 최종 결과를 전달하게끔 할 수 있다.

 

JdbcTemplate의 구성 객체

 

JdbcTemplate

  • JDBC Template의 Core Class
  • JDBC 사용을 단순화하고 일반적인 예외를 방지하는 데 기능을 제공한다.
  • 설정된 Datasource 를 사용하여 JDBC Template 객체를 생성하게 된다.

 

PreparedStatementSetter

  • JDBC Template Class가 사용하는 Callback Interface
  • JDBC Template가 생성한 PreparedStatement의 매개 변수 값을 설정한다.

 

ResultSetExtractor

  • JDBC Template 질의 결과를 추출하는데 사용되는 Interface
  • ResultSet에서 결과를 추출하는 실제 작업을 수행한다.
  • 내부적으로 하나의 객체에 대해서 데이터를 Binding할 때 사용되는 Interface이다.

 

RowMapper

  • JDBC Template 질의 결과를 추출하는데 사용되는 Interface
  • 쿼리 질의 결과에 각각의 행들을 객체에 Mapping 하는데 사용된다.

 

그외에도

  • NamedParameterJdbcTemplate : ?를 통한 값 대입 대신 param 형식을 사용할 때 이용한다.
// 기존 방식 
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(?, ?)"; 
Object[] args = new Object[] {1L, "Lob"}; 
template.update(query, args); 

// Param 방식 
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(:id, :name)"; 
Map<String,Object> params = new HashMap<String,Object>(); 
params.put("id", 1L); 
params.put("name", "lob"); 
template.update(query, params);

 

  • SimpleJdbcTemplate : JdbcTemplate와 NamedParameterJdbcTemplate 에서 주로 사용되는 기능을 포함하는 인터페이스이다.
    • query, queryForXXX (Type, Collections), Update들이 포함되었다.

 

  • SimpleJdbcInsert, SimpleJdbcCall
    • 테이블 이름, 열 이름, 열값 이나 호출 프로시저, 함수명과 매개 변수만을 제공하게끔 구성되어있다.
  • INSERT 문과 프로시저 호출을 진행할 때 필요한 코드들을 단순화하기 위해 만들어진 클래스이다. 메타 데이터 처리를 방식을 제공한다.

 

  • MappingSqlQuery하위 클래스는 mapRow 메서드를 구현하여야 한다.
  • ResultSet의 각 행을 객체로 변환하기 위해 사용되는 추상 클래스이다.

 

  • SqlUpdateSetter를 통해 객체를 구성한 후에는 멀티스레드 환경에서 안전한 사용을 할 수 있다.
  • SQL 업데이트임을 나타내는 재사용 가능한 객체이며, 여러 형식의 Update 메서드를 제공한다.

 

  • StoredProcedure해당 타입의 서브클래스는 execute 메서드를 통해 저장 프로시저를 호출한다.
  • 관계형 데이터베이스의 저장 프로시저를 추상화한 클래스이다.

등이 존재한다.

 

Statement vs PreparedStatement

Statement의 Flow?

  1. 쿼리 문장의 분석
  2. 쿼리 컴파일
  3. 쿼리 실행

Statement는 매 작업시 위의 작성된 flow를 따르게 된다.

 

PreparedStatement는 이미 반환한 트랜잭션에 대해서는 (주로 조회성) 캐싱을 함으로써 이미 실행한 쿼리에 대해서는 불 필요한 Flow를 진행하지 않고 바로 값을 반환하게 된다.

 

 

Statement의 String SQL?

  • Statement 객체는 SQL 쿼리를 문자열 변수를 통하여 받게된다. Param들에 대해서도 문자열 연산을 통하여 제공받아야 되는데 이는
    • 코드의 가독성을 떨어트리고
    • SQL 인젝션에 취약한 환경을 만들며
    • 쿼리의 최적화가 되지 않는다.
String final query = "INSERT INTO PERSON(ID, NAME) VAULES("+postPerson.getId()+" ","+postPerson.getName()+")";

 

 

PreparedStatement의 String SQL?

  • PreparedStatement는 인자로 넘기는 문자열에 대하여서 parameters 기능을 제공한다.코드가 더 길어보이지만, 가독성을 생각하면 좀 더 나은 모습을 보인다.
String final query = "INSERT INTO PERSON(ID, NAME) VALUES(?, ?)"; 
PreparedStatement pr = con.preparedStatement(query); 

for (PostPerson person : PostPersons){ 
	pr.setInt(1, person.getId()); 
    pr.setString(2, person.getName()); pr.addBatch(); 
} 
preparedStatement.executeBatch();

 

 

PreparedStatement의 사전 컴파일, 일괄 실행?

 

사전 컴파일?

  • DB가 SQL을 받았을 경우 우선적으로 캐시를 확인하게끔 동작한다.
  • 캐시가 존재한다면 해당 정보를 제공하고, 캐시되지 않은 경우 쿼리를 실행하고 데이터를 캐싱한다.
  • 해당 기능은 non-SQL binary protocol 를 내부적으로 사용함으로써 DB와 JVM간의 통신 속도를 높인다. (패킷에 데이터가 적기 때문에 서버 간의 통신이 빨라진다. )

 

일괄 실행?

  • 대용량 쿼리를 한번에 전송 가능한 기능이다.
  • addBatch를 통하여 쿼리를 메모리에 적재하고, executeBatch를 통하여 한번에 전송한다. (내부적으로 ArrayList 를 활용한다.)
    • 매번 통신을 연결하고 데이터 전달을 진행하는 리소스들을 줄인다.
    • 한번에 여러 쿼리를 질의함으로써 불필요한 절차를 제거하고 빠르게 결과를 받는다.
// StatementImpl protected Query query; 
// mysql sub class ClientPreparedStatement 

public void addBatch() throws SQLException { 
	try { 
    	synchronized(this.checkClosed().getConnectionMutex()) { 
        	QueryBindings<?> queryBindings = ((PreparedQuery)this.query).getQueryBindings(); 
            queryBindings.checkAllParametersSet(); 
            this.query.addBatch(queryBindings.clone()); 
        } 
    } catch (CJException var6) { 
    	throw SQLExceptionsMapping.translateException(var6, this.getExceptionInterceptor()); 
    }     
} 

public void addBatch(String sql) throws SQLException { 
	try { 
    	synchronized(this.checkClosed().getConnectionMutex()) { 
        	this.batchHasPlainStatements = true; 
            super.addBatch(sql); 
    	} 
    } catch (CJException var6) { 
    	throw SQLExceptionsMapping.translateException(var6, this.getExceptionInterceptor()); 
    } 
} 

// AbstractQuery 
public void addBatch(Object batch) { 
	if (this.batchedArgs == null) { 
    	this.batchedArgs = new ArrayList(); 
    } 
    this.batchedArgs.add(batch); 
}

 

결론

PreparedStatement는 Statement가 가지고 있던 불필요한 절차, 리소스 사용, Non-Caching 등의 요소들을 최적화한 하위 클래스이다. DB와의 쿼리 성능이 중요한 Server든 아니든간에 유의미한 성능차이를 보이므로, PreparedStatement를 사용하는 것이 좋다.

 

참고자료

Proxy Pattern?

Proxy Pattern?

실제 비즈니스 로직과 그 외의 코드를 분리시키는 역할을 수행하는 객체를 만드는 패턴이다.

-> 실제로 로직을 수행하는 객체의 인터페이스 역할을 하게된다. 

 

객체를 호출하는 지점에서 프록시 객체를 호출하여 부가적인 코드를 수행한 뒤 참조 변수를 통해 실제 서비스 객체를  호출한다.

 

 

클라이언트는 프록시를 통해 서비스 객체 기능을 사용하기에

  • 접근 권한
  • 부가기능
  • 리턴 값 변경
  • 로깅
  • 트랜잭션 등

이러한 기능을 추가 할 수 있다.

 

즉 서비스 객체의 SRP를 해치지 않고 프록시를 통해 책임을 추가할 수 있다.

 

 

해당 Pattern의 특징

  1. 프록시 객체는 실제 서비스 객체와 같은 이름의 메서드를 구현한다.
  2. 프록시 객체는 합성을 통해 서비스 객체에 대한 참조 변수를 가진다.
  3. 프록시 객체는 실제 서비스의 메서드를 호출하고 그 결과를 클라이언트에게 반환한다.
  4. 프록시 객체는 실제 서비스 메서드의 호출 전, 후에 별도의 로직을 수행할 수 있다.

 

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

 

 

CGLIB Library를 통한 Dynamic Proxy 구현 방식

 

CGLIB Dynamic Proxy (Spring, Hibernate - embed Library)?

코드 생성 라이브러리(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

 

 

예제 Repo

Lob-dev/JavaEE-DesignPattern

 

Lob-dev/JavaEE-DesignPattern

Java EE 디자인 패턴 학습 Repo. Contribute to Lob-dev/JavaEE-DesignPattern development by creating an account on GitHub.

github.com

 

 

참고 자료

외부, 다른 도메인의 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 빈을 사용하지 않고 수동으로 재정의한 것을 사용함으로써 변경된 인스턴스를 반환하게 된다.

 

RestTemplate에서 제공하는 메서드

 

HTTP DELETE

  • delete()

    지정된 URL 에서 특정 Resource를 삭제한다.

      restTemplate.delete("http://example.com/Posts/{postId}", post.getId());
    

     

 

HTTP GET

  • getForObject()

    지정된 URL에 대한 GET을 수행하고 응답이 있는 경우에 해당 Resource를 반환한다.

      // URL에 제공될 PathVarialbe에 대하여서 값을 전달할 수 있다.
      String result = restTemplate.getForObject(
              "http://example.com/Location/{locationId}/Posts/{postId}", String.class
              , "42", "21");
  • getForEntity()

    지정된 URL 에 대한 GET을 수행하여 ResponseEntity를 반환받는다.

      ResponseEntity<String> response = restTemplate.getForEntity(
              "http://example.com/Posts/{postId}", String.class, "1");

 

HTTP HEAD

  • headForHeders()

    URL로 지정된 리소스의 모든 헤더를 검색하고 반환한다. (HttpHeaders Type)

      HttpHeaders httpHeaders = restTemplate.headForHeaders(url);
      // httpHeaders.getContent()...

    HttpHeaders?

    HTTP 요청이나 응답에 포함된 헤더를 나타내는 일종의 데이터 구조(객체)이다.

    문자열 헤더 이름을 문자열 값 목록에 매핑하고 유형에 따른 접근자를 제공한다.

    • getFirst(String) : 주어진 헤더 이름과 관련된 첫 번째 값을 반환한다.

    • 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가 적용되지 않는다.

      WebClient client = WebClient.create();
    
      WebClient client = WebClient.create("http://example.com/");
  • 추가 :) TCP 커넥션 시 연결 시간에 대한 설정 정보를 커스텀하는 방법 ( TcpClient )

      TcpClient tcpClient = TcpClient
        .create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 연결 시간 제한을 설정
        .doOnConnected(connection -> {
            connection.addHandlerLast(new ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS));
            connection.addHandlerLast(new WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS));
        });
    
      WebClient client = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(HttpClient.from(tcpClient)))
        .build();

 

WebClient에서 제공하는 메서드

 

Any Method

  • method(HttpMethod method)

      WebClient webClient = WebClient.create("http://example.com/");
    
      Post post = webClient.method(HttpMethod.GET)
              .url("/posts")
              .retrieve()
              .bodyToMono(Post.class);
  • mutate()

    해당 WebClient 인스턴스의 속성을 변경(정의)하는 빌더를 반환한다.

  • retrieve()

    앞서 체이닝 된 정보대로 HTTP 요청을 수행하고 응답 본문을 가져오게 된다.

  • exchange()

    응답 상태 및 헤더를 가진 클라이언트의 응답 엔티티를 반환받는다.

    해당 엔티티에서 응답 본문을 가져올 수 있다.

 

HTTP GET

  • get()

    요청을 통해 단일 리소스나 여러 건의 리소스를 가져올 수 있다.

    • bodyToFlux(Obejct.class) : 여러건의 리소스 반환

    • bodyToMono(Object.class) : 단건 리소스 반환

      WebClient webClient = WebClient.create("http://example.com/");
      
      public Mono<Post> findById(Long id) {
            return webClient.get().url("/posts/"+ id)
                    .retrieve().bodyToMono(Post.class);
      }
      
      public Flux<Post> findAll() {
            return webClient.get()
                    .uri("/posts/")
                    .retrieve()
                    .bodyToFlux(Post.class);
      }

 

HTTP POST

  • post()

    일반적으로 요청을 통해 리소스를 만드는 데 사용된다.

      webClient.post()
              .url("/posts")
              .body(Mono.just(entity), Post.class)
              .retrieve()
              .bodyToMono(Post.class);
    
      Post savedPost = Mono.just(entity, Post.class);

 

HTTP DELETE

  • delete()

    일반적으로 요청을 통해 리소스를 삭제하는 데 사용된다.

      WebClient webClient = WebClient.create("http://example.com/");
    
      return webClient.delete()
              .url("/post/" + "1")
              .retrieve()
              .bodyToMono(Void.class);

 

HTTP HEAD

  • head()

    요청을 통해 해당 URI의 지원 헤더를 응답받는 데 사용한다.

 

HTTP PUT

  • put()

    요청을 통해 하나의 리소스 정보를 업데이트하는 데 사용된다.

      WebClient webClient = WebClient.create("http://example.com/");
    
      return webClient
              .put()
              .url("/posts/" + "1")
              .retrieve()
              .bodyToMono(Void.class);

 

HTTP OPTIONS

  • options(). options()

    요청을 통해 지원하는 메서드 정보들을 응답받는다.

 

HTTP PATCH

  • patch()

    요청을 통해 리소스의 일부분을 업데이트하는 데 사용된다.

      WebClient webClient = WebClient.create("http://example.com/");
    
      Post modifyEntity = webClient
              .options()
              .uri("/posts")
              .retrieve()
              .bodyToMono(Post.class);

 

RestTemplate vs WebClient

 

RestTemplate

앞서 말한 것처럼 RestTemplate는 내부적으로 하나의 요청에 하나의 스레드를 생성하여 동작시키는 요청당 스레드 모델을 구현한 Java Servlet API를 사용한다.

 

각각 스레드가 일정량의 메모리와 CPU주기를 사용하고, 응답을 받을 때까지 스레드가 차단되게 되는데, 이는 요청마다 스레드를 생성하기에 많은 스레드를 생성하여 스레드 풀을 소모하거나 사용 가능한 모든 메모리를 차지할 수 있으며 빈번한 CPU 콘텍스트 (스레드) 전환으로 인해 성능 저하가 발생할 수 있다.  (요청 수에 연관되어 성능 저하가 발생한다.)

 

WebClient

각 이벤트에 대해 Task를 만들고 대기열에 넣은 뒤 적절한 응답이 있는 경우에만 실행하게 된다. 즉 이벤트 루프 방식을 구현한 Reactive Streams API를 사용한다.

→ 응답이 있는 경우에만 실행 스레드가 동작하게 된다.

 

해당 방식은 RestTemplate에 비해 적은 스레드와 리소스를 사용하면서 동시에 더 많은 로직을 처리할 수 있다. (요청 수에 관계없이 일정한 성능을 제공하는 것에 의의를 둔다.)

 

선택?

상황에 따라 다를 수 있지만 들어오는 요청의 수가 많다고 가정하면 WebClient, 비동기 방식을 사용하는 것이 일반적으로 좋은 선택이다. (필요한 만큼만의 스레드, 리소스를 사용하기 때문이다.)

→ 대부분 이런 상황일 수 있기에 WebClient를 권장하는 것 같다.

 

참고 자료

+ Recent posts