인프런 강의 김영한
27. 스프링 빈 조회 - 상속 관계
부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면
모든 스프링 빈을 조회한다
27.1 부모타입 자식 둘이상
부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류가 발생한다
@Test
void findBeanByParentTypeDuplicate() {
// DiscountPolicy bean = ac.getBean(DiscountPolicy.class); 오류
Assertions.assertThrows(NoUniqueBeanDefinitionException.class, () -> ac.getBean(DiscountPolicy.class));
}부모 타입으로 조회시 자식이 둘 이상 있으면 빈 이름을 지정하면 된다
@Test
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy",
DiscountPolicy.class);
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
}27.2 특정 하위타입으로조회
타입으로 조회하고 싶다면 구체적인 타입을 지정해서 할 수 있지만 좋지 않은 방법이다.
@Test
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}27.3 부모타입으로 모두 조회
부모타입으로 조회하면 자식클래스가 모두 나온다
Object는 모든 객체의 부모이기 때문에 스프링 내부 Bean까지 모든 객체를 조회한다.
@Test
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
void findAllBeanByObjectType() {
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value=" +
beansOfType.get(key));
}
}실제 테스트 케이스때는 출력을 하면안된다. 통과안한다 한다만 봐야한다!
기본적인 기능을 모두 보었다. ApplicationContext에서 직접 getBean할일은 거의 없다.
실제 개발할때 빈을 볼일은 없지만 기본 개념이기 때문에 배운 것이다.
이런걸 알아두어야 자동 의존관계 주입 등을 잘 이해할 수 있다.
28. BeanFactory와 ApplicationContext
beanFactory와 ApplicationContext에 대해서 알아보자.
BeanFactory는 스프링 컨테이너의 최상위 인터페이스다.
ApplicationContext의 확장된 내용이라고 보면된다.
스프링 빈을 관리하고 조회하는 역할을 담당한다. getBean() 을 제공한다.
지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
ApplicationContext는 BeanFactory 기능을 모두 상속받아서 제공한다.
빈을 관리하고 검색하는 기능을 BeanFactory가 제공해준다.
그러면 둘의 차이가 뭘까?
애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고 수 많은 부가기능이 필요하다.
1.ApplicatonContext가 제공하는 부가기능
2.메시지소스를 활용한 국제화 기능
예를 들어서 한국에서 들어오면 한국어로 영어권에서 들어오면 영어로 출력하기
3.환경변수
로컬, 개발, 운영등을 구분해서 처리
4.애플리케이션 이벤트
이벤트를 발행하고 구독하는 모델을 편리하게 지원
5.편리한 리소스 조회
파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
정리
ApplicationContext는 BeanFactory의 기능을 상속받는다.
ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.
29. 다양한 설정 형식 지원 - 자바 코드, XML
자바코드로 하는 것을 배웟는데 XML설정을 가볍게 배워보도록 하자.
스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.
29.1 어노테이션 기반 자바 코드 설정 사용
지금까지 했던 것이다.
new AnnotationConfigApplicationContext(AppConfig.class)
AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.
29.2 XML 설정 사용
최근에는 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는다.
아직 많은 레거시 프로젝트 들이 XML로 되어 있고
또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 괜찮다.
GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다.
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>bean id=등록할이름 class실제 등록할 클래스,인터페이스
형식이 XML일뿐이지 자바코드와 비슷하게 생겼다.
30. 스프링 빈 설정 메타 정보 - BeanDefinition
스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까
그 중심에는 BeanDefinition 이라는 추상화가 있다.
빈정보자체를 추상화하는 것이다.
쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것이다.
XML나 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
스프링 컨테이너는 자바 코드인지 XML인지 몰라도 된다.
오직 BeanDefinition만 알면 된다.
BeanDefinition 을 빈 설정 메타정보라 한다.
@Bean , <bean> 당 각각 하나씩 메타 정보가 생성된다.
스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다
AnnotationConfigApplicationContext는
AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.
GenericXmlApplicationContext는
XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정정보를 읽고 BeanDefinition 을 생성한다.
코드를 읽어서 빈의 메타정보들을 담아놓는다.
새로운 형식의 설정 정보가 추가되면
XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성하게 된다.
BeanDefinition 살펴보기
BeanDefinition 정보
BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
factoryMethodName: 빈을 생성할 팩토리 메소드 지정, 예) memberService
Scope: 싱글톤(기본값)
lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한
생성을 지연처리 하는지 여부
InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메소드 명
DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메소드 명
Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음
정리
BeanDefinition을 직접 생성해서 스프링 컨테이너에 등록할 수 도 있다.
하지만 실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다.
어려우면 그냥 있다는 것을 알고 넘어가자.
BeanDefinition에 대해서는 너무 깊이있게 이해하기 보다는
스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.
가끔 스프링 코드나 스프링 관련 오픈 소스의 코드를 볼 때 BeanDefinition 이라는 것이 보일 때가 있다.
이때 이러한 메커니즘을 떠올리면 된다.
30. 웹 애플리케이션과 싱글톤
싱글톤 패턴 객체가 현재 나의 JVM에 하나만 잇어야한다.
웹어플리케이션에서 싱글톤패턴이 왜 많이 사용되는건가?
스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
대부분의 스프링 애플리케이션은 웹 애플리케이션이다.
물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.
웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.
클라이언트 A B C가 MemberService에 다 요청하게된다.
고객이 세번 요청하면 객체가 3개가 만들어지면 문제가 된다.
스프링 없는 순수한 DI 컨테이너를 만들어보자.
이것의 문제점을 알아볼것이다.
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);
}
}MemberService생성할때마다 다른객체가 나온다.
고객의 요청이 참 많은데 정말 많이 요청이 들어오면 JVM이 효율적이지 않게 된다.
우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로생성한다.
고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다 메모리 낭비가 심하다.
해결방안은 해당 객체가 딱 1개만 생성되고 공유하도록 설계하면 된다. -> 싱글톤 패턴
32. 싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.
자기자신을 내부에 static으로 만든다. 정적멤버는 클래스영역이기때문에 하나만 올라가게 된다. (자바 기본)
얘를 조회하려면 내부적으로 실행해서 참조에 넣어둔것을 getInstance메소드로 조회한다.
생성자를 private로 만들어서 생성을 못하게 해줘야한다.
instance를 꺼내는 방법은 getInstance밖에없다. 참조하는 방법도 없다.
public class SingletonService {
// 1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
// 2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance() {
return instance;
}
// 3. 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}1.static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
2.이 객체 인스턴스가 필요하면 오직 getInstance() 메소드를 통해서만 조회할 수 있다.
이 메소드를 호출하면 항상 같은 인스턴스를 반환한다.
3.딱 1개의 객체 인스턴스만 존재해야 하므로 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
테스트를 하면 다음과 같다. 결국 같은 객체이다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
// private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
// new SingletonService();
// 1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
// 2. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
// 참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonService1 == singletonService2
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}Test에서 isSameAs == 참조비교 / isequals javaeqals이다.
인스턴스 비교이기 때문에 isSameAs를 사용해줘야한다.
참고할점
싱글톤 패턴을 구현하는 방법은 여러가지가 있다.
여기서는 객체를 미리 생성해두는 가장 단순하고안전한 방법을 선택했다.
싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라
이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
그럼 AppConfig를 싱글톤으로 하면되는데 스프링은 알아서 싱글톤으로 만들어준다.
하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.
싱글톤 패턴 문제점
1.싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다. getInsetance만들어서 넣고 private생성자 등등
2.의존관계상 클라이언트가 구체 클래스에 의존한다. getInsetance로 꺼내야하기 때문이다. DIP를 위반한다.
3.클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
4.테스트하기 어렵다.
5.내부 속성을 변경하거나 초기화 하기 어렵다.
6.private 생성자로 자식 클래스를 만들기 어렵다.
7.결론적으로 유연성이 떨어진다. DI적용이 어렵다.
8.안티패턴으로 불리기도 한다.
장점도 있지만 수많은 단점을 가지고 있다.
그런데 스프링은 이 싱글톤 단점을 제거하고 장점만 살려두게 되었다.
31. 싱글톤 컨테이너
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서
객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.
지금까지 우리가 학습한 스프링 빈이 바로 싱글톤으로 관리되는 빈이다.
싱글톤 컨테이너
스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
이전에 설명한 컨테이너 생성 과정을 자세히 보자.
컨테이너는 @Bean이 붙은 것을 미리 만들어서 보관한다.
즉 객체를 하나만 생성해서 관리한다.
스프링 컨테이너는 싱글톤 컨테이너 역할을 한다.
이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1 = ac.getBean("memberService",
MemberService.class);
// 2. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService2 = ac.getBean("memberService",
MemberService.class);
// 참조값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1 == memberService2
assertThat(memberService1).isSameAs(memberService2);
}싱글톤 컨테이너 적용 후
스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라
이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.
AppConfig를 직접 작성하는 것보다 코드가 좀 많아 보이는데 싱글톤까지 적용하면 매우 더 길어진다.
참고할점
스프링의 기본 빈 등록 방식은 싱글톤이지만 싱글톤 방식만 지원하는 것은 아니다.
요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
32. 싱글톤 방식의 주의점
싱글톤 패턴이든 싱글톤 컨테이너는 조심해야할게있다.
여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다
무상태(stateless)로 설계해야 한다
특정 클라이언트에 의존적인 필드가 있으면 안된다.
특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
가급적 읽기만 가능해야 한다.
필드 대신에 자바에서 공유되지 않는 지역변수 파라미터 ThreadLocal 등을 사용해야 한다.
스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다.
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;
}
}class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean("statefulService",
StatefulService.class);
StatefulService statefulService2 = ac.getBean("statefulService",
StatefulService.class);
// ThreadA: A사용자 10000원 주문
statefulService1.order("userA", 10000);
// ThreadB: B사용자 20000원 주문
statefulService2.order("userB", 20000);
// ThreadA: 사용자A 주문 금액 조회
int price = statefulService1.getPrice();
// ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
System.out.println("price = " + price);
assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}원래는 멀티 스레드로 해야하는데 일단 싱글스레드로 만들었다.
ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.
같은 객체이기 때문에 userA 10000원넣은게 인터셉트 당해버린다.
StatefulService 의 price 필드는 공유되는 필드인데 특정 클라이언트가 값을 변경한다.
사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.
실무에서 이런 경우를 종종 보는데 이로인해 정말 해결하기 어려운 큰 문제들이 터진다.(몇년에 한번씩 꼭 만난다.)
복구하는데 로그 다보고 몇달 걸리게 된다. 실무에서는 매우 복잡한 코드에서 터지게 되는 것이다.
진짜 공유필드는 조심해야 한다.
스프링 빈은 항상 무상태(stateless)로 설계해야한다.
33. @Configuration과 싱글톤
@Configuration은 싱글톤을 위해서 존재한다.
AppConfig 코드를 보면 이상한점이 보인다.
memberService 빈을 만드는 코드를 보면 memberRepository()를 호출한다.
이 메소드를 호출하면 new MemoryMemberRepository() 를 호출한다.
orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.
이 메소드를 호출하면 또 new MemoryMemberRepository() 를 호출한다.
여기서 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지나 안깨지나 고민이 되게 된다.
각각 구현클래스에 테스트 용도로 MemberRepository를 조회할 수 있는 기능을 추가한다.
public MemberRepository getMemberRepository() {
return memberRepository;
}public class ConfigurationSingletonTest {
@Test
void configurationTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService = ac.getBean("memberService",
MemberServiceImpl.class);
OrderServiceImpl orderService = ac.getBean("orderService",
OrderServiceImpl.class);
MemberRepository memberRepository = ac.getBean("memberRepository",
MemberRepository.class);
// 모두 같은 인스턴스를 참고하고 있다.
System.out.println("memberService -> memberRepository = " +
memberService.getMemberRepository());
System.out.println("orderService -> memberRepository = " +
orderService.getMemberRepository());
System.out.println("memberRepository = " + memberRepository);
// 모두 같은 인스턴스를 참고하고 있다.
assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
}
}확인해보면 memberRepository 인스턴스는 모두 같은 인스턴스가 공유되어 사용된다.
AppConfig의 자바 코드를 보면 분명히 각각 2번 new MemoryMemberRepository 호출해서 다른 인스턴스가 생성되어야 하는데
어떻게 된 일일까 혹시 두 번 호출이 안되는 것인지 실험을 통해 알아보자.
스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다.
그래서 memberRepository() 는 다음과 같이 총 3번이 호출되어야 하는 것 아닐까싶다.
1.스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
2.memberService() 로직에서 memberRepository() 호출
3.orderService() 로직에서 memberRepository() 호출
그런데 다 같은 인스턴스이다.
34. @Configuration과 바이트코드 조작의 마법
스프링 컨테이너는 싱글톤 레지스트리다.
따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.
그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다.
자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다.
그래서 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.
모든 비밀은 @Configuration 을 적용한 AppConfig 에 있다.
@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가 출력되어야하는데
bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70
스프링이 조작해서 내가만든게 아니라 임의의 클래스를 등록한다.
아예 다른 것을 만들어서 넣어놓는 것이다. 이름은 AppConfig인데 아닌 것이다.
이 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다.
@Bean이 붙은 메소드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고
스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다고 생각하면된다.
덕분에 싱글톤이 보장되는 것이다
정리
@Bean만 사용해도 스프링 빈으로 등록되지만 싱글톤을 보장하지 않는다.
memberRepository()처럼 의존관계 주입이 필요해서 메소드를 직접 호출할 때 싱글톤을 보장하지 않는다.
크게 고민할 것이 없다. 스프링 설정 정보는 항상 @Configuration 을 사용해줘야한다.
35.컴포넌트 스캔과 의존관계 자동 주입 시작하기
지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
예제에서는 몇개가 안되었지만 이렇게 등록해야 할 스프링 빈이 수십 수백개가 되면 일일이 등록하기도 귀찮고 설정 정보도 커지고 누락하는 문제도 발생한다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.
@ComponentScan이 붙어있으면 @Component이 붙은 것을 자동으로 스프링에 등록을 해준다.
@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다
참고사항
컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에
AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다.
그래서 xcludeFilters를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외했다.
보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만
기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다고 하신다.
컴포넌트 스캔은 이름 그대로 @Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
각 클래스에 @Component 를 붙여주자.
이전에 AppConfig에서는 @Bean 으로 직접 설정 정보를 작성했고 의존관계도 직접 명시했다.
이제는 이런 설정 정보 자체가 없기 때문에 의존관계 주입도 이 클래스 안에서 해결해야 한다.
@Autowired 는 의존관계를 자동으로 주입해준다. 타입에 맞는 것을 알아서 주입해준다.
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일하다.
설정 정보로 AutoAppConfig 클래스를 넘겨준다.
실행해보면 기존과 같이 잘 동작하는 것을 확인할 수 있다.
1.@ComponentScan
@ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.
이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
빈 이름 기본 전략: MemberServiceImpl 클래스 memberServiceImpl
빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면
@Component("memberService2") 이런식으로 이름을 부여하면 된다
2.@Autowired 의존관계 자동 주입
생성자에 @Autowired 를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
getBean(MemberRepository.class) 와 동일하다고 이해하면 된다
생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.
36. 탐색위치와 기본 스캔대상
탐색위치와 시작위치를 지정할 수 있다.
모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문이다.
그래서 꼭 필요한 위치부터 탐색하도록 시작하게 위치를 지정할 수 있다.
@ComponentScan(
basePackages = "hello.core",
}basePackages
탐색할 패키지의 시작 위치를 지정한다.
지정한 패키지를 포함해서 하위 패키지를 모두 탐색한다.
basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.
그냥 두면 모든 라이브러리들을 모두 Scan하게 된다. 심지어 들어있는 모든 라이브러리를 다 뒤진다.
여러 라이브러리들이 섞여잇을때 사용하면 좋다.
basePackageClasses
지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장하는 방법
개인적으로 강사님이 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고
설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다.
최근 스프링 부트도 이 방법을 기본으로 제공한다.
예를 들어서 프로젝트가 다음과 같이 구조가 되어 있으면
com.hello
com.hello.serivce
com.hello.repository
com.hello 프로젝트 시작 루트에 AppConfig 같은 메인 설정 정보를 두고
@ComponentScan 어노테이션을 붙이고 basePackages 지정은 생략한다.
이렇게 설정 하면 com.hello 를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다.
프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다고 할 수 있다.
참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다.
이 어노테이션에 바로 @ComponentScan 이 들어있다
그래서 지금까지 @ComponentScan을 하지 않앗더라도 알아서 스캔하는 거엿다.
여기에서 불필요한것들을 필터에 넣어서 빼는게 좋다.
부트는 알아서 설정해준다.
컴포넌트 스캔 기본 대상
컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.
@Component : 컴포넌트 스캔에서 사용
@Controlller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용
해당 클래스의 소스 코드를 보면 @Component 를 포함하고 있다.
사실 어노테이션에는 상속관계라는 것이 없다.
그래서 이렇게 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고 스프링이 지원하는 기능이다.
컴포넌트 스캔의 용도 뿐만 아니라 다음 어노테이션이 있으면 스프링은 부가 기능을 수행한다.
@Controller : 스프링 MVC 컨트롤러로 인식
@Repository : 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해준다.
특정db에 예외가 발생하면 서비스까지 올라가게 된다. 이러면 다른 계층도 흔들리는데 이것을 추상화해서 따로 변환해준다.
@Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
@Service : @Service 는 특별한 처리를 하지 않는다.
대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 된다.
참고할점
useDefaultFilters 옵션은 기본으로 켜져있는데 이 옵션을 끄면 기본 스캔 대상들이 제외된다.
그냥 이런 옵션이 있구나 정도 알고 넘어가자
37. 필터
includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
임의로 어노테이션을 만들고 컴포넌트 스캔에 추가할 것이다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}@MyIncludeComponent
public class BeanA {
}@MyExcludeComponent
public class BeanB {
}테스트를 해보자.
나만의 필터를 만들어서 이 어노테이션이 붙은 것들을 스캔하지 않도록 할 수 있다.
includeFilters 에 MyIncludeComponent 어노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
excludeFilters 에 MyExcludeComponent 어노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.classpublic class ComponentFilterAppConfigTest {
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppconfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
Assertions.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 {
}
}FilterType 옵션
FilterType은 5가지 옵션이 있다.
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는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.
특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공해서
옵션을 변경하면서 사용하기보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고 선호된다.
38. 중복 등록과 충돌
컴포넌트 스캔에서 같은 빈 이름을 등록하면 예외가 발생한다.
다음 두가지 상황이 있다.
1.자동 빈 등록 vs 자동 빈 등록
2.수동 빈 등록 vs 자동 빈 등록
38.1 자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
ConflictingBeanDefinitionException 예외 발생한다.
38.2 수동 빈 등록 vs 자동 빈 등록
이 경우 수동 빈 등록이 우선권을 가진다.
수동 빈이 자동 빈을 오버라이딩 해버린다.
물론 개발자가 의도적으로 이런 결과를 기대했다면 자동 보다는 수동이 우선권을 가지는 것이 좋다.
하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는
여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다
그러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그다.
그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.
@SpringBootApplication이 그냥 튕겨버리게 만들어놨다.
하도 충돌 문제가 많이 생기고 명확하지 않으면 이러면 안된다.
개발은 혼자하는게 아니라서 이런 문제가 발생하게 되는 것이다.
2023.05.15
싱글톤과 컴포넌트 자체에 대한 내용을 알게 되었다.
스프링웹개발보다는 들으면 들을 수록 스프링 자체에 대한 설명을 배우는 것같다.
배워두면 필요는 잇어보이고 웹개발은 국비에서 프로젝트를 하면서 천천히 나아가보면 될 듯하다.
'기초단계 > SPRING' 카테고리의 다른 글
| 2023.05.20 Spring (0) | 2023.05.22 |
|---|---|
| 2023.05.17 Spring (0) | 2023.05.19 |
| 2023.05.13 Spring (0) | 2023.05.16 |
| 2023.05.11 Spring (0) | 2023.05.11 |
| 2023.05.10 Spring (0) | 2023.05.11 |