-
7장) 7.6 스프링 3.1의 DIJava & Spring/토비의 스프링 3.1 2021. 8. 30. 13:42반응형
7장 스프링 핵심 기술의 응용
7.6 스프링 3.1의 DI
- 자바 언어의 변화와 스프링
- DI가 적용된 코드를 작성할 때 사용하는 핵심 도구인 자바 언어의 대표적인 두 가지 변화
- 애노테이션의 메타정보 활용
- 정책과 관례를 이용한 프로그래밍
- 애노테이션의 메타정보 활용
- 자바 코드의 메타정보를 이용한 프로그래밍
- 자바 코드의 일부를 리플렉션 API 등을 이용해 어떻게 만들었는지 보고 그에 따라 동작하는 기능이 점점 많이 사용됨
- 리플렉션 API
- 초기 버전부터 class, interface, field, method 등의 메타정보를 살펴보거나 조작하기 위해 사용
- 최근에는 자바 코드의 메타정보를 데이터로 활용하는 스타일의 프로그래밍 방식에 더 많이 활용
- 애노테이션이 정점
- 애노테이션
- 자바 코드가 실행되는 데 직접 참여 X
- 복잡한 리플렉션 API로 애노테이션 메타정보 조회, 애노테이션 내 설정 값을 가져와 참고하는 방법이 전부
- 활용이 늘어난 이유
- Application 핵심 로직을 담은 자바 코드
이를 지원하는 IoC 방식의 프레임워크
프레임워크가 참조하는 메타정보
위의 세 가지로 구성하는 방식에 잘 어울리고 유리한 점이 많기 때문
- Application 핵심 로직을 담은 자바 코드
- 앞선 학습 사항
- UserDao에 객체지향 프로그래밍의 특징을 최대한 적용하며 유연하게 확장 가능한 코드로 다듬음
- 서로 영향을 주지 않고 확장 가능한 핵심 로직 코드
핵심 코드가 런타임 시 동적으로 관계를 맺고 동작하게 하는 DaoFactory
DaoFactory를 활용해 핵심 로직 코드가 관계를 맺고 동작하는 과정을 제어하는 클라이언트
세 가지로 구분됨 - 클라이언트는 일종의 IoC 프레임워크, DaoFactory는 IoC 프레임워크가 참고하는 일종의 메타정보로 의미가 있음
- 서로 영향을 주지 않고 확장 가능한 핵심 로직 코드
- XML로의 전환
- UserDao 한 가지가 아닌 애플리케이션을 구성하는 오브젝트 관계를 IoC/DI를 통해 프레임워크와 메타정보를 활용하는 방식으로 작성하도록 발전시키려면, 자바 코드로 만들어진 관계 설정 책임을 담은 코드가 불편
- 애노테이션 적용
- 여타 외부 파일과 달리 자바 코드의 일부로 사용
- 코드의 동작에 직접 영향 X, 메타정보로 활용되는 데 XML에 비해 유리함
- 정의에 따라 타입, 필드, 메소드, 파라미터, 생성자, 로컬 변수의 한 군데 이상 적용 가능하며, 메타정보를 얻을 수 있음
- 리팩토링에 유리
- 클래스의 패키지를 변경하거나 클래스 이름을 바꾸는 경우, 해당 클래스를 참조하는 코드도 자동으로 바꿔줌 <-> XML을 매번 수정
- 단점
- 자바 코드에 존재
- 변경할 때마다 클래스를 새로 컴파일해줘야함
- 자바 코드에 존재
- 스프링 3.1부터 애노테이션을 이용한 메타정보 작성 방식으로 거의 모든 영역이 확대됐고, 애노테이션을 활용한 프로그래밍이 성행
- UserDao에 객체지향 프로그래밍의 특징을 최대한 적용하며 유연하게 확장 가능한 코드로 다듬음
- 정책과 관례를 이용한 프로그래밍
- XML
- 미리 정의한 정책으로 특정 기능이 동작하게 만듦
- 자바 코드로 모든 작업 과정을 직접 표현할 때보다 작성할 내용이 줄어듦
- 프로그래밍 언어나 API 사용법 외에 미리 정의된 많은 규칙과 관례를 기억해야 하고, 메타정보를 보고 프로그램이 어떻게 동작하는지 이해하는 등 학습 비용의 높음과 찾기 힘든 버그 양산 가능성이 높음
- 애노테이션과 같은 메타정보를 활용하는 프로그래밍 방식
- 코드를 이용해 명시적으로 동작 내용을 기술하는 대신, 코드 없이도 미리 약속한 규칙 or 관례를 따라 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극적으로 포용
- 작성하는 코드 양에 비해 부가 정보가 많고, 일정한 패턴을 따르는 경우 관례를 부여해 명시적 설정을 최대한 배제 ~> 코드가 간략
@Transactional
대체 정책 예시- 중첩된 설정이 있는 경우 적용 우선순위를 직접 지명 (
(order=1)
) - 충돌을 방지하기 위해 4단계의 우선순위를 가진 대체 정책을 정해둠
- 관례를 직접 사용해야하는데, 잘못 기억하고 있는 경우 의도한 대로 동작하지 않을 수 있고 디버깅에 어려움
- 중첩된 설정이 있는 경우 적용 우선순위를 직접 지명 (
- XML
- DI가 적용된 코드를 작성할 때 사용하는 핵심 도구인 자바 언어의 대표적인 두 가지 변화
- 앞서 발전시켜왔던 사용자 DAO, 서비스 기능의 예제 코드를 스프링 3.1 DI 스타일로 수정해보자
7.6.1 자바 코드를 이용한 빈 설정
- 자바 코드를 이용한 빈 설정
- XML 제거 ~> 애노테이션 으로 수정
- Test 코드 수정하기
- 자바 코드 기능을 테스트하는 UserTest 제외
- UserDaoTest, UserServiceTest 수정
- 테스트 컨텍스트 변경
-
@RunWith(SpringJunit4ClassRunner.class) @ContextConfiguration(locations="/test-applicationContext.xml") public class UserDaoTest {} @RunWith(SpringJunit4ClassRunner.class) @ContextConfiguration(classes=TestApplicationContext.class) public class UserDaoTest {}
- xml파일 참조에서 클래스 기반 참조로 수정
-
@Configuration @ImportResource("/test-applicationContext.xml") public class TestApplicationContext{}
- DI 정보로 사용될 클래스 생성
- 바로 XML을 제거하면 부담스러울 수 있기 때문에,
@ImportResource
를 통해 주입하여 단계적으로 제거
-
- <context:anotation-config /> 제거
- <context:annotation-config>에 의해 등록되는 빈 후처리기가
@PostConstruct
와 같은 표준 애노테이션을 인식해서 자동으로 메소드를 수정해줌- 하지만, 이제 컨테이너가 참고하는 DI 정보 위치가 TestApplicationContext로 바뀌었으므로 불필요
- 컨테이너가 직접
@PostConstruct
애노테이션을 처리하는 빈 후처리기를 등록해줌
- <context:annotation-config>에 의해 등록되는 빈 후처리기가
- <bean>의 전환
- <bean>으로 정의된 DI 정보는 자바코드의
@Bean
이 붙은 메소드로 거의 1:1 매칭 -
@Bean public DataSource dataSource() { SimpleDriverDataSource dataSource = new SimpleDataSource(); dataSource.setDriverClass(Driver.class); dataSource.setUrl("jdbc:mysql://url~~"); dataSource.setUsername("spring"); dataSource.setPassword("book"); return dataSource; }
- dataSource 빈은 UserDao 등에서 DataSource 타입의 프로퍼티를 통해 주입 받아 사용
- SimpleDriverDataSource 클래스는 DataSource의 한 가지 구현일 뿐
- DI 원리에 따라 빈의 구현 클래스는 자유롭게 변경 가능
- 하지만, dataSource() 메소드의 리턴 값을 SimpleDriverDataSource으로 하면, 참조하는 쪽에서 SimpleDriverDataSource 타입으로 주입 받을 위험이 있음
- 빈 의존관계가 바뀌면 참조하는 다른 빈의 코드도 변경해야함
- DataSource 인터페이스를 통해 안전하게 관계를 맺도록 설정
-
@Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; }
- dataSource 빈을 참조해서 transactionManager의 프로퍼티에 주입
- 위와 동일한 이유로 PlatformTransactionManager 인터페이스로 느슨하고 안전하게 관계를 맺음
-
//@Autowired SqlService sqlService; // 필드 주입은 권장되지 않음 @RequiredArgsConstructor private final SqlService sqlService; // final로 생성자 주입이 권장됨 @Bean public UserDao userDao() { UserDaoJdbc dao = new UserDaoJdbc(); dao.setDataSource(dataSource()); dao.setSqlSErvice(sqlService()); return dao; } @Bean public UserService userService() { UserServiceImpl service = new UserServiceImpl(); service.setUserDao(userDao()); service.setMailSender(mailSender()); return service; } @Bean public UserService testUserServive() { TestUserService testService = new TestUserService(); testService.setUserDao(userDao()); testService.setMailSender(mailSender()); return testService; } @Bean public MailSender mailSender() { return new DummyMailSender(); }
- testUserService를 XML에서 userService와 프로퍼티 정의 부분이 동일해서 parent 정의를 사용하여 상속했는데, 프로퍼티 값을 모두 직접 넣어줘야함
- TestUserService 클래스는 public 접근 제한자로 설정하여 패키지가 달라도 접근할 수 있게 수정해줘야 함
public static class TestUserService extends UserServiceImpl
- 기존에는 UserServiceTest의 내부 스태틱 멤버 클래스로 정의했었음
- XML에서 정의한 빈을 자바에서 참조하기 위해
@Autowired를 사용- 권장되지 않음
-
@Bean public SqlService sqlService() { OxmSqlService sqlService = new OxmSqlService(); sqlService.setUnmarshaller(unmarshaller()); sqlService.setSqlRegistry(sqlRegistry()); return sqlservice; } @Resource Database embeddedDatabase; @Bean public SqlRegistry sqlRegistry() { EmbeddedSqlRegistry sqlRegistry = new EmbeddedSqlRegistry(); sqlRegistry.setDataSource(this.embeddedDatabase); return sqlRegistry; } @Bean public Unmarshaller unmarshaller() { Jax2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setContextPath("springbook.user.sqlservice.jaxb"); return marshaller; }
- embeddedDatabase 빈은 자바 코드로 변환하지 않았으니 @Resource를 통해 필드로 주입받아 사용
@Autowired
vs@Resource
- Autowired
- 필드의 타입을 기준으로 빈을 탐색
- Resource
- 필드의 이름을 기준으로 탐색
- Autowired
- TestApplicationContext에 DataSource 타입의 dataSource 빈이 존재하므로
@Resource
를 사용
- <bean>으로 정의된 DI 정보는 자바코드의
- 전용 태그 전환
- <jdbc:embedded-database>, <jdbc:script>
- <jdbc:embedded-database>는 내장형 DB를 생성
- <jdbc:script>는 스크립트로 초기화한 뒤, DataSource 타입 DB의 커넥션 오브젝트를 빈으로 등록
-
@Bean public DataSource embeddedDatabase() { return new EmbeddedDatabaseBuilder() .setName("embeddedDatabase") .setType(HSQL) .addScript("classpath:springbook/user/.../~.sql") .build(); }
- EmbeddedDatabaseBuilder를 통해 내장형 DB 종류와 초기화 스크립트를 지정하면, 위의 과정을 모두 진행
- 앞선 코드에서
@Resource
를 제거하고 sqlRegistry 빈에 프로퍼티를 embeddedDatabase 빈을 사용하도록 수정-
@Bean public SqlRegistry sqlRegistry() { EmbeddedSqlRegistry sqlRegistry = new EmbeddedSqlRegistry(); sqlRegistry.setDataSource(embeddedDatabase()); return sqlRegistry; }
-
- <tx:annotation-driven /> 태그 제거
- 트랜잭션 AOP를 적용하려면 복잡하고 많은 빈이 동원됨
- 어드바이스와 포인트컷
- 애노테이션 정보에서 트랜잭션 속성을 가져와 어드바이스에서 사용하게 해주는 기능
- 전용 태그를 사용하면 4가지 클래스를 빈으로 등록해줌
- org.springframwork.aop.autoproxy.InfrastructureAdvisorAutoProxyCreator
- org.springframwork.transaction.annotation.AnnotationTransactionAttributeSource
- org.springframwork.transaction.interceptor.TransactionInterceptor
- org.springframwork.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor
- 스프링 3.1부터는
@EnableTransactionManagement
애노테이션을 사용해 위의 모든 것을 해결 -
@Configuration @EnableTransactionManagement public class TestApplicationContext { /* * DB 연결 및 트랜잭션 */ /* * 위에서 예시로 적었던 모든 빈 설정들 */ }
- 트랜잭션 AOP를 적용하려면 복잡하고 많은 빈이 동원됨
- <jdbc:embedded-database>, <jdbc:script>
- XML 설정을 1:1로 자바 코드로 전환하는 작업을 진행
- 장점은 아직 보이지 않지만, 다듬어가면서 장점을 학습하자
7.6.2 빈 스캐닝과 자동와이어링
@Autowired
를 이용한 자동와이어링- UserServiceImpl과 UserDaoJdbc 클래스에
@Autowired
적용@Autowired
는 자동와이어링 기법을 이용해서 조건에 맞는 빈을 찾아 자동으로 수정자 메소드나 필드에 주입- 주입 가능한 타입의 빈이 하나라면 스프링이 수정자 메소드를 호출해서 주입
- 두 개 이상이라면 그 중 프로퍼티와 동일한 이름의 빈이 있는지 찾아 주입
- 최종 후보를 찾지 못한 경우에 에러
- UserDao 빈의 구현 클래스인 UserDaoJdbc는 dataSource와 sqlService 두 개의 빈에 의존
- dataSource 설정과 sqlService 프로퍼티에
@Autowired
적용-
public class UserDaoJdbc implements UserDao { @Autowired public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Autowired private SqlService sqlService; public void setSqlService(SqlService sqlService) { this.sqlService = sqlService; } }
- 메소드에
@Autowired
를 붙여 dataSource를 자동으로 주입 - DataSource 타입의 빈은 userDao가 사용하는 dataSource 빈, SQL 서비스용인 embeddedDatabase 빈 두 개
- dataSource 빈이 수정자 메소드와 이름이 동일하므로 dataSource를 주입
- sqlService가 자동 와이어링을 통해 주입되므로, 기존 userDao 빈의 userDao() 메소드는 빈 인스턴스만 생성하도록 수정하고, 기존에 Autowired로 SqlService를 주입하던 코드를 제거
-
@Autowired SqlService sqlService
-
@Bean public UserDao userDao() { return new UserDaoJdbc(); }
- 메소드에
-
- 자바는 private 필드에 클래스 외부에서 값을 넣을 수 없게 되어있지만, 스프링은 리플렉션 API를 통해 제약조건을 우회해서 값을 주입
- 수정자 메소드는 없어도 되지만, 다른 오브젝트를 주입해서 테스트해야하는 경우 수정자 메소드가 필요
@Autowired
같은 자동와이어링은 적절히 사용하면 DI 관련 코드를 대폭 감소시켜 편리하지만, 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져있는지 한눈에 파악하기 힘듦
- UserServiceImpl과 UserDaoJdbc 클래스에
@Component
를 이용한 자동 빈 등록@Component
- 클래스에 부여하여 빈 스캐너를 통해 자동으로 빈 등록
@Component
나 이를 메타 애노테이션으로 갖고 있는 애노테이션이 붙은 클래스가 자동 빈 등록 대상
- 클래스에 부여하여 빈 스캐너를 통해 자동으로 빈 등록
- TestApplicationContext에 userDao() 메소드 제거,
@Autowired
적용하기-
@Autowired UserDao userDao; @Bean public UserService userService() { UserServiceImpl service = new UserServiceImpl(); service.setUserDao(this.userDao); service.setMailSender(mailSender()); return service; } @Bean public UserService testUserService() { TestUserService testService = new TestUserService(); testService.setUserDao(this.userDao); testService.setMailSender(mailSender()); return testService; }
- userDao() 삭제 후, userDao 빈이 등록될 방법이 없어서 테스트에 실패
-
- UserDaoJdbc 클래스에
@Component
추가-
@Component public class UserDaoJdbc implements UserDao {}
- 자동 빈 등록 대상임을 명시
-
- TestApplicationContext에
@ComponentScan
적용-
@Configuration @EnableTransactionManagement @ComponentScan(basePackages="springbook.user") public class TestApplicationContext {}
@Component
애노테이션이 달린 클래스를 스캔할 때, 프로젝트 내의 모든 클래스패스를 다 찾는 것은 부담이 큼- 특정 패키지 아래서만 찾도록
@ComponentScan
을 부여
- 특정 패키지 아래서만 찾도록
-
- 위의 과정을 거쳐 다시 테스트가 통과되는데, 빈의 아이디가 userDaoJdbc로 바뀌었는데 왜 성공할까??
- 빈을 참조하는 테스트인 UserServiceTest나 DI 설정 클래스인 TestApplicationContext에서 모두
@Autowired
를 이용해 빈을 주입받기 때문 - 아이디를 기준으로 주입할 빈을 찾지 않고 UserDao라는 타입으로 빈을 찾기 때문
@Component
가 붙은 클래스 이름을 다른 빈 아이디로 사용하고 싶은 경우, 애노테이션 이름을 부여- ex)
Component("userDao")
- ex)
- 빈을 참조하는 테스트인 UserServiceTest나 DI 설정 클래스인 TestApplicationContext에서 모두
- 메타 애노테이션
- 애노테이션 정의에 부여된 애노테이션
- 여러 개의 애노테이션에 공통적인 속성을 부여하려면 메타 애노테이션을 이용
- 스프링은
@Component
외의 애노테이션으로 자동 빈 등록이 가능- 빈 스캔 검색 대상으로 만드는 것 외에 부가적인 용도의 마커로 사용하기 위함
@Transactional
이 대표적인 예시
- 애노테이션이 빈 스캔을 통해 자동등록 대상으로 인식되게 하려면 애노테이션 정의에
@Component
를 메타 애노테이션을 붙여주면 됨-
@Component public @interface SnsConnector {}
-
@SnsConnector public class FacebookConnector {}
- @SnsConnector 애노테이션을 부여하면, 자동 빈 등록 대상으로 만들고 Sns 커넥션과 관련된 부가 정보를 함께 담을 수 있음
-
- DAO 빈은
@Repository
를 사용하는 것을 권장하는데,@Component
를 메타 애노테이션으로 가지고 있음- 앞서 작성한 UserDaoJdbc 코드도
@Component
에서@Repository
로 수정
- 앞서 작성한 UserDaoJdbc 코드도
- 애노테이션 정의에 부여된 애노테이션
- 자동 빈 등록을 적용하는게 좋은 빈과 그렇지 않은 빈이 존재하기 때문에, 적절한 판단이 필요
- UserServiceImpl에
@Component
와@Autowired
적용하기-
@Service("userService") public class UserServiceImpl implements UserService { ... @Autowired private UserDao userDao; @Autowired private MailSender mailSender; }
- 빈 자동등록을 했으니 TestApplicationContext의 userService() 메소드를 제거하는게 맞는데, 문제가 생김
- UserService 타입의 빈이 userServiceImpl과 testUserService 두 개가 존재하기 때문인데, 주입할 빈을 결정하지 못하기 때문
- 따라서, userService라는 아이디를 부여하여 해결
- 빈 자동등록을 했으니 TestApplicationContext의 userService() 메소드를 제거하는게 맞는데, 문제가 생김
-
- dataSource와 transactionManager 빈은 자동등록 기능을 적용할 수 없음
- 스프링이 제공해주는 클래스를 사용하기 때문에 소스코드에
@Component
나@Autowired
적용 불가 - dataSource 빈은 프로퍼티에 텍스트 값을 설정해줘야하기 때문에 더욱 불가능
- 스프링이 제공해주는 클래스를 사용하기 때문에 소스코드에
7.6.3 컨텍스트 분리와 @Import
- 지금까지 작성한 정보는 테스트 DI 정보와 애플리케이션이 동작하는데 필요한 DI 정보가 혼재
- 성격이 다른 DI 정보 분리하기
- 테스트용 컨텍스트 분리
- testUserService 빈은 테스트에서만 사용되고, 현재 작성한 mailSender 같은 경우에는 운영 중에 사용되면 안되기 때문에 분리
-
@Configuration public class TestAppContext { @Bean public UserService testUserService() { TestUserSerivce testService = new TestUserService(); testService.setUserDao(this.userDao); testService.setMailSender(mailSender()); return testService; } @Bean public MailSender mailSender() { return new DummyMailSender(); } }
- 테스트용 DI 설정 클래스인 TestAppContext를 만들어 빈 설정 애노테이션, 필드, 메소드를 옮김
- 기존 TestApplicationContext는 ApplicationContext로 네이밍을 수정하고, 이와 혼동되지 않게 테스트용은 TestAppContext로 네이밍
- testUserService 빈은 userDao와 mailSender 빈에 의존하는데, userDao 빈은 자동으로 등록되도록
@Repository
를 적용해서@Autowired
로 빈을 주입- TestUserService 클래스가 UserServiceImpl을 상속했기 때문에, userDao 프로퍼티는 자동와이어링 대상
- 따라서 프로퍼티 설정 코드와
@Autowired
필드를 제거하는 것이 더 깔끔
- 따라서 프로퍼티 설정 코드와
- TestUserService 클래스가 UserServiceImpl을 상속했기 때문에, userDao 프로퍼티는 자동와이어링 대상
-
@Configuration public class TestAppContext { @Bean public UserService testUserService() { return new TestUserService(); } @Bean public MailSender mailSender() { return new DummyMailSender(); } }
- 테스트용 DI 설정 클래스인 TestAppContext를 만들어 빈 설정 애노테이션, 필드, 메소드를 옮김
- TestUserService 클래스에 @Componnet를 붙이고 @ComponentScan을 이용해 자동 등록이 되게할 수 있지만, UserDaoServiceImpl과 UserServiceTest 등이 같은 패키지 아래 존재하므로 기준 패키지를 정하기 어려움
- 또한, 테스트용으로 특별히 만든 빈은 설정정보에 내용이 드러나있는 것이 더 좋음
- 운영 시스템은 AppContext만 참조, 테스트는 AppContext와 TestAppContext 두 개의 DI 정보를 함께 사용
-
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes={TestAppContext.class, AppContext.class}) public class UserDaoTest {}
@Import
- SQL 서비스용 빈은 독립적인 모듈로 취급
- 다른 애플리케이션에서도 사용될 수 있고, DAO에서는 sqlService 타입의 빈을 DI 받기만 하면 되지 구체적인 구현 방법을 알 필요가 없음
- 독립적으로 개발되거나 변경될 가능성이 높음
- SQL 서비스와 관련된 빈 분리
-
@Configuration public class SqlServiceContext { // unmarshaller, registry를 설정하는 빈, sqlRegistry의 dataSource에 embeddedDbSqlRegistry를 설정하는 빈, unmarshaller 빈, embeddedDatabaseBuilder를 사용하는 DataSource 빈 등록 코드 }
-
- SQL 관련된 DI 설정 정보를 담은 클래스를 생성했으니, 운영용과 테스트용에 등록
- AppContext에 포함되는 보조 설정이므로, classes에 추가하기 보다 설정정보를
@Import
를 통해 AppContext에서 정보를 받아오는 것이 더 좋음 -
@Configuration @EnableTransactionManagement @ComponentScan(basePackages="springbook.user") @Import(SqlServiceContext.class) pubilc class AppContext {}
- AppContext에 포함되는 보조 설정이므로, classes에 추가하기 보다 설정정보를
- SQL 서비스용 빈은 독립적인 모듈로 취급
7.6.4 프로파일
- 운영용 mailsender
- JavaMail 기반의 메일 발송용 클래스 JavaMailSenderImpl 활용
-
@Bean public MailSender mailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost("mail.mycompany.com"); return mailSender; }
-
- UserServiceTest 실행 시 문제 발생
- AppContext와 TestAppContext에 정의된 빈들이 함께 사용돼서, mailSender이 충돌
- 빈 설정 정보를 읽는 순서에 따라 우선순위가 적용됨
- JavaMail 기반의 메일 발송용 클래스 JavaMailSenderImpl 활용
- 운영환경에서 반드시 필요하지만 테스트 실행 중에는 배제돼야하는 빈 설정을 별도의 설정 클래스를 만들어 관리
- ProductionAppContext
-
@Configuration public class ProductionAppContext { @Bean public MailSender mailSender() { JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); mailSender.setHost("localhost"); return mailSender; } }
- 앞서 사용한 것처럼, AppContext에서
@Import
로 가져온다면, 테스트에서도 사용되는 문제가 그대로 발생 @Profile
을 이용
@Profile
과@ActiveProfiles
- 설정환경에 따라 빈 구성이 달라지는 내용을 프로파일로 정의하고, 실행 시점에 어떤 프로파일 빈 설정을 사용할지 지정
- 프로파일은 설정 클래스 단위로 지정
-
@Configuration @Profile("test") public class TestAppContext {}
-
@Configuration @Profile("production") public class ProductionAppContext {}
-
@Configuration @EnableTransactionManagement @ComponentScan(basePackages="springbook.user") @Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class}) public class AppContext {}
- AppContext나 SqlServiceContext는 default 프로파일로 취급 ~> 항상 적용
- 위와 같이 AppContext가 다 import 하고 있으므로, UserDaoTest, UserServiceTest의 @ContextConfiguration에서 AppContext.class만 설정해주면 된다
@ContextConfiguration(classes=AppContext.class)
- 위의 설정을 하고 테스트를 돌리면 mailSender 빈 충돌이 발생
- 두 설정 클래스가 프로파일이 지정되어 있어서 현재 테스트 설정으로는 어디도 포함되지 않음
- ActiveProfile을 통해 활성 프로파일을 설정해주자
-
@RunWith(SpringRunner.class) @ActiveProfiles("test") @ContextConfiguration(classes=AppContext.class) public class UserServiceTest {}
-
- 애플리케이션 운영 환경은 production으로 지정해주어 사용
- 컨테이너 빈 등록 정보 확인
- 스프링 컨테이너는 BeanFactory라는 인터페이스를 구현
- DefaultListableBeanFactory는 BeanFactory를 구현한 클래스인데, 거의 대부분의 스프링 컨테이너가 이 클래스로 빈을 등록하고 관리
@Autowired
로 주입받아서 빈 이름과 클래스를 확인해보면 profile별로 configuration이 적용됨을 확인
- 중첩 클래스를 이용한 프로파일 적용
- 프로파일에 따라 분리한 설정 정보를 하나의 파일로 모으기
- 전체 구성을 살펴보기가 번거로워짐
- 의존관계를 맺는 빈이 더 많아지면 더욱 단점
- 프로파일이 지정된 독립된 설정 클래스의 구조는 유지한 채 소스코드의 위치만 통합
- 스태틱 중첩 클래스 활용
-
@Configuration @EnableTransactionManagement @ComponentScan(basePackages="springbook.user") @Import({SqlServiceContext.class}) public class AppContext { ... @Configuration @Profile("test") public static class TestAppContext {} @Configuration @Profile("production") public static class ProductionAppContext {} }
- ProductionAppContext와 TestAppContext 는 중첩 클래스로 만들었으므로 클래스 파일은 삭제
- 중첩 클래스로 프로파일 설정 클래스를 포함했으므로,
@Import
에 지정하지 않아도 적용
- 전체 구성을 살펴보기가 번거로워짐
- 빈 설정 정보가 많으면 하나의 파일로 모았을 때 전체 구조를 파악하기 쉽지 않음
- 현재 예시의 구조는 모으는 방법이 더욱 깔끔
- 프로파일에 따라 분리한 설정 정보를 하나의 파일로 모으기
7.6.5 프로퍼티 소스
- AppContext에 테스트 환경에 종속되는 dataSource의 DB 연결정보가 남아있으므로 이를 분리해보자
- 실행 환경에 따른 설정하기
- XML or properties 같은 텍스트 파일에 저장하는 것이 좋음
- 빌드 작업이 따로 필요 없고 수정에 용이함
@PropertySource
-
#database.properties 파일 db.driverClass=com.mysql.jdbc.Driver db.url=jdbc:mysql://localhost/springboot?characterEncoding=UTF-8 db.username=spring db.password=book
-
@Configuration @EnableTransactionManagement @ComponentScan(basePackages="springbook.user") @Import({SqlServiceContext.class) @PropertySource("/database.properties") public class AppContext {}
- properties의 내용을 가져와 DB 연결정보를 프로퍼티에 주입
- 컨테이너가 프로퍼티 값을 가져오는 대상을 프로퍼티 소스(property source)라고 함
- 환경 변수나 시스템 프로퍼티, 프로퍼티 파일이나 리소스 위치를 지정하는 등 다양한 프로퍼티 소스 존재
@PropertySource
를 통해 프로퍼티 소스 등록
@Autowired
를 통해 Environment 오브젝트를 주입받아 사용하는 방법이 예시에 있지만, Driver Class를 지정해줄 때 try - catch 블록을 활용해야하는 등 번거로움- PropertySourcesPlaceholderConfigurer 활용
- properties의 내용을 가져와 DB 연결정보를 프로퍼티에 주입
-
- PropertySourcesPlaceholderConfigurer
@Value
애노테이션을 활용-
@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(); }
- 빈 팩토리 후처리기로 사용되는 빈을 정의
- 빈 설정 메소드는 반드시 스태틱 메소드로 선언
@Value
로 가져온 네 개의 필드는@PropertySource
로 지정한 파일에서 가져온 프로퍼티 값이 자동으로 주입됨
- dataSource 빈에서 값 사용하기
-
@Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(this.driverClass); ds.setUrl(this.url); ds.setUsername(this.username); ds.setPassword(this.password); return ds; }
7.6.6 빈 설정의 재사용과
@Enable*
- SQL 서비스 빈은 서비스 인터페이스, 즉 API인 SqlService만 Dao 노출하면 됨
- 나머지 구현 기술, 방법은 내부에 감추고 필요에 따라 자유롭게 변경 가능해야함
- SQL 서비스 구현 클래스는 애플리케이션의 다른 빈에 의존하지 않아서 독립적으로 패키징해서 배포 가능
- 이미 분리가 되어있어서
@Import(SqlServiceContext.class)
를 통해 필요한 곳에서 SQL 서비스를 사용 가능
- 빈 설정자
- SQL 서비스를 재사용 가능한 독립 모듈로 만들기 위한 해결할 문제점
- OxmlSqlService의 내부 클래스인 OxmlSqlReader
- sqlmap.xml 파일 위치를 지정하는 부분이 예제 코드의 UserDao 위치로 고정되어 있음
- sqlmap 프로퍼티의 디폴트 값을 UserDao 같은 사용자 예제에 종속되지 않게 수정하기
private Resource sqlmap = new ClassPathResource("/sqlmap.xml");
- default 값 이외에 빈 클래스 외부에서SQL 매핑 리소스를 설정하기
-
@Bean public sqlService sqlService() { OxmSqlService sqlService = new OxmSqlService(); ... sqlService.setSqlmap(new ClassPathResource("sqlmap.xml", UserDao.class)); return sqlService; }
- SQL 서비스 구현 클래스 내부 의존성이 제거됐지만, UserDao.class라는 애플리케이션 종속 정보가 남아있어서, 다른 애플리케이션에서 SqlServiceContext를 수정 없이
@Import
로 사용 불가 - 의존성을 제거해서 SqlServiceContext까지 독립적인 모듈로 분리하자
- sqlmap 리소스의 위치는 바뀔 일이 없으니 초기에 한 번만 지정하면 됨
- 기본적인 DI 활용
- SQL과 같이 매번 달라지는 내용은 콜백 형태로 만들어서 템플릿/콜백 패턴을 쓰지만, 위의 경우는 기본 DI가 적절
-
public interface SqlMapConfig { Resource getSqlMapResource(); }
-
public class UserSqlMapConfig implements SqlMapConfig { @Override public Resource getSqlMapResource() { return new ClassPathResource("sqlmap.xml", UserDao.class); } }
-
@Configuration public class sqlServiceContext { @Autowired SqlMapConfig sqlMapConfig; @Bean public SqlService sqlService() { ... sqlService.setSqlmap(this.sqlMapConfig.getSqlMapResource()); return sqlService; } }
-
public class AppContext { ... @Bean public SqlMapConfig sqlMapConfig() { return new UserSqlMapConfig(); } }
- SqlServiceContext가 변하지 않는 SqlMapConfig 인터페이스에만 의존하고, SqlMapConfig 구현 클래스는 빈으로 정의해 런타임 시 주입
- SqlMapConfig를 구현한 UserSqlMapConfig 클래스를 빈으로 등록하여 AppContext에서 빈을 생성
- SQL 매핑 파일 위치 변경에 영향을 받지 않게되면서, SqlServiceContext는 SqlMapConfig와 함께 SQL 서비스 모듈에 함께 패키징되어 수정 없이 재사용 가능
- sqlmap 리소스의 위치는 바뀔 일이 없으니 초기에 한 번만 지정하면 됨
-
- AppContext에서 SqlMapConfig를 직접 구현하기
- SQL 매핑 파일 리소스 위치도 애플리케이션 빈 설정 관련 정보인데, 새로운 클래스를 추가한 것이 복잡
- AppContext는 빈을 정의하고 DI 정보를 제공하는 설정용 클래스인 동시에 스스로도 빈이기 때문에, AppContext에서 직접 구현해도 무관
- 컨테이너에 의해 빈 오브젝트로 만들어짐
-
public class AppContext implements sqlMapConfig { ... @Override public Resource getSqlMapResource() { return new ClassPathResource("sqlmap.xml", UserDao.class); } }
- SQL 서비스를 재사용 가능한 독립 모듈로 만들기 위한 해결할 문제점
@Enable*
애노테이션@Import
를 통해 SqlServiceContext를 사용할 수 있지만, 조금 더 직관적인 의미가 있다면 더욱 좋음@Import
애노테이션과 빈 설정 클래스 값을 메타 애노테이션으로 넣어 애노테이션을 생성하자-
@Import(value=SqlServiceContext.class) public @interface EnableSqlService {}
-
@Import
대신@EnableSqlService
로 사용하기-
@Configuration @ComponentScan(basePackages="springbook.user") @EnableTransactionManagement @EnableSqlService @PropertySource("/database.properties") public class AppContext implements SqlMapConfig {}
- SQL 서비스를 사용한다는 의미가 더욱 명확
-
- 메타 애노테이션을 부여하면서 애노테이션을 만들어 사용하면, 애노테이션을 정의하면서 엘리먼트를 넣어 옵션을 지정하게 할 수도 있다
- SQL 매핑 파일을 전달하게 했던 방식을 더욱 간결하게 만들 수 있음
@EnableSqlService("classpath:/springbook/user/sqlmap.xml")
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
8장) 8.3 POJO 프로그래밍 ~ 8.4 스프링의 기술 (2) 2021.09.07 8장) 8.1 스프링의 정의 ~ 8.2 스프링의 목적 (0) 2021.09.07 7장) 7.4 인터페이스 상속을 통한 안전한 기능확장 ~ 7.5 DI를 이용해 다양한 구현 방법 적용하기 (0) 2021.08.20 7장) 7.3 서비스 추상화 적용 (0) 2021.08.08 7장) 7.1 SQL과 DAO의 분리 ~ 7.2 인터페이스의 분리와 자기참조 빈 (0) 2021.08.06 - 자바 언어의 변화와 스프링