[스프링 기본편] 역할과 구현의 분리

may.log·2022년 3월 5일
post-thumbnail

이 글은 스프링 핵심 원리 - 기본편 강의를 듣고 정리한 내용입니다.

📌1. 비즈니스 요구사항과 설계

회원

  • 회원은 일반과 VIP 등급으로 나뉜다.
  • 회원 데이터는 자체 DB를 구출할 수 있고, 외부 시스템과 연동할 수 있다. → 미확정

주문과 할인 정책

  • 회원 등급에 따라 할인 정책을 적용한다.
  • 할인정책 : 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용한다.
    → 변경 가능

정리 : 회원 데이터, 할인 정책은 아직 명확히 정해지지 않았다. 그렇다고 정책이 결정될 때 까지 개발을 기다릴 수 없다‼️ 이때 구현과 역할을 분리해서 이 문제를 해결할 수 있다. 즉, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하는 것이다.

📌2. 구현과 역할의 분리

  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. → 미확정

    🌈 MemberRepository 인터페이스를 만들고 MemoryMemberRepository와 DbMemberRepository를 구현체로 설계한다!

  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 않았다. → 미확정
    🌈 Discount 인터페이스를 만들고 FixDiscountPolicy와 RateDiscountPolicy를 구현체로 설계한다!

🔎 코드

🌈 Grade

public enum Grade {
	BASIC,
    VIP 
}

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

🌈 MemberRepository

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

🌈 MemoryMemberRepository

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

🌈 MemberService

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}

🌈 MemberServiceImpl

public class MemberServiceImpl implements MemberService {

      private final MemberRepository memberRepository = new MemoryMemberRepository();
      
      public void join(Member member) {
          memberRepository.save(member);
	}
      
      public Member findMember(Long memberId) {
          return memberRepository.findById(memberId);
	} 
}

🌈 DiscountPolicy

public interface DiscountPolicy {

    //할인된 가격 반환
    int discount(Member member, int price);
}

🌈 FixDiscountPolicy

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

🌈 Order

public class Order {

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

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

    //생성자
    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = 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 +
                '}';
    }
}

🌈 OrderService

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

🌈 OrderServiceImpl

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

📌3. 새로운 할인 정책 개발

악덕의 기획자가 기존의 할인정책(FixDiscountPolicy)을 변경하고 싶다고 한다‼️ 우리는 객체지향 설계 원칙을 잘 준수했기 때문에 DiscountPolicy 인터페이스를 구현한 RateDiscountPolicy로 갈아 끼우면 된다.

🌈 RateDiscountPolicy

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

📌4. 새로운 할인 정책 적용과 문제점

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.

public class OrderServiceImpl implements OrderService {
  //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
  }

🔎 문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했다. OK

  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK

  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다.
    그렇게 보이지만 사실은 아니다‼️

  • DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?

    → 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고있다‼️
    → 추상(인터페이스) 의존: DiscountPolicy
    → 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy

    🔎 지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각했다.
    잘보면 클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐 만 아니라 FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. 실제 코 드를 보면 의존하고 있다! DIP 위반

  • OCP: 변경하지 않고 확장할 수 있다고 했는데!
    지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다‼️


🔎 문제 해결

  • 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다.
  • 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
  • DIP 위반 → 추상에만 의존하도록 변경(인터페이스에만 의존)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.

→ 인터페이스에만 의존하도록 설계와 코드를 변경했다.
→ 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
→ 실제 실행을 해보면 NPE(null pointer exception)가 발생한다.

🔎 최종 해결방안

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다. → 뒤에 나올 AppConfig 클래스, 스프링 컨테이너의 역할‼️

📌5. AppConfig 등장

애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 설정 클래스를 만든다.

public class AppConfig {

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

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

⭐️ 구현 객체를 생성한다

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

⭐️ 생성한 구현 객체 인스턴스의 참조를 생성자를 통해서 주입한다.

  • MemberServiceImpl가 MemoryMemberRepository를 주입
  • OrderServiceImpl가 MemoryMemberRepository, FixDiscountPolicy를 주입

🌈 MemberServiceImpl 클래스의 생성자 주입

public class MemberServiceImpl implements MemberService {

        private final MemberRepository memberRepository;
				
				//생성자 주입 
        public MemberServiceImpl(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
}

생성자를 주입하므로서 MemberServiceImpl는 MemoryMemberRepository에 의존하지 않고, 인터페이스 MemberRepository에만 의존하므로 DIP 원칙을 준수한다!

즉, MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다. MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.


⭐️ 객체의 생성과 연결은 AppConfig 가 담당한다.
⭐️ DIP 완성: MemberServiceImpl 은 MemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
⭐️ 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

🌈 OrderServiceImpl의 생성자 주입

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

설계 변경으로 OrderServiceImpl 은 FixDiscountPolicy 를 의존하지 않는다! 단지 DiscountPolicy 인터페이스만 의존한다. OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다. OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig )에서 결정한다. OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.

🌈 테스트 코드

class OrderServiceTest {

      MemberService memberService;
      OrderService orderService;

      @BeforeEach
      public void beforeEach() {
          AppConfig appConfig = new AppConfig();
          memberService = appConfig.memberService();
          orderService = appConfig.orderService();
	} 
}

0개의 댓글