본문 바로가기
Framework & Library/Spring & Egov

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

by 뒹굴거리는프로도 2018. 7. 16.
반응형

토비의 스프링 3.1




7.6 스프링 3.1의 DI 


스프링의 정체성은 객체지향 언어인 자바의 특징과 장점을 극대화하는 프로그래밍 스타일과 이를 지원하는 도구라는 것에 있다. 스프링을 이용해서 만들어지는 애플리케이션 코드가 DI패턴을 이용해서 안전하게 발전하고 확장할 수 있는 것처럼, 스프링 프레임워크 자체도 DI원칙을 충실하게 따라서 만들어졌기 때문에 기존 설계와 코드에 영향을 주지 않고도 꾸준히 새로운 기능을 추가하고 확장해나가는 일이 가능하다.


-자바 언어의 변화와 스프링


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

: 원래 리플렉션 API는 자바 코드나 컴포넌트를 작성하는 데 사용되는 툴을 개발 할 때 이용하도록 만들어 졌는데, 언제부턴가 본래 목적보다는 자바 코드의 메타정보를 데이터로 활용하는 스타일의 프로그래밍 방식에 더 많이 활용되고 있다. 이런 프로그래밍 방식의 절정은 자바 5에서 등장한 애노테이션일 것이다. 

 애노테이션은 복잡한 리플렉션 API를 이용해 애노테이션의 메타정보를 조회하고, 애노테이션 내에 설정 된 값을 가져와 참고하는 방법이 전부다. 하지만 애플리케이션의 핵심 로직을 담은 자바 코드와 이를 지원하는 IoC 방식의 프레임워크, 그리고 프레임워크가 참조하는 메타정보라는 세 가지로 구성하는 방식에 잘 어울린다. 애노테이션은 프레임워크가 참조하는 메타정보로 사용되기에 여러 가지 유리한 점이 많다.


애노테이션은 xml이나 여타 외부파일과 달리 자바 코드의 일부로 사용된다.

1
2
3
4
5
6
7
8
9
10
11
12
packgae com.mycompany.myproject;
 
@Special
public class MyClass{
    ...
 
}
 
// 1) 애노테이션이 클래스에 부여되었다는 것을 알 수 있다.
// 2) MyClass 클래스의 메타정보를 얻을 수 있다.
//(클래스의 패키지, 클래스 이름, 접근 제한자, 상속 클래스 등)
 
cs


동일한 정보를 XML로 표현하려면 모든 내용을 명시적으로 나타내야 한다. 

이는 오타 발생 가능성을 높이거나, 리팩토링을 할 때도 번거롭다는 단점이 있다.

1
<x:special target="type" class="com.mycompany.myproject.MyClass"/>
cs


애노테이션의 단점은 자바 코드에 존재하므로 변경할 때마다 매번 클래스를 컴파일 해줘야 한다는 것이다. 



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

: @Transactional의 예

한 오브젝트가 네 가지 종류(클래스, 인터페이스, 각각의 메소드)의 @Transactional을 모두 갖고 있다면 트랜젝션 속성이 최종적으로 어떻게 적용될까? 

이 때 적용 우선순위를 직접 지정하도록 만들 수 있다. @Transactional(order=1)과 같은 식이 될 것이고, order가 높은 쪽의 설정이 우선적으로 적용되게 만들면 된다. 그런데 @Transactional을 제대로 활용하려면 관례화된 이 정책을 기억하고 코드를 작성해야 한다. 




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


지금까지는 자바 코드보다 간결하고 편하게 DI 정보를 담을 수 있었기 때문에 XML을 사용해왔지만 이제는 애노테이션과 새로운 스타일의 자바 코드로 바꿀 것이다. 이제부터 진행할 작업에는 DI 관련 정보와 코드를 스프링 3.1로 변경하는 일과 함께, 테스트용으로 만들어진 기존 XML에서 애플리케이션이 운영환경에서 동작할 때 필요로 하는 DI 정보를 분리해내는 일도 포함된다.



-테스트 컨텍스트의 변경


 : 스프링 3.1 은 애노테이션과 자바 코드로 만들어진 DI의 설정정보와 XML을 동시에 사용할 수 있는 방법을 제공해준다. 가장 먼저 할 일은 테스트 코드에서 DI 정보가 XML에 담겨 있다고 정의한 부분을 찾아 DI 정보를 담은 자바 코드를 사용하도록 바꾸는 것이다.


-XML 파일을 사용하는 UserDaoTest

1
2
3
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/test-applicationContext.xml")
public class UserDaoTest{
cs


@ContextConfiguration은 스프링 테스트가 테스트용 DI정보를 어디서 가져와야 하는지 지정할 때 사용하는 애노테이션이다.

locations 엘리먼트는 DI 설정정보를 담은 XML 파일의 위치를 가리킨다. 이제 @ContextConfiguration이 XML 위치 대신, DI정보를 담고 있는 자바 클래스를 이용하게 만들어보자. 먼저 DI 정보로 사용될 자바 클래스를 만들고, 이것을 XML 대신 사용한다.


-DI 메타정보로 사용 될 TestApplicationContext 클래스

1
2
3
4
5
@Configuration
public class TestApplicationContext{
 
//@Configuration을 달아주면, DI정보로 사용 될 수 있다.
}
cs



1
2
3
4
5
6
7
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=TestApplicationContext.class)
 
// locations 엘리먼트를 제거하고 대신 classes를 넣어서
// TestApplicationContext를 지정하도록 한다.
public class UserDaoTest{
 
cs


 하지만 아직 아무런 빈도 만들어지지 않았으니 테스트는 실패할 것이다. 아무런 DI 정보가 없기 때문이다. 테스트를 성공시키기 위해 일단 XML에 있던 빈 설정정보를 testApplicationContext.java에 옮겨야 한다. 자바 클래스로 만들어진 DI 설정정보에서 XML의 설정정보를 가져오게 만들 수 있다. @ImportResource 애노테이션을 이용하면 된다.


1
2
3
4
5
@Configuration
@ImportResource("/test-applicationContext.xml")
public class TestApplicationContext{
    
}
cs


이제 테스트는 성공할 것이다. 이제부터 XML의 DI 정보를 단계적으로 TestApplicationContext로 옮길 수 있다. 



- <context:annotation-config />제거

xml의 내용을 TestApplicationContext 내부로 옮겨보자.


test-applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" encoding="UTF-8" ?>
<beans ...>
    <context:annotation-config />
 
    <!-- db -->
    <bean id="dataSource" 
        class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver" />
        <property name="url"
            value="jdbc:mysql://localhost/springbook?characterEncoding=UTF-8" />
        <property name="username" value="spring" />
        <property name="password" value="book" />
    </bean>
 
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DatasourceTransactionManager" >
        <property name="datasource" ref="dataSource" />
    </bean>
 
    <!-- sql service -->
    <bean id="sqlService" class="springbook.user.sqlservice.OxmSqlService">
        <property name="unmarchaller" ref="unmarchaller" />
        <property name="sqlRegistry" ref="sqlRegistry" />
    </bean>
 
    <bean id="sqlRegistry"
        class="springbook.user.sqlservice.updatable.EmbeddedDbSqlRegistry">
        <property name="dataSource" ref="embeddedDatabase" />
    </bean>
 
    <jdbc:embedded-database id="embeddedDatabase" type="HSQL" >
        <jdbc:script location=
            "classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql" />
    </jdbc:embedded-database>
 
    <bean id="unmarchaller" class="org.springframework.oxm.jaxb.Jaxb2Marchaller">
        <property name="contextPath" value="springbook.user.sqlservice.jaxb" />
    </bean>
 
    <!-- aop -->
    <tx:annotation-driven />
 
    <!-- application components -->
    <bean id="userDao" class="springbook.user.dao.UserDaoJdbc">
        <property name="dataSource" ref="dataSource" />
        <property name="sqlService" ref="sqlService" />
    </bean>
 
    <bean id="userService" class="springbook.user.service.UserServiceImpl">
        <property name="userDao" ref="userDao" />
        <property name="mailSender" ref="mailSender" />
    </bean>
 
    <bean id="testUserService" 
        class="springbook.user.service.UserServiceTest$testUserService"
        parent="userService" />
 
    <bean id="mailSender" class="springbook.user.service.DummyMailSender" />
</beans>
 
 
cs



여기서 가장 앞부분의 <context:annotation-config />를 생략해도 좋다. 스프링 컨테이너가 참고하는 DI 정보 위치가 XML에서 TestApplicationContext라는 자바 클래스로 바뀌었기 때문이다. xml에 담긴 DI정보를 이용하는 스프링 컨테이너를 사용하는 경우에는 @PostConstruct와 같은 애노테이션의 기능이 필요하면, 반드시 <context:annotation-config />을 포함시켜서 필요한 빈 후처리기가 등록되게 만들어야 한다. 반면 @Configuration이 붙은 설정 크래스를 사용하는 컨테이너가 사용되면 필요없다. 컨테이너가 직접 @PostConstruct 애노테이션을 처리하는 빈 후처리기를 등록해주기 때문이다. (Vol.2)




- <bean>의 전환


db연결과 트랜젝션 매니저 빈을 옮겨보자.

: <bean>으로 정의된 DI 정보는 자바 코드, 특별히 @Bean이 붙은 메소드와 거의 1:1로 매핑된다. <bean>은 @Bean이 붙은 public 메소드로 만들어주면 된다. 메소드 이름은 <bean>의 id 값으로 한다. dataSouce빈의 경우, 리턴 타입은 빈의 의존관계가 인터페이스를 통해 안전하게 맺어지도록 DataSource 인터페이스로 하는 것이 좋다. 


자바 코드로 작성한 dataSource 빈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public DataSource dataSource(){
    
// 프로퍼티들이 구현클래스에 의존적이기 때문에 
// 빈 오브젝트를 저장할 로컬 변수 타입을 SimpleDriverDataSource로 한다.
 
 SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
 
 dataSource.setDriverClass(Driver.class);
 dataSource.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8");
 dataSource.setUsername("spring");
 dataSource.setPassword("book");
 
 return dataSource;
 
 
cs


자바 코드로 정의한 transactionManager 빈

1
2
3
4
5
6
7
8
@Bean 
public PlatformTransactionManager transactionManager(){
    DataSourceTransactionManager tm = new DataSourceTransactionManager();
    
    //주입해 줄 빈의 메소드를 직접 호출해서 그 리턴 값을 수정자 메소드에 넣어준다.     
    tm.setDataSource(dataSource());
    return tm;
}
cs


XML의 빈 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Autowired SqlService sqlService; //xml 빈 주입
 
@Bean
public UserDao userDao(){
    UserDaoJdbc dao = new UserDaoJdbc();
    dao.setDataSource(dataSource());
    dao.setSqlService(this.sqlService);
    return dao;
}
 
@Bean
public UserService userService(){
    UserServiceImpl service = new UserServiceImpl();
    service.setUserDao(userDao());
    service.setMailSender(mailSender());
    return service;
}
 
@Bean
public UserSerivce testUserService(){
    // 접근 제한자는 public으로 한다.
    // 또한 xml에 정의되었을 때 parent정의를 이용해서
    // userService 프로퍼티 정의 부분을 그대로 상속받았었다.
    // 따라서 @Bean 메소드로 전환할 때도
    // userSerivce 빈 설정을 참고해서 프로퍼티 값을 넣어줘야 한다.
    TestUserService testService = new TestUserService();
    testService.setUserDao(userDao());
    testService.setMailSender(mailSender());
    return testService;
}
 
@Bean
public MailSender mailSender(){
    return new DummyMailSender();
}
cs

자바 코드에서 XML 에 정의된 빈을 참조하게 하려면, @Autowired가 붙은 필드를 선언해서, XML에 정의된 빈을

컨테이너가 주입해주게 해야 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Bean 
public SqlService sqlService(){
    OxmSqlService sqlService = new OxmSqlService();
    sqlService.setUnmarshaller(unmarshaller());
    sqlService.setSqlRegistry(sqlRegistry());
    return sqlService;
}
 
@Resource Database embeddedDatabase; //추후 변경
 
@Bean 
public SqlRegistry sqlRegistry(){
    EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry();
    sqlRegistry.setDataSource(this.embeddedDatabase);
    return sqlRegistry;
}
 
@Bean 
public Unmarshaller unmarshaller(){
    Jaxb2Marchaller marshaller = new Jaxb2Marchaller();
    marshaller.setContextPath("springbook.user.sqlservice.jaxb");
    return marshaller;
}
 
cs


@Autowired는 필드의 타입을 기준으로 빈을 찾고, @Resource는 필드 이름과 일치하는 빈 아이디를 가진 빈을 찾는다.



- <전용 태그 전환>

: <bean>이 아니라, 특별한 용도로 사용하도록 만들어진 전용 태그가 있다.  전용 태그도 빈을 등록하는데 사용된다.

여기서는 2개의 전용 태그를 바꿔보도록 한다.


1. <jdbc: embedded-database>

: 내장형 DB 빈을 생성하는 @Bean 메소드

1
2
3
4
5
6
7
8
9
10
11
@Bean  
public DataSource embeddedDatabase(){
    //EmbeddedDatabaseBuilder는 <jdbc:embedded-database>가
    //내부적으로 해주는 작업과 거의 동일한 작업을 한다.
    return new EmbeddedDatabaseBuilder()
        .setName("embeddedDatabase")
        .setType("HSQL")
        .addScript(
            "classpath:springbook/user/sqlservice/updatable/SqlRegistrySchema.sql")
        .build();
}
cs



2.<tx:annotation-driven />

: 트랜젝션 AOP 적용 하기 위해 위의 옵션을 주지 않는다면 기본적으로 아래 네 가지 클래스를 빈으로 등록한다. 

1
2
3
4
org.springframework.aop.framework.autoproxy.InfrastrctureAdvisorAutoProxyCreator
org.springframework.transaction.annotation.AnnotationTransactionAttributeSource
org.springframework.transaction.interceptor.TransactionInterceptor
org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor
cs


하지만 스프링 3.1에서는 전용태그에 대응되는 @EnableTransactionManagement를 붙여주면 된다.


이제 xml 에서 빈 설정정보를 가져오도록 추가한 @ImportResource를 제거한다. dlwp test-applicationContext.xml 파일을 제거한다. 

아래는 스프링 DI 정보를 담은 XML 파일의 내용을 모두 DI 설정용 자바 클래스로 이전한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@Configuration
@EnableTransactionManagement
public class TestApplicationContext{
 
 
    //db 연결과 트랜젝션 
 
    @Bean 
    public DataSource dataSource(){
        SimpleDriverDataSource db = new SimpleDriverDataSource();
        ds.setDriverClass(Driver.class);
        ds.setUrl("jdbc:mysql://localhost/springbook?characterEncoding=UTF-8");
        ds.setUsername("spring");
        ds.setPassword("book");
        return ds;
    }
 
    @Bean 
    public PlatformTransactionManager transactionManager(){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource());
        return tm;
    }
 
 
    //애플리케이션 로직 & 테스트
 
    @Autowired SqlService sqlService;
 
    @Bean 
    public UserDao userDao(){
        UserDaoJdbc dao = new UserDaoJdbc();
        dao.setDataSource(dataSource());
        dao.setSqlService(this.sqlService);
        return service;        
    }
 
    @Bean 
    public UserService userService(){
        UserServiceImpl service = new UserServiceImpl();
        service.setUserDao(userDao());
        service.setMailSender(mailSender());
        return service;
    }
 
    @Bean 
    public UserService testUserService(){
        testUserService testService = new testUserService();
        testService.setUserDao(userDao());
        testService.setMailSender(mailSender());
        return testService;
    }
       
    @Bean
    public MailSender mailSender(){
        return new DummyMailSender();
    }
 

// SQL 서비스

    @Bean
    public SqlService sqlService(){
        OxmSqlService sqlService = new OxmSqlService();
        sqlService.setUnmarshaller(unmarshaller());
        sqlService.setSqlRegistry(sqlRegistry());
        return sqlService;
    }
 
    @Bean 
    public SqlRegistry sqlRegistry(){
        EmbeddedDbSqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry();
        sqlRegistry.setDataSource(embeddedDatabase());
        return sqlRegistry;
    }
 
    @Bean
    public Unmarshaller unmarshaller(){
        Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
        marshaller.setContextPath("springbook.user.sqlservice.jaxb");
        return marshaller;
    }
 
    @Bean
    public DataSource embeddedDatabase(){
        return new EnbeddedDatabaseBuilder()
            .setName("embeddedDatabase")
            .setType(HSQL)
            .addScript("classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql")
            .build();
    }
 
}
  cs


반응형