좌충우돌 개발

Toby 스프링3 1부 7장 스프링 핵심기술의 응용 2

|

7.6 스프링 3.1의 DI

객체지향 코드의 장점 : 유연성과 확장성

자바 언어의 변화와 스프링

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

자바 코드의 메타정보를 이용한 프로그래밍 방식으로 발전해 왔다.
애노테이션은 자바코드가 실행되는데 직접 참여하지 못 한다. 복잡한 리플랙션API를 이용해 애노테이션의 메타정보를 조회하고, 애노테이션 내에 설정된 값을 가져와 참고하는 방법이 전부이다.
그렇다면 왜 애노테이션의 활용성이 늘었을까?
스프링은 핵심로직을 담은 자바코드와 IOC프레임워크, 프레임워크가 참조하는 메타정보로 구성되어 있다.
IOC프레임워크는 메타정보를 통해 객체의 관계를 파악하고 핵심로직이 담긴 객체를 생성한다.

DAOFactory -> xml -> 애노테이션 순으로 발전

애노테이션은 xml이나 여타 외부파일과 달리 자바코드의 일부로 사용된다.
장점
a. xml은 애노테이션 하나를 자바코드에 넣는 것에 비해 작성한 정보가 많다.
b. 텍스트 정보이기에 오타가 발생할 수 있다.
c. 리펙토링이 쉽다.
단점
xml은 어느 환경에서나 손쉽게 편집이 가능하고 내용이 변경되어도 다시 빌드할 필요가 없다. 하지만 애노테이션은 재컴파일을 해줘야한다.

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

애노테이션과 같은 메타정보를 활용하는 프로그래밍 방식은 코드를 이용해 명시적으로 동작 내용을 기술하는 대신 코드 없이 미리 약속한 규칙 또는 관례를 따라서 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극 포용하여 만들어 왔다.
장점 : 작성해야할 내용이 줄어든다.
단점 : 미리 정의된 규칙과 관례를 기억해야 하고, 메타정보를 보고 프로그램이 어떻게 동작할지 이해해야한다. 높은 학습비용이 필요하고 잘못 이해할 시에는 찾기 힘든 버그를 만들어낼 수있다.

현재는 자바 9과 스프링5에서는 위의 문제점을 개선한다고 한다.(링크 참조)

토비님 발표 자바9과 스프링 5


7.6.1 자바코드를 이용한 빈 설정

기존의 메타정보를 담은 xml로 관리하였으나 이를 java코드로 변환하는 실습

테스트 컨텍스트의 변경

@ContextConfiguration: 스프링 테스트가 테스트용 DI 정보를 어디서 가져와야 하는지 지정할 때 사용하는 애노테이션
@ContextConfiguration이 XML 대신 DI 정보를 담고 있는 클래스를 이용하도록 변경

  1. DI정보를 담은 클래스는 평범한 자바 클래스에 @Configruation 애노테이션을 달아주면 된다.
  2. 자바 클래스로 만들어진 DI 설정 정보에 xml의 설정 정보를 가져오게 할수 있다.
    @ImportResource 를 사용
<context: annotation-config/> 제거

<context: annotation-config/>는 \@PostConstruct를 붙인 메소드가 빈이 초기화된 후 자동 실행되도록 하는 용도로 사용되었다.

<bean>의 전환

<bean>은 @Bean이 붙은 public 메소드로 만들어주면 된다.
메소드 이름은 <bean>의 id값으로 한다.
메소드의 리턴타입은 빈이 주입 받아서 사용하는 빈에서 어떤 타입으로 사용 되는지 확인할 필요가 있다. 생성할 빈 오브젝트의 클래스는 <bean>의 class에 나온것을 그대로 사용하면 된다.
프로퍼티가 있는 경우엔 일단 로컬변수로 받아둬야 한다.
@bean메소드 내부에서는 빈의 구현 클래스에 맞는 프로퍼티값 주입이 필요하다. 프로퍼티는 구현 클래스에 의존적인 경우가 대부분이다. 따라서 빈 내부에서 new키워드를 사용해 빈 인스턴스를 만드는 경우 구현 클래스 타입으로 변수를 만들어야 한다.

@Configruation 자바 파일에서 생성한 빈과 xml로 생성한 빈은 서로 참조가 가능하다.

@Autowired 가 붙은 필드의 티입과 같은 빈이 있으면 해당 빈을 필드에 자동으로 넣어준다.

@Resource 는 @Autowired와 유사하지만 @Autowired는 타입기준으로 찾고 @Resource는 필드이름 기준으로 빈을 검색한다.

전용태그 전환

<jdbc: embedded-database> 전용 태그는 type에 지정한 내장형 DB를 생성하고 <jdbc:script>로 지정한 스크립트로 초기화한 뒤에 DataSource 타입 DB의 커낵션 오브젝트를 빈으로 등록한다.
자바 코드에서는 EmbeddedDatabaseBuilder 를 이용해 위의 작업을 진행한다.

<tx: annotation-driven/> 의 제거
AOP를 위해서 어드바이스와 포인트컷이 필요하고, 애노테이션 정보에서 트랜잭션 속성을 가져와 어드바이스에서 사용하게 해주는 역할을 한다.
스프링 3.1 부터는 \@EnableTransactionManagement 를 지원한다.

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

7.6.2 빈스캐닝과 자동와이어링

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

빈의 프로퍼티에 다른 빈을 넣어서 런타임시 관계를 맺어 주려면 <bean>의 <property>를 사용해 빈을 정의하거나 자바코드로 직접 수정자 메소드를 호출해 줘야 했다.

@Autowired 는 자동와이어링기법을 이용해서 조건에 맞는 빈을 찾아 자동으로 수정자 메소드나 필드에 넣어 준다. 컨테이너가 자동으로 주입할 빈을 결정하기 어려운 경우도 있다. 이럴 경우에는 직접 프로퍼티에 주입할 대상을 지정하는 방법을 병행하면 된다.
필드의 접근 제한자가 private로 선언되어 문제가 되지 않는다 스프링은 리플랙션 API를 이용하여 접근 제한자를 우회하여 값을 넘겨 준다.

스프링과 무관하게 직접 오브젝트를 생성하고 다른 오브젝트에 주입해서 테스트 하는 순수 단위 테스트의 경우 수정자 메소드가 필요하다.

@Autowired와 같은 자동와이어링을 적절하게 사용의 장단점

장점: DI관련 코드를 대폭 줄일 수 있어서 관리가 편리하다.
단점: 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져 있는지 한눈에 파악하기 힘들다

@Component를 이용한 자동 빈 등록
  • @Component가 붙은 클래스는 빈스캐너를 통해 자동으로 빈으로 등록된다.
  • @Component 애노테이션이 달린 클래스를 자동으로 찾아서 빈을 등록해주게 하려면 빈 스캐닝 기능을 사용하겠다는 애노테이션 정의가 필요하다. 빈 자동등록이 컨테이너가 디폴트로 제공하는 기능이 아니기 때문이다. 프로젝트내의 모든 클래스패스를 뒤져서 @Component 애노테이션이 달린 클래스를 찾는 것은 부담이 많이 가는 작업이다. 그래서 특정 패키지만 찾도록 기준이 되는 패키지를 지정할 필요가 있다. 이때 사용되는 애노테이션이 @ComponentScan 이다.
  • @Component가 붙은 클래스가 발견되면 새로운 빈을 자동으로 추가한다. 빈의 클래스는 @Component가 붙은 클래스이고, 빈의 아이디는 따로 지정하지 않는다면 클래스 이름의 첫글자를 소문자로 바꾸어 사용한다.
  • @Component에 의해 자동으로 빈을 등록하면 @Autowired에서 사용할 때는 별 문제가 안되지만 @Resource로 사용할 때는 아이디를 기준으로 찾음으로 문제가 발생할 수 있다. 만약 @Component가 붙은 클래스의 이름 대신에 다른 이름을 빈을 아이디로 사용하고 싶다면 @Component(“이름”) 을 넣어준다.

애노테이션은 @interface 키워드를 이용해 정의한다.

public @interface Component {
......
}

메타 애노테이션: 애노테이션의 정의에 부여된 애노테이션
여러개의 애노태이션에 공통의 속성을 부여할 때 메타 애노테이션을 이용한다.

@Repository : DAO 빈을 자동등록할 때 사용
@Service : 비즈니스 로직을 담고 있는 서비스 계층의 빈을 구분하기 위해 사용

7.6.3 컨텍스트 분리와 @Import

성격에 따른 DI 정보 분리하기

테스트용 컨텍스트 분리

DI 설정 정보를 분리하는 방법
DI 설정 클래스를 추가하고 관련된 빈설정 애노테이션, 필드 메소드를 옮기면 된다.
테스트용으로 들어난 빈은 설정정보가 드러나는 편이 좋다.

@Import

테스트용 설정 정보는 핵심설정정보와 깔끔하게 분리하는게 낫다.

  @Configruation
  @EnableTransactionManagement
  @ComponentScan(basePackage="springbook.practice.user")
  @Import(SqlServiceContext.class) // SQL서비스 설정 클래스를 따로 빼고 메인 설정에 import 시킨다.
  public class AppContext {
    ......
  }

7.4.2 프로파일

테스트 환경과 운영환경에서 각기 다른 빈 정의가 필요한 경우가 종종 있다.
이 문제를 해결하려면 운영환경에서는 반드시 필요하지만 테스트 실행 중에는 배제해야 하는 빈설정은 별도의 설정 클래스를 만들어 따로 관리할 필요가 있다.
(예를 들어 테스트에는 mock 메일 발송 클래스를 만들어 테스트 하지만 운영환경에서는 실제 메일을 발송해야하는 빈을 사용해야 한다)

//실제 운영환경에서 사용할 context
 @Configuration
 public class ProductionContext {
     @Bean
     public MailSender mailSender() {
         JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
         mailSender.setHost("mail.mycompany.com");
         return mailSender;
     }

 }

테스트 환경에서는 @ContextConfiguration에 AppContext와 TestAppContext를 지정해서 두 개 클래스의 빈 설정을 조합해서 테스트가 동작하게 만들고, 운영환경에서는 AppContext와 ProductionContext 두 개의 클래스가 DI정보로 설정하면 된다.
하지만 이런 식으로 실행환경이나 기능에 따라서 설정 파일이 여러개 쪼개지고 그중에 몇개를 선택해서 동작하도록 구성하는 일은 번거롭다 애플리케이션이 커질 수록 파일조합을 이용한 DI설정은 불편하다.

@ContextConfiguration(classes={TestAppContext.class, AppContext.class})
@Profile과 @ActiveProfile

스프링 3.1은 환경에 따라서 빈 설정정보가 달라져야 하는 경우에 파일을 여러 개로 쪼개고 조합하는 등의 번거로운 방법 대신 간단히 설정정보를 구성할 수 있는 방법을 제공한다. 실행환경에 따라 달라지는 내용을 프로파일로 정의해서 만들어두고, 실행 시점에 어떤 프로파일의 빈 설정을 사용할지 지정하는 것이다.

@Configuration
@Profile("test")
public class TestAppContext {

@Configuration
@Profile("production")
public class ProductionContext {

@import를 통해 모든 설정 클래스를 추가한다.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackage="springbook.practice.user")
@Import({ SqlServiceContext.class, TestAppContext.class, ProductionContext.class })
public class AppContext {

활성 프로파일 지정하기

@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test") //운영환경이라면 test를 production으로 한다.
@ContextConfiguration(classes = {AppContext.class })
public class UserServiceTest {
컨테이너의 빈 등록 정보 확인
@Autowired
DefaultListableBeanFactory bf;

@Test
public void beans() {
    for(String name : bf.getBeanDefinitionNames()) {
        System.out.println(name + "\t"+ bf.getBean(name).getClass().getName());
    }
}

스프링은 BeanFactory 인터페이스를 구현하고 있다.
BeanFactory의 구현 클래스중 DefaultListableBeanFactory이 있는데 거의 대부분의 스프링 컨테이너는 이 클래스를 이용해 빈을 등록하고 관리한다. 위의 코드와 같이 사용하여 등록된 빈의 정보를 조회할 수 있다.

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

프로파일 에 따라 클래스를 파일을 분리하였지만 흩어져 있어 파악하기 힘들다. 이를 static 중첩 class를 사용하여 하나의 설정파일로 묶는다. AppContext의 내부 static 클래스로 테스트와 ProductionContext를 들어오게 만들면 아래와 같이 @Import항목에서 해당 내부 중첩 클래스를 삭제할 수 있음으로 단순화 되는 장점이 있다.

@Import({ SqlServiceContext.class, TestAppContext.class, ProductionContext.class })
        |
        V
@Import({ SqlServiceContext.class})

7.6.5 프로퍼티 소스

아직 AppContext에는 테스트 환경에 종속되는 정보가 남아 있다. 바로 DataSource의 DB 연결 정보이다.
운영환경이라면 JNDI를 이용해 서버가 제공하는 DataSource를 가져오거나 애플리케이션에 내장할 수 있는 DB 커넥션 풀을 이용할 필요가 있다.
DB 설정 정보는 필요에 따라 손쉽게 변경할 수 있으면 좋다. 이런 외부 서비스 연결에 필요한 정보는 자바 클래스에서 제거하고 손쉽게 편집할 수 있고 빌드 작업이 필요없는 XML 이나 프로퍼티 파일같은 텍스트 파일에 저장해두는 편이 낫다.

@PropertySource
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "springbook.practice.user")
@EnableSqlService
@PropertySource("/database.properties") //@PropertySource를 사용하여 프로퍼티 파일을 등록해 준다.
public class AppContext implements SqlMapConfig {

스프링 3.1은 빈 설정 작업에 필요한 프로퍼티 정보를 컨테이너가 관리하고 제공해 준다. 스프링 컨테이너가 지정된 정보 소스로부터 프로퍼티 값을 수집하고, 이를 빈 설정 작업 중에 사용할 수 있게 해준다. 컨테이너가 프로퍼티 값을 가져오는 대상을 프로퍼티 소스라고 한다.
@PropertySource로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장된다. 환경 오브젝트는 @Autowired를 통해 필드로 주입받을 수 있다. 이 객체를 사용하여 프로퍼티 값에 접근하여 DB 연결정보를 세팅한다.

@Autowired Environment env;

@Bean
public DataSource dataSource() {
    SimpleDriverDataSource simpleDriverDataSource = new SimpleDriverDataSource();

    try{
        simpleDriverDataSource.setDriverClass(
          (Class<? extends java.sql.Driver>)Class.forName(
                  env.getProperty("db.driverClass")));      
                  //드라이버 클래스 이름을 String 값으로 가져오고 이 이름을 Class.forName() 넣어서 드라이버 클래스를 생성한다.
    } catch (ClassNotFoundException e) {
      throw new RuntimeException(e); // RuntimeException으로 매핑하여 예외를 넘김으로 써 상위 호출단에서 예외처리를 꼭하지 않아도 되게 해준다.
    }


    simpleDriverDataSource.setUrl(env.getProperty("db.url"));
    simpleDriverDataSource.setUsername(env.getProperty("db.username"));
    simpleDriverDataSource.setPassword(env.getProperty("db.password")); // 실제환경에서 프로퍼티에 암호를 입력할 때는 암호화 시켜서 노출 되지 않도록 한다.
    return simpleDriverDataSource;
}

getProperty()로 가져오는 정보는 스트링타입으로 만약 입력 값이 드라이버 클래스라면 Class.forName(“클래스이름”)를 사용하여 Class타입으로 변환하여 이용한다.
이제 연결 환경이 바뀌게 되면 프로퍼티 파일의 내용만 바꾸면 된다.

PropertySourcePlaceholderConfigurer

dataSource 빈의 프로퍼티는 빈 오브젝트가 아니므로 @Autowired를 사용할 수 없다. 대신 @Value 애노테이션을 사용할 수 있다.
@Value의 사용에는 여러가지 방법이 있으나 여기에서는 프로퍼티 소스로부터 값을 주입받을 수 있도록 치환자(placeholder)를 이용 하겠다.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "springbook.practice.user")
@Import({SqlServiceContext.class})
@PropertySource("/database.properties")
public class AppContext implements SqlMapConfig {
    /**
     * DB 연결과 트랜잭션
     */

    @Value("${db.driverClass}")
    Class<? extends Driver> driverClass;
    @Value("${db.url}")
    String url;
    @Value("${db.username}")
    String username;
    @Value("${db.password}")
    String password;

    @Bean //프로퍼티 소스를 이용한 치환자 설정용 빈 : 꼭 추가해줘야 치환자를 사용할 수 있다.
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
      return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource simpleDriverDataSource = new SimpleDriverDataSource();

        simpleDriverDataSource.setDriverClass(this.driverClass);
        simpleDriverDataSource.setUrl(this.url);
        simpleDriverDataSource.setUsername(this.username);
        simpleDriverDataSource.setPassword(this.password);
        return simpleDriverDataSource;
    }

위에서 제시한 두 가지 방법 중 편한 것을 사용하면 된다.

7.6.6 빈 설정의 재사용과 @Enable*

빈 설정자

SQL 서비스를 재사용 가능한 독립적인 모듈로 만들려면 해결할 문제가 하나 남았다.
UserDao 인터페이스가 위치한 클래스패스로부터 sqlmap.xml을 가져오게 되어 있다.
SQL 서비스를 사용하는 각 애플리케이션은 SQL 매핑파일의 위치를 직접 지정할 수 있어야 한다.

public class OxmSqlReader implements SqlReader {
    private Unmarshaller unmarshaller;
    private final static String DEFAULT_SQLMAP_FILE = "/sqlmap.xml";
    private String sqlmapFile = DEFAULT_SQLMAP_FILE;

    private Resource sqlmapResource = new ClassPathResource(sqlmapFile, UserDao.class);
                              |
                              V
    private Resource sqlmapResource = new ClassPathResource(sqlmapFile);

위와 같이 변경하여 UserDao.class 클래스패스에 대한 종속성을 제거한다.
SQL 매핑 리소스는 빈 클래스 외부에서 설정할 수 있어야 한다.

@Bean
public SqlService sqlService() {
    OxmSqlService sqlService = new OxmSqlService();
    sqlService.setSqlRegistry(sqlRegistry());
    sqlService.setUnmarshaller(unmarshaller());

    sqlService.setSqlMapResource(new ClassPathResource("sqlmap.xml", UserDao.class));

    return sqlService;
}

하지만 아직 SqlServiceContext에 UserDao.class에 대한 의존성이 남아 있어 모듈로 제공하기 어렵다.
외존성을 제거하기 위하여 Resource를 제공 하는 인터페이스를 만든다.

import org.springframework.core.io.Resource;

public interface SqlMapConfig {
    Resource getSqlMapResource();
}

SqlMapConfig 인터페이스의 구현 클래스

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;

public class SqlMapConfigApp implements SqlMapConfig {
    @Override
    public Resource getSqlMapResource() {
        return new ClassPathResource("/sqlmap.xml", UserDao.class);
        //인텔리J로 실습할때면 해당 xml파일이 빌드할때마다 삭제됨으로 xml파일은 Resources 아래 놓고 실습힌다.
        //UserDao.class 삭제
    }
}

SqlMapConfig 인터페이스를 의존하도록 수정 하고 @Autowired를 통해 해당 타입의 빈을 가져온다.

@Autowired
SqlMapConfig sqlMapConfig;

@Bean
public SqlService sqlService() {
    OxmSqlService sqlService = new OxmSqlService();
    sqlService.setSqlRegistry(sqlRegistry());
    sqlService.setUnmarshaller(unmarshaller());
    sqlService.setSqlMapResource(this.sqlMapConfig.getSqlMapResource());
    return sqlService;
}

SqlMapConfig 인터페이스의 빈 생성은 AppContext에서 설정 해준다. @Bean을 통해서 등록할수도 있고 AppContext 클래스가 SqlMapConfig를 인터페이스를 구현 하도록 만들 수도 있다. 해당 인터페이스를 구현 하도록 하면 자동으로 빈에 등록이 된다. @Configuration 애노테이션이 @Component를 메타애노테이션으로 가지고 있기 때문이다.

@Enable* 애노테이션
import org.springframework.context.annotation.Import;
import springbook.practice.SqlServiceContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) //책의 실습 예제에는 이 부분이 없다. 이부분을 제외 시키면 빈생성중에 에러가 발생한다.
@Import(value = SqlServiceContext.class)
public @interface EnableSqlService {
}

애노테이션을 만들고 @Import({SqlServiceContext.class})@EnableSqlService 으로 바꾼다.

정리

  • SQL처럼 변경될 수 있는 텍스트로 된 정보는 외부 리소스에 담아두고 가져오게 만들면 편리하다.
  • 성격이 다른 코드가 한데 섞여 있는 클래스라면 먼저 인터페이스를 정의해서 코드를 각 인터페이스별로 분리하는게 좋다. 다른 인터페이스에 속한 기능은 인터페이스를 통해 접근하게 만들고, 간단히 자기참조 빈으로 의존관계를 만들어 검증한다. 검증을 마쳤으면 아예 클래스를 분리해도 좋다.
  • 자주 사용되는 의존 오브젝트는 디폴트로 미리 정의해두면 편리하다.
  • XML과 오브젝트 매핑은 스프링의 OXM추상화 기능을 활용한다.
  • 특정 의존 오브젝트를 고정시켜 기능을 특화하려면 맴버 클래스로 만드는 것이 편리하다.
  • 외부의 파일이나 리소스를 사용하는 코드에서는 스프링의 리소스 추상화와 리소스 로더를 사용한다.
  • DI를 의식하면서 코드를 작성하면 객체지향 설계에 도움이 된다.
  • DI에는 인터페이스를 사용한다. 인터페이슬르 사용하면 인터페이스 분리 원칙을 잘 지키는데도 도움이 된다.
  • 클라이언트에 따라서 인터페이스를 분리할 때, 새로운 인터페이스를 만드는 방법과 인터페이스를 상속하는 방법 두 가지를 사용할 수 있다.
  • 애플리케이션에 내장하는 DB를 사용할 때는 스프링의 내장형 DB추상화 기능과 전용 태그를 사용하면 편리하다.

Comments