이 글은 “토비의 스프링” 책 내용을 정리한 글입니다.

만약 저작권 관련 문제가 있다면 “gunjuko92@gmail.com”로 메일을 보내주시면, 바로 삭제하도록 하겠습니다.

토비의 스프링 - DI

스프링 3.1의 DI

스프링이 처음 등장한 이후 많은 변화를 겪은 것은 사실이지만 스프링이 근본적으로 지지하는 객체지향 언어인 자바의 특징과 장점을 극대화하는 프로그래밍 스타일과 이를 지원하는 도구로서의 스프링 정체성은 변하지 않았다.

많은 변화 속에서 스프링이 꾸준히 호환성을 지키고 안정된 기술을 유지해올 수 있었던 것은 스프링이 지지하고 지원하는 객체지향적인 코드의 장점인 유연성과 확장성을 스프링 스스로가 충실하게 지켜왔기 때문이다.

자바 언어의 변화와 스프링

애노테이션의 메타정보 활용

  • 자바 코드가 실행되는 것이 목적이 아니라 다른 자바 코드에 의해 데이터처럼 취급되기도 한다. 자바 코드의 일부를 리플렉션 API 등을 이용해 어떻게 만들었는지 살펴보고 그에 따라 동작하는 기능이 점점 많이 사용되고 있다.
  • 애노테이션은 옵션에 따라 컴파일된 클래스에 존재하거나 애플리케이션이 동작할 때 메모리에 로딩되기도 하지만 자바 코드가 실행되는데 직접 참여하지 못한다.
  • 애노태이션 자체가 클래스의 타입에 영향을 주지도 못하고, 일반 코드에서 활용되지도 못하기 때문에 일반적인 객체지향 프로그래밍 스타일의 코드나 패턴 등에 적용할 수도 없다. 복잡한 리플렉션 API를 이용해 애노태이션의 메타정보를 조회하고, 애노태에션 내에 설정된 값을 가져와 참고하는 방법이 전부다.
  • 스프링은 애노태이션의 적극적인 활용에 앞장 서왔다. 애노테이션은 프레임워크가 참조하는 메타정보로 사용되기에 여러 가지 유리한 점이 많다.
  • 애노테이션은 메타정보로서 활용되는 데는 XML에 비해 유리한 점이 많다.
  • 단순한 애노태이션 하나를 자바 코드에 넣는 것만으로도, 애노테이션을 참고하는 코드에서는 다양한 부가 정보를 얻어낼 수 있다. 동일한 정보를 XML로 표현하려면 모든 내용을 명시적으로 나타내야 한다. 간단히 클래스가 무엇인지만 지정하려고 할 때 XML을 이용하는 방식과 애노테이션을 이용하는 방식을 살펴보면, 애노테이션을 이용하는 방식이 훨씬 간단하다.
<x:special target="type" class="com.mycompany.myproject.MyClass" />
@Special
public class MyClass {
	
}
  • XML은 어느 환경에서나 손쉽게 편집이 가능하고, 내용을 변경하더라도 다시 빌드를 거칠 필요가 없다. 반면에 애노태이션은 자바 코드에 존재하므로 변경할 때마다 매번 클래스를 새로 컴파일해줘야 한다.

정책과 관례를 이용한 프로그래밍

  • 애노테이션 같은 메타정보를 활용하는 프로그래밍 방식은 코드를 이용해 명시적으로 동작 내용을 기술하는 대신 코드 없이도 미리 약속한 규칙 또는 관례를 따라서 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극적으로 포용하게 만들어왔다.
  • 애노태에션 덕분에 코드는 간결해진다. 하지만 정책을 기억 못하거나 잘못 알고 있을 경우 의도한 대로 동작하지 않는 코드가 만들어질 수 있다.

1. 자바 코드를 이용한 빈 설정

  • DI 설정정보를 담은 클래스는 평범한 자바 클래스에 @Configuration 애노태이션을 달아주면 만들 수 있다.
  • 자바 클래스로 만들어진 DI 설정정보에서 XML의 설정정보를 가져오게 만들 수 있다. 아래와 같이 @ImportResource 애노테이션을 이용하면 된다.
@Configuration
@ImportResourcece("/test-applicationContext.xml")
public class TestApplicationContext {
	
}
  • TestApplicationContext에 자바 코드와 애노테이션으로 정의된 Dl 정보와 @ImportResource로 가져온 XML Dl 정보가 합쳐져서 최종 DI 설정정보로 통합된다.
  • 스프링 컨테이너의 구현도 여러 가지다. XML에 담긴 DI 정보를 이용하는 스프링 컨테이너를 사용하는 경우에는 @PostConstruct와 같은 애노테이션의 기능이 필요하면 반드시 을 포함시켜서 필요한 빈 후처리기가 등록되게 만들어야 한다. 반면에 @Configuration이 붙은 설정 클래스를 사용하는 컨테이너가 사용되면 더 이상 을 넣을 필요가 없다. 컨테이너가 직접 @PostConstruct 애노테이션을 처리하는 빈 후처리기를 등록해주기 때문이다.
  • <bean>으로 정의된 DI 정보는 자바 코드, 특별히 @Bean이 붙은 메소드와 거의 1:1로 매핑된다. @Bean은@Configuration이 붙은 DI 설정용 클래스에서 주로 사용되는 것으로 메소드를 이용해서 빈 오브젝트의 생성과 의존관계 주입을 직접 자바 코드로 작성할 수 있게 해준다.
  • 메소드 이름은 <bean>의 id 값으로 한다. 메소드의 리턴 값 타입은 조금 신중하게 결정해야 한다. 많은 경우에 class 애트리뷰트의 클래스를 그대로 사용해도 상관없지만 정확히 하려면 빈을 주입받아서 사용하는 다른 빈이 어떤 타입으로 이 빈의 존재를 알고 있는지 확인할 필요가 있다.
  • @Configuration 자바 클래스에서 정의한 빈과 XML에서 정의한 빈은 얼마든지 서로 참조가 가능하다.
  • 스프링의 <bean>에 넣는 클래스는 굳이 public이 아니어도 된다. 내부적으로 리플렉션 API를 이용하기 때문에 private으로 접근을 제한해도 빈의 클래스로 사용할 수 있다.
  • @Autowired가 붙은 필드의 타입과 같은 빈이 있으면 해당 빈을 필드에 자동으로 넣어준다. 빈 클래스 내부에서 수정자 주입을 대신해서 필드 주입으로 사용할 수도 있고, @Configuration 클래스에서 XML 등으로 정의된 빈을 가져올 때도 사용할 수 있다. 또한 @Configuration 클래스를 하나 이상 사용하는 경우 다른 클래스의 @Bean 메소드로 정의된 빈을 참조할 때도 사용할 수 있다.
  • @Resource는 @Autowired와 유사하게 필드에 빈을 주입받을 때 사용한다. 차이점은 @Autowired는 필드의 타입을 기준으로 빈을 찾고 @Resource는 필드 이름을 기준으로 한다는 점이다.
  • 트랜잭션 AOP를 적용하려면 제법 복잡하고 많은 빈이 동원돼야 한다. AOP를 위해 기본적으로 어드바이스와 포인트컷이 필요하고 애노테이션 정보에서 트랜잭션 속성을 가져와서 어드바이스에서 사용하게 해주는 기능도 필요하다. 은 옵션을 주지 않는다면 기본적으로 다음 네 가지 클래스를 빈으로 등록해준다.
    • InfrastructureAdvisorAutoProxyCreator
    • AnnotationTransactionAttributeSource
    • TransactionInterceptor
    • BeanFactoryTransactionAttributeSourceAdvisor
  • 과 같이 특별한 목적을 위해 만들어진, 내부적으로 복잡한 로우 레벨의 빈을 등록해주는 전용 태그에 대응되는 애노테이션을 제공해준다. 은 @EnableTransactionManagement 애노테이션으로 대체할 수 있다.

  • 스프링 3.1은 XML에서 자주 사용되는 전용 태그를 @Enable로 시작하는 애노테이션으로 대체할 수 있게 다양한 애노태이션을 제공한다.

2. 빈 스캐닝과 자동 와이어링

@Autowired를 이용한 자동와이어링

  • @Autowired는 자동와이어링 기법을 이용해서 조건에 맞는 빈을 찾아 자동으로 수정자 메소드나 필드에 넣어준다. 자동와이어링을 이용하면 컨테이너가 이름이나 타입을 기준으로 주입될 빈을 찾아주기 때문에 빈의 프로퍼티 설정을 직접해주는 자바 코드나 XML의 양을 대폭 줄일 수 있다. @Autowired는 아래와 같이 사용이 가능하다.
public class UserDaoJdbc implements UserDao {
    
	// 필드를 통해 빈 주입
    @Autowired
    private SqlService sqlService;

    // 수정자 메소드를 통해 빈 주입
    @Autowired
	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate =new JdbcTemplate(dataSource);
   }
    
   // ...
}
  • @Autowired가 붙은 수정자 메소드가 있으면 파라미터 타입을 보고 주입 가능한 타입의 빈을 모두 찾는다.
    • 주입 가능한 타입의 빈이 하나라면 스프링이 수정자 메소드를 호출해서 넣어준다. 만약 두 개 이상이 나오면 그중에서 프로퍼티와 동일한 이름의 빈이 있는지 찾는다. 만약 타입과 이름을 모두 비교해도 최종 후보를 찾아내지 못하면 주입할 빈을 찾을 수 없다는 에러가 날 것이다.
  • 비즈니스 로직을 갖고 있어서 목 오브젝트를 적절히 활용해 빠르게 동작하는 단위 테스트를 만들 수 있는 클래스는 @Autowired를 이용하더라도 수정자 메소드를 남겨두는 편이 안전하다.
  • @Autowired와 같은 자동와이어링은 적절히 사용하면 DI 관련 코드를 대폭 줄일 수 있어서 편리하다. 반면에 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져 있는지 한 눈에 파악하기 힘들다는 단점도 있긴 하다.

@Component를 이용한 자동 빈 등록

  • @Component는 클래스에 부여된다. @Component가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록된다. 정확히는 @Component 또는 @Component를 메타 애노테이션으로 갖고 있는 애노테이션이 붙은 클래스가 자동 빈 등록 대상이 된다.
    • 빈의 아이디는 따로 지정하지 않았으면 클래스 이름의 첫 글자를 소문자로 바꿔서 사용한다.
    • @Component가 붙은 클래스의 이름 대신 다른 이름을 빈의 아이디로 사용하고 싶다면, @Component(“userDao”) 같이 애노테이션에 이름을 넣어주면 된다.
  • @Component 애노태이션이 달린 클래스를 자동으로 찾아서 빈을 등록해주게 하려면 빈 스캔 기능을 사용하겠다는 애노테이션 정의가 필요하다. @ComponentScan는 특정 패키지를 기준으로해서 빈으로 등록될 클래스가 있는지를 찾는다.
@ComponentScan(basePackages = "springbook.user")
@Configuration
public class TestApplicationContext {


}
  • @ComponentScan의 basePackages 엘리먼트는 @Component가 붙은 클래스를 스캔할 기준 패키지를 지정할 때 사용한다. 기준 패키지는 여러 개 넣어도 된다. 지정한 패키지 아래의 모든 서브패키지를 다 검색한다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	String value() default "";

}
  • 빈 자동등록에 @Component 애노테이션만 사용할 수 있는 것은 아니고 @Component 애노테이션을 메타 애노테이션으로 갖고 있는 애노태이션도 시용할 수 있다.
  • 스프링은 @Component 외의 애노테이션으로도 자동 빈 등록이 가능하게 해준다. 단지 빈 스캔 검색 대상으로 만드는 것 외에 부가적인 용도의 마커로 사용하게 하기 위해서다.
  • 여러 개의 애노태이션에 공통적인 속성을 부여하려면 메타 애노테이션을 이용한다. 메타 애노테이션은 애노태이션의 정의에 부여된 애노테이션을 말한다. 애노태이션이 빈 스캔을 통해 자동등록 대상으로 인식되게 하려면 애노테이션 정의에 @Component를 메타 애노테이션으로 붙여주면 된다.
  • SNS 서비스에 접속하는 기능을 제공하는 빈을 AOP 포인트컷으로 지정할 수 있도록 구분이 필요하다면 @SnsConnector 라는 애노태이션을 하나 만들어 사용할 수 있을 것이다. @SnsConnector 애노태이션을 정의할 때 메타 애노테이션으로 @Component를 부여해주면 클래스마다 @Component를 따로 붙여주지 않아도 자동 빈 등록대상으로 만들 수. 있다.
// @SnsConnector를 클래스에 부여해주면 자동 빈 등록 대상이 된다.
@Component
public @interface SnsConnector {
	// ...
}
  • 스프링이 제공하는 애노테이션
    • @Repository : DAO 빈을 자동등록 대상으로 만들 때 사용
    • @Service : 비즈니스 로직을 담고 있는 서비스 계층의 빈을 구분하기 위해 사용된다. 서비스 계층은 트랜잭션 경계가 되는 곳이라 @Transactional이 함께 사용되는 경우가많다.
    • @Controller : 컨트롤러 역할을 하는 빈을 등록할 때 사용. Dispatcher는 @Controller 주석 된 클래스를 검색하고 그 안에 @RequestMapping 애노테이션을 감지한다. @Controller 애노테이션이 달린 클래스에서만 @RequestMapping 을 사용할 수 있다.
    • @Repository, @Service, @Controller는 @Component를 메타 애노테이션으로 갖고 있다.

@Repository 애노테이션이 붙은 빈들은 실행 도중에 Resource 관련 예외가 발생하면, PersistenceExceptionTranslationAdvisor에 의해 예외가 DataAccessException으로 변환된다.

3. 컨텍스트 분리와 @Import

  • DI 정보를 담은 설정 클래스도 성격이 다르고 변경 이유와 주기가 다르다면 분리하는게 좋다.

@Import

  • 설정 정보를 분리하더라도, 각 설정 클래스들을 긴밀하게 연결해주는게 좋을 경우가 많다.
@Configuration
@Import(SqlServiceContext.class)
public class AppContext {
	
}
  • 자바 클래스로 된 설정정보를 가져올 때는 @ImportResource 대신 @Import를 이용한다.
  • AppContext가 메인 설정정보가 되고, SqlServiceContext는 AppContext에 포함되는 보조 설정정보로 사용한다.

4. Profile

  • 같은 타입이면서 아이디도 같은 두 개의 빈이 있으면 스프링이 빈 정보를 읽는 순서에 따라 뒤의 빈 설정이 앞에서 발견된 빈 설정에 우선해서 적용된다.
  • 테스트환경과 운영환경에서 각기 다른 빈 정의가 필요한 경우가 종종 있다.

@Profile과 ActiveProfiles

  • 실행환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행 시점에 어떤 프로파일의 빈 설정을 사용할지 지정하는 기능을 제공한다.
  • 프로파일은 설정 클래스 단위로 지정한다. 다음과 같이 @Profile 애노테이션을 클래스 레벨에 부여하고 프로파일 이름을 넣어주면 된다.
@Configuration
@Profile("test")
public class TestAppContext {

}
  • 프로파일이 지정되어 있지 않은 빈 설정은 default 프로파일로 취급한다. 이름 그대로 디폴트 빈 설정정보로 취급되어 항상 적용된다.
  • @Profile이 붙은 설정 클래스는 @Import로 가져오든 @ContextConfiguration에 직접 명시하든 상관없이 현재 컨테이너의 활성 active 프로파일 목록에 자신의 프로파일 이름이 들어 있지 않으면 무시된다.
  • 활성 프로파일이란 스프링 컨테이너를 실행할 때 추가로 지정해주는 속성이다.
  • 프로파일을 지정하려면 @ActiveProfiles 애노태이션을 사용하면 된다.

컨테이너의 빈 등록 정보 확인

  • 스프링 컨테이너는 모두 BeanFactory라는 인터페이스를 구현하고 있다.
  • BeanFactory의 구현 클래스 중에 DefaultListableBeanFactory가 있는데 거의 대부분의 스프링 컨테이너는 이 클래스를 이용해 빈을 등록하고 관리한다. 스프링은 친절하게도 DefaultListableBeanFactory 오브젝트를 @Autowired로 주입받아서 이용하게 해준다.
  • DefaultListableBeanFactory에는 getBeanDefinitionNames() 메소드가 있어서 컨테이너에 등록된 모든 빈 이름을 가져올 수 있고 빈 이름을 이용해서 실제 빈과 빈 클래스 정보 등도 조회 해 볼 수 있다.

중첩 클래스를 이용한 프로파일 적용

  • 아래와 같이 static 중첩 클래스를 이용해서 프로파일을 적용할 수도 있다. 설정 파일을 분리하지 않았기 때문에 한눈에 빈 설정 정보를 확인할 수 있다.
@Configuration
public class AppContext {
	
    @Configuration
    @Profile("test")
    public static class TestAppContext {
    
    }
    
    @Configuration
    @Profile("production")
    public static class ProductionAppContext {
    
    }
}
  • 스태틱 중첩 클래스로 넣은 @Configuration 클래스는 스프링이 자동으로 포함해주기 때문에 @Import에 TestAppContext, ProductionAppContext를 포함시킬 필요가 없다.

5. 프로퍼티 소스

  • DB 연결정보는 환경에 따라 다르게 설정될 수 있어야 한다. 또한 같은 종류의 환경이더라도 필요에 따라 손쉽게 변경할수 있으면 좋겠다. 그래서 이런 외부 서비스 연결에 필요한 정보는 자바 클래스에서 제거하고 손쉽게 편집할 수 있고 빌드 작업이 따로 필요 없는 XML이나 프로퍼티 파일 같은 텍스트 파일에 저장 해두는편이 낫다.

@PropertySource

  • 프로퍼티 파일의 확장자는 보통 properties이고, 내부에 키=값 형태로 프로퍼티를 정의한다.
  • 스프링은 빈 설정 작업에 필요한 프로퍼티 정보를 컨테이너가 관리하고 제공해준다. 스프링 컨테이너가 지정된 정보 소스로부터 프로퍼티 값을 수집하고, 이를 빈 설정 작업 중에 사용할 수 있게 해준다. 컨테이너가 프로퍼티 값을 가져오는 대상을 프로 퍼티 소스라고 한다.
  • 환경 변수나 시스템 프로퍼티처럼 디폴트로 프로퍼티 정보를 끌어오는 프로퍼티 소스도 있고 프로퍼티 파일이나 리소스의 위치를 지정해서 사용되는 프로퍼티 소스도 있다.
  • 특정 파일을 프로퍼티 소스로 등록하고 싶은 경우에는 @PropertySource 애노테이션을 사용하면 된다.
@Configuration
@PropertySource("/database.properties")
public class AppContext {

}
  • @PropertySource로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장된다. 환경 오브젝트는 빈처럼 @Autowired를 통해 필드로 주입받을 수 있다.
  • Environment 오브젝트의 getProperty() 메소드는 프로퍼티 이름을 파라미터로 받아 스트링 타입의 프로퍼티 값을 돌려준다.

환경에 따라 달라지는 빈 설정은 활성 프로파일을 변경해주면 되고, DB 연결정보와 같이 환경에 따라 달라지는 정보는 프로퍼티 파일 내용을수정하기만 하면 된다.

PropertySourcesPlaceholderConfigurer

  • Environment 오브젝트 대신 프로퍼티 값을 직접 DI 받는 방법도 가능하다. @Value 애노태이션을 이용하면 된다.
  • @Value의 사용 방법은 여러 가지가 있는데, 대표적으로 프로퍼티 소스로부터 값을 주입받을 수 있게 치환자(placeholer)를 사용할 수 있다.
@PropertySource("/database.properties")
public class AppContext {
    @Value("db.url") String url;

}
  • @Value와 치환자를 이용해 프로머티 값을 필드에 주입하려면 PropertySourcesPlaceholderConfigurer 빈으로 선언해야 한다.

빈 설정의 재사용과 @Enable*

  • 하나의 빈이 꼭 한 가지 타입일 필요는 없다. 빈 클래스 하나가 여러 개의 인터페이스를 구현해도 된다. 빈을 DI 받아서 사용하는쪽은 빈이 특정 인터페이스를 구현하고 있는지에만 관심이 있다. 그래서 코드의 양이 많지 않고, 같은 그룹으로 묶을 수 있는 여러 개의 빈을 하나의 클래스로 만들기도한다.

@Enable* 애노테이션

  • 스프링은 모듈화된 빈 설정을 가져올 때 사용하는 @Import를 다른 애노테이션으로 대체할 수 있는 방법을 제공한다.
  • @Import도 다른 이름의 애노테이션으로 대체 기능하다. @Import 애노테이션과 빈 설정 클래스 값을 메타 애노테이션으로 넣어서 아래와 같이 애노테이션을 만들어주면 된다.
@Import(value=SqlServiceContext.class)
public @interface EnableSqlService {

}
  • 직접 정의한 애노태이션에 @Import를 메타 애노태이션으로 부여해서 사용하는 방법은 이 밖에도 여러 가지 장점이 있다.
    • 의미가 잘 드러나고 깔끔해보인다.
    • 애노태이션을 정의하면서 엘리먼트를 넣어서 옵션을 지정하게 할 수도 있다.

정리

  • 빈 설정정보를 애노테이션과 자바 코드로 변경
  • 자동등록과 와이어링 기능을 적용
  • 실행환경에 따라 달라지는 빈 설정정보를 분리해서 프로파일을 적용
  • 외부 서비스 접속에 필요한 정보는 프로퍼티 파일로 만들어두고 프로퍼티 소스를 통해 빈 설정에서 사용
  • 독립적으로 재사용 가능한 코드와 설정정보를 분리해내고, 손쉽게 가져다 쓸 수 있게 전용 애노테이션도 도입