스프링 적용하기 전 객체지향설계

    이번 포스팅에서는 자바만을 활용해서 객체 지향 설계를 먼저 완성을 하는 것을 목표로 한다. 객체 지향 설계를 완성한 후, SOLID 원칙 관점에서 점검을 해보도록 한다.


     

    클래스 다이어그램 

     

    1. 멤버 클래스의 구현

    • 멤버 클래스는 멤버변수 ID, 이름, 등급을 가진다.
    • 멤버 클래스는 생성자와 Getter / Setter를 가진다.
    • 등급은 enum으로 상수 클래스를 추가해서 사용한다.
    package myname.core2.member;
    
    public class Member {
    
        private Long memberId;
        private String memberName;
        private Grade grade;
    
        public Member(Long memberId, String memberName, Grade grade) {
            this.memberId = memberId;
            this.memberName = memberName;
            this.grade = grade;
        }
    
        public Long getMemberId() {
            return memberId;
        }
    
        public void setMemberId(Long memberId) {
            this.memberId = memberId;
        }
    
        public String getMemberName() {
            return memberName;
        }
    
        public void setMemberName(String memberName) {
            this.memberName = memberName;
        }
    
        public Grade getGrade() {
            return grade;
        }
    
        public void setGrade(Grade grade) {
            this.grade = grade;
        }
    }
    
    package myname.core2.member;
    
    public enum Grade {
        BASIC,
        VIP
    }

     

    2-1. 멤버 리포지토리의 구현

    • 멤버 리포지토리는 어떤 데이터베이스를 사용할지 정해지지 않았다. 따라서, 인터페이스를 설정하고 각 구현체를 구현하는 것으로 접근한다.
    • 멤버 리포지토리는 멤버의 저장, 멤버 조회 두 가지 기능을 가진다.
    • 멤버 리포지토리는 멤버변수로 저장소를 가진다. 저장소는 Key, Value 형태로 데이터를 저장한다.
    // MemberRepository 인터페이스 설정(대본, 역할 설정)
    package myname.core2.member;
    
    public interface MemberRepository {
        void join(Long memberId,Member member);
        Member findById(Long memberId);
    }

    멤버 리포지토리의 인터페이스를 설정했다. 메모리 기반 리포지토리 구현체를 구현해 사용하기로 했으므로, 메모리를 활용한 리포지토리를 구현한다.

    package myname.core2.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 join(Long memberId, Member member) {
            store.put(memberId, member);
        }
    
        @Override
        public Member findById(Long memberId) {
            return store.get(memberId);
        }
    }

    2-2. 멤버 리포지토리의 테스트 코드 작성

    • Main 함수를 활용해 정상적으로 회원이 저장되었는지를 출력해본다.
    • JUNIT을 사용해 정상적으로 회원이 저장되었는지를 확인해본다.

    Main 함수 활용 검증

    1. 멤버를 하나 생성한다
    2. 멤버 리포지토리를 하나 생성한다.
    3. 멤버 리포지토리에 멤버를 저장한다.
    4. 멤버 ID로 회원을 찾는다
    5. 저장된 회원과 기존 회원이 같은지 확인한다.
    package myname.core2;
    import myname.core2.member.Grade;
    import myname.core2.member.Member;
    import myname.core2.member.MemberRepository;
    import myname.core2.member.MemoryMemberRepository;
    
    public class MemberApp {
    
        public static void main(String[] args) {
    
            Long memberId = 1L;
            Member member1 = new Member(memberId, "memberA", Grade.VIP);
            MemberRepository memberRepository = new MemoryMemberRepository();
    
            memberRepository.join(memberId,member1);
            Member findMemeber = memberRepository.findById(memberId);
            System.out.println("findMemeber = " + findMemeber.getMemberName() + " Origin Member = " + member1.getMemberName());
    
        }
    }

    JUNIT 활용한 검증

    메인 함수를 활용한 것과 동일하게 로직을 짠다. 단, Assertions.assertThat을 활용해서 기대값과 실제값을 비교해서 컴퓨터가 비교할 수 있도록 한다.

    public class MemberApplicationTest {
    
        @Test
        void memberTest(){
            Long memberId = 1L;
            Member member = new Member(memberId, "memberA", Grade.VIP);
            MemberRepository memberRepository = new MemoryMemberRepository();
    
            memberRepository.join(memberId, member);
            Member findMember = memberRepository.findById(memberId);
    
            Assertions.assertThat(findMember.getMemberId()).isEqualTo(memberId);
        }

     

    3-1. DiscountPolicy의 구현

    • 할인 정책은 아직 결정되지 않았다. 따라서 인터페이스를 기반으로 개발이 필요하다.
    • 정액 할인 정책을 구현한다. 인터페이스는 고객 정보를 바탕으로 VIP면 1000원 정액 할인해준다.

     

    // 인터페이스 구현
    package myname.core2.discount;
    import myname.core2.member.Member;
    public interface DiscountPolicy {
        int discount(Member member, String itemName, int itemPrice);
    }
    
    
    
    // 인터페이스의 구현체 구현 
    package myname.core2.discount;
    import myname.core2.member.Grade;
    import myname.core2.member.Member;
    public class FixDiscountPolicy implements DiscountPolicy {
        private static int discountFixAmount = 1000;
    
        @Override
        public int discount(Member member, String itemName, int itemPrcie) {
            if (member.getGrade() == Grade.VIP) {
                return discountFixAmount;
            }else{
                return 0;
            }
        }
    }

    3-2. DiscountPolicy의 Test 코드

    • JUNIT으로 검증한다.
    • VIP 멤버변수를 매개변수로 넘겨준다.
    • 멤버의 정액 할인이 '1000원'이 맞는지 확인한다.
    package myname.core2.discount;
    import myname.core2.member.Grade;
    import myname.core2.member.Member;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    public class DiscountPolicyTest {
    
        @Test
        @DisplayName("VIP회원 정액 할인은 1000원입니다.")
        void discountTest() {
            Member member = new Member(1L, "memberA", Grade.VIP);
            DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
            int discount = discountPolicy.discount(member, "itemA", 10000);
            Assertions.assertThat(discount).isEqualTo(1000);
        }
    
        @Test
        @DisplayName("일반회원 저액 할인은 0원입니다.")
        void discountTestBasic(){
            Member member = new Member(1L, "memberA", Grade.BASIC);
            DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
            int discount = discountPolicy.discount(member, "itemA", 10000);
            Assertions.assertThat(discount).isEqualTo(0);
        }
    }

     

    4-1 멤버 서비스 인터페이스 / 구현

    • 멤버 서비스는 회원 가입/ 회원 조회 기능이 있다.
    • 멤버 서비스 인터페이스를 구현한다.
    // 멤버서비스 인터페이스
    package myname.core2.member;
    public interface MemberService {
        void save(Long memberId, Member member);
        Member findMember(Long memberId);
    }
    
    // 멤버서비스 구현체
    
    package myname.core2.member;
    import myname.core2.discount.DiscountPolicy;
    import myname.core2.discount.FixDiscountPolicy;
    public class MemberServiceImpl implements MemberService{
        MemberRepository memberRepository = new MemoryMemberRepository();
    
        @Override
        public void save(Long memberId, Member member) {
            memberRepository.join(memberId, member);
        }
    
        @Override
        public Member findMember(Long memberId) {
            return memberRepository.findById(memberId);
        }
    }

     

    4-2 멤버서비스 테스트 코드

    • 멤버를 하나 생성해서 멤버서비스를 통해 저장한다.
    • 멤버서비스를 통해 정상적으로 멤버가 불러지는지 확인한다.
    public class MemberServiceTest {
        @Test
        void saveTest(){
            Long memberId = 1L;
            Member member = new Member(memberId, "memberA", Grade.VIP);
            MemberService memberService = new MemberServiceImpl();
            memberService.save(memberId, member);
    
            Member findMember = memberService.findMember(memberId);
            assertThat(findMember.getMemberName()).isEqualTo("memberA");
        }
    }

     

    5-1 OrderService 인터페이스 / 구현체 구현하기

    • Order 클래스는 memberId, itemName, itemPrice, discountPrice 멤버변수를 가진다.
    • Order 클래스는 생성자와 Getter//Setter를 가진다.
    • Order 클래스는 할인 금액을 반영한 금액을 계산하는 메서드를 가진다.
    • OrderService는 itemName, memberId, itemPrice를 매개변수로 받고, 할인된 금액이 저장된 Order를 return 해주는 메소드를 가진다.
    • OrderServiceImpl은 OrderService의 구현체다.
    // 오더클래스를 구현함.
    public class Order {
        private Long memberId;
        private String itemId;
        private int itemPrice;
        private int discountPrice;
    
        public Order(Long memberId, String itemId, int itemPrice, int discountPrice) {
            this.memberId = memberId;
            this.itemId = itemId;
            this.itemPrice = itemPrice;
            this.discountPrice = discountPrice;
        }
        public int calculateDiscount(){
            return this.itemPrice - this.discountPrice;
        }
        public Long getMemberId() {
            return memberId;
        }
    
        public void setMemberId(Long memberId) {
            this.memberId = memberId;
        }
    
        public String getItemId() {
            return itemId;
        }
    
        public void setItemId(String itemId) {
            this.itemId = itemId;
        }
    
        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;
        }
    }

    아래는 OrderService의 인터페이스와 구현체를 함께 구현했다.

    1. 주어진 멤버ID 매개변수를 활용해, 멤버 서비스에서 멤버를 찾아온다
    2. 멤버 서비스를 DiscountPolicy에 넘겨주어 디스카운트 금액을 받아온다
    3. 받은 값들로 Order 객체를 생성해서 넘겨준다. 
    // OrderService의 Interface
    package myname.core2.order;
    public interface OrderService {
        Order createOrder(Long memberId, String itemName, int itemPrice);
    }
    
    // OrderService의 구현체
    package myname.core2.order;
    import myname.core2.discount.DiscountPolicy;
    import myname.core2.discount.FixDiscountPolicy;
    import myname.core2.member.Member;
    import myname.core2.member.MemberRepository;
    import myname.core2.member.MemoryMemberRepository;
    
    public class OrderServiceImpl implements OrderService{
        DiscountPolicy discountPolicy = new FixDiscountPolicy();
        MemberRepository memberRepository = new MemoryMemberRepository();
    
        @Override
        public Order createOrder(Long memberId, String itemName, int itemPrice) {
            // 멤버이름으로 검색해서, 멤버를 가져온다
            Member member = memberRepository.findById(memberId);
            // 멤버를 넘겨주고, 할인된 디스카운트 금액을 가져온다.
            int discountPriceAmount = discountPolicy.discount(member, itemName, itemPrice);
            // 디스카운트 금액을 Order에 반영해서 넘겨준다.
            return new Order(memberId, itemName, itemPrice, discountPriceAmount);
        }
    }

    5-2 OrderService의 Test 코드 작성 with JUNIT

    • Member를 하나 생성해서 MemberService에 넣어준다.
    • 한 Member가 주문을 했고, 이에 따른 Order의 Discount 금액이 정액할인 적용금액인 9000원인지 확인한다.
    package myname.core2.order;
    import myname.core2.discount.DiscountPolicy;
    import myname.core2.discount.FixDiscountPolicy;
    import myname.core2.member.*;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    import static org.assertj.core.api.Assertions.assertThat;
    public class OrderServiceTest {
    
        MemberService memberService = new MemberServiceImpl();
        OrderService orderService = new OrderServiceImpl();
    
        @Test
        void createOrderTEst(){
            Long memberId = 1L;
            int itemPrice = 10000;
            Member member = new Member(memberId, "memberA", Grade.VIP);
    
            memberService.save(memberId, member);
            Order order = orderService.createOrder(memberId, "itemA", itemPrice);
            assertThat(order.calculateDiscount()).isEqualTo(9000);
        }
    }

    5-3 OrderApplication의 main 함수에서의 테스트

    package myname.core2;
    import myname.core2.member.Grade;
    import myname.core2.member.Member;
    import myname.core2.member.MemberService;
    import myname.core2.member.MemberServiceImpl;
    import myname.core2.order.Order;
    import myname.core2.order.OrderService;
    import myname.core2.order.OrderServiceImpl;
    public class OrderApp {
    
    
        public static void main(String[] args) {
    
            MemberService memberService = new MemberServiceImpl();
            OrderService orderService = new OrderServiceImpl();
    
            Long memberId = 1L;
            Member member1 = new Member(memberId, "memberA", Grade.VIP);
            memberService.save(memberId, member1);
            Order order = orderService.createOrder(memberId, "itemA", 10000);
    
            System.out.println("order = " + order.toString());
            System.out.println("order 최종금액 = " + order.calculateDiscount());
        }
    }

     

    위 코드의 문제점


    OCP 원칙과 DIP 원칙에 위배된다. 예를 들어, 가격할인정책이 정율할인정책으로 바뀐다고 가정하자. 그렇게 되면 정율할인정책 클래스를 구현한 후, OrderServiceImpl 구현체의 코드가 변경되어야만 한다. 이유는 구현체 내에서 사용할 객체를 선택하고 있기 때문이다. 즉, 구현체는 추상화에만 의존하는 것이 아닌 구현체에도 의존하고 있다. 따라서 DIP 원칙에 위배되는 것을 알 수 있다. 

    DIP 원칙에 위배되기 때문에 할인 정책이 바뀌게 되면 OrderServiceImpl 구현체의 내부 코드가 변경되어야 한다. 즉, 확장은 가능하나 변경은 있으면 안되는 OCP 원칙에도 위배된다. 이런 이유는 구현체가 추상화에만 의존해야하는데, 구현체에도 동시에 의존하기 때문에 발생하는 일이다. 

    public class OrderServiceImpl implements OrderService{
        DiscountPolicy discountPolicy = new FixDiscountPolicy();
        MemberRepository memberRepository = new MemoryMemberRepository();
    
        @Override
        public Order createOrder(Long memberId, String itemName, int itemPrice) {
            // 멤버이름으로 검색해서, 멤버를 가져온다
            Member member = memberRepository.findById(memberId);
            // 멤버를 넘겨주고, 할인된 디스카운트 금액을 가져온다.
            int discountPriceAmount = discountPolicy.discount(member, itemName, itemPrice);
            // 디스카운트 금액을 Order에 반영해서 넘겨준다.
            return new Order(memberId, itemName, itemPrice, discountPriceAmount);
        }
    }

    그렇다면 위의 문제를 어떻게 하면 해결할 수 있을까? 

     

     

     

     

     

     

     

     

     

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

    스프링 컨테이너 관련 정리  (0) 2021.11.07
    스프링 프레임워크가 필요한 이유  (0) 2021.11.04
    8. 웹 MVC 개발, 회원관리 예제  (0) 2021.10.31
    7. Spring API 아주 기초  (0) 2021.10.31
    6. Spring MVC 아주 기초  (0) 2021.10.30

    댓글

    Designed by JB FACTORY