의존성 주입(DI)과 스프링 컨테이너
생성자 주입으로 DI를 이해하고, 스프링 빈 등록·조회 방법과 컨테이너가 빈을 싱글톤으로 관리하는 이유를 정리한다.
시리즈 · Spring2 / 14
- SOLID 원칙과 OCP·DIP를 지키는 방법
- 의존성 주입(DI)과 스프링 컨테이너
- 쓰레드 풀과 싱글톤 서블릿의 동시성 문제
- FrontController 패턴과 핸들러 어댑터로 스프링 MVC 구조 이해하기
- @ModelAttribute와 PRG 패턴으로 폼 요청 처리하기
- System.out 대신 SLF4J로 로그 남기기
- 세션 직접 만들어 보기와 HttpSession
- 스프링 인터셉터와 예외 발생 시 오류 페이지 흐름
- 서블릿 Part와 MultipartFile로 파일 업로드 처리하기
- JDBC로 CRUD 구현하기와 커넥션 풀, DataSource
- JDBC 트랜잭션에서 스프링 트랜잭션 동기화와 트랜잭션 AOP까지
- JPA 변경 감지(dirty checking)와 병합(merge)
- 서블릿 컨테이너 초기화와 스프링 컨테이너 등록 과정
- 싱글톤 빈의 동시성 문제와 ThreadLocal
의존성 주입이란
의존성 주입(Dependency Injection, DI)은 객체가 필요한 의존 객체를 스스로 만들지 않고 외부에서 주입받는 설계 패턴이다. 필드 주입, setter 주입, 생성자 주입 세 가지 방법이 있는데, 의존관계가 실행 중에 동적으로 바뀌는 경우는 거의 없으므로 생성자 주입만 정리한다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
이렇게 작성하면 MemberService는 인터페이스(MemberRepository)뿐 아니라 구현 클래스(MemoryMemberRepository)에도 의존하게 된다. 테스트를 작성할 때도 문제가 드러난다.
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
}
MemberService 내부에서 생성한 리포지토리와 테스트에서 생성한 리포지토리는 서로 다른 객체다. 같은 객체를 쓰려면 MemberService가 생성자로 리포지토리를 받도록 바꾸고, 테스트에서 넣어 주면 된다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
}
이렇게 외부에서 객체를 넣어 주는 것이 의존성 주입이다.
스프링 빈 등록
스프링은 애플리케이션이 시작될 때 객체를 스프링 컨테이너에 등록해 관리한다. 등록하는 방법은 두 가지다.
컴포넌트 스캔
@Component가 붙은 클래스의 객체를 만들어 컨테이너에 등록한다. @Service, @Controller의 선언부를 열어 보면 @Component가 붙어 있어 이들도 스캔 대상이 된다. 필요한 의존 객체는 @Autowired로 연결한다. 컨테이너에 등록된 객체끼리 이어 준다고 생각하면 된다.
자바 코드로 직접 등록
설정 클래스를 만들고 @Bean 메서드로 등록한다. 의존관계는 생성자에 직접 넣어 준다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
빈 조회
AnnotationConfigApplicationContext로 컨테이너를 만들고 getBean()으로 빈을 꺼낼 수 있다.
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
타입으로 조회할 때 같은 타입의 빈이 둘 이상이면 NoUniqueBeanDefinitionException이 발생한다. 이때는 빈 이름을 함께 지정한다.
ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
부모 타입으로 조회하면 자식 타입의 빈도 함께 조회된다.
ApplicationContext는 BeanFactory의 기능을 모두 상속받고 여러 부가 기능을 더한 것이다. BeanFactory를 직접 사용하는 일은 거의 없다.
싱글톤과 스프링 컨테이너
스프링 없이 순수한 자바 설정 클래스로 객체를 관리하면 요청할 때마다 객체가 새로 생성된다. 웹 애플리케이션은 많은 요청을 동시에 처리하므로 요청마다 객체를 만드는 것은 메모리 낭비가 심하다.

이 문제는 클래스의 인스턴스가 하나만 생성되도록 보장하는 싱글톤 패턴으로 해결할 수 있다.
public class SingletonService {
private static final SingletonService instance = new SingletonService();
public static SingletonService getInstance() {
return instance;
}
// private 생성자로 외부에서 객체를 생성하는 것을 막는다.
private SingletonService() {
}
}
다만 싱글톤 패턴을 직접 구현하면 단점이 있다.
- 클래스마다 위와 같은 코드를 작성해야 한다.
- private 생성자 때문에 자식 클래스를 만들기 어렵다.
- 결과적으로 유연성이 떨어진다.
스프링 컨테이너는 이런 단점 없이 객체를 싱글톤으로 관리해 준다. 싱글톤 패턴 코드를 작성하지 않아도 컨테이너에 등록된 빈은 하나의 인스턴스로 공유된다.

참고
- 김영한, 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (인프런)
- 김영한, 스프링 핵심 원리 - 기본편 (인프런)