스프링(Spring) #2 - 스프링에서의 제어의 역전(IoC) 및 의존성 주입(DI)

햄햄·2023년 3월 6일
0

Spring

목록 보기
2/3

출저: https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring
위 포스트를 번역 및 요약하였습니다. 의역이 포함되어 있습니다.

제어의 역전이란?

제어의 역전은 프로그램의 일부 또는 객체에 대한 제어를 컨테이너나 프레임워크로 이전하는 소프트웨어 엔지니어링 원칙이다. 객체 지향 프로그래밍에서 자주 사용된다.

사용자가 작성한 코드가 라이브러리를 호출하는 전통적인 프로그래밍과 달리, IoC를 통해 프레임워크가 프로그램의 흐름을 제어하고 사용자가 작성한 코드를 호출할 수 있게 된다. 이를 위해, 프레임워크는 동작을 내장한 추상화를 사용한다. 만약 동작을 추가하고 싶다면, 프레임워크의 클래스를 확장하거나 자체적으로 만든 클래스를 플러그인 해야 한다.

이 아키텍처의 장점은 다음과 같다.

  • 작업의 실행과 구현을 분리할 수 있다.
  • 서로 다른 구현 간에 쉽게 전환할 수 있다.
  • 프로그램의 모듈성을 향상한다.
  • 컴포넌트를 격리하거나 의존성을 모킹하고, 컴포넌트가 약속(계약)을 통해 상호작용할 수 있게 함으로써 테스트가 훨씬 쉬워진다.

제어의 역전은 다음과 같은 메커니즘을 통해 달성할 수 있다: 전략 패턴, 서비스 로케이터 패턴, 팩토리 패턴, 의존성 주입(DI)

의존성 주입이란?

의존성 주입은 IoC를 구현하는 데 사용할 수 있는 패턴으로, 객체의 의존성을 설정하는 제어가 역전되는 것이다.

객체를 다른 객체와 연결하거나, 다른 객체에 객체를 "주입(injecting)"하는 작업은 객체 자체가 아닌 따로 조립하는 곳에서 수행한다.
전통적인 프로그래밍에서는 다음과 같이 객체 의존성을 생성한다.

public class Store {
    private Item item;
 
    public Store() {
        item = new ItemImpl1();    
    }
}

위의 예제에서는 Item 인터페이스를 Store 클래스 자체 내에서 인스턴스화 해야한다.

DI를 사용하면 우리가 어떤 Item을 원하는지 구현을 구체화하지 않고 다음과 같이 작성할 수 있다.

public class Store {
    private Item item;
    public Store(Item item) {
        this.item = item;
    }
}

스프링 IoC 컨테이너

IoC 컨테이너는 IoC를 구현하는 프레임워크의 공통적인 특징이다.

스프링 프레임워크에서는 ApplicationContext라는 인터페이스가 IoC 컨테이너를 나타낸다. 스프링 컨테이너는 빈(bean)이라고 하는 객체를 인스턴스화하고, 구성하고, 조립하고, 수명 주기를 관리하는 역할을 한다.

스프링 프레임워크는 ApplicationContext 인터페이스에 대한 다양한 구현을 제공한다. 예를 들면 독립형 어플리케이션을 위한 ClassPathXmlApplicationContextFileSystemXmlApplicationContext, 그리고 웹 어플리케이션을 위한 WebApplicationContext가 있다.

컨테이너는 빈을 조립하기 위해 XML 또는 어노테이션 형태의 구성 메타데이터를 사용한다.

다음은 컨테이너를 수동으로 인스턴스화하는 한가지 방법이다.

ApplicationContext context
  = new ClassPathXmlApplicationContext("applicationContext.xml");

위 예제에서 item 속성을 설정하기 위해 메타데이터를 사용할 수 있다. 그러면 컨테이너가 이 메타데이터를 읽고 런타임에 빈을 조립한다.

스프링의 의존성 주입은 생성자, setter, 필드를 통해 수행할 수 있다.

생성자 기반 의존성 주입

생성자 기반의 의존성 주입의 경우, 컨테이너는 주입하려는 의존성을 나타내는 인자와 함께 생성자를 호출한다.

스프링은 각 인자를 주로 타입별로 리졸브하고, 그 다음 속성의 이름과 인덱스를 사용하여 명확하게 구분한다. 어노테이션을 사용하여 빈과 그 의존성을 구성하는 방법을 살펴보자.

@Configuration
public class AppConfig {

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

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

@Configuration 어노테이션은 해당 클래스가 빈을 정의하는 소스임을 나타낸다. 이를 여러 구성 클래스에 추가할 수도 있다.

메서드에는 @Bean 어노테이션을 사용하여 빈을 정의한다. 이름을 따로 지정하지 않으면 기본적으로 메서드 이름이 빈 이름이 된다.
싱글톤 스코프(디폴트)에서의 빈의 경우, 스프링은 먼저 캐싱된 빈 인스턴스가 존재하는지 확인하고 존재하지 않는 경우에만 새 인스턴스를 생성한다. 프로토타입 스코프를 사용하는 경우 컨테이너는 메서드를 호출할 때마다 새 빈 인스턴스를 반환한다.

setter 기반의 의존성 주입

setter 기반 DI의 경우, 컨테이너는 인자가 없는 생성자 혹은 인자가 없는 정적 팩토리 메서드를 호출하여 빈을 인스턴스화 한 다음 클래스의 setter 메서드를 호출한다. 어노테이션을 사용해 구성을 만들어보자.

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

동일한 빈에 대해 생성자 기반과 setter 기반 주입 유형을 결합할 수 있다. 스프링 문서에서는 필수 의존성에는 생성자 기반 주입을, 선택적 의존성에는 setter 기반 주입을 사용할 것을 권장한다.

필드 기반 의존성 주입

필드 기반 DI의 경우, 의존성을 @Autowired 어노테이션으로 표시하여 주입할 수 있다.

public class Store {
    @Autowired
    private Item item; 
}

Store 객체를 생성하는 동안 Item 빈을 주입할 생성자 또는 setter 메서드가 없는 경우 컨테이너는 리플렉션을 사용하여 Item을 Store에 주입한다.

이 방식은 더 간단하고 깔끔해 보일 수 있지만 다음과 같은 단점들이 있으므로 사용하지 않는 것이 좋다.

  • 이 방법은 리플렉션을 사용하여 의존성을 주입하므로 생성자 기반 또는 setter 기반 주입보다 비용이 더 많이 든다.
  • 이 접근 방식을 사용하면 여러 의존성을 계속 추가하기가 정말 쉽다. 생성자 주입을 사용하는 경우 인자가 여러 개 있으면 클래스가 두 가지 이상의 작업을 수행하는 것으로 간주되어 단일 책임 원칙에 위배될 수 있다.

Autowiring Dependencies

와이어링을 통해 스프링 컨테이너는 정의된 빈들을 검사함으로써 협력하는 빈들 사이의 의존성을 자동으로 리졸브할 수 있다.

오토와이어링 모드는 네 가지가 있다.

  • no: 기본값. 빈이 오토와이어링 되지 않으며 의존성의 이름을 명시적으로 지정해야 한다.
  • byName: 프로퍼티 이름을 기반으로 오토와이어링이 수행된다. 그러므로 스프링은 세팅될 프로퍼티와 이름이 같은 빈을 찾는다.
  • byType: byName과 유사한데, 프로퍼티의 타입에 기반한다. 스프링은 세팅될 프로퍼티와 동일한 타입의 빈을 찾는다. 해당 타입의 빈이 두개 이상 있는 경우 프레임워크는 예외를 던진다.
  • constructor: 생성자 인자들을 기반으로 오토와이어링이 수행된다. 스프링은 생성자 인자들과 동일한 타입의 빈을 찾는다.

위에서 정의한 item1 빈을 byType으로 store 빈에 오토와이어링 해보자.

@Bean(autowire = Autowire.BY_TYPE)
public class Store {
    
    private Item item;

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

타입 별 오토와이어링은 @Autowired 어노테이션을 사용해 빈을 주입할 수도 있다.

public class Store {
    
    @Autowired
    private Item item;
}

동일한 타입의 빈이 두개 이상 있는 경우, @Qualifier 어노테이션을 사용해 빈을 이름으로 참조할 수 있다.

public class Store {
    
    @Autowired
    @Qualifier("item1")
    private Item item;
}

초기화 지연 빈

기본적으로 컨테이너는 초기화 중에 모든 싱글톤 빈을 생성하고 구성한다. 이를 방지하려면 빈 구성에서 lazy-init 속성을 true로 설정할 수 있다.

<bean id="item1" class="org.baeldung.store.ItemImpl1" lazy-init="true" />

이제 item1 빈은 시작 시점이 아니라 처음 요청될 때 초기화된다. 이 방법은 초기화 시간이 더 빨라진다는 장점이 있지만, 애플리케이션이 이미 실행된 후 몇 시간 또는 며칠이 지나 빈에 요청이 갈 때까지 구성 오류를 발견하지 못한다는 단점이 있다.

profile
@Ktown4u 개발자

0개의 댓글