📖/spring(김영한)
스프링 핵심 원리 - 기본편 4일차
모팔구
2023. 8. 9. 09:03
728x90
반응형
섹션4 스프링 컨테이너와 스프링 빈
스프링 빈 조회 - 상속 관계
빈을 조회할 때 부모 타입으로 조회하면 자식 타입도 함께 조회한다. 그래서 모든 자바 객체의 최고 부모인 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.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(DiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회(추천X)")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType(){
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + " value = " + beansOfType.get(key));
}
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - 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 DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
사실 애플리케이션 컨텍스트에서 직접 getBean()을 할 일은 없다. 그럼에도 수업을 진행한 이유는 기본 기능이기도 하고 가끔 순수한 자바 애플리케이션에서 스프링 컨테이너를 생성해서 사용할 일이 있을 수 있기 때문이다. 참고참고
BeanFactory와 ApplicationContext
- BeanFactory
- 스프링 컨테이너의 최상위 인터페이스이다.
- 스프링 빈을 관리하고 조회하는 역할을 담당한다.
- getBean()을 제공한다
- 여태 사용한 기능은 대부분 BeanFactory가 제공하는 기능이다
- ApplicationContext
- BeanFactory 기능을 모두 상속받아서 제공한다
- 빈을 관리하고 검색하는 기능을 BeanFactory에서 제공하지만 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고 수많은 부가기능이 필요하다
- MessageSource(메시지 소스)를 활용한 국제화 기능: 예를 들어, 한국에서 웹사이트에 진입하면 한국어로, 영어권에서 진입하면 영어로 출력
- EnvironmentCapable(환경 변수): 로컬, 개발, 운영 등을 구분해서 처리
- ApplicationEventPublisher(애플리케이션 이벤트): 이벤트를 발행하고 구독하는 모델을 편리하게 지원
- ResourcePatternResolver(편리한 리소스 조회): 파일, 클래스패스, 외부 등에서 리소스를 편하게 조회
- BeanFactory
- BeanFactory를 직접 사용할 일은 거의 없고 부가기능이 포함된 ApplicationContext를 주로 사용한다.
- BeanFactory나 ApplicationContext 모두 스프링 컨테이너라고 한다
다양한 설정 형식 지원 - 자바 코드, XML
- 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다. 자바 코드, XML, Groovy 등등을 사용한다.
- ApplicationContext 인터페이스를 구현한 AnnotationConfigApplicationContext는 자바 코드를, GenericXmlApplicationContext는 xml파일을, 그리고 임의로 XxxApplicationContext로 xxx 확장자를 가지는 파일을 참조하도록 설정할 수 있다.
- XML 설정 사용
- 최근에는 스프링 부트를 많이 사용하면서 XML 기반의 설정은 잘 사용하지 않는다. 그래도 아직 많은 레거시 프로젝트들(학원ㅎ)이 XML로 되어 있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있기 때문에 배우는 것도 괜찮다.
- GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다.
- resources/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"/> </bean> <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/> <bean class="hello.core.discount.RateDiscountPolicy" id="discountPolicy"/> <bean class="hello.core.order.OrderServiceImpl" id="orderService"> <constructor-arg name="memberRepository" ref="memberRepository"/> <constructor-arg name="discountPolicy" ref="discountPolicy"/> </bean> </beans>
위 xml파일로 빈을 등록해 아래 자바코드처럼 부르면 된다public class XmlAppContext { @Test void xmlAppContext() { ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml"); MemberService memberService = ac.getBean("memberService", MemberService.class); assertThat(memberService).isInstanceOf(MemberService.class); } }
스프링 빈 설정 메타 정보 - BeanDefinition
- 스프링은 어떻게 이런 다양한 설정 형식을 지원할까? 왜냐 하면 BeanDefinition이라는 추상화가 있기 때문이다 즉 역할과 구현을 개념적으로 나눈 것이다
- xml을 읽어서 BeanDefinition을 만들고 자바 코드를 읽어서 BeanDefinition을 만든다
- 스프링 컨테이너는 자바 코드인지 xml인지 몰라도 되고 오직 BeeanDefinition만 알면 된다
- BeanDefinition을 빈 설정 메타정보라 한다. @Bean, <bean> 각각 하나씩 메타 정보가 생성된다 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다
섹션5 싱글톤 컨테이너
웹 애플리케이션과 싱글톤
- 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 그렇기 때문에 매번 new로 객체를 생성하는 방식을 사용하면 메모리 낭비가 심하다. 이런 문제를 해결하려면 해당 객체가 딱 하나만 생성되고 공유하도록 설계하면 된다. 이것이 싱글톤 패턴이다.
- Assertions.assertThat(객체1).isNotSameAs(객체2): 객체1과 객체2는 달라야함
싱글톤 패턴
- 클래스의 인스턴스가 딱 하나만 생성되도록 보장하는 디자인 패턴이다. 그래서 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
- private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 막야아 한다.
public class SingletonService { private static final SingletonService instance = new SingletonService(); private SingletonService() { } public static SingletonService getInstance() { return instance; } public void logic() { System.out.println("싱글톤 객체 로직 호출"); } }
- static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
- 이 객체 인스턴스가 필요하면 오직 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
- 딱 한개의 객체 인스턴스만 존재해야 하므로 생성자를 private로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
- private 생성자를 사용해 외부에서 임의로 new 키워드를 사용하지 못하도록 막야아 한다.
- Assertions.assertThat(객체1).isSameAs(객체2): 두 객체가 참조하는 주소값이 같은지를 비교(==) ↔︎ isNotSameAs (!=)
- Assertions.assertThat(객체1).isEqualTo(객체2): 자바의 equals()메서드와 같은 동작
- 싱글톤 패턴을 AppConfig에 적용해야 하는데 일일이 코딩할 필요가 없다 왜냐하면 스프링 컨테이너를 쓰면 스프링 컨테이너가 기본적으로 객체를 다 싱글톤으로 만들어 관리하기 때문!
- 싱글톤 패턴의 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스(getInstance())에 의존한다 이는 DIP를 위반한다
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다
- 내부 속성을 변경하거나 초기화하기 어렵다
- private 생성자로 자식 클래스를 만들기 어렵다.
- 유연성이 떨어진다.
- 안티패턴으로 불리기도 한다.
싱글톤 컨테이너
- 싱글톤 컨테이너
- 스프링 컨테이너는 싱글턴 패턴을 적용하지않아도, 객체 인스턴스를 싱글톤으로 관리한다.
- 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
- 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글턴으로 유지할 수 있다.
- 싱글턴 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
- DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.
- 스프링의 기본 빈 등록 방식은 싱글톤이지만 싱글톤 방식만 지원하는 것은 아니다. 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
싱글톤 방식의 주의점
- 싱글톤 패턴이든, 스프링같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 방식은 여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(staeful)하게 설계하면 안된다 즉, 무상태(stateless)로 설계해야 한다.
- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다.
- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
- 스프링 빈의 필드에 공유값을 설정하면 정말 큰 장애가 발생할 수 있다.
- 약간 .. . void로 빈을 정의하지 말고 return값이 존재하게 하면 된다는 뜻같음
@Configuration과 싱글톤
-
@Configuration과 바이트코드 조작의 마법
- @Configuration을 붙이지 않으면 AppConfig의 자손인 AppConfig@CGLIB가 생성되지 않는다 그리고 클래스 내 각 메서드가 빈으로 등록되긴 하지만 모두 다른 객체를 참조한다
섹션6 컴포넌트 스캔
컴포넌트 스캔과 의존관계 자동 주입 시작하기
- 지금까지 스프링 빈을 등록할 때는 자바코드의 @Bean이나 xml파일의 <bean>을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다. 이렇게 등록해야 할 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고 누락하는 문제도 발생한다. 그래서 스프링은 설정 정보가 없어도 자동으로 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 그리고 의존관계도 자동으로 주입하는 @Autowired 기능도 제공한다
- 컴포넌트 스캔 기능을 사용하려면 구성 클래스(AutoAppConfig)에 @Configuration과 @ComponentScan 어노테이션을 추가한다.
- 컴포넌트 스캔은 이름 그대로 @Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
- @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다 이때 스프링 빈의 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다 @Component("이름")으로 빈 이름을 임의로 지정할 수 있다.
- 생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다 getBean()과 동일하다고 이해하면 된다
탐색 위치와 기본 스캔 대상
- 탐색할 패키지의 시작 위치 지정
- basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
@ComponentScan(basePackages = "hello.spring.member") 하나만 지정
@ComponentScan(basePackages = {"hello.core.member", "hello.core.order", ...}) 여러개 지정 - basePackageClasses: 지정할 클래스의 패키지를 탐색 시작 위치로 지정한다.
- 만약 지정하지 않으면 @ComponentScan이 붙은 클래스의 패키지가 시작 위치가 된다.
- 권장하는 방법
- 쌤이 즐겨쓰는 방법은 패키지 위치를 지정하지 않고 구성 클래스의 위치를 프로젝트 최상단(루트)에 둔다. 이렇게 하면 최상위 패키지를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다 그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 위치에 두는 것이 좋다 참고로 스프링 부트를 시작하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트 시작 루트 위치에 두는 것이 관례이다. (그 안에 @ComponentScan이 들어있다)
- basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
- 컴포넌트 스캔 기본 대상
- @Component: 컴포넌트 스캔 대상 클래스
- @Controller: 스프링 MVC 컨트롤러로 인식 및 사용
- @Service: 스프링 비즈니스 로직에서 사용하지만 이 어노테이션 자체가 부가적인 처리를 하지는 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나하고 비즈니스 계층을 인식하는데 도움이 된다.
- @Repository: 스프링 데이터 접근 계층에서 인식 및 사용하고 데이터 계층의 예외를 스프링 예외로 변환해준다
- @Configuration: 스프링 설정 정보에서 인식 및 사용하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다
- 어노테이션에는 상속관계라는 것이 없다. 그래서 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능이 아니고 스프링이 지원하는 기능이다.
반응형
728x90
반응형