스프링 부트 - 자동구성 2

    자동 구성 직접 만들기 - 기반 예제 

    자동 구성에 대해서 자세히 알아보기 위해 간단한 예제를 만들어보자. 실시간으로 자바의 메모리 사용량을 웹으로 확인할 수 있는 기능을 제공함.

    @Getter
    public class Memory {
        private Long used;
        private Long max;
    
        public Memory(Long used, Long max) {
            this.used = used;
            this.max = max;
        }
    
        @Override
        public String toString() {
            return "Memory{" +
                    "used=" + used +
                    ", max=" + max +
                    '}';
        }
    }
    
    
    @Slf4j
    public class MemoryFinder {
    
        public Memory get() {
            long max = Runtime.getRuntime().maxMemory();
            long total = Runtime.getRuntime().totalMemory();
            long free = Runtime.getRuntime().freeMemory();
            long used = total - free;
            return new Memory(used, max);
        }
    
        // 빈으로 등록할 것인데, 잘 등록되었는지 확인하기 위해 로그를 남김
        @PostConstruct
        public void init() {
            log.info("init memory Finder");
        }
    }
    
    @Slf4j
    @RestController
    @RequiredArgsConstructor
    public class MemoryController {
    
        private final MemoryFinder memoryFinder;
    
        @GetMapping("/memory")
        public Memory system() {
            final Memory memory = memoryFinder.get();
            log.info("memory = {}", memory);
            return memory;
        }
    }

    예제 코드는 다음과 같이 작성한다. 

    • Memory
    • MemoryFinder
    • MemoryController
    @Configuration
    public class MemoryConfig {
        
        @Bean
        public MemoryController memoryController() {
            return new MemoryController(memoryFinder());
        }
        
        @Bean
        public MemoryFinder memoryFinder() {
            return new MemoryFinder();
        }
    }
    • 현재 Memory 패키지는 분리되어있음. 분리된 Memory 패키지는 분리해서 라이브러리로 사용하기도 편리함. 
    • Memory 패키지에 있는 기능을 가져다 쓰고 싶다면, 스프링 컨테이너에 스프링빈으로 등록해야한다. 

    현재의 패키지 구조는 다음과 같다.

    패키지 위치

    • 패키지를 memory / hello로 나눈 이유는 두 개가 완전히 별도의 모듈이라는 것을 의미한다. 
    • 결론적으로 hello에서 memory 패키지의 기능을 불러다 사용한다고 이해하면 된다. 

     


    @Conditional

    • 앞서 만든 메모리 조회 기능을 항상 사용하는 것이 아니라 특정 조건일 때만 해당 기능이 활성화 되도록 해보자.
    • 개발 서버에서 확인 용도로만 해당 기능을 사용, 운영 서버에서는 해당 기능을 사용하지 않는 것. 
    • 여기서 핵심은 소스코드를 고치지 않고 이런 것이 가능해야 한다는 점이다. (프로젝트를 빌드해서 나온 빌드 파일을 개발 서버에도 배포, 운영서버에도 배포해야함) 
    • 같은 소스 코드인데 특정 상황일 때만 특정 빈들을 등록해서 사용하도록 도와주는 기능이 @Conditional임.
    package org.springframework.context.annotation; 
    
    @FunctionalInterface
    public interface Condition {
       boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    }

    @Conditional 기능을 사용하려면 먼저 Condition 인터페이스를 구현해야한다.  Condition 인터페이스는 다음과 같다. 

    • matches() 메서드 : 판단함.
      • true 반환 → 조건 만족으로 판단. 동작함.
      • false 반환 → 조건 만족하지 않음으로 판단. 동작하지 않음. 
    • 매개변수
      • ConditionContext : 스프링 컨테이너, 환경 정보등을 담고 있음.
      • AnnotatedTypeMetadata: 어노테이션 메타 종브를 담고 있음. 

     

    구현 목표

    $ java -Dmemory=on -jar project.jar

    여기서 구현할 컨디셔널 빈등록 목표는 자바 실행 옵션에 memory=on이 들어오는 경우에 스프링 빈을 등록하도록 한다. 

    @Slf4j
    public class MemoryCondition implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            // -Dmemory=on
            final String memory = context.getEnvironment().getProperty("memory");
            log.info("memory = {}", memory);
            return "on".equals(memory);
        }
    }

    이것을 위한 Condition 인터페이스를 구현했다. 

    @Configuration
    @Conditional(MemoryCondition.class)
    public class MemoryConfig {
    ...
    }

    구현한 Condition 인터페이스를 사용하기 위해서 @Conditional(MemoryCondition.class)를 MemoryConfig에 붙였다. 이것은 다음과 같이 동작한다.

    • MemoryCondition.class가 True를 반환 → MemoryConfig가 설정 정보로 등록됨.
    • MemoryCondition.class가 False를 반환 → MemoryConfig가 설정 정보로 등록되지 않음.

    실행해보기

    memory.MemoryCondition : memory = null
    • IDE로 실행해보면, 스프링이 시작될 때 다음과 같은 로그가 나온다.
    • MemoryCondition 클래스에서 스프링부트가 시작할 때, matchs() 메서드가 호출된다. 이 때, 실행변수로 주입된 -Dmemory의 값이 on인지 확인하는 절차를 진행할 때 찍힌 로그다.
    • memory = null이면, MemoryCondition은 false를 반환하고 MemoryConfig에 있는 설정 정보는 스프링 컨테이너에 등록되지 않는다. 즉, MemoryController는 자동구성으로 등록되지 않는다. 

    • IDE로 자동 구성을 사용하려면 add VM Option에서 -Dmemory=on을 추가해줘야한다. 

    추가하면 스프링이 시작될 때, MemoryCondition의 matchs() 메서드가 평가된다. 

     


    @Conditional - 다양한 기능

    • 앞에서는 Condition 인터페이스의 구현체를 직접 구현했다.
    • 스프링은 이미 필요한 대부분의 Condition 구현체를 만들어 두었고, 이것을 가져다 쓰기만 하면 된다. 
    @Configuration
    // @Conditional(MemoryCondition.class)
    @ConditionalOnProperty(name = "memory", havingValue = "on") // 위와 같은 기능임. 구현하지 않아도 됨.
    public class MemoryConfig {
    
        @Bean
        public MemoryController memoryController() {
            return new MemoryController(memoryFinder());
        }
    
        @Bean
        public MemoryFinder memoryFinder() {
            return new MemoryFinder();
        }
    }
    • @ConditionalOnProperty(name = "memory", havingValue = "on")을 추가하자.
      • 앞서 Condition 인터페이스를 직접 구현해서 ConditionOn에 넣어준 부분이 모두 포함된다. 
    ...
    @Conditional(OnPropertyCondition.class) // OnPropertyCondition 클래스 바라봄.
    public @interface ConditionalOnProperty {
    ...
    }
    
    @Order(Ordered.HIGHEST_PRECEDENCE + 40) // OnPropertyCondition은 SpringBootCondition과 관련
    class OnPropertyCondition extends SpringBootCondition {
    ...
    }
    
    // SpringBootCondition은 Condition 인터페이스 구현.
    public abstract class SpringBootCondition implements Condition {

    위의 코드의 동작을 살펴보기 위해서 @ConditionalOnProperty 어노테이션을 살펴보면 @ConditionOn + Condition 인터페이스 구현체의 조합으로 동작하는 것을 볼 수 있다. 


    @ConditionalOnXxx

    스프링은 @Conditional과 관련해서 개발자가 편리하게 사용할 수 있도록 많은 @ConditionalOnXxx를 제공함.

    • @ConditionalOnClass, @ConditionalOnMissingClass : 클래스가 있는 경우 동작함. 나머지는 반대.
    • @ConditionalOnBean, @ConditionalOnMissingBean : 빈이 등록되어 있는 경우 동작함. 나머지는 그 반대
    • @ConditionalOnProperty : 환경정보가 있는 경우 동작함.
    • @ConditionalOnResource : 리소스가 있는 경우 동작함.
    • @ConditionalOnWebApplication, @ConditionalOnNotWebApplication : 웹 어플리케이션인 경우 동작함.
    • @ConditionalOnExpression : SpEL 표현식에 만족하는 경우 동작함. 

    @ConditionalOnXxx는 주로 스프링부트의 자동 구성에 사용되고 있다. 


    참고

    @Conditional 자체는 스프링부트가 아니라 스프링 프레임워크가 제공하는 기능이다. 

    댓글

    Designed by JB FACTORY