[Spring] DI/ IOC
개인적으로 공부하면서 정리한 내용입니다 : )
FLAG Project - BE Study
2022.12.08
dependency 의존관계란 무엇인가?
“A가 B를 의존한다.”는 표현은 어떤 의미일까? 추상적인 표현이지만, 토비의 스프링에서는 다음과 같이 정의한다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p113
즉, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다.
⏺ IoC(Inversioin of Control) / DI(Dependency Injection)
IoC (제어의 역전)과 DI(의존성 주입) 예시
파인트는 3개의 아이스크림을 담는다.
만약 파인트가 [ 바닐라, 초콜릿, 체리쥬빌레 ]로 정해져있으면 ?
[민트초코, 그린티, 오레오 쿠키앤크림] 넣어달라고 라고 했는데
>> 안됩니다. 파인트는 바닐라, 초코, 체리쥬빌레입니다 라고 말하고 [ 바닐라, 초콜릿, 체리쥬빌레] 를 담으심
[민트초코, 그린티, 오레오 쿠키앤크림]를 주문했는데
먹고 싶은걸 못먹는 아주 슬픈일이 벌어진다😂
아이스크림 종류에 대한 제어권이 객체 내부에 있는 것임. 외부(클라이언트)의 요청에 따라 유연한 처리가 불가능하다.
제어의 역전 - 객체 외부로 빼기
아이스크림 종류에 대한 제어권을 파인트 객체가 갖고 있었다면
이제는 주문하는 손님(외부)에서 제어권을 갖고 있음(파인트 객체 생성 시점에)
아이스크림 종류에 대해 자유롭게 선택이 가능해짐! DI (의존성 주입) 을 통해 바깥에서 아이스크림 종류를 전달해줌
>> 유연한 코드 작성 가능 (낮은 결합도)
>> 가독성과 유지보수의 효율이 좋아짐! 코드의 중복을 줄일 수 있따.
⏺ 스프링은 스프링 컨테이너를 통해 객체를 주입한다.
- 컨테이너는 개발자가 정의한 Bean을 객체로 만들어 관리하고 개발자가 필요로 할 때 제공함
- ApplicationContext을 스프링 컨테이너라고 한다.
- 스프링 컨테이너는 Configuration Metadata를 사용한다.
- @Configuration이 붙은 클래스를 설정정보로 사용해 스프링 빈을 등록한다.
- @Bean이 붙은 메서드를 모두 호출하고, 반환(return)된 객체를 스프링 컨테이너에 등록한다.
- 스프링 컨테이너에 등록된 객체 == 스프링 빈
- @Configuration이 붙은 클래스를 설정정보로 사용해 스프링 빈을 등록한다.
- 스프링 빈은 applicationContext.getBean("이름",<타입>)을 이용해 찾을 수 있다. '
- 그치만 getBean은 사용하면 안된다 왜일까?
- ApplicationContext.getBean() 은 제어의 역전을 위반한다!!
- IoC로 빈의 이름을 몰라도 사용할 수 있어야하는데 getBean을 쓰면 빈의 이름을 알아야해서..
- 결국 의존성 종속이 되어버린다.
⏺ BeanDefinition
스프링 빈 설정 메타 정보
다양한 형태의 설정정보들이 있는데,
스프링 컨테이너는 XML인지 자바코드인지 스프링 컨테이너는 알 필요가 없다.
다양한 형태의 설정정보를 BeanDefinition으로 추상화해서 사용한다
⏺ 스프링 컨테이너의 기본값은 싱글톤이다!
- 애플리케이션 컨텍스트가 싱글톤으로 빈을 관리하는 이유는 대규모 트래픽을 처리할 수 있도록 하기 위함이다.
- 스프링은 최초에 설계될 때 부터 대규모의 엔터프라이즈 환경에서 요청을 처리할 수 있도록 고안되었다.
- 그에 따라 계층적으로 처리 구조(Controller, Service, Repository 등) 가 나뉘어지게 되었다.
- 매번 클라이언트에서 요청이 올 때마다 각 로직을 처리하는 빈을 새로 만들어서 사용한다고 생각해보면
- 요청 1번에 5개의 객체가 만들어진다고 하고, 1초에 500번 요청이 온다고 하면 초당 2500개의 새로운 객체가 생성된다
- 아무리 GC의 성능이 좋아졌다고 해도 부하가 걸리면 감당이 안됨
이러한 문제를 해결하고자 빈을 싱글톤 스코프로 관리하여 1개의 요청이 왔을 때 여러 쓰레드가 빈을 공유해 처리하도록 한것
✅ Component Scan
스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공함
@Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다.
@Configuration
@ComponentScan
public class AutoDependencyConfig {
}
주로 정형화된 <컨트롤러 - 서비스 - 리포지토리 >같은 코드는 컴포넌트 스캔 원리를 활용한다.
정형화된 코드는 @Component 대신 @Controller, @Service, @Repository를 사용할 수 있음
>> 이 세 가지 어노테이션들이 @Component라는 어노테이션을 포함하기 때문에 가능
((참고해서 보면 좋을것 같아 가져왔다.))
⏺ @Configuration vs @Component
두 Annotation이 동작하는 방식이 비슷해보이는데, 실제로 정의를 다시봐도 역할이 비슷하다.
@Configuration이 붙은 클래스를 설정정보로 사용해 스프링 빈을 등록한다.
- @Bean이 붙은 메서드를 모두 호출하고, 반환(return)된 객체를 스프링 컨테이너에 등록한다.
@Component 어노테이션이 있으면 스프링 빈으로 자동 등록됨
--> 둘 다 Annotation이 기술된 클래스를 빈으로 등록시키며, 내부의 @Bean 애노테이션이 붙은 클래스들을 스프링컨텍스트에 등록시킨다.
--> @Configuration에는 @Component가 적용되어있기 때문에 ComponentScan할 때 자동으로 스프링 빈 등록이 된다.
✔️ 두 어노테이션의 차이점은 무엇일까?
결과가 서로 다르다.
// @Configuration 사용 예제
@Configuration
public static class Config {
@Bean
public SimpleBean simpleBean() {
return new SimpleBean();
}
@Bean
public SimpleBeanConsumer simpleBeanConsumer() {
return new SimpleBeanConsumer(simpleBean());
}
}
-----------------
// @Component 사용 예제
@Component
public static class Config {
@Bean
public SimpleBean simpleBean() {
return new SimpleBean();
}
@Bean
public SimpleBeanConsumer simpleBeanConsumer() {
return new SimpleBeanConsumer(simpleBean());
}
}
simpleBeanConsumer() 메소드 내에서 simpleBean() 를 호출하는 부분이 있는데 이 부분이 동작하는 방식이 다르다.
- 첫 번째 예제 (@Configuration) 의 메소드에서는 simpleBean() 를 호출시 :: 스프링 컨텍스트에 등록되어 있는 SimpleBean 클래스를 반환
- 두번째 예제 (@Component )에서는 순수 메소드 호출을 한것 처럼 스프링 컨텍스트에 등록되어 있는 빈이 반환되는 것이 아니라 새로 생성된 빈이 반환
@Componet 예제에서 스프링 컨텍스트에 있는 SimpleBean를 반환하고 싶다면
아래와 같이 @Autowired를 이용해 해당 빈을 DI 받고 사용해야한다.
@Component
public static class Config {
@Autowired
SimpleBean simpleBean;
@Bean
public SimpleBean simpleBean() {
return new SimpleBean();
}
@Bean
public SimpleBeanConsumer simpleBeanConsumer() {
return new SimpleBeanConsumer(simpleBean);
}
}
*** CGLIB(Code generation library)이 두 애노테이션 마다 다르게 동작하기 때문에 차이가 있다. ***
기본적으로 @Bean 매소드들은 최초 한 번 호출되어서 생성된 객체가 스프링 컨텍스트에 저장이 된다.
@Configuration 애노테이션이 사용된 클래스 내부에서는 @Bean 메소드의 내부에서 호출한 메소드가 @Bean 메소드일 경우
컨텍스트에 등록된 빈을 반환하도록 되어 있다
"빈은 기본적으로 싱글톤이기 때문에
@Bean이 붙은 메소드는 여러 번 호출돼도 하나의 오브젝트만 리턴되는 것이 보장된다”고 했지만
이는 @Configuration 클래스 안에서 사용된 @Bean 메소드에만 해당된다.
일반 POJO클래스 안에서 사용된 @Bean메서드는 매번 다른 오브젝트를 리턴한다.
✅ 의존 관계 주입 방법
- 생성자 주입 (Constructor - Spring 공식문서 권장)
- 불변/필수
- 수정자 주입 (Setter)
- 가변
- 나머지 주입방법은 일단 이게 있다~ 정도만 알고 넘어가도 될듯
- 필드 주입 (불변도 가변도 아닌 애매모호함 → 사용안함)
- 인텔리제이는 Field injection is not recommended 경고 발생)
- 일반 메서드 주입
- 얘네 쓸바엔 불변/가변 상황에 따라 생성자/수정자 주입을 적절하게 사용해야한다.
- 필드 주입 (불변도 가변도 아닌 애매모호함 → 사용안함)
- 대부분이 불변 의존 관계이다. > 결국 생성자 주입이 권장됨
⏺ 생성자 주입 방식 - (Constructor)
특징: 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. (싱글톤)
불변, 필수 의존관계에 사용
스프링 빈의 경우 생성자가 1개만 있으면 @Autowired를 생략해도 자동 주입 된다.
- 의존 관계가 눈에 잘 보이지 않아 추상적이라서 의존성관계가 과도하게 복잡해질 수 있다.
- 생성자주입과 수정자주입은 의존성을 명확하게 커뮤니케이션한다.
- SRP 에 위반하는 안티 패턴
- DI Container와 강한 결합을 가진다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository ;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
⏺ 수정자 주입 방식(setter 주입)
특징 : 선택, 변경 가능성이 있는 의존관계에 사용한다.
@Autowired 어노테이션을 선언해 주입받는 방법이다. (메소드 이름을 setter 대신 다른걸로 해도 주입은 가능하지만 좋은 방법은 아님)
- 의존성이 선택적으로 필요한 경우에 사용한다.
- 생성자에 모든 의존성을 기술하면 과도하게 복잡해질 수 있어 선택적으로 나눠 주입할 수 있게 부담을 던다.
- final 선언이 불가하다.
public class MemberServiceImpl implements MemberService{
private MemberRepository memberRepository ;
@Autowired
public void setMemberRepository(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
⏺ 필드 주입
필드에 @Autowired 붙여서 바로 주입하는 방법
- 외부에서 변경이 불가능하여 테스트하기 힘들다
- DI 프레임워크가 없으면 아무것도 할 수 없음
- 결국 setter가 필요하게 되서 수정자 주입을 사용하는게 더 편리
@Component
public class CoffeeService {
@Autowired
privat MemberRepository memberRepository;
@Autowired
privat CoffeeRepository coffeeRepository;
}
⏺ 생성자 주입을 사용해야 하는 이유
1. 불변
- 대부분의 의존관계 주입은 한번 일어난 후 애플리케이션이 종료될 때까지 의존관계를 변경할 일이 없음 ( 오히려 애플리케이션 종료 전까지 변하면 안됨)
- 수정자 주입을 사용하면 set 메서드를 public으로 열어두어야하는데, 이렇게 되면 누군가 실수로 변경할 수 도있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아님
2. 디버깅, 테스트 용이 (누락 / final 사용가능)
- 생성자 주입은 불변하기 때문에 final을 선언하는데, 생성자 주입코드에 코드를 작성하지 않았을 경우 컴파일 오류가 뜨면서 빠르게 해결이 가능함.
- 나머지 주입방식 (수정자 주입...)의 경우 final을 선언할 수 없다.
- final로 선언된 레퍼런스 타입의 변수는 선언과 함께 반드시 초기화가 되어야 함
>>> 나머지 주입방식 사용시 생성자 이후에 호출되기 때문에, 의존관계 주입을 받을 필드에 final 선언이 불가능하다. - final은 클래스 내부에서 해당 필드를 바꿔치기 할 수 없다
- test code 작성시에 생성자를 누락할수도 있는데, 이때도 컴파일 오류로 빠르게 해결이 가능하다.
- set을 사용하면, 들어가는 인자가 무엇이었는지 기억나지 않을 경우 해결이 어려움
- NullPointerException 을 방지할 수 있다. (null을 주입하지 않는 한 발생하지 않음)
3. 순환참조의 방지
- A 가 B를 참조하고, B가 A를 참조하는 순환참조 코드가 있다 가정
- 필드 주입, 수정자 주입은 빈이 생성된 이후에 참조를 하기 때문에 application은 아무런 오류 , 경고 없이 구동된다.
- 실제 코드가 호출될때 까지 문제를 알 수 없다.
- 생성자 주입을 할 경우 실행시 BeanCurrentlyInCreationException를 알려줌
- 또 의존 관계에 내용을 외부로 노출 시킴으로써 어플리케이션을 실행하는 시점에서 오류를 체크할 수 있다.
⏺ @Autowired
: 생성자에 붙여놓으면 스프링 컨테이너에 있는 인스턴스를 가져와서 연결해준다.
- Autowired를 통한 DI는 memberService, helloController와 같이 스프링이 관리하는 객체에서만 동작한다.
- 스프링빈으로 등록되지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
- (결국 스프링 빈으로 등록된 객체만 Autowired가 가능하다)
- @Service생성자에 repository객체를 연결할 수 있게 @Autowired를 추가한다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
- 순수자바코드에선 @Autowired가 동작하지 않는다.
주입할 스프링 빈이 없을 때 동작해야하는 경우가 있다.
- @Autowired는 옵션 기본값인 true가 사용되므로, 주입할 대상이 없을 때 Exception을 터트린다.
- Autowired(required = false)로 지정해주면 주입할 대상이 없어도 동작함
- setter와 필드에 Autowired를 사용하게 되면 만약 BookRepository가 빈으로 등록되어 있지 않다 해도 BookService는 빈으로 등록 가능
// 01) BookRepository가 빈으로 등록되어 있지 않은 경우
// Autowired (True)
@Service
public class BookService {
BookRepository bookRepository;
@Autowired //error -> autowired 기본값이 true라서
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
@Service
public class BookService {
BookRepository bookRepository;
@Autowired //error ->setter여도 마찬가지다 !!!
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
----------------------------------------------
// 02) Autowired = False
// BookService의 객체는 빈으로 등록이 되지만 BookRepository는 빈으로 등록되지 않게 된다.
@Service
public class BookService {
BookRepository bookRepository;
//setter에 사용 가능
@Autowired(required = false)
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
-----------
@Service
public class BookService {
//필드에도 사용 가능
@Autowired(required = false)
BookRepository bookRepository;
}
-----------
@Service
public class BookService {
//false여도 생성자는 불가능!
@Autowired(required = false)
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
//생성자에 Autowired를 쓴 상황에서
//BookRepository가 빈으로 등록되어 있지 않다면
//BookService도 빈으로 등록되지 못하는 경우가 생김
}