자바/스프링 부트

섹션6.자동 구성 기반 애플리케이션

UroJem 2023. 2. 12. 18:29

메타 애노테이션과 합성 애노테이션

Annotaion : 사전적인 의미로 주석. 자바에서 사용될 떄는 코드 사이에 주석처럼 쓰여서 특별한 의미, 기능을 수행하도록 하는 기술. 즉, 프로그램에게 추가적인 정보를 제공해주는 메타데이터라고 볼 수 있다.

JDK 1.5부터도입된 기능으로 정보를 알려주는 기능과 컴파일러에게 문법 에러 체크를 제공하기도 한다. 

DispatcherServlet에게 등록할 Bean을 알려주는 역할도 한다.

 

Meta-data : 데이터에 관한 구조화된 데이터로, 다른 데이터를 설명해주는 데이터.

메타 데이터는 데이터에 대한 데이터이다. 대량 정보 가운데 찾고있는 정보를 효율적으로 찾아내서 이용하기 위해 일적한 규칙에 따라 컨텐츠에 부여되는 데이터.

 

Meta-Annotaion : 다른 Annotaion 에서 사용되는 Annotaion을 말한다. @Controller와 @Service 어노테이션은 빈으로 등록되기 위해 @Component를 포함하고 있는데 이 어노테이션 안에서 사용되고 있는 어노테이션인 @Component를 메타 어노테이션이라고 부른다. 보통 커스텀 어노테이션을 만들 때 많이 사용된다.

그 외 @Target, @Retention, @Document 등등이 있다.

애노테이션을 다른 애노테이션의 메타 애노테이션으로 사용하기 위해 @Target 메타 애노테이션에 ElementType.ANNOTATION_TYPE 이 선언되어야 한다.

 

https://math-coding.tistory.com/182

 

[Java] Annotation, Meta data란?

이 글은 "자바 온라인 스터디"를 통해 공부한 내용을 작성하였습니다. Annotation이란? Anntation은 JDK 1.5부터 도입된 것으로 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종입니다. 대

math-coding.tistory.com

https://sanghye.tistory.com/39

 

[Spring] Meta Annotation 이란?(@Target, @Retention)

Spring 에서는 Anntotation 사용에 대한 기능을 상당히 많이 제공하고 있습니다. 주로 사용하는 @Controller, @Service, @Repostiroy 등 많은 Annotation 이 존재합니다. 해당 Annotion 은 각 기능에 필요한 만큼 많은

sanghye.tistory.com

 

 

package tobyspring.helloboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@UnitTest
@interface FastUnitText {

}

// 어플리케이션 LifeCycle에서 annotaion이 유지되는 범위
// 컴파일 이후 런타임시에도 참조가능 주로 리플렉션이나 로깅에 사용
@Retention(RetentionPolicy.RUNTIME)
// Meta Annotaion으로 사용하기 위해 Annotation 타입에 적용 가능하도록 Target 정의
// 메소드 범위에서 사용
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
// custom Test annotation 사용을 위해 meta annotaion @Test 사용
@Test
@interface UnitTest {

}

public class HelloServiceTest {

	@FastUnitText
	void simpleHelloService(){
		SimpleHelloService helloService = new SimpleHelloService();

		String ret = helloService.sayHello("Test");

		Assertions.assertThat(ret).isEqualTo("Hello Test");
	}

	@UnitTest
	void helloDecorator() {
		HelloDecorator helloDecorator = new HelloDecorator(name -> name);

		final String ret = helloDecorator.sayHello("Test");

		Assertions.assertThat(ret).isEqualTo("*Test*");
	}
}

 

Composed-Annotaion: 메타 애노테이션을 하나 이상을 적용해서 만드는 경우. @RestController의 경우 @Controller와 @ResponseBody를 사용하고 있다. 여러개의 애노테이션이 붙어있으면 복잡해 보이기도 하는데 커스텀 애노테이션을 만들어서 해당 애노테이션에서 사용하고 커스텀한 애노테이션을 붙이면 깔끔해 보인다.

 

package tobyspring.helloboot;

import org.springframework.boot.SpringApplication;

import tobyspring.config.MySpringBootApplication;

@MySpringBootApplication
public class HellobootSpringApplication {

	public static void main(String[] args) {

		//MySpringApplication.run(HellobootSpringApplication.class, args);
		SpringApplication.run(HellobootSpringApplication.class, args);

	} // end of main

} // end of class
package tobyspring.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

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

}

 

 

Bean Object의 역할과 구분

애플리케이션 빈 : 개발자가 사용할 빈 구성 정보 제공

  • 애플리케이션 로직 빈 : 애플리케이션의 기능, 비지니스 로직, 도메인을 담고있는 빈
    • HelloController, SimpleHelloService
  • 애플리케이션 인프라스트럭쳐 빈 : 개발자가 만드는게 아닌 이미 만들어진 기능을 사용한다고 명시적으로 구성정보를 작성한 빈
    • DataSource, JpaEntityManagerFactory, JdbcTransactionManager

 

컨테이너 인프라스트럭쳐 빈 : 스프링 컨테이너 자신이거나 스프링 컨테이너가 기능을 확장하며 빈으로 등록해서 사용하는 빈. 개발자가 요청한 것이 아닌 컨테이너 스스로 필요한 부분에서 등록하여 동작 시킴

  • ApplicationContext, BeanFactory, BeanPostProcessor, DefaultAdvisorAutoProxyCreator, Enviroment, BeanFactoryPostProcessor

 

TomcatServletWebServerFactory와 DispatcherServlet은 애플리케이션 동작 시키는데 중요한 Bean들이고 없으면 서비스가 돌아가지 않기때문에 컨테이너 인프라스트럭쳐 빈이라고 생각할 수 있지만 스프링 컨테이너가 스스로 등록시키는 작업을 하지 않고 개발자가 명시적으로 선언을 해줘야 한다는 점에서 애플리케이션 인프라스트럭쳐 빈이라고 볼 수 있다.

 

HelloController와 SimpleHelloService는 @ComponentScan 애노테이션으로 스프링이 시작될 때 스캔을 하며 빈을 등록하는데 구성정보인 TomcatServletWebServerFactory와 DispatcherServlet는 어떻게 등록할까?

자동 구성정보(AutoConfiguration) 메카니즘을 사용한다.

 

자동구성정보는 어떤 구성방식으로 적용되는가?

애플리케이션 작동에 필요한 애플리케이션 인프라스트럭쳐 빈을 각각 @Configuration 애노테이션으로 설정 정보를 가진 Bean으로 만들어서 기능 구분을 하고 스프링부트가 애플리케이션 필요에 따라 해당하는 구성정보를 가진 빈을 등록하고 자동 적용한다.(AutoConfiguration)

 

 

로직 빈과 인프라스트럭쳐 빈 패키지 분리

AutoConfig 대상 객체 분리

 

 

package tobyspring.config.autoconfig;

import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;

import tobyspring.config.MyAutoConfiguration;

@MyAutoConfiguration
public class DispatcherServletConfig {

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

}
package tobyspring.config.autoconfig;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;

import tobyspring.config.MyAutoConfiguration;

@MyAutoConfiguration
public class TomcatWebServerConfig {

	@Bean
	public ServletWebServerFactory servletWebServerFactory(){
		return new TomcatServletWebServerFactory();
	}

}
package tobyspring.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAutoConfigImportSelector.class)
public @interface EnableMyAutoConfiguration {
}

AutoConfig 대상 동적으로 불러오는 구문을 가진 대상 Import하는 애노테이션

package tobyspring.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyAutoConfigImportSelector implements DeferredImportSelector {

	private final ClassLoader classLoader;

	public MyAutoConfigImportSelector(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		List<String> autoConfigs = new ArrayList<>();

		// META-INF.spring.full-qualified-annotation-name.imports 라는 파일을 읽어와서
		// auto config 대상들을 load한다
		ImportCandidates.load(MyAutoConfiguration.class, classLoader).forEach(autoConfigs::add);

		return autoConfigs.toArray(new String[0]);
	}
}

AutoConfig 대상 객체를 동적으로 추가해서 가져오는 ImportSelector 객체 사용

package tobyspring.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Configuration;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Configuration(proxyBeanMethods = false)
public @interface MyAutoConfiguration {
}

 

 

@Configuration과 proxyBeanMethods

 

@Component 애노테이션이 클래스에 붙으면 스프링 컨테이너 런타임시 @ComponentScan에 의해 Bean이 등록되고 컨테이너에 의해 객체가 1개만 생성되도록 컨트롤 된다.

 

@Bean 애노테이션은 메소드 레벨 선언으로 개발자가 직접 제어가 불가능한 외부 라이브러리를 Bean으로 등록할 때 사용한다. 생성되는 객체는 모두 다르다

 

@Configuration 애노테이션이 붙은 클래스 내에서 @Bean메서드를 사용해 객체생성을 하면 프록시 패턴이 적용되어 등록되는 빈이 싱글턴으로 생성됨이 보장된다.

 

1개의 객체만 생성되야 하는 Bean이 @Bean으로 같은 객체가 여러개 생성되지 못하게 하려고 클래스에 @Configuration 애노테이션을 사용한다.

이를 제어하기 위해 proxyBeanMethods 설정값이 나왔는데 proxyBeanMethods = false 로 @Configuration을 설정할 시 @Bean으로 여러 객체를 생성할 수 있다.

이는 실수를 유발할 수 있는 상황이 생기기도 하지만

@Bean 애노테이션을 사용하여 객체 생성시 또 다른 bean을 의존하지 않아 객체를 여러개 호출하지 않는 코드로 만들지 않았을 경우 비싼 Proxy 객체를 생성하여 시간과 비용을 낭비하여 등록할 필요가 없기 때문

 

package tobyspring.study;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

public class ConfigurationTest {

	@Test
	void configuration(){
		AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
		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);
	}

	@Test
	void proxyCommonMethod() {
		final MyConfigProxy myConfigProxy = new MyConfigProxy();

		final Bean1 bean1 = myConfigProxy.bean1();
		final Bean2 bean2 = myConfigProxy.bean2();

		Assertions.assertThat(bean1.common).isSameAs(bean2.common);

	}

	static class MyConfigProxy extends MyConfig {
		private Common common;

		@Override
		Common common() {
			if (this.common == null) this.common = super.common();

			return this.common;

		}
	}

	@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;
		}
	}

	private static class Common {

	}
}