Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller와 같은 annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능입니다.
개발자가 직접 Bean을 등록하지 않고도 Spring이 자동으로 관리해야할 객체들을 찾습니다.
@Component annotation이 붙은 클래스를 자동으로 찾아 Spring Bean으로 등록합니다.@SpringBootApplication@ComponentScan(basePackages = {"com.exmaple", "com.another"})@ComponentScan(basePackageClasses = MyApp.class)@ComponentScan(excludeFilters = @ComponentScan.Filter(SomeClass.class))@ComponentScan(includeFilters = @ComponentScan.Filter(Service.class))여기서 다른 것들은 몰라도 basePackages는 알고 갑니다!
@ComponentScan이 지정된 패키지를 탐색합니다.@Component 또는 annotation이 붙은 클래스를 찾습니다.@Configuration이 붙은 클래스를 Bean으로 등록한 다음, @Bean이 붙은 메서드를 찾아 Bean을 생성합니다. 여기서 Bean의 이름은 해당 메서드의 이름이 됩니다.
Spring Bean을 등록하는 방식에는 자동, 수동 두 가지가 존재합니다.
@ComponentScan, @Component annotation을 사용해서 Bean을 등록하는 것은 자동으로 Bean을 등록하는 방식입니다.
@Component이 붙은 클래스의 앞글자만 소문자로 변경하여 Bean의 이름으로 등록합니다.
// myService 라는 이름의 Spring Bean
@Component
public class MyService {
public void doSomething() {
System.out.println("Spring Bean 으로 동작");
}
}
위에서 언급했던 것처럼, @ComponentScan이 @Component가 붙은 클래스를 찾아 Bean으로 등록합니다.
@Configuration과 @Bean을 사용하여 Bean을 등록하는 것은 수동으로 Bean을 등록하는 방식입니다.
// 인터페이스
public interface TestService {
void doSomething();
}
// 인터페이스 구현체
public class TestServiceImpl implements TestService {
@Override
public void doSomething() {
System.out.println("Test Service 메서드 호출");
}
}
// 수동으로 빈 등록
@Configuration
public class AppConfig {
// TestService 타입의 Spring Bean 등록
@Bean
public TestService testService() {
// TestServiceImpl을 Bean으로 등록
return new TestServiceImpl();
}
}
// Spring Bean으로 등록이 되었는지 확인
public class MainApp {
public static void main(String[] args) {
// Spring ApplicationContext 생성 및 설정 클래스(AppConfig) 등록
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
// 등록된 TestService 빈 가져오기
TestService service = context.getBean(TestService.class);
// 빈 메서드 호출
service.doSomething();
}
}
주의해야 할 점은, 수동으로 Bean으로 등록할 때 항상 @Configuration과 함께 사용해야 Bean이 Singleton으로 관리된다는 것입니다.
Bean의 등록 방식에는 자동과 수동 두 가지가 존재한다고 하였습니다. 여기서 Bean은 각각의 이름으로 설정이 되는데, 이름이 같은 Bean이 설정되고자 하면 충돌이 발생하게 됩니다.
public interface ConflictService {
void test();
}
// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV1 implements ConflictService {
@Override
public void test() {
System.out.println("Conflict V1");
}
}
// Bean의 이름을 service로 설정
@Component("service")
public class ConflictServiceV2 implements ConflictService {
@Override
public void test() {
System.out.println("Conflict V2");
}
}
// componentScan의 범위를 conflict 패키지 하위로 설정
@ComponentScan(basePackages = "com.example.springconcept.conflict")
public class ConflictApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp.class);
// Service 빈을 가져와서 실행
ConflictService service = context.getBean(ConflictService.class);
service.test();
}
}
위 코드를 실행시키면, ConflictingBeanDefinitionException이 발생합니다.
// conflictService 이름으로 Bean 생성
@Component
public class ConflictService implements MyService {
@Override
public void doSomething() {
System.out.println("ConflictService 메서드 호출");
}
}
public class ConflictServiceV2 implements MyService {
@Override
public void doSomething() {
System.out.println("ConflictServiceV2 메서드 호출");
}
}
// 수동으로 Bean 등록
@Configuration
public class ConflictAppConfig {
// conflictService 이름으로 Bean 생성
@Bean(name = "conflictService")
MyService myService() {
return new ConflictServiceV2();
}
}
@ComponentScan(basePackages = "com.example.springconcept.conflict2")
public class ConflictApp2 {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp2.class);
// Service 빈을 가져와서 실행
MyService service = context.getBean(MyService.class);
service.doSomething();
}
}
수동 Bean 등록이 자동 Bean 등록을 오버라이딩해서 우선권을 가지게 됩니다.
Spring Boot에서는 수동 Bean 등록과 자동 Bean 등록의 충돌이 발생하면 오류가 발생하게 됩니다.
Bean 테스트
테스트 할 때 자동 Bean 등록 충돌 패키지의 Bean Name을 지워야 하고, 반드시
@SpringBootApplication으로 실행시켜주어야 합니다.
수동, 자동 Bean을 동시에 등록할 때 이름이 같은 경우, 수동 Bean이 오버라이딩 해 우선권을 가지게 하려면 application.properties나 application.yml과 같은 설정 파일에서
spring.main.allow-bean-definition-overriding의 값을 true로 변경해주어야 합니다.
의존 관계 주입을 하는 방법으로는 생성자 주입, setter 주입, 필드 주입, 메서드 주입으로 총 4가지 방법이 존재합니다.
@Autowired는 의존성으로 자동으로 주입할 때 사용하는 annotation입니다. 여기서, 기본적으로 주입할 대상이 없다면 오류가 발생하게 됩니다.
생성자를 통해서 의존성을 주입하는 방법입니다. 가장 중요하기 때문에, 다른 방법은 몰라도 잘 알아두어야 합니다.
최초에 한 번 생성된 후에는 값을 수정할 수 없습니다.
public interface MyService {
void doSomething();
}
// Spring Bean으로 등록
@Service
public class MyServiceImpl implements MyService {
@Override
public void doSomething() {
System.out.println("MyServiceImpl 메서드 호출");
}
}
// 생성자 주입 방식
@Component
public class MyApp {
// 필드에 final 키워드 필수!
private final MyService myService;
// 생성자를 통해 의존성 주입, 생성자가 유일하면 생략이 가능
@Autowired
public MyApp(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
@ComponentScan(basePackages = "com.example.springdependency.test")
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
// 등록된 MyApp Bean 가져오기
MyApp myApp = context.getBean(MyApp.class);
// Bean 메서드 호출
myApp.run();
}
}
setter를 사용하여 의존성을 주입하는 방법입니다.
@Component
public class MyApp {
private MyService myService;
// Setter 주입
@Autowired
public void setMyService(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
생성자 주입 방식과 같은 경우는 필수 값을 사용하여 final 키워드를 붙여주었는데, setter 주입 방식은 선택하거나, 변경이 가능한 의존관계에 사용합니다.
// MyService가 Spring Bean으로 등록되지 않은 경우에도 주입이 가능
@Autowired(required = false)
public void setMyService(MyService myService) {
this.myService = myService;
}
// 실행 도중 인스턴스를 바꾸고자 하는 경우
// setMyService(); 메서드를 외부에서 호출하면 됨(이런 경우는 거의 없음)
필드에 직접 주입하는 방법입니다.
@Component
public class MyApp {
@Autowired
private MyService myService; // 필드에 직접 주입
public void run() {
myService.doSomething();
}
}
매우 간결하지만, Spring이 없다면 사용할 수 없다는 점 때문에 가장 추천되지 않습니다.
// Spring을 사용하지 않는 경우 실행이 불가능하다.
public class MainV2 {
public static void main(String[] args) {
MyApp myApp = new MyApp();
myApp.run();
}
}
외부에서 myService 값을 설정하거나 변경할 방법이 없기 때문에 결국 setter를 만들어야 합니다. 위에서 언급했다시피 Spring이 없으면 사용할 수 없어 순수 Java 코드로 사용할 수 없습니다. 이렇게 되면 테스트 코드 작성이 힘들어집니다.
Application 실행과 관련이 없는 @SpringBootTest 테스트 코드나 Spring에서만 사용하는 @Configuration 같은 곳에서 주입할 때 주로 사용합니다.
생성자 주입, setter 주입으로 대체가 가능하기 때문에 사용하지 않습니다.
@Component
public class MyApp {
private MyService myService;
// 일반 메서드 주입
@Autowired
public void init(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
의존관계를 자동으로 주입할 객체가 Spring Bean으로 등록되어 있어야 @Autowired로 주입이 가능합니다.
DI를 가지고 있는 대부분의 Framework가 생성자 주입 방식을 권장합니다.
실제 Web Application을 개발하면 대부분이 불변 객체이며 생성자 주입 방식을 선택하게 됩니다. 여기서 코드가 반복되는데 이를 편안하게 작성하기 위해 Lombok에서 @RequiredArgsConstructor를 지원합니다.
final 필드들을 모아서 생성자를 자동으로 만들어주는 역할을 하고, annotation processor가 동작하여 컴파일 시점에 자동으로 생성자 코드를 만들어줍니다.
@Component
@RequiredArgsConstructor
public class MyApp {
// 필드에 final 키워드 필수! 무조건 값이 있도록 만들어줌(필수)
private final MyService myService;
// Annotation Processor가 만들어 주는 코드
// public MyApp(MyService myService) {
// this.myService = myService;
// }
public void run() {
myService.doSomething();
}
}
이런 식으로 생성자를 하나 만들고 @Autowired를 사용한 코드와 똑같이 동작하게 됩니다.
만약, 생성자가 필요한 경우가 생긴다면, 생성자 주입 방식을 직접 선언하여 사용하면 됩니다.
같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 annotation입니다.
@Autowired annotation은 타입으로 먼저 주입을 시도합니다. 그리고 같은 타입의 Bean이 여러 개라면 필드 이름 또는 파라미터 이름으로 매칭합니다.
public interface MyService { ... }
@Component
public class MyServiceImplV1 implements MyService { ... }
@Component
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
// 필드명을 Bean 이름으로 설정
@Autowired
private MyService myServiceImplV2;
...
}
Bean을 등록할 때 추가 구분자를 붙여줍니다. 생성자 주입과 setter 주입을 사용할 수 있습니다.
@Component
@Qualifier("firstService")
public class MyServiceImplV1 implements MyService { ... }
@Component
@Qualifier("secondService")
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
private MyService myService;
// 생성자 주입에 구분자 추가
@Autowired
public ConflictApp(@Qualifier("firstService") MyService myService) {
this.myService = myService;
}
// setter 주입에 구분자 추가
@Autowired
public void setMyService(@Qualifier("firstService") MyService myService) {
this.myService = myService;
}
...
}
@Primary annotation은 Bean에 우선 순위를 부여합니다. 이 annotation이 붙은 Bean은 우선 순위를 가지게 됩니다.
@Component
public class MyServiceImplV1 implements MyService { ... }
@Component
@Primary
public class MyServiceImplV2 implements MyService { ... }
@Component
public class ConflictApp {
private MyService myService;
@Autowired
public ConflictApp(MyService myService) {
this.myService = myService;
}
...
}
Database를 두 개 사용하는 경우를 예시로 들 수 있습니다. 만약 메인 DB를 MySQL, 보조 DB를 Oracle로 사용한다고 하면, MySQL에 @Primary를, Oracle이 필요할 때 @Qualifier를 사용하면 됩니다.
MySQL에 @Primary를 붙이고 Orcale에 Qualifier?
Oracle을 보조로 사용하므로 기본적으로 MySQL을 사용하기 위해 @Primary를 붙여주는데, 만약 @Primary와 @Qualifier 두 개가 모두 사용이 된다면, @Qualifier의 우선 순위가 더 높아지게 되어서 Oracle을 사용할 수 있게 됩니다.
Bean을 여러 개 사용하고 싶다면?
같은 타입의 Bean이 여러 개 조회되었는데, 모든 Bean을 사용하고 싶다면, Map, List와 같은 자료구조를 활용할 수 있습니다.
Annotation 기반 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용합니다. 자동으로 쉽게 등록할 수 있는 @Component, @Controller, @Service, @Repository 등의 annotation들을 지원하며 기본적으로 ComponentScan방식을 사용합니다.
위와 같이 수동 Bean 등록을 사용하는 경우가 아니라면, 자동 Bean 등록을 사용하면 됩니다.
자료 및 코드 출처: 스파르타 코딩클럽