프레임워크 · Spring · IoC · DI · 빈

Spring · IoC와 의존성 주입

Spring의 핵심은 한 문장으로 줄일 수 있습니다 — "객체를 만들고 연결하는 일을 개발자가 아니라 Spring이 대신한다." 이것이 IoC(제어의 역전) 이고, 그 구체적인 방법이 DI(의존성 주입)? 입니다. 이 페이지에서는 빈을 등록하고 주입받는 모든 방법을 차근차근 풀어봅니다.

1. IoC — 제어의 역전

보통 프로그램은 내가 필요한 객체를 직접 만듭니다. 예를 들어 서비스 안에서 new UserRepo() 라고 적어 리포지토리를 직접 생성하죠. IoC(Inversion of Control, 제어의 역전) 는 이 "객체를 만들고 서로 연결하는 권한"을 개발자에게서 빼앗아 Spring? 컨테이너에게 넘기는 것입니다. 누가 객체를 만들지 그 제어권이 나 → 프레임워크로 뒤집혔다(역전) 고 해서 이런 이름이 붙었어요.

음식점을 떠올려 보세요. 손님이 주방에 들어가 직접 재료를 사 와 요리하지는 않습니다. "비빔밥 하나요" 하고 주문(요청) 만 하면, 주방(컨테이너)이 재료를 모아 완성된 요리를 가져다줍니다. 내가 재료를 챙기는 게 아니라 주방이 챙겨준다 — 이 역할 뒤집힘이 바로 IoC입니다.
제어가 역전되면 좋은 점
  • 코드가 느슨해진다 — 객체끼리 "누가 누구를 직접 만드는지" 몰라도 되니 서로 덜 얽힌다
  • 바꿔 끼우기 쉽다 — 같은 자리에 다른 구현을 넣어도 사용하는 쪽 코드는 그대로
  • 테스트가 편하다 — 진짜 객체 대신 가짜(mock) 객체를 넣어 시험할 수 있다

2. 빈과 컨테이너

Spring 컨테이너가 만들고 관리하는 객체를 빈(Bean) 이라고 부릅니다. 일반적인 객체와 다를 것은 없지만, "내가 new 로 만든 게 아니라 Spring이 만들어 보관 중인 객체"라는 점이 다릅니다. 이 빈들을 담아두고 필요할 때 꺼내주는 보관소가 컨테이너 입니다.

이름설명
BeanFactory빈을 만들고 보관하는 가장 기본적인 컨테이너 인터페이스. 최소 기능.
ApplicationContextBeanFactory를 확장한 실제 사용 컨테이너. 자동 설정·이벤트·국제화 등 추가 기능 포함. Spring Boot? 가 실행될 때 이게 떠서 빈들을 채워 넣는다.

애플리케이션이 시작되면 컨테이너가 빈들을 전부 만들어 서로 연결(주입)한 뒤, 요청이 들어올 때마다 이미 준비된 빈을 꺼내 씁니다. 우리가 직접 객체를 조립할 필요가 없는 이유가 여기 있습니다.

3. 빈 등록 방법

어떤 객체를 빈으로 쓰고 싶으면 Spring에게 "이건 빈이야"라고 알려줘야 합니다. 크게 두 가지 길이 있습니다.

3-1. 스테레오타입 애너테이션 + 컴포넌트 스캔

클래스 위에 스테레오타입(stereotype, 역할 표시) 애너테이션 을 붙이면, Spring이 시작할 때 패키지를 훑어 (이것을 컴포넌트 스캔 이라 합니다) 표시된 클래스를 자동으로 빈으로 등록합니다. 아래 다섯 개는 사실 모두 @Component? 의 특수한 형태입니다 — 동작은 같고 "이 클래스가 어느 층 소속인지" 의미만 다릅니다.

애너테이션의미 / 쓰는 곳
@Component?가장 일반적인 빈. 아래 다른 것에 해당하지 않는 모든 빈에 사용.
@Service비즈니스 로직을 담는 서비스 층. 기능은 @Component와 같지만 "서비스다"라는 의미를 준다.
@Repository?DB 접근 층. 추가로 DB 예외를 Spring 공통 예외로 자동 번역해 준다.
@Controller웹 요청을 받는 층(주로 화면을 반환하는 MVC).
@RestController?@Controller + @ResponseBody. 반환값을 JSON으로 응답하는 REST API 진입점.
@Configuration설정 클래스. 이 안에서 @Bean 메서드로 빈을 직접 만들어 등록할 수 있다.

그럼 Spring은 어느 패키지를 훑을까요? @ComponentScan 이 그 시작 지점을 정합니다. 그런데 보통은 이걸 직접 쓸 일이 없습니다 — 앱 진입점에 붙은 @SpringBootApplication 안에 이미 @ComponentScan 이 포함되어 있어, 진입점 클래스가 있는 패키지와 그 하위 를 자동으로 스캔하기 때문입니다. 이 프로젝트라면 com.example.study 패키지 아래가 전부 스캔 대상입니다.

@SpringBootApplication 한 줄에 들어있는 것

@SpringBootApplication@SpringBootConfiguration(내부적으로 @Configuration) + @EnableAutoConfiguration + @ComponentScan 세 개를 묶은 것입니다. 그래서 이 한 줄이 자동 설정도 켜고 빈 스캔도 시작합니다.

3-2. @Bean — 메서드 단위 등록

내가 만든 클래스가 아니라 외부 라이브러리 객체 처럼 애너테이션을 붙일 수 없는 경우, 또는 생성 과정을 직접 손봐야 하는 경우엔 @Configuration 클래스 안에 @Bean 메서드를 만듭니다. 메서드가 반환하는 객체가 빈으로 등록되고, 기본 빈 이름은 메서드 이름이 됩니다.

@Configuration
public class AppConfig {

  @Bean                                  // 반환 객체가 빈으로 등록됨
  public ObjectMapper objectMapper() {   // 빈 이름 = "objectMapper"
    ObjectMapper m = new ObjectMapper();
    m.registerModule(new JavaTimeModule());
    return m;
  }
}

정리하면 — 내 코드 클래스는 스테레오타입 애너테이션, 외부/조립이 필요한 객체는 @Bean 으로 등록합니다.

4. 의존성 주입 3가지 방법

빈이 다른 빈을 필요로 할 때, 그 필요한 객체(의존성)를 Spring이 넣어주는 것이 주입(injection) 입니다. 넣는 방법은 세 가지가 있습니다.

4-1. 생성자 주입 (권장)

생성자의 매개변수로 필요한 빈을 받습니다. 필드를 final 로 선언하면 한 번 주입된 뒤 바뀌지 않습니다(불변). 생성자가 하나뿐이면 @Autowired 를 붙이지 않아도 Spring이 알아서 주입합니다. 실무에서는 Lombok의 @RequiredArgsConstructorfinal 필드용 생성자를 자동으로 만들어 주므로 코드가 깔끔합니다.

@Service
@RequiredArgsConstructor             // final 필드를 받는 생성자 자동 생성
public class UserService {
  private final UserRepo userRepo;   // Spring이 생성자로 주입

  // 생성자가 하나면 @Autowired 생략 가능
}
왜 생성자 주입을 권장하나

Spring 공식 문서도 생성자 주입을 권장합니다. 이유는 세 가지입니다. (1) 불변final 로 만들 수 있어 중간에 의존성이 바뀌지 않는다. (2) 테스트 편의 — Spring 없이도 new UserService(가짜리포) 로 직접 만들어 시험할 수 있다. (3) 순환 참조 조기 발견 — A가 B를, B가 A를 서로 필요로 하는 막힌 구조를 앱이 뜰 때 바로 에러로 알려준다.

4-2. 세터 주입

세터(set 메서드)에 @Autowired 를 붙여 주입합니다. 있어도 되고 없어도 되는 선택적 의존성 에 어울립니다. 나중에 다시 넣어줄(재설정) 수 있다는 장점이 있지만, 객체가 만들어진 직후엔 그 의존성이 비어 있을 수 있다는 단점이 있습니다.

@Service
public class ReportService {
  private MailSender mailSender;     // 선택적 의존성

  @Autowired(required = false)       // 없어도 통과
  public void setMailSender(MailSender mailSender) {
    this.mailSender = mailSender;
  }
}

4-3. 필드 주입

필드 위에 바로 @Autowired 를 붙입니다. 가장 짧지만 권장하지 않습니다. final 로 만들 수 없고, 테스트할 때 직접 값을 넣기 어려우며, 의존성이 몇 개인지 한눈에 안 보여 클래스가 비대해지는 것을 가립니다.

@Service
public class OrderService {
  @Autowired                         // 필드 주입 — 짧지만 비권장
  private UserRepo userRepo;
}
방식장점단점
생성자불변·테스트 쉬움·순환참조 조기발견의존성 많으면 생성자가 길어짐(사실 리팩터링 신호)
세터선택적 의존성·재설정 가능생성 직후 비어 있을 수 있음
필드코드가 가장 짧음final 불가·테스트 어려움·비권장

5. 주입을 돕는 애너테이션

같은 타입의 빈이 여러 개라 Spring이 "어느 걸 넣어야 하지?" 헷갈릴 때 길잡이가 되는 애너테이션들입니다.

애너테이션역할
@Autowired주입 지점 표시. required = false 면 해당 빈이 없어도 에러 없이 넘어간다(기본값은 true).
@Qualifier("이름")같은 타입 빈이 여럿일 때 이름으로 콕 집어 선택한다.
@Primary같은 타입 후보 중 기본으로 뽑힐 빈 을 지정. @Qualifier가 없으면 이게 선택된다.
@Resource(name="이름")Jakarta 표준 애너테이션. 이름 기준 으로 주입한다(@Autowired는 타입 기준이 먼저).
public interface PaymentGateway {}

@Component @Primary                          // 기본 후보
public class TossGateway implements PaymentGateway {}

@Component("kakao")
public class KakaoGateway implements PaymentGateway {}

@Service
@RequiredArgsConstructor
public class CheckoutService {
  private final PaymentGateway gateway;      // @Primary 덕에 TossGateway 주입

  // 특정 구현이 필요하면:
  // public CheckoutService(@Qualifier("kakao") PaymentGateway g) { ... }
}
여러 개·없을 수도 있는 빈 주입
  • 컬렉션 주입List<PaymentGateway> 로 받으면 해당 타입 빈을 전부 넣어준다.
  • Map 주입Map<String, PaymentGateway> 로 받으면 빈 이름 → 빈 으로 채워준다.
  • 없을 수도 있을 때Optional<Foo> 로 받거나 ObjectProvider<Foo> 로 받아 "있으면 꺼내 쓰는" 식으로 늦게(lazy) 다룬다.

6. 빈 스코프 — 빈은 몇 개나 만들어지나

스코프(scope) 는 "빈을 얼마나 만들어 얼마나 오래 쓸지"를 정합니다. 아무것도 지정하지 않으면 기본은 싱글톤 입니다 — 컨테이너 안에 딱 하나만 만들어 모두가 그 하나를 공유합니다.

스코프의미
singleton (기본)컨테이너당 하나의 인스턴스를 만들어 공유. 대부분의 빈이 여기 해당.
prototype요청(주입)할 때마다 매번 새로 만든다.
requestHTTP 요청 하나당 하나. (웹 환경에서만)
session사용자 세션 하나당 하나. (웹 환경에서만)
application서블릿 컨텍스트(앱 전체) 당 하나. (웹 환경에서만)
@Component
@Scope("prototype")        // 주입할 때마다 새 객체
public class Cart {}
싱글톤이라 "상태"를 조심

싱글톤 빈은 모든 요청이 같은 객체를 함께 씁니다. 그래서 빈 안에 요청마다 달라지는 값(상태)을 필드로 보관하면, 여러 요청이 서로 값을 덮어쓰는 사고가 납니다. 서비스 빈은 보통 상태 없이(stateless) 만드세요. request/session/application 스코프는 일반 웹 앱이 아니라 spring-boot-starter-web 같은 웹 환경에서만 동작합니다.

7. 빈 생명주기 콜백

빈은 "만들어진 직후"와 "사라지기 직전"에 할 일을 정해둘 수 있습니다 — 연결 풀 준비, 캐시 채우기, 리소스 정리 같은 것이죠. 방법은 세 가지이고, 표준 애너테이션 방식이 가장 권장됩니다.

방법초기화 / 소멸비고
애너테이션 (권장)@PostConstruct / @PreDestroy둘 다 jakarta.annotation 패키지. 간단하고 Spring에 덜 묶인다.
인터페이스 구현InitializingBean.afterPropertiesSet() / DisposableBean.destroy()Spring에 강하게 결합되어 비권장.
@Bean 속성@Bean(initMethod="...", destroyMethod="...")외부 라이브러리 객체의 init/cleanup 메서드를 지정할 때 유용.
@Component
public class CacheWarmer {

  @PostConstruct                 // jakarta.annotation.PostConstruct
  public void init() {
    // 주입이 모두 끝난 직후 1회 실행 — 캐시 미리 채우기 등
  }

  @PreDestroy                    // jakarta.annotation.PreDestroy
  public void cleanup() {
    // 앱 종료 직전 1회 실행 — 리소스 정리 등
  }
}

실행 순서는 초기화 때 @PostConstructafterPropertiesSet(), 소멸 때 @PreDestroydestroy() 입니다.

8. 그 밖에 알아둘 것

애너테이션용도
@Lazy앱 시작 때 미리 만들지 않고, 처음 필요할 때 만들도록 미룬다.
@DependsOn("이름")지정한 빈이 먼저 만들어진 뒤에 이 빈을 만들도록 순서를 강제한다.
@Conditional(...)특정 조건이 맞을 때만 빈을 등록한다(조건은 코드로 정의).
@ConditionalOnXxxSpring Boot 전용 단축형. 예: @ConditionalOnProperty(설정값이 있을 때), @ConditionalOnMissingBean(같은 빈이 없을 때)만 등록.
환경별 빈은 @Profile로

개발·운영처럼 환경에 따라 다른 빈을 쓰려면 @Profile 을 씁니다. 이 주제는 설정 페이지에서 다룹니다 → Spring 설정 · 프로파일.

9. 예제 — 레이어가 서로 주입되는 흐름

엔티티?리포지토리? → 서비스로 이어지는 전형적인 구성을, 전부 생성자 주입 으로 엮어 보겠습니다.

// (1) 엔티티 — DB 테이블과 매핑되는 객체 (빈 아님)
@Entity
public class User {
  @Id @GeneratedValue
  private Long id;
  private String name;
}

// (2) 리포지토리 — DB 접근 빈
public interface UserRepo extends JpaRepository<User, Long> {
  List<User> findByNameContaining(String keyword);
}

// (3) 서비스 — 비즈니스 로직 빈, 리포지토리를 주입받음
@Service
@RequiredArgsConstructor
public class UserService {
  private final UserRepo userRepo;          // 생성자로 주입

  @Transactional(readOnly = true)
  public List<User> search(String keyword) {
    return userRepo.findByNameContaining(keyword);
  }
}
객체 생성·연결의 주체

[개발자가 하던 일]                [Spring 컨테이너가 대신]
new UserRepo()          ──IoC──▶  UserRepo 빈 생성
new UserService(repo)             UserService 빈 생성
service.userRepo = repo           생성자로 repo 주입
                                  완성된 빈을 보관 → 필요할 때 제공

개발자는 new 를 한 번도 쓰지 않았습니다. 어떤 빈이 어떤 빈을 필요로 하는지(final UserRepo)만 선언하면, 컨테이너가 알아서 만들고 연결합니다. 이것이 IoC와 DI가 함께 만들어내는 결과입니다.

한눈에 — 애너테이션 카탈로그

애너테이션용도붙이는 위치
@Component일반 빈 등록클래스
@Service서비스 층 빈클래스
@RepositoryDB 접근 층 빈 (예외 번역)클래스
@Controller웹 MVC 컨트롤러 빈클래스
@RestControllerREST API 컨트롤러 빈 (JSON 응답)클래스
@Configuration설정 클래스 (@Bean 정의처)클래스
@ComponentScan스캔 시작 패키지 지정설정 클래스
@SpringBootApplication설정+자동설정+스캔 묶음진입점 클래스
@Bean메서드 반환 객체를 빈으로 등록@Configuration 내 메서드
@Autowired주입 지점 표시생성자·세터·필드
@Qualifier이름으로 빈 선택주입 지점·매개변수
@Primary기본 후보 빈 지정클래스·@Bean 메서드
@Resource이름 기준 주입 (Jakarta)필드·세터
@Scope빈 스코프 지정클래스·@Bean 메서드
@PostConstruct초기화 콜백 (jakarta)메서드
@PreDestroy소멸 콜백 (jakarta)메서드
@Lazy지연 생성클래스·주입 지점
@DependsOn생성 순서 강제클래스·@Bean 메서드
@Conditional조건부 빈 등록클래스·@Bean 메서드
@Profile환경(프로파일)별 빈클래스·@Bean 메서드
이 프로젝트와의 관계

이 프로젝트의 백엔드(com.example.study 패키지)는 모든 빈을 생성자 주입 으로 연결하며, Lombok @RequiredArgsConstructor 로 생성자를 자동 생성합니다. 포트는 8084에서 동작하고, 버전은 Spring Boot 3.3.1 / Java 17 / Jakarta EE 10 이라 모든 표준 애너테이션은 jakarta.* 를 import 합니다(javax.* 아님). 실제 코드로 더 자세히 보려면 → be-01 Spring 기초.

다음 단계