https://www.inflearn.com/course/스프링-핵심-원리-기본편
스프링 웹 애플리케이션 개발 전반에 대한 학습
- 객체 지향 설계와 스프링
- 스프링 핵심 원리 이해1 - 예제 만들기
- 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
- 스프링 컨테이너와 스프링 빈
- 싱글톤 컨테이너
- 컴포넌트 스캔
- 의존관계 자동 주입
- 빈 생명주기 콜백
- 빈 스코프
- 스프링이란? : 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크
- 좋은 객체 지향 프로그래밍(SOLID)
- SRP 단일 책임 원칙(single responsibility principle) <- 한 클래스는 하나의 책임을 가져야 한다.
- OCP 개방-폐쇄 원칙 (Open/closed principle) <- 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- LSP 리스코프 치환 원칙 (Liskov substitution principle) <- 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
- ISP 인터페이스 분리 원칙 (Interface segregation principle) <- 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
- DIP 의존관계 역전 원칙 (Dependency inversion principle) <- 구현 클래스에 의존하지 말고, 인터페이스에 의존한다.
다형성 만으로는 OCP, DIP를 지킬 수 없다...?
- 객체 지향 설계와 스프링
- 스프링은 DI 기술로 다형성 + OCP, DIP 가능하게 지원
- DI(Dependency Injection): 의존관계, 의존성 주입
- 비즈니스 요구사항과 설계 : 회원가입 및 조회, 회원등급 : (일반, VIP) , 회원데이터 : 자체 DB or �외부 시스템(미확정), 상품 주문(회원만), 할인 정책 적용
- 회원 도메인 설계 및 개발
-
새로운 할인 정책 개발
-
새로운 할인 정책 적용과 문제점
-
관심사의 분리(해결방안 : 생성자 주입) , AppConfig 리팩토링
- 클라이언트인
memberServiceImpl
입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
- 클라이언트인
-
AppConfig 리팩토링
-
새로운 구조와 할인 정책 적용(SOLID 원칙 활용) <- 여기서 3가지 SRP, DIP, OCP 적용
public DiscountPolicy discountPolicy() { return new RateDiscountPolicy(); //return new FixDiscountPolicy(); }
-
IoC, DI, 그리고 컨테이너
- IoC(제어의역전): 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어
OrderServiceImpl
도 AppConfig가 생성한다. 그리고 AppConfig는OrderServiceImpl
이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체OrderServiceImpl
은 묵묵히 자신의 로직 을 실행할 뿐이다. 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다. - (참고) 프레임워크 vs 라이브러리 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit) 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.
- DI : 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다. 애플리케이션 **실행 시점(런타임)**에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- 컨테이너 : AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.
- IoC(제어의역전): 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어
-
스프링으로 전환하기
코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까?
-
스프링 컨테이너 생성
주의: 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생한다.
-
컨테이너에 등록된 모든 빈 조회 TEST - beanfind package 참고
public class ApplicationContextInfoTest { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); @Test @DisplayName("모든 빈 출력하기") void findAllBean() { String[] beanDefinitionNames = ac.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { Object bean = ac.getBean(beanDefinitionName); System.out.println("name = " + beanDefinitionName + "object = " + bean); } } @Test @DisplayName("애플리케이션 빈 출력하기") void findApplicationBean() { String[] beanDefinitionNames = ac.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); //ROLE_APPLICATION : 직접 등록한 애플리케이션 빈 //ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈 if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){ Object bean = ac.getBean(beanDefinitionName); System.out.println("name = " + beanDefinitionName + "object = " + bean); } } }
-
스프링 빈 조회 (기본, 동일한 타입이 둘 이상, 상속 관계) TEST - beanfind package 참고
-
BeanFactory와 ApplicationContext
- MessageSource : 국제화 기능 (한국 -> 한국어, 영어권 -> 영어)
- EnvironmentCapable : 환경변수 (로컬, 개발, 운영등을 구분해서 처리)
- ApplicationEventPublisher : 애플리케이션 이벤트 (이벤트를 발행하고 구독하는 모델을 편리하게 지원)
- ResourceRoader : 편리한 리소스 조회 (파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회)
-
다양한 설정 형식 지원 - 자바 코드, XML
<- resources - appConfig.xml -
스프링 빈 설정 메타 정보 - BeanDefinition (역할과 구현 개념적으로 나눈 것)
BeanDefinition
을 빈 설정 메타정보라 한다.
-
웹 애플리케이션과 싱글톤
- 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성한다.
- 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다! 메모리 낭비가 심하다.
- 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. 싱글톤 패턴 필요이유
-
싱글톤 패턴, 컨테이너
public class SingletonService { private static final SingletonService instance = new SingletonService(); public static SingletonService getInstance() { return instance; } private SingletonService() { } public void logic() { System.out.println("싱글톤 객체 로직 호출"); } }
- 이 객체 인스턴스가 필요하면 오직
getInstance()
메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
- 이 객체 인스턴스가 필요하면 오직
이러한 문제점을 스프링은 어떻게 보완하였을까?
- 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
-
싱글톤 방식의 주의점
@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 주문 금액 조회 System.out.println("price = " + userAPrice); // Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000); }
-
StatefulService
의price
필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다 -
@Configuration과 싱글톤, 바이트코드 조작의 마법
- @Configuration 사용할 때 : 결과 -> (한번씩 호출) call AppConfig.memberService, call AppConfig.memberRepository, call AppConfig.orderService
- @Configuration 사용 안할 때 : 결과 -> call AppConfig.memberService, call AppConfig.memberRepository, call AppConfig.orderService, call AppConfig.memberRepository, call AppConfig.memberRepository
- @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
- 크게 고민할 것이 없다. 스프링 설정 정보는 항상
@Configuration
을 사용하자.
-
컴포넌트 스캔과 의존관계 자동 주입 시작하기
@Configuration @ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class) ) public class AutoAppConfig { }
-
@ComponentScan
은@Component
가 붙은 모든 클래스를 스프링 빈으로 등록한다.(getBean(MemberRepository.class)과 동일하다고 이해)@Component public class MemberServiceImpl implements MemberService { private final MemberRepository memberRepository; @Autowired public MemberServiceImpl(MemberRepository memberRepository) { this.memberRepository = memberRepository; } }
-
-
컴포넌트 기본 스캔 대상
@Controller
: 스프링 MVC 컨트롤러로 인식@Repository
: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.@Configuration
: 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.@Service
: 사실@Service
는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다.
-
필터
@Configuration @ComponentScan( includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) )
includeFilters
에MyIncludeComponent
애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.excludeFilters
에MyExcludeComponent
애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.
옵션을 변경하면서 사용하기 보다 는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.
- 중복 등록과 충돌
- 수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
- 수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러
-
다양한 의존관계 주입 방법
- 생성자 주입 <- 불변, 필수 의존관계에 사용
- 수정자 주입(setter 주입) <- 선택, 변경 가능성이 있는 의존관계에 사용
- 필드 주입, 일반 메서드 주입 (일반적으로 사용 x)
-
옵션 처리
@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력된다.Optional<>
: 자동 주입할 대상이 없으면Optional.empty
가 입력된다.
-
생성자 주입
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징 을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
-
롬복과 최신 트랜드
@Component @RequiredArgsConstructor //<- Lombok 제공 public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; }
-
@Autowired 필드 명, @Qualifier, @Primary
- @Autowired private DiscountPolicy discountPolicy
- @Qualifier("mainDiscountPolicy")
- @Autowired 시에 여러 빈이 매칭되면
@Primary
가 우선권을 가진다.
-
애노테이션 직접 만들기
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented @Qualifier("mainDiscountPolicy") public @interface MainDiscountPolicy { }
- @MainDiscountPolicy로 사용
-
조회한 빈이 모두 필요할 떄, List, Map
Map<String, DiscountPolicy>
: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로DiscountPolicy
타입으로 조회한 모든 스프링 빈을 담아준다.List<DiscountPolicy>
:DiscountPolicy
타입으로 조회한 모든 스프링 빈을 담아준다.
-
자동, 수동의 올바른 실무 운영 기준
- 편리한 자동 기능을 기본으로 사용하자
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자
- 빈 생명주기 콜백 시작
- 스프링 빈의 이벤트 라이프사이클
- 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
- 소멸전 콜백: 빈이 소멸되기 직전에 호출
- 인터페이스 InitializingBean, DisposableBean
InitializingBean
은afterPropertiesSet()
메서드로 초기화를 지원한다.DisposableBean
은destroy()
메서드로 소멸을 지원한다.- 참고: 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법 들이 있어서 거의 사용하지 않는다.
- 빈 등록 초기화, 소멸 메서드
@Bean(initMethod = "init", destroyMethod = "close")
처럼 초기화, 소멸 메서드를 지정할 수 있다.
- 애노테이션 @PostConstruct, @PreDestroy
- 최신 스프링에서 가장 권장하는 방법이다.
- 애노테이션 하나만 붙이면 되므로 매우 편리하다.
- 패키지를 잘 보면
javax.annotation.PostConstruct
이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다. - 컴포넌트 스캔과 잘 어울린다.
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.
-
빈 스코프란?
- 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
- 웹 관련 스코프
- request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
- session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
- application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.
-
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점 -> Provider로 문제 해결
- 싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만, 프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고, 더는 관리하지 않는다.
- 따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때
@PreDestroy
같은 종료 메서드가 전혀 실행되지 않는다.
-
웹 스코프
-
스코프와 프록시