의존성이란?
한 클래스가 다른 클래스(혹은 객체)를 사용하는 관계,
쉽게 생각해서 의존성은 "내가 뭔가를 쓴다?" 그러면 의존하고 있는 것이다.
public class Car { private Engine engine = new Engine(); // Car는 Engine에 의존 public void start() { engine.run(); // Engine 없으면 Car 못 씀 } }
코드를 보면, Car클래스는 Engine 객체에 의존하고 있다.
ex)
스마트폰은 배터리에 의존 (배터리 없으면 못씀)
자동차는 연료에 의존 (연료 없으면 못 움직임)
커피숍은 커피머신에 의존 (커피머신 없으면 커피를 못 만듦)
내가 뭔가를 사용하고 있다 >> 내가 어떠한 개념에 의존하고 있다.
"IoC (Inversion Of Control) 제어의 역전"
제어의 역전은 말 그대로 제어의 흐름이 바뀌는 것을 의미,
전통적인 프로그래밍 방식에서는 개발자가 코드 내에서 사용할 객체를 직접 생성하고 관리했다.
예시 코드
public class Car {
private Engine engine = new Engine(); // Car는 Engine에 의존
public void start() {
engine.run(); // Engine 없으면 Car 못 씀
}
}
예를 들어 A클래스가 B클래스의 기능을 사용하려면, A클래스 내부에서 B클래스의 인스턴스를 직접 생성해야했다.
위에 예시코드를 보면, Car클래스에서 Engine이라는 기능을 사용하기 위해 new키워드를 사용하여 엔진 객체를 직접 생성했다.
하지만, 스프링에서는 객체 생성과 관리를 개발자가 하지 않고, 스프링이 직접 해준다.
한마디로 개발자가 new키워드를 사용하여 객체를 직접 생성하지 않아도 된다.
원래는 개발자가 객체 생성을 제어해야하지만, 스프링이 대신 해주므로 제어의 역전!
"DI(Dependency Injection)" 의존성 주입
DI는 Ioc 개념을 실제로 구현하는 대표적인 방법, IoC와 DI는 뗄레야 뗄 수 없는 관계

스프링이 우리 대신 객체를 생성해서 관리를 해주고 있는데, 이것을 우리 코드로 가져오는것을 DI라고 한다.
원래는 우리가 new키워드를 사용하여 객체를 생성해줘야 했다. 하지만 이것을 이제 스프링이 해주고, 우리는 스프링이 만들어 놓은 객체를 가져와서 사용하기만 하면 된다.
그러면, 스프링 입장에서는 우리 코드에 의존성을 꽂아주고 있다. 주입하고 있다.
스프링이 개발자 대신 생성한 객체를 관리하고 있는데, 그 객체를 가져와서 주입해준다.
"Bean"
IoC는 스프링이 개발자 대신해서 객체를 생성하고 관리하는 개념이다.
이 때 스프링이 관리하는 객체를 "Bean"이라고 하고, 이 Bean을 관리하는 어떤 상자가 IoC Container이다.
한마디로, 스프링이 관리하는 객체가 Bean인데, 이것을 IoC Container에서 보관하며 관리한다.
1. Spring 컨테이너에 의해 생성, 관리, 소멸된다.
2. 애플리케이션 전역에서 재사용 가능하다.
3. 기본적으로 "싱글톤" 스코프로 관리된다.
싱글톤이란?
- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴, 애플리케이션 전체에 해당 클래스의 객체를 하나만 만들고, 그것을 공유해서 사용한다.
- 싱글톤을 사용하는 이유
- 메모리 효율성 : 최초 한 번만 객체를 생성하므로 메모리 낭비를 방지할 수 있다.
- 데이터 공유와 일관성 : 시스템 전반의 설정 정보나 공통 자원을 관리할 때 데이터의 일관성을 유지하기 쉽다.
스프링 IoC Container
- Bean의 생성 및 생명주기 관리
- 의존성 주입(DI)
- Bean 설정 정보 관리
- Bean 간의 의존 관계 설정

위에서 봤던 초록색 상자의 정체는 IoC Container이고, 이 상자 안에 들어있는 객체의 정체는 Bean이다.
IoC라는 것은 스프링이 개발자 대신 객체를 생성하여 관리하는데, 관리하는 객체의 정체는 Bean이고,
이 Bean객체들을 IoC Container 안에 담아서 관리한다.
IoC/DI를 사용하는 경우와 new를 사용하는 경우
- IoC/DI를 사용해야 하는 경우
- 재사용되거나 교체 가능한 비즈니스 로직/인프라의 경우 IoC/DI를 사용한다.
- 한마디로 @Sevice, @Repository, @Component, @Controller의 경우 사용
예시 코드
// ✅ Spring Bean으로 관리
@Service
public class UserService { } // 비즈니스 로직
@Repository
public class UserRepository { } // 데이터 접근
@Component
public class EmailSender { } // 인프라
@Controller
public class UserController { } // 컨트롤러
- new 키워드를 사용해야 하는 경우
- 매번 새로운 상태를 가지는 데이터 객체의 경우 new키워드를 사용하여 객체를 직접 생성한다.
- 한마디로 @Sevice, @Repository, @Component, @Controller가 아닌 경우 new키워드를 사용
예시 코드
// ❌ Bean으로 만들면 안 됨!
public class User {
private String name;
private int age;
}
public class OrderRequest {
private Long userId;
private List<Item> items;
}
public class SearchCondition {
private String keyword;
private LocalDate startDate;
}
스프링 Bean 등록하는 방법
1. Bean 자동 등록
@Component 어노테이션을 활용한다.
Spring이 @Component 어노테이션이 붙은 클래스를 찾아서 Bean으로 자동 등록하는 방식
@ComponentScan에 설정되어 있는 패키지를 기준으로 하위의 모든 @Component 클래스를 탐색하여 Bean으로 자동 등록하는 원리이다.
- @Component
- @Component가 붙은 클래스를 Bean으로 자동 등록
- @Component 어노테이션과 관련 합성 어노테이션으로 총 4가지가 있다.
- @Component
- @Controller (@RestController)
- @Service
- @Repository
- @ComponentScan
- @ComponentScan은 이 컨테이너에게 어디에서 빈으로 사용할 클래스들을 찾아야 할지 알려주는 안내자 역할을 한다. 지정된 경로와 그 하위 패키지를 모두 탐색한다.
- 하지만, 이 어노테이션은 우리가 사용한 적은 없지만 이때 까지 IoC/DI가 잘 작동을 했었다, 그 이유는 메인 메서드에 있는 @SpringBootApplication 어노테이션에 이미 포함되어있기 때문
@SpringBootApplication
public class Schedule2Application {
public static void main(String[] args) {
SpringApplication.run(Schedule2Application.class, args);
}
}
2. Bean 수동 등록
Spring의 @Configuraion 어노테이션이 붙은 클래스와 @Bean 메서드에 붙는 어노테이션을 사용하여 명시적으로 Bean을 등록하는 방식, @Configuration 클래스에 정의된 @Bean 메서드의 반환 객체가 Spring IoC 컨테이너에 Bean으로 등록된다.
- @Configuration이 붙은 클래스는 설정 클래스로 인식된다.
- @Configuration 클래스 내부의 @Bean 메서드들이 Bean 정의를 포함하고 있다.
- @Bean 어노테이션은 메서드 레벨에서 사용되며, 해당 메서드가 반환하는 객체를 Spring 컨테이너에 Bean으로 등록한다.
- Bean의 이름은 기본적으로 메서드명이 되며, @Bean(name = "customName")으로 Bean의 이름을 커스텀 할 수 있다.
예시 코드
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Configuration 어노테이션은 클래스에 붙고, @Bean 어노테이션은 메서드에 붙는다.
이렇게 되면 해당 클래스가 설정 클래스로 인식이 되고, @Bean 어노테이션이 붙은 메서드가 반환하는 객체가 Spring IoC컨테이너에 Bean으로 등록이 된다. 예시 코드의 경우 리턴되는 new MemoryMemberRepository( );가 Bean으로 등록되는 객체가 된다.
기본적으로 자동 등록이 우선이며, 불가피한 경우에만 수동 등록을 활용한다.
비율적으로보면 @Component, @Controller(@RestController), @Service, @Repository를 활용한 Bean 자동 등록이 사용 빈도가 높고, 수동 등록 같은 경우에는 보통 외부 의존성(라이브러리)를 Bean으로 등록하고 싶을 때 많이 활용한다.
왜냐하면 우리가 외부 라이브러리의 코드를 직접 수정할 수 없기에 @Component(혹은 관련 어노테이션)를 붙일 수 없기 때문이다.
"DI (Dependency Injection) 방식 비교"
Spring에서 의존성 주입은 "생성자 주입", "세터 주입", "필드 주입"방식으로 구현이되는데,
필드 주입과 세터 주입의 경우 필드에 final 키워드를 사용할 수 없다. 그러다 보니, 변수에 할당된 객체를 null 또는 다른 객체로 바꿀 수 있는 위험이 있어서 안전하지 않기 때문에 사용하지 않는다.
반면, 생성자 주입은 적극 권장받는 방식이다. (우리가 지금까지 사용해오던 방식)
그리고 원래는 @Autowired 어노테이션을 필드에 직접 선언해서 의존성을 주입 받아야 하는데,
생성자가 1개만 있으면 생략이 가능하다. 그래서 우리는 지금까지 @Autowired 어노테이션을 사용하지 않았던 것이다.
예시 코드
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
final 선언으로 런타임에 의존성이 변경될 위험이 없어서 안전하다.
이를 "불변성(Immutability)" 보장이라고 한다.