관리 메뉴

JIE0025

[Spring] DI/ IOC 본문

백엔드/스프링, 스프링부트, JPA, Spring Webflux

[Spring] DI/ IOC

Kangjieun11 2022. 12. 8. 20:50
728x90

 

 

개인적으로 공부하면서 정리한 내용입니다 : )

 

 


 

 

 

FLAG Project - BE Study 

2022.12.08





FLAG

 

 

 

 

 


 

 

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)된 객체를 스프링 컨테이너에 등록한다. 
      • 스프링 컨테이너에 등록된 객체 == 스프링 빈
  • 스프링 빈은 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도 빈으로 등록되지 못하는 경우가 생김
    
}