Dev/Spring

[스프링 핵심 원리] SoC와 DI

oxdjww 2023. 9. 20. 17:48
728x90
반응형

Intro

본 카테고리는 Inflearn 김영한 강사님의 스프링 핵심 원리 강의를 수강하며 이해하고 학습한 내용을 정리한 내용으로 구성되어 있다.
본 포스팅에서는 순수 자바코드와, 이 코드에 객체지향 설계원칙을 적용해나가는 과정을 담았다.

비즈니스 요구사항과 설계


  • 회원

    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.

    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

Code Overview


member


  • Member
package hello.core.member;

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

멤버에서는 가입한 회원의 id, 이름, 등급(VIP/BASIC)라는 멤버변수를 갖는다.

  • MemberRepository
 package hello.core.member;

 public interface MemberRepository {  
    void save(Member member);

    Member findById(Long memberId);
}

Member의 정보를 저장할 MemberRepository interface이다.

추상화된 interface로, 구현체인 MemberinterfaceMemberRepository를 의존하게끔 설계 할 것이다.
또한, 하단의 MemberMemoryRepositoryMemberRepository를 구현한다.

  • MemberMemoryRepository
package hello.core.member;

import java.util.HashMap;
import java.util.Map;

public class MemoryMemberRepository implements MemberRepository {
    private static Map<Long, Member> store = new HashMap<>();


    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }


}

MemberMemoryRepository는 별도의 database를 갖지 않고 memory에 저장될 휘발성 저장소이다.
추후에 database에 연결할 때 구현 class만 바꿔끼면 된다.

  • MemberService

package hello.core.member;

public interface MemberService {  
void join(Member member);


Member findMember(Long memberId);

}

Member가 사용할 MemberService이다.
마찬가지로 추상화된 interface로, 하단의 MemberServiceImpl에서 구현되어 있다.

  • MemberServiceImpl
package hello.core.member;

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();


    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

MemberService의 추상 메서드들을 override하여 구현했다.

Order


  • Order
package hello.core.order;

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

    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }


}

주문 양식을 갖고 있는 Order class이다.

  • OrderService
package hello.core.order;

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

추상화된 interface형태의 OrderService이다.
하단의 OrderServiceImpl에서 이를 구현할 것이다

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

추상화된 interfaceMemberRepositoryDiscountPolicy에 의존하는 OrderServiceImpl이다.

기존 코드의 문제점


바로 위에서 설명한 OrderServiceImpl을 다시 보자.

우선 OrderServiceImpl(구현체)가 추상화된 interface에만 의존하는 것이 아니다.
MemberMemoryRepository, FixDiscountPolicy라는 구현체에도 의존하고 있기에 DIP를 위반한다.

그리고 새로운 할인 정책이 도입되었을 때, 구현체인 ServiceImpl의 코드까지 수정해야 한다.
그러므로 OCP까지 위반하게 된다.

마지막으로 구현 객체를 생성하고, 연결하고, 또 서비스(회원가입 등)을 실행하는 다양한 책임을 가지게 된다.
이는 SRP(단일책임원칙, 한 클래스는 한 가지 책임만 가진다.)도 위반하게 된다.

이를 해결하기 위해서는 OrderServiceImpl이란 클래스에서 interface에만 의존하도록 코드를 수정해야 한다.
다음과 같이 코드를 수정하고, 이 멤버변수를 초기화 할 때 쓰일 생성자도 추가한다.


public class OrderServiceImpl implements OrderService {
    private MemberRepository memberRepository;
    private 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);
    }

}  
...

이렇게 설계를 하며 DIP, OCP, SRP를 적용할 수 있었다.

OrderServiceImpl 뿐만 아니라 MemberServiceImpl에도 동일한 문제가 있기에 마찬가지로 문제를 해결하고, AppConfig에서 구현 객체를 생성해줄 것이다.


public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

...

새로운 문제점의 발생


이렇게 의존 관계를 해결하여, 구현체에 의존하는 문제(DIP)를 해결하고, 또 개방폐쇄원칙인 OCP원칙까지 해결할 수 있었다.
하지만, 멤버변수들이 이렇게 인터페이스에만 의존하게 되면 코드를 실행할 수 없다.

결국, 어딘가에서 멤버변수에 구현체를 초기화해주어야 한다.
(MemberServiceImpl의 경우 memberRepository)
(OrderServiceImpl의 경우 memberRepository, discountPolicy)

여기서 AppConfig 라는 새로운 클래스를 만들어, 구현 객체를 생성하며 연결해주는 책임을 갖게 할 수 있다.
이렇게 어플리케이션의 실행, 설정 등 다양한 책임들을 분리하는 것을 SoC(Seperation of Concerns), 즉 관심사의 분리라고 한다.

AppConfig



package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemberService;
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());
    }

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

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

    private static DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

}

이렇게 AppConfig은 어플리케이션 실행에 있어 구현 객체를 생성해주고, 이 구현 객체들을 구현 클래스의 생성자를 통해 연결해준다.

설계를 변경하게 되면서 구현체인 MemberServiceImpl, OrderServiceImpl은 더이상 MemberMemoryRepositoryFixDiscountPolicy를 의존하지 않는다.

*Impl 클래스의 입장에서 보면 의존관계를 외부에서 주입하는 형태를 가진다.
그래서 이를 DI (Dependency Injection), 즉 의존성 주입 (의존관계 주입)이라 한다.
우리는 생성자를 이용하여 주입하였으므로 이를 생성자 주입이라 한다.

  • 참고 : 의존주입의 종류
    • 생성자 주입(Constructor Injection)
    • 필드 주입(Field Injection)
    • 수정자 주입(Setter Injection)

testcode를 통해 잘 적용되었는지 확인해보자.

Test Code (JUnit)


Test

  • RateDiscountPolicyTest

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;

import static org.junit.jupiter.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  
        Assertions.assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
    void vip_x() {
        // given  
        Member member = new Member(1L, "memberBASIC", Grade.BASIC);
        // when  
        int discount = discountPolicy.discount(member, 10000);
        // then  
        Assertions.assertThat(discount).isEqualTo(0);
    }

}

회원 등급에 따른 할인에 대한 testcode이다.

  • 실행 결과 (O)


  • 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;

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, "memberA", Grade.VIP);

        // when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        // then
        Assertions.assertThat(member).isEqualTo(findMember);


    }

}

새로운 Member가 회원가입을 하고, 이를 다시 찾아내어 제대로 회원가입이 되었는지 확인하는 testcode

  • 실행 결과 (O)


  • OrderServiceTest


package hello.core.order;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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 createOrder() {
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);


        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);


    }

}

주문 생성이 제대로 되고, membergrade에 따른 할인 정책이 잘 적용되었는지를 확인하기 위한 testcode

  • 실행 결과 (O)

감사합니다.

728x90
반응형