[Section 2] DI, Spring Container, Bean

Kim·2022년 10월 13일
0

Boot Camp

목록 보기
29/64
post-thumbnail

지난번에 배운 DI 실습을 해보았는데, 코드가 정말 많다. 최대한 요약해서 올려봐야지..

DI (Dependency Injection)

DI란 IoC라는 원칙을 구현하기 위해 사용되는 방법 중 하나다.
Dependency Injection는 의존성 주입이라 해석할 수 있다.

public class MemberService {
    private final MemberRepository memberRepository = new MemberRepository();

    public void createMember(Member member) {
        memberRepository.postMember(member);
    }

    public Member getMember(Long memberId) {
        return memberRepository.getMember(memberId);
    }

    public void deleteMember(Long memberId) {
        memberRepository.deleteMember(memberId);
    }
}

위 코드는 MemberService 라는 객체에서 MemberRepository 라는 객체에 의존성을 가지고 있다. 아래 구현한 3개의 메서드들은 모두 MemberRepository 객체에서 구현한 메서드를 사용하고 있다.

MemberRepository 객체를 다른 객체로 교체해야 하는 경우 어떻게 해야할까? MockRepository 객체로 교체해보자.

private final MemberRepository memberRepository = new MemberRepository();
private final MemberRepository memberRepository = new MockRepository();

첫 번째 코드를 두 번째 코드처럼 MemberRepositoryMockRepository로 바꾸면 해결될 수도 있다. 하지만, 이렇게 객체를 변경하면 아래에 구현한 메서들이 MockRepository 객체에 존재하지 않는다면, 해당 코드도 모두 변경해야 하는 문제가 생긴다. 객체간의 관계가 변경될 때마다 직접 해당 코드를 찾아 수정하고, 문제점이 없는지 살펴보는 과정을 거쳐야 한다.

의존성 주입 적용하기

위에 발생하는 문제점들은 의존성 주입을 통해 해결할 수 있다.

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    public void createMember(Member member) {
        memberRepository.postMember(member);
    }

    public Member getMember(Long memberId) {
        return memberRepository.getMember(memberId);
    }

    public void deleteMember(Long memberId) {
        memberRepository.deleteMember(memberId);
    }
}

위와 같이 생성자를 통해 의존성을 주입받음으로써 객체가 생성되는 순간, 의존관계를 설정할 수 있다.
다른 방식으로도 의존성 주입이 가능하지만, 일반적으로 생성자를 통한 의존관계 주입을 사용한다. 생성자를 통해 의존성을 주입하게 되면, 스프링에서 의존성 주입을 도와준다. 스프링에서 공식적으로 추천하는 방법

MemberService와 같이 CoffeeService 객체도 생성자 주입을 통한 코드로 변경한다.
이후 DependencyConfig.java 파일을 생성하고 아래와 같이 코드를 입력한다.

public class DependencyConfig {

  public MemberService memberService() {
    return new MemberService(memberRepository());
  }

  public MemberRepository memberRepository() {
    return new MemberRepository();
  }

  public CoffeeService coffeeService() {
    return new CoffeeService(coffeeRepository());
  }

  public CoffeeRepository coffeeRepository() {
    return new CoffeeRepository();
  }
}

역할을 명확하게 구분해 작성하면, DependencyConfig.java에서 구현 부분만 수정하면 된다.

MemberService와 같이 CoffeeService 입장에서는 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다. 어떤 객체가 주입될지는 DependencyConfig에서 결정하게 된다. MemberService와 같이 CoffeeService는 오로지 실행에만 집중하게 된다.
위 코드를 입력·수정하고 나면 MemberTestCoffeeTest 두 파일에서 서비스 객체를 생성하는 부분에 오류가 발생하게 된다. 해당 객체를 생성하는 코드의 수정이 필요하다.

코드를 모두 수정하면 작성했던 테스트가 정상 동작하는 것을 확인할 수 있다.
프레임워크에서 사용하는 방식이 아닌, 컨테이너를 활용해 객체의 의존 관계를 주입하는 방식이다.


Spring Container

스프링 컨테이너는 스프링 프레임워크의 핵심이다. 내부에 존재하는 애플리케이션의 Bean의 생명주기를 관리한다. Bean의 생성, 제거, 관리 등의 역할을 담당한다.

Application Context

ApplicationContext를 스프링 컨테이너라 하며 인터페이스로 구현되어 있다. ApplicationContextBeanFactory 인터페이스의 하위 인터페이스이다. 즉, ApplicationContextBeanFactory에 부가기능을 추가한 것이다.

스프링 컨테이너는 XML, 애너테이션 기반의 자바 설정 클래스로 만들 수 있다. 이전에는 개발자가 XML을 통해 모두 설정했으나, Spring Boot의 등장으로 거의 사용하지 않게 되었다.
스프링 컨테이너를 통해 원하는 만큼 많은 객체를 가질 수 있으며, 서로 다른 Bean을 연결해 애플리케이션의 Bean을 연결하는 역할을 한다.

스프링 컨테이너가 해결 방법이 되는 이유

기존의 방식에서는 변경 사항이 생기면 수작업으로 수정을 해야 했었다. 서비스의 코드가 거대해질 경우, 의존도는 높아질 것이며 이에 따른 코드의 변경도 많이 필요할 것이다. 하지만, 스프링 컨테이너를 사용하면서 구현 클래스에 있는 의존을 제거함으로써 인터페이스에만 의존하도록 설계할 수 있다.

스프링 컨테이너의 생성 과정

Annotation-based Container Configuration(애너테이션 기반 컨테이너 구성)

스프링 컨테이너는 Configuration Metadata를 사용한다.
피라미너로 넘어온 설정 클래스 정보를 사용해 Spring Bean을 등록한다.
new AnnotationConfigApplicationContext(구성정보.class)로 스프링에 있는 @Bean의 메서드를 등록한다.
아래는 스프링 컨테이너를 생성하는 코드 예시다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(DependencyConfig.class);

ApplicationContext 인터페이스 구현체 확인하기

스프링 컨테이너를 만드는 다양한 방법은 ApplicationContext 인터페이스의 구현체다.
Windows 기준으로 Ctrl + N 키를 누르고 ApplicationContext를 검색하면, ApplicationContext 인터페이스를 구현한 하위 클래스들을 확인할 수 있다.
Max은 Cmd + O 키를 누르면 될 것이다.

스프링 컨테이너의 종류

BeanFactory

스프링 컨테이너의 최상위 인터페이스다. BeanFactory는 Bean을 등록하고 생성하고 조회하고 돌려주는 등의 Bean을 관리하는 역할을 한다.
getBean() 메서드를 통해 Bean을 인스턴스화할 수 있다. @Bean이 붙은 메서드 명을 Spring Bean의 이름으로 사용해 Bean을 등록한다.

ApplicationConetext

BeanFactory의 기능을 상속받아 제공한다. Bean을 관리하고 검색하는 기능을 BeanFactory가 제공하며, 그 외의 부가기능을 제공한다.

  • MessageSource: 메세지 다국화를 위한 인터페이스
  • EnvironmentCapable: 개발, 운영 등 환경변수 등으로 나누어 처리하고, 애플리케이션 구동 시 필요한 정보들을 관리하기 위한 인터페이스
  • ApplicationEventPublisher: 이벤트 관련 기능을 제공하는 인터페이스
  • ResourceLoader: 파일, 클래스 패스, 외부 등 리소스를 편리하게 조회

컨테이너 인스턴스화

ApplicationContext 생성자에 제공된 위치 경로 혹은 컨테이너가 로컬 파일 시스템, Java CLASSPATH 등과 같은 다양한 외부 리소스로부터 구성 메타데이터를 로드할 수 있도록 하는 리소스 문자열이다.

// Annotation
ApplicationContext context = new AnnotationConfigApplicationContext(DependencyConfig.class);

// XML
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

Bean

스프링 컨테이너에 의해 관리되는 재사용 소프트웨어 컴포넌트다.
Bean은 인스턴스화된 객체를 의미하며, 스프링 컨테이너에 등록된 객체를 Spring Bean이라 한다.
@Bean이 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록한다.

ApplicationContext 사용하여 bean 정의를 읽고 액세스할 수 있다.

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("memberRepository", memberRepository.class);

// use configured instance
List<String> userList = service.getUsernameList();

스프링은 다양한 설정 형식을 BeanDefinition이라는 추상화 덕분에 지원할 수 있다.
Bean은 BeanDefinition(Bean 설정 메타정보)으로 정의되고 BeanDefinition에 따라 활용하는 방법이 달라진다.


Bean Scope

Bean Definition은 레시피다. Bean Definition을 만들 때 Bean Definition에 정의된 클래스들의 실제 인스턴스를 만들기 위한 레시피를 만든다.

특정 bean 정의에서 생성된 개체에 연결할 다양한 의존성 및 구성 값뿐만 아니라, 특정 bean 정의에서 생성된 개체의 범위도 제어할 수 있다.
Spring Framework는 6개의 범위를 지원하며, 그 중 4개는 ApplicationContext를 사용하는 경우에만 사용할 수 있다.
bean은 여러 범위 중 하나에 배치되도록 정의할 수 있다.
구성을 통해 생성하는 개체의 범위를 선택할 수 있기 때문에 강력하고 유연하며, 사용자 정의 범위를 생성할 수도 있습니다.

Singleton Scope

싱글톤 스코프는 클래스의 인스턴스가 딱 하나만 생성되는 것을 보장하는 디자인 패턴이다.
스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될 때까지 유지된다. 싱클톤 Bean의 하나의 공유 인스턴스만 관리하게 된다. private 생성자를 사용햐 외부에서 임의로 new를 사용하지 못하게 막는다.
해당 Bean Definition와 일치하는 ID 혹은 ID를 가진 Bean에 대한 모든 요청은 스프링 컨테이너에서 해당 특정 Bean 인스턴스를 반환한다.
스프링 컨테이너가 종료되면 소명 메서드도 자동으로 실행된다.

정리하자면,
싱글톤은 해당 Bean의 인스턴스를 오직 하나만 생성해서 사용하는 것을 의미한다.
단일 인스턴스는 싱글톤 Bean의 캐시에 저장된다.
이름이 정해진 Bean에 대한 모든 요청과 참조는 캐시된 개체를 반환한다. 싱글톤 스코프의 Bean은 여러 번 호출해도 모두 같은 인스턴스 참조 주소값을 갖는다.

핵심 point

Bean 하나에 하나씩 메타 정보가 생성되며 스프링 컨테이너는 이러한 메타 정보를 기반으로 Spring Bean을 생성한다.


참고자료

Core Technologies

[Spring] 스프링 컨테이너(Spring Container)
[Spring] 스프링 컨테이너(ApplicationContext)


이미지 출처

Spring

0개의 댓글