개발 공부/백엔드

스프링 부트 - 스프링 빈과 의존관계

baby-t 2025. 9. 17. 22:56

https://www.inflearn.com/

 

인프런 - 라이프타임 커리어 플랫폼

프로그래밍, 인공지능, 데이터, 마케팅, 디자인등 입문부터 실전까지 업계 최고 선배들에게 배울 수 있는 곳.

www.inflearn.com

 

인프런 사이트의 김영한님의 강의를 보면서 작성한 글입니다.

 

스프링으로 애플리케이션을 개발할 때, 우리는 Controller, Service, Repository와 같이 역할을 분리하여 계층형 구조로 설계합니다. 이때 MemberController가 MemberService를 필요로 하는 것처럼, 각 컴포넌트는 다른 컴포넌트를 의존하게 됩니다.

이러한 의존관계는 개발자가 직접 객체를 생성(new)하여 관리할 수도 있지만, 스프링을 사용하는 가장 큰 이유는 스프링 컨테이너에게 이 모든 것을 맡기기 위해서입니다.

이번 포스팅에서는 스프링 컨테이너가 객체를 어떻게 관리(스프링 빈)하고, 이 객체들 사이의 의존관계를 어떻게 연결(DI)해 주는지 알아보겠습니다.


## 1. 스프링 빈(Bean)과 스프링 컨테이너

MemberController 클래스에 @Controller 어노테이션을 붙이면, 스프링은 시작 시점에 이 클래스의 객체를 하나 생성해서 스프링 컨테이너라는 특별한 공간에 보관합니다. 이렇게 스프링 컨테이너가 관리하는 모든 객체를 우리는 **스프링 빈(Bean)**이라고 부릅니다.

만약 컨트롤러가 new MemberService()처럼 서비스를 직접 생성하면, 다른 컨트롤러가 MemberService를 필요로 할 때마다 새로운 객체를 계속 만들어야 합니다. 이는 메모리 낭비입니다. 따라서 MemberService 역시 스프링 컨테이너에 단 하나만(싱글톤) 등록해두고, 모두가 이를 공유해서 쓰는 것이 훨씬 효율적입니다.


## 2. 의존관계 주입(DI)과 @Autowired

MemberController가 스프링 컨테이너에 있는 MemberService 빈을 사용하려면, 둘 사이를 연결해달라는 요청이 필요합니다.

Java
 
@Controller
public class MemberController {

    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}
  • 생성자 주입: 위와 같이 생성자의 파라미터로 MemberService를 받도록 선언합니다.
  • @Autowired: 생성자에 이 어노테이션을 붙여주면, 스프링이 컨테이너에서 알아서 MemberService 타입의 빈을 찾아 주입해 줍니다. 이것이 바로 **의존관계 주입(Dependency Injection, DI)**입니다.

여기서 문제가 발생합니다. @Controller는 스프링 빈이지만, MemberService는 @Service 같은 어노테이션이 없는 순수 자바 클래스이므로 스프링 빈이 아닙니다. 따라서 스프링 컨테이너는 주입할 MemberService 빈을 찾지 못해 오류를 발생시킵니다.

이 문제를 해결하려면, 의존성의 대상이 되는 MemberService와 그가 의존하는 MemberRepository 역시 모두 스프링 빈으로 등록해야 합니다.


## 3. 스프링 빈을 등록하는 2가지 방법

### 방법 1: 컴포넌트 스캔 (Component Scan)

가장 간단한 방법은 각 클래스에 어노테이션을 붙여주는 것입니다. 스프링은 애플리케이션 시작 위치(HelloSpringApplication)를 기준으로 하위 패키지를 모두 스캔하며 다음 어노테이션이 붙은 클래스를 찾아 자동으로 빈으로 등록합니다.

  • @Controller: 컨트롤러 계층
  • @Service: 서비스 계층 (핵심 비즈니스 로직)
  • @Repository: 리포지토리 계층 (데이터 접근)

이 어노테이션들은 모두 내부에 @Component 어노테이션을 포함하고 있어, 사실상 모두 같은 스캔 대상입니다.

### 방법 2: 자바 코드로 직접 등록 (Java Configuration)

컴포넌트 스캔 방식 대신, 설정 파일을 만들어 수동으로 빈을 등록할 수 있습니다. 이는 향후 구현체를 변경해야 할 때 매우 유용합니다.

service 폴더 등에 SpringConfig 클래스를 하나 만들고 다음과 같이 작성합니다.

Java
 
@Configuration
public class SpringConfig {

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

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • @Configuration: 이 파일이 스프링의 설정 파일임을 알립니다.
  • @Bean: 이 메소드가 반환하는 객체를 스프링 빈으로 등록하라고 알립니다.

## 4. DI의 3가지 방식과 '생성자 주입'을 권장하는 이유

DI를 수행하는 방법에는 크게 3가지가 있습니다.

  1. 필드 주입 (Field Injection): 필드에 바로 @Autowired를 붙이는 방식입니다.
    • 단점: 코드가 간결해 보이지만, 외부에서 변경이 불가능하여 테스트하기가 매우 어렵습니다.
       
    • Java
      @Autowired private MemberService memberService;
      
  2. Setter 주입 (Setter Injection): set 메소드에 @Autowired를 붙이는 방식입니다.
    • 단점: public으로 열려있어, 애플리케이션 동작 중에 누군가 이 메소드를 호출하여 의존관계를 변경할 위험이 있습니다.
       
    • Java
      @Autowired
      public void setMemberService(MemberService memberService) {
          this.memberService = memberService;
      }
      
  3. 생성자 주입 (Constructor Injection)
    • 장점: 스프링이 가장 권장하는 방식입니다. 객체가 생성되는 시점에 단 한 번만 호출되며, 의존관계를 **final**로 선언하여 불변성(immutability)을 확보할 수 있습니다. 또한, 테스트 코드 작성 시 의존성을 누락하지 않고 주입하기 편리합니다.
       
       
    • Java
      @Autowired
      public MemberController(MemberService memberService) {
          this.memberService = memberService;
      }
      

## 결론: 어떤 방식을 선택해야 하는가?

현재 우리의 상황(MemberRepository가 인터페이스임)을 고려할 때, 두 번째 방법인 **'자바 코드로 직접 등록'**하는 것이 가장 이상적입니다.

  • Controller, Service처럼 정형화된 컴포넌트는 컴포넌트 스캔을 사용해도 좋습니다.
  • 하지만 Repository처럼, 지금은 MemoryMemberRepository를 쓰지만 나중에는 JdbcMemberRepository나 JpaMemberRepository 등으로 구현 클래스를 변경할 가능성이 있는 컴포넌트는, SpringConfig에서 수동으로 빈을 등록하는 것이 좋습니다.

그렇게 하면, 나중에 데이터베이스가 정해졌을 때 SpringConfig의 코드 단 한 줄만 수정하면(return new Memory... -> return new Jdbc...) 애플리케이션의 다른 코드 수정 없이 의존관계를 손쉽게 교체할 수 있습니다.