본문 바로가기
Study/Java Spring Boot

[Spring] 핵심 원리 3 객체 지향 원리 적용 (DI컨테이너)

본 글은 김영한님의 강의 내용을 바탕으로 정리한 글입니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 학습 페이지

지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

www.inflearn.com

 

 

1. 새로운 할인정책 추가

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

테스트도 해보자

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;

public class RateDiscountPolicyTest {
    DiscountPolicy discountPolicy = new RateDiscountPolicy();
    @Test
    @DisplayName("VIP 10% 할인")
    void vip_o(){
        Member member = new Member(1L,"memberVIP", Grade.VIP);
        int discount = discountPolicy.discount(member,10000);
        Assertions.assertThat(discount).isEqualTo(1000);
    }
    @Test
    @DisplayName("BASIC 할인 없음")
    void vip_x(){
        Member member = new Member(1L,"memberBASIC", Grade.BASIC);
        int discount = discountPolicy.discount(member,10000);
        Assertions.assertThat(discount).isEqualTo(0);
    }
}

 

 

 

2. 문제점

 

 SOILD에 의거해서 지금까지의 구현코드를 되짚어보자!

SRP(단일 책임 원칙)을 잘 지켜 도메인을 잘 설계하여, 각각 역할과 책임을 가지도록 결합도를 낮췄다.

ISP: 인터페이스 분리 원칙을 바라보면, 인터페이스에서 사용하지 않는 부분이 없고 , 문제가 될 사항이 없다 

LSP(리스코프 치환 원칙) 에 해당되는 부분이 없으니 고려사항이 아니다.

새로운 정책이 추가 개발되면, 

OCP에 의거 기존 코드는 변경(Closed)없이 기능을 수정하거나 추가(Open)할 수 있도록 만들어야 한다.

하지만 할인 구현체가 기존에 고정 할인정책에 의존하는 부분이 있고.

일일이 찾아 수정해줘야하는 OCP 위반이 일어난다.

이런 일이 발생한 이유는

DIP 의존관계 역전 원칙 (구현체가 아닌 추상체에 의존해야함) 이 먼저 지켜지지 않았기 떄문이다.

저렇게 구현체에 의존해선 안되고, 추상클래스에 의존해야만 한다.

저런식으로 코드가 구현된다면, 지속적으로 개발이 이루어질 때마다. 

구현체 연결부분들을 코드상에서 전부 찾아 신규 구현체를 연결해야하기에

객체 지향적이지 못한것이다.

 

그러니 우리는 구현체에 의존하지 않도록 추상체에 의존하도록 바꿔주어야 한다. 

그리고 구현체를 생성하고 주입해주는 부분을 한군데 (AppConfig)로 몰아서 수행되도록 관리하면 된다.

 

AppConfig를 통해 VM실행하는 동시에 인스턴스가 생성되어

필요한 부분에 주입되도록 만들어주므로 결합도를 낮출수 있다!

 

지금부터 구현체에 의존하던 모든 부분을 찾아서 제거 해보자

 

 

 

3. 추상체 의존, 생성자 주입 

먼저 Member 도메인부터 올바르게 문제점을 제거해보자.

 

3-1) 도메인 상에 DIP 위반 제거

현재 OrderServiceImpl 하나만 두고 볼 때, 인터페이스와 구현체를 모두 의존하고 있다.

구현체 인스턴스를 만들어주는 문제에 부분을 DIP원칙에 의거하여 

인터페이스 객체변수만 선언하고, 외부에서 인스턴스를 주입받도록 구현해주자!

인터페이스를 주입받기 위해서는 인스턴스를 받아서 해당 클래스에 넣어주도록 생성자를 만들어야 한다.

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();
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy){
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

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

이런 식으로 만들어주면, 더이상 구현체가 아닌 추상체에 의존하면서, 

새로운 클래스 확장시 이 클래스는 건들지 않고 변경할수 있다.

그렇다면 이 생성자는 OrderApp 과 같은 클라이언트측 클래스에서

인스턴스를 만들어 주입하도록 호출해주면된다.

 

일단, MemberRepository 도 같은 방식으로 MemoryMemberRepository 를 의존하는 부분을 제거해주자.

package hello.core.member;

public class MemberServiceImpl implements MemberService{

//  private final MemberRepository memberRepository = new MemoryMemberRepository();
    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);
    }
}

 

이제 만들었던 도메인 상에서 서비스에서 사용할 구현체클래스들이 모두 DIP를 지키게 되었다.

이제 호출하는 부분인 MemberApp과 OrderApp을 변경해서 정상 수행시켜보자.

package hello.core;

import hello.core.member.*;
import hello.core.order.*;

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, "item1", 10000);
        System.out.println("order = " + order);
    }
}
package hello.core;

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

import java.util.Arrays;

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

        Member findMember = memberService.findMember(member1.getId());
        System.out.println("findMember = " + findMember.getName());
        System.out.println("member1 = " + member1.getName());
    }
}

이렇게 되면 정상적으로 도메인상에 DIP위반은 없다.

하지만. 생각해보면, 도메인을 수행하는 ~~App 클래스들은 결국 또 다시 DIP를 위반한다.

이 모두를 한곳에서 명확히 확인하고 관리해줄수 있는 방법으로 AppConfig를 만들어보자!

 

 

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

이렇게 core패키지 상에 AppConfig를 만들어서 다양한 확장에 어떤 구현체를 넣을지는

이 컨테이너 하나만 보고 확인하고 변경할수 있다.

AppConfig는 java클래스이지만, 설정을 위한 구성으로보고 DIP를 위반했다고 말하지 않는다.

 

클라이언트쪽인 App부분도 변경해보자

package hello.core;

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

import java.util.Arrays;

public class MemberApp {
    public static void main(String[] args) {
//        MemberService memberService = new MemberServiceImpl();

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

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

        Member findMember = memberService.findMember(member1.getId());
        System.out.println("findMember = " + findMember.getName());
        System.out.println("member1 = " + member1.getName());
    }
}
package hello.core;

import hello.core.member.*;
import hello.core.order.*;

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

        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, "item1", 10000);
        System.out.println("order = " + order);
    }
}

 

앞으로의 ClientApp들도 모두 이런식으로 AppConfig만 의존하도록 만들어주면 

OCP, DIP를 모두 지키는 객체 지향적인 백엔드개발이 된다. 

 

Test쪽도 의존중인 부분을 다 변경해보자

package hello.core.order;

import hello.core.AppConfig;
import hello.core.member.*;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {
    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, "item1", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
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,"Member1",Grade.VIP);
        //when
        memberService.join(member);
        Member member1 = memberService.findMember(1L);
        //then
        Assertions.assertThat(member).isEqualTo(member1);

    }
}

 

3-3) AppConfig상에 불분명함

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

잠시 위 AppConfig를 생각해보면

우리는 Service호출 부분마다 새로운 객체를 생성하도록 만들어 두었다.

비 직관적이고 난해하기 때문에, 이 또한 아래와같이 사용 도메인 객체별로 깔끔하게 코딩하자.

package hello.core;

import hello.core.discount.*;
import hello.core.member.*;
import hello.core.order.*;

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(); // 고정 할인 정책으로 생성
    }

}

이제 새로운 확장시에 단 한줄만 바꿔주면 쉽게 모든것을 변경해줄 수 있다.

 

 

4. DI 컨테이너

지금까지 우리가 만든 AppConfig가 사실 백엔드개발에서 너무나도 중요한 DI, IoC 라고 말하는 부분이다.

IoC 제어의 역전 : 제어부분이 사용영역에서 역전되어 설정영역으로 빠진 상황 

DI 의존성 주입 :  클래스 내부에 의존적인 부분이 사라지고, 특정 클래스에 의해서 의존성이 주입되는것.

AppConfigDI컨테이너 = IoC컨테이너 = Object 팩토리 = 어셈블러 등으로 부른다.

 

 

이렇게 SOLID를 잘 지키는 객체지향적인 순수 JAVA 프로그래밍으로 회원,주문,할인 등의 도메인 개발을 진행해보았다.

다음부터는 Spring 으로 전환하면서, 어떠한 부분에 혁신적이고 큰 변동이 있는지를 보자.

 

이번 글까지의 소스코드 ↓