스프링부트

[SpringBoot] Spring Container

민석삼 2025. 3. 9. 12:54

스프링 컨테이너란

스프링 애플리케이션의 핵심 구성 요소로, 애플리케이션에 사용되는 빈 객체를 관리하는 역할을 한다. 

애플리케이션의 시작과 종료, 객체의 생성과 소멸, 의존성 주입 등을 처리

스프링 컨테이너의 주요 역할

1. 빈 관리

스프링 애플리케이션의 객체(빈)을 생성하고, 그 생명 주기를 관리

빈을 스프링 컨테이너에 등록하고, 필요한 곳에 주입하여 객체 간의 의존성 문제를 해결

2. 의존성 주입 (Dependency Injection)

빈 객체 간의 의존성을 자동으로 주입

@Autowired, 생성자 주입, 세터 등을 사용하여 빈을 주입받을 수 있다.

3. 빈 생명 주기 관리

빈의 생성, 초기화, 소멸 관리.

4. 스코프(Scope) 관리

빈의 스코프 관리.

5. 컴포넌트 스캔 (Component Scan)

애플리케이션에서 정의된 빈을 자동으로 스캔 및 등록.

@Component, @Service, @Repository, @Controller 등의 어노테이션을 사용하여 빈을 정의하고, 스프링은 이를 찾아 컨테이너에 등록한다.

 

1, 2, 5번 역할에 대해 알아보자

 

서버 실행 과정 (스프링 컨테이너 동작 과정)

1. 서버 시작

main() 메서드에서 SpringApplication.run(YourApplication.class, args); 실행

2. 스프링 서버 내부에 스프링 컨테이너(Application Context)가 만들어짐

Application Context가 생성되면서 빈(Bean)들을 관리할 컨테이너가 생성됨

설정파일(@Configuration)과 컴포넌트 스캔(@ComponentScan)을 통해 필요한 클래스를 찾음

3. 스프링 컨테이너에 빈(Bean) 등록 및 의존성 주입

-1 미리 작성되어 있던 프레임워크의 코드들로 인해서 기본 스프링 빈들이 등록됨 (JdbcTemplatae, DataSource, Environment 등...)

-2 우리가 설정해준 스프링 빈이 등록됨

@Component, @Service, @Repository, @Controller 등을 사용한 클래스가 스프링 컨테이너에 등록됨

이때 클래스와 함께 다양한 정보들이 들어가며, 인스턴스화가 이루어짐

등록된 빈들 간의 의존성이 @Autowired, 생성자 주입 등을 통해 자동으로 연결됨

*인스턴스화한다? 다른 클래스에서 @Autowired 등을 통해 어떤 클래스를 주입받을 때, 이미 생성된 객체(빈)을 가져다 씀

*빈(Bean): 스프링 컨테이너에서 관리하는 인스턴스화된 객체

*스프링 빈 (Spring Bean): 스프링 컨테이너에 등록된 인스턴스(빈)

4. 이때 필요한 의존성이 자동으로 설정됨

 

예시

Controller 코드에 사용하는 @RestController 어노테이션의 기능

- 바로 아래 있는 클래스를 API의 진입 지점으로 만들어 줌

- 해당 클래스를 스프링 빈으로 등록시킴

Controller 코드에서 JdbcTemplate을 통해 sql을 자동으로 database로 넘겨주도록 했을 때, JdbcTemplate을 Controller코드에 의존성 등록 해줘야 함.

이 JdbcTemplate도 진작 스프링 컨테이너 안으로 등록되어 있고, Controller 코드를 스프링 빈으로 인스턴스화 할때 JdbcTemplate도 등록

 

JdbcTemplate은 다음과 같은 순서를 따른다.

JdbcTemplate 파일의 위치는 SpringBoot AutoConfigure 패키지 내부(org.springframework.boot.autoconfigure.jdbc)

JdbcTemplateAutoConfigurarion 클래스가 JdbcTemplate을 스프링 빈으로 등록하는 역할

1. build.gradle 파일에 spring-boot-starter-data-jpa 의존성을 추가

-> 이 스타터가 필요한 라이브러리, 프레임워크를 자동으로 포함시킴

2. starter-data-jpa는 JPA 관련 설정과 함께 spring-boot-starter-jdpc도 포함한다

3. spring-boot-starter-jdpc가 JDBC 관련 설정 및 JdbcTemplate을 자동 구성

4. JdbcTemplateAutoConfiguration 클래스가 실행되면서 JdbcTemplate을 스프링 빈으로 등록함

5. 스프링 컨테이너에 등록된 빈을 @Autowired 등을 통해 바로 주입받아 사용 가능

 

스프링 컨테이너는 서로 필요한 관계에 있는 스프링 빈끼리 연결해줌

 

컨트롤러 --> 서비스 --> 레포지토리의 관계도, 모두 스프링 빈으로 등록해서, 컨트롤러의 생성자에서 서비스를, 서비스의 생성자에서 레포지토리를 가져오도록 하면, 컨테이너 생성과 동시에 모든 의존성이 연결될 것

 

스프링 컨테이너를 사용하는 이유?

Spring Container를 사용하지 않는 경우

의존하는 인터페이스의 구현체를 바꿔주는 경우, 기존 방식대로(의존하는 클래스에서 직접 구현체를 선언해주는 방식) 개발자가 직접 설정해주는 방식으로 한다면, 의존되는 클래스의 코드 뿐 아니라 의존하는 클래스의 코드까지 바꿔줘야 하는 귀찮은 일이 발생할 수 있다.

Spring Container를 사용하는 경우

스프링 컨테이너가 어떤 구현타입을 쓸지 대신 결정해주어, 해당 클래스를 사용하던 곳의 코드 변경 없이 우리가 원하는 클래스를 주입할 수 있다. (제어의 역전)

 

예시

Repository를 다른 클래스로 교체하는 상황. 이는 Repository의 역할이니까, Service 코드까지 바꾸지 않고 레포지토리만 바꿔주고 싶다.

-> 인터페이스 사용 Repository 인터페이스 안에 saveUser();라는 함수 선언만 해주고, 구현은 다른 클래스에서 하자.

그러면 service 클래스에 Repository를 선언할 땐 인터페이스를 선언하고, 어떤 구현 클래스를 사용할 지만 지정해주면 됨

 

스프링 컨테이너를 사용하면(스프링 컨테이너에 Service와 Repository를 모두 등록해놓으면), 컨테이너가 Service 클래스를 대신 인스턴스화하고, 그때 알아서 사용할 구현 Repository 클래스를 연결해준다. (구현 클래스까지 모두 컨테이너에 등록해놔야 함)

 

이렇게 우리가 서비스에서 new [클래스 네임]으로 어떤 구현체를 쓸지 결정하는 게 아니라, 스프링컨테이너가 결정해주는 이런 방식을 제어의 역전(IoC, Inversion of Control)이라 함

컨테이너가 특정 구현체를 선택해 의존하는 클래스에 연결해주는 과정: 의존성 주입(DI, Dependency Injection)

 

제어의 역전에서 의존성 선택 방법

스프링 컨테이너는 어떤 기준으로 여러 개 중 하나의 레포지토리를 주입할까?

사용하기로 결정한 클래스에, 이 스프링 빈이 우선권이 있음을 @Primary  어노테이션을 붙여 표시해줌

@Primary: 우선권을 결정하는 어노테이션

 

스프링 컨테이너 사용법

스프링 빈을 등록하는 방법

  • @RestController, @Service, @Repository 어노테이션
    • 클래스 위에 붙이면,해당 클래스를 API 컨트롤러, 서비스, 레포지토리 역할로 등록하고
    • 해당 클래스를 컨테이너에 스프링 빈으로 등록하여 관리
    • 개발자가 직접 만든 클래스를 스프링 빈으로 등록할 때 많이 사용
  • @Configuration + @Bean
    • 둘은 세트
    • @Configuration: 클래스에 붙이는 어노테이션. @Bean을 사용할 때 함께 사용해줘야 한다.
    • @Bean: 메소드에 붙이는 어노테이션. 메소드에서 반환되는 객체를 스프링 빈에 등록한다.
    • @Bean을 사용할 클래스 위에 @Configuration을 붙이면, "이 클래스 안에서 스프링 빈을 등록할 거야" 라는 의미가 되고, @Bean이 붙은 메서드에서 반환하는 객체를 스프링 컨테이너가 빈으로 등록한다. 
    • 이렇게 등록된 빈은 다른 클래스에서 의존성 주입을 통해 사용할 수 있다.
    • 외부 라이브러리, 프레임워크에서 만든 클래스를 등록할 때 사용
  • @Component 어노테이션
    • 주어진 클래스를 '컴포넌트'라고 간주하도록 하는 어노테이션
    • '컴포넌트'는, 스프링 서버가 시작할 때 자동으로 감지되어 스프링 컨테이너에 스프링 빈으로 등록되는 클래스.
    • @Component는, @RestController, @Service, @Repository 어노테이션 안에 포함되어 있다.
    • 컨트롤러, 서비스, 레포지토리가 모두 아니고, 개발자가 직접 작성한 클래스를 스프링 빈으로 등록할 때 사용하기도 한다.
    • 적은 경우에 사용되는 스프링 등록 방법

스프링 빈을 주입받는(가져오는) 방법

  • (가장 권장) 생성자를 이용해 주입받는 방식
    • 지금까지 사용한 방식
    • 변수를 만들고, 생성자를 만들면 스프링빈이 주입됨 
    • @Autowired 생략 가능
public class MyService {
	private final MyRepository myRespository;
	
    public MyService(MyRepository myRepository) {
    	this.myRepository = myRepository;
    }
더보기
원래는 생성자 위에 @Autowired라는 어노테이션을 입력했어야 했다.
@Autowired: 이 생성자의 매개변수에 스프링 빈을 집어넣어, 자동으로 연결해줘. 라는 어노테이션
스프링 버전이 업데이트되면서, 이 어노테이션을 사용하지 않아도 생성자 방식일 때는 바로 주입되도록 변경됨.
  • setter와 @Autowired를 사용하는 방법
    • 누군가 setter를 사용하면 다른 인스턴스로 교체되기 때문에 오작동할 수 있다.
public class MyService {
	private MyRepository myRespository;
	
    @Autowired
    public void setMyRepository(MyRepository myRepository) {
    	this.myRepository = myRepository;
    }
  • 필드에 직접 @Autowired 사용
    • 생성자 없이 이렇게만
    • 테스트코드 작성 시 테스트를 어렵게 만드는 요인
public class MyService {
	@Autowired
	private MyRepository myRespository;

 

@Qualifier

스프링 빈을 사용하는 쪽과 스프링 빈을 등록하는 쪽 모두 사용 가능한 어노테이션

인터페이스 Service에 의존하는 클래스가 있고, 이 인터페이스를 구현한 클래스가 여러 개인 경우, Qualifier를 사용하여 어떤 클래스를 사용할 지 정해줄 수 있다.

  • 스프링 빈을 사용하는 쪽에서 사용: 빈의 이름 적어주기
    • 이 경우 의존하는 클래스의 생성자에 @Qualifier("사용할 클래스 이름")을 입력하면 해당 클래스가 들어감
    • 여러 개의 후보군이 있을 때 그 중 하나를 특정해서 가져오고 싶은 경우, 스프링 빈의 이름을 @Qualifier를 통해 지정함으로써 여러 개의 후보 중 특정 클래스를 가져올 수 있게끔 해줌
    • *스프링 빈을 수동으로 등록할 경우, 스프링 빈의 이름은 클래스의 이름 중 맨 앞글자만 소문자로 바뀐 형태. (그 클래스가 인스턴스화 되어 컨테이너에 들어가는 거니까)
public class MyService {
	private final MyRepository myRespository;
	
    public MyService(MyRepository myRepository, @Qualifier("thisRepository") MyRepository myRepository) {
    	this.myRepository = myRepository;
    }

 

  • 사용하는 쪽과 등록하는 쪽 모두 사용: @Qualifier끼리 연결됨
    • 스프링 빈을 등록할 때부터 특별한 이름을 등록하고, 사용하는 쪽에도 그 특별한 이름을 가리키게 하면 특정 두 개를 연결시켜 준다.
    • 인터페이스의 구현 클래스 중 한 클래스에 @Qualifier("main")이라고 쓰고, 그 인터페이스에 의존하는 클래스에도 @Qualifier("main")이라고 쓰면, 해당 클래스를 주입한다. (괄호 안의 글자가 동일해야함)

 

그렇다면 @Primary v.s. @Qualifier?

하나의 인터페이스를 구현한 세 가지 클래스 중, 하나에는 @Primary를, 또다른 하나에는 @Qualifier를 달아준다면?

@Qualifier를 적어준 쪽이 우선순위를 갖게 된다.

스프링은 일반적으로 더 특수하게 사용자가 명시해준 것을 우선순위를 높게 가져가는 경향이 있다.