Spring Framework/토비의 스프링

Chapter 06. 메타 애너테이션과 합성 애너테이션

계란💕 2023. 7. 14. 00:19

메타 애너테이션

 

  • 메타 애너테이션이란?
    • 메타 애너테이션: 애너테이션을 위한 애너테이션을 말한다. ex) @Component
    • @Service, @Controller 를 타고 들어가면 상위에 @Component가 붙어있는 걸 알 수 있다. 
    • 메타 애너테이션이 불은 클래스를 사용하면 기능적으로는 차이가 없다. 
    • 그런데, @Service / @Controller와 같이 다른 이름을 붙이는 것은 WebMvc 에서 컨트롤러 / 서비스 어느 역할인지를 명확히 할 수 있다. 
    • 메타 애너테이션에는 상속 개념이 없다. 
    • 에너테이션에는 @Retention(RetentionPolicy.RUNTIME), @Target 을 줘야한다.
      • @Retention(RetentionPolicy)의 디폴트 값은 RetentionPolicy.CLASS 인데 이는 애너테이션 정보가 컴파일된 클래스 파일까지만 정보가 살아있도록 설정해준다. 즉, 해당 애너테이션이 달린 다른 클래스를 로딩할 때는 정보가 사라진다는 뜻이다. 따라서 RUNTIME을 주도록 한다. 
    • 이 때, 메타 애너테이션의  @Target 안에 있는 ElementType 속성이 일치해야한다. 

 

  • 애너테이션 만들기
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Test
@interface UnitTest {
}

class SimpleHelloServiceTest {
    @UnitTest
    void sayHello() {
        SimpleHelloService helloService = new SimpleHelloService();
        String ret = helloService.sayHello("Test");
        assertThat(ret).isEqualTo("Hello! Test");
    }
    @UnitTest
    void helloDecorator() {
        HelloDecorator decorator = new HelloDecorator(name -> name);
        String ret = decorator.sayHello("Test");
        Assertions.assertThat(ret).isEqualTo("*Test*");
    }
}

 


합성 애너테이션의 적용

  • 합성 애너테이션
    • ex) @RestController = @ResponseBody + @Controller + @Component

 

  Ex) 

<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
public @interface MySpringBootAnnotation {
}

 

  • 아래의 두 개의 빈에 대한 정보가 없으면 에러가 난다. 
<hide/>
@MySpringBootAnnotation
public class HellobootApplication {
    
    @Bean
    public ServletWebServerFactory servletWebServerFactory(){
        return new TomcatServletWebServerFactory();
    }
    
    @Bean
    public DispatcherServlet dispatcherServlet(){
        return new DispatcherServlet();
    }
    
    public static void main(String[] args) {
        SpringApplication.run(HellobootApplication.class, args);
    }
}

 

  • test
<hide/>
public class HelloApiTest {

    @Test
    void helloApi() {
        TestRestTemplate rest = new TestRestTemplate();
        ResponseEntity<String> response = rest.getForEntity(
            "http://localhost:8080/hello?name={name}", String.class, "Spring");
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE)).startsWith(
            MediaType.TEXT_PLAIN_VALUE);
        assertThat(response.getBody()).isEqualTo("*Hello! Spring*");
    }

    @Test
    void failHelloApi() {
        TestRestTemplate rest = new TestRestTemplate();
        ResponseEntity<String> response = rest.getForEntity(
            "http://localhost:8080/hello?name=", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

 

  Note) 실행 결과 - 성공

  • 그런데 위에 HellobootApplication 클래스 안에 있는 두 개의 빈이 없는 경우는 문제가 생긴다. 
  • 따라서, 이에 대한 구성 정보를 다른 클래스에 빼내서 만들 필요가 있다. 
  • @Component를 붙일 수도 있으나 @Configurtion을 붙이는 게 좋다. 
<hide/>
@Configuration
public class Config {
    @Bean
    public ServletWebServerFactory servletWebServerFactory(){
        return new TomcatServletWebServerFactory();
    }
    @Bean
    public DispatcherServlet dispatcherServlet(){
        return new DispatcherServlet();
    }
}

 


빈 오브젝트와 역할과 구분

  • content
  • ???: 스프링 부트가 컨테이너리스한 속성을 지원하기 위해서  내장형 서블릿 컨테이너를 이용하는 독립 실행형 애플리케이션 방식으로 동작하도록 "TomcatServletWebServerFactory", "DispatcherServlet" 이 꼭 필요하다. 
  • 1) 애플리케이션 빈: 개발자가 명시적으로 넣는 정보
    • 애플리케이션 로직 빈 / 애플리케이션 인프라스트럭처 빈으로 구분한다. 
    • 애플리케이션 로직 빈: 애플리케이션의 기능, 비즈니스 로직을 담고 있는 빈들, 사용자 구성 정보(ComponentScan)를 담고 있다. 
    • 애플리케이션 인프라스트럭처 빈: 기술과 관련된 것들로서 개발자가 직접 작성하지는 않는다.  자동 구성 정보(AutoConfiguration)를 담고 있다. ex) TomcatServletWebServerFactory, DispatcherServlet
  • 2) 컨테이너 인프라스트럭처 빈: 스프링 컨테이너 자신이거나 컨테이너가 기능을 확장하면서 추가해온 것들을 빈으로 등록

 


인프라 반 구성 정보의 분리

  • 기존에 만들었던 구성 정보를 담고 있는 Config 라는 클래스는 사용자 구성 정보에 담지 말아야한다. 
    • 컴포넌트 스캔 대상에서 제외시켜야한다. 
    • 제외시키기 위해 다음과 같이 새로운 폴더를 만들어서 config 클래스를 이동한다. 
    • 이 상태로 애플리케이션을 돌리면? 

  Note) 실행 결과

  • 다음과 같이 서버가 뜨지 않는다
  • 기본적으로 같은 패키지 안에 있는 정보들만 
  •  스캐너는 config 클래스 안에 있는 두 개의 팩토리 빈을 실행할 수 없다. 
  •  
***************************
APPLICATION FAILED TO START
***************************

Description:

Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.

Action:

Check your application's dependencies for a supported servlet web server.
Check the configured web application type.

 

  • 해결
    • 따라서,  Config를 컴포넌트 스캔 대상은 아니더라도 구성 정보에는 포함이 되도록 만들어야한다. 
    •  @Import를 MySpringBootApplication 인터페이스 위에 붙이고 안에 Config.class 를 넣어준다. 
    • 클래스 이름을 추가하면 구성 정보를 추가할 수 있다.  

 

  • 다음과 같이 클래스를 나눈다. 
    • 기존의 config 클래스를 다음과 같이 두 개의 클래스로 분리했다. 
    • 해당 클래스들은 나중에 자동 구성의 대상으로 삼을 것이다.
@Configuration
public class TomcatWevServerConfig {
    @Bean
    public ServletWebServerFactory servletWebServerFactory() {
        return new TomcatServletWebServerFactory();
    }
}

 

@Configuration
public class DispatcherServletConfig {
    @Bean
    public DispatcherServlet dispatcherServlet() {
        return new DispatcherServlet();
    }
}

 

 

  • 그런데 여기서 문제는?
    • 구성 정보가 늘어날 때마다 import() 안에 데이터가 추가될 것이다. 
    • 최상위 레벨의 애너테이션에 어떤 정보가 나열되는 것은 바람직하지 않다. 
    • 따라서 새로운 애너테이션을 만들고 이 애너테이션을 MySpringBootApplication에 붙인다. 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({TomcatWebServerConfig.class, DispatcherServletConfig.class})
public @interface EnableMyAutoConfiguration {

}

 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
@ComponentScan
@EnableMyAutoConfiguration
public @interface MySpringBootApplication {
}

동적인 자동 구성 정보 등록(AutoConfiguration)

  • 구성 정보들을 동적으로 추가하려면 어떻게 해야할까?
  • ImportSelector 인터페이스를 이용한다.
    • selectImport() 메서드를 하나 가지고 있다. 
  • 아래 메서드의 반환값 안에 import 할 configuration 클래스의 이름을 string으로 만들어준다. 
    • 그럼 컨테이너가 해당 클래스들을 구성 정보로 사용한다. 

 

  • 새로운 셀렉터 클래스를 만든다. 
<hide/>
public class MyAutoConfigImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{
            "tobyspring.config.autoconfig.DispatcherServletConfig",
            "tobyspring.config.autoconfig.TomcatWebServerConfig",
        };
    }
}

 

 

  • 기존에 만들었던 애너테이션에서 import 안에 값을 다음과 같이 바꾼다. 
    • Import를 통해 위에서 넣은 config 클래스 두 개를 가져와서 서버가 정상적으로 뜬다. 
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({MyAutoConfigImportSelector.class})
public @interface EnableMyAutoConfiguration {

}

 


자동 구성 정보 파일 분리

  • MyAutoConfig 라는 클래스를 만든다. 
    • 메타 애너테이션으로 @Config 를 붙인다. 
    •  
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
public @interface MyAutoConfiguration {
}

 

 

  • Selector 클래스
    • setBeanClassLoader(): 
    • 아래와 같이 넣으면 candidates 에는 자동 구성에 사용할 설정 정보에 대한 목록이 들어간다. 
    •  autoConfigs.toArray(new String[0]);
    •  = Arrays.copyOf(autoConfigs.toArray(), autoConfigs.size(), String[].class);
    • load()안에 있는 값들을 모두 읽어와서 String으로 바꿔서 반환한다. 
Arrays.copyOf(autoConfigs.toArray(), autoConfigs.size(), String[].class);

 

 

  • 다음과 같이 
    • List => Array 로 바꿀 수 있다. Java 8부터 가능
 autoConfigs.toArray(new String[0]);

 

 


 

tobyspring.config.autoconfig.TomcatWebServerConfig
tobyspring.config.autoconfig.DispatcherServletConfig

 

resources 폴더 하위에 파일을 만든다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration
public @interface MyAutoConfiguration {

}

<hide/>
2023-07-19 23:58:01.097 ERROR 24660 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.

Action:

Check your application's dependencies for a supported servlet web server.
Check the configured web application type.

 

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
/** 오류
    List<String> autoConfigs = new ArrayList<>();
    ImportCandidates.load(MyAutoConfiguration.class, classLoader).forEach(
    autoConfigs::add);
    return autoConfigs.toArray(new String[0]);
*/
    return new String[]{
        "tobyspring.config.autoconfig.TomcatWebServerConfig",
        "tobyspring.config.autoconfig.DispatcherServletConfig"};
}
  • 오류: ServletWebServerFactory 빈이 존재하지 않아서 서버를 시작할 수 없다. 
    • 위의 코드에서 아래와 같이 "Tomcat", "Dispatcher.." 에 대한 파일명을 직접 넣으면 서버가 정상 구동되지만 위의 autoConfigs.코드를 이용하면 서버가 구동되지 않는다. 
  • 원인
    • ??
  • 해결

 


자동 구성 애너테이션 적용

  • MyAutoConfigs
    • 다음과 같이 proxyBeanMethods 속성을 false로 변경한다. 
    • 메타 애너테이션이 MyAutoConfigs 의 하위에 있는 애너테이션들이 프록시로 만들어지지 않고 사용되므로 false 설정한다. 
<hide/>
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration(proxyBeanMethods = false)
public @interface MyAutoConfiguration {

}

 

 

 


@Configuration과 proxyBeanMethods

  • 테스트 클래스를 만든다. 
<hide/>
public class ConfigurationTest {

    @Configuration
    static class MyConfig {

        @Bean
        Common common(){
            return new Common();
        }
        @Bean
        Bean1 bean1(){
            return new Bean1(common());
        }
        @Bean
        Bean2 bean2(){
            return new Bean2(common());
        }
    }
    
    static class Bean1{
        private final Common common;

        Bean1(Common common){
            this.common = common;
        }
    }
    static class Bean2{
        private final Common common;

        Bean2(Common common){
            this.common = common;
        }
    }

    static class Common{
    }
}

 

  Ex)  위 클래스 안에 다음 테스트 코드를 짠다. 

  • 실패
    • 주솟값이 다르기 때문에 둘이 동일하지 않다. 
<hide/>
@Test
void configuration(){
    MyConfig myConfig = new MyConfig();
    Bean1 bean1 = myConfig.bean1();
    Bean2 bean2 = myConfig.bean2();
    Assertions.assertThat(bean1.common).isSameAs(bean2.common);
}

 

  • 성공
    • 둘이 동일하다. 
    • 어떻게?
@Test
void configuration(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
    ac.register(MyConfig.class);
    ac.refresh();
    Bean1 bean1 = ac.getBean(Bean1.class);
    Bean2 bean2 = ac.getBean(Bean2.class);
    Assertions.assertThat(bean1.common).isSameAs(bean2.common);
}

 

 

  • 테스트 클래스 내부에  'MyConfigProxy' 라는 static 클래스를 만들어서 확장한다. 
    • 타겟 오브젝트에 대한 접근 방식을 제어하는 프록시를 만든다. 
    • 프록시는 확장해서 대체하는 방식으로 동작한다. 

 

  • 성공
    • 확장한 프록시를 통해서. Common () 메서드가 생성하는 오브젝트가 딱 하나로 제한하고 이를 제한할 수 있는 캐싱하는 방식이다. 
    • 즉, 앞에서 봤던 속성 proixyBeanMethods = true 인 것은 스프링 컨테이너가 프록시 클래스를 생성하고 @Configuration이 붙은 빈 오브젝트로 사용하는 것이다. 
    • 스프링 컨테이너는 @Configuration이 붙은 클래스는 자동으로 프록시를 만들어서 기능을 확장해준다. 
<hide/>
@Test
void proxyCommonMethod() {
    MyConfigProxy myConfigProxy = new MyConfigProxy();
    Bean1 bean1 = myConfigProxy.bean1();
    Bean2 bean2 = myConfigProxy.bean2();
    Assertions.assertThat(bean1.common).isSameAs(bean2.common);
}

 

 

  • 만약 false로 설정한다면?
    • 다음과 같이 경고창이 뜬다. 
    • 경고: 프록시 빈 메서드가 false인 경우에 다른 빈 오브젝트를 직접 호출하는 방식을 사용하는 것은 위험하다. 
    • cf) SchedulingConfiguration 클래스에는 해당 설정이 false 로 되어 있다. 

 

 

 


title

  • content
  • content

 


title

  • content
  • content

 


title

  • content
  • content

출처 - 인프런 토비의 스프링

https://www.inflearn.com/course/%ED%86%A0%EB%B9%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%9D%B4%ED%95%B4%EC%99%80%EC%9B%90%EB%A6%AC/dashboard