[Spring] 예제 만들기

Ho·2022년 7월 12일
0

Spring

목록 보기
2/8

요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용한다.
    • 할인 정책은 변경 가능성이 높다.

위의 요구사항을 스프링 프레임워크를 사용하지 않고 순수 java 코드로 먼저 구현해본다.


회원 도메인 설계

  • 회원가입, 회원조회 기능
  • 회원 등급
  • 회원 데이터 저장

회원도메인은 위와 같이 설계한다. 클라이언트는 회원 서비스를 이용하고 회원 서비스는 회원가입, 회원조회 기능을 수행할 때 회원 저장소에서 데이터를 읽고 저장한다.

회원 클래스 다이어그램

회원서비스와 데이터를 저장하는 실제 구현은 변경될 가능성이 있으므로 인터페이스 기반으로 설계한다.


회원 도메인 구현하기

회원 엔티티

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

id, 이름, 등급을 가지는 간단한 회원 클래스를 만든다.

회원 저장소 인터페이스

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}

데이터를 어떤 방식으로 어느 db에 저장해야할지 정해지지 않은 상황에서 임시로 회원을 저장할 저장소를 만들고 변경이 가능하도록 하기위해 인터페이스를 만든다.

회원 저장소 구현

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

임시로 어플리케이션을 실행하는동안 회원을 메모리에 저장하여 관리하도록 하는 구현 클래스를 만든다.

회원 서비스 인터페이스

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

회원 서비스 구현

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

회원 서비스는 회원가입, 회원 조회를 하기위해 MemberRepository를 클래스 내부에 포함하고있다. 회원서비스 구현체는 회원 저장소에 의존한다.

회원 도메인 실행

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("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}

멤버어플리케이션이 실행되면서 멤버 서비스 객체를 생성한다. 이때 멤버서비스 구현체를 객체로 생성한다. 회원가입 및 회원 조회 기능을 수행한다.


주문 도메인 설계

주문 도메인 전체

주문 도메인 클래스 다이어그램

  • 주문 서비스는 회원 저장소와 할인정책에 의존한다.
  • 회원 저장소와 할인 정책은 변경될 수 있다.

주문 도메인 구현

할인 정책 인터페이스

public interface DiscountPolicy {
    int discount(Member member, int price);
}

정액 할인 정책 구현체

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

    }
}

정률 할인 정책 구현체

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

주문 엔티티

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

    
}

주문 서비스 인터페이스

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

주문 서비스 구현체

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

주문 할인 실행

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", 20000);
        System.out.println("order = " + order);
        System.out.println("order.cal = " + order.calculatePrice());

    }
}

문제점

위와 같이 회원서비스와 주문할인서비스를 순수 자바 코드로 구현하였다.
이러한 상황에서 할인 정책을 변경해야하는 상황이 생겼다고 가정해보자. 할인 정책의 변경을 고려하여 인터페이스 기반으로 설계하였고 정률 할인 정책에 대한 구현체도 만들어 놓았다. 다음과 같이 주문 서비스에서 할인 정책의 구현체만 변경하면 된다.

주문서비스 구현체

public class OrderServiceImpl implements OrderService {

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

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

문제점 발견

  • 역할과 구현을 분리하여 인터페이스와 구현 객체를 분리하였다.
  • 다형성을 활용하였다.
  • OCP, DIP와 같은 객체지향 원칙을 준수한것 같아보인다...?

DIP 위반

OrderServiceImpl은 DiscountPolicy를 포함하므로 DiscountPolicy 인터페이스에 의존한다. 하지만 인터페이스의 구현 클래스를 직접 객체로 생성하고 있으므로 RateDiscountPolicy에도 의존하는 소스가 된다.

OCP 위반

변경에는 닫혀있고 확장에는 열려있어야한다. 정률 할인 정책 기능을 확장하기 위해 변경하면 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;
    }

  	//...
}

주문 서비스에서 회원 저장소와 할인 정책에 대한 구현 객체를 직접 생성하지 않도록 변경한다. 생성자를 통해 필드를 초기화한다.

그럼 생성자를 통해 구현 객체는 누가 주입해주는 것일까?


관심사의 분리

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

AppConfig

public class AppConfig {

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

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }


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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

}
  • AppConfig는 애플리케이션에 실제 동작에 필요한 구현 객체를 생성한다.
  • AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해 주입해준다.

회원 서비스 구현체 생성자 주입

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);
    }
}
  • 이제 회원 서비스는 회원 저장소 구현 클래스에 의존하지 않고 인터페이스에만 의존한다.
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다.
  • 생성자를 통해서 어떤 구현 객체가 주입될지는 오직 외부에서 결정된다.
  • MemberServiceImpl은 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

객체 인스턴스의 관계는 다음과 같다.

appConfig는 memberServiceImpl 객체를 생성할 때 memoryMemberRepository 객체를 생성한 뒤 memberServiceImpl 객체를 생성하면서 생성자로 전달한다.


AppConfig 실행

MemberApp 에서의 사용

public class MemberApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();

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

        Member findMember = memberService.findMember(1L);
        
    }
}

OrderApp 에서의 사용

public class OrderApp {
    public static void main(String[] args) {
        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", 20000);
        System.out.println("order = " + order);
        System.out.println("order.cal = " + order.calculatePrice());

    }
  • AppConfig를 통해 관심사 분리
  • 구체적인 클래스를 선택하여 의존관계를 주입하고 어플리케이션이 어떻게 동작해야할지에 대한 전체적인 구성을 책임진다.
  • AppConfig에 대한 변경만으로 기능의 확장이 가능하다 -> OCP
  • 추상에만 의존한다 -> DIP

할인 정책 변경

public class AppConfig {


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

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }


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

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

}

기능을 확장, 변경하여도 구성 영역에만 영향을 받고 사용 영역은 영향을 받지 않는다.


정리

  • SRP (단일 책임 원칙)

    • 하나의 클래스는 하나의 책임만 가져야 한다.
      • 클라이언트 객체는 실행하는 책임만 담당한다.
      • 구성 클래스는 구현 객체를 생성하고 연결하는 책임을 담당한다.
  • DIP (의존관계 역전 원칙)

    • 추상(인터페이스)에만 의존한다.
      • AppConfig가 객체 인스턴스를 대신 생성하여 클라이언트 코드에 의존관계를 주입한다.
  • OCP (개방-폐쇄 원칙)

    • 확장에는 열려있으나 변경에는 닫혀있어야 한다.
      • 애플리케이션을 사용 영역과 구성 영역으로 나눈다.
      • AppConfig가 의존관계를 변경하여 주입하므로 클라이언트 코드는 변경하지 않아도 된다.
      • 소프트웨어 요소를 새롭게 확장하여도 사용영역의 변경은 닫혀있다.

IOC, DI, 컨테이너

IoC(Inversion of Control) 제어의 역전

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행
  • AppConfig를 만든 이후 구현 객체는 자신의 로직을 실행하는 역할만 담당
  • 프로그램의 제어 흐름은 AppConfig가 가져간다
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하도록 하는 것을 제어의 역전(IoC)라고 한다.

의존관계 주입 DI(Dependency Injection)

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달하여 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라고 한다
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다
  • 의존관계는 정적인 클래스 의존관계와 실행시점에 결정되는 동적인 객체 의존관계를 분리해서 생각해야한다
  • 의존관계 주입은 클래스 의존관계를 변경하지 않고 객체 의존관계를 쉽게 변경할 수 있다.

클래스 다이어그램

객체 다이어그램

IoC 컨테이너, DI 컨테이너

  • AppConfig와 같이 객체를 생성하고 관리하며 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.

스프링 적용하기

AppConfig 스프링 기반으로 변경

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

}
  • AppConfig에 @Configuration 을 붙인다.
  • 각 메서드에 @Bean 을 붙인다.
  • 스프링은 @Configuration을 보고 @Bean이 사용된 메서드를 실행하여 스프링 컨테이너에 스프링 빈으로 등록한다.

MemberApp에서 사용

public class MemberApp {
    public static void main(String[] args) {
   
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        
        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에서 사용

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

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

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

        Order order = orderService.createOrder(memberId, "itemA", 20000);
        System.out.println("order = " + order);
        System.out.println("order.cal = " + order.calculatePrice());

    }
}
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했다
  • @Configuration을 사용하면 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
  • @Bean 이 사용된 메서드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록한다.
  • 스프링 컨테이너에 등록된 객체를 스프링 빈 이라고 한다.
  • 스프링 빈은 @Bean이 사용된 메서드의 이름을 스프링빈의 이름으로 사용한다.
  • applicationContext.getBean() 메서드를 사용해서 필요한 객체를 찾을 수 있다.
  • 스프링 컨테이너에 객체를 스프링빈으로 등록하고 스프링 컨테이너에서 찾아서 사용한다.

0개의 댓글