개발놀이터

순수한 자바로 DI 구현하기 본문

Spring/Spring

순수한 자바로 DI 구현하기

마늘냄새폴폴 2021. 12. 21. 19:14

이 포스팅은 인프런 김영한님의 스프링 핵심 원리 기본편을 보고 각색한 포스팅입니다. 자세한 내용은 강의를 확인해주세요

 

코드에 들어가기 전 이 코드의 배경이 되는 스토리가 있는데 

 

사장님이 할인 정책을 구상하는데 VIP에게 정액할인제를 할지 정률할인제를 할지 고민하는 상황이다. 사장님이 할인 정책에 대해서는 중요한 사항이므로 런칭하기 바로 직전까지 생각하다 적용할 예정이다. 기획자는 일단 둘 다 만들어 놓고 우선 정액할인제를 적용한 상태로 개발에 착수하라는 지령이 떨어졌다. 

 

우리의 개발자는 언제 할인 정책이 바뀔지 모르는 상황에서 개발에 착수해야 하며 런칭 바로 직전에 할인정책이 바뀔수도 있는 상황이다. 

 

Member.java

public class Member {

    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}

Member 클래스를 만들고 우선 할인 정책을 두개 만들어 놔야 한다.

public interface DiscountPolicy {

    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}
public class FixDiscountPolicy implements DiscountPolicy{

    private int discountFixAmount = 1000; //1000원 할인

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }
        else {
            return 0;
        }
    }
}
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;
        }
    }
}

정액할인정책 (FixDiscountPolicy) 정률할인정책 (RateDiscountPolicy) 이렇게 두개를 만들었다. 언제 바뀔지 모르기 때문에 다형성을 이용한 개발을 완료했다. Member를 인자값으로 받아와서 Grade에 따라 할인을 할지 안할지를 결정하는 코드이다.

 

이제 주문 관련 코드를 만들어보자

 

public class Order {

    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    public int calculatePrice() {
        return itemPrice - discountPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }
}
public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    @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);
    }
}

이제 모든 준비가 완료되었다. 우리는 다형성을 이용해 개발했기 때문에 언제든지 정책이 바뀌면 그에 걸맞는 코드로 갈아끼울 수 있게 되었다. 

 

자 이제 런칭일이 다가오고 있다. 우리는 기획자의 말대로 정액할인정책에 초점을 둔 상태로 개발에 착수했다. 이때 사장님이 정액할인정책말고 더 고객친화적인 정률할인정책을 펼치겠다고 선언하셨다. 

 

하지만 우린 이미 다형성을 이용해 개발을 완료했다. 

private final DiscountPolicy discountPolicy = new FixDiscountPolicy()

이 부분을 

private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

이렇게 바꿔주기만 하면 된다. 

 

하지만 여기서 문제가 발생했다. 바로 객체지향의 5가지 원칙 SOLID중 SRP (단일 책임 원칙), OCP (개발 폐쇄 원칙), DIP (의존관계 역전 원칙) 을 지킬 수 없게 된것이다. 

 

https://coding-review.tistory.com/81

 

객체지향 설계 5원칙 SOLID

*객체지향 설계 SOLID 1. SRP (Single Responsibility Principle) : 단일 책임 원칙 -한 클래스는 하나의 책임만 가져야 한다. -하지만 하나의 책임이라는 것은 모호하다. --클 수도 있고, ..

coding-review.tistory.com

하나씩 살펴보자

 

1. SRP 위반

SRP에 대해 간단하게 정리하면 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 위의 코드에서 SRP가 위배된 부분은 바로 OrderServiceImpl 구현체는 Order 관련된 책임만을 가지고 있어야 하는데 할인정책까지 직접 선택해야하는 책임이 새로 부여된 것이다. 

 

2. OCP 위반

OCP에 대해 간단하게 정리하면 소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다는 원칙이다. 이는 즉 무슨말이냐하면 지금 정액할인정책에서 정률할인정책으로 서비스를 확장해야하는 상황에서 서비스는 확장하되 코드의 변경은 없어야 한다는 의미이다. 우리는 DiscountPolicy를 Fix -> Rate로 변경했다. 이는 OCP의 위반이라고 볼 수 있다.

 

3. DIP 위반

DIP에 대해 간단하게 정리하면 프로그래머는 추상화에 의존해야지 구체화에 의존하면 안된다는 원칙이다. 즉 역할 (인터페이스) 과 구현 (구현체) 중 역할만 알고있어야 한다는 의미이다. 얼핏 보면 역할에 의존하는 것 같이 보이지만 구현도 알고있다.

private final DiscountPolicy(역할) discountPolicy = new RateDiscountPolicy()(구현);

이 세개를 위반하지 않게 하려면 코드를 어떻게 리팩토링 해야할까? 바로 이렇게 바꾸면 된다.

private DiscountPolicy discountPolicy;

엥? 이렇게 바꾸면 된다고? 인터페이스만 선언하고 구현체는 선언하지 않지 않았는가 이러면 분명 NullPointerException이 터질게 분명하다. 

 

물론 저렇게 바꾸기만 하면 당연히 NullPointerException이 터진다. 추가적으로 코드를 작성해주어야 한다. 

 

우리는 앞서 책임에 대해서 이야기한 적이 있다. 바로 SRP를 얘기할 때이다. 위의 코드가 오류가 나지 않게 하려면 새로운 책임을 부여해주면 된다. 바로 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들면 된다. 

 

AppConfig.java

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy(););
    }
}

이렇게 동작 방식을 구성하기 위한 클래스를 새로 만들어주었다. 그럼 기존의 코드는 어떻게 변경되어야 할까?

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);
    }
}

생성자를 통해 이 OrderServiceImpl 클래스가 생성될 때 AppConfig에서는 해당하는 내용을 주입해준다. 이를 생성자 주입이라고 표현한다. 

 

이렇게 하면 SRP, OCP, DIP를 위반하지 않고 코드를 작성할 수 있다. 

 

SRP : OrderServiceImpl은 Order 관련된 책임만을 가지고 있고, AppConfig는 애플리케이션 동작을 총괄하는 책임을 가지고있다. 

OCP : 서비스가 확장했음에도 코드의 변경이 일어나지 않았다.

DIP : 코드에서 나와있듯이 추상화에만 의존할 뿐 구체화에 의존하고 있지 않다.

 

한가지 아쉬운 점이 있다면 AppConfig에서의 코드 중복이다. 살짝 리팩토링을 해준다면 다음과 같이 깔끔하게 바뀔 것이다.

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    private MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

만약 할인정책이 정액할인정책에서 정률할인정책으로 바뀐다면 어떻게 하면 될까? 앞으로는 AppConfig에서 DiscountPolicy 부분을 바꿔주기만 하면 된다. 

 

이렇게 객체지향을 잘 준수하면서 사용하기 용이한 서비스가 완성되었다. 

 

*포스팅 후기

이번에 종강을 맞이해서 무엇을 공부할까 고민하다가 이전에 들었던 스프링 강의와 JPA강의를 전부 2회독 하기로 결심했는데 이 강의를 처음 들었던 약 4개월 전에 나와 비교했을 때 지금의 내가 많이 성장했다는 느낌을 받았다. 이 강의를 처음 들었을 때는 인터페이스도 처음 써보고 Repository, Service, Controller 구조도 처음 봤다. 하지만 지금은 이런 구조가 익숙하고 처음 들을때와 다르게 이해도 잘 되는것 같았다. 이 포스팅을 시작으로 모든 강의를 듣고 포스팅을 남길 생각이다. 나와 함께 성장해나갈 블로그를 잘 지켜봐주셨으면 좋겠다. 

'Spring > Spring' 카테고리의 다른 글

싱글톤과 @Configuration  (0) 2021.12.27
스프링을 사용해서 DI 구현하기  (0) 2021.12.26
객체지향 설계 5원칙 SOLID  (0) 2021.12.20
타임리프 classappend  (0) 2021.12.14
스프링 타입컨버터  (0) 2021.11.13