Spring · IoC와 의존성 주입
Spring의 핵심은 한 문장으로 줄일 수 있습니다 — "객체를 만들고 연결하는 일을 개발자가 아니라 Spring이 대신한다." 이것이 IoC(제어의 역전) 이고, 그 구체적인 방법이 DI(의존성 주입)? 입니다. 이 페이지에서는 빈을 등록하고 주입받는 모든 방법을 차근차근 풀어봅니다.
1. IoC — 제어의 역전
보통 프로그램은 내가 필요한 객체를 직접 만듭니다.
예를 들어 서비스 안에서 new UserRepo() 라고 적어 리포지토리를 직접 생성하죠.
IoC(Inversion of Control, 제어의 역전) 는 이 "객체를 만들고 서로 연결하는 권한"을
개발자에게서 빼앗아 Spring? 컨테이너에게 넘기는 것입니다.
누가 객체를 만들지 그 제어권이 나 → 프레임워크로 뒤집혔다(역전) 고 해서 이런 이름이 붙었어요.
- 코드가 느슨해진다 — 객체끼리 "누가 누구를 직접 만드는지" 몰라도 되니 서로 덜 얽힌다
- 바꿔 끼우기 쉽다 — 같은 자리에 다른 구현을 넣어도 사용하는 쪽 코드는 그대로
- 테스트가 편하다 — 진짜 객체 대신 가짜(mock) 객체를 넣어 시험할 수 있다
2. 빈과 컨테이너
Spring 컨테이너가 만들고 관리하는 객체를 빈(Bean) 이라고 부릅니다.
일반적인 객체와 다를 것은 없지만, "내가 new 로 만든 게 아니라 Spring이 만들어 보관 중인 객체"라는 점이 다릅니다.
이 빈들을 담아두고 필요할 때 꺼내주는 보관소가 컨테이너 입니다.
| 이름 | 설명 |
|---|---|
| BeanFactory | 빈을 만들고 보관하는 가장 기본적인 컨테이너 인터페이스. 최소 기능. |
| ApplicationContext | BeanFactory를 확장한 실제 사용 컨테이너. 자동 설정·이벤트·국제화 등 추가 기능 포함. 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 은 @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의 @RequiredArgsConstructor 가 final 필드용 생성자를 자동으로 만들어 주므로 코드가 깔끔합니다.
@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 | 요청(주입)할 때마다 매번 새로 만든다. |
| request | HTTP 요청 하나당 하나. (웹 환경에서만) |
| 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회 실행 — 리소스 정리 등
}
}실행 순서는 초기화 때 @PostConstruct → afterPropertiesSet(), 소멸 때 @PreDestroy → destroy() 입니다.
8. 그 밖에 알아둘 것
| 애너테이션 | 용도 |
|---|---|
| @Lazy | 앱 시작 때 미리 만들지 않고, 처음 필요할 때 만들도록 미룬다. |
| @DependsOn("이름") | 지정한 빈이 먼저 만들어진 뒤에 이 빈을 만들도록 순서를 강제한다. |
| @Conditional(...) | 특정 조건이 맞을 때만 빈을 등록한다(조건은 코드로 정의). |
| @ConditionalOnXxx | Spring Boot 전용 단축형. 예: @ConditionalOnProperty(설정값이 있을 때), @ConditionalOnMissingBean(같은 빈이 없을 때)만 등록. |
개발·운영처럼 환경에 따라 다른 빈을 쓰려면 @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 | 서비스 층 빈 | 클래스 |
@Repository | DB 접근 층 빈 (예외 번역) | 클래스 |
@Controller | 웹 MVC 컨트롤러 빈 | 클래스 |
@RestController | REST 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 기초.
다음 단계
- 이 빈들로 HTTP API를 만드는 방법 → Spring 웹 · REST
- 리포지토리·엔티티로 DB를 다루는 방법 → JPA
- 실제 코드로 배우는 백엔드 → be-01 Spring 기초