의존관계 자동주입

다양한 의존관계 주입 방법

  • 의존관계 주입은 크게 4가지 방법이 있습니다.
  • 생성자 주입
    • 생성자를 통해서 의존관계를 주입 받는 방법입니다.
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장됩니다.
    • 불변, 필수 의존관계에 사용

  • 수정자 주입(setter 주입)
    • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법입니다.
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법입니다.
    • 선택, 변경 가능성이 있는 의존관계에 사용

  • 필드 주입
    • 이름 그대로 필드에 바로 주입하는 방법입니다.
    • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능하기 때문에 테스트하기 힘들다는 단점이 있습니다.
    • DI 프레임워크가 없으면 아무것도 할 수 없습니다.
    • 사용하지 않는 것이 좋음

    • 일반 메서드를 통해서 주입 받을 수 있습니다.
    • 한번에 여러 필드를 주입 받을 수 있습니다.
    • 일반적으로 잘 사용하지 않습니다.일반 메서드 주입

옵션 처리

  • 주입할 스프링 빈이 없어도 동작해야 할 때가 있는데, @Autowired만 사용하면 required옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생합니다.
  • @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않습니다.
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력됩니다.
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력됩니다.

생성자 주입을 선택해야 한다!

  • 과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI프레임워크 대부분이 생성자 주입을 권장합니다. 이유는 다음과 같음
  • 불변
    • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야한다.)
    • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두여아 한다.
    • 누군가 실수로 변경할 수 있고, 변경하면 안되는 메서드를 열어두는 것을 좋은 설계방법이 아님
    • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없기 때문에 불변하게 설계할 수 있다.
  • 누락
    • 수정자 주입을 사용했을 때(위 수정자 주입 코드 참고), 아래의 코드를 수행하면 실행은 되지만 결과는 Null Point Exception이 발생하게 된다. 이는 OrderServiceImple을 생성할 때 의존관계 주입이 누락되었기 때문입니다.
    • 하지만 생성자 주입을 사용하면 아래 코드처럼 의존관계 주입이 누락 되었을 때 컴파일 오류가 발생하게 됩니다.
    • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있는데 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다.
       
@Test void createOrder() { 
	OrderServiceImpl orderService = new OrderServiceImpl(); 
	orderService.createOrder(1L, "itemA", 10000); 
}

정리

  • 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 합니다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 됩니다.
  • 항상 생성자 주입을 선택! 가끔 옵션이 필요하다면 수정자 주입을 선택하지만 필드 주입은 사용하지 않는게 좋습니다.

롬복

  • 롬복 적용은 구글 참고
  • 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어주는 역할을 합니다. 그러므로 생성자를 만드는 코드를 작성하지 않아 짧고 간결한 코드작성이 가능합니다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

}

컴포넌트 스캔

컴포넌트 스캔과 의존관계 자동 주입

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링을 나열했지만 이렇게 등록하다보면 스프링 빈이 정말 많아지고, 설정정보도 커지면서 누락하는 문제도 발생하게 됩니다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공하면서 의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공합니다.

AutoAppConfig 생성하기

  • 기존에는 AppConfig에 스프링 빈을 등록했어야 했지만, AutoAppConfig에서는 @ComponentScan을 이용하여 클래스를 등록할 필요가 없습니다.

  • 컴포넌트 스캔을 사용하면 Configuration이 붙은 설정 정보가 자동으로 등록되기 때문에 이전에 만들었던 AppConfig 등 설정 정보가 함께 등록되게 되는데, 이를 방지하기 위하여 excludeFilters를 이용하여 스캔 대상에서 제외시켰습니다. 보통은 설정정보를 스캔 대상에서 제외하지 않음. 기존 예제 코드를 유지하기 위해서 사용함

@Component와 @Autowired

  • 스프링 빈으로 등록 할 각 클래스가 컴포넌트 스캔의 대상이 되도록 모든 클래스에 @Component 애노테이션을 붙여주면 됩니다.
  • 컴포넌트 스캔을 사용하여 자동으로 스프링 빈을 등록하게 되면 의존관계 주입도 해당 클래스 안에서 해결해야 하기 때문에 @Autowired를 사용하여 자동으로 주입해주면 됩니다. @Autowired로 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입해줍니다.

탐색 위치와 기본 스캔 대상

  • @ComponentScan(basePackages = "") 컴포넌트 스캔의 어노테이션 안에 basePackages(해당 패키지를 포함하여 하위 패키지 모두 탐색) 또는 basePackageClasses(지정한 클래스의 패키지를 탐색 시작 위치로 지정)를 사용하여 시작 위치를 지정할 수 있습니다.
  • 패키지의 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것을 권장합니다.
  • @Component, @Controller, @Service, @Repository, @Configuration는 컴포넌트 스캔의 기본 대상이 됩니다.

싱글톤 컨테이너

웹 어플리케이션과 싱글톤

  • 대부분의 스프링 애플리케이션은 웹 어플리케이션이고, 웹 어플리케이션은 보통 여러 고객이 동시에 요청을 합니다. 하지만 스프링이 없는 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성합니다. 이러한 방식은 메모리 낭비가 심해집니다. 이러한 문제를 해결하기 위하여 객체를 생성할 때 1개만 생성하고, 공유하도록 설계(싱글톤 패턴)하면 됩니다.

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디저인 패턴입니다.
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 합니다.(private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록)
  • 싱글톤 패턴을 구현하는 방법은 여러가지가 존재합니다.(예제는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택)
  • 싱글톤 패턴을 적용하면 이미 만들어진 객체를 공유하여 효율적으로 사용할 수 있지만 수많은 문제점들을 가지고 있습니다.
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어갑니다.
    • 의존관계상 클라이언트가 구체 클래스에 의존합니다. -> DIP 위반
    • 클라이언트가 구체클래스에 의존해서 OCP원칙을 위반할 가능성이 높습니다.
    • 테스트하기가 어렵습니다.
    • 내부 속성을 변경하거나 초기화 하기가 어렵습니다.
    • private 생성자로 자식 클래스를 만들기 어렵습니다.
    • 유연성이 떨어집니다.
    • 안티패턴으로 불리기도 합니다.
  • 문제점

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리합니다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 하고, 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 합니다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 장점을 해결하면서 객체를 싱글톤으로 유지할 수 있습니다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 됩니다.
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있습니다.

참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공함

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안됩니다.
  • 무상태로 설계해야 합니다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안됨
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨
    • 가급적 읽기만 가능해야됨
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 함
  • 스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 수 있습니다.

@Configuration과 싱글톤

  • 위 코드를 보면 memberRepository()는 총 세번 호출되어야 하는데, 결과는 한번만 호출하는 것으로 확인됩니다. 스프링에서 @Configuration을 적용한 AppConfig는 스프링 빈이 싱글톤이 되도록 보장해주기 때문에 세번이 아닌 한번이 호출되게 됩니다.
  • 스프링에선 @Configuration을 적용하면 스프링에선 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig클래스를 상속받은 임이의 다른 클래스를 만들고, 그 클래스를 스프링 빈으로 등록합니다. 그렇기 때문에 싱글톤이 보장됩니다. 아마도? 스프링 빈이 등록되어 있으면 스프링 빈을 반환하고, 아니면 생성하여 스프링 빈으로 등록하는 코드가 있을 것이라고 생각됩니다.
  • @Configuration을 적용하지 않고, @Bean만 적용했을 경우에는 스프링 빈으로는 등록되지만, 싱글톤을 보장하진 않습니다. 그러니 스프링 설정 정보는 항상 @Configuration을 사용하면됩니다.

스프링 빈

스프링 빈이란?

  • 스프링 컨테이너가 관리하는 자바 객체를 빈이라고합니다.

스프링 컨테이너 생성

  • ApplicationContext를 스프링 컨테이너라고 하고, 이는 인터페이스입니다.
  • new AnnotationConfigApplicationContext(AppConfig.class)는 ApplicationContext의 구현체입니다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 어노테이션 기반의 자바 설정 클래스로 만들 수 있습니다.

참고: 정확히는 스프링 컨테이너를 부를 때 BeanFactory, ApplicationContext로 구분해서 이야기하지만, BeanFactory를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext라고합니다.

스프링 빈 등록

  • 스프링 컨테이너를 생성할 때 AppConfig.class를 구성 정보로 지정함으로 스프링 컨테이너는 파라미터로 넘어온 AppConfig.class의 정보를 스프링 빈에 등록한다.

  • 빈 이름은 메서드 이름을 사용하고, 직접 부여할 수 도 있습니다. 하지만 빈 이름은 항상 다른 이름을 부여해야 합니다.

IoC, DI, 컨테이너

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했습니다.
  • 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다. 예를들어 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 알 수 없습니다.
  • 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고있다. 심지어 OrderServiceImpl도 AppConfig가 생성하고, AppConfig는 OrderServiceImple이 아닌 OrderService인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사림도 모른체 OrderServiceImpl은 묵묵히 자신의 로직을 실행합니다.
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다. 이를 통해 프로그램 제어의 책임이 개발자에서프레임워크로 위임되므로, 개밪라는 핵심 비즈니스 로직에 더 집중할 수 있는 장점이 있습니다.

의존관계 주입DI(Dependency Injection)

  • OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 알 수 없습니다.
  • 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야합니다.
  • 정적인 클래스 의존 관계
    • 클래스가 사용하는 import코드만 보고 의존관계를 쉽게 판단할 수 있고, 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석이 가능합니다. OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있지만 이러한 클래스 의존관계 만으로는 실제 어떤 구현 객체가 OrderServiceImpl에 주입될지 알 수 없습니다.

  • 동적인 객체 인스턴스 의존 관계
    • 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계입니다.

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

컨테이터

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 합니다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.

프레임워크 vs 라이브러리

  • 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크입니다.
  • 내가 작성한 코드가 직접 제어의 흐름을 담당한다면, 그것은 프레임워크가 아니라 라이브러리입니다.

객체 지향 설계와 스프링

스프링을 왜 만든건가?

자바 언어는 객체 지향 언어이며, 스프링은 자바 언어 기반의 프레임워크입니다. 이러한 스프링은 좋은 객체 지향 어플리케이션을 개발할 수 있도록 도와주기위해 탄생한 프레임워크 입니다.

스프링 부트란 ?

  • 스프링을 편리하게 사용할 수 있도록 지원하는 프레임워크입니다.
  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성해줍니다.
  • Tomcat같은 웹 서버를 내장하고있어, 별도의 웹 서버를 설치하지 않아도 됩니다.
  • 손쉬운 빌드 구성을 위한 starter 종속성을 제공합니다.
  • 스프링과 써드파티 라이브러리를 자동으로 구성해줍니다.
  • 메트릭, 상태 확인, 외부 구성 같은 프로덕션 준비 기능을 제공합니다.
  • 관례에 의한 간결한 설정이 가능합니다.

객체 지향의 특징

  • 추상화, 캡슐화, 상속, 다형성 4가지 특징을 가지고 있습니다.
  • 객체들의 모임으로 파악하고, 각각의 객체는 메세지를 주고받고 데이터 처리가 가능합니다.
  • 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에서 많이 사용됩니다.

다형성

다형성을 쉽게 설명하기 위해 세상을 역할구현으로 나누어보겠습니다.

  • 자동차라는 역할이 있다면, 자동차의 구현체로는 K3, 아반떼, 테슬라 모델3 등이 있습니다.
  • 로미오와 줄리엣이라는 역할이 있다면, 구현체로는 원빈, 장동건, 김태희, 송혜교 등이 있습니다.

이렇게 역할과 구현으로 분리하게 되면 세상이 단순하고, 유연해지며 변경이 용이해집니다.

장점

  • 클라이언트는 대상의 역할만 알면 된다.
  • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
  • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.

자바 언어에서의 역할과 구현을 분리

  • 자바 언어의 다형성을 활용(역할 = 인터페이스, 구현 = 클래스, 구현객체)
  • 객체를 설계할 때 역할과 구현을 명확하게 분리합니다.
  • 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만듭니다.

자바 언어의 다형성

자바 기본 문법인 오버라이딩을 떠올려보면, 실제로 프로그램이 실행될 때는 구현체에 맞게 오버라이딩 된 메서드가 실행됩니다. 해당 메서드는 인터페이스를 구현한 것으로 실행 시점에 유연하게 변경할 수 있습니다. 물론 클래스 상속 관계도 다형성, 오버라이딩이 적용 가능합니다.

다형성의 본질은 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다는 점이고, 이를 이해하려면 협력이라는 객체 사이의 관계에서 시작합니다.즉, 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다는 점 입니다.

 

로버트 마틴의 좋은 객체 지향 설계의 5가지 원칙(SOLID)

  • SRP(Single Responsibility Principle) 단일 책임의 원칙
    • 한 클래스는 하나의 책임만 가진다. 즉, 변경이 있을 때 파급효과가 적으면 이를 잘 따른것입니다.
  • OCP(Open/Closed Principle) 개방-폐쇄의 원칙
    • 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다.
  • LSP(Liskov Substitution Principle) 리스코프 치환 원칙
    • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으며 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • ISP(Interface Segregation Principle) 인터페이스 분리 원칙
    • 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
  • DIP(Dependency Inversion Principle) 의존관계 역전 원칙
    • 추상화에 의존해야지 구현체에 의존하면 안된다. 즉, 인터페이스에 의존하라는 뜻이다.

+ Recent posts