스프링 핵심 원리 - 기본편
인프런 김영한님 Spring 로드맵 2번 강의
섹션1. 객체 지향 설계화 스프링 ~ 섹션4. 스프링 컨테이너와 스프링 빈
Open/closed principle
// MemoryMemberRepository에서 JdbcMemberRepository로 변경할 때
// 아래와 같은 경우 Service인터페이스에 코드변경이 불가피함.
public class MemberService {
// private MemberRepository memberRepository = new MemoryMemberRepository(); 변경 전
private MemberRepository memberRepository = new JdbcMemberRepository(); // 변경 후
}
의존이란?
A가 B를 알고있다면 A는 B에 의존한다고 표현한다.
=> A가 B를 알고있는 상태에서 B에 변경이 생기면 A에도 변경이 필요하기 때문이라 생각하면 됨.
가상 시나리오를 바탕으로 비즈니스 요구사항 정리
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루
고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
=> 회원 저장소 역할과 할인 정책 역할을 추상화, 이를 구현하는 구현체를 만들어 적용하면 OCP와 DIP를 지키는 프로그램을 만들 수 있다.
VIP회원의 주문에만 정액 할인 정책 1000원을 적용하는 단계까지 만든 상황에서의 프로젝트 구조.
변동 가능성이 높은 할인 정책, DB 연동 기능은 구현체를 이용하여 쉽게 갈아끼울 수 있도록 구조가 설계되어 있다.
할인 정책 개편에 따라 정액 할인 정책을 정률 할인 정책으로 변경한다.
- OrderService -
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
- OrderServiceImpl -
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@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);
}
}
위와 같이 OrderServiceImpl
에서 클라이언트인 DiscountPolicy
인터페이스만 의존하는 것이 아닌 구체 클래스인 RateDiscountPolicy
클래스도 의존하고 있음을 확인할 수 있다.
결국 FixDiscountPolicy
를 RateDiscountPolicy
로 변경하면 DIP
와 OCP
를 모두 위반하게 된다.
공연으로 비유한다면...
배우
와 별개의 책임을 가진 기획자
라는 역할을 추가한다.AppConfig
클래스를 이용하여 생성자 주입을 하도록 변경.final키워드와 멤버변수 초기화
final
키워드는 변수의 값을 변경하지 못하게 만듦(=상수).
상수는 사용 전 반드시 초기화가 선행되어야 함.
이때 인스턴스 변수에final
키워드가 사용될 경우 생성자에서 초기화가 가능함.
구현체를 변경할 경우
- MemberServiceImpl -
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);
}
}
- OrderServiceImpl -
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);
}
}
클라이언트에 영향을 주지 않고 구현체만 바꿀 수 있도록 변경.
final
키워드로 인스턴스 변수 초기화 시 생성자를 통한 초기화가 가능하게 한 후 AppConfig
클래스에서 해당 구현체를 선택하여 생성하도록 함.
- AppConfig -
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemoryMemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
=> 이와 같은 구조로 설계하면 구현체를 다른 것으로 변경해도 AppConfig
클래스만 변경하면 된다.
AppConfig
클래스는 구성 영역을, 나머지는 사용 영역을 담당하게 되는 셈.
- 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());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- MemberApp -
public class MemberApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("member = " + member.getName());
System.out.println("findMember = " + findMember.getName());
}
}
- OrderApp -
public class OrderApp {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = ac.getBean("memberService", MemberService.class);
OrderService orderService = ac.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order);
}
}
앞서 작성했던 코드를 위와 같이 변경한다.
여기서 사용된 @Bean
과 ApplicationContext
에 대해서는 개념정리가 필요하므로 아래 섹션4에서 정리하도록 한다.
스프링 컨테이너 생성 과정
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
ApplicationContext
가 스프링 컨테이너에 해당한다. 정확히 일치하는 개념은 아니지만 요즘은 거의 동일시 하는 편이다.ApplicationContext
는 인터페이스로, AnnotationConfigApplicationContext
는 구현체가 된다.AnnotationConfigApplicationContext
는 어노테이션 기반으로 스프링을 구성할때 사용하는 구현체이다.BeanFactory
라는 최상위 객체와 ApplicationContext
를 구분해서 사용했다.BeanFactory
를 직접 사용하는 경우가 드물어 스프링 컨테이너 = ApplicationContext
라고 보면 된다.@Bean
어노테이션을 이용하면 해당 객체를 스프링 컨테이너에 등록하여 관리하도록 한다.테스트 클래스 생성하여 현재 스프링 컨테이너에 등록된 빈을 확인해보는 예제.
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);
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name = " + beanDefinitionName + "object = " + bean);
}
}
}
}
findApplicationBean()
메서드를 실행시킨 결과.
AppConfig
클래스에서 등록한 빈과 @Service
, @Configuration
어노테이션을 이용해 등록한 빈들을 직접 확인할 수 있다.
findAllBean()
메서드 실행 시 스프링 내부에서 등록된 빈까지 모두 확인이 가능하다. (사진은 생략)
public class ApplicationContextBasicFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName(){
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("이름없이 타입으로만 조회")
void findBeanByType(){
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체타입으로 조회")
void findBeanByName2(){
// 구체타입을 조회 시 유연성이 떨어질 수 있음에 유의.
MemberServiceImpl memberService = ac.getBean(MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈이름으로 조회x")
void findBeanByNameX(){
// MemberService memberService = ac.getBean("xxx", MemberServiceImpl.class);
// assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
// assertThrows : 우측 로직에서 발생한 오류가 좌측에 기입한 오류와 같으면 테스트 성공. 오류안나도 실패.
assertThrows(NoSuchBeanDefinitionException.class, ()-> ac.getBean("xxx", MemberService.class));
}
}
테스트 영역에 새로 클래스를 만들어 확인.
등록된 빈을 검색한다.
이때 이름으로 조회, 타입으로 조회, 구체타입으로 조회 등 다양한 조건으로 조회할 수 있다.
맨 아래의 findBeanByNameX()
메서드는 실패 테스트를 위한 것으로, assertThrows(발생할 것으로 예상되는 Exception클래스, Exception을 발생시킬 코드);
의 형식으로 테스트한다.
코드실행시 예상한 Exception이 발생하면 테스트 성공.
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 중복 오류가 발생한다.")
void findBeanByTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면 빈 이름을 지정하면 된다.")
void findBeanName() {
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();
}
}
}
findAllBeanByType()
메서드 실행결과.
타입으로 빈을 찾을 때 동일한 타입이 2개이상이면 빈 이름을 지정해 찾거나 특정 타입을 모두 검색하면 된다.
참고
class 내에서 static class 사용했다는 것은 이 클래스 내에서만 해당 클래스를 사용한다는 의미를 가짐.
Object
타입을 조회하면 모든 스프링 빈을 조회한다.public class ApplicationContextExtendsFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다.")
void findBeanByParentTypeDuplicate(){
DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class, ()->ac.getBean(DiscountPolicy.class));
}
@Test@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 빈 이름을 지정하면 된다.")
void findBeanByParentTypeBeanName(){
DiscountPolicy reteDiscountPolicy = ac.getBean("reteDiscountPolicy", DiscountPolicy.class);
assertThat(reteDiscountPolicy).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));
}
}
@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 reteDiscountPolicy(){
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy(){
return new FixDiscountPolicy();
}
}
}
findAllBeanByParentType()
메서드 실행결과.
실제로 개발과정에서 직접 빈 객체를 찾을 일은 거의 없지만 빈 객체를 조회했을때 상속 관계에서는 자식 타입까지 모두 조회된다, 빈 이름 혹은 타입으로 검색할 수 있다는 등의 개념을 알아두어야 하기 때문에 빈 검색 실습을 진행하였음.
Bean Factory
나 Application Context
를 스프링 컨테이너
라 한다.getBean()
을 제공한다Bean Factory
가 제공하는 기능이다Bean Factory
를 직접 사용할 일은 거의 없다.Application Context
를 사용한다.Bean Factory
의 기능을 모두 상속받아 제공한다Bean Factory
의 빈을 관리하고 검색하는 기능에 더해 수많은 부가기능을 제공한다Application Context
는 Bean Factory
만 아니라 MessageSource
, EnvoironmentCapable
, ApplicationEventPublisher
등 다양한 인터페이스를 상속받아 제공한다new AnnotationConfigApplicationContext(AppConfig.class)
AnnotationConfigApplicationContext
클래스를 사용하면서 자바 코드로 된 설정 정보를 넘기면 된다.public class XmlAppContext {
@Test
void xmlAppContext(){
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
- 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 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>
xml로 설정한 내용과 앞서 실습에서 사용한 AppConfig.class
에서 설정한 내용은 동일하다.
스프링은 BeanDefinition
이라는 것으로 스프링 메타 정보를 추상화 한다.
스프링 빈을 만드는 방법은 크게 두 가지.
Factory Bean
을 이용해 등록 => 예) 어노테이션을 이용한 방법window os 기준 단축키
- 리스트 형태의 객체가 있을 때 자동으로
for
문 생성 : 객체 아래서iter
입력 후Teb
클릭- 선택한 영역을 영역 바로 아래 붙여넣기 :
ctrl
+D
- import static :
alt
+Enter
- 이전에 작업한 파일으로 이동 :
ctrl
+E
- 코드 컴플리션으로 넘어가기 :
ctrl
+shift
+Enter
- 객체 생성 시 좌항 자동완성 :
alt
+Enter
도서 - 웹 개발 워크북 (구멍가게 코딩단 저)