Back-End (Web)/Spring

[Spring] Bean 등록

JABHACK 2024. 12. 25. 16:03

@ComponentScan

📌 Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller 같은 Annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능이다. 개발자가 Bean을 직접 등록하지 않고도 Spring이 자동으로 관리할 객체들을 찾는다.

 

ComponentScan의 역할

  • Chef가 요리할 재료를 자동으로 식료품 저장고에서 찾아오는 과정, Chef는 스스로 필요한 재료를 찾아 요리에 사용한다.

  • 요리사(개발자)가 직접 재료(Bean)를 찾아서 가져올 필요가 없다.

 

@ComponentScan

  1. 특정 패키지 내에 @Component Annotation이 붙은 클래스를 자동으로 찾아서 Spring Bean으로 등록한다.
    • Annotation을 이용해 Bean을 등록할 수 있어 코드가 간결해지고 유지보수가 쉬워진다.
  2. 스캐닝 범위는 주로 애플리케이션의 루트(최상위) 패키지에서 시작된다.
  3. @SpringBootApplication
    • 스프링 부트 애플리케이션의 시작점을 정의하기 위해 사용하는 애너테이션입니다. 이 애너테이션은 여러 기능을 결합한 복합 애너테이션으로, 스프링 부트 애플리케이션을 간단히 설정하고 실행할 수 있도록 돕습니다.
    • SpringBoot로 프로젝트를 생성하면 main() 메서드가 있는 클래스 상단에 @SpringBootApplication Annotation 이 존재한다.

 

 

  • @ComponentScan의 속성
    • basePackages: 특정 패키지를 스캔할 때 사용, 배열로 여러개를 선언할 수 있다.
      • 예시: @ComponentScan(basePackages = {"com.example", "com.another"})
    • basePackageClasses: 특정 클래스가 속한 패키지를 기준으로 스캔할 수 있다.
      • 예시: @ComponentScan(basePackageClasses = MyApp.class)
    • excludeFilters: 스캔에서 제외할 클래스를 필터링할 수 있다.
      • 예시: @ComponentScan(excludeFilters = @ComponentScan.Filter(SomeClass.class))
    • includeFilters: 특정 조건에 맞는 클래스만 스캔하여 포함할 수 있다.
      • 예시: @ComponentScan(includeFilters = @ComponentScan.Filter(Service.class))

 

@ComponentScan의 동작 순서

1. 스프링 컨테이너가 빈 등록

  • Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색한다.
  • @ComponentScan은 @Component, @Service, @Repository, @Controller 등의 애너테이션이 붙은 클래스를 탐색합니다.
  • 해당 클래스들을 스프링 빈으로 등록합니다.
  • 구현 클래스뿐만 아니라, 인터페이스와 그 구현체도 함께 빈으로 등록됩니다.

2. 의존성 정의 (인터페이스 기반 설계)

  • 일반적으로 **인터페이스(추상화)**를 의존성으로 정의합니다.
    구현체는 스프링이 자동으로 선택하고 주입합니다.

3. 구현체 자동 연결

  • 스프링은 등록된 빈 중에서 의존성으로 선언된 인터페이스에 맞는 구현체를 자동으로 찾아 주입합니다.
  • 이 과정은 타입 매칭을 통해 이루어집니다.

 

 

 

@Configuration, @Bean

📌 Spring Bean을 등록하는 방법에는 수동, 자동 두가지가 존재한다.

 

Spring Bean 등록 방법

  • Spring Bean은 Bean의 이름으로 등록된다.
    • 1. 자동 Bean 등록(@ComponentScan, @Component)
      • @Component 이 있는 클래스의 앞글자만 소문자로 변경하여 Bean 이름으로 등록한다.
// myService 라는 이름의 Spring Bean
@Component
public class MyService {

    public void doSomething() {
        System.out.println("Spring Bean 으로 동작");
    }
    
}
  • @ComponentScan 을 통해 @Component로 설정된 클래스를 찾는다.

 

  • 2. 수동 Bean 등록(@Configuration, @Bean)
    • @Configuration 이 있는 클래스를 Bean으로 등록하고 해당 클래스를 파싱해서 @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이 싱글톤으로 관리된다. CGLIB 라이브러리와 연관이 있다.
더보기

@Configuration과 싱글톤 관리

  • **@Configuration**은 스프링에서 Java Config 클래스를 정의할 때 사용하는 애너테이션입니다. 이 애너테이션을 붙인 클래스는 스프링 컨테이너가 관리하는 설정 클래스로 동작하며, 해당 클래스에 정의된 Bean은 싱글톤으로 관리됩니다.
  • 싱글톤 관리:
    • @Configuration이 붙은 클래스는 내부적으로 CGLIB 동적 프록시 객체로 변환됩니다.
    • 이 프록시 객체는 @Bean 메서드 호출 시, 이미 생성된 Bean이 있으면 이를 반환하고, 없으면 새로운 Bean을 생성합니다.
    • 이를 통해 동일한 Bean이 여러 번 생성되지 않도록 보장합니다.

 

 

Bean 충돌

📌 Bean 등록 방법에는 수동, 자동 두가지가 존재하고 Bean은 각각의 이름으로 생성된다. 이때 이름이 같은 Bean이 설정되고자 한다면 충돌이 발생한다.

 

같은 이름의 Bean 등록

  • 자동 Bean 등록 VS 자동 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 발생

 

수동 Bean 등록 VS 자동 Bean 등록

// 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등록의 충돌이 발생하면 오류가 발생한다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

 

설정 변경(application.properties)

// 수동, 자동 Bean을 동시에 등록할 때 이름이 같으면 수동 Bean이 오버라이딩
spring.main.allow-bean-definition-overriding=true 

// 기본값
spring.main.allow-bean-definition-overriding=false

 

 

@Qualifier, @Primary

📌 같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 Annotation

  • 같은 타입의 Bean 충돌 해결 방법
    1. @Autowired + 필드명 사용
      • @Autowired 는 타입으로 먼저 주입을 시도하고 같은 타입의 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;
	...
}
  1. @Qualifier 사용
    • 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;
		}
	...
}
  1. @Primary 사용
    • @Primary로 지정된 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가 (메인 MySQL, 보조 Oracle) 두개 존재하는 경우
      • 기본적으로 MySQL을 사용할 때 @Primary를 사용하면 된다.
      • 필요할 때 @Qualifier로 Oracle을 사용하도록 만들 수 있다.
      • 동시에 사용되는 경우 @Qualifier 의 우선순위가 높다.
같은 타입의 Bean이 여러개 조회되었지만 모든 Bean이 필요하다면, Java의 자료구조 Map, List를 사용하는 방법도 있다.

 

 

수동 VS 자동

📌 Annotation 기반의 Spring에서는 자동 Bean 등록과 의존관계 주입을 사용하는 경우를 주로 사용한다. @Component 뿐만 아니라 @Controller, @Service, @Repository 등 자동으로 쉽게 등록할 수 있는 Annotation들을 지원하고 Spring Boot는 ComponentScan 방식을 기본으로 사용한다.

 

  • 자동 Bean 등록을 사용하는 이유
    1. 다양한 Annotation으로 편리하게 등록할 수 있다.
    2. Spring Boot는 ComponentScan 방식을 기본으로 사용한다.
    3. 간단하지만 OCP, DIP를 준수하며 개발할 수 있다.
  • 수동 Bean 등록을 사용하는 경우
    1. 외부 라이브러리나 객체를 Spring Bean으로 등록할 때
      • 외부 라이브러리에서 제공하는 클래스는 자동 등록이 불가능하다.
    2. 데이터베이스 연결과 같이 비지니스 로직을 지원하는 기술들에 사용한다.
      • 비지니스 로직보다 그 수가 아주 적지만 Application에 광범위하게 적용된다.
      • 설정 정보에 명시되어 있어서 유지보수성이 증가한다.
    3. 같은 타입의 Bean 여러개 중 하나를 명시적으로 선택해야 할 때

꼭 필요한 경우가 아니라면 자동 Bean 등록을 사용하면 된다.