강의를 수강한 후, 전반적인 흐름 및 나중에 복기할 수 있게 정리해보려한다..(과외 학생 설명하듯이 진행해보겠다)
"승수씨.. 정말 미안한데 부탁이 있어요. 우리가 급히 기능을 만들어야해요.
회원 / 주문&할인정책 을 급하게 만들어야하는데... 부탁해요.
회원 같은 경우
1. 회원을 가입하고 조회할 수 있어야하고
2. 회원은 일반과 VIP 두 가지 등급이 있어요.
3. 회원 데이터는 자체 DB를 쓸 수도 있고, 외부 시스템을 사용할 수도 있어요. 아직 미정이에요 ㅠㅠ(젠장할)
주문&할인 정책의 경우
1. 회원은 상품을 주문할 수 있어야해요
2. 회원 등급에 따라 할인 정책이 적용되는데
3. 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 할인 정책을 적용할거에요. 근데 나중에 변경될 수도 있어요.
사람일이라는게 어떻게 될지 모르잖아요? ㅎㅎㅎ(ㅋㅋ..)
4. 할인 정책은 변경 가능성이 원래 높아요. 아직 할인 정책을 확정짓기가 힘들어보여요.
지금 상황을 보면 회원 데이터, 할인 정책들이 지금 결정하기 어려운 부분들이다.
그렇다고 그러한 정책들이 결정될 때까지 기다릴 순 없다.
해결책은, 인터페이스를 만들어서 개요를 만들어놓고
구현체를 나중에 만들어서 언제든지 갈아끼울 수 있도록 설계하면 된다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
현재 MemberRepository는 우리가 그냥 로컬을 구현한 MemoryMemberRepository() 구현체를 활용하기로 했고
DiscountPolicy는 고정 할인 정책을 구현한 FixDiscountPolicy() 구현체를 활용하기로 했다.
근데 여기서 기획자가 고정 할인 정책말고 할인율 정책으로 바꿔달라했다 해보자.
그렇다면 구현체를 바꿔주면 된다.
public class OrderServiceImpl implements OrderService{
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
여기서 우리가 주의깊게 봐야할 점이 있다.
우리는 역할과 구현을 인터페이스와 구현체로 구분을 해놓았다.
다형성도 잘 활용했다.
OCP, DIP를 준수하였는가?
여기서 까먹었을까봐 짚고 넘어가자면
* OCP (Open-Closed Principle) 개방 폐쇄 원칙 : 기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다.
- > 지금 코드의 경우 확장해서 변경하면, 클라이언트 코드에 영향을 준다. 왜냐하면 우리(클라이언트)가 구현체를 변경해줘야하니까.
* DIP(Dependency Inversion Principl) 의존 역전 원칙 : 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다.
저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야 한다.
-> 현재 엄연히 DiscountPolicy가 인터페이스이므로 고수준이다.
근데 저수준인 구현체에 의존하고 있다.
지금 보면은 클라이언트인 OrderServiceImpl은
DiscountPolicy 인터페이스 뿐만이 아니라 FixDiscountPolicy라는 구현 클래스에도 의존하고 있는 것이다.
그래서 FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간
클라이언트인 OrderServiceImpl에서도 변경이 일어난다.
이를 해결하기 위해서는 어떻게 해야할까?
우선 우리가 원하는 목표는 클라이언트가 구현 클래스가 아닌, 인터페이스에만 의존하도록 의존관계를 변경해야한다.
public class OrderServiceImpl implements OrderService{
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
그냥 인터페이스에만 의존하도록 변경했다.
근데 이렇게 되면, 구현체가 없어서 코드를 실행할 수 없다.
그렇기에 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현체를 대신 생성하고 주입해줘야한다.
이걸 기획자라고 비유해서 표현해보겠다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
현재 AppConfig 클래스는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성해준다.
그리고 생성한 객체의 인스턴스를 생성자를 통해 주입해주고 있다
.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
설계 변경으로 인해서 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않는다.
단지 인터페이스인 MemberRepository에만 의존하고 있다.
클라이언트인 MemberServiceImpl 입장에선 생성자를 통해 어떤 구현 객체가 들어올지 예상할 수가 없다.
생성자를 통해서 어떤 구현 객체를 주입할지는 오로지 외부(AppConfig)에서 결정된다
이제, MemberServiceImpl은 의존관계에 대한 고민은 외부에 맡기고, 실행에만 집중하면 된다.
그러면 이제, 할인 정책을 고정에서 정률로 변경해보자.
아까는 구현체를 변경해야했어서 문제가 되었다.
하지만 이제는 외부인 AppConfig에서만 변경하면 된다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
사용 영역에선 어떠한 코드도 변경할 필요가 없고, 구성 영역에서 변경이 일어난다.
AppConfig의 등장으로 애플리케이션이 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리되었다.
이제 스프링으로 코드를 변경해보겠다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
AppConfig에 설정을 구성한다는 뜻의 @Configuration을 붙여줬다.
또 각 메서드에 @Bean을 붙여주었다. 이러면 스프링 컨테이너에 스프링 빈으로 등록한다.
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
public class OrderApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// OrderService orderService = appConfig.orderService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
}
}
Application을 스프링 컨테이너라고 한다
기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 의존관계 주입을 해주었지만,
이제부터는 스프링 컨테이너를 통해서 사용한다.
스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
여기서 @Bean이라 적힌 메서드를 전부 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
스프링 빈은 @Bean이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다.
이제는 스프링 컨테이너를 통해 스프링 빈(객체)을 찾아야한다.
스프링빈은 ApplicationContext.getBean()을 사용해 찾는다.
왜 스프링 컨테이너를 사용할까? 어떤 장점이 있을까?
스프링 컨테이너에 대해 알아보자.
// 스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
- ApplicationContext를 스프링 컨테이너라 한다.
- ApplicationContext는 인터페이스이다
생성과정)
1. 스프링 컨테이너 생성
new AnnotationConfigApplicationContext(AppConfig.class)
스프링 컨테이너를 생성할 때는 구성 정보를 지정해줘야한다.
여기선 Appconfig.class를 구성 정보로 주었다.
2. 스프링 빈 등록
스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
빈 이름은 메서드 이름을 사용하고,
우리가 원하면 직접 부여도 할 수 있다( @Bean(name ="memberService2"))
3. 스프링 빈 의존관계 설정
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("memberService",MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByNameX() {
//ac.getBean("xxxxx", MemberService.class);
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
ac.getBean("xxxxx", MemberService.class));
}
}
BeanFactory와 ApplicationContext에 대해 알아보자.
BeanFactory의 경우 스프링 컨테이너의 최상위 인터페이스이다.
스프링 빈을 관리하고 조회하는 역할을 하며, getBeanI()을 제공한다.
우리가 썼던 기능들의 대부분은 BeanFactory가 제공하는 기능들이다.
ApplicationContext의 경우 BeanFactory의 기능들을 모두 상속받아서 제공한다.
그렇다면 우리는 왜 BeanFactory를 잘 안쓰고, ApplicationContext를 주로 쓸까?
애플리케이션을 개발할 때, 빈을 관리하고 조회하는 기능은 물론, 수 많은 부가 기능들이 필요하다.
ApplicationContext의 경우
메세지 소스를 활용한 국제화 기능(한국에서 오면 한국어로 출력 등등..)
환경변수(로컬, 개발, 운영 등을 구분해서 처리)
애플리케이션 이벤트(이벤트를 발행하고 구독하는 모델 지원)
편리한 리소스 조회(파일, 외부 등에서 리소스 편리하게 조회) 등을 지원하므로
실제론 ApplicationContext를 쓴다고 인지하면 된다.
스프링이 다양한 설정 형식을 지원하는 이유의 중심엔 BeanDefinition이라는 추상화가 있다.
XML이든, 자바든 해당 코드를 읽어서 BeanDefinition을 만든다.
컨테이너 입장에선 그저 BeanDefinition을 통해 스프링 빈을 생성한다.
하지만 실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없다.
우리 입장에선, 스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용한다 정도만 알고 있으면 된다.
대부분의 스프링 애플리케이션은 웹 애플리케이션이다.
웹 애플리케이션의 경우 보통 여러 고객이 동시에 요청을 한다.
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);
}
}
우리가 만든 AppConfig를 쓰려면 요청을 할 때마다 객체로 새로 생성해야한다.
이건 메모리 낭비가 심하다.
해결 방안은 해당 객체가 딱 1개만 생성하게 하고, 공유하도록 설계하면 된다.
-> 싱글톤 패턴
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 singletonServiceTest() {
//private으로 생성자를 막아두었다. 컴파일 오류가 발생한다.
//new SingletonService();
//1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
//2. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
}
하지만 싱글톤 패턴의 문제점은 분명히 존재한다.
우선 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
또 클라이언트가 구현 클래스에 의존하고 있기 때문에 DIP이나 OCP 원칙을 위반할 가능성이 높다.
또 내부속성을 변경하거나 초기화하기 어렵고, private 생성자이기 때문에 자식 클래스를 만들기 어렵다.
이러한 문제들을 해결하기 위해, 스프링 컨테이너가 등장했다. 스프링 컨테이너는 싱글톤 컨테이너이다.
스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
그 이유는 , 우리가 아까 컨테이너를 생성하는 과정(
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);)
에서 객체를 하나만 생성하고 관리한다.
void springContainer(){
ApplicationContext ac=new AnnotationConfigApplicationContext(AppConfig.class);
//1. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService1=ac.getBean("memberService",MemberService.class);
//2. 조회: 호출할 때 마다 같은 객체를 반환
MemberService memberService2=ac.getBean("memberService", MemberService.class);
}
싱글톤 패턴이든, 싱글톤 컨테이너이든 간에 논리는 싱글톤이고
이는 하나의 같은 객체 인스턴스를 공유하기 때문에, 싱글톤 객체는 상태를 유지하게 설계하면 된다.
(클라이언트에 의존적인 필드가 있다거나, 클라이언트가 값을 변경하게 한다거나,...)
다시 한번 AppConfig 코드를 보자.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
...
}
MemberService와 OrderService에서 각각 memberRepository()를 통해 new MemoryMemberRepository()를 호출한다.
이렇게 되면 각각 다른 2개의 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것 같아 보인다.
실제로 그렇다. 하지만 우린 아까 스프링 컨테이너는 싱글톤이라 했다.
어떻게 유지할까?
핵심은 @Configuration이다.
@Configuration이 붙으면 해당되는 내가 만든 클래스가 아니라 스프링이 바이트코드 조작 라이브러리를 활용해서
AppConfig클래스(내가 만든 클래스)를 상속받은 임의의 다른 클래스를 만들고, 그 클래스를 스프링빈으로 등록한 것이다.
만약 @Configuration이 없고 그저 @Bean만 적용한다면 각자 따로따로 생성되서 싱글톤을 보장하지 않는다.
'스프링 강의 필기 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
섹션 7 - (2). 의존관계 자동 주입 (0) | 2022.07.13 |
---|---|
섹션 7 -(1) 의존관계 자동 주입 (0) | 2022.07.13 |
섹션 6. 컴포넌트 스캔 (0) | 2022.07.13 |
섹션 5 - 싱글톤 컨테이너 (0) | 2022.07.09 |
섹션4 - 스프링 컨테이너와 스프링 빈 (0) | 2022.07.09 |