Spring Bean

Jemin·2023년 10월 4일
0

백엔드

목록 보기
15/21
post-thumbnail

Spring Bean

Spring Framework에서 Bean은 Spring 컨테이너에 의해 관리되는 객체를 가리킨다. Bean은 Spring 애플리케이션의 핵심 구성 요소 중 하나로, 이를 통해 애플리케이션의 객체들을 생성하고 관리할 수 있다.

Spring Container

스프링 컨테이너는 스프링 빈의 생명 주기를 관리하며, 생성된 스프링 빈들에게 추가적인 기능을 제공하는 역할을 한다. IoC와 DI의 원리가 스프링 컨테이너에 적용된다.

개발자는 new 연산자, 인터페이스 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸하지만, 스프링 컨테이너를 사용하면 해당 역할을 대신해 준다. 즉, 제어 흐름을 외부에서 관리하게 된다. 또한, 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 만들어 준다.

Spring Container 생성

스프링 컨테이너를 생성하는 방법으로는 두 가지 주요한 방법이 있다.

XML 기반 설정

이 방법은 스프링 컨테이너 설정을 XML 파일로 정의하는 방법이다. 주로 과거에 많이 사용되었으며 아직도 많은 프로젝트에서 사용하고 있다. 스프링 설정 파일로 applicationContext.xml과 같은 이름을 사용하는 것이 일반적이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!--  AppConfig.java 파일과 동일하다  -->
    <bean id="memberService" class = "hello.core.member.MemberServiceImpl" >
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />

    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>

    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />

</beans>

Java 코드에서 스프링 컨테이너를 생성하는 방법은 다음과 같다

package hello.core.xml;

import static org.assertj.core.api.Assertions.assertThat;

public class XmlAppContext {

    @Test
    void xmlAppContext() {
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

Java 기반 설정

이 방법은 스프링 어노테이션을 사용하여 자바 클래스로 스프링 컨테이너 설정을 정의하는 방법이다. 주로 최근에는 이 방법이 더 많이 사용되고 있다.

package hello.core;

@Configuration // 스프링 설정 정보는 항상 @Configuration을 사용하자
public class AppConfig { // 객체의 생성과 연결 담당

    @Bean
    public MemberRepository memberRepository() { // 회원 저장소
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() { // 할인 정책
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() { // 회원 서비스
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() { // 주문 서비스
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

Java 코드에서 스프링 컨테이너를 생성하는 방법은 다음과 같다

package hello.core;

public class MemberApp {
    public static void main(String[] args) {

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); // 스프링 컨테이너에 생성
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class); // 스프링 컨테이너에서 memberService라는 Bean을 꺼내온다

        Member member = new Member(1L, "memberA", Grade.VIP); // 회원 객체 생성
        memberService.join(member); // 회원 가입

        Member findmember = memberService.findMember(1L);// ID를 통해 검색
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findmember.getName());
    }
}

두 가지 방법 모두 스프링 컨테이너를 생성하고 설정 파일 또는 설정 클래스를 기반으로 빈을 관리하며, 필요한 빈을 얻어와서 사용할 수 있게 한다. 선택한 방법에 따라 스프링 프로젝트를 구성하고 스프링 컨테이너를 생성할 수 있다.

컨테이너에 등록된 빈 조회

모든 빈 조회하기

package hello.core.beanfind;

public class ApplicationContextInfoTest {
    // AnnotationConfigApplicationContext: 스프링 컨테이너를 생성하는 클래스다
    // AppConfig.class는 스프링 빈들을 구성하는 정보가 포함되어 있다
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 모든 빈의 이름들 배열로 가져오기
        for (String beanDefinitionName : beanDefinitionNames) { // iter를 작성하면 자동으로 배열에 대한 for문이 나옴
            Object bean = ac.getBean(beanDefinitionName); // 빈 이름으로 빈 객체를 가져온다
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); // 빈의 정보 가져오기

            // Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
            // Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { // 직접 빈을 등록한 경우에만 출력
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " object = " + bean);
            }
        }
    }
}

BeanDefinition.ROLE_APPLICATION과 비교하여 개발자가 직접 등록한 빈만 확인할 수 있다.

빈 조회 - 기본

package hello.core.beanfind;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

public class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); // memberService 객체가 MemberService.class의 인스턴스인지 확인
    }

    @Test
    @DisplayName("이름 없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class); // 이름을 제거하고 타입으로만 조회한다
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); // memberService 객체가 MemberService.class의 인스턴스인지 확인
    }

    @Test
    @DisplayName("구체 타입으로 조회") // 구체 타입으로 조회하면 유연성이 떨어진다 (같은 타입이 있을 수 있다)
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class); // 구체 타입을 넘겨준다
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class); // memberService 객체가 MemberService.class의 인스턴스인지 확인
    }

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        // xxxxx라는 이름으로 빈을 가져오려 했을 때 Exception이 발생해야 성공
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxxx", MemberService.class));
    }
}

각각의 빈은 빈의 이름, 타입, 구체 타입으로 조회할 수 있다.

빈 조회 - 타입 중복

package hello.core.beanfind;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() {
        // 고유한 빈이 아니라는 예외처리
        assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName() {
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class); // 동일한 타입의 빈들을 Map으로 받는다.
        for (String key : beansOfType.keySet()) { // Map의 key를 순회
            System.out.println("key = " + key + " value = " + beansOfType.get(key)); // key값과 key값으로 가져온 bean
        }
        System.out.println("beansOfType = " + beansOfType);
        assertThat(beansOfType.size()).isEqualTo(2);
    }

    @Configuration
    static class SameBeanConfig {
        @Bean
        public MemberRepository memberRepository1() {
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2() {
            return new MemoryMemberRepository();
        }
    }
}

동일한 타입의 빈이 두 개 이상 존재할때 타입으로 조회를 하게 된다면 오류가 발생한다. 이 경우에는 빈 이름을 지정하여 조회하면 된다.

동일한 타입의 빈을 모두 조회하고 싶다면 getBeansOfType을 사용하여 같은 타입의 모든 빈을 Map으로 반환받을 수 있다.

빈 조회 - 상속 관계

package hello.core.beanfind;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

부모 타입으로 조회할 경우 동일한 타입이므로 중복 오류가 발생한다. 이 경우에도 위와 동일하게 이름을 지정하여 조회할 수 있다.

모두 조회하고 싶다면 getBeansOfType를 사용하여 Map으로 받아 조회할 수 있다.

빈 메타 정보 조회

package hello.core.beandefinition;

public class BeanDefinitionTest { // 빈 메타 정보 확인하기
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 빈 이름을 배열로 받아오기
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); // 빈 정보 가져오기

            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { // 직접 생성한 빈만 출력한다
                System.out.println("beanDefinitionName = " + beanDefinitionName +
                        " beanDefinition = " + beanDefinition);
            }
        }
    }
}

getBeanDefinitionNames를 사용하여 문자열 배열로 받아와서 조회할 수 있다. 이를 통해 각각의 빈의 각종 메타 정보를 확인할 수 있다.

@Configuration과 싱글톤

@Configuration@Bean에 추가 설정을 줘서 싱글톤으로 만들지 않는 이상 무조건 빈에 대해 싱글톤을 보장한다.

싱글톤 패턴이란

package hello.core;

@Configuration // 스프링 설정 정보는 항상 @Configuration을 사용하자
public class AppConfig { // 객체의 생성과 연결 담당

    // @Bean memberService -> new MemoryMemberRepository()
    // @Bean orderService -> new MemoryMemberRepository()
    // 둘다 같은 MemoryMemberRepository()를 생성하는 것으로 보이는데 이러면 싱글톤이 깨지는 것 처럼 보인다
    // ConfigurationSingletonTest 파일로 확인해보자

    @Bean
    public MemberRepository memberRepository() { // 회원 저장소
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() { // 할인 정책
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() { // 회원 서비스
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() { // 주문 서비스
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

}

주석에 설명한 것과 같이 MemberService와 OrderService를 빈으로 등록할 때 모두 memberRepository() 메서드를 호출하는 것을 알 수 있다. 결과적으로 각각 다른 2개의 MemoryMemberRepository가 생성되어 싱글톤이 깨진다고 생각할 수 있다.

하지만 @Configuration은 클래스의 바이트 코드를 조작하는 라이브러리인 CGLIB를 사용하여 싱글톤을 보장한다. CGLIB는 프록시 객체의 일종으로 AppConfig가 빈으로 등록될 때, AppConfig 대신 AppConfig 클래스를 상속 받은 AppConfig$CGLIB 형태의 다른 클래스가 등록된다. 이 덕분에 싱글톤이 보장되는 것이다.

위와 같이 이름은 appConfig가 되고, 실제 등록되는 스프링 빈은 CGLIB 클래스의 인스턴스가 등록된다. CGLIB는 대강 아래와 같이 구현이 되어 있다고 생각하면 편하다.

@Bean
public MemberRepository memberRepository() {
    if(memorymemberRepository가 이미 스프링 컨테이너에 등록되어있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { // 스프링 컨테이너에 없으면
        기존로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}

@Bean이 등록된 메서드마다 이미 스프링 빈이 존재하면 기존에 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

Component 스캔

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan을 설정 정보에 붙여주면 된다.

package hello.core;

@Configuration
// @Component가 붙은 클래스를 모두 찾아서 빈으로 등록한다
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class) // Configuration 이라는 어노테이션이 붙은 클래스는 제외한다
)
public class AutoAppConfig {

}

컴포넌트 스캔은 @Component가 붙은 클래스를 찾아서 자동으로 빈을 추가하는 방법이다. 클래스 위에 @Component를 붙이면 스프링이 알아서 스프링 컨테이너에 빈으로 등록한다.

@Autowired

빈을 추가했다면 의존관계를 주입해야 하는데 이 때 사용하는 방법이 @Autowired 어노테이션이다. 스프링 빈으로 등록된 MemberRepository를 찾아서 자동으로 의존관계를 주입시켜준다.

getBean(MemberRepository.class)와 동일하다고 이해하면 된다. 생성자 파라미터가 많아도 다 찾아서 자동으로 주입해준다.

package hello.core.member;

@Component
public class MemberServiceImpl implements MemberService{ // 회원 서비스 구현체

    private final MemberRepository memberRepository;

    @Autowired // 자동 의존관계 주입
    public MemberServiceImpl(MemberRepository memberRepository) { // 생성자
        this.memberRepository = memberRepository; // 의존성 주입
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

Component 등록

@ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명으로 사용하되 맨 앞글자만 소문자를 사용한다.

  • 빈 이름 기본 전략: MemberServiceImpl 클래스 -> memberServiceImpl

  • 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶다면 @Component("memberService2") 이런 식으로 이름을 부여하면 된다.

탐색할 패키지의 시작 위치 지정

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

basePackages를 사용해 탐색할 패키지의 시작 위치를 지정한다.

package hello.core;

@Configuration
@ComponentScan(
        basePackages = "hello.core.member", // 탐색할 패키지의 시작 위치 지정
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

지정하지 않는다면 해당 파일이 존재하는 위치부터 탐색을 시작한다. 시작 위치를 지정하지 않는다면 현재 파일의 위치인 hello.core 부터 탐색을 시작한다. 그렇기 때문에 프로젝트 시작 루트 위치에 생성하는 것이 관례다.

스프링 부트를 사용하면 @ComponentScan을 사용하지 않아도 부트에서 자동으로 스캔한다.

Component Scan 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함된다.

  • @Component: 컴포넌트 스캔에 사용

  • @Controller: 스프링 MVC 컨트롤러에 사용

  • @Service: 스프링 비즈니스 로직에서 사용

  • @Repository: 스프링 데이터 접근 계층에서 사용

  • @Configuration: 스프링 설정 정보에서 사용

해당 클래스들의 소스 코드를 보면 @Component를 포함하고 있는 것을 알 수 있다.

Component Scan Filter

includeFilters, excludFilters를 사용해 컴포넌트 스캔에서 제외할 대상과 추가할 대상을 지정해줄 수 있다.

@ComponentScan(
            // type = FilterType.ANNOTATION: 어노테이션과 관련된 필터
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )

FilterType 옵션

FilterType은 5가지 옵션이 있다.

  • ANNOTATION: 기본값, 어노테이션을 인식해서 동작한다.

  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.

  • ASPECTJ: AspectJ 패턴 사용

  • REGEX: 정규 표현식

  • CUSTOM: TypeFilter 라는 인터페이스를 구현해서 처리

스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데,
옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.

중복 등록과 충돌

컴포넌트 스캔에 같은 빈 이름을 등록해서 충돌하는 경우가 있다.

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이름이 같은 경우 스프링은 오류를 발생시킨다.

수동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 등록된 빈과 수동으로 등록한 빈이 이름이 같다면 수동으로 등록한 빈의 우선순위가 높기 때문에 수동으로 등록한 빈이 우선권을 가진다.

수동 빈이 자동 빈을 오버라이딩 해버린다. 수동 빈 등록시 로그에 남는다.
하지만 현실에서는 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다. 이런 경우 아주 잡기 어려운 버그가 만들어진다.

그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

의존관계 주입 방법

의존관계 주입은 크게 4가지 방법이 있다.

  • 생성자 주입

  • 수정자 주입(setter 주입)

  • 필드 주입

  • 일반 메서드 주입

생성자 주입

이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.

  • 불변, 필수 의존관계에 사용한다.

package hello.core.order;

@Component
public class OrderServiceImpl implements OrderService{ // 주문 서비스 구현체

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired // 생성자가 하나일 때에는 @Autowired를 생략해도 자동 주입 된다. (스프링 빈에만 해당)
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { // 생성자
        this.memberRepository = memberRepository; // 의존성 주입
        this.discountPolicy = discountPolicy; // 의존성 주입
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId); // 회원 찾기
        int discountPrice = discountPolicy.discount(member, itemPrice);// 할인 가격

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. (스프링 빈에만 해당)

수정자 주입

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

  • 선택, 변경 가능성이 있는 의존관계에 사용한다.

  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

package hello.core.order;

@Component
public class OrderServiceImpl implements OrderService{ // 주문 서비스 구현체

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        System.out.println("memberRepository = " + memberRepository);
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        System.out.println("discountPolicy = " + discountPolicy);
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId); // 회원 찾기
        int discountPrice = discountPolicy.discount(member, itemPrice);// 할인 가격

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

자바빈 프로퍼티 규약은 필드의 값을 직접 변경하지 않고, setget을 사용하여 값을 읽거나 수정하는 규칙이다.

필드 주입

이름 그대로 필드에 바로 주입하는 방법이다.

  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.

  • DI 프레임워크가 없으면 아무것도 할 수 없다.

  • 사용하지 않는 것을 권장한다.

    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드에서 사용된다.

    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용된다.

package hello.core.order;

@Component
public class OrderServiceImpl implements OrderService{ // 주문 서비스 구현체

    @Autowired private MemberRepository memberRepository;
    @Autowired private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId); // 회원 찾기
        int discountPrice = discountPolicy.discount(member, itemPrice);// 할인 가격

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

일반 메서드 주입

일반 메서드를 통해서 주입 받을 수 있다.

  • 한번에 여러 필드를 주입 받을 수 있다.

  • 일반적으로 잘 사용하지 않는다.

 	@Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository; // 의존성 주입
        this.discountPolicy = discountPolicy; // 의존성 주입
    }

의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 그런데 @Autowired만 사용하면 required 옵션의 기본 값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

  • @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨

  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.

  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

package hello.core.autowired;

import java.util.Optional;

public class AutowiredTest { // 빈이 없어도 동작해야 하는 경우

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    @Component
    static class TestBean {
        // Member는 스프링 빈이 아니다

        @Autowired(required = false) // 빈 주입 필수 유무 옵션
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1); // 의존관계가 없으므로 메서드 자체가 호출되지 않는다
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2) { // 주입할 대상이 없다면 null 입력
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) { // 주입할 대상이 없다면 Optional.empty 입력
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

자동 빈 등록 시 타입 중복 문제

@Autowired는 타입으로 조회하기 때문에, 선택된 빈이 2개 이상일 때 문제가 발생한다.

이때 하위 타입으로 지정할 수 도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.

스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

// 기존 코드
@Autowired
private DiscountPolicy discountPolicy
// 필드 명을 빈 이름으로 변경
@Autowired
private DiscountPolicy rateDiscountPolicy

필드 명이 rateDiscountPolicy 이므로 정상 주입된다. 필드 명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

  1. 타입 매칭

  2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파리미터 명으로 빈 이름 매칭

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

// 빈 등록시 @Qualifier를 붙여준다
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
	@Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  1. @Qualifier끼리 매칭

  2. 빈 이름 매칭

  3. NoSuchBeanDefinitionException 예외 발생

만약 @QualifiermainDiscountPolicy를 못찾는다면 같은 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier@Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다.

햇갈리는 행동은 하지 말자.

@Primary

@Primary는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{}

@Primary@Qualifier 중에 어떤 것을 사용하면 좋을 지 고민이 될 것이다. @Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여주어야 한다는 점이다. 반면에 @Primary를 사용하면 모든 코드에 작성할 필요가 없다.

@Primary, @Qualifier 활용

코드에서 자주 사용되는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자.

메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

우선순위

@Primary는 기본값 처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선순위를 가져갈까?

스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 @Qualifier의 우선순위가 높다.

참고
[Spring] Spring Bean 총 정리

profile
경험은 일어난 무엇이 아니라, 그 일어난 일로 무엇을 하느냐이다.

0개의 댓글