객체 지향 원리 적용
새로운 할인 정책 개발
새로운 할인 정책을 확장해보자
참고: 애자일 소프트웨어 개발 선언 https://agilemanifesto.org/iso/ko/manifesto.html
기획자의 요구사항 변동으로 인해 FixDiscountPolicy를 RateDiscountPolicy로 변경할것이다.
- RateDiscountPolicy 추가

회원의 등급을 조회하고 VIP일때 할인 금액이였던 1000원을 return했던 FixdiscountPolicy와 다르게 price의 10%를 return하는 class를 설계했다.
package hello.core.discount;
import hello.core.member.Grade;
import hello.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 hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다")
void vip_o(){
// given
Member member = new Member(1L, "memberVIP", Grade.VIP);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x(){
// given
Member member = new Member(2L, "memberVIP", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isNotEqualTo(1000);
}
}
각각의 테스트를 진행했을때 VIP 멤버는 1000원 할인이 적용되지만 BASIC 멤버는 할인이 적용되지 않았음을 알 수 있다.

새로운 할인 정책 적용과 문제점
테스트가 끝난 새로운 할인 정책을 적용해보자.
할인 정책을 변경하려면 OrderServiceImpl의 코드를 수동으로 직접 변경해줘야 한다.
지금 코드의 상태를 확인해보면 interface인 DiscountPolicy와 구체 클래스인 RateDiscountPolicy를 둘 다 의존하고 있음을 확인할 수 있다.
이는 SOLID 원칙의 OCP를 위반하는 코드다.


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.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
/*
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
Service 클래스에 직접 들어가 FixDiscountPolicy를 변경해주는 모습
이는 SOLID 원칙의 OCP에 위배되는 방법이다.
*/
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);
}
}
이를 개선하기 위해 DIP(제어역전)을 적용시킨 AppConfig를 설계해볼것이다.
AppConfig
우선 인터페이스와 구현 클래스를 모두 의존하고 있는 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.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private 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);
}
}
하지만 지금 이 상태로는 discountPolicy는 아무런 객체가 할당되지 않은 순수 interface 상태여서 이대로 코드를 실행 시 NullPonterException이 발생하게 된다.
이를 해결하기 위해 구현 객체를 대신 생성하고 주입해주는 AppConfig class를 정의해 줄것이다.
package hello.core;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),
new RateDiscountPolicy());
}
}
package hello.core.member;
public class MemberServiceImpl implements MemberService{
// 자바의 OCP 원칙에 위배되는 코드. MemoryMemberRepository를 DbMemberRepository로 변경시
// 직접 코드를 변경해주어야 한다. 이를 극복하기 위해 스프링을 사용한다.
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member); // Repository의 save 메소드를 호출해서 저장
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
// Repository의 findById 메소드에 memberId값을 넘겨 Member 객체를 호출한다.
}
}
MemberServiceImpl는 AppConfig를 통해 호출되며 생성자에 내가 원하는 class를 넘겨준다. 이렇게 의존성을 주입해줌으로써 MemberServiceImpl는 더이상 OCP에 위배되지 않는다.

뒤에서 객체들의 의존성을 주입해주는 감독과 같은 역할을 한다.
AppConfig에서 MemberService 객체를 직접 생성해보는 모습이다.
package hello.core.member;
import hello.core.AppConfig;
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
// MemberService memberService = new MemberServiceImpl();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member);
System.out.println("find Member = " + findMember);
}
}
하지만 이런 방법은 중복과 역할에 따른 구현을 한눈에 알아보기 힘들다. 이를 해결하기 위해 AppConfig를 리펙터링 해보자
AppConfig 리펙터링
AppConfig 리펙터링을 통해 구현 클래스를 한눈에 들어오게 변경해주었다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
public MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(MemberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
하지만 아무리 리펙터링을 통해 가시성을 높혔다고 해도 모든 의존성을 수동으로 주입하기엔 무리가 있다. 이를 해결하기 위해 스프링을 사용해보자
Spring
AppConfig에 Spring 적용하기
AppConfig 클래스에 @Configuration 어노테이션과 @Bean 어노테이션으로 Spring 컨테이너에 등록해준다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.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() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(MemberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
스프링 컨테이너에 AppConfig.class 를 등록해주고 등록된 스프링 컨테이너에서 원하는 Bean을 조회하여 할당해준다.
package hello.core.member;
import hello.core.AppConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = new MemberServiceImpl();
// 스프링 컨테이너에 AppConfig를 등록한다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// Bean의 이름을 조회하고 어떤 타입으로 return할건지 선언해준 뒤 memberService에 할당해준다.
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);
System.out.println("find Member = " + findMember);
}
}
스프링 컨테이너는 @Configuration 어노테이션이 붙은 AppConfig를 설정 정보로 사용한다. 여기서 @Bean 이라 적힌 메소드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.
스프링 빈은 @Bean 이 붙은 메소드의 명을 스프링 빈의 이름으로 사용한다.
'코딩딩 > Spring' 카테고리의 다른 글
싱글톤 컨테이너 (1) | 2023.08.28 |
---|---|
Tomcat (0) | 2023.08.28 |
좋은 객체지향 설계의 5가지 원칙(SOLID) (0) | 2023.08.13 |
스프링 핵심 원리 이해1 - 예제 만들기 (0) | 2023.08.13 |
DIP에 대한 정리 (0) | 2023.07.21 |