개발/Java|Spring

스프링 핵심원리 기본 - IoC, DI, ApplicationContext, 의존관계 주입 등

달리초이 2023. 4. 6. 10:58

 

아주 오랜만에 김영한님 강의를 핵심원리부터 다시 보기 시작했다. 기본적인 내용이지만 다시 들어보니 머릿속이 좀 정리가 되는 기분이다. 그 땐 몰랐는데 다시 보니 팍팍 꽂히는 내용도 수두룩하다. 도대체 처음 듣던 당시엔 뭘 이해했던 건가 싶어 자괴감이 들기도... 그래도 그간 줏어듣고 코드 쫌 깨작거렸다고 이해도가 약간은 올라갔나보다. 또 돌아서면 까먹을 게 뻔하니 조금이라도 기록하고 남겨보자. 내용이 방대해서 부분적으로 발췌하고 요약했다.

 


1. 제어의 역전(IoC)

스프링은 좋은 객체 지향 설계를 위해 역할과 구현을 분리시킬 수 있는 많은 방법을 제공하는데, 그 중 가장 기본적이고 좋은 도구가 바로 DI 컨테이너이다. 구현 객체는 자신의 로직을 실행하는 역할만 담당하고 프로그램의 제어 흐름은 스프링이 맡아서 관리한다. 이를 제어의 역전(IoC, Inversion Of Control)라고 한다.

 

 

2. 의존관계 주입 DI(Dependency Injection)

의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다. 정적인 클래스 의존 관계는 import 코드만 봐도 쉽게 판단할 수 있다. 동적인 객체 인스턴스 의존 관계는 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다. 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.

의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

 

 

3. DI 컨테이너

이처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다. DI컨테이너를 제공하는 프레임워크는 많지만, 자바 진영에서 가장 유명하고 많이 사용하는 프레임워크가 바로 스프링이다. 사실상 표준으로 취급된다.

 

개인적인 감상이지만, 스프링에게 DI를 맡기는 게 당연한 상태로 개발을 시작해서 그런지 처음엔 왜 좋은지 몰랐다. 지금 와서 보니 객체 지향과 좋은 설계에 대한 선배들의 눈물, 콧물, 다크써클이 느껴진다. 처음부터 다 된 밥에 숟가락만 얹어서 시작했더니 개념도 없고 실력도 없는 바보였구나 반성이 된다.(하지만 스프링을 쓸 수 없었다면 떼려치지 않았을까..ㅎ)

 

 

4. @Configuration과 @Bean

클래스 타입에 @Configuration을, 각 메서드에 @Bean 어노테이션을 붙여주면 스프링 컨테이너에 스프링 빈으로 등록된다. 스프링에서 스프링 컨테이너는 ApplicationContext 객체를 말하고, 스프링 빈이라고 하면 스프링 컨테이너가 관리하는 자바 객체를 말한다. 스프링 컨테이너에 등록된 Bean 객체들은 필요한 곳곳에 주입되는 등 스프링이 관리해준다(IoC).

 

 

5. 스프링 컨테이너 ApplicationContext

스프링 컨테이너는 자바 콛, XML, Groovy 등 다양한 형식의 설정 정보를 받아드릴 수 있도록 유연하게 설계되어 있다. 이전에는 XML을 기반으로 만드는 경우가 많았지만, 최근에는 스프링 부트를 많이 사용하면서 어노테이션 기반의 자바 설정 클래스로 만드는 경우가 더 많다. 필요한 경우 ApplicationContext 구현체를 커스터마이징하여 어노테이션이 아니라 JSON 등 다른 방식으로 만들어 사용할 수도 있겠지만.(그럴 일은 거의 없을 거 같다.) JSON ApplicationContext를 혹시 누군가 만들어보지 않았을까 싶어 잠깐 검색해보니 역시나 있다.ㅎㅎ 궁금하신 분들은 여기로.(ObjectMapper를 이용하여 Reader를 만들고 있다.)

 

 

5-1. ApplicationContext가 제공하는 부가기능

ApplicationContext는 BeanFactory의 기능을 상속받는데, 빈 관리기능과 아래처럼 편리한 부가기능을 제공한다.

- 메시지소스를 활용한 국제화 기능: 예를 들어 한국에서 들어오면 한국어, 영어권에서 들어오면 영어로 출력

- 환경변수: 로컬, 개발, 운영 등을 구분해서 처리

- 애플리케이션 이벤트: 이벤트를 발행하고 구독하는 모델을 편리하게 지원

- 편리한 리소스 조회: 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

5-2. 싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤으로 관리한다. 스프링 컨테이너가 싱글톤 컨테이너 역할을 하는데, 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다. 스프링 컨테이너는 DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다. 단 주의해야 될 점은 하나의 같은 객체 인스턴스를 공유하기 때문에 상태를 유지하게 설계하면 안된다. Stateless(무상태)로 설계해야 한다.

- 특정 클라이언트에 의존적인 필드가 있으면 안된다.

- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.

- 가급적 읽기만 가능해야 한다.

- 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

 

5-3. @Configuration과 바이트코드 조작의 마법

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. @Configuration 어노테이션을 적용하면, 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록하게 된다. @Configuration 어노테이션을 적용한 클래스에 @Bean이 붙은 메서드마다 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 그렇게 싱글톤을 보장해 준다. @Configuration이 없고 @Bean만 적용하면 CGLIB 기술 없이 순수한 클래스로 스프링 빈에 등록된다. 즉 싱글톤을 보장받으려면 @Configuration을 사용하면 된다.

 

 

6. 컴포넌트 스캔

@Configuration과 @Bean을 이용해 직접 스프링 빈을 등록하는 설정 정보가 없더라도 스프링은 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또 의존관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.

 

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다. 말그대로 @Component 어노테이션이 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다. 스캔 대상에서 제외시키려면 excludeFilters라는 옵션을 이용하면 된다. 반대로 대상을 지정하는 includeFilters도 있다.

 

여기서 잠깐, @Bean과 @Component 의 차이점이 뭔지 궁금하신 분들은 향로님 글 참고.

 

컴포넌트 스캔은 basePackages를 설정할 수 있는 옵션이 있는데 패키지 위치를 지정하지 않으면 기본적으로 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다. 권장하는 방법은 성정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다. @SpringBootApplication 어노테이션 속을 까보면 @ComponentScan이 들어있는 것을 알 수 있다. 또 @ComponentScan은 @Component 뿐만 아니라 @Controller, @Service, @Repository, @Configuration도 스캔 대상에 자동 포함하는데, 이 어노테이션들 속을 까보면 @Component이 들어있다.

 

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.
- @Controller : 스프링 MVC 컨트롤러로 인식
- @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
- @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
- @Service : 특별한 처리는 없고, 대신 핵심 비즈니스 로직을 명시해 비즈니스 계층을 인식하는데 도움이 된다.

 

 

7. 의존관계 자동 주입

의존관계 주입은 크게 4가지 방법이 있다.

- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입

 

의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지
않는다.

 

7-1. 생성자 주입

생성자를 통해 의존관계를 주입 받는 방법이다. 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 불변, 필수 의존관계에 사용한다. 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다.

 

7-2. 수정자 주입(setter 주입)

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

 

7-3. 필드 주입

필드에 바로 주입하는 방법이다. 코드가 간결해서 유혹적이지만 변경이 불가능해서 테스트하기 힘들다는 단점이 있다. DI 프레임워크가 없으면 아무것도 할 수 없다. 사용하지 않는 것을 권장한다.

// 필드 주입 예시
// 애플리케이션의 실제 코드와 관계 없는 테스트 코드
// 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용
@Component
public class OrderServiceImpl implements OrderService {
 @Autowired
 private MemberRepository memberRepository;
 @Autowired
 private DiscountPolicy discountPolicy;
}

 

7-4. 일반 메서드 주입

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

 

결론적으로, 생성자 주입을 사용하자. 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지
않는게 좋다.

아래는 과거 생성자주입 변천사에 대해 짤막하게 정리한 글이다. 롬복을 사용한다면 final 키워드와 @RequiredArgsConstructor를 이용해 쉽고 깔끔하게 생성자주입을 이용할 수 있다. (물론 롬복에 의존되지만...)

 

스프링 의존성주입 생성자주입(@RequiredArgsconstructor 쓰는 이유)

1. @Autowired는 변경이 어렵다 @Service @Transactional(readOnly = true) public class MemberService { @Autowired private MemberRepository memberRepository; } 2. 그래서 변경을 위해 Setter Injection 사용 하지만 조립한 이후에 바꿀

petaverse.pe.kr

 

 

참고로 조회 빈이 2개 이상일 때 방법

- @Autowired 필드 명 매칭
- @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
- @Primary 사용

 

 

8. 빈 생명주기 콜백

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고 애플리케이션 종료 시점에 연결을 모두 끊어내는 작업을 하려면 객체의 생명주기를 알아야 한다. 스프링 빈의 라이프사이클은 아래와 같다.

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

- 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출

- 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

참고로 객체의 생성과 초기화는 분리시키는 것이 좋다. 생성자는 필수 정보를 받고 메모리를 할당해서 생성하는 책임만 두고, 이 생성값을 이용해 외부 커넥션 등 무거운 동작을 수행하는 초기화 작업은 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도라면 생성자에서 처리하는 게 나을 수 있다.

 

스프링은 다양한 방식으로 생명주기 콜백을 지원하는데 크게 3가지가 있다.

- 인터페이스(InitializingBean, DisposableBean)

- 설정 정보에 초기화 메서드, 종료 메서드 지정 - @Bean(initMethod = "init", destroyMethod = "close")

- @PostConstruct, @PreDestroy 어노테이션 지원

 

여기선 @Bean, @PostConstruct, @PreDestroy만 살펴보자.

 

8-1. @Bean(initMethod = "init", destroyMethod = "close")

메서드 이름을 자유롭게 줄 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는다. 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다. @Bean의 destroyMethod에 특별히 이름을 명시하지 않으면, 내부에 close, shutdown 이름의 메서드가 존재하면 자동으로 destroyMethod로 추론하여 호출해준다.

 

8-2. @PostConstruct, @PreDestroy

최신 스프링에서 가장 권장하는 방법이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다. 컴포넌트 스캔과 잘 어울린다. 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

 

9. 빈 스코프

스프링은 다음과 같은 다양한 스코프를 지원한다.

 

- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

 

- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다. 프로토타입 빈을 관리할 책임은 빈을 받은 클라이언트에 있다. 그래서 @PreDestroy 같은 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다. 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다. 싱글톤 빈과 함께 사용 시 주의가 필요하다.

 

- 웹 관련 스코프:

  request: 웹 요청이 들어오고 나갈때 까지 유지(Log 등 사용하는 경우가 있음)

  session: 웹 세션이 생성되고 종료될 때 까지 유지

  application: 웹의 서블릿 컨텍스트와 같은 범위로 유지

 

@Scope("prototype")
@Component
public class HelloBean {}

 

9-1. Dependency Lookup (DL)

의존관계를 외부에서 주입(DI) 받는 게 아니라 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라고 한다. 프로토타입으로 지정한 빈 등 컨테이너 대신 직접 찾을 때(DL) 필요한 기능을 알아보자.

 

9-2. ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다. 참고로 과거에는 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어졌다.

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
 PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
 prototypeBean.addCount();
 int count = prototypeBean.getCount();
 return count;
}

 

9-3. JSR-330 Provider

JSR-330 자바 표준을 사용하는 방법이다. 기능이 매우 단순하고 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다. 단점은 별도의 라이브러리를 추가해야 된다는 점이다.

스프링 부트 3.0 미만에선 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.

스프링 부트 3.0 이상에선 jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가해야 한다.

@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
 PrototypeBean prototypeBean = provider.get();
 prototypeBean.addCount();
 int count = prototypeBean.getCount();
 return count;
}

 

9-4. 스코프와 Provider 또는 프록시

만약 request 스코프를 사용한다면 스프링 애플리케이션 시작부터 해당 request 스코프 빈을 찾지 못 해 오류가 발생한다. 이 문제를 해결하기 위해 앞서 나온 Provider로 request 스코프 빈의 생성을 지연시켜 해결할 수 있다. 하지만 프록시를 이용하면 더욱 간편하게 해결이 가능하다. 프록시 설정을 추가하면 CGLIB 라이브러리로 타겟 클래스의 가짜 프록시 객체를 만들어서 주입해 두고 요청이 오면 그 때 내부에서 진짜 빈을 요청하는 위임 로직이 돌아간다. 결국 두가지 방법 모두 핵심 아이디어는 진짜 객체 조회를 필요한 시점까지 지연처리 한다는 점이다. scope 기능은 유지보수가 어려워질 수 있으니 최소화해서 사용하자.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}

 

 

[참고]

스프링 핵심 원리 - 기본편 (김영한)

728x90
반응형