반응형

# 의존관계 자동 주입

## 다양한 의존관계 주입 방법

1. 생성자 주입

  • 생성자를 통해 의존 관계를 주입하는 방법.
  • 특징
생성자 호출 시점에서 딱 1번만 호출되는 것을 보장한다.

불변이면서 필수인 의존관계에 사용.
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}
  •  생성자가 1개만 존재할 경우 @Autowired 생략 가능 생략해도, 자동 주입 됨. (스프링 빈에만 해당)

2. 수정자 주입 (= setter 주입)

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방법.
  • 특징 
선택, 변경 가능성이 있는 의존관계에 사용

자바 빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;
 
	@Autowired
	public void setMemberRepository(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}
 
	@Autowired
	public void setDiscountPolicy(DiscountPolicy discountPolicy) {
		this.discountPolicy = discountPolicy;
	}
}
  • required = false 옵션으로 선택적 의존관계 주입 가능. (@Autowired 의 기본 동작은 주입할 대상이 없으면 오류 발생. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.)
@Autowired(required = false)
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
  • 자바빈 프로퍼티 규약 : 자바빈 프로퍼티, 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙. 

3. 필드 주입

  • 필드에 바로 주입하는 방법.
  • 특징
코드가 간결하지만, 외부에서 변경이 불가능해 테스트 하기 힘들다는 치명적인 단점 존재.

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

사용하지 말자!
	- 애플리케이션의 실제 코드와 관계 없는 테스트 코드
	- 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용
@Component
public class OrderServiceImpl implements OrderService {
	@Autowired
	private MemberRepository memberRepository;
 
	@Autowired
	private DiscountPolicy discountPolicy;
}

4. 일반 메서드 주입 (일반적으로 잘 사용하지 않음)

  • 일반 메서드를 통해 주입 받을 수 있다.
  • 특징
한번에 여러 필드를 주입 받을 수 있다.

일반적으로 잘 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
	private MemberRepository memberRepository;
	private DiscountPolicy discountPolicy;
 
	@Autowired
	public void init(MemberRepository memberRepository, DiscountPolicy iscountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}
  • 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
  • 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

## 옵션 처리

  • 주입할 스프링 빈이 없어도 동작해야 할 때가 존재.
  • @Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
@Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨

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

Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
package hello.core.autowired;

import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;

import java.util.Optional;

public class AutowiredTest {

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

    static class TestBean {

        //호출 안됨
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        // null 호출
        @Autowired
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }

        // Optional.empty 호출
        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}
  • Member는 스프링 빈이 아니다.
  • setNoBean1() 은 @Autowired(required=false) 이므로 호출 자체가 안된다.
  • @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. (예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.)

## 생성자 주입을 선택해야 하는 이유.

  • 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장.

1. 불변

대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 
오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.)

수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.

누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.

생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할수 있다.

2. 누락

  • 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 다음과 같이 수정자 의존관계인 경우

final 키워드

  • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.

정리

  • 생성자 주입 방식을 선택하는 이유는 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우 수정자 주입 방식을 옵션으로 부여하면 된다.
  • 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 항상 생성자 주입을 선택!!! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택!!!
  • 필드 주입은 사용하지 않는게 좋다. (필드 주입 사용 시 테스트에서 값을 넣을 수 있는 방법이 없고, 스프링 컨테이너 없이는 테스트 조차 할 수 없게 됨) 
반응형
반응형

# 컴포넌트 스캔

## 컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 지금까지 스프링 빈 등록 시 자바 코드의 @Bean 또는 XML을 통해 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 실무에서는 등록해야 할 스프링 빈이 수십, 수백개가 되는데, 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생할 수 있다.
  • 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.
  • excludeFilters : 제외 설정 가능.
package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
            // 제외 설정
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}
  • @ComponentScan  : 컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다. (위 코드를 살펴보면 기존의 AppConfig와 다르게 @Bean으로 등록한 클래스가 하나도 없다.)
  • 컴포넌트 스캔을 사용 시 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버리기 때문에 excludeFilters 를 이용, 설정정보는 컴포넌트 스캔 대상에서 제외. 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택.
package hello.core.member;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}
package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;

@Component
public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10; // 할인율 10%

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
package hello.core.member;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired  // ac.getBean(MemberRepository.class)
    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);
    }

    // 테스트 용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @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);
    }

    // 테스트 용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
  • @Autowired : 지금까지 AppConfig에서 @Bean 으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시했다. 이제 이런 설정 정보 자체가 없기 때문에, 의존관계 주입도 이 클래스 안에서 해결해야 한다. @Autowired 는 의존관계를 자동으로 주입해준다. (@Autowired 사용 시 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.)
package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class AutoAppConfigTest {

    @Test
    void basicScan() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

    }
}
  • AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일. 설정 정보로 AutoAppConfig 클래스를 넘겨준주고, 실행 시 기존과 같이 잘 동작하는 것을 확인할 수 있다.
  • @ComponentScan
@ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.

이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
	- 빈 이름 기본 전략: MemberServiceImpl 클래스 memberServiceImpl
	- 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 
	 @Component("memberService2") 이런식으로 이름을 부여하면 된다
  • @Autowired 의존관계 자동 주입
생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.

생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.

## 탐색 위치와 기본 스캔 대상

### 탐색위치

  • basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다. 
@ComponentScan(
	basePackages = "hello.core",
}
  • 아래와 같이 여러개를 지정할 수도 있다.
basePackages = {"hello.core", "hello.service"} 	// 이렇게 여러 시작 위치를 지정할 수 있음.

basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

basePackages / basePackageClasses  를 지정하지 않을 경우 : @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

권장 사용방법 : 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것. (최근 스프링 부트도 이 방법을 기본으로 제공한다.)

프로젝트가 아래와 같은 구조라고 한다면 com.hello 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략한다.

com.hello
com.hello.serivce
com.hello.repository
  • 이렇게 하면 com.hello 를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다. 그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다.
  • 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝트 시작 루트 위치에 두는 것이 관례. (그리고 이 설정안에 @ComponentScan 이 들어있다.)
package hello.core;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CoreApplication {

   public static void main(String[] args) {
      SpringApplication.run(CoreApplication.class, args);
   }

}

### 기본 스캔 대상

  • 컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다
@Component : 컴포넌트 스캔에서 사용

@Controlller : 스프링 MVC 컨트롤러에서 사용

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

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

@Configuration : 스프링 설정 정보에서 사용
  • 애노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능이 아닌, 스프링이 지원하는 기능이다.
  • 컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행.
@Controller : 스프링 MVC 컨트롤러로 인식

@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.

@Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

@Service : @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에
있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다.

## 필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}
package hello.core.scan.filter;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

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

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        assertThrows (
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("BeanB", BeanB.class));
    }

    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {

    }
}
  • includeFilters 에 MyIncludeComponent 애노테이션을 추가, BeanA가 스프링 빈에 등록된다.
  • excludeFilters 에 MyExcludeComponent 애노테이션을 추가, BeanB는 스프링 빈에 등록되지 않는다.

### FilterType 옵션

  • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.  (ex) org.example.SomeAnnotation)
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다. (ex) org.example.SomeClass)
  • ASPECTJ: AspectJ 패턴 사용 (ex) org.example..*Service+)
  • REGEX: 정규 표현식 (ex) org\.example\.Default.*)
  • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리 (ex) org.example.MyTypeFilter)
  • @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용.

## 중복 등록과 충돌

1. 자동 빈 등록 vs 자동 빈 등록

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

2. 수동 빈 등록 vs 자동 빈 등록

package hello.core;

import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
            // 제외 설정
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 이 경우 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 해버린다.)
  • 수동 빈 등록시 아래와 같이 로그가 남는다.
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing
  • 개발자가 의도적으로 이런 결과를 만드는 경우도 있지만, 여러 설정이 꼬여 이런 결과가 만들어지는 경우가 대부분. (정말 잡기 어려운 버그가 만들어짐, 항상 잡기 어려운 버그는 애매한 버그다.) 그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본값을 바꿔버림.
  • 수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러. (스프링 부트인 CoreApplication 을 실행해보면 오류를 볼 수 있다.)
  • application.properties 에 아래와 같이 설정하면 가능.
Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true
반응형
반응형

# 싱글톤 컨테이너

## 웹 애플리케이션과 싱글톤

  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. (웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.)
  • 보통 웹 애플리케이션은 여러 고객이 동시에 요청을 한다. (고객 요청 시 마다 DI 컨테이너(AppConfig)에서 new 하여 객체를 생성하기 때문에 문제 발생.)
  • 스프링 없는 순수 DI 컨테이너
package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너.")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService1 = appConfig.memberService();

        // 2. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService2 = appConfig.memberService();

        // 참조 값 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }
}
  • 이전에 작업한 스프링 없는 순수한 DI 컨테이너(AppConfig)의 경우 요청 시 마다 객체를 생성. 
  • 이대로 진행하면 고객 트래픽 마다 객체가 생성, 소멸되어 메모리의 낭비가심하다.
  • 해결 방법은 싱글톤 패턴. (해당 객체가 1개만 생성되고 공유)

## 싱글톤 패턴

  • 싱글톤 패턴 : 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴.
  • private 생성자를 이용 외부에서임의로 new 할 수 없도록 하여, 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
package hello.core.singleton;

public class SingletonService {

    // static 영역에 객체를 1개만 생성.
    private static final SingletonService instance = new SingletonService();

    // public으로 하여 객체 인스턴스가 필요 시 해당 static 메서드로만 조회하도록 함.
    public static SingletonService getInstance() {
        return instance;
    }

    // private 생성자 : 외부에서 new 키워드를 사용한 객체 생성 불가하도록 방지.
    private SingletonService() {

    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출함.");
    }
}

1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.

2. 해당 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 해당 메서드를 호출하면 항상 같은 인스턴스를 반환.

3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아, 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너.")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService1 = appConfig.memberService();

        // 2. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService2 = appConfig.memberService();

        // 참조 값 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용.")
    void singtonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        // Same : == 비교
        // equal : 자바 equals 메서드 비교
        assertThat(singletonService1).isSameAs(singletonService2);
    }
}
  • 싱글톤 패턴 적용 시 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유하여 효율적으로 사용할 수 있다.
  • 하지만 싱글톤 패턴 사용의 문제점도 존재한다.
싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.

의존관계상 클라이언트가 구체 클래스에 의존한다. (DIP를 위반)

클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.

테스트하기 어렵다.

내부 속성을 변경하거나 초기화 하기 어렵다.

private 생성자로 자식 클래스를 만들기 어렵다.

유연성이 떨어진다.

안티패턴으로 불리기도 한다.

## 스프링 컨테이너 (=싱글톤 컨테이너)

  • 스프링 컨테이너(=싱글톤 컨테이너)는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다. 
  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.
  • 싱글톤 레지스트리 : 싱글톤 객체를 생성하고 관리하는 기능.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다. (DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다)
package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너.")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        // 1. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService1 = appConfig.memberService();

        // 2. 조회 - 호출 시 마다 객체 생성하는지 조회.
        MemberService memberService2 = appConfig.memberService();

        // 참조 값 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용.")
    void singtonServiceTest() {
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        // Same : == 비교
        // equal : 자바 equals 메서드 비교
        assertThat(singletonService1).isSameAs(singletonService2);
    }

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤.")
    void springContainer() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService1 = ac.getBean("memberService", MemberService.class);
        MemberService memberService2 = ac.getBean("memberService", MemberService.class);

        // 참조값이 다른 것 확인.
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        // memberService1 != memberService2
        assertThat(memberService1).isSameAs(memberService2);
    }
}
  • 스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아닌, 이미 만들어진 객체를 공유하여 효율적으로 재사용 가능.

## 싱글톤 방식 주의점

  • 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안되고, 무상태(stateless)로 설계해야 한다.
특정 클라이언트에 의존적인 필드가 있으면 안된다.

특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.

가급적 읽기만 가능해야 한다.

필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
package hello.core.singleton;

public class StatefulService {

    private int price; // 상태를 유지하는 필드.

    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);

        this.price = price; // 여기서 문제발생.
    }

    public int getPrice() {
        return price;
    }
}
package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(testConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자가 10000원 주문
        statefulService1.order("userA", 10000);

        // ThreadB : B 사용자가 20000원 주문
        statefulService2.order("userB", 20000);

        // ThreadA : 사용자A 가 주문 금액 조회.
        int price = statefulService1.getPrice();

        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class testConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}
  • 위 코드에서 ThreadA가 사용자A 코드를 호출, ThreadB가 사용자B 코드를 호출한다고 가정.  
  • StatefulService 의 price 필드는 '공유 필드'인데, 특정 클라이언트가 값을 변경, 사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나옴. 공유필드는 조심해야 한다. (멀티 스레드 문제)
  • 스프링 빈은 항상 무상태(stateless)로 설계해야 한다.
  • 아래와 같이 코드 변경.
package hello.core.singleton;

public class StatefulService {

//    private int price; // 상태를 유지하는 필드.

    public int order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);

//        this.price = price; // 여기서 문제발생.
        return price;
    }

    /*public int getPrice() {
        return price;
    }*/
}
package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

public class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(testConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        // ThreadA : A 사용자가 10000원 주문
        int userAPrice = statefulService1.order("userA", 10000);

        // ThreadB : B 사용자가 20000원 주문
        int userBPrice = statefulService2.order("userB", 20000);

        // ThreadA : 사용자A 가 주문 금액 조회.
//        int price = statefulService1.getPrice();

        System.out.println("price = " + userAPrice);

//        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class testConfig {

        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

## @Configuration 과 싱글톤

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // @Bean MemberService 호출 -> new MemoryMemberRepository() 호출.
    // @Bean OrderService 호출 -> new MemoryMemberRepository() 또 호출.

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();    // 추후 DB 변경 시 여기만 바뀌면 됨.
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();     // 정액 할인_추후 할인 정책 변경 시 여기만 바뀌면 됨.
        return new RateDiscountPolicy();    // 정률 할인
    }
}
  • AppConfig에서 memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다. 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다. 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    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);
    }

    // 테스트 용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    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);
    }

    // 테스트 용
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}
package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // impl에 작성한 테스트 코드 테스트 위해 구체 타입으로 꺼냄. -> 좋은 방식은 아님.
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}
  • 위 테스트 코드를 확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.
  • 아래와 같이 soutm 사용하여 호출 테스트.
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // @Bean MemberService 호출 -> new MemoryMemberRepository() 호출.
    // @Bean OrderService 호출 -> new MemoryMemberRepository() 또 호출.

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();    // 추후 DB 변경 시 여기만 바뀌면 됨.
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();     // 정액 할인_추후 할인 정책 변경 시 여기만 바뀌면 됨.
        return new RateDiscountPolicy();    // 정률 할인
    }
}

## @Configuration과 바이트코드 조작의 마법

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // @Bean MemberService 호출 -> new MemoryMemberRepository() 호출.
    // @Bean OrderService 호출 -> new MemoryMemberRepository() 또 호출.

    // 호출 (순서는 보장하지 않음)
    // 아래와 같이 호출되어야 할 것 같은데..
    // call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.memberRepository
    // call AppConfig.orderService
    // call AppConfig.memberRepository

    // 실상은 아래와 같이 호출.
    // call AppConfig.memberService
    // call AppConfig.memberRepository
    // call AppConfig.orderService

    @Bean
    public MemberService memberService() {
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();    // 추후 DB 변경 시 여기만 바뀌면 됨.
    }

    @Bean
    public OrderService orderService() {
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();     // 정액 할인_추후 할인 정책 변경 시 여기만 바뀌면 됨.
        return new RateDiscountPolicy();    // 정률 할인
    }
}
@Test
void configurationDeep() {
 ApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);
 //AppConfig도 스프링 빈으로 등록된다.
 AppConfig bean = ac.getBean(AppConfig.class);

 System.out.println("bean = " + bean.getClass());
 //출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
}
  • 순수한 클래스라면 다음과 같이 출력되어야 한다.
class hello.core.AppConfig
  • 예상과 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡하게 표시 됨.
  • 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용, AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것. (임의의 다른 클래스가 싱글톤이 보장되도록 바이트 코드를 조작해서 작성했을 것임.)
  • AppConfig@CGLIB 예상 코드
@Bean
public MemberRepository memberRepository() {

 if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
 return 스프링 컨테이너에서 찾아서 반환;
 } else { //스프링 컨테이너에 없으면
 기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
 return 반환
 }
}
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들져서 덕분에 싱글톤이 보장되는 것.

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면?

//@Configuration 삭제
public class AppConfig {
}

AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된 것을 확인할 수 있다.

AppConfig 출력 결과가아래와 같아짐.

call AppConfig.memberService
call AppConfig.memberRepository
call AppConfig.orderService
call AppConfig.memberRepository
call AppConfig.memberRepository
  • 그래서 당연히 인스턴스가 같은지 테스트 하는 코드도 실패, 각각 다 다른 MemoryMemberRepository 인스턴스를 가지고 있다.
  • 정리
@Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.

memberRepository() 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.

스프링 설정 정보는 항상 @Configuration 사용!!!
반응형
반응형

# 스프링 컨테이너와 스프링 빈

## 스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext 를 스프링 컨테이너라고 한다.
  • ApplicationContext 는 인터페이스이다. (다형성 적용)
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고 / 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • AppConfig 를 사용했던 방식이 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것.
  • 자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext )를 생성
new AnnotationConfigApplicationContext(AppConfig.class); 

이 클래스는 ApplicationContext 인터페이스의 구현체이다.
  • 스프링 컨테이너 생성 과정은 아래와 같다.
1. 스프링 컨테이너 생성.
new AnnotationConfigApplicationContext(AppConfig.class)

스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다.
여기서는 AppConfig.class 를 구성 정보로 지정했다.


2. 스프링 빈 등록.
스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 빈을 등록한다. 
(@Bean 어노테이션 붙은 메서드)
(빈 이름은 보통 메서드 이름 사용, 빈 이름을 아래와 같이 임의로 부여 할 수도 있다.)
(빈 이름 임의 부여 예 : @Bean(name="orderService12"); )
(빈 이름은 항상 다른 이름을 부여해야 한다. 만약 같은 이름으로 부여하면 해당 빈을 무시하거나 기존 빈을 덮어버릴수있음.)


3. 스프링 빈 의존관계 설정 준비


4. 스프링 빈 의존관계 설정 완료
- 스프링 빈은 설정 정보를 참고해서 의존관계 주입(DI)
- 단순히 자바 코드를 호출 한것 같지만 차이 존재.
  • 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다.
  • 그런데 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. 

## 컨테이너에 등록된 전체 빈 조회

  • 모든 빈 출력
실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.

ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.

ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
  • 애플리케이션 빈 출력하기
스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.

ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈

ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("등록된 전체 빈 출력")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        // iter + Tab 키 입력 시 반복문 자동 완성.
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " Object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();

        // iter + Tab 키 입력 시 반복문 자동 완성.
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            // Role ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " Object = " + bean);
            }

            // Role ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
            if(beanDefinition.getRole() == BeanDefinition.ROLE_INFRASTRUCTURE) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + " Object = " + bean);
            }
        }
    }
}

## 스프링 빈 조회_가장 기본적인 방법

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.junit.jupiter.api.Assertions.*;

class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
        Assertions.assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회되는게 없는 경우.")
    void findBeanByNameX() {
        //MemberService xxxx = ac.getBean("xxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxx", MemberService.class));
    }
}

## 스프링 빈 조회_동일한 타입이 2개 이상인 경우

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

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

public class ApplicationContextSameBeanFindeTest {

    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);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }

        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();
        }
    }
}

## 스프링 빈 조회_상속관계 (중요!!!)

  • 부모 타입으로 조회 시, 자식 타입도 함께 조회한다.
  • 자바 객체의 최고 부모인 Object 타입으로 조회 시, 모든 스프링 빈을 조회한다.
package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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));
        }
        System.out.println("beansOfType = " + beansOfType);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회 - Object")
    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();
        }
    }
}

## BeanFactory 와 ApplicationContext

BeanFactory (빈 관리, 조회)

스프링 컨테이너의 최상위 인터페이스.

역할 : 스프링 빈 관리, 조회

getBean() 을 제공. 
지금까지 작업하면서 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능.

ApplicationContext (BeanFactory 기능 모두 상속 + 부가기능 제공)

BeanFactory 기능을 모두 상속받아서 제공.

애플리케이션을 개발에 필요한 부가기능 제공.

1. 메시지소스를 활용한 국제화 기능
- 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력

2. 환경변수
- 로컬, 개발, 운영등을 구분해서 처리

3. 애플리케이션 이벤트
- 이벤트를 발행하고 구독하는 모델을 편리하게 지원

4. 편리한 리소스 조회
- 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
  • BeanFactory를 직접 사용할 일은 거의 없고, 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라고 한다.

## 다양한 설정 형식 지원_자바 코드, XML

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다. (자바 코드, XML, Groovy 등)
  • 과거에는 설정 정보로 XML 형식 많이 사용, 최근에는 자바 코드 형식 사용.
  • 애노테이션 기반 자바 코드 설정 사용
new AnnotationConfigApplicationContext(AppConfig.class)

AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.
  • XML 설정 사용
최근 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는다.

많은 레거시 프로젝트 들이 XML로 되어 있고, XML 사용 시 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점 존재.

GenericXmlApplicationContext 를 사용하면서 xml 설정 파일을 넘기면 된다.
  • xml 기반의 스프링 빈 설정 정보 아래와 같이 생성.
  • src/main/resources/appConfig.xml (자바 코드가 아닌건 모두 resources 하위에 두기)
<?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">

    <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>
package hello.core.xml;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

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);
    }
}
  • xml 기반의 appConfig.xml 스프링 설정 정보 / 자바 코드로 된 AppConfig.java 설정 정보를 비교해 보면 거의 비슷하다는 것을 알 수 있다. 

## 스프링 빈 설정 메타 정보_BeanDefinition

  • 스프링이 다양한 설정 형식을 지원하는 중심에는 BeanDefinition 라는 추상화가 존재. (역할과 구현을 개념적으로 나눈 것)
XML을 읽어서 BeanDefinition을 만들면 된다.

자바 코드를 읽어서 BeanDefinition을 만들면 된다.

스프링 컨테이너는 자바 코드인지, XML인지 몰라도 되고, BeanDefinition만 알면 된다.
  • BeanDefinition 을 빈 설정 메타정보라 한다.
@Bean , <bean> 각각 하나씩 메타 정보가 생성된다.

 

  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
  • AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.
  • GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성.
  • 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성하면 된다.

### BeanDefinition 살펴보기

package hello.core.beandefinition;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class BeanDefinitionTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    //GenericXmlApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

    @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);
            }
        }
    }
}
  • BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다. 하지만 실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다.
반응형

+ Recent posts