본문 바로가기
Framework/Spring

[Spring] 스프링 핵심 원리 (기본) - 2. 설계와 개발, 객체 지향 원리의 적용

by pilgyeong 2023. 2. 2.

2. 스프링 핵심 원리 - 예제


2.1 주문 도메인 설계

  • 주문 도메인의 협력, 역할, 책임
  • 주문 도메인 전체
  • 주문 클래스 다이어그램
  • 주문 객체 다이어그램



2.2 주문 도메인 개발

public class OrderServiceImpl implements OrderService {
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//  private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

위 코드는 OCP와 DIP 원칙을 동시에 어긴다. 기능을 확장하기 위해 클라이언트 코드에 영향을 주기 때문에 OCP를 위반하고, 주문 서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스를 의존하는 것과 동시에 구현 클래스인 FixDiscountPolicyRateDiscountPolicy에도 의존하고 있기 때문에 DIP를 위반한다.

따라서, 위 코드를 다음과 같이 인터페이스에만 의존하도록 설계해야 한다.

public class OrderServiceImpl implements OrderService {
    private final DiscountPolicy discountPolicy;
}

단, 이때 구현체가 없기 때문에 null pointer exception이 발생하여 동작하지 않을 것이다. 따라서, 클라이언트(OrderServiceImpl)에 DiscountPolicy의 구현 객체를 대신 생성해주고 주입해줘야 한다.



2.3 AppConfig 등.장.

애플리케이션의 전체 동작 방식을 구성하기 위해 구현 객체를 생성하고, 연결하는 책임을 갖는 별도의 구성정보 설정 클래스이다. 이것을 클라이언트(OrderServiceImpl)에 구현하지 않는 이유는 SRP 원칙을 지키기 위해서이다. OrderServiceImplDiscountPolicy의 구현 객체를 가지고 주문 로직을 수행하는 역할을 수행해야 한다. 여기에 각 인터페이스에 어떤 구현 객체가 들어가야 하는지(또 다른 책임)를 추가한다면, 클라이언트는 복잡해진다. 따라서, 각 책임을 확실히 분리하기 위해 AppConfig를 별도로 생성하는 것이다.

// AppConfig.class
public class AppConfig {
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
// OrderService.class
public class AppConfig {
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
// OrderApp.class
public class OrderApp {

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

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP
        Order order = orderService.createOrder(memberId, "itemA", 20000);

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

이렇게 코드를 짜면, OrderServiceImpl은 더이상 구현 클래스에 의존하지 않는다.
OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다. 오직, 외부(AppConfig)에서 결정한다.

별도의 설정 클래스 AppConfig를 사용함으로써, OCP와 DIP 원칙을 지킬 수 있고, 기존에 계획한 설계를 만들었다. 비즈니스 로직상 DiscountPolicy 인터페이스의 구현 객체로 다른 클래스가 추가되더라도, 구성 영역인 AppConfig에서 수정하면 사용 영역에서 코드 수정없이 확장할 수 있게 된다.



2.4 의존 관계 주입

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

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

 

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

OrderServiceImplDiscountPolicy인터페이스에만 의존한다. 실제 어떤 구현 객체가 사용될지 모른다. 이런 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계로 분리해서 봐야한다.

  • 정적인 클래스 의존 관계
    클래스가 사용하는 import 코드만 보고 의존 관계를 쉽게 판단할 수 있다. 정적인 의존 관게는 애플리케이션을 실행하지 않아도 분석할 수 있다. OrderServiceImplMemberRepositoryDiscountPolicy에 의존함을 알 수 있듯이 쉽게 분석 가능하다.
  •  
  • 동적인 객체(인스턴스) 의존 관계
    애플리케이션 실행 시점에 실제 생성된 인스턴스의 참조가 연결된 의존관계이다.

참고. DI 개념 정리

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

 

2.4.3 DI 컨테이너

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

 

2.5 스프링으로의 변환

@Configuration
public class AppConfig {
    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
public class OrderApp {

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

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

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

        Long memberId = 1L;
        Order order = orderService.createOrder(memberId, "itemA", 20000);

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





요약.

  • ApplicationContext를 '스프링 컨테이너'라고 한다.
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제는 스프링 컨테이너를 사용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다. 이때, @Bean이 적힌 메소드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이런 방식으로 스프링 컨테이너에 등록된 객체를 '스프링 빈'이라고 한다.
  • 기존에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제는 스프링 컨테이너를 통해 필요한 스프링 빈(객체)를 찾는다.
  • 기존에는 개발자가 직접 자바 코드로 모든 것을 했다면, 이제는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아 사용하도록 변경됐다.