WEEK 6-4: Spring Bean

ensalada.de.pollo·2025년 5월 16일

be

목록 보기
26/44

Spring Bean 등록(1)

@ComponentScan

Spring이 특정 패키지 내에서 @Component, @Service, @Repository, @Controller와 같은 annotation이 붙은 클래스를 자동으로 검색하고, 이를 Bean으로 등록하는 기능입니다.

개발자가 직접 Bean을 등록하지 않고도 Spring이 자동으로 관리해야할 객체들을 찾습니다.

  • 특정 패키지 내에 @Component annotation이 붙은 클래스를 자동으로 찾아 Spring Bean으로 등록합니다.
    • annotation을 이용하여 Bean을 등록할 수 있기 때문에 코드가 간결해지고 유지보수가 쉬워집니다.
  • 스캐닝 범위는 주로 application의 root package에서 시작이 됩니다.
  • @SpringBootApplication
    • SpringBoot로 프로젝트를 생성하면 main()메서드가 있는 클래스 상단에 해당 annotation이 붙습니다.

속성

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

여기서 다른 것들은 몰라도 basePackages는 알고 갑니다!

동작 순서

  1. Spring Application이 실행되면 @ComponentScan이 지정된 패키지를 탐색합니다.
  2. 해당 패키지에서 @Component 또는 annotation이 붙은 클래스를 찾습니다.
  3. 찾은 클래스를 Spring 컨테이너에 Bean으로 등록합니다.
  4. 등록된 Bean은 DI와 같은 방식으로 다른 Bean과 연결이 됩니다.

@Configuration, @Bean

@Configuration이 붙은 클래스를 Bean으로 등록한 다음, @Bean이 붙은 메서드를 찾아 Bean을 생성합니다. 여기서 Bean의 이름은 해당 메서드의 이름이 됩니다.

Spring Bean을 등록하는 방식

Spring Bean을 등록하는 방식에는 자동, 수동 두 가지가 존재합니다.

자동 Bean 등록(@ComponentScan, @Component)

@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으로 등록합니다.

수동 Bean 등록(@Configuration, @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은 각각의 이름으로 설정이 되는데, 이름이 같은 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이 발생합니다.

수동 Bean 등록과 자동 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 등록과 자동 Bean 등록의 충돌이 발생하면 오류가 발생하게 됩니다.

Bean 테스트

테스트 할 때 자동 Bean 등록 충돌 패키지의 Bean Name을 지워야 하고, 반드시 @SpringBootApplication으로 실행시켜주어야 합니다.
수동, 자동 Bean을 동시에 등록할 때 이름이 같은 경우, 수동 Bean이 오버라이딩 해 우선권을 가지게 하려면 application.properties나 application.yml과 같은 설정 파일에서

spring.main.allow-bean-definition-overriding의 값을 true로 변경해주어야 합니다.

의존관계 주입

의존 관계 주입을 하는 방법으로는 생성자 주입, setter 주입, 필드 주입, 메서드 주입으로 총 4가지 방법이 존재합니다.

의존관계 주입 방법

@Autowired는 의존성으로 자동으로 주입할 때 사용하는 annotation입니다. 여기서, 기본적으로 주입할 대상이 없다면 오류가 발생하게 됩니다.

1. 생성자 주입

생성자를 통해서 의존성을 주입하는 방법입니다. 가장 중요하기 때문에, 다른 방법은 몰라도 잘 알아두어야 합니다.

최초에 한 번 생성된 후에는 값을 수정할 수 없습니다.

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();
    }
}

2. Setter 주입

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(); 메서드를 외부에서 호출하면 됨(이런 경우는 거의 없음)

3. 필드 주입

필드에 직접 주입하는 방법입니다.

@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 같은 곳에서 주입할 때 주로 사용합니다.

4. 일반 메서드 주입

생성자 주입, 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가 생성자 주입 방식을 권장합니다.

왜?

  • 불변(immutable)
    : 어떤 application을 만들지 정했다면 이미 Bean과 의존 관계가 결정이 되어있습니다. 객체를 생성할 때 최초 한 번만 호출되어 불변의 성격을 가집니다. 만약, setter 주입을 사용하게 된다면 접근 제어자가 public으로 설정이 되어 누구나 수정할 수 있게 됩니다.
  • 실수 방지
    : 순수 Java 코드로 사용할 때, 생성자의 필드를 필수로 입력하도록 만들어 줍니다. 컴파일 시점에 오류를 발생시키기 때문에 실행 전 오류를 알 수 있게 해주기도 합니다.

@RequiredArgsConstructor

실제 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를 사용한 코드와 똑같이 동작하게 됩니다.

만약, 생성자가 필요한 경우가 생긴다면, 생성자 주입 방식을 직접 선언하여 사용하면 됩니다.

Spring Bean 등록(2)

@Qualifier, @Primary

같은 타입의 Bean이 중복된 경우 해결하기 위해 사용하는 annotation입니다.

@Autowired + 필드명 사용

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

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

@Primary 사용

@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와 같은 자료구조를 활용할 수 있습니다.

수동 vs 자동

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

자동 Bean 등록을 사용하는 이유?

  • 다양한 annotation으로 편리하게 등록할 수가 있습니다.
  • Spring Boot는 ComponentScan 방식을 기본적으로 사용합니다.
  • 사용이 간단한데 OCP, DIP를 준수할 수 있게 도와줍니다.

수동 Bean 등록을 사용하는 경우?

  • 외부 라이브러리나 객체를 Spring Bean으로 등록하는 경우가 있습니다. 외부 라이브러리에서 제공하는 것들은 자동 등록이 불가능하기 때문에 수동으로 등록해주어야 합니다.
  • 데이터베이스 연결과 같이 비즈니스 로직을 지원하는 기술들에 사용합니다.
  • 같은 타입의 Bean 여러 개 중 하나를 명시적으로 선택해야 하는 경우에 수동 Bean 등록을 사용합니다.

위와 같이 수동 Bean 등록을 사용하는 경우가 아니라면, 자동 Bean 등록을 사용하면 됩니다.

자료 및 코드 출처: 스파르타 코딩클럽

0개의 댓글