목차
- 새로운 할인 정책 개발
- 새로운 할인 정책 적용과 문제점
- 관심사의 분리
- AppConfig 리팩터링
- 새로운 구조와 할인 정책 적용
- 전체 흐름 정리
- 좋은 객체 지향 설계의 5가지 원칙의 적용
- IoC, DI, 그리고 컨테이너
- 스프링으로 전환하기
핵심은 객체지향 원리 적용하기
새로운 할인 정책 개발
정률 할인 정책 개발
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
int discountPercent = 10; //10프로 할인
@Override
public int discount(Member member, int price) { //컨트롤 쉬프트 티 =테스트 클래스 생성 단축키
if (member.getGrade() == Grade.VIP){
return price*discountPercent/100; //vip이면 10할인
}
else{
return 0;
}
}
}
새로운 할인 정책 적용과 문제점
새로운 정책 할인 적용 및 테스트
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService{
MemberRepository memberRepository = new MemberMemoryRepository();
//DiscountPolicy discountPolicy = new RateDiscountPolicy(); 변경 전
DiscountPolicy discountPolicy = new RateDiscountPolicy(); //변경 후
@Override
public Order orderCreate(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
테스트
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class RateDiscountPolicyTest {
RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("vip 맞을시") //테스트함수명대신 나오는 스트링
public void 퍼센트할인테스트() {
Member member = new Member(1L, "승빈", Grade.VIP); //새로운 멤버 생성
int discount = rateDiscountPolicy.discount(member, 10000);
Assertions.assertThat(discount).isEqualTo(1000); //할인 가격이 1000이 맞는지 확인
}
//성공 테스트도 중요하지만 실패 테스트도 꼭 만들어 봐야한다.
@Test
@DisplayName("vip 아닐시")
public void 퍼센트할인테스트2() {
Member member = new Member(1L, "승빈", Grade.BASIC);
int discount = rateDiscountPolicy.discount(member, 10000);
Assertions.assertThat(discount).isEqualTo(0);
}
}
문제점
//DiscountPolicy discountPolicy = new RateDiscountPolicy(); 변경 전
DiscountPolicy discountPolicy = new RateDiscountPolicy(); //변경 후
- 새로운 정채을 변경할때 새로운 정책의 구현채만 바꾸어주면 되지만 여기에는 아래와 같은 문제점이 있다
- OCP,DIP 객체지향 설계 원칙 위반
- DIP : OrderServiceImpl클래스는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨거같지만 사실은 인터페이스 뿐만아니라 구현 클래스에도 의존하고 있다.
- OCP: 지금 코드는 기능을 확장해서 변경하려면, 클라이언트 코드에 영향을 준다.
- OCP,DIP 객체지향 설계 원칙 위반
해결 방안
이 문제를 해결하기위해선 누군가가 클라이언트인 OrderService에 DiscountPolicy이 구현 객체를 대신 생성하고 주입해주어야 한다.
(AppConfig라는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 , 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!)
관심사의 분리
애플리케이션의 전체 동작 방식을 구성(config) 하기위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.
AppConfig
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
//인텔리제이 단축키 : 컨트롤 e 과거 히스토리나와서 빠르게 화면 전환가능
/*애플리케이션의 전체 동작 방식을 구성(config)하기위해, config 클래스에서 구현객체를 구성하고, 연결하는 책임을
가지는 별도의 설정 클래스를 만든다.*/
/*이전에는 인터페이스의 구현객체가 필요한기능의 구현객체를 직접 new해서 생성했지만, 이제는 AppConfig
클래스가 동작에 필요한 구현객체를 생성한다.*/
//그리고 AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입(연결)해준다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImp(new MemberMemoryRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemberMemoryRepository(),new FixDiscountPolicy());
}
}
MemberServiceImpl
package hello.core.member;
public class MemberServiceImp implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImp(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
// 생성자 주입을 통해서 MemberRepository에 어떤 객체가 들어갈지 결정한다. -> 의존관계에 대한 고민은 외부에서 결정 ->실행에만 집중
// DIP 완성
@Override
public void 회원가입(Member member) {
memberRepository.save(member);
}
@Override
public Member 회원조회(Long memberId) {
return memberRepository.findById(memberId);
}
}
OrderServiceImpl
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
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 orderCreate(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
구현체에서 필요한 구현체(기능)들을 AppConfig에서 정해진 구현체를 파라미터로 받아 각각의 인터페이스의 구현체를 할당받았다. 따라서 MemberServiceImpl,OrderServiceImpl은 필요 구현체의 인터페이스만 의존함으로 해당 구현체의 로직에만 집중 할 수 있다. 또 DIP원칙을 지킬 수 있게 된다.
테스트 코드
MemberServiceTest
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join(){
//given
Member member = new Member(1L,"승빈",Grade.VIP);
//when
memberService.회원가입(member);
Member member1 = memberService.회원조회(1L);
//then
Assertions.assertThat(member).isEqualTo(member1);
}
}
테스트코드에서 @BeforeEach 는 각테스트 코드가 실행되기 전에 실행된다.
- 테스트코드도 AppConfig에서 memberService()를 호출해 리턴받은 구현체를 memberService에 할당해 테스트한다.OrderServiceTest
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.*;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void join(){
//given
Long memberId = 1L;
Member member = new Member(memberId, "승빈", Grade.VIP);
memberService.회원가입(member);
//when
Order order = orderService.orderCreate(memberId, "물통", 10000);
//then
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
- AppConfig에서 memberServic(),orderService()를 호출해 리턴받은 구현체를 memberService,orderService를 할당해 테스트코드에 사용한다.

### 정리
- AppConfig를 통해서 관심서를 확실하게 분리했다.
- 각각의 Impl은 기능을 실행하는 책임만 지면 된다.
<br>
<br>
<br>
# AppConfig 리팩터링
### 리팩터링 전
~~~java
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImp(new MemberMemoryRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemberMemoryRepository(),new FixDiscountPolicy());
}
}
- 리팩터링 전은 전체 구성의 역할이 명확하게 구분이 힘들다.리팩터링 후
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImp(memberRepository());
}
public MemberRepository memberRepository(){
return new MemberMemoryRepository();
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
}
//리팩토링 해줌으로써 역활이 명확하게 구별된다.
- 리팩터링을 해줌으로서 전체적인 열할을 구분하기 쉬워 졌다.
-new MemberMemoryRepository() 중복이 해결되며 MemberRepository의 구현체 변경이 필요할 경우 구현체만 MemberRepository의 구현체만 바꾸어 주면 된다.
<br>
<br>
<br>
# 새로운 구조와 할인 정책 적용
- 처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
~~~java
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberMemoryRepository;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImp(memberRepository());
}
public MemberRepository memberRepository(){
return new MemberMemoryRepository();
}
public DiscountPolicy discountPolicy(){
// return new RateDiscountPolicy();
return new RateDiscountPolicy(); // 정률(새로운)할인정책으로 변경
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
}
- AppConfig(구성영역)을 따로 만들어 줌으로 써 기능영역(클라이언트 코드)을 변경할 필요없이 구성영역의 코드만 변경해줌으로써 DIP,OCP원칙을 지키면서 새로운 정책할인으로 변경할 수 있게 되었다.
전체 흐름 정리
- 새로운 할인 정책 개발
- 새로운 할인 정책 적용과 문제점
- 관심서의 분리
- AppConfig 리팩터링
- 새로운 구조와 할인 정책 적용
새로운 할인 정책 개발
- 다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것은 문제가 없었다.
새로운 할인 정책 적용과 문제점
- 다형성덕에 새로운 정책코드를 추가 개발하는 것에는 문제가 없지만, 이를 적용하려하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야해서 (인터페이스와 구현클래스를 함께의존)
인터페이스만 의존해야하는 DIP법칙을 위반하게 되었다.
관심서의 분리
- DIP 를 지키기 위해 사용 영역과 구성영역을 나눠 전체 동작 방식을 구성하는 AppConfig클래스를 생성하여 구현객체를 생성하고, 연결하는 책임을 주었다.
AppConfig 리팩터링
- 리팩터링을 함으로써 구성 정보에서 역할과 구현을 명확하게 분리
- 역할이 잘 드러난다.
- 중복이 제거되었다.
새로운 구조와 할인 정책 적용
- FixDiscountPolicy 에서 RateDiscountPoric로 할인정책 변경
- 기존에는 Impl클래스에서 필요한 구현객체를 변경하였지만 이제는 구성영역인 AppConfig에서만 변경이 가능해 졌다.
정리- 전체적인 구성영역(AppConfig)를 만듬으로써 DIP,OCP원칙을 지키면서 새로운 정책으로 변경할 수 있게 되었다.
좋은 객체 지향 설계의 5가지 원칙의 적용
지금까지 3가지 SRP,DIP,OCP원칙을 적용해 보았다.
SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
- 클라이언트 객체는 실행하는 책임만 담담
DIP 의존관계 역전 원칙
프로그래머는 "추상화에 의존해야지 , 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
- AppConfig로 구성영역을 따로 나누기 전은 Impl들이 필요한 기능이 있을떄 인터페이스와 구혀늘래스 둘다 의존 했기때문에 추상화에만 의존해야하는 DIP원칙을 위반 했다.
- AppConfig가 클라이언트 코드 대신 클라이언트 코드에 의존관계를 주입해 DIP원칙을 따라 문제를 해결했다.
OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 구성요소를 따로 담당한는 AppConfig가 있기 때문에 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.
IoC, DI, 그리고 컨테이너
IoC(Inversion of Control) 제어의 역전
- 이전에는 OverServiceImpl에서 직접 필요한 구현객체를 생성했지만 AppConfig를 만든 이후로는 AppConfig가 OrderService인터페이스에 다른 구현객체를 생성하고 실행하여 프로그램의 제어의흐름을 가저간다.
OverServiceImpl는 로직만 실행한다. - 이렇게 프로그램의 제어의 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IOC)라고 한다.
프레임워크 vs 라이브러리
- 프레임워크 : 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크이다.(JUnit) = 프레임워크는 자기만의 동작 흐름이있고 개발자는 이 개발흐름에 맞춰 개발한다.
- ex JUnit에는 @BeforeEach,@Test가 있고
@Test 가 실행되기전 @BeforeEach가 실행되야한다.
따라서 개발자는 이 흐름에 맞춰 개발을 해야하며 내가 작성한 코드를 프레임워크가 제어한다. - 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당한다. = 라이브러리는 필요에 따라 라이브러리를 사용해 개발자가 직접 개발흐름을 정할수있다.
DI (Dependency Injection) 의존관계 주입
- 의존관계란 말그대로 서로 의존하는 관계라는 것이다
예를들어 A가 B를 의존할때 B가 변하면 A도 영향을 미친다. - 의존 관계는 정적인 클래스 의존 관계, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 를 분리해서 생각해야한다.
정적인 클래스 의존관계
클래스 다이어그램
- 정적인 클래스 의존관계는 import 코드만 보고 의존관계를 쉽계 판단할 수 있다.
- OverserviceImpl은 DiscountPolicy,MemberRepository에 의존한다는 것을 알 수있다.
- 정적인 클래스 의존관계는 실제로 각 인터페이스에 어떤 객체가 주입 될지는 알 수 없다.
동적인 객체 인스턴스 의존 관계 - 객체 다이어그램
- 객체 다이어그램은 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계이다.
IoC컨테이너와 DI컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC컨테이너 또는 DI컨테이너라고한다.
스프링으로 전환하기
'study > 스프링_핵심원리_기본' 카테고리의 다른 글
스프링 컨테이너와 스프링 빈-스프링 핵심원리 기본 (0) | 2022.04.29 |
---|---|
스프링 핵심 원리 이해1 -예제 만들기 (0) | 2022.04.14 |
스프링, JPA의 탄생 (0) | 2022.04.14 |