본문 바로가기
Programming/Languages (Java, etc)

[SPRING] Bean의 정의와 IoC 컨테이너 (부제 : IoC는 DI가 아니에요)

by kghworks 2023. 2. 14.

bean class in intellij

 

Unfortunately, there aren't clear answers to the simple question of what a Spring bean really is.....

* 출처 : https://www.baeldung.com/spring-bean 

 

 그렇다고 합니다.... 이렇다 보니 구글링 해서 나오는 bean, IoC, DI에 대한 포스팅들은 해석이 약간씩 다릅니다. 그래서 이번만큼은 최대한 스프링 공식 문서 (docs.spring.io)와 스프링 공식문서만큼 많이 보는 (절망적 이게도) baeldung에 의존합니다. 아래 공식문서를 붙여드립니다.

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans 

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

목차

  • Bean의 정의
  • IoC (Inversion of Control) a.k.a DI
  • 의존성 주입 방법 (Dependency Injection)
  • 참고

Bean의 정의

 Bean은 Spring IoC Container에 의해 관리되고 (managed), 객체가 되고 (instatiated), 가공되는(assembled) object입니다.

 

Spring Framework의 IoC 컨테이너가 Bean을 관리합니다. 객체들은 개발자가 직접 사이클 (생성, 소멸)을 관리하지 않고 스프링에게 맡깁니다. 스프링은 개발자가 생성한 설정 파일 (xml 파일의 <bean/> , @Component와 같은)을 읽어 빈을 생성합니다.

 

 * Bean의 이름은 컨테이너 안에서 유일성을 가져야 합니다. (한 컨테이너 안에 userRepository라는 빈이 2개 이상일 수 없음) 컨테이너가 빈을 찾지 못할 경우에는 @Qualifier를 통해 스프링 컨테이너가 주입할 빈을 알려줄 수 있습니다. (NoUniqueBeanDefinitionException 발생 시 요령)

https://www.baeldung.com/spring-qualifier-annotation

 

 

왜 개발자가 object를 관리하지 않고, Spring IoC 컨테이너에게 위임할까요.

일반적인 vo 클래스들은 빈으로 등록 안 하면서, 왜 Controller, Service, Repository는 빈으로 등록할까요.

 

IoC (Inversion of Control, 제어의 역전)에 답이 있습니다.

 


IoC (Inversion of Control) a.k.a DI

IoC is also known as dependency injection (DI)

 * 출처 : spring.io docs

 

위 문장을 해석하기 앞서 아래 3가지 용어를 명확하게 이해하고 가야 합니다.

 

  • 의존성 주입 (DI, Dependency Injection) : 객체 간의 의존관계를 주입 (ex. User 객체의 생성자 안에 Address 인자)
  • 제어 : object 간의 의존관계를 설정, 객체의 생성 / 소멸 등을 관리하는 행위
  • 제어의 역전 : 객체 (class 안에서)가 능동적으로 제어하지 않고  수동적으로 다른 것으로부터 제어를 당할 수 있게 제어 행위 주체를 역전시킴

 

 의존성 주입은 IoC 가 아니어도 구현할 수 있습니다. IoC는 DI가 아닙니다. 

 

 스프링은 객체 간의 의존성 주입을 각 객체들이 스스로 클래스 안에서 코드로서 주입하도록 두지 않고, IoC 컨테이너가 주도적으로 할 수 있게 위임할 수 있습니다. 

 

 객체를 빈(bean)으로 등록하고, 의존성을 명시해 주면 의존성 주입을 스프링 IoC 컨테이너가 해줍니다. 이것이 제어의 역전입니다! 개발자 (객체)가 안 하고 스프링이 하니까요. 즉 제어의 역전이라 함은, 오브젝트 간의 연관관계 (의존성, 그 외에도 생성시점, 소멸 등)를 java code로 class file 안에서 객체가 능동적으로 할 수 있게 하지 않고, 의존관계를 사전에 명시한 다음 그 객체를 BeanFactory에 bean으로 등록하여 스프링 IoC 컨테이너가 할 수 있게 위임한다는 것을 말합니다.

 

 스프링 IoC 컨테이너가 객체를 빈으로서 미리등록한 다음 관리한다고 했습니다. IoC 컨테이너는 빈을 만들고, 객체 간의 의존성을 연결 (주입)해주고, 프로그램 (클라이언트)에게 그 객체를 제공해 줍니다.

 

 왜 의존성 주입을 스프링이 하는 게 편할까요?

 

 전통적인 방식으로 개발자가 직접 의존성을 주입하는 예제를 보여드리겠습니다.

public class Company {

    private String name;

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }
    ...
}

...

public class Car {

    private String name;

    private Company company;

    public Car() {
    }

    public Car(String name, Company company) {
        this.name = name;
        this.company = company;
    }
  ...
}

 

 이제 BMW Company에 자동차가 100대가 있다고 해 봅시다. 노가다가 시작됩니다. Car 객체를 만들 때마다 BMW 의존성을 주입해주어야 합니다.

 

Company bmw = new Company("BMW");
Car x1 = new Car("x6", bmw);
Car x2 = new Car("x6", bmw);
Car x3 = new Car("x6", bmw);
Car x4 = new Car("x6", bmw);
Car x5 = new Car("x6", bmw);
Car x6 = new Car("x6", bmw);
...

 

빈에 등록한 다음 스스로 의존성을 주입하도록 해보겠습니다.

@Component
public class Car {

    private String name;

    private Company company;

    public Car() {
    }

    public Car(String name, Company company) {
        this.name = name;
        this.company = company;
    }
  ...
}

@Configuration
@ComponentScan(basePackageClasses = Car.class)
public class AppConfig {
    @Bean
    public Company getCompany() {
        return new Company("BMW");
    }
}

 

* 빈에 등록하는 방법은 다른 레퍼런스들이 많으니 참고 바랍니다.

 

 빈을 가져와 보면 이미 BMW Company의 의존성이 주입되어 있습니다. 즉 개발자가 사전에 빈에 등록 시 의존성을 주입 (명시)해주면 스프링은 빈을 관리할 때 의존성을 주입해두고 있습니다.

 

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Car bmw_ = context.getBean("car", Car.class);
bmw_.getCompany().getName();  // BMW

 

 사실 VO 클래스들은 스프링 빈으로 등록을 잘 안 합니다. 위에는 의존성 주입을 스프링이 해주는 아주 간단한 예시를 보여준 것 일 뿐이고요. 대부분의 객체 간의 의존성이 이미 RDBMS로부터 주입되어 나오죠. 게다가 스프링의 빈들은 기본적으로 싱글톤으로 (Single Tone)으로 관리되기 때문에 VO 객체와는 성격 자체가 다릅니다.

* 대부분의 VO 객체들은 프로포토 타입, 즉 매번 다른 객체입니다.

 

이제 빈에 등록하여 스프링이 의존성을 주입하는 것에 의의가 있는 예제를 보겠습니다. Controller-Service-Repository 의존관계를 구현해 보겠습니다.

 

개발자가 직접 의존성 주입을 하려면

 

public class UserController {

    @RequestMapping("/tst2")
    @ResponseBody
    public void tst2()  {
        UserRepository userRepository =  new UserRepository();
        UserService userService = new UserService(userRepository);
        
        userService.viewMember();

    }
}

public class UserService {

    private UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

}

public class UserRepository {
}

 

위 코드는 2가지가 나쁩니다.

  • UserRepository 내용은 런타임에 바뀔 일이 없는 데 매번 새로운 객체로 생성해서 만들고 있고 (성능 저하)
  • UserService 객체를 만들 때마다 매번 UserRepository 의존성을 주입해줘야 하는 번거로움이 있습니다 (코드 반복)

 

따라서 스프링은 빈으로 등록하면

  • 싱글톤 패턴으로 해당 객체를 관리하도록 할 수 있게 하고
  • 빈으로 등록된 객체에 한해 아주 쉽게 의존성을 주입할 수 있는 방법을 제공합니다.

 

 이렇게 쉬운 의존성 주입은 빈끼리만 가능합니다. (빈에서 빈이 아닌 객체의 의존성을 주입하는 방법도 있긴 한데 예외적입니다)

public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping("/tst2")
    @ResponseBody
    public void tst2()  {
       userService.viewMember();
    }
}

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;
}

@Repository
public class UserRepository {
}

 

 @Service, @Repository 어노테이션으로 빈으로 등록, @Autowired로 손쉽게 의존성을 주입했습니다. 

 


의존성 주입 방법 (Way to Dependency Injection)

 

  다시 말하지만, 빈 들에 한해서 IoC 컨테이너는 의존성 주입을 아주 쉽게 할 수 있도록 해줄 뿐  의존성 주입은 개발자도 할 수 있습니다. 의존성 주입은 아래 예제가 끝입니다. 

 

public class Company {

    private String name;

    public Company() {
    }

    public Company(String name) {
        this.name = name;
    }
    ...
}

...

public class Car {

    private String name;

    private Company company;

    public Car() {
    }

    public Car(String name, Company company) {
        this.name = name;
        this.company = company;
    }
  ...
}

 Car는 Company를 의존하고 있고, Company에 대한 의존성 주입은 Car 생성자에서 이루어집니다. 이게 의존성 주입 (DI)입니다. 

의존성을 주입하는 방법은 아래 3가지가 있고 스프링의 경우 생성자를 통해 주입하는 방법을 추천합니다.

 

  • 필드
  • Setter-based DI (setter method 기반 의존성 주입)
  • Constructor-based DI (생성자 기반 의존성 주입) (Recemended)

 

필드

@Service
public class UserService {

    @Autowired 
    private UserRepository userRepository;
    
}

 

Setter-based DI (setter method 기반 의존성 주입)

@Service
public class UserService {

    private UserRepository userRepository;

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

}

 setter 기반의 의존성 주입은 주로 optional 하게 의존성 주입을 해주어야 할 경우 사용합니다. optional 하게 의존성을 주입해주어야 할 합리적인 이유가 있으면 됩니다. 그러나 의존 대상 객체가 null 이어도 서비스 구동에 문제가 없기 때문에, 런타임에 NullPointerException이 발생할 수 있습니다. 따라서 의존 대상 객체가 기본 값 (default value)를 가지게 하는 것이 좋습니다. 

 

 setter 기반 의존성 주입이 주는 장점으로는 setter로 통해 해당 클래스를 나중에 재설정하거나 다시 의존성 주입을 할 수 있다는 것입니다. (당연한 얘기..)

 

 

Constructor-based DI (생성자 기반 의존성 주입) (Recemended)

@Service
public class UserService {

    private UserRepository userRepository;

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

 

 스프링은 공식적으로 생성자 기반을 더 추천합니다. 그 이유는 Bean 때문입니다. Bean의 의존성 주입에 한해서 아래 3가지 이점을 취할 수 있습니다.

 

  • 의존 객체를 불변 객체로 생성 가능 (final 객체)
  • 순환 참조 사전 감지
  • 의존 객체 Not Null (NullPointerException 방지)

 위에 예시를 든 경우를 보면 앞에 두 방법은 userService Bean이 만들어진 다음 BeanFactory에서 의존해야 하는 객체를 가져와 주입합니다. 그러나 생성자 기반의 의존성주입은 userService Bean이 만들어질 때 의존해야 하는 객체를 BeanFactory로부터 가져와서 주입해야 합니다.

 

 즉 의존해야 하는 객체를 final (불변 객체, immutable)로 만들 수 있습니다. 이렇게요.

@Service
public class UserService {

    private final UserRepository userRepository;

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

 

그리고 순환참조를 감지합니다. 위의 경우 UserRepository가 UserService에 의존하려고 한다면, 서비스 시작 시 에러가 발생합니다.

@Service
public class UserService {

    private final UserRepository userRepository;

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

@Repository
public class UserRepository {

    private final UserService userService;
    
    @Autowired
    public UserRepository(UserService userService) {
        this.userService = userService;
    }
}

 

순환 참조 에러

세 번째로 NullPointerException을 방지할 수 있습니다. 마찬가지로 생성자를 통한 의존성 주입은 객체 생성시점에 의존 대상 객체를 BeanFactory에서 찾아와 주입해야 하므로 의존 대상 객체가 null이면 서비스 구동 자체가 안되게 됩니다. 

 

 

spring은 생성자 기반 의존성 주입을 더 추천한다. (출처 : https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-setter-injection)

 


참고

 

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using.spring-beans-and-dependency-injection

 

Spring Boot Reference Documentation

This section goes into more detail about how you should use Spring Boot. It covers topics such as build systems, auto-configuration, and how to run your applications. We also cover some Spring Boot best practices. Although there is nothing particularly spe

docs.spring.io


https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-setter-injection
https://www.baeldung.com/spring-bean

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

https://jwchung.github.io/DI%EB%8A%94-IoC%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%84%EB%8F%84-%EB%90%9C%EB%8B%A4

 

DI는 IoC를 사용하지 않아도 된다

Dependency Injection(DI)을 얘기할 때 빠지지 않는 글이 있다. Martin Folwer의 Inversion of Control Containers and the Dependency Injection pattern이다. 이 글에서 DI란 용어가 생겨나게된 배경을 볼 수 있다. 관련한 내용

jwchung.github.io

 

댓글