728x90

인프런 김영한님의 강의를 참고했습니다.

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

저번에 자바 코드만을 이용해 주문하는 프로그램을 만들어보았다.

 

기존 프로그램은 할인 정책이 고정적인 금액을 할인해 주는 방법이었는데, 이 방법에서 일정 비율로 할인을 해주는 방법으로 변경을 해본다고 하자.

 

그러면 discount패키지에

package hello.core.Discount;

import hello.core.customer.Customer;
import hello.core.customer.Grade;

public class RateDiscountPolicy implements DiscountPolicy{

    private int VIPDiscountPercent = 5; // 할인되는 비율
    private int VVIPDiscountPercent = 10; //할인되는 비율

    @Override
    public int discount(Customer customer, int price) {
        if(customer.getGrade() == Grade.VIP){
            return price * VIPDiscountPercent / 100;
        }
        else if (customer.getGrade() == Grade.VVIP){
            return price * VVIPDiscountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

이런 코드가 추가되게 된다.

 

그러고 이 RateDiscountPolicy로 정책을 변경하려면

package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;
import hello.core.customer.MemoryCustomerRepository;

public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository = new MemoryCustomerRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }
}

이렇게 OrderServiceImpl class의 코드를 변경해야 한다.

하지만 여기서의 문제는 분명 우리는 할인정책을 변경했지만 관련이 없는 Order 부분의 코드를 수정해야 한다.

지금 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 주기에 지금은 OCP를 위반 중이다.

 

 

현재 클래스 다이어그램을 보면 OrderServiceImpl은 FixDiscoubtPolicy도 의존하고 있고 이것을 변경하려면 FixDiscountPolicy에서 RateDiscountPolicy로 의존관계를 변경하는 것이다.

우리는 이런 그림에서 OrderServiceImpl이 완전히 DiscountPolicy에만 의존할 수 있도록 변경해야 한다.

이렇게 만들기 위해서는 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

 

우리는 관심사를 분리해주어야 한다.

하나의 클래스는 하나의 임무만을 가져야하고 지금 OrderServiceImpl은 본인의 임무도 수행을 하며 DiscountPolicy에 해당하는 클래스도 찾고 있는 것이다.

이 프로그램의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 관리하는 책임을 가지는 별도의 설정 클래스를 만들어야 한다.

package hello.core;

import hello.core.Discount.FixDiscountPolicy;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {
    
    public CustomerService customerService(){
        return new CustomerServiceImpl(new MemoryCustomerRepository());
    }
    
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryCustomerRepository(), new FixDiscountPolicy());
    }
}

이렇게 AppConfig 객체를 만들고 각 클래스에 들어가서 생성자를 통해 private final 객체를 입력받도록 만든다.

package hello.core.customer;

public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }
}

그렇게 해서 만든 CustomerServiceImpl을 잠깐 보면 CustomerRepository만 의존을 하고 MemoryCustomerRepository에 대한 내용은 나오지 않는 것을 볼 수 있다.

이제 CustomerServiceImlp은 의존관계에 대한 고민은 하지 않고 맡은 임무에만 집중을 하면 된다.

 

이에 대한 내용은 OrderServiceImlp도 마찬가지이다.

이렇게 의존관계를 외부에서 주입해주는 것 같다고 해서 우리말로 의존성 주입(Dependency Injection)이라고 한다.

 

이제 AppConfig에 맞게 App 클래스들에서 코드를 수정해 주자.

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;

public class CustomerApp {

    public static void main(String[] args) {
        
        AppConfig appConfig = new AppConfig();
        
        CustomerService customerService = appConfig.customerService();

        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        customerService.join(customer);

        Customer findCustomer = customerService.findCustomer(1L);
        System.out.println("new customer = " + customer.getName());
        System.out.println("find customer = " + findCustomer.getName());
    }
}

 

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

    public static void main(String[] args) {
        
        AppConfig appConfig = new AppConfig();
        
        CustomerService customerService = appConfig.customerService();
        OrderService orderService = appConfig.orderService();
        

        Long id = 1L;
        Customer customer = new Customer(id, "seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);

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

import 된 코드들을 보면 interface들에만 의존하는 것을 볼 수 있다.

 

테스트 코드들도 AppConfig에 맞게 수정을 하고

package hello.core.customer;

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

public class CustomerServiceTest {

    CustomerService customerService;
    
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        customerService = appConfig.customerService();
    }

    @Test
    void join(){
        //given
        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        //when
        customerService.join(customer);
        Customer findCustomer = customerService.findCustomer(1L);

        //then
        Assertions.assertEquals(customer, findCustomer);
    }
}
package hello.core.order;

import hello.core.AppConfig;
import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    CustomerService customerService;
    OrderService orderService;

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

    @Test
    void createOrder(){
        Long id = 1L;
        Customer customer = new Customer(id, "Seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);
        Assertions.assertEquals(500, order.getDiscountPrice());
    }
}

테스트를 모두 실행해 보면

이렇게 문제없이 잘 수행되는 것을 볼 수 있다.

 

추가적으로 AppConfig에 가서 중복된 부분을 정리하도록 하자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }
    

    public OrderService orderService(){
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }
    
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }
    
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}

만약 여기서 할인 정책을 변경하려고 하면

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


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

    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

이렇게 AppConfig의 코드만 수정을 하면 된다.

 

좋은 객체 지향 설계는 SOLID를 준수한다고 했다.

여기서 우리는

SRP

현재 하나의 클래스는 단 하나의 책임만 가지고 있다.

DIP

의존성 주입을 사용했기에 interface에 의존을 하고 있다.

OCP

요소를 확장하더라도 AppConfig만 수정을 하고 클라이언트들의 코드는 변경할 필요가 없다.

 

기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.

하지만 AppConfig를 만든 이후에 구현 객체는 자신의 로직만을 실행하게 된다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간 것이다. OrderServiceImpl과 같은 클래스는 자신이 어떤 객체를 실행할지도 모른다.

이렇게 프로그램의 제어 흐름을 직업 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전 (Inversion of Control)이라고 한다.

 

애플리케이션 런타임 중에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.

의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

그리고 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 DI 컨테이너라고 한다.

 

이번에는 spring을 사용하여 DI를 적용해보자.

당연히 AppConfig 먼저이다.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
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 {

    @Bean
    public CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

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

이렇게 AppConfig에 @Configuration을 붙여주고 각 메서드에 @Bean을 붙여준다. 이렇게 하면 스프링 빈이 등록이 된다.

 

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomerApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();

        //CustomerService customerService = appConfig.customerService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        CustomerService customerService = applicationContext.getBean("customerService", CustomerService.class);

        Customer customer = new Customer(1L, "Seungkyu", Grade.VIP);

        customerService.join(customer);

        Customer findCustomer = customerService.findCustomer(1L);
        System.out.println("new customer = " + customer.getName());
        System.out.println("find customer = " + findCustomer.getName());
    }
}

그리고 기존에 AppConfig에서 꺼내오던 것을 ApplicationContext에서 꺼내오며, 각 의존관계들은 getBean을 이용해 받아온다.

 

OrderApp도

package hello.core;

import hello.core.customer.Customer;
import hello.core.customer.CustomerService;
import hello.core.customer.Grade;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();

        //CustomerService customerService = appConfig.customerService();
        //OrderService orderService = appConfig.orderService();

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


        Long id = 1L;
        Customer customer = new Customer(id, "seungkyu", Grade.VIP);
        customerService.join(customer);

        Order order = orderService.createOrder(id, "Latte", 10000);

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

이렇게 수정을 해준다.

그러면 전과 동일하게 동작하는 것을 볼 수 있다.

 

그리고 ApplicationContext를 스프링 컨테이너라고 한다.

저번에는 AppConfig를 직접 사용했지만, 이제부터는 스프링 컨테이너를 사용하면 된다.

 

스프링 컨테이너에서는 @Configuration이 붙은 클래스를 구성정보로 사용한다.

@Configuration이 붙은 클래스에서 @Bean이 붙은 메서드들을 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 한다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 8일차  (0) 2023.02.07
스프링 7일차  (0) 2023.02.07
스프링 5일차  (0) 2023.02.04
스프링 4일차  (0) 2023.02.03
스프링 3일차  (0) 2023.02.01

+ Recent posts