IoC
IoC(Inversion of Control, 제어의 역전)의 개념
- 객체의 생성에서부터 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미, 또는 제어 권한(관리 권한)을 자신이 아닌 다른 대상에게 위임하는 것이다. => 제어권을 누가 가져갔을까? Framework Container(Spring에서는 IoC 컨테이너)
- 이 방식은 대부분의 프레임워크에서 사용하는 방법으로, 개발자는 필요한 부분을 개발해서 끼워 넣기의 형태로 개발하고 실행하게 된다. 프레임워크가 이러한 구조를 가지기 때문에 개발자는 프레임워크에 필요한 부품을 개발하고 조립하는 방식의 개발을 하게 된다.
- 이렇게 조립된 코드의 최종 호출은 개발자에 의해서 제어되는 것이 아니라 프레임워크의 내부에서 결정된 대로 이뤄지게 되는데, 이러한 현상을 "제어의 역전"이라고 표현한다.
라이브러리와 프레임워크의 차이
- IoC의 개념이 적용되었나의 차이
- 라이브러리를 사용하는 애플리케이션 코드는 애플리케이션 흐름을 직접 제어한다. 단지 동작하는 중에 필요한 기능이 있을 때 능동적으로 라이브러리를 시용할 뿐이다.
- 반면에 프레임워크는 거꾸로 애플리케이션 코드가 프레임워크에 의해 사용된다. 프레임워크는 내가 작성한 코드를 제어하고, 대신 실행해준다.
- ex) JUnit =>
assertEquals(a, b)는 객체 A와 B가 같은 값을 가지는지 확인하는 함수이다. 테스트를 하고 싶은 함수 위에 @Test 애노테이션만 작성하고 그 안에 assertEquals를 작성하면 실행은 JUnit(자바 프로그래밍 언어용 유닛 테스트 프레임워크)이 알아서 해준다.
- ex) JUnit =>
IoC(또는 Spring) 컨테이너
- Spring 프레임워크도 객체에 대한 생성 및 생명주기를 관리할 수 있는 기능을 제공하고 있다. => IoC 컨테이너 기능
- IoC 컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
- POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
- 개발자들이 직접 POJO를 생성할 수 있지만 컨테이너에게 맡긴다.
- POJO란?
- Plain Old Java Object - 평범하고 오래된 자바 객체
- 순수한 자바 객체를 의미
- 특정 기술에 종속되지 않은 순수한 JAVA Object
- POJO 가 아닌 예시) HttpServlet 을 상속 받음으로써 특정 기술에 종속되게 되므로 POJO 가 아니다.
-
public HelloServlet extens HttpServlet {...}
- POJO란?
- 스프링에서는 IoC 컨테이너를 빈 팩토리, DI 컨테이너, 애플리케이션 컨텍스트(또는 Spring 컨테이너)라고 부른다.
- 오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 보면, 컨테이너를 빈 팩토리 또는 DI 컨테이너라고 부른다. (빈 팩토리는 스프링 컨테이너 최상위 인터페이스이다. => 애플리케이션 컨텍스트(스프링 컨테이너)는 빈 팩토리를 다중 상속한다.)
- 그러나 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하는데, DI를 위한 빈 팩토리에 여러 가지 기능을 추가한 것을 애플리케이션 컨텍스트(또는 Spring 컨테이너)라고 한다.
- 정리하자면, 애플리케이션 컨텍스트(Spring 컨테이너)는 그 자체로 IoC와 DI 그 이상의 기능을 가졌다고 보면 된다.
애플리케이션 컨텍스트
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory,
HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher,
ResourcePatternResolver {
- 애플리케이션 컨텍스트는 빈 팩토리 기능을 모두 상속 받아서 제공한다.
- 위의 인터페이스에서 extends한 인터페이스들은 모두 빈 팩토리 인터페이스의 서브 인터페이스이며, 빈 팩토리에게 없는 추가 기능을 가지고 있다.(메세지 소스를 활용한 국제화 기능 - 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력, 파일과 외부 등에서 리소스를 편리하게 조회하는 기능 등)
IoC 분류
IoC는 DL과 DI로 분류할 수 있다. Spring은 DL, DI 방법을 둘 다 지원한다.
- DL(Dependency Lookup, 의존성 검색)
- 저장소에 저장되어 있는 Bean에 접근하기 위해 컨테이너가 제공하는 API를 이용하여 Bean을 Lookup 하는 것이다.
- DL은 컨테이너가 제공하는 API를 사용하기 때문에 컨테이너 종속성이 증가하여, 주로 DI를 사용한다. (컨테이너 API의 이름, 타입 등이 바뀌면 이를 사용하고 있는 클래스의 코드도 다 바뀌어야 한다.)
- DI(Dependency Injection, 의존성 주입)
- 각 클래스간의 의존관계를 빈 설정(Bean Definition) 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것이다.
DI
싱글톤 패턴의 단점
DI는 싱글톤 패턴의 단점을 보완한다. 먼저 싱글톤 패턴에 대해 알아보자.
- 싱글톤 패턴(singleton pattern)은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다. 보통 데이터베이스 연결 모듈에 많이 사용된다. 하나의 인스턴스를 만들어 놓고 해당 인스턴스를 다른 모듈들이 공유하며 사용하기 때문에 인스턴스를 생성할 때 드는 비용이 줄어드는 장점이 있다. 하지만 의존성이 높아진다는 단점이 있다.
- 싱글톤 패턴은 TDD(Test Driven Development)를 할 때 걸림돌이 된다.
- TDD를 할 때 단위 테스트를 주로 하는데, 단위 테스트는 독립적이어야 하며 테스트를 어떤 순서로든 실행할 수 있어야 한다.
- 싱글톤 패턴은 미리 생성된 하나의 인스턴스를 기반으로 구현하는 패턴이므로 각 테스트마다 '독립적인' 인스턴스를 만들기가 어렵다.
- 싱글톤 패턴은 사용하기가 쉽고 굉장히 실용적이지만 모듈 간의 결합을 강하게 만들 수 있다는 단점이 있다.
- 이때 DI를 통해 모듈 간의 결합을 조금 더 느슨하게 만들어 해결할 수 있다.
DIP(Dependency Inversion Principle, 의존 역전 원칙)
의존성 주입은 DIP를 지키며 만들어야 하며 DIP는 SOLID 원칙 중 하나로 원칙은 다음과 같다.
첫째, 상위 모듈은 하위 모듈에 의존해서는 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다.
둘째, 추상화는 세부 사항에 의존해서는 안된다. 세부사항이 추상화에 의존해야 한다.
DIP는 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것에 의존하기 보다는, 변화하기 어려운것, 거의 변화가 없는 것에 의존하라는 원칙이다.
DIP 예제
현재는 겨울이기 때문에 스노우 타이어를 구매하여 자동차에 끼도록 설계하였다. 즉, 상위 모듈인 자동차가 하위 모듈인 스노우 타이어에 의존하는 상태이다.
하지만, 날씨가 따뜻해지면서 더 이상 스노우 타이어를 사용할 필요가 없어졌다. 그래서 일반 타이어로 교체하기로 결정하였지만 단순히 스노우 타이어를 일반 타이어로 바꾼다고 코드가 끝나는 것이 아니다. 이것에 의존하고 있던 자동차의 코드도 연쇄적으로 영향을 끼치게 된다.
이것은 OCP(개방-폐쇄 원칙)을 위반하는 것이므로 추상화나 다형성을 통해 문제를 고쳐야 한다. 의존 역전 원칙은 그 중에서도 추상화를 이용한다. 바로, 스노우 타이어나 일반 타이어를 '타이어' 자체로 추상화하는 것이다.
여기서 타이어는 하위 모듈보다는 상위 모듈인 자동차 입장에서 만들어지는데, 이것은 상위 모듈이 하위 모듈에 의존했던 상황이 역전되어 하위 모듈이 상위 모듈에 의존하게 된다는 것을 의미한다. 이런 맥락에서 이 원칙의 이름이 의존 역전 원칙인 것이다.
Bean Definition(스프링 빈 설정 메타 정보)
DI를 알아보기 전에 핵심 개념 Bean에 대해 알아보자.
Bean이란
더보기Bean 개념
- 컨테이너 안에 들어있는 객체
- 컨테이너에 담겨 있으며, 필요할 때 컨테이너에서 가져와서 사용한다.
Bean Life Cycle(생명주기)
- 객체 생성 => 의존 설정 => 초기화 => 사용 => 소멸
- 스프링 컨테이너에 의해 생명주기가 관리된다.
- 스프링 컨테이너 초기화 시 빈 객체 생성, 의존 객체 주입 및 초기화
- 스프링 컨테이너 종료 시 빈 객체 소멸
Bean Definition(스프링 빈 설정 메타 정보)
더보기설정 메타 정보는 빈을 어떻게 만들고 어떻게 동작하게 할 것인가에 관한 정보이다. Spring 컨테이너는 자바 코드, XML, Groovy 등 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계되어 있다.
* Groovy: JVM 위에서 동작하는 동적 타입 프로그래밍 언어, 하지만 대부분 Gradle Script를 작성하기 위해 사용
다음과 같이 MyService와 MyRepository 클래스를 정의한다고 했을 때 Bean 설정 방법에 대해 살펴보자.
public class MyService {
private final MyRepository myRepository;
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
}
public class MyRepository {
}
1. annotation 기반 자바 코드 설정
- @Configuration: 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야 한다.
- @Bean: 클래스를 빈으로 등록할 때 사용한다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
}
2. XML 기반의 스프링 빈 설정
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://
www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
</bean>
</beans>
- <bean> 엘리먼트 : 등록할 Bean 정보를 명시한다.
- <bean>의 id 속성 : Bean의 이름을 대문자로 시작으로 명시한다.
- <bean>의 class 속성 : Bean 등록을 원하는 클래스의 위치를 명시한다.
- <property> 엘리먼트 : Bean에 의존성 주입에 대한 정보를 명시한다.
- <property>의 name 속성 : 클래스에 정의한 의존성 주입 받을 변수명을 명시한다.
- <property>의 ref 속성 : 의존성 주입 받을 Bean의 이름을 소문자로 시작으로 명시한다.
- XML 기반으로 설정하는 것은 최근에 잘 사용하지 않는다.
스프링은 어떻게 이런 다양한 형식을 지원하는 것일까? 그 중심에는 BeanDefinition이라는 추상화가 있다.
- 쉽게 말하자면, XML을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만든다. 따라서 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 되고 오직 BeanDefinition만 알면 된다.
- BeanDefinition을 빈 설정 메타 정보라 하는데, @Bean과 <bean> 당 각각 하나씩 메타 정보가 생성된다.
- AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성한다.
- GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성한다.
- 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 된다.
Bean 설정 파일이 아닌 컴포넌트 스캔을 통해 등록하는 방법: 컴포넌트 스캔과 자동 의존관계 설정
* 컴포넌트 스캔: @Component를 가진 모든 대상을 가져와서 빈에 등록하기 위해 찾는 과정(빈 설정파일 + @Bean을 통해 빈을 하나하나 지정할 필요가 없음)
- @Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
- @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다.
=> @Controller @Service @Repository
Dependency(의존 관계)
- Dependency Injection, 의존성 주입에서 Dependency(의존성, 의존 관계)란?
더보기Dependency(의존 관계) 개념
- "A가 B를 의존한다" => 의존 대상 B가 변하면, 그것이 A에 영향을 미친다. 즉, B의 기능이 추가되거나 변경되면 그 영향이 A에 미치는 것이다.
Dependency 위험한 이유
- 하나의 모듈이 바뀌면 의존한 다른 모듈까지 변경되야 한다.
- 단위 테스트의 목적 자체가 다른 모듈로부터 독립적으로 테스트하는 것을 요구하기 때문에 테스트 가능한 어플을 만들 때 의존성이 있으면 단위 테스트 작성이 어렵다.
class BurgerChef {
private HamBurgerRecipe hamBurgerRecipe;
public BurgerChef() {
hamBurgerRecipe = new HamBurgerRecipe();
}
}
- 위 코드의 경우, 햄버거 레시피가 변화게 되었을 때(예를 들어 HamBurger => CheeseBurger), 변화된 레시피에 따라서 BurgerChef 클래스를 수정해야 한다. 레시피의 변화가 요리사의 행위에 영향을 미치기 때문에 요리사는 레시피에 의존한다고 말할 수 있다.
Dependency를 인터페이스로 추상화
위 예제를 보면, BurgerChef는 HamburgerRecipe만 의존할 수 있는 구조로 되어 있다. 더 다양한 햄버거 레시피를 의존할 수 있게 구현하려면 인터페이스로 추상화해야 한다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef() {
burgerRecipe = new HamBurgerRecipe();
//burgerRecipe = new CheeseBurgerRecipe();
//burgerRecipe = new ChickenBurgerRecipe();
}
}
interface BugerRecipe {
newBurger();
}
class HamBurgerRecipe implements BurgerRecipe {
public Burger newBurger() {
return new HamBerger();
}
}
위 코드에서 볼 수 있듯이, 다양한 버거 레시피에 의존할 수 있는 BurgerChef가 되었다. 이처럼 의존 관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해지며 결합도가 낮아진다.
DI개념
Spring Framework에서 지원하는 IoC의 형태 중 DI(Dependency Injection, 의존 관계 주입)에 대해 더 자세히 알아보자.
(IoC는 프로그램 제어권을 역전시키는 개념이고, DI는 해당 개념을 구현하기 위해 사용하는 디자인 패턴 중 하나이다.)
지금까지의 구현에서는 BurgerChef 내부적으로 의존 관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다. 이때 DI는 어떤 햄버거 레시피를 만들 지는 버거 가게 사장님이 정하는 상황이라 할 수 있다. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부(사장님)에서 결정하고 주입하는 것이다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef(BurgerRecipe bugerRecipe) {
this.burgerRecipe = bugerRecipe;
}
}
//의존관계를 외부에서 주입 -> DI
new BurgerChef(new HamBurgerRecipe());
new BurgerChef(new CheeseBurgerRecipe());
new BurgerChef(new ChickenBurgerRecipe());
- 스프링에서는 외부의 대상이 IoC 컨테이너가 되어, 빈을 알아서 주입해 준다.
- 즉, 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자(Dependency Injector)가 이 부분을 가로채 메인 모듈이 간접적으로 의존성을 주입하는 방식이다.
- 이를 통해 메인 모듈(상위 모듈)은 하위 모듈에 대한 의존성이 떨어지게 되고 이를 '디커플링 된다'고도 한다.
- 정리하면 Spring에서 DI는 각 클래스간의 의존관계를 빈 설정(Bean Definition) 정보를 바탕으로 컨테이너(의존성 주입자)가 자동으로 연결해주는 것을 말한다. => 빈 설정 정보는 개발자가 XML, annotation 등으로 설정한다.
- 개발자들은 제어를 담당할 필요없이 빈 설정 파일에서 의존관계가 필요하다는 정보를 추가하면 된다.
- 객체 레퍼런스를 컨테이너로부터 주입 받아서, 실행 시에 동적으로 의존관계가 생성된다. (* 객체 레퍼런스: 객체를 가리키는 변수)
- 컨테이너가 실행 흐름의 주체가 되어 애플리케이션 코드에 의존관계를 주입해주는 것이다.
DI 장점
- 코드가 단순해지고, 컴포넌트 간의 결합을 느슨하게 만들 수 있다.
- DI가 등장한 이후 구현 객체는 자신의 로직만 실행하는 역할을 담당한다.(SRP 준수)
- 클래스를 재사용 할 가능성을 높이고, 다른 클래스와 독립적으로 클래스를 테스트 할 수 있다.
- 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 마이그레이션 하기도 수월하다.
- 마이그레이션이란? 한 시스템에서 다른 시스템으로 이동하는 것이다.
- 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어 주기 때문에 애플리케이션 의존성 방향이 일관되고, 애플리케이션을 쉽게 추론할 수 있으며, 모듈 간의 관계들이 조금 더 명확해진다.
DI의 단점
- 모듈들이 더욱더 분리되므로 클래스 수가 늘어나 복잡성이 증가될 수 있다.
- 약간의 런타임 페널티가 생긴다.
DI 구현 방법
* 스프링에 여러 가지 주입 방법 중 가장 간편하게 쓰이는 방법으로 @Autowired가 있다. @Autowired => 여기에 의존성을 주입해달라는 뜻, 다만 주입받을 클래스(BurgerRecipe)도 스프링 Bean이어야 한다.
1. Field Injection(필드 주입): 멤버 변수를 이용한 의존성 삽입
변수 선언부에 @Autowired 애노테이션을 붙인다. 굉장히 간단하고 편리해보이지만 IntelliJ에서는 사용을 비권장하고 있다.(아래와 같은 단점들 때문에)
@Service
public class BurgerService {
@Autowired
private BurgerRecipe burgerRecipe;
}
- 장점
- 사용하기 편하다.
- 단점
- SRP(Single Responsibility Principle, 단일 책임 원칙) 위반 가능성이 커진다.
- SRP란 SOLID 원칙 중 하나로 모든 클래스는 각각 하나의 책임만 가져야 하는 원칙이다. 예를 들어 A라는 로직이 존재한다면 어떠한 클래스는 A에 관한 클래스여야 하고 이를 수정한다고 했을 때도 A와 관련된 수정이어야 한다.
- @Autowired 선언만 하면 되므로 의존성을 주입하기 쉽다.
- 따라서, 하나의 클래스가 많은 책임을 갖게 될 가능성이 높다.
- 의존 관계가 보이지 않는다.
- 생성자 주입에 비해 의존 관계를 한 눈에 파악하기 어렵다.
- DI 컨테이너와의 결합도가 커지고, 테스트하기 어렵다.
- 테스트 등의 이유로 자동이 아닌 수동 의존성 주입을 하고 싶어도 생성자도, setter도 없으므로 개발자가 직접 의존성을 넣어줄 수가 없다.
- 순환 참조가 발생할 수 있다. => 순환 참조는 아래에서 설명
- SRP(Single Responsibility Principle, 단일 책임 원칙) 위반 가능성이 커진다.
2. Setter Injection(수정자 주입): setter 메서드를 이용한 의존성 삽입
@Service
public class BurgerService {
private BurgerRecipe burgerRecipe;
private PotatoRecipe potatoRecipe;
@Autowired
public void setBurgerRecipe(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
@Autowired
public void setPotatoRecipe(PotatoRecipe potatoRecipe) {
this.potatoRecipe = potatoRecipe;
}
}
setter를 사용한 주입이다. setter 주입은 필드, 생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다. (실제로 변경이 필요한 경우는 극히 드물다.)
- 장점
- 주입받는 객체가 변경될 가능성이 있기 때문에 선택적인 의존성을 사용할 수 있다.
- 런타임에 setter을 호출하면 주입해주었던 의존성을 변경해줄 수가 있다. 그래서 주로 런타임에 의존성을 수정해줘야 하거나 의존성을 선택적으로 주입할 때 사용한다.
- 주입받는 객체가 변경될 가능성이 있기 때문에 선택적인 의존성을 사용할 수 있다.
- 단점
- setter 메서드가 필요하고 주입받는 객체가 변경될 가능성이 있기 때문에 파이널 필드를 만들 수 없다.(불변성을 보장 할 수 없다.)
- 선택적인 의존성을 사용할 수 있다는 것은 BurgerService에 모든 구현체를 주입하지 않아도 burgerRecipe 객체를 따로 생성할 수 있고, 객체의 메소드를 호출할 수 있다. 즉, 주입받지 않은 구현체를 사용하는 메소드에서 NPE(Null Pointer Exception)가 발생한다. (주입할 대상이 없어도 동작하도록 하려면 @Autowired(required = false)를 통해 설정할 수 있다.)
- 필드 주입과 마찬가지로 순환 참조 문제가 발생할 수 있다.
3. Contructor Injection(생성자 주입): 생성자를 이용한 의존성 삽입
@Service
public class BurgerService {
private BurgerRecipe burgerRecipe;
@Autowired
public BurgerRecipe(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
}
- 스프링에서 공식적으로 가장 권장하는 방식이다. 스프링 4.3부터 생성자가 1개만 있을 경우에 @Autowired를 생략해도 주입이 가능하다.
- 장점
- 의존 관계를 모두 주입 해야만 객체 생성이 가능하므로 NPE를 방지할 수 있다.
- 불변성을 보장할 수 있다.
- 실제로 개발을 하다 보면 의존 관계의 변경이 필요한 상황은 거의 없다. 하지만 setter 주입이나 일반 메소드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어 유지보수성을 떨어뜨린다. 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.
- 순환 참조를 컴파일 단계에서 찾아낼 수 있다.
- 단점
- 의존성을 주입하기 번거롭고, 생성자 인자가 많아지면 코드가 길어져 위기감을 느낄 수 있다.
- 이를 바탕으로 SRP 원칙을 생각하게 되고, 리팩터링을 수행하게 된다.
- 의존성을 주입하기 번거롭고, 생성자 인자가 많아지면 코드가 길어져 위기감을 느낄 수 있다.
4. Method Injection(일반 메서드 주입): 일반 메서드를 이용한 의존성 삽입
일반 메소드를 통해 의존 관계를 주입하는 방법이다. 수정자 주입과 동일하며 마찬가지로 거의 사용할 필요가 없는 주입 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
순환 참조
순환 참조란 서로 다른 여러 빈들이 서로를 참조하고 있음을 의미한다.
CourseService에서 StudentService에 의존하고, StudentService가 CourseService에 의존하면 순환 참조라고 볼 수 있다.
1. 필드 주입인 경우
@Service
public class CourseServiceImpl implements CourseService {
@Autowired
private StudentService studentService;
@Override
public void courseMethod() {
studentService.studentMethod();
}
}
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private CourseService courseService;
@Override
public void studentMethod() {
courseService.courseMethod();
}
}
이 상황은 StudentServiceImple의 studentMethod()는 CourseServiceImpl의 courseMethod()를 호출하고, CourseServiceImpl의 courseMethod()는 StudentServiceImple의 studentMethod()를 호출하고 있는 상황이다. 서로 서로 주거니 받거니 호출을 반복하면서 끊임없이 호출하다가 결국 StackOverflowError를 발생시키고 죽는다.
이처럼 필드 주입이나 수정자 주입은 객체 생성 후 비즈니스 로직 상에서 순환 참조가 일어나기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 없다.
2. 생성자 주입인 경우
@Service
public class CourseServiceImpl implements CourseService {
private final StudentService studentService;
@Autowired
public CourseServiceImpl(StudentService studentService) {
this.studentService = studentService;
}
@Override
public void courseMethod() {
studentService.studentMethod();
}
}
@Service
public class StudentServiceImpl implements StudentService {
private final CourseService courseService;
@Autowired
public StudentServiceImpl(CourseService courseService) {
this.courseService = courseService;
}
@Override
public void studentMethod() {
courseService.courseMethod();
}
}
생성자 주입일 때 애플리케이션을 실행하면 아래와 같은 로그가 찍히면서 실행이 실패한다.
***************************
APPLICATION FAILED TO START
***************************
Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
| courseServiceImpl defined in file [/.../CourseServiceImpl.class]
↑ ↓
| studentServiceImpl defined in file [/.../StudentServiceImpl.class]
이처럼 생성자 주입은 스프링 컨테이너가 빈을 생성하는 시점에 순환 참조를 확인하기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 있다.
생성자 주입을 사용해야 하는 이유
최근에는 Spring을 포함한 DI 프레임워크의 대부분이 생성자 주입을 권장하고 있다.
생성자 주입을 사용해야 하는 이유를 다시 정리하면
1. 객체의 불변성 확보
2. 테스트 코드의 작성
- 메인 코드는 DI 프레임워크 위에서 동작하지만 테스트 코드는 그러지 않기 때문에 의존성 주입이 정상적으로 되지 않아 필드나 setter 주입 같은 경우 테스트 진행 시 주입 될 객체가 Null이기 때문에 NPE가 발생한다. 그래서 반드시 Mockito를 이용해 Mocking한 후 테스트를 진행해야 한다.
- 하지만 생성자 주입은 Mockito와 적절히 섞어서 테스트를 할 수도 있고 테스트 코드 안에서 직접 new 연산을 통해 주입 될 객체를 생성할 수 있다는 장점이 있다.
3. final 키워드 작성
4. 순환 참조 에러 방지
DJANGO DI VS SPRING DI
두 개의 프레임워크를 사용해봤기 때문에 갑자기 궁금해져서 조사했다. Django에도 DI가 존재할까?
Django도 프레임워크에 DI를 내장 하지만 dictionary에 환경 별 dependency를 key-value 형태로 명시하고, python duck typing 기능을 활용해서 의존성을 주입한다.
DRF(Django Rest Framework)같은 경우에는 class 기반으로 의존성을 주입한다. 클래스의 메서드로 기능을 추가한다고 한다. 하지만 필드, 메소드, 생성자 단위로 DI를 하는 스프링을 따라잡을 수 없다.
스프링 프레임워크는 애노테이션 하나만으로 DI가 되기 때문에 훨씬 편리하다.
즉, DI는 스프링 프레임워크의 최대 장점이다.
Django DI 에 대해 더 자세히 알고싶다면 아래 링크 클릭!
https://www.humphreyahn.dev/blog/dependency-injector#9b987463-025c-43ba-a521-90fccd0b959b
파이썬 애플리케이션 의존성 주입 - dependency injector
Dependency Injector로 파이썬 애플리케이션에서 낮은 결합도, 높은 응집도를 가진 코드 만들기
www.humphreyahn.dev
참고:
https://github.com/WeareSoft/tech-interview/blob/master/contents/spring.md#ioc%EB%9E%80
GitHub - WeareSoft/tech-interview: 🙍 tech interview
:loudspeaker:🙍 tech interview. Contribute to WeareSoft/tech-interview development by creating an account on GitHub.
github.com
https://www.humphreyahn.dev/blog/dependency-injector#9b987463-025c-43ba-a521-90fccd0b959b
파이썬 애플리케이션 의존성 주입 - dependency injector
Dependency Injector로 파이썬 애플리케이션에서 낮은 결합도, 높은 응집도를 가진 코드 만들기
www.humphreyahn.dev
https://doozi316.github.io/java/2020/07/01/WEB21/
POJO와 Bean의 개념과 차이
doozi316.github.io
https://code-lab1.tistory.com/127
[Spring] IoC,DI, 스프링 컨테이너(Container), 스프링 빈(Bean)이란?
IoC(Inversion of Control)란? IoC는 제어의 역전이라는 뜻으로 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 말한다. 이전에는 개발자가 객체를 생성하고 관리하며 프로
code-lab1.tistory.com
https://www.youtube.com/watch?v=GKoCibDM6Ns
https://mangkyu.tistory.com/226
[OOP] 의존성(Dependency)이란? 컴파일타임 의존성과 런타임 의존성의 차이 및 비교
공부를 하다보면 런타임 의존성과 컴파일타임 의존성이라는 얘기가 나옵니다. 그런데 이와 관련되어 잘 정리된 글이 없어서 많은 분들이 이해하는데 어려움을 겪는 것 같습니다. 그래서 이번에
mangkyu.tistory.com
https://velog.io/@ehddek/JUnit-%EC%9D%B4%EB%9E%80
[JUnit] JUnit 이란?
[JUnit] JUnit 이란?
velog.io
https://steady-coding.tistory.com/600
[Spring] Spring IoC와 DI란?
spring-study에서 스터디를 진행하고 있습니다. IoC란? IoC란 Inversion of Control의 줄임말이며, 제어의 역전이라고 한다. 스프링 애플리케이션에서는 오브젝트(빈)의 생성과 의존 관계 설정, 사용, 제거
steady-coding.tistory.com
Gradle을 위한 Groovy 문법 한 번에 정리하기 : Data Type, String, Closure, Collection, Method, Class
Groovy란? Groovy란 JVM위에서 동작하는 동적 타입 프로그래밍 언어이다. Java뿐만 아니라 Python, Ruby 등에 많은 영향을 받은 프로그래밍 언어로 문법이 간결하다. 하지만, 사실상 대부분 Gradle Script를
kotlinworld.com
https://mangkyu.tistory.com/125
[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)
Spring 프레임워크의 핵심 기술 중 하나가 바로 DI(Dependency Injection, 의존성 주입)이다. Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 이용하는 방법이 있는데, 각각의 방
mangkyu.tistory.com
Spring 핵심 원리 기본편 (6) - 컴포넌트 스캔(Component Scan) / @Autowired
지금까지 스프링 빈(Bean)을 등록할 때 구성파일에 @Bean 을 사용했다 \--> 관리할 빈이 많아지면 관리하기 번거로워 진다굳이 빈 설정파일을 만들지 않거나, @Bean을 안쓰고 빈 등록을 할 수 있다
velog.io
https://dreamcoding.tistory.com/69
SOLID 원칙 5 - DIP: 의존성 역전 원칙 (Dependency Inversion)
DIP 객체 지향 프로그래밍 및 설계에서 5가지 기본원칙(SRP, OCP, LSP, ISP, DIP)의 마지막 다섯번째 원칙인 DIP(Dependency Inversion Principle)에 대해 알아보겠습니다. 의존관계 역전 원칙 (Dependency inversion princ
dreamcoding.tistory.com
https://luckydavekim.github.io/development/back-end/spring-framework/create-spring-bean
Spring Bean 등록하는 여러 가지 방법
Spring Bean을 XML 및 Java로 등록하는 방법과 컴포넌트 스캔을 활용하는 법에 대해 알아봅니다.
luckydavekim.github.io
https://programforlife.tistory.com/111
[Spring] 생성자 주입을 사용해야 하는 이유
인턴을 시작한 초기 단계에, Spring 프로젝트 코드 분석을 하다가 신기한 점을 발견해서 질문을 한 기억이 있습니다. Spring프로젝트에서 Controller를 작성할 때, 저는 항상 @Autowired 어노테이션을 사
programforlife.tistory.com
주홍철.『면접을 위한 CS 전공지식 노트』.길벗, 2022.