본문 바로가기

SpringBoot

ch3 08. @Import와 @Conditional

6. Condition과 @Conditional

- 조건에 따라 빈의 등록 여부를 결정. @Bean, @Component와 같이 사용

- Condition의 matches()를 구현한 클래스를 @Conditional로 지정

class FalseCondition implements Condition {
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    	return false;
    } 
}

class TrueCondition implements Condition {
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    	return true;
    }
}

Condition 인터페이스를 구현한 조건 클래스 작성.
조건 중 false가 반환되면 빈 등록 X, true가 반환되면 빈 등록 O 

 

이렇게 어떤 조건을 만든 다음에 @Component나 @Bean 애너테이션에 같이 @Conditional을 붙여준다. 

@Component
@Conditional(FalseCondition.class)
class Engine {public String toString() {return "Engine{}";} }

@Component
@Conditional(TrueCondition.class)
class Door {public String toString() {return "Door{}";} }

 @Conditional이 없을 시 무조건 빈을 등록하는데, @Conditional을 이용해서 조건을 만족할 때만 빈이 등록되도록 
하는 것이다. 

예제에서는 Engine은 빈으로 등록 X, Door는 빈으로 등록 O

 

 

* 조건부 빈 등록
1) @Bean @Component + @Conditional 
개별 빈. 한 개에 대해서 조건적으로 등록

2) @Configuration + @Import + @ImportSelector

빈 여러 개를 묶어놓고, 이 빈을 통째로 등록할지 말지를 @ImportSelector가 조건부 결정을 한다. 

 

 

* 스프링부트 핵심 2가지

1) Starter : 의존 라이브러리 자동 관리

2)  AutoConfiguration : 빈 등록 자동 관리 (여기 내용!)

 

 

* Conditional

1) Condition 인터페이스 구현 -> metadata는 @Conditional이 붙은 클래스가 metadata로 들어온다. 

그래서 어떤 클래스에 @Conditional을 붙였는지 metadata를 통해 알 수 있다. 

ConditionCotext는 빈 목록, 환경 정보와 같은 정보들을 제공받을 수 있다. 

 

 

 

6. Condition과 @Conditional

- 그 밖에 여러 조건부로 빈을 등록하는 여러 애너테이션을 제공

조건부 빈 등록 애너테이션 설 명
@ConditionalOnBean 지정한 빈들이 이미 등록되어 있을 때
@ConditionalOnClass 지정된 클래스들이 classpath에 있을 때
@ConditionalOnMissingBean 지정한 빈들이 아직 등록되어 있지 않을 때
@ConditionalOnMissingClass 지정된 클래스들이 classpath에 있지 않을 때
@ConditionalOnProperty 지징된 property가 지정된 value와 일치할 때
@ConditionalOnResource 지정된 리소스가 classpath에 있을 때
@ConditionalOnJava
지정된 JVM 버전에서 application이 실행 중 일 때
@ConditionalOnWebApplication 웹 애플리케이션일 때
@ConditionalOnNotWebApplication 웹 애플리케이션이 아닐 

 

 

 

7. @import와 ImportSelector

- 조건에 따라 다른 Configuration을 적용할 때 사용

- ImportSelector(조건 작성)를 구현하고 이를 @Import하는 애너테이션을 작성해서 적용 

1) 먼저 ImportSelector를 구현

-> if문으로 자바 설정 클래스를 지정

EnableMyAutoConfiguration이라고 애너테이션을 만들고, 거기에 @Import를 붙이면 EnableMyAutoConfiguration이
importingClassMetadata가 된다.

importingClassMetadata는 @Import 어노테이션이 붙은 대상(애너테이션, 클래스..)가 들어간다. 

거기에서 AnnotationAttributes.fromMap을 이용해서 애너테이션에 지정된 값을 읽어올 수 있다. 

Value = "test"와 같아서 간단히 test를 쓴 것. AnnotationAttributes는 이런 것들을 말하며, 사용자가 어떤 값들을 지정했는지

읽어오려고 하는 것이다. 

읽어온 값을 확인해서 해당하는 설정 파일을 반환한다. 

 

 

 

<실습>

[Main.java]

@Component
@Conditional(TrueCondition.class)
class Engine {
    @Override
    public String toString() {
        return "Engine{}";
    }
}

@Component
@Conditional(FalseCondition.class)
class Door {
    @Override
    public String toString() {
        return "Door{}";
    }
}
class TrueCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return true;
    }
}

class FalseCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }
}


//@SpringBootApplication // 은 아래의 3개 애너테이션을 붙인것과 동일
@Configuration
//@EnableAutoConfiguration
@ComponentScan
public class Main {
    public static void main(String[] args) {
        ApplicationContext ac = SpringApplication.run(Main.class, args);
        System.out.println("ac = " + ac);
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        Arrays.sort(beanDefinitionNames); // 빈 목록이 담긴 배열을 정렬
        Arrays.stream(beanDefinitionNames) // 배열의 스트림을 반환
                .filter(b->!b.startsWith("org"))
                .forEach(System.out::println); // 스트림의 요소를 하나씩 꺼내서 출력
    }

    @Bean
    MyBean myBean() {return new MyBean();}
}

    class MyBean {}

조건에 맞는 engine만 빈으로 등록이 되었다. 

이런식으로 조건을 조금 더 복잡하게 설정해서 적용하는 것도 가능하다.

 

 

 

- Import와 Importselector

[Main.java]

3개의 클래스와 설정 파일 생성

class Car {
    public String toString() {
        return "Car{}";
    }
}

class SportsCar extends Car {
    public String toString() {
        return "SportsCar{}";
    }
}

class SportsCar2 extends Car {
    public String toString() {
        return "SportsCar2{}";
    }
}
class MainConfig{ // 자바 설정 파일
    @Bean Car car() {return new Car();}
}

class Config1{ // 자바 설정 파일
    @Bean Car sportsCar() {return new SportsCar();}
}

class Config2{ // 자바 설정 파일
    @Bean Car sportsCar() {return new SportsCar2();}
}
public class Main {
    public static void main(String[] args) {
//        ApplicationContext ac = SpringApplication.run(Main.class, args);
        ApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig.class, Config1.class, Config2.class);
        System.out.println("ac = " + ac);
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        Arrays.sort(beanDefinitionNames); // 빈 목록이 담긴 배열을 정렬
        Arrays.stream(beanDefinitionNames) // 배열의 스트림을 반환
                .filter(b->!b.startsWith("org"))
                .forEach(System.out::println); // 스트림의 요소를 하나씩 꺼내서 출력

        System.out.println("ac.getBean(\"sportsCar\") = " + ac.getBean("sportsCar"));
    }

자바 설정 파일이 빈으로 등록되었다. 

실제로 어떤게 등록되었는지 출력해보면, SportsCar가 등록된 것을 볼 수 있다. 

순서대로 읽어서 등록을 하니, 앞에서 읽었던 것을 뒤에서 덮은 것이다. 

 

 

 

- 이번엔 @Import를 이용해보자 

public class Main {
    public static void main(String[] args) {
//        ApplicationContext ac = SpringApplication.run(Main.class, args);
//        ApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig.class, Config1.class, Config2.class);
        ApplicationContext ac = new AnnotationConfigApplicationContext(MainConfig.class);
        System.out.println("ac = " + ac);
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        Arrays.sort(beanDefinitionNames); // 빈 목록이 담긴 배열을 정렬
        Arrays.stream(beanDefinitionNames) // 배열의 스트림을 반환
                .filter(b->!b.startsWith("org"))
                .forEach(System.out::println); // 스트림의 요소를 하나씩 꺼내서 출력

        System.out.println("ac.getBean(\"sportsCar\") = " + ac.getBean("sportsCar"));
    }
@Import({Config1.class, Config2.class})
class MainConfig{ // 자바 설정 파일
    @Bean Car car() {return new Car();}
}

class Config1{ // 자바 설정 파일
    @Bean Car sportsCar() {return new SportsCar();}
}

class Config2{ // 자바 설정 파일
    @Bean Car sportsCar() {return new SportsCar2();}
}

MainConfig 설정 파일 하나만 적어주고, @Import로 Config1, Config2를 넣어줘서 다 포함되어 있는 것과 같은 효과를 
만들 수 있다. 

위와 같은 결과를 얻었다. 

 

 

 

- @Import를 조건부로 설정해보자. - ImportSelector를 구현

@EnableMyAutoConfiguration("test")
class MainConfig{ @Bean Car car() {return new Car();}}

class Config1{ @Bean Car sportsCar() {return new SportsCar();}}

class Config2{ @Bean Car sportsCar() {return new SportsCar2();}}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImportSelector.class)
@interface EnableMyAutoConfiguration {
    String value() default "";
}

class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        AnnotationAttributes attr =
                AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(EnableMyAutoConfiguration.class.getName(), false));
//        String mode = "test";
        String mode = attr.getString("value");
        if(mode.equals("test"))
        return new String[]{Config1.class.getName()};  // import할 클래스 지정
        else
            return new String[]{Config2.class.getName()};  // import할 클래스 지정
    }
}

+ 실습해보니 어렵다... 꼼꼼하게 복습하자 ㅠ