📌 고정 게시글

📢 공지합니다

이 게시글은 메인 페이지에 항상 고정되어 표시됩니다.

최코딩의 개발

[리메이크] 스프링 컨테이너와 빈에 관해서 본문

스프링/스프링 기본

[리메이크] 스프링 컨테이너와 빈에 관해서

seung_ho_choi.s 2025. 6. 22. 18:16
728x90

📦 스프링 컨테이너란?

스프링 컨테이너는 자바 객체(빈, Bean)를 생성하고 관리하며, 의존관계를 자동으로 연결해주는 역할을 한다. 단순한 객체 생성만 담당하는 것이 아니라, 생명주기 관리, 의존성 주입(DI), AOP 지원 등 다양한 기능을 제공한다.

스프링에서 이 객체들을 "빈(Bean)"이라고 부른다.

☑️스프링 컨테이너의 종류

  • BeanFactory : 가장 기본적인 컨테이너 (거의 사용되지 않음)
  • ApplicationContext : BeanFactory 포함 + 메시지소스, 이벤트, 국제화, AOP 등 다양한 기능 추가 → 실무에서 주로 사용됨
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = context.getBean(MyService.class);

즉, 우리가 평소에 사용하는 스프링 컨테이너는 ApplicationContext다.


🫘 스프링 빈(Bean)이란?

스프링 컨테이너가 생성하고 관리하는 객체를 빈이라고 부른다. 일반적으로 개발자가 직접 new 하지 않고, 스프링이 대신 생성해주며, 필요시 생성자나 필드 등에 주입해준다.

@Component
public class Board {}

@Service
@RequiredArgsConstructor
public class MemberService {
    private final Board board;
}

위 예제에서 Board는 스프링이 먼저 생성하고, MemberService의 생성자에 주입된다. 개발자는 new 하지 않아도 된다.


🧠 Board 객체는 누가 생성하나? 생성 순서는?

다음 구조처럼 DI가 구성되어 있다고 해보자:

Controller → Service → Repository

즉, 컨트롤러가 서비스를 가지고, 서비스는 리포지토리를 가진다. 그럼 생성 순서는?

1. Repository 생성 (맨 아래부터)
2. Service 생성 (Repository 주입)
3. Controller 생성 (Service 주입)

이 순서를 따라 스프링이 내부적으로 의존성을 파악하고, 가장 아래쪽(leaf)에 있는 빈부터 순차적으로 생성 후, 상위 빈의 생성자에 주입해 나간다.

즉, 생성자 주입 방식에서는 반드시 "안쪽 객체부터 먼저 생성되어야" 위쪽에서 주입이 가능하다.


🔬 리플렉션(Reflection)이란?

스프링이 자동 주입을 위해 사용하는 기술 중 하나가 바로 리플렉션이다.

 

리플렉션이란? → 자바의 클래스, 메서드, 필드 정보를 런타임에 동적으로 조회하거나 조작할 수 있는 기능

 

 

예를 들어, 스프링은 아래처럼 동작한다:

Class<?> clazz = MemberService.class;
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
Parameter[] params = constructor.getParameters();

for (Parameter param : params) {
    Object bean = applicationContext.getBean(param.getType());
    // 생성자 호출 시 필요한 파라미터를 준비함
}

MemberService service = (MemberService) constructor.newInstance(...);

 

즉, 생성자 파라미터의 타입을 런타임에 조사해서, 그에 맞는 빈을 스프링 컨테이너에서 꺼내서 자동으로 newInstance로 주입해주는 방식이다.

리플렉션은 강력하지만:

  • 일반적인 코드보다 느림
  • 컴파일 타임 오류 발견 불가 → 런타임에 문제 발생할 수 있음

그래서 우리가 직접 쓸 일은 거의 없지만, 스프링 내부에선 리플렉션 없이는 DI 자체가 불가능하다.


🧩 @Configuration과 @Bean (싱글톤 보장)

@Configuration
public class AppConfig {
    @Bean
    public Board board() {
        return new Board();
    }
}
  • @Configuration은 내부적으로 CGLIB을 사용해 프록시 클래스를 생성함으로써, 해당 클래스 안의 모든 @Bean 메서드를 싱글톤으로 보장한다.
  • 즉, @Bean 메서드가 여러 번 호출되더라도 실제로는 단 하나의 객체만 생성된다.
Board b1 = appConfig.board();
Board b2 = appConfig.board();
System.out.println(b1 == b2); // true

프록시 방식은 바이트코드를 조작하는 방식으로 복잡하게 구현되어 있다. 따라서 임의의 다른 클래스가 싱글톤을 보장할 수 있는 것은 아니며, Configuration 클래스만 해당된다.


🎯 @Component와 @ComponentScan

  • @Component는 클래스 자체를 빈으로 등록하겠다는 선언이다.
  • @ComponentScan이 설정되면, basePackages 경로 하위에 있는 모든 @Component, @Service, @Repository, @Controller 클래스들이 자동으로 스프링 빈으로 등록된다.
@Component
public class OrderService {}

@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {}

🔁 @Component vs @Configuration 차이점

항목 @Configuration (with @Bean)  @Component
빈 등록 방식 메서드 단위로 등록 클래스 자체 등록 
내부 제어력 높음 낮음
대표 예시 설정 전용 클래스, 빈 팩토리 역할 서비스 클래스, DAO 클래스 등
언제 쓰나? - 여러 @Bean 메서드를 가지는 설정 클래스
- 외부 라이브러리 Bean 구성 시
- 커스터마이징한 설정을 명시적으로 분리할 때
- @Service, @Repository, @Controller로 분류되지 않는 일반적인 컴포넌트
- 비즈니스 로직을 처리하거나, 단일 기능 클래스를 빈으로 등록하고 싶을 때

생성자 주입에 기반하여 명확하고 불변한 객체 설계를 하고 싶다면, Configuration + @Bean 구조가 좀 더 유연하다.


💉 의존관계 주입 방법 (DI)

1. 생성자 주입 (권장)

@RequiredArgsConstructor
@Component
public class MyService {
    private final MyRepository repository;
}
  • 한 번만 호출되며 불변성 유지
  • null 방지 및 순환참조 오류도 빨리 발견 가능
  • 테스트, 리팩터링, 유지보수에 유리
  • 생성자 주입은 객체가 완전히 유효한 상태로 만들어진다.

2. 수정자(setter) 주입

@Component
public class MyService {
    private MyRepository repository;

    @Autowired(required = false)
    public void setRepository(MyRepository repository) {
        this.repository = repository;
    }
}
  • 선택적 의존성 또는 순환참조 해결용
  • 외부에서 바꿀 수 있어 불변성 보장 불가 → 비권장
  • A라는 빈이 초기엔 정상 작동하다가, B 클래스가 실수로 setter 호출해서 주입값 바꿔버림 → 사이드 이펙트 발생

3. 필드 주입

@Component
public class MyService {
    @Autowired
    private MyRepository repository;
}
  • 코드 짧지만 강하게 결합되어있어 DI 프레임워크 없으면 아무것도 못함 
  • 외부에서 주입하거나 mock을 넣으려면 리플렉션 써야 함 → 테스트 지옥
  • 객체 생성 단계
    • new MyService() → 이때 repository는 null 상태
  • 스프링이 리플렉션으로 주입
    • 객체를 만든 다음에, 스프링이 @Autowired 달린 필드에 접근해서 값을 강제로 넣음
  • 필드 주입은 객체 생성 시점에 의존성이 빠져 있어서 "불완전한 객체"가 생길 수 있기 때문에 위험하다.

🧪 선택적 의존성이란?

의존성이 "있으면 좋지만, 없어도 돌아갈 수 있는" 경우를 의미한다.

예를 들어 로컬 환경에서는 Logger가 없어도 되지만, 운영 환경에서는 반드시 Logger가 필요한 경우:

@Component
public class NotificationService {
    private Logger logger;

    @Autowired(required = false)
    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    public void send(String msg) {
        if (logger != null) logger.log(msg);
    }
}

이처럼 "선택적으로 있어도 되고 없어도 되는" 기능에 대해서만 setter 주입을 쓴다. 무조건 필요한 의존성은 생성자 주입을 써야 한다. 그래서 setter는 굳이?


☑️빈이 2개 이상일 때 주입 우선순위 설정

@Autowired
@Qualifier("mainService")
private MyService service;

@Primary
@Component
public class DefaultService implements MyService {}
  • @Qualifier: 수동 지정 → 이름 기준으로 특정 빈 주입
  • @Primary: 자동 주입 시 우선권 부여

둘 다 존재할 경우: @Qualifier가 우선된다 (명시적 지정 > 암시적 우선순위)


📌 @Component, @Service, @Repository, @Controller 차이

어노테이션 설명 및 역할
@Component 가장 기본적인 컴포넌트. 스프링 빈으로 등록됨
@Service 비즈니스 로직 담당. 의미 전달용 (기능 차이 없음)
@Repository DB 접근 계층. 예외 변환(AOP 기반) 기능 추가됨
@Controller 웹 계층 (요청/응답). 내부에 @ResponseBody 없이도 View 처리 가능

☑️마무리 정리

  • 스프링 컨테이너는 객체를 대신 생성하고 의존성을 자동으로 주입해준다 (new 대신 생성자 주입)
  • 생성자 주입을 기본으로 사용하고, 진짜 필요할 때만 setter 주입을 고려하자
  • 빈이 여러 개일 때는 @Qualifier, @Primary로 우선순위를 명확히 하자
  • @Configuration은 CGLIB을 통해 싱글톤을 강력히 보장하고, @Component는 단순 등록용이므로 목적에 맞게 쓰자
  • 객체 생성 순서는 항상 "하위 → 상위", 즉 Repository → Service → Controller 순으로 주입된다.
  • 이 모든 의존성 주입 과정은 내부적으로 리플렉션(Reflection)을 통해 생성자 타입을 조사하고 호출하며 이루어진다.

결국 "객체를 직접 만들지 말고 맡겨라" — 이것이 스프링 DI의 핵심 철학이다.

 

 

 

728x90

'스프링 > 스프링 기본' 카테고리의 다른 글

[12편] 스프링 AOP 이란?  (0) 2025.06.06
[11편] 빈 스코프  (0) 2023.04.10
[10편] 조회 대상 빈이 2개 이상일때  (0) 2023.04.04
[9편] 롬복과 최신트랜드  (0) 2023.04.04
[8편] 의존관계 주입  (0) 2023.04.03