[Spring] Bean이란?

구범모·2023년 10월 9일
0
post-thumbnail

빈이란?

Spring Ioc 컨테이너에 의해 관리되는, 자바 객체.

IoC Container란?

  • Spring에서 ApplicationContext 인터페이스(BeanFactory를 상속한다.)가 IoC 컨테이너를 대표한다.
  • 빈을 구성, 조립, 인스턴스화 시키며, 빈의 생명주기를 관리한다.
  • 컨테이너는 Configuration 메타데이터를 읽은 후 런타임에 빈을 모은다.
  • 이후 빈 객체가 필요한 곳에서 DI(Dependency Injection)로 필요한 빈 객체들을 주입한다. DI는 다음의 3종류가 있다.
    • ⭐ 생성자 주입
    • 세터 주입
    • 필드 주입

각 DI 방식의 설명에 앞서, @Autowired 어노테이션부터 설명하겠다.

@Autowired

해당 어노테이션이 달린 생성자, 메서드(setter), 필드는,

직접 해당 객체를 생성할 필요 없이 Spring IoC Container에 등록된 Bean을 자동 주입(DI) 해 준다.

해당 어노테이션이 달리는 위치에 따라 생성자, setter, 필드주입으로 나뉜다.

다만 생성자에 어노테이션을 달 경우, 생성자의 파라미터가 빈으로 등록되어 있어야 하며, setter와 필드 또한 빈으로 등록되어 있어야 알아서 IoC Container가 Bean을 자동 주입해준다.

생성자 주입

가장 보편적으로 쓰이는 DI 방식이다.

@Configuration
public class AppConfig {

    @Bean
    public Item item1() {
        return new ItemImpl1();
    }

    @Bean
    public Store store() {
        return new Store(item1());
    }
}

위와 같이 Configuration 클래스에서 관리되는 빈들이 있다. 만약 Item 객체를 다른 클래스에서 사용하고 싶다고 가정한다.

public class Order {

	private final Item item;

	@Autowired
	public Order(Item item) {
			this.item = item;
	}

}

특이한 점은, 클래스의 생성자가 하나이고, 그 생성자로 주입받을 객체가 빈으로 등록되어 있다면 @Autowired를 생략 할 수 있다.

Setter 주입

public class Order {
	
	private Item item;

	@Autowired
	public void setItem(Item item) {
			this.item = item;
	}

}

필드 주입

public class Order {

	@Autowired
	private Item item;

}

3가지의 주입 방식을 비교해 보며, 가장 좋은 방식이 무얼까 한번 고민해 보자.

생성자 주입 vs 필드 주입

얼핏 보면 생성자 주입에 비해 필드 주입이 가장 코드량이 적고, 편리해 보여서 쓰고 싶어진다.

하지만 생성자 주입을 쓰는데, 그 이유는

  1. SRP원칙의 위배
    1. 위에서 봤듯이 필드 주입은 너무나 간단하다. 이는 한 클래스 내부에 무분별하게 많은 의존성을 추가함으로써, 하나의 클래스가 갖는 책임이 많아지는 결과가 생기므로, 곧 SRP를 위배하게 된다.
      만약 생성자 주입을 사용했었다면 충분히 개발 단계에서 이를 인지하고 책임을 분배할 수 있을 것이다.
  2. DI 컨테이너(IoC 컨테이너)와의 강한 결합
    1. 단위 테스트를 작성한다고 가정해 보자. 이때는 DI 컨테이너 없이도 필요한 의존성을 개발자가 끌어올 수 있어야 하는데, 필드주입을 사용하는 클래스의 단위테스트 때는 DI 컨테이너 없이는 의존성을 주입할 수 없게 된다. 이는 곧 DI 컨테이너와의 결합도가 높다는 것을 의미하며, 충분히 지양해야 하는 설계이다.
    2. 만약 생성자 주입이였다면, DI컨테이너 없이도 그저 생성자의 파라미터로, 개발자가 새로운 객체를 만들어 주입시켜주면 끝나는 문제이다.
  3. 불변성
    1. 생성자 주입과 다르게, 필드 주입은 해당 필드를 final로 선언하지 못한다. 따라서 변경(정확히는 재할당)이 가능하므로, 어플리케이션 동작 중에 예기치 못한 오류를 만날 수 있다.

결론은 생성자 주입 승.

생성자 주입 vs setter 주입

💡 생성자 주입과 setter 주입의 가장 큰 차이점은 무엇일까?
  • 정답은 강제성이다.
    • Setter는 항상 optional한 의존성을 주입하는 데에 사용돼야 한다. setter를 호출하는 클래스는, 의존성이 주입되는 대상이 없이도 동작 가능해야 한다. (그것이 optional한 의존성이기 때문에.)
    • 만약 개발자의 실수로 setter 호출을 까먹는다고 하자. 그리고 setter의 대상이 되는 의존성을 갖는 필드의 메소드를 호출한다면? 개발자가 가장 무서워하는 NPE가 터질 것이고 이는 절대 작은 문제가 아니다.
  • 또한 생성자 주입은 setter 주입과 다르게, Circular dependency(순환 참조)를 컴파일 시점에 잡아낼 수 있는 장점이 있다.

이런 관점에서, 역시나 생성자 주입 승.

그렇다면 Spring의 IoC Container의 DI와 자바에서의 생성자 / setter / 필드주입과 다른 것이 뭔데?

@Configuration
@ComponentScan("com.baeldung.constructordi")
public class Config {

    @Bean
    public Engine engine() {
        return new Engine("v8", 5);
    }

    @Bean
    public Transmission transmission() {
        return new Transmission("sliding");
    }
}
@Component
public class Car {

    @Autowired
    public Car(Engine engine, Transmission transmission) {
        this.engine = engine;
        this.transmission = transmission;
    }

}
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Car car = context.getBean(Car.class);

위 코드 어디에서도 Car객체를 생성하기 위해 Engine, Transmission의 객체를 생성하는 부분이 없다.

이는 Car 객체를 생성할 때

  1. Spring IoC 컨테이너가 알아서 Bean으로 등록된 Engine, Transmission 객체를 컨테이너 안에서 꺼내와서,
  2. 알아서 Car 생성자의 파라미터로 주입시켜주어 Car객체를 생성해 주는 것이다.

이는 개발자가 직접 주입할 객체를 생성하고, 주입하는 코드를 작성할 필요가 없다는 편리함이 있다.

Bean 생명주기

출처 : https://www.geeksforgeeks.org/bean-life-cycle-in-java-spring/

처음에 말했듯, 빈은 Spring Ioc 컨테이너에 의해 관리된다.

맨 처음 프로그램이 시작되면, Ioc 컨테이너가 시작되고, 컨테이너가 Bean 어노테이션 혹은 Component 어노테이션이 붙은 것들을 스캔하여 각각의 객체들이 필요한 의존성을 주입해 준다.

만약 빈이 만들어진 이후 실행시키길 원하는 메소드가 있다면

init메소드를 만든 이후에 @PostConstruct 어노테이션을 붙여준다면, 빈이 생성된 이후에 알아서 해당 메소드를 실행시킨다.

마찬가지로 빈이 사라지기 전에 실행시킬 원하는 메소드가 있다면 메소드를 만든 이후에 @PreDestroy 어노테이션을 붙여주면 된다.

이렇게 빈의 생명주기가 관리된다.

💡 참고 : 빈의 생성시기

보통은 Spring 어플리케이션이 동작되자마자 Bean객체들이 생성 & 설정되지만, @Lazy어노테이션을 추가로 붙인다면, 해당 Bean이 호출되는 시점에 생성된다.
그렇다면 무조건 호출되는 시점에 Bean을 생성하는게 이득 아니야 ? 라고 생각할 수도 있지만, 아니다.
Bean과 관련된 잘못된 설정이나 환경을 프로그램 실행 시점에 잡을 수 있는 것이 아니라,
해당 Bean이 호출되는 시점에 잡을 수 있으므로, 이는 프로그램 운영 관점에서 보면 아주 치명적이다.

결론 : Lazy initialization 쓰지 말자.

Bean Scope

빈의 생성방식. 6가지가 있다.

  • singleton
  • prototype
  • request
  • session
  • application
  • websocket

아래 4가지 방식은 web application에서만 쓰이는 방식이며, 스프링은 기본적으로 singleton scope로 Bean을 생성한다.

사용방법은 간단하다. 아래와 같이 Bean으로 등록할 메소드 위에 @Scope 어노테이션을 붙이고 원하는 scope을 명시하면 된다.

@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)

혹은 value = 와 함께 정의되어 있는 SCOPE상수를 써도 된다.

자주 사용하는 singleton, prototype 두가지의 기능만 간단히 설명하겠다.

Singleton Scope

우리가 아는 그 singleton 패턴이다.

다만 기존의 singleton 패턴은 Classloader마다 클래스의 인스턴스가 하나 만들어 지는 것이고,

Spring singleton은 IoC Conatiner마다 클래스에 대한 하나의 Bean 인스턴스만 만들어지고 쓰인다.

모든 Bean 요청에 대해 spring container는 같은 Bean을 리턴한다.

출처 : https://docs.spring.io/spring-framework/docs/3.0.0.M3/reference/html/ch04s04.html#beans-factory-scopes-singleton

Prototype Scope

singleton과 달리, 해당 Bean에 대한 요청이 있을 때 마다(ex : DI, getBean()호출 등.) 새로운 Bean 인스턴스를 만드는 방식.

모든 Bean 요청에 대해 spring container는 각각 다른 Bean을 리턴한다.

Singleton Scope와 다르게, 생성된 Bean을 스프링 컨테이너가 관리하지 않으므로, 클라이언트에서 소멸 메소드 등을 별도로 관리해야 한다.

Singleton VS Prototype

SingletonPrototype
인스턴스 갯수IoC 컨테이너당 하나의 Bean 저장.하나의 인스턴스는 컨테이너의 cache에 저장돼 있으며, Bean 요청때마다 캐싱된 빈이 리턴된다.DI시점 / Bean 요청때마다 새로운 인스턴스가 생성된다.
Bean 생성시기lazy옵션이 없다면, application context(spring container)가 실행될 때.Bean이 호출될 때
State 유무공유되는 Bean이므로, 기본적으로 stateless하다.생성되는 Bean마다 공유하는 자원이 없으므로, stateful하다.

Singleton Scope의 단점은 기존 디자인 패턴에서의 Single톤 패턴의 단점과 비슷하다. 이에 대해 별도의 포스팅을 하겠다.

Reference

profile
우상향 하는 개발자

0개의 댓글