[Spring] 의존성 투입 DI

9 minute read

Chapter 01 들어가며

  • spring 5와 java8을 사용한다.
  • %JAVA_HOME% : C:\Program Files\Java\jdk1.8.0_251
  • 모든 스프링 모듈은 메이븐 중앙 리포지토리를 통해서 배포된다.
  • eclipse에서 jdk path를 못찾는 경우 C:\Users\nikla\eclipse\java-2020-03\eclipse\eclipse.ini의 -vm를 {JAVA_HOME}\bin으로 수정한다.
  • 프로젝트 루트에 pom.xml을 추가하고, 프로젝트 루트에서 mvn compile을 진행
    • maven 로컬 repo에서 필요한 .jar가 있는지 확인. 없으면 maven 원격 repo에서 로컬로 다운로드
    • 원격에서 다운로드할 때 필요한 libraryName.pom을 먼저 받아서 추가적으로 필요한 dependency를 함께 다운로드

commit : bdf263a

  • 아래 두 개의 폴더 모두 이클릭스 메이븐 프로젝트의 소스 폴더가 된다.
    • src\main\java : 자바 소스 코드
    • src\mainresources : 자바 소스 외의 다른 자원 파일
  • src\main\webapp : 웹 애플리케이션 기준 폴더로 사용.
    • 이 폴더를 기준으로 JSP 소스, WEB-INF\web.xml 파일 등을 작성
  • @Configuration : 해당 클래스를 스프링 설정 클래스로 지정
  • @Bean : 스프링이 객체 생성 및 초기화할 수 있도록 정의
    • 해당 메서드가 생성한 객체를 스프링이 관리하는 빈 객체로 등록하도록 함
  • 스프링의 핵심 기능은 객체를 생성하고 초기화하는 것인데 이 기능은 ApplicationContext라는 interface에 정의되어 있다.
  • AnnnotationConfigApplicationContext 클래스는 이 인터페이스를 구현한 클래스로, 자바 클래스에서 정보를 읽어와 객체 생성과 초기화를 수행한다.
  • xml 파일이그루비 설정 코드에서 객체 정보를 읽어와서 생성, 초기화를 하는 클래스도 있다.
  • ApplicatinContext 인터페이스를 구현한 어떤 class를 사용하든, 각 구현 클래스는 설정 정보로부터 Bean이라고 불리는 객체를 생성하고 그 객체를 내부에 보관한다. 그리고 getBean() 메서드를 실행하면 해당하는 빈 객체를 제공한다.
  • greeter의 예제(commit : bdf263a)에서는 ctx 객체를 생성하는 과정이 설정 정보를 이용해서 Bean 객체를 생성하는 과정이고 getBean()을 호출할 때 bean 객체를 제공하는 시점이다.
  • ApplicationContext는 빈 객체의 생성,초기화,보관, 제거 등을 관리하고 있기 때문에 Container라고도 불린다.
    • 이 때 container는 chap02.Greeter 객체 정보에 대한 포인터를 가지고 있다.

commit : 1dba1fe

별도의 설정을 하지 않은 경우 스프링은 한 개의 빈 객체만을 생성한다. 이 때 빈 객체는 singleton 범위를 갖는다. 싱글톤은 단일 객체를 의미한다. 스프링은 기본적으로 한 개의 @Bean annotation에 대해서 한 개의 빈 객체를 생성한다.

Chapter 03 스프링 DI

의존 객체를 전달하는 방법으로서의 DI

DI는 dependency injection의 약자로 우리말로는 의존 주입이라고 번역한다. 여기서의 의존은 객체 간의 의존을 의미한다. class A의 객체가 class B의 객체를 사용할 때 A의 내부에서 직접 B를 생성하는 것은 유지보수 관점에서 문제가 있다.

객체를 생성하고, 생성한 객체에 의존성을 주입하는 객체 조립기

DI는 의존 객체를 직접 생성하는 대신 의존 객체를 전달받는 방식을 사용한다. 그런데 전달할 의존 객체가 많아지거나 전달 받은 의존 객체를 사용해서 다른 객체를 연쇄적으로 생성하는 경우 객체 조립기의 사용을 고려해볼 수 있다. 객체 조립기는 객체를 자신 내부에서 직접 생성하고, 여러 객체들의 의존 객체를 주입하는 기능을 제공한다. 그리고 특정 객체가 필요한 곳에 getter를 통해서 제공한다. 의존 객체를 전달하기 위해서는 이단 의존 객체를 사용할 객체의 밖에서 생성해야하는데, 이 생성 과정이 복잡하지만 반복적으로 일어나는 경우 객체 조립기를 사용한다.

스프링은 DI를 지원하는 조립기이다. 스프링은 조립기처럼 객체를 생성하고, 생성한 객체에 의존성을 주입한다. 조립기는 정의한 특정 클래스에 한정되지만, 스프링은 annotaion을 이용한 범용 조립기이다.

객체 조립기의 역할을 범용적으로 수행하는 ApplicationContext

Assembler는 직접 객체를 생성하는 반면에 AnnotationConfigApplicationContext는 설정 파일(AppCtx 클래스)로부터 생성할 객체와 의존 주입 대상을 정한다.

DI의 방법 두 가지

  1. 생성자에 parameter로 의존객체들을 전달한다.
    • 빈 객체를 생성하는 시점에 모든 의존 객체가 주입된다.
    • 단점 : parameter가 많은 경우 생성자의 코드를 확인해야 한다.
  2. 객체 생성 후 setter로 의존 객체들을 설정한다.
    • 세터 메서드 이름을 통해 어떤 의존 객체가 주입되는지 알 수 있다.
    • 단점 : 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에 객체를 사용하는 시점에 NullPointerEception이 발생할 수 있다.

@Configuration 설정 클래스의 @Bean 설정과 싱글톤

스프링은 설정 클래스(Appctx.java)를 그대로 사용하지 않는다. 대신 설정 클래스를 상속한 새로운 설정 클래스를 만들어서 사용한다. 새로운 설정 클래스는 직접 작성한 설정 클래스에 @Bean으로 등록된 Class의 객체의 생성을 관리한다. ctx.getBean("changePwdSvc", ChangePasswordService.class);는 ctx 안에 있는 ChangePasswordService의 객체를 Map에서 조회하고 존재한다면 그 객체를 return 해준다고 생각할 수 있다. 따라서 싱글톤 형태로 객체레 접근할 수 있다.

두 개 이상의 설정 파일 사용하기

@Bean을 등록해야하는 클래스가 많아지는 경우 설정 파일을 분할한다. 이 때 서로 다른 설정 파일에 있는 Bean을 사용하는 경우 @Autowired를 사용한다. Autowired는 해당 타입의 빈을 찾아서 필드에 할당한다. AppConf1.java에 저장된 Bean을 사용하는 AppConf2.java의 예이다.

@Configuration
public class AppConf2 {
	@Autowired
	private MemberDao memberDao;
	@Autowired
	private MemberPrinter memberPrinter;

  ...
}

추가적으로, Autowired는 스프링 빈에 의존하는 다른 빈을 자동으로 주입하고 싶을 때 사용한다. 따라서 Autowired를 붙히면 아래처럼 설정 클래스의 @Bean 메서드에 의존 주입을 위한 코드를 작성하지 않아도 된다.

//MemberInfoPrinter.java
public class MemberInfoPrinter {
  @Autowired
	private MemberDao memDao;
  @Autowired
	private MemberPrinter printer;
  ...
}

//AppConf2.java

@Configuration
public class AppConf2 {
	@Autowired
	private MemberDao memberDao;
	@Autowired
	private MemberPrinter memberPrinter;

  @Bean
	public MemberInfoPrinter infoPrinter() {
		MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
		// infoPrinter.setMemberDao(memberDao); 추가하지 않아도 memberDao가 autowired로 설정되어 있어서 의존 투입이 된다.
		// infoPrinter.setPrinter(memberPrinter); 추가하지 않아도 memberDao가 autowired로 설정되어 있어서 의존 투입이 된다.
		return infoPrinter;
	}
}

설정 파일을 전달할 때는 여러 개를 모두 적으면 된다.

ctx = new AnnotationConfigApplicationContext(AppConf1.class, AppConf2.class);

AppConf1.java에서 @Configuration 다음에 @import(AppConf2.class)를 하면 아래와 같이 하나의 설정 파일만 전달해도 같은 효과를 낸다.

ctx = new AnnotationConfigApplicationContext(AppConf1.class);

아래와 같이 여러 개의 설정 클래스를 지정할 수도 있다.


@Configuration
@Import( { AppConf1.class, AppConf2.class })
public class AppConfImport{

}

주입 대상 객체를 모두 빈 객체로 설정해야 하는가

빈이 아닌 직접 생성한 객체를 설정 파일에서 프로퍼티로 가지고 있어도 된다. 객체를 스프링 빈으로 등록할 때와 등록하지 않을 때의 차이는 스프링 컨테이너가 객체를 관리하는지 여부이다. DI와 라이프사이클 관리 등 다양한 기능은 빈으로 등록한 객체에만 적용할 수 있다. 일반적으로는 빈으로 등록한다.

@Autowired annotation 사용법 두 가지

  1. 의존 객체를 사용하는 클래스에서 의존 객체 property를 @Autowired로 설정
  2. 의존 객체를 사용하는 클래스에서 의존 객체를 사용하는 setter method를 @Autowired로 설정

@Autowired에 의해서 다수의 Bean이 발견된 경우, @Qualifier

자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 수 있는 방법이 필요하다. 이 때 @Qualifier를 사용한다. 설정파일(AppCtx.java)에서 같은 타입의 bean이 등록된 경우, DI 과정에서 특정 bean을 선택할 수 없게 된다. 따라서 설정파일과 bean을 DI 받아서 사용하는 method(setter)에서 @Qualifier(“uniqueName”)을 사용하면 된다.

default @Qualifier

@Qualifier가 없으면 빈의 이름이 한정자가 된다. public MemberInfoPrinter infoPrinter(){…}의 경우 한정자는 infoPrinter가 된다. 같은 MemberInfoPrinter 타입을 가지는 bean이 여러개라도 infoPrinter2, infoPrinter3와 같이 메서드의 이름은 다르기 때문에 setter 부분에서만 @Qualifier를 사용해도 구분할 수 있다.

상속 관계에서의 @Qualifier

appCtx.java에는 classA 타입의 Bean과 ClassB 타입의 Bean이 모두 등록되어 있다고 가정하자. 그런데 ClassB는 ClassA를 상속받은 클래스이다. 이때 DI 과정에서 ClassA의 자동 주입이 일어나면 ClassA와 ClassB모두 ClassA 타입이 될 수 있기 때문에 어떤 Bean을 등록할지 모르는 상태가 된다. 이런 경우에도 @Qualifier를 사용해서 Bean을 특정할 수 있다.

@Autowired 필수 지정

  1. 첫 번째 방법 @Autowired(required=false)
    • @Autowired는 해당하는 Bean이 없는 경우 익셉션을 발생시킨다. 만약 의존 Bean을 DI 받아서 사용하는 메서드가 Bean이 없는 경우까지 커버할 수 있도록 구성되어 있으면 @Autowired가 bean이 없을 때 익셉션을 발생하는 것이 불필요할 수 있다. 이럴 때는 @Autowired를 하지만 Bean이 없을 때는 DI를 실행하지 않아서 익셉션을 발생하지 않도록 할 수 있다.
  2. 두 번째 방법 Optional optObj
      @Autowired
      public void setDateFormatter(Optional<DataTimeFormatter> formatterOpt){
     if(forammterOpt.isPresent()){
       this.dateTimeFormatter = formatterOpt.get();
     }else{
       this.dateTimeFormatter = null;
       //물론 null에 대한 처리가 추가되어야 한다.
     }
      }
    
  3. 세 번째 방법 @Nullable
    • @Autowired를 붙인 setter에서 @Nullable을 의존 주입 파라미터에 붙히면 스프링 컨테이너는 setter를 호출할 때 빈이 존재하면 전달하고, 존재하지 않으면 Null을 전달한다.
        @Autowired
        public void setDateFormatter(@Nullable DataTimeFormatter dateTimeFormatter){
         this.dateTimeFormatter = null;
         //물론 null에 대한 처리가 추가되어야 한다.
       }
        }
      

직접 DI를 하는 경우와 @Autowired 하는 경우가 겹칠 때

ClassA를 상속받은 ClassB에 대해서, 직접 ClassA를 setter를 통해서 DI를 진행하고 ClassB는 @Autowired를 통해서 DI가 진행될 때 결과적으로는 ClassB가 DI된다. 직접 전달하는 것보다 Autowired를 통해서 DI하는 것이 더 우선된다.

Chapter 05 컴포넌트 스캔

DI와 함께 사용하는 추가 기능이 컴포넌트 스캔이다. 컴포넌트 스캔은 스프링이 직접 클래스를 검색해서 빈으로 등록해주는 기능이다. 설정 클래스에 빈으로 등록하지 않아도 원하는 클래스를 빈으로 등록할 수 있다. 이를 사용하면 설정 코드가 크게 줄어든다.

@Component로 스캔 대상 지정

스프링이 검색해서 빈으로 등록할 수 있도록 하려면 클래스에 @Component를 붙인다. @Component는 해당 클래스를 스캔 대상으로 표시한다. 클래스 파일에 붙히면 자동으로 설정 파일에 @Bean으로 등록하는 것과 같은 효과를 가진다.

@Component(“listPrinter”)

default로는 클래스 이름의 첫 글자만 소문자로 바꾼 빈 이름을 사용한다. 이름을 지정하고 싶을 때는 (““)를 사용해서 지정한다. 스캔을 통해서 빈이 등록된 경우 ctx.getBean(객체이름, 객체타입)에 오류가 발생할 수 있으니 ctx.getBean(객체타입)으로 변경한다. 같은 객체 타입의 빈이 여러개 있는 경우 @Qualifier를 사용한다.

@ComponentScan(basePackages = {“spring”}[,excludeFilters])

클래스 파일에서 @Component를 명시한 빈들을 설정 파일이 찾기 위해서는 설정 파일에 @ComponentScan을 추가해야 한다. basePackages를 “spring”으로 설정하면 spring package와 하위 package에서 @Component를 찾아서 빈으로 등록한다.

excludeFileters = @Filter(type FilterType.REGEX, pattern = “spring\..*Dao”)

정규표현식을 사용해서 제외 대상을 지정한다. pattern = “spring\..*Dao”는 spring.으로 시작하고 Dao로 끝나는 정규표현식으로 spring.MemberDao는 스캔 대상에서 제외한다.

AspectJ 패턴을 이용한 excludeFilters

excludeFileters = @Filter(type FilterType.ASPECTJ, pattern = “spring.*Dao”)를 사용한다. 대신 pom.xml에서 aspectjweaver 모듈을 추가해야 한다.

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver></artifactId>
  <version>1.8.13</version>
</dependency>

특정 @Annotation을 제외하는 excludeFilters

@Retention(RUNTIME)
@Target(TYPE)
public @interface NoProduct{

}

@Retention(RUNTIME)
@Target(TYPE)
public @interface ManualBean{

}
////
@ManualBean
@Component
public class MeberDao{

}
///
@ComponentScan(basePackages = {"spring"},
excludeFileters = @Filter(type FilterType.ANNOTATION,
classes = {NoProduct.class, ManualBean.Class}))
public class AppCtxWithExclude{
  @Bean
  public MemberDao memberDao(){
    return new MemberDao();
  }
  ...
}

특정 타입을 스캔에서 제외하기

///
@ComponentScan(basePackages = {"spring"},
excludeFileters = @Filter(type FilterType.ASSIGNABLE_TYPE,
classes = {MemberDao.class}))
public class AppCtxWithExclude{
  @Bean
  public MemberDao memberDao(){
    return new MemberDao();
  }
  ...
}

두개 이상의 필터를 사용해서 스캔하기

///
@ComponentScan(basePackages = {"spring"},
excludeFileters = {
  @Filter(type FilterType.ASSIGNABLE_TYPE, classes = {MemberDao.class}),
  @Filter(type FilterType.ANNOTATION, classes = {NoProduct.class, ManualBean.Class}))
public class AppCtxWithExclude{
  @Bean
  public MemberDao memberDao(){
    return new MemberDao();
  }
  ...
}

스캔 대상이 되는 @Annotation들

  1. @Component
  2. @Controller
    • @Component annotation에 또 다른 annotation을 붙힌 형태
  3. @Service
  4. @Repository
  5. @Aspect
  6. @Configuration

스캔 충돌

  1. @ComponentScan(basePackages = {“spring”, “spring2”}에서 두 개의 패키지 안에 같은 이름의 빈이 @Component를 가지고 있는 경우 충돌한다. 이 때는 명시적으로 다른 이름을 지정해야 한다.
  2. 수동 등록한 빈과 충돌 : 수동 등록한 빈이 우선한다.

Chapter 06 빈 라이프사이클과 범위

컨테이너 초기화와 종료

  1. 컨테이너 초기화
    • 초기화 시점에서 설정 클래스에서 정보를 읽어와 알맞은 빈 객체르 생성하고 각 빈을 DI하는 작업을 수행한다.
  2. 컨테이너에서 빈 객체를 구해서 사용
  3. 컨테이너 종료

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);
Greeter g = ctx.getBean("greeter", Greeter.class);
String msg = g.greet("스프링")";
ctx.close();

스프링 빈 객체의 라이프사이클

스프링 컨테이너가 관리하는 빈 객체의 라이프 사이클은 다음과 같다.

  1. 객체 생성
  2. 의존 설절
  3. 초기화
  4. 소멸

스프링 컨테이너를 초기화 할 때 가장 먼저 빈 객체를 생성하고 의존을 설정한다.

초기화, 파괴 interface InitializingBean, DisposableBean

스프링 컨테이너는 빈 객체를 초기화하고 설명하기 위해서 빈 객체의 지정한 메서드를 호출한다. 스프링은 다음의 두 인터페이스에 이 메서드를 정의하고 있다.

//org.springframework.beans.factory.InitializingBean
public interface InitializingBean{
  void afterPropertiesSet() throws Exception;
}
//org.springframework.beans.factory.DisposableBean
public interface DisposableBean{
  void destroy() throws Exception;
}

빈 객체가 위의 두 인터페이스를 구현하면 스플이 컨테이너는 빈 객체의 초기화 과정에서 afterPropertiesSet()를 실행한다. 빈 객체의 소멸 과정에서는 destroy()를 호출한다.

초기화와 소멸 과정이 필요한 예가 데이터베이스 커넥션 풀이다. 커넥션 풀을 위한 빈 객체는 초기화 과정에서 데이터베이스 연결을 생성한다. 컨테이너를 사용하는 동안 연결을 유지하고 빈 객체를 소멸할 때 사용중인 데이터베이스 연결을 끊어야 한다. 스프링 컨테이너를 초기호할 때 여러 빈 객체의 생성 - 의존 설정 - 초기화가 이루어진다. 이 때 각 빈 객체가 위의 interface를 상속했다면 각 빈 객체가 interface를 구현한 방식에 따라 afterPropertiesSet()가 호출된다.

또 다른 예는 채팅 클라이언트가 있다. 채팅 클라이언트는 시작할 때 서버와 연결을 생성하고 종료할 때 연결을 끊는다. 이때 서버와의 연결을 생성하고 끊는 작업을 초기화 시점과 소멸 시점에 수행하면 된다.

RTMP Relay의 경우는 이 방식이 그대로 적용될 수 있는지는 아직 잘 모르겠다. publisher의 요청에 따라서 그 떄 그 때 media server와 연결이 되어야 하는데, 위 interface는 빈 객체가 초기화되는 과정에서 afterPropertiesSet()을 호출한다고 한다. 채팅 클라이언트가 서버와 연결을 생성하고 종료할 때 연결을 끊는 방법에 대한 깊은 이해가 먼저 선행되어야 한다.

  1. Main.java : AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
  2. AppCtx.java : 등록된 Bean의 객체 생성 및 setter 실행(properties 설정)
  3. Client.java : afterPropertiseSet() 호출
  4. Main.java : ctx.getBean() 호출

@Bean(initMethod = “connect”, destroyMethod = “close”)

직접 구현한 클래스가 아닌 외부에서 제공받은 클래스를 스프링 빈 객체로 설정하고 싶을 때도 있다. 이 경우 소스 코드를 받지 않았다면 두 인터페이스를 구현하도록 수정할 수 없다. 인터페이스를 구현할 수 없거나, 사용하고 싶지 않은 경우에는 직접 메서드를 지정할 수 있다.

이 때 initMethod와 destoryMethod는 parameter를 받을 수 없다.

싱글톤이 아닌 프로토타입의 범위의 빈 설정

  • default : 싱글톤 : @Scope(“singleton”)
  • 프로토타입 : @Scope(“prototype”)

프로토타입의 빈은 완전한 라이프사이클을 따르지 않는다. 프로토타입의 빈 객체를 생성하고 프로퍼티를 설정하고 초기화하는 작업까지는 수행하지만, 컨테이너를 종료한다고 해서 생성한 프로토타입 빈 객체의 소멸 메서드를 실행하지는 않는다. 따라서 프로토 타입의 범위의 빈을 사용할 때는 빈 객체의 소멸 처리 코드를 직접 해야한다.


@Configuration
public class AppCtx {

	@Bean
	@Scope("prototype")
	public Client client() {
		Client client = new Client();
		client.setHost("host");
		return client;
	}
}

다수의 aspect가 적용되는 순서

@Aspect에서 @Order(n)으로 먼저 적용될 순서를 지정한다.


@Aspect
@Order(1)
public class CacheAspect {
 ...
}

@Aspect
@Order(2)
public class ExeTimeAspect {
 ...
}