Spring/강의

[📙 숙련 Spring] 1-2. Spring Bean 등록

가지코딩 2025. 5. 15. 14:49

📙 목차

  1. Spring Bean 등록
  2. Spring Bean 등록 2
  3. 의존관계 주입

1. Spring Bean 등록

Spring에서 객체를 Bean으로 등록하는 방법은 크게 두 가지가 있다.

  • 자동 등록 - @Component 기반
  • 수동 등록 - @Configuration, @Bean 기반

* Spring에서는 기본적으로 자동 등록을 사용하며, 특정 상황에서만 수동 등록을 사용한다.

 

 

자동 등록 - @Component 기반

  • Spring은 @ComponentScan을 통해 특정 패키지를 스캔한다.
  • 해당 패키지 내의 클래스 중 @Component, @Service, @Repository, @Controller 등의 어노테이션이 붙은 클래스를 자동으로 Bean으로 등록한다.
  • 등록된 클래스는 클래스 이름의 앞글자를 소문자로 바꿔 Bean 이름이 된다.
  • @ComponentScan은 일반적으로 @SpringBootApplication에 포함되어 있어 별도로 명시하지 않아도 동작한다.
  • 자동 등록 사용 시점
    • 대부분의 일반적인 Bean 등록은 @Component, @Service, @Repository, @Controller 등으로 자동 등록
    • Spring Boot는 기본적으로 @ComponentScan을 통해 자동 등록을 수행
    • OCP, DIP를 만족하면서도 개발이 간편함
@Component  // → Bean 이름: myService
public class MyService {
    public void doSomething() {
        System.out.println("Spring Bean으로 동작");
    }
}

 

 

수동 등록 - @Configuration, @Bean 기반

  • 직접 Bean을 등록하고자 할 때 사용하는 방식이다.
  • 설정 클래스에 @Configuration을 붙이고, 해당 클래스 안에서 @Bean 메서드를 통해 원하는 객체를 반환하면 Bean으로 등록된다.
  • 주의할 점
    • @Configuration 없이 @Bean만 사용하면 싱글톤이 보장되지 않는다.
  • 수동 등록 사용 시점
    • 외부 라이브러리 객체를 Spring Bean으로 등록할 때
    • DB 연결 등 기술 인프라용 객체 구성 시
    • 같은 타입의 Bean 중 특정한 Bean을 명확하게 지정하고 싶을 때
@Configuration
public class AppConfig {

    @Bean  // → Bean 이름: testService
    public TestService testService() {
        return new TestServiceImpl();
    }
}
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
TestService service = context.getBean(TestService.class);
service.doSomething();  // "Test Service 메서드 호출"

 

 

Bean 충돌

  • Spring에서 Bean은 각각 고유한 이름을 기준으로 등록된다.
  • 같은 이름의 Bean이 중복 등록되면 충돌(ConflictingBeanDefinitionException) 이 발생하거나, 오버라이딩되어 예기치 않은 결과를 초래할 수 있다.
더보기
더보기

같은 이름의 Bean 등록: 자동 등록 vs 자동 등록

  • ConflictingBeanDefinitionException 발생
@Component("service")
public class ConflictServiceV1 implements ConflictService {
    public void test() { System.out.println("Conflict V1"); }
}

@Component("service")
public class ConflictServiceV2 implements ConflictService {
    public void test() { System.out.println("Conflict V2"); }
}

@ComponentScan(basePackages = "com.example.springconcept.conflict")
public class ConflictApp {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ConflictApp.class);
        ConflictService service = context.getBean(ConflictService.class);
        service.test();
    }
}

// 실행 시
org.springframework.beans.factory.support.BeanDefinitionOverrideException: 
Invalid bean definition with name 'service'

 

 

같은 이름의 Bean 등록: 수동 등록 vs 자동 등록

  • 수동 등록된 Bean이 우선권을 가지며, 자동 등록된 Bean은 덮어쓰기(overriding) 된다.
  • 수동 등록이 자동 등록을 조용히 덮어쓰기 때문에, 실수로 잘못된 객체가 등록되어도 알아차리기 어렵다.
// 자동 등록
@Component  // 이름: conflictService
public class ConflictService implements MyService {
    public void doSomething() { System.out.println("자동 등록된 ConflictService 호출"); }
}

// 수동 등록
@Configuration
public class ConflictAppConfig {
    @Bean(name = "conflictService")
    public MyService myService() {
        return new ConflictServiceV2(); // 수동 등록된 Bean
    }
}

public class ConflictServiceV2 implements MyService {
    public void doSomething() { System.out.println("수동 등록된 ConflictServiceV2 호출"); }
}

2. Spring Bean 등록 2

같은 타입의 Bean이 여러 개 존재할 때, 어떤 Bean을 주입할지 지정하는 방법

  • @Autowired + 필드명
  • @Qualifier
  • @Primary

 

@Autowired + 필드명

  • @Autowired는 타입으로 먼저 주입을 시도하고, 동일한 타입의 Bean이 여러 개 존재하면 필드명 또는 파라미터명으로 매칭하여 주입한다.
@Autowired
private MyService myServiceImplV2;  // 필드명이 Bean 이름과 일치하면 주입됨

 

 

@Qualifier

  • Bean 등록 시 @Qualifier("이름")을 지정하고, 주입 시 @Qualifier로 명시하여 사용한다.
  • 생성자 주입, Setter 주입 모두 사용 가능하다.
@Component
@Qualifier("firstService")
public class MyServiceImplV1 implements MyService { }

@Component
@Qualifier("secondService")
public class MyServiceImplV2 implements MyService { }

@Autowired
public ConflictApp(@Qualifier("firstService") MyService myService) {
    this.myService = myService;
}

 

 

@Primary

  • 같은 타입의 Bean이 여러 개일 때, 기본으로 주입할 Bean을 @Primary로 지정할 수 있다.
  • @Qualifier가 명시된 경우에는 @Primary보다 우선한다.
@Component
@Primary
public class MyServiceImplV2 implements MyService { }

@Autowired
public ConflictApp(MyService myService) {
    this.myService = myService;  // MyServiceImplV2가 주입됨
}

 

 

* 실전 예시

  • DB 다중 구성 (MySQL + Oracle)
    • 기본은 @Primary로 MySQL을 설정하고, 특정 상황에서는 @Qualifier("oracleService") 등으로 보조 DB 사용

3. 의존관계 주입

의존관계를 주입하는 방법 4가지

  • 생성자 주입 (가장 권장되는 방식)
  • Setter 주입
  • 필드 주입
  • 일반 메서드 주입

 

 

생성자 주입 (Constructor Injection)

  • 객체 생성 시 의존성을 주입한다.
  • 불변(immutable) 필드 주입이 가능하여 final 키워드를 사용할 수 있다.
  • 테스트 코드 작성이 용이하다.
  • 생성자가 1개일 경우 @Autowired를 생략할 수 있다.
    • @Autowired: Spring이 의존 객체를 자동으로 주입해주는 어노테이션.
  • 실무에서 가장 많이 사용하며, 스프링 외 환경에서도 적용이 가능하다.
@Component
public class OrderService {
    private final MemberRepository memberRepository;

    @Autowired
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
// @RequiredArgsConstructor (Lombok) 사용 시
// final 필드를 모아서 생성자를 자동으로 만들어 준다.
@Component
@RequiredArgsConstructor
public class OrderService {
    private final MemberRepository memberRepository;
}

 

 

 

Setter 주입

  • 객체 생성 이후 의존성을 주입한다.
  • 선택적 의존성에 적합하다.
  • 주입 시점이 불분명하고, 필드가 변경될 수 있어 안정성이 떨어진다.
  • 외부에서 setter를 호출하여 의존성을 바꿀 수 있기 때문에 주의가 필요하다.
@Component
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

 

 

필드 주입

  • 코드가 간단하고 짧다는 장점이 있다.
  • 테스트 작성이 어렵고, 스프링 컨테이너 없이 사용할 수 없다.
  • 주로 예제 코드나 설정 코드에서 사용한다.
  • 테스트와 유지보수가 어렵기 때문에 실무에서는 지양한다.
@Component
public class OrderService {
    @Autowired
    private MemberRepository memberRepository;
}

 

 

 

일반 메서드 주입

  • 일반 메서드를 통해 의존성을 주입한다.
  • 잘 사용되지는 않지만, 여러 의존성을 한꺼번에 주입할 때 유용할 수 있다.
@Component
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public void init(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}