[Spring] Spring DI 와 IOC 컨테이너

Loopy·2023년 2월 7일
0

스프링

목록 보기
14/16
post-thumbnail

☁️ Spring vs Spring Boot

Spring Framework

🔖 프레임워크 vs 라이브러리

  • 프레임워크(FrameWork) : 프로그램 개발을 위한 여러 요소와 메뉴얼(규약)을 제공하는 프로그램이며, 코드를 제어하고 대신 실행함(ex) JUnit)
  • 라이브러리(Library) : 개발 시 선택적으로 사용 가능한 기능을 제공하는 도구들이며, 작성 코드가 직접 제어의 흐름을 담당

스프링은 자바 기반 프레임워크인 만큼, 순수한 객체 지향 언어의 강점을 살리는 프레임워크이다. 즉, 나머지는 객체 지향을 위한 도구들일 뿐이다.

핵심 기술로는 스프링 DI 컨테이너, AOP, IOC(의존성 역전) 등이 존재한다.
DI, DI 컨테이너 기술로 다형성 + OCP,DIP를 지원함으로써, 클라이언트 코드의 변경 없이 기능 확장이 가능해졌다.

Spring Boot

  1. 톰캣 같은 웹서버(WAS)를 내장하고 있기 때문에 단독으로 실행 가능한 스프링 애플리케이션 jar 파일로 손쉽게 생성 가능하다.
    스프링은 war 파일을 WAS에 별도로 담아 배포해야 한다.

  2. 손쉬운 빌드 구성을 위한 starter 의 종속성을 제공한다.
    스프링의 경우 dependency를 설정해줄 때 설정 파일이 매우 길고, 모든 dependency에 대해 버전 관리도 직접 해줘야 한다.

    <dependency>  // 스프링
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
        <version>5.3.5</version>
    </dependency>
    implementation 'org.springframework.boot:spring-boot-starter-web'  // 스프링 부트
  3. application.yml 를 통해 설정 파일이 간단해진다.
    스프링의 경우 configuration 설정을 할 때도 매우 길고, 모든 어노테이션 및 빈 등록 등을 설정해줘야 한다. 스프링 부트는 @SpringBootApplication 이 있다면 라이브러리를 스캔해서 필요한 모든 클래스들을 빈으로 등록해준다.

🔖 JAR VS WAR
JAR vs WAR 배포의 차이
[Spring Boot]배포 방법 비교 (JAR vs WAR)

☁️ OOP(객체 지향 프로그래밍)

객체 지향 프로그래밍의 특징 중 다형성에 대해 알아보자.

다형성 이란, 객체 설계 시 역할(인터페이스)과 구현(역할 수행 객체)으로 분리하는 것을 의미한다. 다형성을 지키면 설계의 유연성과 변경의 편리함을 느낄 수 있다.

즉, 아래 코드와 같이 클라이언트는 대상의 인터페이스에만 의존하여, 내부 구조와 구현 대상의 변경에 영향이 없는 것을 의미한다.

자바 언어는 이러한 다형성을, 오버라이딩을 통해 구현하고 있다.

Overriding

   public class MemberService {
      private MemberRepository memberRepository = new MemoryMemberRepository();
      private MemberRepository memberRepository = new JdbcMemberRepository();
   }

MemberRepository의 자식 클래스들은 부모 타입으로 모두 할당이 가능하다. 이렇듯,
인터페이스를 구현한 객체를 실행 시점에 유연하게 변경할 수 있다는 장점이 존재한다. 즉 클라이언트 변경하지 않고, 서버의 구현 기능을 유연하게 변경 가능한 것이 다형성의 본질이다.

스프링은 다형성을 극대화해서 사용할 수 있도록 도와주는데, 예를 들어IOCDI는 다형성을 쉽게 사용하도록 지원하는 기능이다.

☁️ SOLID 원칙

SRP(단일 책임 원칙)

한 클래스는 하나의 책임(변경 이유)만을 가져야한다. 만약 변경이 있을 때 그 파급 효과가 적다면 단일 책임 원칙을 잘 지켰다고 볼 수 있다.

OCP(개방-폐쇄 원칙)

확장에는 열려있으나, 기존 코드의 변경에는 닫혀 있어야 한다는 원칙이다.
다형성의 역할과 구현의 분리를 활용하면, 인터페이스를 구현한 새로운 클래스를 만드는 것은 기존 코드를 변경하는 것이 아니기 때문에 개방-폐쇄 원칙을 지킬 수 있다.

하지만 위에서의 MemberRepository는 구현 객체를 변경하려면 클라이언트 코드를 변경해야 하는데, 다형성을 지켰지만 OCP는 지키지 못하는 상황이 발생했다.

이렇게 객체를 생성하고, 연결관계를 맺어주는 역할 즉 설정자를 스프링 컨테이터가 해준다.

LSP(리스코프 치환 원칙)

객체는 프로그램의 정합성을 깨트리지 않으면서 언제나 하위 타입의 인스터스로 변경 가능해야 한다는 원칙이다. 즉, 하위 클래스는 상위 인터페이스의 규약을 다 지켜야 한다.

ISP(인터페이스 분리 원칙)

인터페이스의 단일 책임 원칙으로, 특정 클라이언트 위한 여러개의 인터페이스로 분리해야 한다. 인터페이스가 하나의 역할만 하도록 분리하게 되면 클라이언트도 그에 맞추어서 분리되기 때문에, 결국은 서로의 변경이 다른 인터페이스에 영향을 주지 않게 된다. 또한 인터페이스가 명확해지고, 대체 가능성이 높아진다.

DIP(의존관계 역전 원칙)

프로그래머는 추상화에 의존해야 하지, 구체화에 의존하면 안된다는 원칙이다. 스프링의 의존성 주입(DI)은 이 원칙을 따르는 것으로, 쉽게 말하면 클라이언트 코드는 구체 클레스가 아닌 인터페이스에만 의존해야 변경의 용이성이 높다는 것을 말한다.

🔖 주의사항
객체 지향의 핵심은 다형성이지만, 다형성만으로는 OCPDIP 를 지킬 수 없다. 예를 들어, 아래의 코드는 인터페이스와 구현 클래스에 동시에 의존하고 있기 때문에 DIP를 위반하고 있으며 역시 구현 객체가 변경될 때 클라이언트 코드까지 변경되므로 OCP 를 위반하게 된다.

MemberRepository m = new JdbcMemberRepository() 

스프링은 다형성만으로 부품을 갈아끼우듯이 유연하게 개발할 수 없는 문제를, 관심사의 분리를 통해 해결해준다. 클라이언트의 코드 변경 없이 기능을 확장할 수 있어지는 것이다.

관심사의 분리 및 DI 컨테이너

☁️ 기존 방식의 분제점

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

하지만, 새로운 기능으로 변경하려면 클라이언트 코드가 변경되는 문제가 발생한다. 현재 역할과 책임을 명확히 분리하고, 다형성 또한 지키고 있다.

  1. OrderServiceImpl -> FixDiscountPolicy 구체 클래스에 의존하여 DIP 위반

  2. 구체 클래스 의존 관계로 인해 RateDiscountPolicy로 바꾸는 순간 OrderServiceImpl 코드를 변경해야 하기 때문에 OCP 위반

해결 방법

OrderServiceImpl이 인터페이스(추상)에만 의존하도록 변경하면, 문제가 해결된다.

   public class OrderServiceImpl implements OrderService {
      private DiscountPolicy discountPolicy;
}

그리고 위와 같이 인터페이스만으로는 실행이 안되니 구현체를 실행하려면, 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입하는 무엇인가 필요하게 되는데 이 작업을 스프링이 관심사의 분리를 통해 대신 해준다.

☁️ 관심사의 분리

객체를 생성 및 연결하는 역할(구성)실행하는 역할(사용)을 분리하는 작업을 의미한다. 어플리케이션의 전체 동작 방식을 구성하는 어떠한 설정파일을 별도로 만든다면, 객체들은 각 역할에만 집중할 수 있을 것이다.

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

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

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

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

🔖 AppConfig 역할
1. 애플리케이션 실제 동작에 필요한 구현 객체 생성
2. 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(DI)

즉, 생성자를 통해 어떤 구현 객체가 주입 될지는 오직 외부에서 결정이 되기 때문에 의존 관계에 대한 고민들은 외부에 맡기고 오직 실행에만 집중할 수 있게 되는 것이다. 또한, 이후 어떠한 변경 사유에 대해서도 AppConfig 파일인 구성영역만 변경하면 되기 때문에 클라이언트 코드에 변경이 필요 없다.

public class AppConfig {
   public DiscountPolicy discountPolicy(){
       //return new FixDiscountPolicy();
       return new RateDiscountPolicy();  // 변경 시 구성파일만 수정하면 됌
   } 
 }

AppConfig 도입과 객체 지향의 5가지 원칙을 연관지어서 보면 다음과 같다.

  1. SRP : 실행하는 책임만 가지고, 구현 객체를 생성하고 연결하는 역할은 구성 파일이 담당하도록 변경하였다.
  2. DIP : 추상화 인터페이스만 의존하도록 변경한 이후 구성 파일이 객체를 대신 생성해서 의존 관계를 주입해주었다.
  3. OCP : 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경을 닫도록 변경하였다. 다형성을 적용하고 DIP를 지킨다면 OCP까지 지킬 수 있는 자격을 얻게 된다.

그렇다면, 이전까지는 순수 자바 코드였고 이제 본격적으로 스프링으로 들어가보자.

☁️ IoC(Inversion of Control)

이전 방식

기존 프로그램은 구현 객체가 직접 필요한 객체들을 생성, 연결 및 실행하였다.

의존관계 역전

프로그램의 제어 흐름을 직접이 아닌 외부에서 관리하는것, 즉 제어가 역전되는것을 의미한다. 즉 Appconfig와 같이 외부에서 제어 흐름을 결정하고, 구현 객체는 실행의 역할만 담당하게 되는 것이다.

☁️ DI(Dependency Injection)

Dependency Injection 이란, 외부에서 객체들의 의존 관계를 주입하는 것을 의미한다. 참고로 의존관계는 정적 클래스 의존관계 + 실행 시점에 결정되는 동적 객체 의존 관계 두개로 나눠진다.

정적인 클래스 의존관계

실행하지 않아도 코드 분석(import)를 통해 클래스 간 의존관계를 분석 가능한 관계를 의미한다. 클래스 다이어그램을 생성할 수 있지만, 실제 어떤 객체가 주입될지는 분석이 불가능하다.

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

실행 시점(런타임)에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계이다. 이렇게 어플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 참조를 전달해 연관 관계를 맺는 방식을 의존 관계 주입이라 한다.

의존관계 주입을 통해 클라이언트의 호출 대상의 인스턴스를 변경 가능하고, 정적 클래스 의존관계의 변경 없이 동적 객체 의존관계만 변경 가능해진다.

☁️ DI(IoC) Container와 스프링 컨테이너

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

스프링 도입

스프링 컨테이너가 바로 DI 컨테이너로써, 빈 관리와 조회를 담당하는 BeanFactory 최상위 인터페이스의 부가 인터페이스(기능)들을 상속받아 구현하고 있다.

@Configuration
public class AppConfig {
   @Bean
   public MemberService memberService(){
       return new MemberServiceImpl(memberRepository());
   }
} 
  • Configuration : 해당 어노테이션이 붙은 AppConfig을 구성 파일로 설정한다.
  • @Bean : 해당 어노테이션이 붙은 메서드를 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 메서드명이 스프링 빈의 이름이 된다.
 public class MemberApp {
    public static void main(String[] args) {        
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        // 컨테이너에서 직접 필요한 스프링 빈을 찾아서 사용하도록 변경
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class); 
 }
  • ApplicationContext : 스프링 컨테이너로써, AppConfig의 설정(구성)정보를 가지고 @Bean이 적힌 메소드들을 호출에 반환된 객체를 컨테이너에 등록(스프링 빈)한다.

참고 자료
[Spring] Spring VS Spring Boot 차이점

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글