빈 생명주기 콜백이란?

 

 

'빈 생명주기 콜백'은 스프링 빈이 생성되고 소멸될 때 즉 초기화 작업과 종료 작업을 수행할수 있도록 하는 것이다.

자세하게는 스프링에서 빈의 생성, 초기화, 소멸과 관련된 작업을 수행할 수 있도록 하는 기능이다.

 

 

초기화 작업과 종료 작업이 무슨 작업이지?? 

 

1. 애플리케이션 서버가 시작될때 DB 와 연결설정하는 초기화 작업

보통 애플리케이션은 관계형 데이터베이스를 연결하는데 오래걸리기 때문에

애플리케이션 실행시(서버가 시작될 때) 미리 DB와 애플리케이션을 연결시켜놓는다 

그렇게 하면 여러 요청이 들어올때 새롭게 DB와 연결할 필요가 없어진다.

 

2. 요청이 올때마다 미리 생성된 DB 연결을 재활용

다른 고객의 요청이 올때 DB와 연결을 재사용함으로써 성능이 향상되는 장점이 있다.

 

3. 서버 종료 시 DB연결 미리 끊는 종료 작업

또한 종료될때도 서버가 확 꺼지는게 아니라 애플리케이션 컨텍스트(ApplicationContext) 가 종료될때 DB와의 연결도 함께 미리 종료하여 서버를 종료한다. 그렇게 되면 정상적이고 안전하게 종료 처리가 된다. 

 

 

 

이러한 초기화 작업과 종료 작업은 어떻게 진행되는건지 예제코드로 알아보자 

예제코드니 가짜 네트워크를 연결해보고 단순히 문자로만 출력되도록 해보기 

 

 

[예제코드]

서비스 시작할때 호출되는 connect() 메서드와 메세지를 호출하는 call() 메서드

그리고 서비스가 종료될때 disconnect() 메서드를 호출하여 연결을 끊어야한다. 

public class NetworkClient {
    private String url;
	
    //생성자
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }
    
    public void setUrl(String url) {
    	this.url = url;
    }
    
	//서비스 시작시 호출하는 메서드 : connect() 와 call() 
    public void connect() {
    	System.out.println("connect: " + url);
    }
    
	//메세지:연결한 서버에 메세지를 던져주는 메서드
    public void call(String message) {
    	System.out.println("call: " + url + " message = " + message);
    }
    
	//서비스 종료시 호출되는 메서드 : disConnect()
    public void disconnect() {
    	System.out.println("close: " + url);
    }
}

 

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		//빈 생명주기 
		//1. 스프링 컨테이너 생성
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		
		//컨테이너로부터 빈 조회 
		//(이미 LifeCycleConfig을 스프링 컨테이너에 넣었고 networkClient()에서 NetworkClient 빈을 등록했기떄문에 조회만 함)
		NetworkClient client = ac.getBean(NetworkClient.class); 
		
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		//2. 빈 생성
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient; //리턴시 메서드 호출되는 시점에 객체생성이 되고 빈이 등록된다.
		}
	}
}

 

 

[로그 출력]

생성자 호출, url = null
connect: null
call: null, message = 초기화 연결 메세지

 

생성자 부분을 보면 url 값은 null이 출력된다. 

왜일까 networkClient.setUrl("http://hello-spring.dev"); 을 통해 값을 넣었는데 왜 null이 나올까??

 

스프링 빈의 생명주기를 보면

객체 생성 이후 의존관계 주입이 이루어진다.

객체 생성은 되었으나 의존관계 주입이 이루어지지 않았기때문에 url 값이 null로 출력이 되는 것!

스프링 빈은 객체를 생성하고 의존관계 주입이 모두 끝난 이후에 데이터를 사용할 준비가 된다. 

 

 

 

  • 스프링 빈의 생명주기
  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존관계 주입
  4. 초기화 콜백 사용 : 초기화 작업
    • 객체 생성 및 의존관계 주입 이후 객체를 사용하기 위해 필요한 추가 작업 정도로만 알기
    • 의존관계 주입된 후 초기화 콜백 메서드가 호출되므로 개발자에게 초기화 시점을 알려주는 목적
  5. 소멸 전 콜백 : 종료작업
    • 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.
    • 소멸 콜백 메서드 호출하여 개발자에게 알려줄 수 있다. 
  6. 스프링 종료 : 애플리케이션 종료될때 스프링 컨테이너 종료, 남아있는 빈들의 소멸작업 수행

 

이렇게 초기화 콜백은 빈이 생성되고 의존관계 주입이 완료 후 호출이 되고 

소멸전 콜백은 빈이 소멸되기 직전에 호출되는것

 

 

참고로 객체 생성과 초기화를 분리하는게 일반적으로 객체 지향 프로그램의 원칙 중 하나인 단일책임 원칙을 따른다 

이렇게 명확히 나누는것이 유지보수 관점에서 좋은 설계이다. 

  • 객체 생성은 생성자에서 필수 정보를 파라미터로 받고 메모리에 할당하여 객체를 생성하는 책임을 가진다
  • 초기화는 생성된 값을 동작하도록 별도의 초기화 메소드로 분리하여 외부 커넥션을 연결하는 작업을 가진다  

 

 

 

  • 스프링은 3가지 방법으로 빈 생명주기 콜백을 지원한다.
  1. 인터페이스(InitializingBean, DisposableBean)
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. @PostConstruct, @PreDestroy 어노테이션

 

 

 

빈 등록 시 초기화 메서드와 소멸 메서드

InitializingBean, DisposableBean 인터페이스

 

2003년에 나온 스프링 초창기 방법이라 거의 사용하지는 않음.

 

  • InitializingBean 는 afterPropertiesSet() 메서드로 초기화를 한다.
  • DisposableBean 는 destroy() 메서드로 소멸을 한다. 
public class NetworkClient implements InitializingBean, DisposableBean{ //초기화 빈, 종료 빈

	private String url;

	//생성자
	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	//외부에서 setter로 넣을 수 있도록
	public void setUrl(String url) {
		this.url = url;
	}
	
	//서비스 시작시 호출하는 메서드
	public void connect() {
		System.out.println("connect: " + url);
	}

	//연결된 상태에서 부를 수 있는 메서드 (연결한 서버에 메세지를 던져준다)
	public void call(String message) {
		System.out.println("call: " + url + ", message = " + message);
	}
	
	//서비스 종료시 호출되는 메서드 
	public void disConnect() {
		System.out.println("close: " + url);
	}

	//InitializingBean 의존관계 주입이 끝나면 호출하는 '초기화 메서드'
	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("NetworkClient.afterPropertiesSet");
		connect();
		call("초기화 연결 메세지");
	}

	//DisposableBean 종료시 호출하는 '소멸 메서드'
	@Override
	public void destroy() throws Exception {
		System.out.println("NetworkClient.destroy");
		disConnect();
	}
}

 

 

[테스트 클래스는 그대로]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

[결과로그]

 

 

 

 

 

 

빈 등록 초기화, 소멸 메서드 지정하기

@Bean(initMethod = "init", destroyMethod = "close")

 

설정 정보 사용 특징

  • 초기화, 소멸전 콜백 메서드 이름을 자유롭게 정할 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는 장점이 있다. (NetworkClient 클래스 코드를 보면 알 수 있음)
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용가능하다.
public class NetworkClient{ 

	private String url;

	//생성자
	public NetworkClient() {
		System.out.println("생성자 호출, url = " + url);
	}

	//외부에서 setter로 넣을 수 있도록
	public void setUrl(String url) {
		this.url = url;
	}
	
	//서비스 시작시 호출하는 메서드
	public void connect() {
		System.out.println("connect: " + url);
	}

	//연결된 상태에서 부를 수 있는 메서드 (연결한 서버에 메세지를 던져준다)
	public void call(String message) {
		System.out.println("call: " + url + ", message = " + message);
	}
	
	//서비스 종료시 호출되는 메서드 
	public void disConnect() {
		System.out.println("close: " + url);
	}

	//초기화 콜백메서드
	public void init() {
		System.out.println("NetworkClient.init");
		connect();
		call("초기화 연결 메세지");
	}

	//소멸전 콜백메서드
	public void close() {
		System.out.println("NetworkClient.close");
		disConnect();
	}
}

 

 

[테스트 클래스 BeanLifeCycleTest]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean(initMethod = "init", destroyMethod = "close")
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

 

[결과로그]

 

 

 

 

마지막으로 어노테이션 사용하는걸 권장

@PostConstruct, @PreDestroy 사용하기

 

  • 최신 스프링에서 가장 권장하는 방법
  • 어노테이션만 추가하면 되므로 가장 편리
  • 컴포넌트 스캔과 어울린다. 
  • 유일한 단점은 외부 라이브러리와 적용을 못한다. 코드를 고쳐야하기 때문!
    • 만약 외부 라이브러리를 초기화, 종료해야한다면 두번째 방법대로 @Bean 의 initMethod , destroyMethod 사용하기

 

위의 NetworkClient 클래스 코드에서 초기화, 종료 메서드에 어노테이션만 추가하기 

@PostConstruct
public void init() {
    System.out.println("NetworkClient.init");
    connect();
    call("초기화 연결 메세지");
}

@PreDestroy
public void close() {
    System.out.println("NetworkClient.close");
    disConnect();
}

 

 

[테스트 클래스 BeanLifeCycleTest]

public class BeanLifeCycleTest {

	@Test
	public void lifeCycleTest() {
		ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
		NetworkClient client = ac.getBean(NetworkClient.class); //빈조회
		ac.close(); // ApplicationContext > ConfigurableApplicationContext > AnnotationConfigApplicationContext
	}
	
	@Configuration
	static class LifeCycleConfig {
		
		@Bean
		public NetworkClient networkClient() {
			NetworkClient networkClient = new NetworkClient();
			networkClient.setUrl("http://hello-spring.dev");
			return networkClient;
		}
	}
}

 

 

 

[결과로그]

 

 

 

 

반응형
LIST

 

 

기존에 공부했던 AutoAppConfig 는 @ComponentScan 어노테이션이 붙어있어서  @Component 가 붙은 클래스를 모두 스캔한다.  

@Configuration 
@ComponentScan(
		basePackages = "hello.core.member", //member 패키지를 포함한 하위 패키지까지 컴포넌트 스캔 대상이 됨
		basePackageClasses = AutoAppConfig.class, //지정한 클래스의 패키지만 컴포넌트 스캔 대상이 됨 
		excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

@Component가 붙은 FixDiscountPolicy,RateDiscountPolicy 

@Component
public class FixDiscountPolicy implements DiscountPolicy{
	~~
}​

@Component
public class RateDiscountPolicy implements DiscountPolicy{
	~~
}​


 

 

 

[AllBeanTest]

public class AllBeanTest {

	@Test
	void findAllBean() {
		ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
	}
	
	static class DiscountService{
		private final Map<String, DiscountPolicy> policyMap;
		private final List<DiscountPolicy> policies;
		
		//생성자
		@Autowired
		public DiscountService(Map<String, DiscountPolicy> policyMap,
				List<DiscountPolicy> policies) {
			this.policyMap = policyMap;
			this.policies = policies;
			
			System.out.println("policyMap = "+ policyMap);
			System.out.println("policies = "+ policies);
		}
	}
}

 

 

 

빈 등록과 함께 의존성 주입이 이루어지는데 

먼저 빈등록 코드 설명을 하자면

  • ApplicationContext ac = new AnnotationConfigApplicationContext(DiscountService.class);

이렇게 하면 DiscountService 클래스만 스프링빈에 등록이 되니까 AutoAppConfig.class 도 추가해주기

  • ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

첫번째 파라미터 AutoAppConfig 클래스는 컴포넌트 스캔을 통해 @Component 어노테이션이 붙은 클래스들을 스프링 빈으로 등록하고

두번째 파라미터 DiscountService 클래스는 직접 스프링 빈으로 등록하는 역할을 한다. 

 

  • public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies)

DiscountService 클래스의 생성자에서 DiscountPolicy 를 의존성 주입이 이루어졌다 

그리고 Map , List 형태로 담는데 

DiscountPolicy 인터페이스를 구현한 여러 클래스( FixDiscountPolicy,RateDiscountPolicy ) 를 Map , List  형태로 담는다. 

 

 

Map , List  형태란?? 

  • Map : policyMap 은 각 구현 클래스의 이름을 key 값으로 하고, 구현 객체를 value로 가진다. ex) {키 = 값, 키 = 값}    
  • List  : policies 는 모든 객체를 리스트 형태로 가져온다. ex) [ ]
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;

 

 

[결과]

 

 

 

 

 

반응형
LIST

 

의존관계 주입 방법 4가지 

  1. 생성자 주입
  2. 수정자 주입 (setter 주입)
  3. 필드 주입
  4. 일반 메서드 주입

 

 

1. 생성자 주입

  • 이름 그대로 생성자를 통해 의존관계를 주입 받는 방법
  • 특징
    • 생성자 호출시점에서 딱 한번만 호출되는 것을 보장한다.
  • 장점
    • 불변 : 두 번이상 호출이 안되기때문에 변하지 않음
    • 필수 : 생성자를 통해 들어온 값이 무조건 있어야 함
  • 생성자가 하나일때는 자동으로 @Autowired가 적용이 된다. (생략가능) 
  • 예시
@Component
public class OrderServiceImpl implements OrderService{
	
	private final MemberRepository memberRepository; 
	
	private final DiscountPolicy discountPolicy; 
	
	//생성자 (생성자가 하나라서 @Autowired 생략가능)
	@Autowired
	public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
}

 

컴포넌트 스캔할 때 OrderServiceImpl 를 스프링 빈으로 등록이 되면 @Autowired 어노테이션이 부여된 생성자가 있기 때문에 생성자를 호출을 하여 의존성 주입을 한다. 

어떻게? 스프링 빈이 MemberRepositoryDiscountPolicy라는 타입의 빈(Bean)을 찾아서 인자로 받아들여 주입시키고  OrderServiceImpl 빈을 생성

 

즉 스프링은 OrderServiceImpl 빈을 생성할 때 MemberRepositoryDiscountPolicy의 구현체를 찾아와서 해당 인스턴스를 생성자를 통해 주입한다. 

 

 

MemberRepository 구현체 [MemoryMemberRepository]

@Component
public class MemoryMemberRepository implements MemberRepository{ //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);
	}
}

 

 

 

 

 

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

 

  • memberRepository , discountPolicy 필드명에 set 붙이고 카멜표기법으로 필드값을 메서드로 통해 직접 수정하기
    • setMemberRepository, setDiscountPolicy
@Component
public class OrderServiceImpl implements OrderService{
	
	private MemberRepository memberRepository; 
	private DiscountPolicy discountPolicy; 
	
	//setter 의존관계 주입
	@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;
	}
 }

 

    * 생성자 필요 없음

[테스트 결과]

 

 

 

  • 특징 (선택, 변경)
    • 선택적으로 의존관계를 주입할 수 있다.
    • @Autowired(required=false) : 원래는 @Autowired 의 기본동작은 주입할 대상이 없으면 오류가 발생하지만 주입할 대상 없이도 동작하게 된다. 
@Autowired(required=false) //의존관계 안해도 됨.
public void setMemberRepository(MemberRepository memberRepository) { }
    • 변경가능성 있는 의존관계 

 

 

 

 

 

3. 필드 주입

  • 이름 그대로 필드에 바로 주입하는 방법 (권장하지 않음)
  • 특징 
    • 코드가 간결해서 외부에 변경이 불가능해 테스트하기가 어렵다.
    • 실제코드와 관련없는 테스트 코드에 사용
  • 예시 
    • memberRepository, discountPolicy 필드앞에 @Autowired 붙이기 
@Component
public class OrderServiceImpl implements OrderService {

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

 

 

 

 

4. 일반 메서드 주입

  • 일반 메서드를 통해 주입 받을 수 있다.
  • 특징
    • 아무 메서드에 @Autowired 를 붙일 수 있다.
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 그러나 생성자 주입이나 수정자 주입을 대부분 사용하기 때문에 일반 메서드 주입은 일반적으로 사용하지 않는다. 
  • 예시
public class OrderServiceImpl implements OrderService{
	
	private MemberRepository memberRepository; 
	private DiscountPolicy discountPolicy;
	
	//일반 메서드 주입
	@Autowired
	public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
		this.memberRepository = memberRepository;
		this.discountPolicy = discountPolicy;
	}
    
  }

 

 

※ 참고 

OrderServiceImpl 은 스프링 컨테이너가 관리하는 스프링 빈이라서 @Autowired 가 기능한다.
 
( OrderServiceImpl 는 @Component 을 붙였기 때문에 스프링 컨테이너에 의해 빈으로 관리하는 클래스다. )
 
일반 클래스는 @Autowired 를 적용해도 아무 동작을 하지 않는다.

 

 

반응형
LIST

 

원래 스프링 빈을 직접 등록할때 방법은 두가지 방법이 있다.

  • 자바코드에서 @Bean 
  • xml 에서 <bean> 

이렇게 등록하면 수십, 수백개를 일일이 등록해야하는 귀찮음과 누락하는 문제점이 있다.

그래서 스프링 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 '컴포넌트 스캔' 이라는 기능을 제공한다.

 

 

 

 

컴포넌트 스캔하는 AutoAppConfig 클래스로 테스트해보자

기존의 설정정보인 AppConfig 대신해서 테스트용 설정정보 AutoAppConfig 클래스를 만들기

 

[기존의 AppConfig]

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	@Bean
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
	
	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
	}
	
	@Bean
	public DiscountPolicy discountPolicy() {
//		return new FixDiscountPolicy();  // 1만원이든 2만원이든 천원 할인적용
		return new RateDiscountPolicy(); // 1만원 천원할인, 2만원이면 2천원할인
	}
}

 

 

 

[테스트용 AutoAppConfig]

@Configuration //설정정보에 붙이면 싱글톤 유지
@ComponentScan(
	excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

AutoAppConfig 에서는 @Bean 을 사용하지 않고 빈을 등록할 수 있다. 

그렇기 때문에 기존의 AppConfig 설정정보는 사용하지 않아서 제외시켜주자

 

@ComponentScan

 

  •   1. @Component 가 붙은 모든 클래스를 찾아(스캔해서) 자동으로 스프링 빈으로 등록해주는 어노테이션
  •   2. 기존의 AppConfig 설정정보는 사용하지 않아서 제외시켜준다.
    • @ComponentScan(
       excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
    • @Configuration이 붙은 Appconfig는 @Bean을 써서 스프링 빈을 수동등록 해놨기 때문에 @ComponentScan 에서 제외시킨다. 
    • 컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동 등록해주기 때문에 excludeFilters 를 써서 제외시킨다.

 

근데 왜 컴포넌트 스캔은 @Configuration 도 스캔하는 것일까??

 

 

 @Configuration 이 컴포넌트 스캔의 대상이 된 이유

@Configuration 소스코드를 보면 @Component 가 붙어있기 때문! 

 

 

 

 

 

 

이제 AutoAppConfig 에서 @Bean 을 쓰지않고 어떻게 스프링 빈으로 등록을 할 수 있을까?

@Component 가 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록해주기 때문에 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component를 붙여준다.  

 

** 어떤 클래스에 @Component 를 붙이나?

AppConfig 에서 @Bean이 붙은 메서드의 반환되는 객체의 타입이 스프링 빈으로 등록이 되니까 MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl, RateDiscountPolicy 의 클래스에 @Component 를 붙이면 된다. 

 

 

MemberServiceImpl 에 @Component 붙이기

@Component
public class MemberServiceImpl implements MemberService{}

 

 

MemoryMemberRepository@Component 붙이기

 

@Component
public class MemoryMemberRepository implements MemberRepository{}

 

 

OrderServiceImpl에 @Component 붙이기

@Component
public class OrderServiceImpl implements OrderService{}

 

 

RateDiscountPolicy에 @Component 붙이기

@Component
public class RateDiscountPolicy implements DiscountPolicy{}

 

 

 

 

  • 스프링 빈의 기본 이름은 클래스명을 사용하는데 맨 앞글자를 소문자로 바꿔서 사용한다.
    • 빈 이름 MemberServiceImpl > memberServiceImpl 변경
    • 직접 빈이름을 지정 : @Component("memberServiceImpl2") 

 

 

 

그렇다면 이제 의존관계 주입은 어떻게 할것인가??

기존의 AppConfig의 의존관계 주입은 memberService 는 memberRepository 를 의존관계 주입을 했는데 

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

 

 

AutoAppConfig는 아무것도 없다 어떻게 의존관계 주입을 해야할까? @Autowired 를 사용하여 자동주입을 하면 된다.

@Configuration //설정정보에 붙이면 싱글톤 유지
@ComponentScan(
	excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {

}

 

 

 

 

 

@Autowired
의존관계를 자동으로 주입해주는 어노테이션

 

 

  1. @Component 붙은 클래스에서 의존관계 설정을 해주면 된다.  

이전에는 AppConfig에서는 @Bean 을 통해 직접 설정정보를 작성했고 의존 관계도 직접 연결했으나 

이제는 의존관계 주입을 @Component 붙은 클래스에서 생성자에 @Autowired를 붙여주면 된다. 

 

MemberServiceImpl @Autowired 추가

@Component
public class MemberServiceImpl implements MemberService{

	private final MemberRepository memberRepository;
	
	@Autowired 
	public MemberServiceImpl(MemberRepository memberRepository) {
		this.memberRepository = memberRepository;
	}

 

 

그 외 

OrderServiceImpl 클래스에 @Autowired 추가하기

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

* @Autowired 는 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

 

 

 

 

@Autowired 동작 설명

 

  2.  생성자에 @Autowired 를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아 주입한다.

 

@Autowired 
public MemberServiceImpl(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

 

어떻게?

  • MemberRepository 타입과 일치하는 스프링 빈을 찾아준다.
    • ac.getBean(MemberRepository.class) 와 같은 동작이라고 보면 된다.
  • MemberRepository 인터페이스라서 해당 인터페이스를 구현한 MemoryMemberRepository 빈을 찾아 MemberServiceImpl생성자에 넣어준다 
  • 이로써 MemberServiceImpl MemoryMemberRepository 사용할 수 있게된다. 

 

 

+ 만약 생성자의 파라미터에 주입하는 의존관계가 많다면??

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 

  • OrderServiceImpl 생성자처럼 파라미터가 많아도 스프링 컨테이너에서 다 찾아서 자동으로 주입한다. 

 

 

 

 

테스트 했을때 로그를 보면 ComponentScan 이 잘 작동한것을 볼 수 있다.

public class AutoAppConfigTest {

	@Test
	void basicScan() {
		AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
		
		MemberService memberService = ac.getBean(MemberService.class);
		Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
	}
}

 

[결과]

 

 

 


 

@ComponentScan 부가 설명

 

@ComponentScan ( basePackages = "")
탐색 시작 위치 설정하기

 

@ComponentScan(
     basePackages = "hello.core.member",

 

  • 이렇게 하면 member 패키지만! 컴포넌트 스캔의 대상이 된다.  다른 패키지는 스캔이 안됨
  • 설정하는 이유?
    • 자바 코드뿐만 아니라 라이브러리까지 모두 스캔하기때문에 오래걸리기 때문에 지정하는것이 좋다.
  • 여러개 시작 위치를 지정가능
    • @ComponentScan(
           basePackages = { "hello.core.member", "hello.core.order" }
  • basePackageClasses : 클래스를 지정해주면 해당 패키지를 탐색 시작 위치가 된다
    • basePackageClasses = AutoAppConfig.class
    • AutoAppConfig 클래스의 패키지를 스캔한다.
  • 시작 위치를 지정을 하지 않는다면? 
    • @ComponentScan 이 붙은 설정정보 클래스의 패키지와 그 하위 패키지들을 모두 스캔한다 (디폴트)
    • 권장방법은 위치 설정(basePackages)을 해주기보다 설정정보 클래스의 위치를 프로젝트 최상단에 두자 

 

참고로 @ComponentScan 을 설정할 필요가 없다 왜냐

스프링 부트를 사용하면  메인 메소드가 있는 CoreApplication 클래스가 자동으로 만들어지는데 

@SpringBootApplication 안에 들어가보면 @ComponentScan 이 이미 있다.

@SpringBootApplication
public class CoreApplication {

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

}

 

 

 

그렇기 때문에 CoreApplication 의 패키지 hello.core부터 스캔을 해서 스프링 빈으로 등록이 되기때문에

굳이 @ComponentScan 을 할 필요는 없다 ^^  

 

 

 

 

반응형
LIST

 

 

 

- BeanFactory 와  ApplicationContext 를 스프링 컨테이너라고 한다. 

- BeanFactory 기능을 상속받은 ApplicationContext ( ApplicationContext 는 BeanFactory의 부가기능을 더한 것)

그 밑에는 AnnotationConfig ApplicationContext 구현클래스

 

BeanFactory 
  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다
  • getBean() 을 제공
  • 지금까지 우리가 사용했던 대부분의 기능이  BeanFactory가 제공하는 기능이다. 
  • 사실 직접 BeanFactory 를 사용할 일은 거의 없다. 부가기능을 포함 된 ApplicationContext 을 사용한다.

 

ApplicationContext 
  • BeanFactory 기능을 모두 상속받아서 제공
  • BeanFactory 와 ApplicationContext의 차이는?
    • 애플리케이션을 개발할때는 빈은 관리하고 조회하는 기능( BeanFactory )은 물론이고 수많은 부가기능은 ApplicationContext 가 갖고있다.

 

ApplicationContext 부가기능

 

1. 메세지 소스를 활용한 국제화 기능

 : 예를들어 한국에는 한국어로, 영어권으로 가면 영어로 출력

 

2. 환경변수

  : 로컬, 개발, 운영 등을 구분해서 처리

 

3. 애플리케이션 이벤트

  : 이벤트 발행하고 구독하는 모델을 편리하게 지원

 

2. 편리한 리소스 조회

  : 파일, 클래스패스 , 외부 등에서 리소스르 편리하게 조회

 

 

 

 


 

 

스프링은 다양한 설정 형식

 

 

 

 

스프링은 다양한 설정 형식 지원을 한다 > 자바코드, XML , Groovy 등등

 

 

 

어노테이션 기반 자바 코드 설정 사용

 

AnnotationConfigApplicationContext 클래스를 사용하여 자바 코드로 된 설정 정보를 넘긴다. 

new AnnotationConfigApplicationContext(AppConfig.class);

 

 

 

 

XML 설정 사용

 

최근에는 스프링 부트를 많이 사용하면서 XML 기반 설정은 잘 사용하지 않는다.

장점으로는 컴파일 없이 빈 설정 정보를 변경할 수 있다

 

 

 

1. 테스트용 스프링 컨테이너 생성하기

[XmlAppContext 클래스]

public class XmlAppContext {

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

 

GenericXmlApplicationContext 만 바뀌었을뿐 나머지는 ApplicationContext  코드와 같다

 

 

 

2. xml 생성하기

 

resource 폴더안에  appConfig.xml 생성하기

 

 

 

[ appConfig.xml ]

appConfig.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">

	<bean id="memberService" class="hello.core.member.MemberServiceImpl">
		<constructor-arg name="memberRepository" ref="memberRepository"></constructor-arg>
	</bean>
	
	<!--id="memberRepository"가 위의 refer="memberRepository"로 넘어가며 구현객체는 hello.core.member.MemoryMemberRepository-->
	<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>
	
	<bean id="orderService" class="hello.core.order.OrderServiceImpl">
		<constructor-arg name="memberRepository" ref="memberRepository"></constructor-arg>
		<constructor-arg name="discountPolicy" ref="discountPolicy"></constructor-arg>
	</bean>
	
	<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"></bean>

</beans>

 

 

 

아래는 원래 자바코드로 설정한 [ AppConfig.java ] 

@Configuration
public class AppConfig {

	@Bean
	public MemberService memberService() {
		return new MemberServiceImpl(memberRepository());
	}
	
	@Bean
	public MemoryMemberRepository memberRepository() {
		return new MemoryMemberRepository();
	}
    
	@Bean
	public OrderService orderService() {
		return new OrderServiceImpl(memberRepository(), discountPolicy()); //discountPolicy()로 대체해주기
	}
	
	@Bean
	public DiscountPolicy discountPolicy() {
//		return new FixDiscountPolicy();  // 1만원이든 2만원이든 천원 할인적용
		return new RateDiscountPolicy(); // 1만원 천원할인, 2만원이면 2천원할인
	}
	
}

 

 

 

 

AppConfig.java 자바코드와 appConfig.xml 기반의 스프링 설정 정보를 비교해보자

 

[AppConfig.java] 

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

 

 [appConfig.xml]

<bean id="memberService" class="hello.core.member.MemberServiceImpl">
    <constructor-arg name="memberRepository" ref="memberRepository"></constructor-arg>
</bean>
  • <bean id="memberService" class="hello.core.member.MemberServiceImpl">
    • 빈 이름 memberService , 구현 클래스 MemberServiceImpl (패키지 이름까지 모두 적기)
  • <constructor-arg name="memberRepository" ref="memberRepository"></constructor-arg>
    • 생성자 이름 memberRepository , 참조(ref) 를 넣어줘야하는데 없으니 만들자 

 

 

[AppConfig.java] 

@Bean
public MemoryMemberRepository memberRepository() {
    return new MemoryMemberRepository();
}

 

 [appConfig.xml]

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

 

 

 

 

[테스트 결과] 빈으로 등록된 메서드 이름이 나옴 

 

 

 

 

 

 

 

스프링은 이렇게 java와 xml 처럼 다양한 설정 형식을 지원한다 

어떻게 가능한가? 

  • BeanDefinition 이라는 추상화가 있다
    • XML 또는 자바코드를 읽어서 BeanDefinition 을 만든다 
    • 스프링 컨테이너는 BeanDefinition 만 알면  XML 인지 자바코드인지 몰라도 된다 
    • 역할과 구현을 나눈 것 처럼!
  • BeanDefinition 을 비 설정 메타 정보라고 하는데 
    • @Bean, <bean> 을 쓰면 각 하나씩 메타 정보가 생성된다.
  • 스프링 컨테이너는 메타정보를 기반으로 스프링 빈을 생성한다. 

 

 

 

 

반응형
LIST

 

 

 

 

부모 타입으로 조회하면 자식 타입까지 함께 조회된다. 

 

 

 

모든 자바 객체의 최고부모인 'Object 타입' 으로 조회하면 모든 스프링 빈을 조회한다. 

클래스에서 눈에 안보이지만 사실 Extends Object를 하고있다.

public class A extends Obect {
}

 

 

 

 

자식 2개인 부모 DiscountPolicy를 조회해보기
public class ApplicationContextExtendsFindTest {

	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
	
	@Test
	@DisplayName("부모 타입으로 조회시, 자식이 둘 이상이면, 중복 오류가 발생한다.")
	void findBeanByParentTypeDuplicate() {
		DiscountPolicy bean = ac.getBean(DiscountPolicy.class); 
	}
	
	@Configuration
	static class TestConfig {
		
		@Bean
		public DiscountPolicy rateDiscountPolicy() {
			return new RateDiscountPolicy();
		}
		
		@Bean
		public DiscountPolicy fixDiscountPolicy() {
			return new FixDiscountPolicy();
		}
	}
}

 

▶ 부모인 DiscountPolicy 는 자식 RateDiscountPolicy, FixDiscountPolicy 둘 다 조회가 되어 중복오류가 난다. 

 

 

NoUniqueBeanDefinitionException

 

 

 

 

NoUniqueBeanDefinitionException 예외처리 하기 >  assertThrows 

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

 

 

 

빈 이름을 지정해주기
public class ApplicationContextExtendsFindTest {

	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
	
	@Test
	@DisplayName("부모 타입으로 조회시, 자식이 둘 이상이면, 빈 이름을 지정하면 된다.")
	void findBeanByParentTypeBeanName() {
		DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy" ,DiscountPolicy.class);
		Assertions.assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class); 
	}
	
	@Configuration
	static class TestConfig {
		
		@Bean
		public DiscountPolicy rateDiscountPolicy() {  // 이걸로 지정
			return new RateDiscountPolicy();
		}
		
		@Bean
		public DiscountPolicy fixDiscountPolicy() {
			return new FixDiscountPolicy();
		}
	}
}

 

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

 

 

 

특정 하위 타입으로 조회해보기

 

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

 

RateDiscountPolicy 하나밖에 없으니까 에러 안난다.

 

 

 

 


 

여기서 궁금증!

 

아래처럼 반환값을 구현체로 해도 되지않을까? 

@Configuration
static class TestConfig {

    //@Bean
    //public DiscountPolicy rateDiscountPolicy() {  
    //    return new RateDiscountPolicy();
    //}
    
    //@Bean
    //public DiscountPolicy fixDiscountPolicy() {
    //   return new FixDiscountPolicy();
    //}
    
    // 반환값을 구현체로?
    @Bean
    public RateDiscountPolicy rateDiscountPolicy() { 
        return new RateDiscountPolicy();
    }

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

 

▶ 구체적인걸로 해도되지만! 역할과 구현을 나눈 것 처럼 인터페이스(역할)로 해두는게 다형성에도 좋고 가독성에도 좋다!

 

 

반응형
LIST

+ Recent posts