스프링

스프링 기본원리 - SOLID 원칙 /주문과 할인 도메인 설계

ho코딩 2024. 2. 12. 23:55

지난 시간에는 회원 / 회원 서비스에 대한 도메인 설계 및 구현을 스프링 기능을 제외하고 순수 자바코드로 진행했었는데, 

이렇게 짠 코드에는 어떤 문제가 있는지 살펴보겠습니다. 

 

어떠한 문제가 있는지 알아보기 위해서는 객체지향 SOLID 원칙에 대해서 먼저 알아봐야합니다. 

 

 

 

SOLID는 객체지향 프로그래밍에서 다섯 가지 설계 원칙을 나타냅니다. 

이 원칙은 소프트웨어 설계의 유지보수성, 확장성, 가독성, 재사용성 등을 향상시키는데 목적을 두고 있습니다. 

 

1. 단일 책임 원칙(SRP)

- 클래스는 하나의 책임만 가져야 한다. 

 

2.개방/폐쇄 원칙(OCP)

- 소프트웨어 엔티티는 확장에는 열려있고, 변경에는 닫혀있어야 한다. 

-새로운 기능을 추가할 때 기존의 코드를 변경하지 않고 확장할 수 있어야 한다.

개방/폐쇄 원칙 예시

여기서 보면 Shape 인터페이스를 구현하는 새로운 도형 클래스를 추가할 때 코드 변경이 필요하지 않습니다. 

기존의 코드(shape)를 변경하지 않고 Rectangle 클래스를 확장한 것이죠. 

 

 

 

3.리스코프 치환 원칙(LSP)

-서브타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다. 

-즉, 상속 받은 클래스는 기반 클래스를 대체할 수 있어야 하며,

    이를 통해 클라이언트 코드는 변경없이 새로운 서브클래스를 사용할 수 있어야 한다. 

 

여기서는 Bird 클래스의 하위 클래스인 Sparrow 클래스가 Bird 클래스를 대신할 수 있어야 합니다. 

 

4.인터페이스 분리 원칙(ISP)

- 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안됩니다. 

 - 즉, 인터페이스를 클라이언트에 특화된 작은 단위로 분리하여 사용해야합니다. 

 

여기서 보면, Human 클래스는 Worker와 Eater에 있는 work와 eat 메서드를 각각 의존하고 있습니다. 

이렇게 사용하는 메서드가 존재하는 클래스/인터페이스에만 의존해야합니다 

 

 

5.의존성 역전 원칙 (DIP)

-고수준 모듈은 저수준 모듈에 의존하면 안 되며, 두 모두 추상화에 의존해야 한다. 

- 추상화는 구체적인 것에 의존해서는 안 되며, 구체적인 것이 추상화에 의존해야 한다.

 

이 예시를 보면. 추상화인 Switchable 인터페이스에 의존하고 있는 Light , Fan 클래스를 볼 수 있습니다. 

이처럼 상위 수준 모듈이 하위 수준 모듈에 의존하는 것이 아니라, 둘 다 추상화에 의존해야합니다. 

 

 

주문/할인 도메인 설계

 

SOLID 원리를 먼저 알아보았고, 주문과 할인 도메인 설계까지 마무리 한 후 각 원칙에 위배되는 점을 알아보겠습니다.

 

 

주문 도메인을 보면 

1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청합니다. 

2. 회원 조회: 할인을 위해서는 회원 등급이 필요합니다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회합니다.

3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임합니다.

4. 주문 결과 반환 : 주문 서비스는 할인 결과를 포함한 주문 결과를 반환합니다. 

 

결과적으로 전체적인 그림을 보면 

위와 같이 완성됩니다.

 

 

이제 주문 도메인 클래스의 다이어그램을 보겠습니다. 

 

OrderService 인터페이스는 OrderServiceImpl 구현체에 의해 동작되고 

OrderServiceImpl 구현체는 MemberRepository , DiscountPolicy 인터페이스에 의해 동작됩니다. 

 

MemberRepository는 저장 정책에 따라 'MemoryMemberRepository' 또는 'DbMemberRepository'에 의존하고

DiscountPolicy도 할인 정책에 따라 'FixDiscountPolicy'(정액할인), 'RateDiscountPolicy'(정률할인)에 의존합니다.

 

MemberRepository 인터페이스와 DiscountPolicy 인터페이스는 각 구현체에 따라서 알맞는 클래스를 

마치 조립하듯 구현체를 끼워넣기만 하면 됩니다 .

 

 

 

할인 정책 인터페이스 

 

DiscountPolicy 인터페이스를 작성해보았습니다. 

 

DiscountPolicy는 discount 메서드만 사용하게 됩니다. 

때문에 껍데기만 생성해두고 , 매개변수로 회원 등급을 알아야하므로 member와 price를 넘깁니다. 

 

FixDiscountPolicy

 

정액할인을 위한 코드입니다. 

정액할인은 1000원으로 결정하였다고 가정을하고 discountFixAmount 값을 1000으로 설정합니다. 

 

그 후 앞서 인터페이스에서 정의한 discount 메서드를 재정의하므로 

@Overide 어노테이션을 붙여주고 discount 메서드를 정의합니다, 

 

만약 회원의 등급이 VIP라면 discountFixAmount값을 반환해주고 

그렇지 않다면 (일반 회원이라면 0 을 반환합니다) 

 

즉, 회원의 등급이 일반이라면 0원 할인, VIP라면 1000원을 할인해줍니다. 

 

정률 할인은 다음시간에 구현하도록 하고 다음으로는 주문 엔티티를 만들어보겠습니다. 

 

주문 엔티티

 

Order 엔티티에는 주문에 필요한 정보인 memberId, itemName, itemPrice, disxcountPrice 를 설정합니다 .

 

또한 기본 생성자를 생성해주고 Getter와 Setter도 모두 설정해줍니다 .

마지막으로, 여기서는 따로 뷰를 구현하지 않기 때문에 주문을 확인할 toString() 메서드를 구현합니다. 

 

 

 

주문 서비스 인터페이스

 

 

 

OrderService에서는 주문에 관련된 메서드를 선언하는 곳 입니다,

createOrder 메서드를 사용하고 memberId, itemName, itemPrice를 매개변수로 사용합니다. 

(어떤 회원이 , 어떤 아이템을 , 얼마에 주문하는지) 

 

 

주문 서비스 구현체

 

OrderServiceImpl은 OrderService를 구현하므로 implements를 써주고, 

MemberRepository는 Memory 방식을 쓰므로 MemoryMemberRepository()를 

DiscountPolicy는 Fix(정액할인)방식을 쓰므로 FIxDiscountPolicy()를 사용합니다. 

 

그리고 인터페이스에서 선언한 createOrder메서드를 구현해줍니다. 

 

1. Member member = memberRepository.findById(memberId); 를 통해 회원을 조회합니다.

2. int discountPrice = discountPolicy.discount(member, itemPrice); 를 통해 회원에 따른 할인가를 계산합니다.

 

return new Order(memberId, itemName, itemPrice, discountPrice); 

->마지막으로 위에서 조회한 회원 정보, 상품 정보, 할인된 가격 등을 사용하여 'Order'객체를 생성하고 

'Order'클래스의 생성자를 호출하여 새로운 주문 객체를 생성한 후 반환합니다. 

 

 

 

다음시간에는 RateDiscountPolicy 구현체를 만들고 , SOLID에 위배되는 것들과 해결방법에 대해서 알아보겠습니다.