스프링 코어 #
아래 클래스는 스프링 IoC 컨테이너 (이하 IoC 컨테이너) 가 스캐닝한다.
자바 구성 클래스 (Java Configuration Class)
@Configuration
@Bean
자바 컴포넌트 클래스 (Java Component Class)
@Component
@Controller
@Service
@Repository
@Configuration
public class SequenceGeneratorConfiguration {
@Bean
public SequenceGenerator sequenceGenerator() {
SequenceGenerator seqGen = new SequenceGenerator();
...
return seqGen
}
}
스프링은 @Configuration 의 클래스를 스캐닝하면, 그 안에서 (Bean 인스턴스를 생성해 반환하는) @Bean 자바 메소드를 찾는다.
기본적으로 Bean
의 이름은 메소드 명을 따라간다. Bean
의 이름을 따로 명시하려면 @Bean
의 name
값을 설정한다.
IoC 컨테이너 #
위의 Annotation(@Configuration, @Bean, @Component, …) 를 스캐닝하기 위해선, 우선 Ioc 컨테이너를 인스턴스화 해야 한다.
즉, IoC 컨테이너를 먼저 생성해야 한다.
스프링은 2 종류의 IoC 컨테이너(구현체, 인터페이스 등)를 제공한다.
- Bean Factory
- Application Context
(* Application Context 는 빈 팩토리를 확장하기에, 되도록 Application Context 를 사용하는 것을 권장한다.)
Application Context (인터페이스)의 구현체는 여러 개 있지만, 그 중 AnnotationConfigApplicationContext
를 권장한다.
(IoC 인터페이스) (IoC 구현체) (자바 구성 클래스)
ApplicationContext context = new AnnotationConfigApplicationContext(SequenceGeneratorConfiguration.class);
IoC 컨테이너에서 POJO 인스턴스(이하 빈) 가져오기 #
// context.getBean("빈 이름") 은 Object 형태를 return 한다.
SequenceGenerator generator = (SequenceGenerator) context.getBean("sequenceGenerator);
// context.getBean("빈 이름", "타입") 의 형태도 가능하다.
SequenceGenerator generator = context.getBean("sequenceGenerator, SequenceGenerator.class);
( * Bean 이 한 개라면 빈 이름을 생략할 수 있다. )
@Component 로 Bean 생성하기 #
public class Sequence {
...
}
public interface SequenceDao {
...
}
@Component("sequenceDao") <-- @Component 사용, Bean ID 는 sequenceDao
public class SequenceDaoImpl implements SequenceDao {
...
}
@Component
에 이름을 설정하지 않으면 class 명의 camel-case 형태로 bean ID 가 설정된다.
@Component
는 Spring 이 스캐닝할 수 있도록 POJO 에 붙이는 범용 Annotation 이다.
Spring 에는 세 계층이 있다.
- Persistence Layer
- Service Layer
- Persentation Layer
@Component
: 범용 Annotation@Repository
: Persistence(영속성) 계층을 위한 Annotation@Service
: Service(서비스) 계층을 위한 Annotation@Controller
: Presentation(표현) 계층을 위한 Annotation
POJO 의 쓰임새(목적)이 명확하지 않을 땐 @Component
를 사용해도 된다. 그러나 되도록 (목적에 맞는) 구체적인 Annotation 을 사용하는 것을 권장한다.
컨테이너 스캐닝 시, 필터(필터링) 적용 (필터로 IoC 컨테이너 초기화) #
( * 스캐닝 하는 객체가 많아 질 때, 모든 패키지를 스캐닝하면 불필요하게 속도가 느려질 수 있다. )
Spring 이 지원하는 필터 표현식 은 4 종류이다.
- annotation
- assignable (assignable_type)
- regex
- aspectj
@ComponentScan(
includeFilters = {
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = {
"com.gg-pigs.*Dao", <-- 해당 패키지 내의 Dao 로 끝나는 클래스 스캐닝 (@Component 등의 Annotation 이 없어도 스캐닝 된다.)
"com.gg-pigs.*Service" <-- 해당 패키지 내의 Service 로 끝나는 클래스 스캐닝
}
)
},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = {org.springframework.stereotype.Controller.class} <-- @Controller 클래스 제외
)
}
)
POJO 의 의존성 해결 #
- Setter 이용
@Autowired
이용 (+@Primary
,@Qualifier
)@Primary
: 타입이 같은 클래스(구현체)가 여럿일 때, 우선순위를 부여한다.@Qualifier
: 타입이 같은 클래스(구현체)가 여럿일 때, 이름으로 무엇을 사용할지 결정할 수 있다.
@Resource
이용@Inject
이용
// Setter 사용
@Configuratin
public class SequenceConfiguration {
@Bean
public DatePrefixGenerator datePrefixGenerator() {
DatePrefixGenerator dpg = new DatePrefixGenerator();
dpg.setPattern("yyyyMMdd");
return dpg;
}
@Bean
public SequenceGenerator sequenceGenerator() {
SequenceGenrator sequence = new SequenceGenrator();
...
sequence.setPrefixGenerator(datePrefixGenerator()); <-- 위의 datePrefixGenerator 메소드 (Bean 이름)
return sequence;
}
}
// Autowired 사용
@Service
public class SequenceService {
@Autowired
private SequenceDao sequenceDao;
public void setSequenceDao(SequenceDao sequenceDao) { <-- @Autowired 를 위해, 이 setter 가 필요한 것으로 알고 있음
this.sequenceDao = sequenceDao;
}
...
}
// Autowired 사용
@Service
public class SequenceService {
@Autowired <-- 위와 동일한 예제, Setter 메소드에 적용했을 경우이다.
public void setSequenceDao(SequenceDao sequenceDao) { <-- @Autowired 를 위해, 이 setter 가 필요한 것으로 알고 있음
this.sequenceDao = sequenceDao;
}
...
}
// 생성자의 경우에도 가능하다.
// Spring 4.3 이상의 버전부터는 생성자가 하나뿐인 클래스에서는 @Autowired 를 생략해도 된다. (기본으로 적용된다.)
// Resource 사용
public calss SequeceGenerator {
@Resource
private PrefixGenerator prefixGenerator;
}
// Inject 사용
public class SequenceGenerator {
@Inject
pirvate PrefixGenerator prefixGenerator;
}
POJO Scope 지정 #
singleton
: IoC 컨테이너 당 Bean 인스턴스 1 개 생성prototype
: 요청할 때마다 Bean 인스턴스 새롭게 생성request
: HTTP 요청 당 1 개의 Bean 인스턴스 생성 (WebApplicationContext 만 해당)session
: HTTP 세션 당 1 개의 Bean 인스턴스 생성 (WebApplicationContext 만 해당)globalSession
: 전역 HTTP 세션 당 1개의 Bean 인스턴스 생성 (PortalApplicationContext 만 해당 (?))
외부 리소스(text, xml, property, image) 사용 #
@PropertySource
:.properties
파일 로드 가능Resource
인터테이스 (+@Value
) : 리소스 로드 가능
// discounts.properties
specialcustomer.discount=0.1
endofyear.discount=0.2
...
@Configuration
@PropertySource("classpath:discounts.peroperties")
@ComponentScan("~")
public class ShopConfiguration {
@Value("${endofyear.discount:0}")
private double specialEndofyearDiscountField;
@Bean
public static PropertySourcePlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public Product dvdrw() {
~~
}
}
// POJO class
public class BannerLoader {
private Resource banner;
public void setBanner(Resource banner) {
this.banner = banner;
}
@PostConstruct <-- Bean 생성 후 실행된다.
public void showBanner() throws IOException {
Files.lines(Paths.get(banner.getURI()), Charset.forName("UTF-8)).forEachOrdered(System.out::println);
}
}
// Configuration class
@Configuration
@PropertySource("classpath:discounts.properties")
@ComponentScan("~")
public class ShopConfiguration {
@Value("classpath:banner.txt") <-- classpath 에 위치한 banner.txt 파일
private Resource banner;
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
~~
}
@Bean
public BannerLoader bannerLoader() {
BannerLoader bl = new BannerLoader();
bl.setBanner(banner);
return bl;
}
}
public class Main {
public static void main(String[] args) throws Exception {
...
Resource resource = new ClassPathResource("discounts.properties"); <-- .properties 파일 읽어들인 후, Resource 객체로 캐스팅
Properties props = PropertiesLoaderUtils.loadProperties(resource); <-- Resource 객체를 Properteis 객체로 변환
System.out.println(props); <-- Properties 내용 출력
}
}
ClassPathResource
: classpath 내부의 리소스 로딩FileSystemResource
: 외부 파일시스템의 리소스 로딩URIResource
: URL 상의 외부 리소스 로딩
Locale 별 다국어 지원 (with Properties) #
MessageSource 인터페이스
- Resource bundle 메세지를 처리하는 메서드가 몇 가지 정의되어 있다.
- 가장 많이 사용되는 구현체 :
ResourceBundleMessageSource
, 로케일 별로 분리된 Resource bundle message 를 해석
@Configuration
public class ShopConfiguration {
// Bean 이름(메소드명)은 반드시 'messageSource' 로 작성해야, ApplicationContext 가 알아서 감지한다.
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:messages");
messageSource.setCacheSeconds(1);
return messageSource;
}
}
‘영어’ 의 로케일을 사용할 때, (자동으로) 아래와 같은 순서를 탐색한다. (?)
messages_en_US.properties
messages_en.properties
messages.properties
POJO 초기화/폐기 커스터마이징 (with Annotation) #
특정한 POJO 는 사용하기 전, 특정한 초기화 작업을 거친다. 예를 들어 File 열기, DB 연결, 메모리 할당 등의 선행 작업이 필요한 경우가 있을 수 있다. (대개 이런 경우 폐기 할 때에도 특정한 작업을 해주어야 한다.)
@Bean
의 속성에 initMethod, destroyMethod 속성을 사용할 수 있다. (초기화, 폐기 콜백 메서드로 인지한다.)@PostConstruct
,@PreDestroy
어노테이션을 사용할 수 있다.- 이 외에
@Lazy
,@DependsOn
의 어노테이션 등이 있다.
// 1) @Bean 속성의 initMethod, destroyMethod 속성을 사용한다.
public class Cashier {
...
public void openFile() throws IOException {
...
}
...
public void closeFile() throws IOException {
...
}
}
@Configuration
public class ShopConfiguration {
@Bean(initMethod = "openFile", destroyMethod = "closeFile") <-- 해당하는 POJO 클래스의 메소드이다.
public Cashier cashier() {
...
}
}
// 2) @PostConstruct, @PreDestroy 어노테이션을 사용한다.
@Component <-- 1) 번 예제와의 차이점이다. 해당 클래스는 직접 @Component 를 붙인 클래스이다.
public class Cashier {
...
@PostConstruct
public void openFile() throws IOException {
...
}
...
@PreDestroy
public void closeFile() throws IOExcetpion {
...
}
}
기본적으로 스프링은 모든 POJO 를 eager 초기화한다. @Lazy
를 붙이면 lazy 초기화로 변경할 수 있다.
lazy 초기화를 하면, 애플리케이션이 요구하거나 다른 POJO 가 참조하여 사용되기 전까지 초기화되지 않는다.
@Component
@Scope("prototype")
@Lazy
public class ShoppingCart {
private List<Product> items = new ArrayList<>();
...
}
@DependsOn
을 사용하여 초기화 순서를 지정할 수 있다. 예를 들어 어떤 POJO 가 다른 POJO 보다 먼저 초기화되도록 지정할 수 있다.
@Configuration
public class SequenceConfiguration {
@Bean
@DependsOn("datePrefixGenerator")
public SequenceGenerator sequenceGenerator() {
// datePrefixGenerator 를 DependsOn 한다고 해서 여기에 datePrefixGenerator 빈을 사용할 필요는 없다.
SequenceGenerator s = new SequenceGenerator();
return s;
}
}
후처리기를 이용하여 POJO 검증/수정하기 #
Bean post-processor(Bean 후처리기) 를 사용하여 초기화 콜백 메소드(initMethod, @PostConstruct) 전후에 원하는 로직을 추가할 수 있다.
Bean 후처리기의 주요한 특징은, IoC 컨테이너 내부의 모든 Bean 인스턴스를 대상으로 한다는 점이다.
보통 Bean property 가 올바른지 검사하거나, 어떤 기준에 따라 Bean property 를 변경하거나, 전체 Bean 인스턴스를 상대로 어떤 작업을 수행하기 위한 목적으로 사용된다.
Bean 후처리기는 BeanPostProcessor
인터페이스를 구현한 객체이다. 스프링은 이 인터페이스를 구현한 Bean 을 발견하면 자신이 관리하는 모든 Bean 인스턴스에 postProcessBeforeInitialization()
, postProcessAfterInitalization()
메소드를 적용한다.
@Component
public class AuditCheckBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeanException {
...
return bean; <-- 인자로 받은 원본 bean 을 무조건 반환해야 한다.
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeanException {
...
// 만약 특정한 Bean 만 처리하고 싶다면 아래와 같은 로직을 사용할 수 있을 것이다.
if (bean instanceof 특정클래스) {
~~~
}
return bean; <-- 인자로 받은 원본 bean 을 무조건 반환해야 한다.
}
}
@Required
로 속성이 설정되었는지 검사할 수 있다.
설정 여부만 검사할 수 있다. null인지, 다른 값인지는 검사하지 않는다.
스프링의 Bean 후처리기 RequiredAnnotationBeanPostProcessor
가 @Required
를 붙인 값들이 설정되었는지 체크한다.
값이 설정되어 있지 않다면, BeanInitializationException
예외를 던진다.
public class SequenceGenerator {
private PrefixGenerator prefixGenerator;
private String suffix;
...
@Required
public void setPrefixGenerator(PrefixGenerator prefixGenerator) {
this.prefixGenerator = prefixGenerator;
}
@Required
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
팩토리(정적 메소드, 인스턴스 메소드, Spring FactoryBean)로 POJO 생성 #
@Bean
메소드 내부에서 팩토리 메소드를 호출해서 POJO 를 생성할 수 있다.Spring 은
FactoryBean
인터페이스를 상속한 템플릿 클래스AbstractFactoryBean
을 제공한다
정적 팩토리 메소드로 POJO 생성
일반 자바 구문을 활용한 케이스이다.
public class ProductCreator {
// 정적 팩토리 메소드
public static Product createProduct(String productId) {
if("aaa".equals(productId)) {
return new Battery("AAA", 2.5);
}
else if("bbb".equals(productId)) {
return new Disc("CD-RW", 1.5);
}
...
}
throw new IllegalArgumentException("Unknown product");
}
@Configuration
public class ShopConfiguration {
@Bean
public Product aaa() {
return ProductCreator.createProduct("aaa"); <-- 정적 팩토리 메소드 사용
}
...
}
인스턴스 팩토리 메소드로 POJO 생성
public class ProductCreator {
// Map 을 사용하여 팩토리 메소드를 구현할 수도 있다.
// private Map<String, Product> products;
public Product createProduct(String productId) {
if("aaa".equals(productId)) {
return new Battery("AAA", 2.5);
}
else if("bbb".equals(productId)) {
return new Disc("CD-RW", 1.5);
}
...
}
}
@Configuration
public class ShopConfiguration {
@Bean
public ProductCreator productCreatorFactory() {
ProductCreator factory = new ProductCreator(); <-- 인스턴스 팩토리 메소드를 사용한다.
...
return factory;
}
@Bean
public Product aaa() {
return productCreatorFactory().createProduct("aaa"); <-- 위에서 정의된 Bean 을 사용한다.
}
...
}
스프링 팩토리 빈으로 POJO 생성하기
(Custom)FactoryBean
은 AbstractFactoryBean
제네릭 클래스를 상속한다.
createInstance()
메소드를 오버라이드 하여 Bean 인스턴스를 생성한다.getObject()
메소드를 오버라이드 하여 자동으로 주입될 수 있도록 한다. (?)- 위에서 언급된 팩토리 메소드들과 유사하지만, Bean 생성 도중 IoC 컨테이너가 식별할 수 있는 스프링의 전용 Bean 이다. (?)
public class DiscountFactoryBean extend AbstractFactoryBean<Product> {
private Product product;
private double discount;
// setter
...
@Override
public Class<?> getObjectType() {
return product.getClass();
}
@Override
protected Product createInstance() throws Exception {
product.setPrice(product.getPrice() * (1 - discount));
return product;
}
}
@Configuration
public class ShopConfiguration {
@Bean
public Battery aaa() {
Battery aaa = new Battery(~);
return aaa;
}
...
@Bean
public DiscountFactoryBean discountFactoryBeanAAA() {
DiscountFactoryBean factory = new DiscountFactoryBean();
factory.setProduct(aaa()); <-- 위에서 정의된 Bean (aaa)
factory.setDiscount(0.2);
return factory;
}
...
}
Spring environment, profile 마다 다른 POJO 로딩 #
(예를 들어, 동일한 POJO 인스턴스를 개발/테스트/운영 환경별로 초기값을 달리하고 싶을 때)
- 자바 Configuration Class 를 여러 개 생성하고, 각 클래스마다 Bean 을 정리한다.
- 의도를 잘 표현할 수 있게 Profile 을 명명한다.
- ApplicationContext 에서 해당하는 Profile 을 설정하여, 해당 POJO 들을 갖고온다.
@Profile
활용
@Configuration
@Profile("global")
public class ShopConfigurationGlobal {
...
}
@Configuration
@Profile({"summer", "winter"})
public class ShopConfigurationSumWin {
...
}
자바 Configuration Class 에 속한 Bean 들은 해당 profile 에 편입된다.
Profile 로드하기
- Profile을 Application에 로드하기 위해서 먼저 해당 profile 을 활성화(activation)한다.
- 프로파일 여러 개를 한 번에 로드하는 것도 가능하다.
- Java Runtime Flag, WAR 파일 초기화 매개변수를 지정해 로드할 수 있다.
- 기본 프로파일이 있어야 하며, 스프링은 활성화시킬 프로파일이 없다면 기본 프로파일을 찾아 적용시킨다.
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getEnvironment().setActiveProfiles("global", "winter"); <-- 프로파일을 활성화한다.
context.scan(~);
countext.refresh();
-Dspring.profiles.active=global,winter <-- 프로파일을 활성화한다.
POJO에서 IoC 컨테이너의 리소스 확인 #
* 컴포넌트(POJO) -> IoC 컨테이너 에 의존관계를 맺는(직접 접근하는(?)) 설계는 바람직하지 않다. 그러나 때로는 Bean(POJO)에서 컨테이너의 리소스를 인지해야 할 수 있다.
Bean 이 IoC 컨테이너 리소스를 인지하려면 Aware
인터페이스를 구현해야 한다.
자주 쓰이는 Aware
인터페이스 종류
BeanNameAware
: IoC 컨테이너에 구성한 인스턴스의 Bean 이름BeanFactoryAware
: 현재의 BeanFactory 를 호출하는 데 쓰인다.ApplicationContextAware
: 현재의 ApplicationContext 를 호출하는 데 쓰인다.MessageSourceAware
: MessageSource, 텍스트 메세지를 해석하는 데 쓰인다.ApplicationEventPublisherAware
: ApplicationEvent(Publisher) 를 발행하는 데 쓰인다.ResourceLoaderAware
: ResourceLoader 를 로드하는 데 쓰인다.EnvironmentAware
: ApplicationContext 인터페이스에 엮인 Environment 인스턴스를 인지하기 위해 쓰인다.
* ApplicationContext
는 MessageSource
, ApplicationEventPublisher
, ResourceLoader
를 상속한 인터페이스이다. 따라서 ApplicationContext
만 인지하면 이들(MessageSource
, ApplicationEventPublisher
, ResourceLoader
)도 액세스할 수 있다. 그러나 요건을 충족하는, 최소한의 범위 내에서 Aware 인터페이스를 사용하는 것이 바람직하다.
* 현재 Spring 최신 버전에서는 Aware 를 사용하지 않아도 된다. @Autowired
등을 통해 ApplicationContext
를 주입받을 수 있기 때문이다.
* Aware 인터페이스를 구현한 클래스는 스프링과 엮이게 되므로 IoC 컨테이너 외부에서는 제대로 작동하지 않는다.
Aware 호출 과정(순서)
- 생성자, 팩토리 메소드를 호출하여 Bean 인스턴스를 생성한다.
- Bean 의 속성들에 값을 주입한다.
- Aware 인터페이스에 정의한 setter 메소드를 호출한다.
- Bean 인스턴스들을 각 Bean 후처리기(postProcessBeforeInitialization()) 메소드로 넘겨 초기화 콜백 메소드를 호출한다.
- Bean 인스턴스들을 각 Bean 후처리기(postProcessAfterInitialization()) 메소드로 넘긴다.
- Bean 을 사용한다.
- 컨테이너가 종료되면 폐기 콜백 메소드를 호출한다.
public class Cashier implements BeanNameAware {
private String fileName; <-- Aware 인터페이스의 setter 메소드가 실행되면서 해당 속성 값이 채워진다.
@Override
public void setBeanName(String beanName) {
this.fileName = beanName;
}
}
@Configuration
...
@Bean(~)
public Cashier cashier() {
final String path = System.getProperty("java.io.tmpdir") + "cashier";
Cashier cashier = new Cashier();
// cashier.setFileName("filename"); <-- Aware setter 메소드를 통해 채워졌다.
cashier.setPath(path);
return cashier;
}
(* 제대로 이해한것인지 모르겠다.)
AOP (with Annotation) #
Aspect 를 정의하려면, 클래스에 @Aspect
를 붙이고 메소드별로 Advice
를 만든다.
Advice
Annotation 은 다음의 종류가 있다.
@Before
@After
@AfterReturning
@AfterThrowing
@Around
IoC 컨테이너에서 Aspect
Annotation 기능을 활성화하려면 Configuration Class 중 하나에 @EnableAspectJAutoProxy
를 붙인다.