반응형

# 객체 지향 원리 적용_2

  • 기존에 적용했던 정액할인 정책(고정 금액 할인)에서 새로운 할인 정책인 정률 할인 정책(금액에 따른 비율로 할인)으로 변경.
  • 할인 정책을 변경하게 되면 클라이언트 코드인 주문 서비스 구현체를 변경해야 하는 문제점이 발생했었다. (DIP 위반)
  • 문제점을 해결하기 위해 관심사를 분리(AppConfig 활용)

## 좋은 객체 지향 설계의 5가지 원칙 적용

  • 현재 작업된 소스에서 SRP, DIP, OCP 가 적용되어있다.

SRP : 단일 책임의 원칙

  • 핵심 : 한 클래스는 하나의 책임만 가져야 한다.
  • 기존에 클라이언트 객체는 직접 구현 객체를 생성하는 등 많은 책임이 존재했다.
  • SRP 적용하여 관심사를 분리 -> AppConfig 가 구현 객체 생성, 연결의 책임을 가지게 됨.
  • 클라이언트 객체의 경우 실행에 대한 책임만 담당.

DIP : 의존관계 역전의 원칙

  • 핵심 : 프로그래머는 추상화에 의존, 구체화에 의존하면 안된다. (의존성 주입은 해당 원칙을 따르는 방법 중 하나임)
  • AppConfig 생성하여 외부에서 객체에 대한 의존 관계를 주입. (객체가 추상화에 의존해야 가능한 것)

OCP

  • 핵심 : 소프트웨어의 요소는 확장에는 열려있고, 변경에는 닫혀있어야 한다.
  • 다형성을 사용, 클라이언트가 DIP를 지킴.
  • 애플리케이션을 사용 영역, 구성 영역으로 분리.
  • APpConfig 가 의존관계를 FixDiscountPolicy 에서 RateDiscountPolicy 로 변경하여 클라이언트에 주입하여 클라이언트의 코드는 변경하지 않아도 되었음. (소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있게 됨(변경할 필요 없었음))

## IoC, DI 그리고 컨테이너

IoC (Inversion of Control) : 제어의 역전 

  • 제어권이 뒤바뀜.
  • 기존 : 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행. (프로그램의 제어 흐름을 스스로 조종)
  • 변경 : AppConfig 등장으로 구현 객체는 자신의 로직을 실행만 하는 역할을 함. (프로그램 제어 흐름이 AppConfig 에 존재)
  • 정리 : 프로그램의 제어 흐름을 직접 제어하는 것이 아닌, 외부에서 관리하는 것을 의미.
  • 프레임워크 vs 라이브러리
프레임워크가 내가 작성한 코드를 제어, 대신 실행하면 프레임워크가 맞음 (JUnit)

내가 작성한 코드가 직접 제어의 흐름을 담당하는 경우는 라이브러리.

DI (Dependency Injection) : 의존 관계 주입

  • 구현체가 인터페이스에 의존, 실제 어떤 구현 객체가 사용될지 모름.
  • 정적 클래스 의존 관계, 실행 시점에 결정되는 동적 객체(인스턴스) 의존 관계를 분리해서 생각해야 함.
  • 정적 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계 판단 가능. 

정적 의존관계는 애플리케이션 실행 없이 분석 가능. (클래스 다이어그램만 보고 판단가능)
  • 동적인 객체 인스턴스 의존 관계
애플리케이션 실행 시점에서 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계를 의미. (객체 다이어그램)
  • 의존관계 주입 : 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성, 클라이언트에 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는 것.
  • 객체 인스턴스를 생성, 그 참조값을 전달해서 연결함.
  • 정리 : 의존관계 주입 사용 시 클라이언트 코드 변경 없이 클라이언트가 호출하는 대상의 타입 인스턴스 변경 가능하다. 그리고 정적 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 가능하다.

DI 컨테이너 (= IoC 컨테이너)

  • AppConfig 처럼 객체 생성, 의존관계 연결, 관리해주는 것을 의미.
  • 의존관계 주입에 초점을 맞춰 DI 컨테이너라고 함. (또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 함)

## 스프링으로 전환 (스프링 컨테이너 사용)

  • 지금까지의 작업물은 순수 자바 코드로 작업 한 것.
  • 순수한 자바 코드만으로 DI 적용한 것을 스프링으로 변경.

AppConfig

  • @Configuration
  • @Bean
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // memberService 역할
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    // memberRepository 역할
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    // orderService 역할
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    // discountPolicy 역할
    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

MemberApp

  • 스프링의 컨테이너 : ApplicationContext
  • 아래와 같이 해주면 AppConfig에 있는 것들을 스프링 컨테이너에 객체 생성 해서 관리하게 함.
// 스프링 컨테이너 : ApplicationContext
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        // 기존
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        // 스프링 컨테이너 : ApplicationContext
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
    public static void main(String[] args) {

        // 기존
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        // 스프링 컨테이너 : ApplicationContext
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

//        MemberService memberService = new MemberServiceImpl();
        Member memberA = new Member(1L, "memberA", Grade.VIP);
        memberService.join(memberA);

        Member findMember = memberService.findMember(1L);
        System.out.println("member : " + memberA.getName());
        System.out.println("findMember : " + findMember.getName());
    }
}

OrderApp

  • MemberApp 에 적용한 것처럼 ApplicationContext 사용.
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;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {
    public static void main(String[] args) {

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

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

//        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("주문 = " + order.toString());
    }
}

스프링 컨테이너  

  • 스프링 컨테이너 : ApplicationContext
  • 기존 : AppConfig 사용해서 직접 객체를 생성 및 의존성 주입 (DI) 진행. (AppConfig 통해서 진행)
  • 변경 : 스프링 컨테이너를 활용. (AppicationContext 통해서 진행)
  • 스프링 빈의 경우 @Bean 이 붙은 메서드 명을 스프링 빈의 이름으로 사용.
  • 스프링 빈 찾기 : appcationContext.getBean("bean이름", 타입)
반응형
반응형

# 객체 지향 원리 적용_1

## 새로운 할인 정책 개발

  • 기존의 고정 금액 할인이 아닌, 주문 금액 당 퍼센트인 정률할인으로 변경.
  • 기존에 DiscountPolicy (인터페이스) 의 구현체로 FIxDiscountPolicy 사용하였는데, 정률할인을 구현체인 RateDiscountPolicy 추가.

RateDiscountPolicy 

package hello.core.discount;

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

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

테스트 코드 생성 단축키.

  • 인텔리제이에서 윈도우의 경우 Ctrl + Shift + T 입력 시 Test 생성 팝업 호출됨.

  • 아래와 같이 검증 코드에 Alt + Enter 입력 시 (윈도우의 경우) Add static import 하여 사용 가능하다.

Alt + Enter (윈도우의 경우)
Add static import 적용된 검증코드.

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

## 새로운 할인 정책 적용과 문제점

  • OCP 위반 : 현재 작업한 소스에서 할인 정책 변경 시 OrderServiceImpl 의 코드를 고쳐야 함.
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();    // 고정할인 
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 정률할인
}
  • 역할과 구현 분리되었고, 다형성 활용 및 인터페이스와 구현 객체 분리 됨.
  • 하지만, OCP, DIP 같은 객체지향 설계 원칙을 준수하지 않음. (문제점)
  • DIP 위반 : 주문 서비스 클라이언트 (OrderServiceImpl) 의 경우 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같지만, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존 중.
여기서.. 
추상(인터페이스) 의존 : DiscountPolicy
구체(구현) 클래스 : FixDiscountPolicy, RateDiscountPolicy

## 관심사의 분리 (DIP, OCP 위반 문제 해결)

  • 공연을 예로 들면 배우는 배우의 역할에만 집중, 배우가 공연을 구성하고 다른 배역을 섭외 하는 등의 업무를 해서는 않된다. 배우는 상대 배우가 누구라도 동일한 공연을 할 수 있어야 함. 이를 위해 공연을 구성하고 배역을 섭외하는 역할을 하는 별도의 기획자가 필요. (AppConfig)

AppConfig (중요!!!!) - 객체의 생성&&주입(연결) 담당. 

  • 구현 객체 생성, 연결하는 책임을 갖는 별도의 설정 클래스.
  • 애플리케이션의 실제 동작에 필요한 구현 객체 생성, 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해 주입(연결) 함.
public class AppConfig {

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

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

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

}
  • 위와같이 설계 변경을 함으로써 Impl이 인터페이스에 의존하게 됨.
  • 오직 외부(AppConfig)에서 어떤 구현 객체가 주입될지 결정 됨. (DI (Dependency Injection, 의존관계 주입))

MemberApp

  • AppConfig 로 의존관계 주입하여 적용.
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) {

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

//        MemberService memberService = new MemberServiceImpl();
        Member memberA = new Member(1L, "memberA", Grade.VIP);
        memberService.join(memberA);

        Member findMember = memberService.findMember(1L);
        System.out.println("member : " + memberA.getName());
        System.out.println("findMember : " + findMember.getName());
    }
}

OrderApp

  • AppConfig 로 의존관계 주입하여 적용.
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) {

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();
//        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("주문 = " + order.toString());
    }
}

MemberServiceTest

  • AppConfig 로 의존관계 주입하여 적용.
  • @BeforeEach : 실행 전 실행됨.
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);
    }
}

OrderServiceTest

  • AppConfig 로 의존관계 주입하여 적용.
  • @BeforeEach : 실행 전 실행됨.
public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

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

}

## AppConfig 리팩터링 

  • 중복 및 역할에 따른 구현이 잘 안보이는 현상 발생하여 이를 개선.

기존 AppConfig

public class AppConfig {

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

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

변경 AppConfig

  • new MemoryMemberRepository() 중복 제거.
  • 역할과 구현 클래스가 한눈에 보이도록 분리.
public class AppConfig {

    // memberService 역할
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    // memberRepository 역할
    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

    // discountPolicy 역할
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

## 새로운 구조와 할인 정책 적용

  • 기존 정액할인 적용 -> 정률할인 정책 적용으로 변경.
  • 정책 변경을 위해 AppConfig 에서만 변경해주면 됨. -> AppConfig로 클라이언트 코드에 대한 변경 없이 정책 변경 가능해짐. (사용영역 전혀 변경할 필요 없음)
public class AppConfig {

    // memberService 역할
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    // memberRepository 역할
    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

    // discountPolicy 역할
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
반응형
반응형

# 예제 프로젝트 진행 (스프링 없는 순수 자바로만 개발 진행)

## 주문과 할인 도메인 설계

주문과 할인 정책

  • 회원은 상품 주문 가능
  • 회원 등급에 따라 할인 정책 적용 가능.
  • 할인 정책의 경우 VIP는 1000원을 할인하는 고정 할인. (변경 가능성 존재.)
  • 할인 정책은 변경 가능성 존재.

주문 도메인 협력과 역할 및 책임

  1. 주문 생성 : 클라이언트는 주문 서비스에 주문 생성 요청.
  2. 회원 조회 : 할인을 위해 회원 등급이 필요. 그래서 주문 서비스는 회원 저장소에서 회원을 조회.
  3. 할인 적용 : 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임.
  4. 주문 결과 반환 : 주문 서비스는 할인 결과를 포함한 주문 결과 반환.

주문 도메인 전체

  • 클라이언트
  • 주문 서비스 역할, 주문 서비스 구현체
  • 회원 저장소 역할, 구현체로 메모리 회원 저장소 또는 DB 회원 저장소.
  • 할인 정책 역할, 구현체로 정액 할인 정책, 정률 할인 정책.
  • 위와 같이 역할과 구현을 분리하여 자유롭게 구현 객체 조립 가능. (회원 저장소 및 할인 정책을 유연하게 변경 가능)

## 주문과 할인 도메인 개발

DiscountPolicy (인터페이스)

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

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

FixDiscountPolicy (DiscountPolicy 의 구현체)

package hello.core.discount;

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

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

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

OrderService (인터페이스)

package hello.core.order;

public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}

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

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

방법1) 메인메서드 이용한 테스트 

OrderApp (OrderApp 생성하여 System.out.print 로 출력해보기_메인 메서드 이용한 테스트는 좋지 않음.)

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("주문 = " + order.toString());
    }
}

방법2) Junit 이용한 테스트

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.assertj.core.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.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

반응형
반응형

# 예제 프로젝트 진행 (스프링 없는 순수 자바로만 개발 진행)

  • 프로젝트 환경 설정을 편리하기 위해 스프링 부트 사용, 해당 예제 프로젝트는 스프링 없는 순수 자바로만 개발 진행.
  • 역할(인터페이스) 구현(구현체) 나누어서 개발 진행.
  • 필요 준비물 : Java 11 / IDE (IntelliJ or Eclipse)

## 프로젝트 생성

스프링 부트 스타터 이용 스프링 프로젝트 생성

Project : Gradle Project

Language : Java

Spring Boot : SNAPSHOR or M 붙지 않은 가장 최신 버전 선택.

Group : hello

Artifact : core (프로젝트 빌드명)

Packaging : Jar

Java : 11

Dependencies : 선택 없이 진행. (의존관계)

  • GENERATE 로 압축 파일 내려받은 후 별도의 위치에 압축해제, 인텔리제이 실행해서 압축해제한 프로젝트 폴더 내 build.gradle 선택하여 프로젝트 실행.
  • 설정이 모두 받아진 후 CoreApplication 실행하여 정상적으로 실행되는지 확인.
  • 설정 변경 : Build and run using 과 Run tests using 을 IntelliJ IDEA로 변경. (윈도우의 경우 File -> Settings -> Gradle 입력)

## 비즈니스 요구사항과 설계

비즈니스 요구사항

1) 회원

  • 회원 가입, 조회
  • 회원 등급 존재 (일반, VIP)
  • 회원 데이터 : 자체 DB 구축 or 외부 시스템과 연동 가능성. (미확정)

2) 주문과 할인 정책

  • 회원은 상품 주문 가능.
  • 회원 등급에 따라 할인 정책 적용.
  • 할인 정책 : VIP는 1000원을 할인 (고정 금액 할인 -> 변경 가능성 존재, 미확정)

미확정된 부분 관련

  • 역할과 구현을 분리 즉, 인터페이스와 구현체를 나누어 미확정된 부분에 대응할 수 있도록 함.

## 회원 도메인 설계

회원 도메인 요구사항

  1. 회원 가입, 회원 조회
  2. 회원 등급 존재 (일반, VIP)
  3. 회원 데이터 (자체 DB 구축 or 외부 시스템 연동 - 미확정)

회원 도메인 협력 관계

  • 클라이언트 -> 회원 서비스 호출 (회원 가입 / 회원 조회 기능) -> 회원 저장소 생성 (인터페이스)
  • 회원저장소(인터페이스) 의 역할(구현체)에 해당하는 메모리 회원 저장소, DB 회원 저장소, 외부 시스템 연동 회원 저장소 구현체 생성 (메모리 회원 저장소의 경우 재 시작 시 데이터 초기화(메모리 휘발성) 되므로 개발용도로 사용.

회원 클래스 다이어그램

  • MemberService (인터페이스) - MemberServiceImpl (MemberService 의 구현체(구현 클래스))
  • MemberRepository (인터페이스) - MemoryMemberRepository, DbMemberRepository (MemberRepository 의 구현체(구현 클래스))

회원 객체 다이어그램 (객체 간 참조)

  • 클라이언트 -> 회원 서비스(MemberServiceImpl) -> 메모리 회원 저장소

## 회원 도메인 개발

  • 인텔리제이 단축키 확인방법 : 윈도우의 경우 File -> Settings -> keymap 검색 해당 항목에서 찾고자 하는 항목 찾아서 사용하면 된다.

Grade

package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}

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

MemberRepository (인터페이스)

package hello.core.member;

public interface MemberRepository {

    void save(Member member);

    Member findById(Long memberId);
}

MemoryMemberRepository (MemberRepository 구현체)

package hello.core.member;

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

public class MemoryMemberRepository implements MemberRepository {

    // 동시성 이슈로 인해 실무에서는 ConcurrentHashMap 사용 해야 함.
    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 (인터페이스)

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

MemberServiceImp (MemberService 구현체)

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

## 회원 도메인 실행 및 테스트

  • 위 작업 내역 정상 동작하는지 테스트.

테스트 방법1) 메인 메서드를 이용한 테스트 (좋은 방법 아님.)

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 memberA = new Member(1L, "memberA", Grade.VIP);
        memberService.join(memberA);

        Member findMember = memberService.findMember(1L);
        System.out.println("member : " + memberA.getName());
        System.out.println("findMember : " + findMember.getName());
    }
}

테스트 방법2) Junit 테스트 프레임워크 이용.

  • 테스트 코드 작성은 필수임. (반드시!)
  • test 에 구조 동일하게 생성 하여 테스트 코드 작성.

package hello.core.member;

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

public class MemberServiceTest {

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

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

현재 회원 도메인 설계 문제점

  • 다른 저장소 변경 시 OCP 원칙 준수?
  • DIP 잘 지키고 있는지?
  • 의존관계 문제. (MemberServiceImpl 에서 아래와 같이 MemberRepository 와 MemoryMemberRepository 모두 의존 중)
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
반응형

+ Recent posts