[Spring] 스프링 컨테이너(IoC, DI 컨테이너)Back-End/Spring2025. 4. 2. 17:44@seungwook_TIL
Table of Contents
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
SRP (Single Responsibility Principle) : 하나의 클래스는 오직 하나의 책임만 가져야 한다.
OCP (Open/Closed Principle) : 소프트웨어 요소는 확장에는 열려 있고, 변경에는 닫혀 있어야 한다.
LSP (Liskov Substitution Principle) : 자식 클래스는 부모 클래스의 기능을 대체할 수 있어야 한다.
ISP (Interface Segregation Principle) : 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
DIP (Dependency Inversion Principle) : 구체화가 아닌 추상화에 의존해야 한다.
OCP, DIP를 지키지 않는 구조
문제 상황
DiscountPolicy 인터페이스의 구현체를 FixDiscountPolicy에서 RateDiscountPolicy 로 변경한다고 가정하자
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
OrderServiceImpl 클래스에서 할인 정책을 RateDiscountPolicy로 변경하는 코드를 수정했다.
이는 OCP 위반이다.
기능을 확장하면, 클라이언트 코드(OrderServiceImpl)에 영향을 준다.
FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스코드도 함께 변경해야 한다
또한 DIP 위반이다.
OrderServiceImpl은 인터페이스(DiscountPolicy)에 의존하는 것 뿐만 아니라 구체적인 클래스(RateDiscountPolicy)에도 의존하고 있다.
이를 그림으로 나타내면 아래와 같다.
해결 방법
@Component
public class OrderServiceImpl implements OrderService{
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
DiscountPolicy 인터페이스의 특정 구현체를 OrderServiceImpl가 알 수 있도록 하는 것이 아닌 오직 인터페이스에만 의존하도록 한다.
@Component는 스프링이 관리하는 자바 빈임을 인식(클래스 레벨에 사용)
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 작성한다.
@Configuration
public class AppConfig {
@Bean
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
@Configuration : 스프링이 설정을 하는 클래스임을 인식
@Bean : 스프링이 관리하는 자바 빈임을 인식(메서드 레벨에 사용)
스프링이 관리하는 모든 Bean은 싱글톤임이 보장된다.
스프링이 관리하는 빈은 모두 싱글톤임이 보장된다. - @Configuration이 붙은 클래스의 @Bean 메서드를 통해 등록된 빈은 스프링이 싱글톤으로 보장해준다.
이는 CGLIB(AOP) 때문이다.
CGLIB는 AppConfig를 상속한 객체를 스프링 빈에 등록되도록 되어있다.
상속한 객체에는 아마 아래와 같은 코드가 있을 것이다.
@Bean
public DiscountPolicy discountPolicy() {
if (discountPolicy가 이미 스프링 컨테이너에 등록되어 있으면?) {
return 스프링 컨테이너에서 discountPolicy를 찾아서 반환;
} else { // 스프링 컨테이너에 등록되지 않았다면
기존 AppConfig.discountPolicy() 로직을 호출해서 RateDiscountPolicy 객체 생성;
스프링 컨테이너에 discountPolicy로 등록;
return 반환;
}
}
- @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. - 덕분에 싱글톤이 보장되는 것이다.
@Component, @Service, @Repository, @Controller 가 붙은 클래스도 @ComponentScan에 의해 스프링 컨테이너에 등록되어 싱글톤이 보장된다. - 이들은 @ComponentScan에 의해 객체가 딱 한 번 생성되고 스프링 컨테이너에 등록된다. - 스프링 컨테이너는 기본 스코프가 singleton이기 때문에 한 번만 만든다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
OrderServiceImpl
RateDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
OrderServiceImpl ← MemoryMemberRepository, 또는 FixDiscountPolicy
정리하면
AppConfig가 DiscountPolicy 를 구현하는 객체를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다.
AppConfig가 의존관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨.
따라서 OCP와 DIP 문제가 모두 해결되었다.
IoC, DI
제어의 역전 IoC(Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.
반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.
프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있다. 그런 사실도 모른채 OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐이다.
이렇듯 프로그램의 제어 흐름을 개발자가 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.
IoC란 프로그램의 제어 흐름을 개발자가 아닌 프레임워크(또는 외부)가 담당하는 것
의존관계 주입 DI(Dependency Injection)
OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
의존관계는 AppConfig가 주입해줄 뿐이다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
실무에서의 사용
이와 같은 구조에서 인터페이스를 구현하는 구현체가 많아지면 아래와 같은 AppConfig에 일일이 모두 적어줘야 한다.
@Configuration
public class AppConfig {
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
누락할 위험도 있고, 무엇보다 귀찮다.
그렇기 때문에 AppConfig 클래스를 삭제하고, 컨트롤러 계층에는 @Controller, 서비스 계층에는 @Service, 리포지토리 계층에는 @Repository를 붙히면 된다.
@Controller, @Service,@Repository 애노테이션 안에는 @Component가 들어있다.
구현체가 특정 계층에 해당하지 않거나 인터페이스의 구현 객체에는 @Component 애노테이션을 붙히면된다.
왜냐하면@SpringBootApplication에 @ComponentScan 애노테이션이 있기 때문에 @Component 가 붙은 클래스를 스프링 컨테이너에 빈으로 등록하고 관리한다.
Spring Data JPA를 사용한다면 @Repository를 붙히지 않아도 된다. Spring Data JPA에서 JpaRepository를 상속하면, Spring이 자동으로 프록시 객체를 만들어서 빈으로 등록해주기 때문에 @Repository 어노테이션을 생략해도 잘 동작한다.
그렇기 때문에 OrderServiceImpl은 아래와 같이 작성하면 된다.
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
여기에서는 생성자 주입 방식을 사용하고 있다.
일반적으로 생성자 주입을 하려면 @Autowired 어노테이션을 생성자에 직접 붙여줘야 하지만,롬복의 @RequiredArgsConstructor 어노테이션을 사용하면 이 과정을 생략할 수 있다.
@RequiredArgsConstructor는 final이 붙은 필드들을 매개변수로 갖는 생성자를 자동으로 생성해주며,스프링에서는 이 생성자에 자동으로 @Autowired가 적용된다.
따라서 생성자도 코드에 따로 작성할 필요가 없고, @Autowired도 생략 가능하여 코드가 간결해지고 유지보수도 편해진다.
하지만 여기서 오류가 발생한다.
스프링 컨테이너 입장에서는 FixDiscountPolicy, RateDiscountPolicy 중 어느것을 DiscountPolicy의 구현체로 주입할지 모르기 때문이다.
가장 간단한 방법은 사용하지 않는 구현체의 @Component 애노테이션을 제거하면 된다.
그렇게 하면 스프링 컨테이너는 해당 클래스를 빈으로 등록하지 않기 때문이다.
// @Component("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
...
}
@Component("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
...
}