✅ 이전
순수 자바 기반으로 회원, 주문, 할인 정책 구현
✅ 내용
- DIP와, OCP 가 잘 지켜지지 못했다.
- 해결방안은 무엇이고, 어떻게 적용할 것인가?
📌새로운 할인 정책 확장(OCP와, DIP 위반)
- 기존 : 항상 1000원 할인
- 변경 : 10% 할인
새로운 할인 정책을 구현한 RateDiscountPolicy를 추가해주기만 하면 된다.
할인 정책 추가
RateDiscountPolicy 구현 클래스 추가
package discount;
import com.example.core.member.Grade;
import com.example.core.member.Member;
public class RateDiscountPolicy implements DisCountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent/100;
}else{
return 0;
}
}
}
테스트 구현
package discount;
import com.example.core.member.Grade;
import com.example.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야한다.") //junit5 부터 추가 됌
void vip_o(){ //vip는 잘 적용되야함
//given
Member member = new Member(1L,"memberVIP", Grade.VIP);
//when
int discount = discountPolicy.discount(member,10000);
//then
assertThat(discount).isEqualTo(1000);
}
//실패테스트도 만들어봐야함!
@Test
@DisplayName("VIP가 아니면 10% 할인이 되면 안된다.")
void vip_x(){ //vip는 잘 적용되야함
//given
Member member = new Member(2L,"memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member,10000);
//then
// Assertions.assertThat(discount).isEqualTo(1000); //실패함
assertThat(discount).isEqualTo(0);
}
}
문제점
👧 새로운 할인 정책을 변경하려면 코드 수정이 필요하다. 코드 수정이 필요한 것은 OCP, DIP 객체지향 설계 원칙을 준수하지 못한 것을 의미한다.
1. DIP 위반 <항상 추상화에 의존해라>
클래스 의존 관계가 추상(인터페이스)뿐만 아니라 실체 구현 클래스에도 의존하고 있다.
추상 클래스 : DiscountPolicy / 구현 클래스 : FixDiscountPolicy, RateDiscountPolicy
2. OCP 위반 <변경하지 않고 확장 가능>
FixDiscountPolicy -> RateDiscountPolicy로 변경하면 OrderServiceImpl 소스코드도 변경되어야한다.
📌OCP, DIP 객체지향 설계 원칙을 준수 문제 해결해보자.
인터페이스에만 의존하도록 의존 관계를 변경하면 된다. but 실제 작동을 시켜보면 Null 포인트 에러가 발생한다.
해결을 위해 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy 구현 객체를 생성하고 주입해주어야 한다.
관심사 분리 하자!
- 책임을 확실히 분리해야 한다.
- EX) 배우는 본인의 역할인 배역만 집중, 배우는 상대 배우가 어떤 배우더라도 똑같이 공연할 수 있어야 함
- EX) 공연 기획자가 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 책임을 담당한다.
AppConfig 사용 하자!
👧 AppConfig는 애플리케이션의 전체 동작을 구성하는 구현 객체를 생성하고, 연결을 책임지는 별도의 설정 클래스
package com.example.core;
import com.example.core.member.MemberRepository;
import com.example.core.member.MemberService;
import com.example.core.member.MemberServiceImpl;
import com.example.core.member.MemoryMemberRepository;
import discount.DisCountPolicy;
import discount.FixDiscountPolicy;
import order.OrderService;
import order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),disCountPolicy());
}
public DisCountPolicy disCountPolicy(){
return new FixDiscountPolicy();
}
}
- AppConfig에 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자 주입을 통해서 연결해준다.
- 객체의 생성과 연결은 AppConfig가 담당하게 된다.
- DIP 완성 : MemberServiceImpl은 MebmerRepository인 추상에만 의존하면 되고, 구체 클래스를 몰라도 된다
- 관심사 분리 : 객체를 생성하고 연결하는 역할과, 실행하는 역할이 분리된다.
실제 AppConfig로 새로운 구조와 할인 정책 적용
새로운 정책을 적용할 거면 Appconfig의 할인 인터페이스의 disCountPolicy에서 RateDiscountPolicy로 변경만 하면 된다.
📌 어떻게 좋은 객체 지향 설계의 5가지 원칙 적용 됐나( SRP, DIP, OCP 적용)
SRP 단일 책임 원칙
- 한 클래스는 하나의 책임만 가져야 한다.
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
- 클라이언트 객체는 실행하는 책임만 담당
DIP 의존관계 역전 원칙
- 추성화에 의존하고, 구체화에 의존하면 안 된다
- AppConfig가 객체 인스턴스를 클라이언트 코드 대신 생성 후, 클라이언트 코드에 의존관계를 주입해서 문제 해결
OCP 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀있어야 한다.
-애플리케이션을 사용 영역과 구성 영역으로 나눔(AppConfig)
- AppConfig가 의존관계를 변경하면 클라이언트 코드에 주입하여 클라이언트 코드는 변경하지 않아도 된다.
📌스프링 전환에 앞서 용어 설명 (IoC, DI, 컨테이너)
IoC(Inversion of Control) 제어의 역전
프레임워크가 내 코드를 대신 호출해줌, 제어권이 뒤바뀌는 것
- AppConfig가 등장 후 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.
- 프로그램에 대한 제어 흐름에 대한 권한은 AppConfig가 가지고 있다.
- 이렇게 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라 한다.
✅ 프레임워크 vs 라이브러리
- 프레임워크가 내가 작성한 코드를 제어하고, 실행하면 프레임워크 EX) JUnit
- 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리
의존 관계 주입 DI(Dependency Injection)
- 서비스 객체는 인터페이스에 의존하기 때문에, 어떤 구현 객체가 사용될지 모른다.
- 의존 관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체 (인스턴스) 의존 관계 둘을 분리해야 한다.
- 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
- 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
- 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
👧 의존관계란?
한 객체가 다른 객체를 사용을 의존이라 하며, 사용할 객체를 직접 쓰지 않고 외부에서 주입받는 형태를 의존 관계 주입이라 한다.
IoC 컨테이너, DI 컨테이너
- AppConfig처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 Ioc 컨테이너 or DI 컨테이너
- 의존관계 주입에 초점을 맞춰 최근하는 주로 DI 컨테이너라 함
- 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.
📌 스프링으로 전환하기
- 순수한 자바 코드만으로 DI 적용 -> 스프링으로 사용
package com.example.core;
import com.example.core.member.MemberRepository;
import com.example.core.member.MemberService;
import com.example.core.member.MemberServiceImpl;
import com.example.core.member.MemoryMemberRepository;
import discount.DisCountPolicy;
import discount.FixDiscountPolicy;
import discount.RateDiscountPolicy;
import order.OrderService;
import order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration //설정정보
public class AppConfig {
@Bean //스프링 컨테이너에 등록
public MemberService memberService(){ //멤버 서비스
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() { // 나중에 DB로 바꿀 경우 여기 코드만 바뀌면된다.
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){ //주문서비스
//OrderServiceImpl안에 MemberMeberRespository와 FixDiscountPolicy가 넘어감
return new OrderServiceImpl(memberRepository(),disCountPolicy());
}
@Bean
public DisCountPolicy disCountPolicy(){
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
public class MemberApp {
public static void main(String[] args) {
//객체들 관리해주는 컨테이너, AppConfig에 있는 Bean들을 스프링 컨테이너에 집어넣어서 관리해줌
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());
}
}
- AppplicationContext를 스프링 컨테이너라 한다.
- 기존에 개발자가 AppConfig를 사용했지만 이제는 스프링 컨테이너를 사용
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용하고 @Bean 이 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록한다.(스프링 빈)
- application.getBean() 메서드로 빈 객체를 찾아 쓸 수 있다.
REFERENCE
'공부 > Spring 핵심 원리' 카테고리의 다른 글
컴포넌트 스캔으로 의존 관계를 자동 주입해보자. (0) | 2021.08.31 |
---|---|
싱글톤 패턴을 보장해주는 스프링 컨테이너와 @Configuration (0) | 2021.08.29 |
스프링 컨테이너와 스프링 빈, 등록 된 빈들을 조회 해보자 (0) | 2021.08.16 |
스프링과 핵심 원리 - 순수 자바 예제로 객체 지향으로 설계해보자 (0) | 2021.08.14 |
스프링의 핵심 개념 - 객체 지향 설계는 어떤 관련이 있을까? (0) | 2021.08.06 |