<Spring> 스프링 핵심원리 이해 3 - 스프링 기반 코드로 변경하기

AppConfig 리팩토링

  • 현재 AppConfig 클래스의 경우, 중복이 존재하고 역할에 따른 구현이 보이지 않는 구조이다.
  • 아래의 그림과 같이 명확히 구현을 나누어서 리팩토링을 해야한다.
기대하는 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());
} }
  • 리팩토링 후의 코드
package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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;

public class AppConfig {

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

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

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

    public DiscountPolicy discountPolicy() {
        return new FixedDiscountPolicy();
    }
}
  • 수정 후의 AppConfig 코드를 보면 역할과 구현이 한번에 나누어져서 애플리케이션의 전체 구성이 어떻게 되어있는지 빠르게 파악 할 수 있다.

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

  • 이제 정액할인 정책을 정률 할인 정책으로 바꿔야 한다.
  • 위 코드의 FixedDiscountPolicyRateDiscountPolicy로 바꾸면 된다.
  • 왜 이렇게 간단한 수정으로 할인 정책을 변경 할 수 있을까?
  • 우리는 사용영역(구현체 및 인터페이스)과 구성영역(AppConfig)을 철저하게 분리시켜 코드를 구현했기에 가능하다.
  • 구현(정책)을 변경하고 싶을때, 사용영역의 경우 단 한줄의 코드도 변경이 없어도 되며 오직 구성영역의 코드만 바꾸면 구현(정책)이 변경된다.

SRP,DIP,OCP의 적용

SRP - 단일 책임 원칙

  • 클라이언트 객체는 직저 구현 객체를 생성하고, 연결하고 실행하는 다양한 책임을 가지고 있다.
  • SRP 단일 책임 원칙을 따르면서 서로의 관심사(책임)을 분리시켰다.
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.(구성영역)
  • 클라이언트 객체는 오직 실행하는 책임만 담당한다.(사용영역)

DIP - 의존관계 역전 원칙

  • 의존성 주입은 해당 원칙을 따르는 방법 중 하나이다.
  • 모든 클라이언트 코드가 추상화 인터페이스에만 의존하도록 코드를 설정했다.
  • 하지만 클라이언트 코드는 인터페이스만으로는 애플리케이션을 실행할 수 없다.(NPE 발생)
  • AppConfig가 객체 인스턴스(구현체)를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입한다.
  • 해당 방식을 통해 DIP 원칙도 지키고, 어플리케이션 실행도 가능하게 한다.

OCP - 소프트웨어 요소는 확장에는 열려 있으나, 변경에은 닫혀 있어야 하는 원칙

  • 애플리케이션을 사용영역과 구성영역으로 나누었다.
  • AppConfig만을 변경함으로써 클라이언트 코드의 변경 없이 새로운 기능을 확장할 수 있다.(해당 예제에서는 정액할인정책에서 정률할인정책으로의 확장)
  • 소프트웨어요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.

IoC, DI 그리고 컨테이너

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

  • 기존의 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고,연결하고,실행했다. 한 마디로 구현객체가 스스로 프로그램의 제어 흐름을 제어했다.
  • 하지만 AppConfig를 구현하고 난 다음부터는, 클라이언트 구현객체는 오직 실행 역할만 담당하고 있다.즉, 프로그램의 제어 흐름에 대한 역할은 AppConfig가 가지고 있다.예를 들어 이제는 OrderServiceImpl 은 본인이 어떠한 구현체를 선택할지 더 이상 알 수 없다. 동시에 OrderServiceImpl 는 묵묵히 자신의 코드만 실행하는 역할을 하고 있다.
  • 이렇듯 프로그램의 제어 흐름을 해당 객체가 직접 제어하는것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.

DI(Dependency Injection) - 의존성 주입

  • OrderServiceImplDiscountPolicy 인터페이스에 의존한다.하지만 실제로 어떤 구현 객체가 스스로에게 사용될지 모른다.
  • 의존 관계는 정적인 클래스 의존관계실행 시점에 결정되는 동적인 객체 의존관계로 나누어서 접근해야한다.

정적인 클래스 의존관계

  • 클래스가 사용하는 import된 코드만 보고도 의존관계를 파악할 수 있는 경우다.
  • 즉, 애플리케이션을 실행하지 않아도 파악이 가능하다.
클래스 다이어그램

동적인 객체 인스턴스 의존관계

  • 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다.
객체 다이어그램
  • 애플리케이션 실행시점에 외부에서 실제 실행 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는것을 의존관계 주입이라고 한다.
  • 외부에서 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고,클라이언트가 호출하는 대상이 타입 인스턴스를 변경 할 수있다.
  • 또한 정적인 클래스 의존관계를 변경하지않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 할 수 있다.

컨테이너

  • AppConfig처럼 객체를 생성하고 의존관계를 연결해주는 것을 IoC컨테이너 또는 DI컨테이너라고 한다.
  • 의존관계 주입에 초점을 둬 최근에는 주로 DI컨테이너라고 부른다.
  • 혹은 어셈블러,오브젝트 팩토리 등으로도 불린다.

스프링으로 전환하기

AppConfig 스프링 기반으로 변경하기

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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 {
    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
  • @Configuration : AppConfig에 설정을 구성해주는 어노테이션
  • @Bean : 스프링 컨테이너에 스프링 빈으로 등록하는 어노테이션

MemberApp에 스프링 컨테이너 적용

  • MemberApp 클래스는 순수 자바코드로 테스트 구현한 클래스이다.
  • memberService가 잘 동작하는지 확인하는 동작을 수행한다.
  • 현재는 스프링을 적용해서 코드를 변경했다.
package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
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 = new AnnotationConfigApplicationContext(AppConfig.class);

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

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

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

OrderApp에 스프링 컨테이너 적용

  • 위와 마찬가지로 OrderService가 잘 동작하는 테스트하는 코드이다.
package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
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();
//        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);

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

        Order order = orderService.createOrder(1L, "gun", 5000);
        System.out.println("order = " + order);;

    }
}

스프링 컨테이너

  • 위 코드에서 ApplicationContext를 스프링 컨테이너라 한다.
  • 이전 코드는 개발자가 직접 AppConfig를 객체 생성 및 할당 후, DI를 진행했지만 변경된 코드는 스프링 컨테이너를 통해서 진행된다.
  • 스프링 컨테이너는 @Configuration 어노테이션이 붙은 AppConfig를 설정 정보로 사용한다. 또한 @Bean 어노테이션이 붙은 메서드를 모두 호출해서 반환된 객체들을 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체들을 스프링 빈이라고 한다.
  • 스프링 빈은 @Bean이 붙은 메서드명을 스프링 빈의 이름으로 사용한다.(memberService,orderService)
  • 스프링빈은 applicationContext.getBean()메서드를 이용해서 찾을 수 있다.
  • 기존에 개발자가 직접 자바코드로 모든 것을 진행했다면, 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.
해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.