ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Bean
    Spring 2022. 7. 25. 23:40

    스프링은 어노테이션, Java 혹은 xml 기반으로 bean 을 정의하면 객체를 bean 으로 등록하여 개발자가 편하게 주입 받아 사용할 수 있습니다. 이번 포스팅에선 Spring Bean 과 Spring IoC 컨테이너에 대해 알아보겠습니다.

    Bean 이란?

    • 스프링 IoC 컨테이너가 관리하는 객체를 의미합니다.
    A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
    ​
    - 스프링 공식문서 -

    그렇다면 스프링 IoC 컨테이너란 무엇을 의미할까요?

    Spring IoC 컨테이너란?

    • ApplicationContext 인터페이스가 Spring IoC 컨테이너를 대표합니다.
    • Bean을 관리하는 객체입니다.
    • Bean을 인스턴스화 하고 구성 및 모으는 책임이 있습니다.
    The org.springframework.context.ApplicationContext interface represents the Spring IoC Container and is responsible for instantiating, configuraring, and assembling the beans
    ​
    - 스프링 공식문서 -

    Spring Bean 을 왜 사용할까?

    그런데 스프링 Bean 이 왜 필요할까요? 그냥 new 키워드를 사용해서 객체를 생성하면 안되는 것일까요? 객체를 Bean으로 등록하지 않고 의존성 주입을 도입한다면 어떤 불편한 점이 있을까요?

    public class Service {
      private final Dao dao;
      
      public Service(Dao dao) {
        this.dao = dao;
      }
    }
    ​
    public class JdbcDao implements Dao {
      // ...
    }
    ​
    ​
    Service service = new Service(new JdbcDao());

    의존성을 주입한다고 하면 어디에선가 new 키워드를 사용하여 객체를 생성해야 합니다. 예시 코드를 보면 Service 의 생성자로 Dao 구현체를 생성하여 넣어주고 있습니다.

    만약 다음과 같이 Dao 구현체가 바뀐다면 어떻게 될까요?

    public class Service {
      private final Dao dao;
      
      public Service(Dao dao) {
        this.dao = dao;
      }
    }
    ​
    public class InMemoryDao() {
      //...
    }
    ​
    Service service = new Service(new InMemoryDao());

    서비스를 생성하는 곳에서도 변경이 일어납니다. 왜 그런 것일까요? 서비스를 생성할 때 Dao 를 초기화 함과 동시에 어떤 Dao 의 구현체를 선택할 것인지에 대한 책임도 있기 때문입니다.

    또한 의존성을 주입할 때 여러개의 의존성이 필요하다면 필요한 객체를 하나하나 생성해서 넣어주어야 하고 자바의 경우 해당 의존성 주입 순서를 알아야합니다.

    LineService lineService = new LineService(new StationDao(), new LineDao(), new SectionDao());

    즉, 직접 의존성을 주입하기 위해서는 의존 관계를 모두 파악해야하는 번거로움이 생깁니다. 또한 많은 객체가 중복 생성될 수 있습니다.

    그래서 의존성 주입이 필요한 객체를 Bean으로 등록하여 스프링 IoC 컨테이너가 객체의 생성과 의존성 주입을 관리하도록 해야합니다.

    이렇게 구현한다면 주입된 의존성을 사용하는 부분에만 집중할 수 있게됩니다.

    정리하자면 Spring Bean 을 사용하면 다음과 같은 이점이 있습니다.

    1. 서비스를 생성하는 곳에서 변경을 신경쓰지 않아도 된다.
    2. 직접 의존관계를 모두 파악하고 주입해주지 않아도 된다.

    Bean과 싱글턴

    Bean은 기본적으로 싱글턴으로 관리됩니다. 만약 스프링이 아닌 개발자가 객체를 싱글톤으로 만들어 사용하게 된다면 어떤 문제점이 생길까요? 객체를 싱글톤으로 만든다는 것은 일반적으로 객체에 싱글톤 패턴을 적용하는 것입니다.

    public class DatabaseConnection {
      private static final DatabaseConnection databaseConnection = new DatabaseConnection();
      
      private DatabaseConnection() {
        
      }
      
      public static DatabaseConnection getConnection() {
        return databaseConnection;
      }
    }

    이런 싱글턴 패턴에는 단점이 있습니다.

    1. 다형성을 이용하지 못합니다.
      • 싱글톤 패턴을 사용하면 생성자의 접근 지정자를 private 으로 설정해야 합니다. 이렇게 되면 해당 객체는 상속이 불가능합니다.
    2. 단위 테스트가 어렵습니다.
      • 객체를 싱글톤 패턴으로 구현할 경우 해당 객체는 공유 객체가 되므로 단위테스트를 실행할 때 테스트의 순서에 따라 결과가 달라집니다.
    @DisplayName("station 을 저장한다.")
    @Test
    void test_1() {
      Station station = new Station("선릉역");
      assertThat(StationjdbcDao.save(station).getName()).isEqualTo("선릉역");
    }
    ​
    ​
    @DisplayName("station 을 저장한 후, stations 을 조회한다.")
    @Test
    void test_2() {
      Station station = new Station("잠실역");
      StationJdbcDao.save(station);
      
      List<Station> stations = StationJdbcDao.findAll();
      assertThat(stations.size()).isEqualTo(1);
    }

    그렇다면 스프링은 싱글턴 단점들을 어떻게 해결했을까요?

    IoC 컨테이너가 Bean의 LifeCycle을 관리하는 과정을 알아보며 이 단점들을 어떻게 해결하는 것인지 알아보겠습니다.

    1. 객체 생성

    먼저 스프링 IoC 컨테이너가 생성이 되면 Bean 스코프가 싱글톤인 객체를 생성합니다. 이때 Bean으로 등록하기 위해서 어노테이션 기반 혹은 Java 설정 클래스 기반 또는 xml 기반의 다양한 Configuration 메타 데이터를 이용하여 통일된 Bean Definition 을 생성합니다.

    그리고 Bean으로 등록할 Pojo와 Bean Definition 정보를 이용하여 Bean을 생성합니다. 이 과정에서 싱글톤 패턴을 사용하는 것이 아닌 평범한 자바 클래스를 이용하여 객체를 생성합니다. IoC 컨테이너는 싱글턴 레지스트리라는 기능도 가지고 있습니다.

    레지스트리란 스프링 뿐 아닌 CS 전반적으로 쓰이는 개념으로 Key와 Value 형태로 데이터를 저장하는 방법입니다. Spring IoC 컨테이너는 Bean 스코프가 싱글톤인 객체에 Bean의 이름을 Key로 객체를 Value 로 저장합니다. 그래서 의존성이 주입되어야 하는 객체가 Bean으로 등록되어 있을때 스프링은 Bean의 이름을 이용하여 항상 동일한 Single Object를 반환합니다.

    이렇게 Bean 객체가 생성이 되면 IoC 컨테이너는 의존 설정을 합니다 .이때 의존성 자동 주입이 일어나게 됩니다.

    그리고 객체를 초기화 하는 과정을 진행합니다. 모든 객체가 다 필요한 것은 아니지만 커넥션 풀처럼 사용전에 초기화 과정이 필요한 객체들은 초기화 과정을 진행합니다.

    초기화가 끝나면 드디어 Bean을 사용할 수 있게 됩니다.

    그리고 스프링 컨테이너가 종료될 때 Bean 스코프가 싱글톤인 객체들도 함께 소멸됩니다 .

    Bean 을 사용할 때 주의할 점

    이렇게 편리한 Bean이지만 Bean으로 설정하기 위해서 주의해야 할 점이 있습니다 .

    예를들어 싱글톤 스코프의 Bean이 value 라는 상태를 가질 때 Thread 1 은 value 의 값을 증가시킨다고 가정해보겠습니다.

    그리고 Thread2는 value 라는 값을 가져와 사용한다고 하면 Thread2 는 매번 다른 값을 사용할 수 있습니다.

    따라서 Bean 스코프를 싱글턴으로 설정한 경우 상태를 가지면 안됩니다.

    하지만 상태를 가질 수 있는 Bean도 있습니다.

    Bean은 생성되고 존재하고 적용되는 범위를 지정할 수 있습니다.

    이를 Bean 스코프라 하고 Bean을 적용하는 객체에 @Scope 어노테이션을 사용하여 설정할 수 있습니다.

    Bean은 기본적으로 싱글톤 타입이지만 이외에도 프로토타입이라는 스코프가 있습니다 .Bean 스코프를 프로토타입으로 설정한다면 IoC 컨테이너와 함께 생성되고 소멸되는 것이 아닌 요청이 올때마다 객체가 생성되기 됩니다. 즉, 모든 스레드에서 공유하는것이 아니므로 해당 객체는 상태를 가질 수 있습니다.

    @Repository
    public class StationInMemoryStationDao implements StationDao {
      
    }
    ​
    @Repository
    public class StationJdbcStationDao implements StationDao {
      
    }
    ​
    @Service
    public class StationService {
      private final StationDao stationDao;
      
      public StationService(final StaionDao stationDao) {
        this.stationDao = stationDao;
      }
    }

    또다른 주의사항으로는 의존성을 자동 주입해야 할 인터페이스에 구현체가 두 개 이상이라면 스프링은 어떤 구현체를 자동 주입할지 정하지 못해서 충돌이 일어나게됩니다.

    이럴 땐 @Primary 또는 @Qualifier 어노테이션을 사용해 Bean 주입의 우선순위를 정할 수 있습니다 .

    만약 사용할 Bean이 하나라면 @Primary 어노테이션을 해당 Bean에 붙여주어 우선순위를 정할 수 있습니다.

    @Repository
    @Primary
    public class StationInMemoryStationDao implements StationDao {
      
    }
    ​
    @Repository
    public class StationJdbcStationDao implements StationDao {
      
    }
    ​
    @Service
    public class StationService {
      private final StationDao stationDao;
      
      public StationService(final StaionDao stationDao) {
        this.stationDao = stationDao;
      }
    }

    다른 한가지 방법은 @Qualifier 어노테이션을 사용하는 방법입니다.

    @Repository
    public class StationInMemoryStationDao implements StationDao {
      
    }
    ​
    @Repository
    public class StationJdbcStationDao implements StationDao {
      
    }
    ​
    @Service
    public class StationService {
      private final StationDao stationDao;
      
      public StationService(@Qualifier("stationInMemoryStationDao") StationDao stationDao) {
        this.stationDao = stationDao;
      }
    }

    이렇게 하면 해당 어노테이션 파라미터 안에 있는 Bean의 값이 우선 설정됩니다.

    참고

    https://www.youtube.com/watch?v=3gURJvJw_T4

    댓글