IoC와 DI, 그리고 Bean
IoC란
- IoC: Inversion of Control, 제어의 역전
public class OrderService {
private final OrderRepository orderRepository;
public OrderService() {
this.orderRepository = new MemoryOrderRepository();
}
}
- 스프링 컨테이너를 사용하지 않는 구조에서는 위와 같이 객체를 직접 생성하고 의존관계를 연결해야 함
- 스프링은 객체를 생성하고 관리하고 연결하는 작업을 개발자가 아닌 스프링 컨테이너가 담당
- 즉, 개발자는 객체를 어떻게 조립할지보다, 어떤 역할의 객체가 필요한지와 비즈니스 로직 자체에 더 집중할 수 있음
DI란
-
DI: Dependency Injection, 의존성 주입
-
객체가 필요한 의존성을 스스로 만들지 않고, 외부에서 주입받아 사용하도록 하는 방식
-
스프링에서는 이 주입 과정을 스프링 컨테이너가 처리
-
여기서 의존성이란 어떤 객체가 자신의 로직을 수행하기 위해 다른 객체를 필요로 하는 관계
public class OrderService {
private final OrderRepository orderRepository;
public OrderService() {
this.orderRepository = new MemoryOrderRepository();
}
}
-
위 코드에서
OrderService는OrderRepository에 의존함 -
OrderService가 생성자 안에서MemoryOrderRepository를 직접 생성하는데, 이 때문에OrderService는OrderRepository인터페이스뿐 아니라MemoryOrderRepository구현 클래스에도 의존함 -
만약 레포지토리 구현체를 바꾸려면
OrderService코드에서new MemoryOrderRepository()부분을 수정해야 함 -
테스트할 때도 mock 객체나 테스트용 구현체를 생성자 밖에서 전달할 수 없음
-
스프링 컨테이너는 객체들을 빈으로 등록하고, 각 빈이 필요로 하는 다른 빈을 찾아 연결함
스프링 컨테이너의 빈 생성과 주입 과정
- POJO 형태로 클래스 작성
- 빈 등록 대상과 의존 관계 정보를 스프링에게 전달
- 스프링 컨테이너가 이 정보를 바탕으로 빈을 생성
- 생성하는 과정에서 필요한 의존 관계를 주입
- 이후 생성된 빈은 컨테이너가 관리
DI 방식
설정 방식에 따른 DI
명시적 DI
- 명시적 DI: 개발자가 설정 클래스에서 빈 생성 및 의존 관계 주입을 직접 정의하는 방식
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository() {
return new MemoryOrderRepository();
}
@Bean
public OrderService orderService() {
return new OrderService(orderRepository());
}
}
-
@Configuration: 해당 클래스가 스프링 설정 클래스임을 표시할 때 사용 -
@Bean: 메서드가 반환하는 객체를 스프링 빈으로 등록 -
@Configuration클래스를 보면 어떤 객체를 빈으로 등록하는지, 어디에 주입하는지 파악 가능 -
코드를 수정할 수 없는 클래스(e.g. 외부 라이브러리)도 빈으로 등록 가능
-
단, 빈의 수가 증가할수록 관리 비용이 증가
@Configuration클래스 안에서@Bean메서드끼리 호출하면 스프링 컨테이너에 등록된 빈을 참조하여 싱글톤으로 유지함
@Configuration
public class AppConfig {
@Bean
public OrderRepository orderRepository() {
return new MemoryOrderRepository();
}
@Bean
public OrderService orderService() {
return new OrderService(orderRepository());
}
}
@Configuration클래스는 일반적으로 프록시를 통해@Bean메서드 호출을 관리함- 즉,
@Bean메서드가 호출되면 스프링이 중간에서 가로채서 이미 등록된 빈을 반환하도록 처리함 - 위 코드에서
orderRepository()를 호출해도 객체를 새로 생성하는 것이 아니라, 스프링 컨테이너에 등록된orderRepository빈을 반환함
묵시적 DI
-
묵시적 DI: 스프링이 컴포넌트 스캔으로 빈 등록 대상을 찾고, 각 빈의 주입 지점에 맞춰 의존 관계를 연결하는 방식
-
클래스에
@Component,@Service,@Repository,@Controller같은 어노테이션을 붙이면 스프링이 컴포넌트 스캔 범위 안에서 해당 클래스를 찾아 빈으로 등록@Bean으로도 물론 등록할 수는 있으나 일반적인 방법은 아님
-
설정 클래스의 복잡성을 줄일 수 있으나, 전체 의존 관계는 명시적 DI보다 한눈에 파악하기 어려움
- 일반적으로 애플리케이션 내부의 서비스, 레포지토리, 컨트롤러는 묵시적 DI를 사용하고, 외부 라이브러리 객체나 생성 과정을 직접 제어해야 하는 객체는 명시적 DI를 사용함
생성자 주입
@Component
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
- 생성자 주입: 객체를 생성할 때 생성자를 통해 의존성을 주입받는 방식
- 어떤 의존성이 필요한지 코드를 통해 바로 알 수 있음
- 필드를
final로 둘 수 있어, 객체가 만들어진 뒤 의존성이 바뀌지 않도록 할 수 있음 - 서로가 서로를 의존하는 구조가 있으면 스프링 컨테이너가 빈을 생성하는 단계에서 순환 참조 문제를 발견할 수 있음
- 다른 방식은 빈 생성 후 주입 과정에서 발견하게 됨
세터 주입
@Component
public class OrderService {
private OrderRepository orderRepository;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
-
세터 주입: setter 메서드를 통해 의존성을 주입하는 방식
-
먼저 객체를 생성한 뒤 필요한 의존성을 주입함
-
선택적으로 의존성을 주입해도 되는 경우에 사용함
-
단, 객체가 생성되는 시점에 필요한 의존성이 모두 주입된다고 보장하기는 어려움
-
세터 호출을 하지 않으면 의존성이 주입되지 않은 상태로 객체가 생성될 수 있음
필드 주입
@Component
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}
-
필드 주입: 필드에 직접 의존성을 주입하는 방식
-
어떤 의존성이 필요한지 파악하기 어렵고, 스프링 컨테이너 없이 객체를 직접 생성해 테스트하기 어렵기에 실무에서는 보통 권장하지 않음
세터 주입과 필드 주입은 객체가 생성된 뒤 의존성을 주입하는 방식 자바의
final필드는 객체 생성이 끝나기 전에 초기화되어야 하므로, 세터 주입이나 필드 주입에서는 의존성 필드를final로 둘 수 없음
@Autowired란
-
@Autowired: 스프링 컨테이너에 등록된 빈을 생성자, 메서드, 필드에 주입할 때 사용하는 어노테이션 -
스프링은 주입 대상에 선언된 타입을 기준으로 빈을 찾음
-
같은 타입의 빈이 여러 개라면
@Primary,@Qualifier로 특정 빈을 선택
@Component
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
- 빈 클래스에 생성자가 하나만 있으면
@Autowired를 생략해도 스프링이 해당 생성자로 의존성을 주입
@Component
public class OrderService {
private final OrderRepository orderRepository;
private final DiscountPolicy discountPolicy;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
this.discountPolicy = null;
}
@Autowired
public OrderService(
OrderRepository orderRepository,
DiscountPolicy discountPolicy
) {
this.orderRepository = orderRepository;
this.discountPolicy = discountPolicy;
}
}
- 생성자가 여러 개라면 어떤 생성자를 사용할지
@Autowired로 지정해야 함
@Primary
@Primary: 같은 타입의 빈이 여러 개일 때 기본으로 주입할 빈을 지정하는 어노테이션
@Primary
@Component
public class MemoryOrderRepository implements OrderRepository { ... }
@Component
public class JdbcOrderRepository implements OrderRepository { ... }
@Component
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
OrderRepository타입 빈이 두 개지만,@Primary가 붙은MemoryOrderRepository가 주입됨- 대부분의 주입 지점에서 같은 구현체를 쓸 때 적합
@Qualifier
@Qualifier: 주입 지점에서 빈 이름을 명시해 특정 빈을 선택하는 어노테이션- 빈을 등록하는 쪽과 주입받는 쪽 모두에
@Qualifier를 선언
@Component
@Qualifier("memoryRepo")
public class MemoryOrderRepository implements OrderRepository { ... }
@Component
@Qualifier("jdbcRepo")
public class JdbcOrderRepository implements OrderRepository { ... }
@Component
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(
@Qualifier("jdbcRepo") OrderRepository orderRepository
) {
this.orderRepository = orderRepository;
}
}
@Qualifier("jdbcRepo")로 어떤 빈을 주입할지 명시적으로 지정@Primary보다 우선순위가 높아, 두 어노테이션이 동시에 적용되면@Qualifier가 선택됨- 주입 시점마다 어떤 구현체를 쓸지 직접 지정해야 할 때 사용
댓글