DI

박윤택·2022년 6월 20일
1

Spring

목록 보기
4/18
post-thumbnail

🎁 스프링 컨테이너

개념

다른 말로 스프링 IoC 컨테이너 또는 IoC 컨테이너, DI 컨테이너라도도 부른다. 스프링 프레임워크의 핵심 컴포넌트로 애플리케이션 빈의 생명주기를 관리한다.
여기서 말하는 빈이란 스프링 컨테이너가 관리하는 객체를 의미한다.

여기서 말하는 컨테이너란 무엇인가?

컨테이너의 의미

  • 소프트웨어 개발 용어의 관점에서 컨테이너란 내부에 또 다른 컴포넌트를 가지고 있는 어떤 컴포넌트를 의미
  • 컨테이너는 먼저 객체를 생성하고 객체를 서로 연결
  • 객체를 설정하는 단계를 지나 마지막으로 생명 주기 전반을 관리
  • 객체의 의존성을 확인해 생성한 후 적절한 객체에 의존성을 주입

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스이다.
  • BeanFactory는 빈을 관리하는 역할을 한다.
  • @Bean 어노테이션이 붙은 메서드 명을 스프링 빈의 이름으로 사용한다.

ApplicationContext


Spring에서 스프링 컨테이너에 해당하는 코드부분이 ApplicationContext interface이다. 여러 interface들을 상속받고 있다. 단순히 빈의 생명주기만 관리하는 것이 아니라 다른 기능들도 담당하고 있다.

  • MessageSource: 메세지 다국화를 위한 인터페이스
  • EnvironmentCapable: 개발, 운영 등 환경변수 등으로 나눠 처리하고, 애플리케이션 구동 시 필요한 정보들을 관리하기 위한 인터페이스
  • ApplicationEventPublisher: 이벤트 관련 기능을 제공하는 인터페이스
  • ResourceLoader: 파일, 클래스 패스, 외부 등 리소스를 편리하게 조회


객체는 구성 메타데이터와 결합되어 ApplicationContext(Spring Container)에 빈 등록을 한 후 사용한다.


스프링 컨테이너 사용 이유

객체 간의 의존성을 낮추기 위해 Spring 컨테이너를 사용한다. DI와 관련이 있다.


⚫ 빈(Bean)

정의

스프링 컨테이너에 의해 관리되는 자바 객체를 의미한다.

Java Code로 빈 등록

  • @Configuration : 구성정보를 담당하는 것을 설정할때 사용
  • @Bean : 메서드에 @Bean 어노테이션을 붙이면 스프링 컨테이너에 자동으로 등록

호출

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
CatStoreService service = context.getBean("myMarket", myMarketService.class);

// use configured instance
List<String> userList = service.getUsernameList();

getBean() 메서드를 호출하면 해당 빈의 부모 빈까지 모두 확인할 수 있다.


BeanDefinition

Spring은 빈 등록을 XML 또는 JAVA Code로 할 수 있다. 이렇게 다양한 설정형식은 BeanDefinition이라는 추상화 덕분에 지원할 수 있다.

  • 속성에 따라 컨테이너가 Bean을 어떻게 생성하고 관리할지 결정
  • @Bean or <bean> 당 각 1개씩 메타 정보가 생성
  • Spring이 설정 메타정보를 BeanDefinition 인터페이스를 통해 관리하기 때문에 컨테이너 설정을 XML, Java로 할 수 있는 것이다.
  • 스프링 컨테이너는 설정 형식이 XML인지 Java 코드인지 모르고 BeanDefinition만 알면 된다.

Bean Scope

bean definition을 만들 때 해당 bean definition에 의해 정의된 클래스의 실제 인스턴스를 만들기 위한 레시피를 만든다. 즉 빈이 존재할 수 있는 범위를 의미한다.

특정 bean 정의에서 생성된 개체에 연결할 다양한 의존성 및 구성 값뿐만 아니라 특정 bean 정의에서 생성된 개체의 범위도 제어할 수 있다.

ScopeDescription
singleton(Default) 각 Spring 컨테이너에 대한 단일 객체 인스턴스에 대한 단일 bean definition의 범위를 지정
prototype스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
request웹 요청이 들어오고 나갈때까지 유지되는 스코프
session웹 세션이 생성되고 종료될 때까지 유지되는 스코프
application웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
websocket단일 bean definition 범위를 WebSocket의 라이프사이클까지 확장, Spring ApplicationContext의 컨텍스트에서만 유효

싱글톤(Singleton) 스코프

클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될때까지 유지된다.

  • Singleton Code Example
public class SingletonService {
  private static final SingletonService instance = new SingletonService();

  public static SingletonService getInstance() {
    return instance;
  }

  private SingletonService() {}

  public void logic() {
    System.out.println("싱글톤 객체를 호출");
  }
}

static, final 키워드를 이용해 공용, 값 변경이 안되게 설정하여 getInstance() 메서드를 이용하여 instance를 가지고 오게 한다.

스프링 컨테이너의 기본값은 싱글톤이다.

일반적인 싱글톤 패턴의 문제점은 다음과 같다.

싱글톤 패턴의 문제점

  • 싱글톤 패턴을 구현하는 코드가 많다.
  • 의존관계상 클라이언트가 구체 클래스에 의존
  • private 생성자를 사용하여 자식 클래스를 만들기 어렵기 때문에 유연성 떨어짐
  • Application 초기 구동 시 인스턴스 생성하기 때문에 싱글톤 빈이 많을수록 구동 시간이 증가

하지만 스프링 컨테이너는 이러한 싱글톤 패턴의 문제점들을 해결해준다. 스프링 컨테이너는 싱글톤 컨테이너의 역할을 수행한다.

import com.example.cmarket.Order.OrderService;
import com.example.cmarket.Order.OrderServiceImpl;
import com.example.cmarket.discount.CurrentDiscountInfo;
import com.example.cmarket.discount.DiscountInfo;
import com.example.cmarket.user.UserRepository;
import com.example.cmarket.user.UserRepositoryImpl;
import com.example.cmarket.user.UserService;
import com.example.cmarket.user.UserServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserServiceImpl(userRepository());
    }
    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(userRepository(), discountInfo());
    }
    @Bean
    public DiscountInfo discountInfo() {
        return new CurrentDiscountInfo();
    }
}

@Configuration@Bean 어노테이션을 추가@Bean 어노테이션을 통해 스프링 컨테이너에 등록하여 싱글톤 방식으로 사용한다.

여기서 주의할 점이 몇가지 있다.

  • 싱글톤 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 무상태로 설계해야한다.
    • 읽기만 가능해야한다.
    • 스프링 빈의 공유값을 설정하면 장애가 발생할 수 있다.
    • 특정 클라이언트가 값을 변경할 수 있으면 안된다.

Bean 설정

@Bean메서드-레벨 어노테이션이다. 해당 객체가 bean definitions의 소스임을 나타내는 @Configuration이 지정된 클래스 내부에서 메서드 위에 사용해야한다.


@Bean을 정의한 코드의 일부이다. @Target 어노테이션을 보면 메서드를 타겟으로 지정하고 있음을 확인할 수 있다.


🎛️ Spring DI

앞서 포스팅한 DI 유형을 조금 더 자세히 살펴보려고 한다.

1. 생성자 주입

  • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장
  • 불변과 필수 의존 관계에 사용
  • 생성자가 1개만 존재하는 경우에는 @Autowired를 생략해도 자동 주입 됨
  • NullPointerException을 방지가능
  • 주입받을 필드를 final로 선언 가능
@Component
public class OrderServiceImpl implements OrderService {
	private final UserRepository userRepository;
  	private final DiscountInfo discountInfo;

	@Autowired
	public OrderServiceImpl(UserRepository userRepository, DiscountInfo discountInfo) {
    this.userRepository = userRepository;
    this.discountInfo = discountInfo;
  }
}

생성자가1개일 때 @Autowired가 없어도 작동되는 이유
스프링이 해당 클래스 객체를 생성하여 빈에 넣어야 하는데 생성할 때 생성자를 부를 수 밖에 없다. 그래서 빈을 등록하면서 의존 관계 주입도 같이 발생한다.


2. Setter 주입

  • 선택과 변경 가능성이 있는 의존 관계에 사용
  • @Autowired를 입력하지 않으면 실행이 되지 않는다.
  • @Component가 실행하는 클래스를 스프링 빈으로 등록한다. 스프링 빈으로 등록한 다음 의존 관계를 주입하게 되는데 @Autowired가 있는 것들을 자동으로 주입
@Component
public class OrderServiceImpl implements OrderService {
	private UserRepository userRepository;
  	private DiscountInfo discountInfo;

	@Autowired
	public void setUserRepository(UserRepository userRepository) {
		this.userRepository = userRepository;
	}

	@Autowired
	public void setDiscountInfo(DiscountInfo discountInfo) {
		this.discountInfo = discountInfo;
	}
}

3. 필드 주입

  • 코드가 간결해서 예전에 많이 사용된 방식이지만, 외부에서 변경이 불가능하여 테스트하기 힘들다는 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 실제 코드와 상관 없는 특정 테스트를 하고 싶을 때 사용할 수 있다.
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private UserRepository userRepository;

	@Autowired
    private DiscountInfo discountInfo;
}

생성자 주입을 사용해야 하는 이유

과거에는 Setter, 필드 주입을 많이 사용했지만, 최근에는 대부분 생성자 주입 사용을 권장한다. 그 이유에 대해 알아보자

  • 불변
    • setter 주입의 경우 메서드를 public을 열어두어 변경이 가능하기 때문에 적합하지 않다.
    • 의존 관계 주입은 처음 애플리케이션이 실행될 때 대부분 정해지고 종료 전까지 변경되면 안된다.
    • 생성자 주입은 객체를 생성할때 최초 1회만 호출되어 불변하게 설계가 가능하다.
  • 누락
    • 생성자 주입을 사용하면 주입 데이터 누락시 컴파일 오류를 확인 가능하다.
  • final 키워드 사용가능
    • 생성자 주입을 제외한 나머지 주입 방식은 생성자 이후에 호출되므로 final 키워드를 사용할 수 없다.
  • 순환 참조
    • 생성자를 통해 순환 참조를 하게 되면 BeanCurrentlyInCreationException이 발생하여 순환 참조를 방지할 수 있다.

Component Scan

스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 Component Scan 기능을 제공한다.
앞에서 본 예제들은 @Bean, @Configuration 어노테이션을 이용하여 직접 작성하고 스프링 빈을 등록하였다. @ComponentScan 어노테이션을 이용하면 @Component 어노테이션이 붙은 모든 클래스를 스프링 빈으로 등록해준다.
또한 의존 관계를 자동으로 주입해주는 @Autowired 기능도 제공한다.

여기서 주의할 점은 @ComponentScan을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록된다.

그 이유는 @Configuration 어노테이션에 @Component 어노테이션이 붙어 있기 때문이다. @Configuration 설정이 된 파일이 있을 때 아래의 코드를 추가하여 @Configuration 부분을 스캔하지 않도록 한다.

@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration 설정.class))

ComponentScan 대상

  • @Component : 컴포넌트 스캔에서 사용
  • @Controller & @RestController : 스프링 MVC 및 REST 전용 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용

@Bean과 @Component

@Bean 어노테이션과 @Component 어노테이션의 기능은 스프링 컨테이너에 bean을 등록하는 것으로 비슷한데 차이점을 다음과 같이 확인할 수 있다.


@Bean메서드를 타겟으로

@ComponentClass를 타겟으로


0개의 댓글