본문 바로가기

SpringBoot

ch3 05. Spring 애너테이션

4. 스프링 애너테이션 - @ComponentScan과 @Component

- @ComponentScan으로 @Component를 자동 검색해서 빈으로 등록

- @Controller, @Service, @Repository, @ControllerAdvice의 메타 애너테이션

@Configuration
// @ComponentScan("com.fastcampus.ch3") - basePackages 생략
// @ComponentScan(basePackages = {"com.fastcampus.ch3"}) - 패키지 지정
// @ComponentScan(basePackageClasses = AppConfig.class) - 클래스 지정
@ComponentScan // 이 애너테이션이 붙은 클래스의 패키지를 자동 스캔
public class AppConfig {
// Bean(name = "superEngine") Engine superEngine() {return new SuperEngine();}
// @Component("superEngine")
   @Component
   class SuperEngine extends Engine {}

프로젝트를 진행할 때 여러 사람이 하다보면, 하나의 파일을 같이 변경해야 하니 불편한 게 있을 수 있다. 

그럴 때 각자 개발한 클래스에 @Component만 붙여줘도 된다. 

이 때 @ComponentScan을 붙여서 빈으로 등록해놓아야 한다. 이렇게 써놓으면, AppConfig가 있는 클래스를 다 
찾는다. 
@Component("superEngine")의 생략 버전이 @Component이기 때문에 앞글자를 소문자로 한 "superEngine"객체가
저장소에 등록되게 된다. 

Key Value
"superEngine" 0x100
... ...

 @Controller, @Service, @Repository, @ControllerAdvice, @Configuration들도 @ComponentScan에 의해서 자동 검색이 된다.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
	@AliasFor(annotation = Component.class)
	String value() default "";
}

이런식으로 애너테이션을 정의할 때 @Component가 들어있는 애너테이션들이 존재한다. 

 

 

<실습>

AppConfig.java의 내부 코드와 Main의 실행문에서 아래 두 줄을 제외한 출력문들을 주석처리한 후 결과를 실행하면 

System.out.println("ac.getBeanDefinitionCount() = " + ac.getBeanDefinitionCount());
System.out.println("ac.getBeanDefinitionNames() = " + ac.getBeanDefinitionNames());

다음과 같이 appConfig만 빈으로 등록된 것을 확인할 수 있다. 

[Main.java]

@Component
class Car {
    Engine engine;
    Door door;

    @Override
    public String toString() {
        return "Car{" +
                "engine=" + engine +
                ", door=" + door +
                '}';
    }
}

@Component
class Engine {}
@Component
class Door {}

[AppConfig.java]

@Configuration
@ComponentScan
public class AppConfig {
//    @Bean
//    Car car() {return new Car();}
//
//    @Bean
//    @Scope("prototype")
//    Engine engine() {return new Engine();}
//
//    @Bean
//    Door door() {return new Door();}
}

AppConfig에 @ComponentScan을 붙여주고, 빈으로 등록할 클래스의 앞에 @Component를 붙여준다면

car, engine, door도 빈으로 등록되는 것을 볼 수 있다. 

여러 사람이 개발할 때는 자신이 개발한 클래스에 @Component를 붙여서 @ComponentScan이 자동적으로 수행되도록
하는 것도 좋은 방법이다. 

기본적인 것은 @Bean 애너테이션을 이용하고, 이후 추가적으로 개발되는 것들은 @Component, @ComponentScan을 
이용하면 좋다. 

 

 

 

5. 스프링 애너테이션 - @Value와 @PropertySource

@Component
@PropertySource("setting.properties")
class SysInfo {
	@Value("#{systemProperties['user.timezone']}")
	String timeZone;
    
	@Value("#{systemEnvironment['PWD']}")
	String currDir;    
    
	@Value("${autosaveDir}")
	String autosaveDir;
    
	@Value("${autosaveInterval}")
	int autosaveInterval;    
    
	@Value("${autosave}")
	boolean autosave;    
}

@Value애너테이션은 String, int와 같은 값을 하드코딩하지 않고, 동적으로 다른 저장된 값을 읽어와서 
timeZone과 같은 변수를 초기화해주는 역할을 한다. 

그걸 읽어올 파일 이름을 @PropertySource로 지정한다. 파일을 읽어오거나 sysProperties, systemEnvironment와 같은 
시스템 환경 변수들을 읽어서 사용할 수 있다. (하드코딩보다 변경에 유리!)

 

Ex) 

[src/main/resource/setting.properties]라는 파일을 만들었다고 가정한다. 

autosaveDir=/autosave
autosave=true
autosaveInterval=30

왼쪽이 key, 오른쪽이 value

파일을 만들어놓고 파일에 있는 내용을 읽으려면 위 예제 코드와 같이 파일을 지정해주고, @Value 애너테이션을 
이용해서 파일의 내용을 읽어와 그 값을 변수에 저장하도록 하면 된다. 

그러면 값을 직접 작성할 필요가 없고, 변경 사항이 발생했을 때 파일만 변경하면 되므로 변경에 유리한 코드가 된다. 

 

Map<String, String> env = System.getenv();
System.out.println("System.getenv() = " + env);

Properties prop = System.getProperties();
System.out.println("System.getProperties = " + prop);

System.out.println("ac.getBean(SysInfo.class) = " + ac.getBean(SysInfo.class));

Map<String, String> env = System.getenv();
은 맵형태로 데이터를 주는데, 여기 들어있는 값들 중 'PWD'라는 값을 읽어서 거기 저장되어 있는 값을 저장.

(cf. PWD는 현재 작업 폴더를 의미)

Properties prop = System.getProperties();

이 안에 어떤 key, value값이 있는지 확인할 수 있다.  

 

 

 

<실습>

[SysInfo.java]

package com.fastcampus.ch3.di3;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("setting.properties")
public class SysInfo {

    @Value("#{systemProperties['user.timezone']}")
    String timeZone;

    @Value("#{systemEnvironment['PWD']}")
    String currDir;

    @Value("${autosaveDir}")
    String autosaveDir;

    @Value("${autosaveInterval}")
    int autosaveInterval;

    @Value("${autosave}")
    boolean autosave;

    @Override
    public String toString() {
        return "SysInfo{" +
                "timeZone='" + timeZone + '\'' +
                ", currDir='" + currDir + '\'' +
                ", autosaveDir='" + autosaveDir + '\'' +
                ", autosaveInterval=" + autosaveInterval +
                ", autosave=" + autosave +
                '}';
    }
}

[setting.properties]

autosaveDir=/autosave
autosave=true
autosaveInterval=30

[Main.java]

       SysInfo info = ac.getBean(SysInfo.class);
        System.out.println("info = " + info);

스프링이 알아서 타입에 맞게 변환을 해준 후 값들을 출력해준다. 

 

 

6. 스프링 애너테이션 - @Autowired

- Spring container(AC)에서 타입으로 빈을 검색(by Type)해서 참조 변수에 자동 주입(DI)

- 검색된 빈이 n개이면, 그 중에 참조변수와 이름이 일치하는 것을 주입

- 주입 대상이 변수일 때, 검색된 빈이 1개 아니면 예외 발생

- 주입 대상이 배열일 때, 검색된 빈이 n개라도 예외 발생X

[참고] @Autowired(required=false)일 때, 주입할 빈을 못찾아도 예외 발생X.

 

@Autowired Engine engine;
@Autowired Engine[] engines;

@Autowired(required = fasle)
SuperEngine superEngine;
@Component
class TurboEngine extends Engine {}

@Component("engine")
class SuperEngine extends Engine {}

TurboEngine과 SuperEngine이 둘 다 engine의 자손이니 2개가 등록이 되어있다. 

그럴 때는 참조 변수와 이름이 일치하는 @Component("engine")을 찾아 주입하게 된다. 

하지만 여기서 SuperEngine은 superEngine으로 이름이 일치하지 않는다. 

이렇게 불일치 하는 경우 TurboEngine, SuperEngine 둘 중 무엇을 넣어야 할 지 판단할 수 없어 에러가 난다. 

이 때 required = false로 두면 에러가 발생하지 않는다.(true가 디폴트)
주입할 빈이 없으면 에러가 나지않고 주입을 하지 않는다. 꼭 필수로 주입할 빈이 아닌 경우 required = false로 두면 된다. 

 

But) 보통은 타입이 1개니 이런 경우는 많이 없다!

 

아니면, 배열로 두 개의 빈을 다 넣을 수도 있다. 

 

 

 

<실습>

[Main.java]

 

@Autowired를 넣기 전 null 값이 나온다. 

@Component
class Car {
    @Autowired
    Engine engine;
    @Autowired
    Door door;

@Autowired를 넣고 실행하면 engine과 door가 잘 들어간 것을 볼 수 있다. 

 

 

- Bean을 더 추가해보자

Engine이 들어간 것을 볼 수 있는데, 보면 같은 타입의 빈이 3개이다.(정확히는 다름)

engine타입으로 instanceof를 검사하니 3개가 다 주입 대상이 되는 것이다.

이렇게 여러 개일 땐 3개의 후보 중에서 engine 이름과 일치하는 것을 찾아서 넣게 되니 Engine이 들어가게 되는 것이다.

Engine에 있는 @Component를 주석처리 하니 에러가 발생한다. 

하나를 넣어야하는데 superEngine과 turboEngine 두 개가 발견된 것이다. 

 

이럴 땐 배열로 처리해주면 빈이 여러개가 검색되도 가능해진다.

engine이 배열로 들어간 것을 볼 수 있다. 

 

 

- @Autowired를 생성자에 붙일 수도 있다.

그러면 빈이 만들어질 때 생성자를 이용해서 빈을 주입할 수 있다.(객체 주소 대입)

참조 변수에 붙어있는 @Autowired를 주석처리 해준 후 코드를 실행해도 빈이 주입되는 것을 볼 수 있다.

생성자에 붙은 @Autowired를 생략해도 생성자가 있는 경우에 자동으로 빈을 주입해준다. 

 

 

- Car라는 기본 생성자가 존재할경우

null이 나오는 것을 볼 수 있는데, 기본 생성자를 이용해서 Car를 생성한 것이다. 

 

=> 생성자가 여러 개라면 @Autowired를 생략할 수 없다!! (기본 생성자를 이용)

 

 

 

 

7. 스프링 애너테이션 - @Resource

- Spring container(AC)에서 이름으로 빈을 검색(by Name)해서 참조 변수에 자동 주입(DI)

- 일치하는 이름의 빈이 없으면, 예외 발생

class Car {
	@Resource(name="superEngine")
    Engine engine;
}

 

리소스를 생략하면 name이 이름으로 지정한 것과 같다. 같은 경우 생략 가능

class Car {
// @Resource(name="engine")
   @Resource
   Engine engine;
}

 

이름으로 찾는 경우 @Qulifier를 사용할수도 있다. 
@Autowired로 검색하는 경우는 by Type으로 검색하는데, 2개 이상이라면 이름으로 찾는다. 

그 이름을 지정해줘서 이름과 일치하는걸 찾아서 참조변수에 주입해준다. 

class Car {
	@Autowired
    @Qualifier("superEngine")
    Engine engine;
}

 

@Resource를 사용해도 되고, @Autowired, @Qualifier를 사용해도 된다. 

 

 

<실습>

@Resource(name="superEngine")을 실행한 결과 SuperEngine이 들어간 것을 볼 수 있다. 

@Autowired와 @Qulifier를 이용한 경우에도 superEngine을 잘 주입한 것을 볼 수 있다. 

 

 

 

8. 스프링 애너테이션 vs 표준 애너테이션(JSR-330)

- javax.inject-1.jar - @Inject, @Named, @Qualifier, @Scoper, @Singleton

- annotations-api.jar - @Resource, @ManagedBean, @PreDestory, @PostContruct

스프링 애너테이션 표준 애너테이션 비고
@Autowired @Inject @Inject에는 required속성이 없음
@Qualifier @Qualifier, @Named 스프링의 @Qualifier는 @Named와 유사
- @Resource 스프링에는 이름 검색이 없음
@Scope("singleton") @Singleton 표준에서는 prototype이 디폴트
@Component @Named, @ManagedBean 표준에서는 반드시 이름이 있어야 함