스프링

스프링 IOC, Bean

o늘do 2022. 1. 17. 21:30

IOC 란 ?

아래 코드를 보자

@Controller
public class OrderController {

    private OrderService orderService = new OrderService();

}

위 코드는 OrderController 라는 클래스에서 OrderService 객체를 new 키워드로 생성해서 사용하고 있다.

위와 같은 경우는 직접 OrderService 라는 의존성을 관리 하고 있는 상태이다.

아래 코드를 보자.

@Controller
public class OrderController {

    private OrderService orderService;

        public Controller(OrderService orderService){
            this.orderService = orderService;
        }

}

위 코드는 OrderController 클래스가 OrderService를 사용하지만 집접 객체를 생성하지 않고 누군가가 Controller 밖에서 생성자를 통해서 받아오는 구조이다.

더이상 OrderController 가 OrderService 라는 의존성을 생성하지 않고 OrderController의 제어권이 아니다. 그렇기 때문에 제어권이 역전 되었다고 하는 것이다.

또한 이렇게 의존성을 주입하는것을 DI 라고 하는데 DI 또한 일종의 IOC 이다.

IOC Container 란?

일단 여기서 Container 란 무엇일까?

컨테이너는 보통 인스턴스의 생명 주기 를 관리하고, 생성된 인스턴스들에게 추가적인 기능을 제공하도록 하는 것이다.

즉 IOC Container 는 객체에 대한 생성 및 생명주기를 관리한다.

IOC Container 는 대표적으로 두가지가 있다.

  • Application Context
  • BeanFactory

BeanFactory 를 사용해도 되지만 아래를 보면 Application Context 는 BeanFactory 를 상속받고 있기 때문에 더 다양한 기능을 가지고 있어서 IOC 컨테이너는 Application Context 를 사용한다.

Bean?

아까 IOC Container 는 객체를 관리한다고 했는데 여기서 이 객체를 Bean 이라고한다.

즉 Bean은 Application Context 가 관리하는 객체이다.

Bean 으로 등록하는 방법

  1. 특정 어노테이션
  2. 특정 인터페이스를 상속받는 방법
  3. 직접 빈으로 등록하는 방법

위와 같은 방법들이 있고 Bean 으로 등록하게 되면 Ioc 컨테이너가 의존성을 주입해주게 된다.

@Controller
public class OrderController {

    private OrderService orderService;

        public Controller(OrderService orderService){
            this.orderService = orderService;
        }

}

위 코드도 @Controller 어노테이션으로 빈으로 등록하게 되면 IOC Container 가 OrderService(**역시 빈으로 등록되어있음**) 객체를 찾아서 의존성을 주입하게 된다.

OrderService 가 역시 빈으로 등록되어있다 라는 말은 보편적으로 IOC Container에 빈으로 등록된 객체들 끼리만 의존성을 주입받을 수 있다는 뜻이다.

물론 IOC Container 에 있는 Bean들을 가져오는 방법을 제공하지만 IOC Container 밖에 있는 객체에는 의존성 주입이 가능하지만 지양하는 편이다.

아래와 같이 직접 applicationContext 이 관리하는 빈을 확인 할수 있다.

@Controller
public class OrderController {

    private OrderService orderService;
        private ApplicationContext applicationContext

        public Controller(OrderService orderService, ApplicationContext applicationContext){
            this.orderService = orderService;
        }

    @GetMapping("/bean") 
    @ResponseBody 
    public String bean() { 
    return "bean : " + applicationContext.getBean(OrderService.class)  
     + "" + "orderService : " + this.orderService; 
    }

}

applicationContext.getBean(OrderService.class) 와 this.orderService 를 비교해보면
같은 해시코드를 가지게된다. 즉 같은 객체라는 말이다. 이러한 형태를 Singleton Scope의 객체라고 한다.

Spring은 Bean을 생성할 때 기본적으로 모든 Bean을 Singleton으로 생성하여 관리하게된다.
SIngleton Bean은 Spring 컨테이너에서 한 번 생성되며, 컨테이너가 사라질 때 같이 제거되며,객체 하나를 Application 전반에서 계속 재사용하게 된다.

**싱글톤으로 관리하게되면 장점?**

에플리 케이션 구동중에 인스턴스를 단 한개만 생성하고 사용하므로 객체에 메모리를 한번만 할당하게되 메모리 측면에서 효율적이다.

그렇다면 단점, 문제점은?

스프링은 멀티 스레드 환경이기 때문에 위그림과 같이 여러 스레드가 스프링 빈을 사용하게 된다.

다음과 같은 코드가 있다고 하자

package com.example.productprj.study.java;

import java.util.stream.IntStream;

class Singleton {

        // count 공유자원, 전역변수
    private int count; 

    private static Singleton singleton = new Singleton(); // static 초기화시 바로 할당

    private Singleton() {
    }

    public static Singleton getInstance() {
        return singleton;
    }

    public int add() {
        return ++count;
    }

}

public class Test {

    public static void main(String[] args) {

        Singleton singleton = Singleton.getInstance();

        IntStream.range(1, 15)
                .forEach(n -> new Thread(() -> {
                            try {
                                Thread.sleep(1000);
                                System.out.println(singleton.add());
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }).start()
                );

    }

}
## 결과
1
1
2
4
3
5
9
8
7
6
10
13
12
11

결과는 1 ~ 14 가 나오길 예상했지만 1 이 두번 중복되는 것을 볼 수 있다.

위 코드에서 count 변수는 공유자원이다. 좀더 자세히 말하면 각 스레드들은 stack 영역은 각자 스레드 마다 고유하지만 힙 영역(전역변수가 저장되는)은 공유되기 때문에 위와 같이 데이터 조작에 문제가 생겼다.

결론은 스프링빈은 싱글톤으로 관리되기 때문에 멀티 스레드 환경에서 Thread-safe 하지 못하다. 내가 코드를 짤때 보통의 경우에 스프링빈에서 불변객체를 사용해서 이러한 동시성 문제가 발생하지 않은 것 뿐이였다.

혹은 스프링 빈사이에서 데이터를 조작할때 메소드의 파라미터(지역변수)를 사용해서 Thread-safe 한것 뿐이였다.

앞으로 코드를 짤때 스프링 빈에서 공유변수를 사용하게 되면 synchronized 키워드나 다른 방법을 사용해서 Thread-safe 한 코드를 짜도록 해야겠다.

Bean

객체 중에서 Spring IoC Container가 관리하는 객체를 Bean 이라고 한다.

스프링의 Bean 등록은 ComponentScan, EnableAutoConfiguration 두단계로 이루어져 있다.

Spring Boot Project 에서 빈을 찾아 등록하는 과정(ComponentScan)

위와 사진과 같이 @SpringBootApplication 어노 테이션 내부에는 @ComponentScan이라는 Annotation 이 적용되어 있다. @ComponentScan 어노테이션의 설정값에 따라 ComponentScan 이 동작하게 된다.

이 때 SpringBootApplication 과 같은 레벨에(같은 패키지 레벨 혹은 하위) 있는 클래스 들중에서 @ComponentScan 으로 Component 어노테이션이 붙어있는 클래스들을 찾아 IOC Container 가 관리하는 빈으로 등록하게 하게 된다.

이때 @Repository, @Service @Configuration, @Controller 가 붙은 클래스들도 빈으로 등록되는 이유는 위 해당 어노테이션 내부에 @Component 이 붙어있기 때문이다.

사실 빈등록은 @ComponentScan 이후에 @EnableAutoConfiguration 으로 등록되는 과정도 있다.

위와 같이 spring.factories 라는 파일에 EnabledConfiguration 이라는 이름으로 여러 클래스들이 선언되어 있는데 @EnableAutoConfiguration 과정에서 빈으로 등록되게 된다.

++ @ComponentScan 어노테이션의 설정값에 따라 ComponentScan 이 동작하게 된다.

아래 예제를 보면 @ConditionalOnMissingBean 어노테이션이 붙어 있다. 이 어노테이션의 역활은 SpringApplicationAdminMXBeanRegistrar 빈으로 등록되어 있지 않을 경우에만
빈으로 등록하라는 뜻이다.(예를 들어 이름이 같은 경우가 발생할 수 있으니까)
이외에도 많은 설정값이 있다 필요할때 찾아보자

  @Bean
    @ConditionalOnMissingBean
    public SpringApplicationAdminMXBeanRegistrar springApplicationAdminRegistrar(
            ObjectProvider<MBeanExporter> mbeanExporters, Environment environment) throws MalformedObjectNameException {
        String jmxName = environment.getProperty(JMX_NAME_PROPERTY, DEFAULT_JMX_NAME);
        if (mbeanExporters != null) { // Make sure to not register that MBean twice
            for (MBeanExporter mbeanExporter : mbeanExporters) {
                mbeanExporter.addExcludedBean(jmxName);
            }
        }
        return new SpringApplicationAdminMXBeanRegistrar(jmxName);
    }

또 개발을 하다보면 모든 클래스를 빈으로 등록하지 않아도 되는 경우가 있다.
예를 들어 배치를 돌릴때 특정 잡을 수행하는 클래스만 빈으로 등록하고 해당 잡을 수행하지 않는 클래스들은 불필요하게 빈으로 등록하지 않아도 되는 경우가 있을 것 이다.

**이럴 때사용할 수 있는 어노테이션이 @ConditionalOnExpression 이다.**

아래 코드를 보자

package com.example.productprj.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnExpression(value = "'${spring.application.name}' == 'hakjun'")
public class Hakjun {

    public String name(){
        return "hakjun";
    }

}

Hakjun 이라는 클래스를 만들었다. @Configuration 을 통해서 컴포넌트 스캔의 대상이 되도록 했지만(일반 적인 경우에는 빈으로 등록) @ConditionalOnExpression 을 value 값을 주어 프로그램 인자가 spring.application.name=hakjun 일경우에만 빈으로 등록되게 하였다.

만약 프로그램 인자값에 다음과 아래와 같이 설정을 하고 어플리케이션을 구동을 하면 아래 ApplicationRunner 코드의 run 함수가 정상적으로 작동할 것이고 아래와 값이 결과값이 나올것이다.

결과)

hakjun
==============
spring boot run!
==============

만약 프로그램 인자값에 올바른 값을 넣어주지 않는 다면 Hakjun 클래스는 컴포넌트 스캔단계에서 빈으로 등록되지않고 ApplicationRunner 에서 Hakjun 은 빈이아니기 때문에 의존성 주입을 받지 못해 다음과 같은 에러와 함께 어플리션이 종료되게 된다.

결과)

Consider defining a bean of type 'com.example.productprj.config.Hakjun' in your configuration.
package com.example.productprj.config;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner implements ApplicationRunner {

    private final Hakjun hakjun;

    public AppRunner(Hakjun hakjun) {
        this.hakjun = hakjun;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(hakjun.name());
        System.out.println("==============");
        System.out.println("Spring Boot Runner!");
        System.out.println("==============");
    }
}

스프링 부트 자동설정

http://wonwoo.ml/index.php/post/20

DI (Dependency Injection)

의존성 주입을 하는 방법에는 여러가지가 있다.

생성자로 주입하는 방법

@Repository
public class ItemRepository {
    private final EntityManager em; 

        @Autowired // 최근 버전부터는 하나의 생성자에 대해서는
                                // 생략이 가능하다.(EntityManager 가 빈으로 등록되어 있어야한다.)
    public ItemRepository(EntityManager em){
        this.em = em;
    }

}

final 로 선언하여 런타임에 의존성을 주입받는 객체가 변하지 않음을 보장할 수 있고
final 로 선언한 생성자 주입방식은 null 이 올수 없게 된다.

**필드로 바로 주입받는 방법** 

@Repository
public class ItemRepository {

    @Autowired
    private EntityManager em;

}

setter 를 사용하는 방법

@Repository
public class ItemRepository {
    private EntityManager em;

    @Autowired
    public void setEntityManager(EntityManager em){
        this.em = em;
    }

}

**스프링에서 권장하는 방법은? 생성자로 주입하는 방법!**

생성자로 의존성을 주입하는 것이 좋은이유는 필수적으로 사용해야하는 참조값 없이는 해당 클래스를 만들수 없도록 강제할 수 있다. 좀더 안정성있는 프로그래밍이 가능해진다. ****

여기서에 안정성은 객체가 생성되는 시점에 빈을 주입하기 때문에 의존성이 주입되지 않아 발생 할 수 있는 NullPointerException 을 방지하게 된다.

하지만 이와 같은 경우에는 순환 참조가 발생할 수 있다. 이때는 필드 의존성 주입이나, setter 의존성 주입을 사용하도록 해결이 가능하다. (필드 의존성 주입도 충분이 순환 참조가 발생할 수 있다..)

어찌됐든 순환 참조가 발생하지 않도록 설계하는 것이 좋다. (이부분은 좀더 알아보자.)

사실 DI 를 주입 받는 순서, 시점이 다르다.

필드로 주입받는 경우와, setter 로 의존성 주입을 받는 경우에는 런타임에 의존성 주입을 하기 때문에 의존성 주입을 하지 않아도 객체가 생성될 수 있다.

반면에 생성자로 주입하는 경우에는 객체가 생성되는 시점에 빈을 주입하기 때문에 의존성이 주입되지 않아 발생할 수 있는 NullPointerException 을 방지한다.

생성자 주입은 테스트 작성이 용이하다.

테스트하려는 클래스가 필드 주입을 받을 때 외부에서 빈을 주입해 줄 수 없기 때문에 해당 필드는 null 값이 된다. 따라서 스프링 빈 및 모든 설정을 가져오고 실행해야 테스트가 가능하다.
하지만 생성자로 의존성을 주입하게 되면 테스트 코드 자체에서 필요한 의존관계를 만들어서 테스트가 가능하게 된다.

실제 개발시에는 lombok 을 활용하는 좋은 방법도 있다.

@RequiredArgsConstructor 을 통해 간단하게 생성자 주입 방식을 사용할 수 있다.

@Repository
@RequiredArgsConstructor
public class ItemRepository {
    private final EntityManager em;
}

'스프링' 카테고리의 다른 글

Spring Boot, Custom Annotation 만들기  (0) 2022.01.25