본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2025 1학기 스프링부트 스터디] 석준환 #2주차

반응형

아래는 위 요구 사항에 맞게 스프링을 이용하지 않고 순수하게 자바로만 구성한 간단한 프로그램이다.

 

회원 엔티티

-Member entity-

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

-getter, setter 설정

-생성자 설정

 

-Grade enum-

package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}

 

 

회원 저장소

-MemberRepository interface-

package hello.core.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

 

위 인터페스는 역할

구현체를 만들어야 한다.(MemoryMemberRepository  or  DbMemberRepository)

 

-MemoryMemberRepository-

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

 

 

회원 서비스

-회원 서비스 인터페이스-

package hello.core.member;

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

 

-회원 서비스-

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

 

문제점이 존재!

DIP(Dependency Inversion Principle): 추상(역할)에만 의존해야 한다는 원칙 -->인터페이스에만 의존해야 한다

를 위반

추상 뿐만 아니라 구현체에도 의존하고 있다

private final MemberRepository memberRepository = new MemoryMemberRepository();

 

회원 도메인 실행과 테스트

-회원가입 Main-

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findmember = memberService.findMember(1L);
        System.out.println(member.getName());
        System.out.println(findmember.getName());
    }
}

-회원 가입 Test-

package hello.core.member;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    private MemberService memberService = new MemberServiceImpl();

    @Test
    void join() {
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

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

        Assertions.assertEquals(member, findmember);
    }
}

 

주문과 할인 정책

-DiscountPolicy Interface-

package hello.core.discount;

import hello.core.member.Member;

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

 

-FixedDiscountPolicty-

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class FixDiscountPolicy  implements DiscountPolicy {

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }
        else {
            return 0;
        }
    }
}

-현재 정책의 방향은 vip 고객 한정 주문을 할때마다 1000원씩 할인해주는 정책 --> 추후에 변경 가능성이 있다

 

-order entity-

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.itemName = itemName;
        this.memberId = memberId;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

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

    public int getDiscountPrice() {
        return discountPrice;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public String getItemName() {
        return itemName;
    }

    public Long getMemberId() {
        return memberId;
    }

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

 

-orderService interface-

package hello.core.order;

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

-orderService 구현체-

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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);
    }
}

member를 discountpolicy로 넘겨서 알아서 할인 정책을 받아온다.

이 부분은 SRP(Solid responsibility Principle) 원칙을 잘 지키는 부분이다!

discount관련된 부분은 discountpolicy만 담당한다

만약 할인 정책이 바뀌었는데 orderServiceImpl 부분을 수정해야 한다면 SRP 원칙이 잘 안지켜진 것이다.

그러나 OCP(Open-Closer Principle): 새로운 기능을 추가할 때 기존 코드를 수정하지 않아야 한다. 

가 위반되었다

-->FixDiscountPolicy() 대신 RateDiscountPolicy()로 바꾸려면 OrderServiceImpl()의 코드도 변경해야 한다

클라이언트 코드에도 영향을 준다!

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

 

주문과 할인 도메인 실행과 테스트

-orderApp-

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        OrderService orderService = new OrderServiceImpl();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);

        System.out.println("orer = " +order);
        System.out.println("orer.calculatePrice = " +order.calculatePrice());
    }
}

**결과**

order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}

 

-OrderServiceTest-

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @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.assertEquals(order.getDiscountPrice(), 1000);
    }
}

문제를 해결해보자

1.DIP

클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 **구체(구현) 클래스에도 의존**하고 있다.

추상(인터페이스) 의존: `DiscountPolicy`

구체(구현) 클래스: `FixDiscountPolicy` , `RateDiscountPolicy`

2.OCP

**지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다!** 따라서 **OCP 위반**한다.

 

관심사 분리

애플리케이션을 하나의 공연이라고 생각하고 각각의 구현체를 배우라고 생각하자

실제 배역에 맞는 배우를 선택하는 건 누가 해야 할까?

이전 코드는 마치 레오나르도 디카프리오가 여자 주인공 캐스팅도 담당하는 느낌이다

즉 공연 기획팀이 해야 할 일을 배우가 하면 안된다!

디카프리오는 공연도 해야하는데 동시에 여자 주인공도 직접 캐스팅하는 다양한 책임을 져야 한다

그래서 관심사를 분리해야 한다

공연 기획자를 만들고 배우와 공연 기획자의 책임을 분리하자!

 

AppConfig(공연 기획자) 등장

package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService() {
        return new OrderServiceImpl(
            new MemoryMemberRepository(),
            new FixDiscountPolicy());
    }
}

 

-MemberServiceImpl-

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

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

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

역할(구현체)의 설정은 AppConfig가 하므로  MemberServiceImpl은 memberRepository(역할)에만 의존하면 된다

private final MemberRepository memberRepository = new MemoryMemberRepository();

따라서 이런 코드를 작성하지 않아도 된다

 

-MemberApp-

public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
//        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findmember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findmember.getName());
    }
}

 

-OrderApp-

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {
    public static void main(String[] args) {
//        MemberService memberService = new MemberServiceImpl(null);
//        OrderService orderService = new OrderServiceImpl(null, null);

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);

        System.out.println("orer = " +order);
        System.out.println("orer.calculatePrice = " +order.calculatePrice());
    }
}

 

 

주석 부분 => 이전 코드 

 

그에 맞게 테스트 코드도 수정했다

-MemberServiceTest-

package hello.core.member;

import hello.core.AppConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

//    private MemberService memberService = new MemberServiceImpl();
    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);

        Assertions.assertEquals(member, findmember);
    }
}

 

-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.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

//    MemberService memberService = new MemberServiceImpl();
//    OrderService orderService = new OrderServiceImpl();

    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.assertEquals(order.getDiscountPrice(), 1000);
    }
}

 

반응형