본문 바로가기
Back-end/Spring

4_IoC, 스프링 컨테이너, DI, 스프링 빈

by 카랑현석 2025. 3. 27.

참고

아래 도서를 일부 각색하였습니다. (이해를 위해 저만의 용어를 사용하고 내용을 일부 추가)

스프링 부트 3 백엔드 개발자 되기 - 자바편

Summary

IoC : 객체의 생성과 관리를 개발자가 하는 것이 아니라 프레임워크(Ex. 스프링)이 대신하는 것

스프링 컨테이너 : 스프링에서 객체관리하는

DI : 외부에서 객체를 주입받아 사용하는 것

스프링 빈 : 스프링 컨테이너가 생성하고 관리하는 객체

@Autowired : 특정 클래스에서 특정 객체를 주입 받는다.

@Bean : 스프링 컨테이너에 스프링 빈 등록

IoC와 스프링 컨테이너

IoC(Inversion of Control)는 제어의 역전이다. 우리는 평소에 자바 코드를 작성해서 객체를 생성할 때 객체가 필요한 곳에서 직접 생성했을 것이다. 아래처럼 B 객체를 사용하기 위해 클래스 A에서 객체를 직접 생성한다.

 

[코드1]

 

제어의 역전은 이 것이 역전된 것이다. 즉, 다른 객체를 직접 생성하거나 제어하는 것이 아니라 외부에서 관리하는 객체를 가져와 사용한다. [코드1]에 제어의 역전을 적용하면 [코드2]의 형태로 바뀐다.

 

[코드2]

[코드2]는 B 객체를 직접 생성(new()) 하는 것이 아니라 어디선가 받아와서 사용하고 있다는 것을 추측할 수 있다.

[코드2] 처럼 제어의 역전을 적용하면 객체를 외부에서 관리하게 되고 실제로 사용할 때는 외부에서 제공해주는 객체를 받아오게 된다.

방금까지 설명한 객체를 관리해주는 "외부" 의 존재는 "스프링 컨테이너" 이다.

조금 더 쉽게 말해 스프링 컨테이너는 스프링에서 객체를 관리하는 통이다.

 

DI과 스프링 빈

위의 설명처럼 스프링에서는 객체들을 관리하기 위해 제어의 역전(IoC)을 사용한다. 제어의 역전(IoC)를 구현하기 위해 사용하는 방법이 DI(Dependency Injection) 이다. 직역하면 "의존성 주입, 의존성을 넣어준다"이다.

 

DI는 어떤 클래스가 다른 클래스에 의존한다는 뜻이다. 코드를 통해 이해해보면 쉽다.

 

[코드3]

[코드3]에서 @Autowired 라는 어노테이션은 스프링 컨테이너에 있는 빈을 주입해주는 역할을 한다. 빈은 스프링 컨테이너에서 관리하는 객체이다.

이 말을 조금 더 쉽게 풀어서 쓰면 스프링 컨테이너(객체 저장소)에 있는 것 중에 저장된 객체 B를 클래스 A에 넣어준다는 의미이다.

 

즉, 어디에선가 B 객체를 생성(new())하고 스프링 컨테이너의 빈으로 저장해둔 것을 클래스 A에서는 @Autowired 라는 주입 명령을 통해 B 객체를 주입받은 것이다.

 

마치 아래 그림에서 [코드1]은 왼쪽과 같은 상황, [코드3]은 오른쪽과 같은 상황인 것이다.

 

 

스프링 컨테이너와 빈

스프링은 스프링 컨테이너를 제공해준다. 스프링 컨테이너는 빈을 생성하고 관리해준다. 즉, 빈이 생성되고 소멸되기까지의 생명주기를 스프링 컨테이너가 관리하는 것이다.

그리고 위의 [코드3] 처럼 @Autowired 라는 어노테이션을 사용해서 빈을 주입받을 수 있도록 DI를 지원한다.

그러면 은 무엇일까?

스프링 컨테이너가 생성하고 관리하는 "객체"이다. 쉽게 이야기해서 스프링 컨테이너에게 관리받고 있는 객체라고 생각하면 된다. [코드3]에서 @Autowired 로 주입받은 B 객체가 바로 이다.

 

빈을 스프링 컨테이너에 등록하는 방법은 XML 파일에서 설정하는 방법, 컴포넌트 스캔으로 자동 의존관계 설정, 자바 코드로 직접 스프링 빈을 등록하는 방법을 제공한다. (최근 XML 파일에서 설정하는 방법은 잘 사용하지 않는다.)

 

[코드4]

 

위 코드([코드4]) 에서 MyBean 클래스에 @Component 어노테이션을 붙이면 MyBean 클래스가 빈으로 등록된다. 그러면 스프링 컨테이너에서 MyBean 클래스를 관리할 수 있다. 빈의 이름은 클래스 이름의 첫 글자를 소문자로 바꿔 관리한다. [코드4]의 빈의 이름은 'myBean' 이다. (스프링 빈을 등록하는 2가지 방법이 있다. 그 중에서 이 방법은 컴포넌트 스캔과 자동 의존관계 설정 방법이다.)

 

즉, 스프링 컨테이너는 빈(객체)를 관리하는 저장/관리소이고 빈은 스프링 컨테이너로 관리되고 있는 객체 라고 생각하면 된다.

 

@Component 는 알겠어요. 그런데 @Controller, @Repository, @Service 어노테이션은요?

// [코드5]
@Controller
public class MemberController {

    private final MemberService memberService; // 2번

    @Autowired
    public MemberController(MemberService memberService) { // 1번
        this.memberService = memberService;
    }
}

 

[코드5] 처럼 Controller 코드에는 항상 @Controller 어노테이션이 붙고, Repository 코드에는 항상 @Repository 어노테이션이 붙고, Service 코드에는 항상 @Service 어노테이션이 붙는 것을 보았다.

 

[코드5]를 보면 MemberService 라는 객체를 new()로 직접 생성하여 사용하는 것이 아니라 MemberController 클래스의 생성자를 호출할 때 @Autowired 가 있으므로(1번) MemberService 객체가 스프링 컨테이너에 저장이 된다. 그리고 스프링 컨테이너에 저장된 'memberService' 빈을 주입(2번)하여 사용한다.

 

그런데 @Controller은 무엇일까? @Controller 코드를 까보면 아래와 같다.

[코드6]

 

위 코드에서 뭔가 익숙한 어노테이션이 있다. 바로 18번째 줄에 @Component 어노테이션이 있다. 즉, @Controller 어노테이션은 @Component 어노테이션을 가지고 있다. 즉, @Component 와 동일하게 클래스를 빈으로 등록하는 기능을 가지고 있는 것이다. (물론, @Controller 어노테이션은 클래스를 빈으로 등록하는 기능 외에도 추가적인 기능들이 많이 있지만 여기 주제와 벗어나기 때문에 생략한다.)

그래서 [코드5]의 MemberController 클래스는 스프링 컨테이너에 빈 이름이 'memberController'으로 저장되게 된다.

 

  위의 @Controller 어노테이션의 설명처럼 @Repository 어노테이션, @Service 어노테이션도 @Component 어노테이션을 가지고 있기 때문에 클래스를 스프링 컨테이너의 빈으로 등록하는 기능을 가지고 있다.

 

그런데, @Controller 어노테이션은 Controller에 특화된 기능들을 가지고 있고, @Repository 어노테이션은 Repository 기능에 특화된 기능들을 가지고 있고 @Service 어노테이션은 Service 기능에 특화된 기능들을 가지고 있기 때문에 위와 같은 어노테이션으로 각각 관례적으로 사용하는 것이다.

 

[(참고) 각각의 역할]

Controller : 외부 요청을 받는다.

Service : 비즈니스 로직 담당

Repository : 데이터를 저장/관리한다.

 

그럼 아무데서나 @Component 로 만들어도 될까?

설정하는 방법은 있지만 default는 안된다고 한다.

 

위와 같은 폴더 구조에서 Test 클래스에 @Component 어노테이션을 붙이면 스프링 컨테이너가 빈으로 인식해서 가져갈 수 있을까? 결론부터 말하자면 default는 아니다.

 

 

main 함수가 있는 HelloSpringApplication 에서 @SpringBootApplication 어노테이션을 까보면 @ComponentScan 이라는 어노테이션이 있는데 이 것이 Service, Repository, Controller, Component로 등록된 어노테이션들의 클래스를 자동으로 스캔하여 빈으로 등록해주는 역할을 하는 것이다.

그런데 스캔을 하는 범위가 main 함수에 해당하는 패키지인 (위 코드의 1번쨋줄) hello.hellospring 하위에 있는 파일들에서만 쫙 다 스캔하기 때문에 hello.hellospring 폴더 밖에 있는 Test 클래스에 @Component 어노테이션을 붙이더라도 스프링 컨테이너에 들어가지 않는다.

 

직접 설정 파일을 만들어서 스프링 빈 등록하기 

이전까지 봤던 것은 컴포넌트 스캔을 통해 스프링 빈을 등록하는 방법이었다.

아까도 3가지를 소개했지만 XML 파일에서 설정하는 방식은 잘 사용하지 않는다. 그러면 남은 방식은 직접 설정 파일을 만들어서 스프링 빈을 등록하는 방법이다. 이 것은 알아두어야 한다.

 

단, Controller는 반드시 컴포넌트 스캔 방식을 사용한다. (Controller 어노테이션의 고유 기능을 활용해야 하므로) 나머지는 설정 파일을 만들어서 스프링 빈을 등록하는 방법을 사용해도 된다.

 

1️⃣SpringConfig 클래스 만들기

main 함수가 위치한 패키지 디렉토리와 동일한 위치에 SpringConfig 클래스를 만든다.

현재 main 함수가 있는 패키지는 hello.hellospring 이므로 SpringConfig 클래스도 이 안에 만들어 줍니다.

 

2️⃣ @Configuration 어노테이션을 추가하면 마치 "스프링 빈을 추가할게~" 라고 미리 언질을 주는 것과 같은 효과이다.

그리고 @Bean 어노테이션에 추가하고 싶은 클래스를 return 값으로 넣어주면  스프링 컨테이너에 스프링 빈을 등록한다.

package hello.hellospring;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService() {
        return new MemberService(memberRepository());
    }

	// memberRepository는 인터페이스고 MemoryMemberRepository가 구현체로 가정한다. (인터페이스는 new 불가)
    @Bean
    MemberRepository memberRepository() { 
        return new MemoryMemberRepository();
    }
}

 

 

기타 알아두면 좋을 것들

1. 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, default는 싱글톤으로 등록한다. (유일하게 하나만 등록해서 공유한다.) 즉, 같은 스프링 빈이면 모두 같은 인스턴스이다. -> 메모리 절약 Good

물론 싱글톤으로 되지 않게 하는 방법도 있지만 일반적인 대부분 경우에는 싱글톤으로 사용한다고 한다.

 

2. DI에는 필드 주입, setter 주입, 생성자 주입이 있다. 생성자 주입을 사용하는 것을 추천한다.

의존관계가 실행중에(runtime 중에) 동적으로 변하는 경우는 없다. 따라서 생성자 주입을 해놓고 바뀌지 않도록 한다.

  • setter 주입의 경우 public 으로 두어야 하는데 그런 경우 다른 개발자가 메서드를 호출할 수도 있기 때문에 좋지 않다.

[생성자 주입 예시 (추천)]

@Controller
public class MemberController {
    
    private final MemberService memberService;
    // 생성자 주입
    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

[필드 주입 예시 (비추천)]

  • 스프링 빈으로 넣은 이후 코드 레벨에서 바꿀 수 있는 방법이 없다. (정확하지 않다.)
@Controller
public class MemberController {

    @Autowired private MemberService memberService; // 필드 주입 (비추천)
}

 

[setter 주입 예시 (비추천)]

  • public이므로 누군가가 memberService.setMemberRepository() 처럼 memberService 객체에 접근해서 호출할 수 있다. 그런데 개발을 할 때는 호출하지 않아도 될 메서드는 최대한 호출하지 않는 것이 좋다. (정확하지 않다.)
  • 그래서 생성자로 딱 한 번 주입해놓고 변경을 못하도록 막는 것이 중요해서 setter 주입 방식은 비추천한다. (정확하지 않다.)
@Controller
public class MemberController {

    private MemberService memberService;

    // setter 주입
    @Autowired
    public void setMemberService(MemberService memberService) {
        this.memberService = memberService;
    }
}

 

 

3. 🌟 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔(@Controller, @Service, @Repository, @Component)을 사용한다. 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정(SpringConfig)을 통해 스프링 빈으로 등록한다.

  • Ex) Repository를 설계할 때 데이터 저장소가 선정되지 않은 경우 MemberRepository 인터페이스를 만들어두고 구현체로 MemoryMemberRepository 클래스를 만든 상황이다. 그런데, MemoryMemberRepository 를 나중에 다른 Repository로 바꿀 예정인 상황 = 상황에 따라 구현 클래스를 변경해야 하는 상황
  • 이때 기존에 작성한 코드를 손대지 않고 설정 클래스에서 해당 스프링 빈만 바꿔서 바꿔치기 하는 방법이 있다.

아래 코드에서 MemoryMemberRepository 을 사용하다가 JpaMemberRepository 을 사용하는 것으로 바꾸는 경우 클래스 내부 코드를 바꾸는 것이 아니라 스프링 빈 코드 설정 파일 클래스(현재는 SpringConfig 클래스)에서 코드 한 줄만 바꾸면 된다.

이렇게 여러 개를 써놓고 언제든지 쉽게 교체할 수 있다.

즉, 확장에는 열려있고 수정에는 닫혀있다. 이를 객체지향의 5대 원리(SOLID) 중 개방-폐쇄의 원칙(Open-Closed Principle) 이라고 한다.

 

--> 인터페이스에서 구현체를 바꾸면서도 기존 코드를 변경 없이 바꿀 수 있는 것이 객체지향의 매력이자 장점

@Configuration
public class SpringConfig {

    private final MemberRepository memberRepository;

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

// 다형성
//    @Bean
//    MemberRepository memberRepository() {
//        return new MemoryMemberRepository();
//        return new JdbcMemberRepository(dataSource);
//        return new JdbcTemplateMemberRepository(dataSource);
        return new JpaMemberRepository(em);
    }
}

 

출처 : 인프런_김영한 스프링 입문 강의 (memberRepository 를 사용하다가 memberRepository 로 바꾼 경우 Bean 설정 파일쪽의 코드만 바꿔주면 된다.

 

 

4. 당연히 스프링 컨테이너에 스프링 빈으로 등록되어 있지 않으면 @Autowired 가 작동하지 않는다. new()로 만든 객체인 경우에도 @Autowired가 작동하지 않는다. 스프링 컨테이너에 올라간 스프링 빈만 @Autowired 가 작동한다.