ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7장) 7.6 스프링 3.1의 DI
    Java & 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 방식의 프레임워크
              프레임워크가 참조하는 메타정보
              위의 세 가지로 구성하는 방식에 잘 어울리고 유리한 점이 많기 때문
      • 앞선 학습 사항
        • UserDao에 객체지향 프로그래밍의 특징을 최대한 적용하며 유연하게 확장 가능한 코드로 다듬음
          • 서로 영향을 주지 않고 확장 가능한 핵심 로직 코드
            핵심 코드가 런타임 시 동적으로 관계를 맺고 동작하게 하는 DaoFactory
            DaoFactory를 활용해 핵심 로직 코드가 관계를 맺고 동작하는 과정을 제어하는 클라이언트
            세 가지로 구분됨
          • 클라이언트는 일종의 IoC 프레임워크, DaoFactory는 IoC 프레임워크가 참고하는 일종의 메타정보로 의미가 있음
        • XML로의 전환
          • UserDao 한 가지가 아닌 애플리케이션을 구성하는 오브젝트 관계를 IoC/DI를 통해 프레임워크와 메타정보를 활용하는 방식으로 작성하도록 발전시키려면, 자바 코드로 만들어진 관계 설정 책임을 담은 코드가 불편
        • 애노테이션 적용
          • 여타 외부 파일과 달리 자바 코드의 일부로 사용
          • 코드의 동작에 직접 영향 X, 메타정보로 활용되는 데 XML에 비해 유리함
            • 정의에 따라 타입, 필드, 메소드, 파라미터, 생성자, 로컬 변수의 한 군데 이상 적용 가능하며, 메타정보를 얻을 수 있음
            • 리팩토링에 유리
              • 클래스의 패키지를 변경하거나 클래스 이름을 바꾸는 경우, 해당 클래스를 참조하는 코드도 자동으로 바꿔줌 <-> XML을 매번 수정
          • 단점
            • 자바 코드에 존재
              • 변경할 때마다 클래스를 새로 컴파일해줘야함
          • 스프링 3.1부터 애노테이션을 이용한 메타정보 작성 방식으로 거의 모든 영역이 확대됐고, 애노테이션을 활용한 프로그래밍이 성행
      • 정책과 관례를 이용한 프로그래밍
        • XML
          • 미리 정의한 정책으로 특정 기능이 동작하게 만듦
          • 자바 코드로 모든 작업 과정을 직접 표현할 때보다 작성할 내용이 줄어듦
          • 프로그래밍 언어나 API 사용법 외에 미리 정의된 많은 규칙과 관례를 기억해야 하고, 메타정보를 보고 프로그램이 어떻게 동작하는지 이해하는 등 학습 비용의 높음과 찾기 힘든 버그 양산 가능성이 높음
        • 애노테이션과 같은 메타정보를 활용하는 프로그래밍 방식
          • 코드를 이용해 명시적으로 동작 내용을 기술하는 대신, 코드 없이도 미리 약속한 규칙 or 관례를 따라 프로그램이 동작하도록 만드는 프로그래밍 스타일을 적극적으로 포용
          • 작성하는 코드 양에 비해 부가 정보가 많고, 일정한 패턴을 따르는 경우 관례를 부여해 명시적 설정을 최대한 배제 ~> 코드가 간략
          • @Transactional 대체 정책 예시
            • 중첩된 설정이 있는 경우 적용 우선순위를 직접 지명 ((order=1))
            • 충돌을 방지하기 위해 4단계의 우선순위를 가진 대체 정책을 정해둠
              • 관례를 직접 사용해야하는데, 잘못 기억하고 있는 경우 의도한 대로 동작하지 않을 수 있고 디버깅에 어려움
    • 앞서 발전시켜왔던 사용자 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 애노테이션을 처리하는 빈 후처리기를 등록해줌
    • <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
            • 필드의 이름을 기준으로 탐색
        • TestApplicationContext에 DataSource 타입의 dataSource 빈이 존재하므로 @Resource를 사용
    • 전용 태그 전환
      • <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 연결 및 트랜잭션
               */
          
              /*
               * 위에서 예시로 적었던 모든 빈 설정들
               */
          }
    • 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 관련 코드를 대폭 감소시켜 편리하지만, 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져있는지 한눈에 파악하기 힘듦
    • @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")
      • 메타 애노테이션
        • 애노테이션 정의에 부여된 애노테이션
          • 여러 개의 애노테이션에 공통적인 속성을 부여하려면 메타 애노테이션을 이용
        • 스프링은 @Component 외의 애노테이션으로 자동 빈 등록이 가능
          • 빈 스캔 검색 대상으로 만드는 것 외에 부가적인 용도의 마커로 사용하기 위함
          • @Transactional이 대표적인 예시
        • 애노테이션이 빈 스캔을 통해 자동등록 대상으로 인식되게 하려면 애노테이션 정의에 @Component를 메타 애노테이션을 붙여주면 됨
          • @Component
            public @interface SnsConnector {}
          • @SnsConnector
            public class FacebookConnector {}
            • @SnsConnector 애노테이션을 부여하면, 자동 빈 등록 대상으로 만들고 Sns 커넥션과 관련된 부가 정보를 함께 담을 수 있음
        • DAO 빈은 @Repository를 사용하는 것을 권장하는데, @Component를 메타 애노테이션으로 가지고 있음
          • 앞서 작성한 UserDaoJdbc 코드도 @Component에서 @Repository로 수정
      • 자동 빈 등록을 적용하는게 좋은 빈과 그렇지 않은 빈이 존재하기 때문에, 적절한 판단이 필요
      • UserServiceImpl에 @Component@Autowired적용하기
        • @Service("userService")
          public class UserServiceImpl implements UserService {
              ...
              @Autowired
              private UserDao userDao;
          
              @Autowired
              private MailSender mailSender;
          }
          • 빈 자동등록을 했으니 TestApplicationContext의 userService() 메소드를 제거하는게 맞는데, 문제가 생김
            • UserService 타입의 빈이 userServiceImpl과 testUserService 두 개가 존재하기 때문인데, 주입할 빈을 결정하지 못하기 때문
            • 따라서, 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 필드를 제거하는 것이 더 깔끔
        • @Configuration
          public class TestAppContext {
              @Bean
              public UserService testUserService() {
                  return new TestUserService();
              }
          
              @Bean
              public MailSender mailSender() {
                  return new DummyMailSender();
              }
          }
      • 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 {}

    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이 충돌
        • 빈 설정 정보를 읽는 순서에 따라 우선순위가 적용됨
    • 운영환경에서 반드시 필요하지만 테스트 실행 중에는 배제돼야하는 빈 설정을 별도의 설정 클래스를 만들어 관리
      • 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 활용
    • 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 서비스 모듈에 함께 패키징되어 수정 없이 재사용 가능
      • AppContext에서 SqlMapConfig를 직접 구현하기
        • SQL 매핑 파일 리소스 위치도 애플리케이션 빈 설정 관련 정보인데, 새로운 클래스를 추가한 것이 복잡
        • AppContext는 빈을 정의하고 DI 정보를 제공하는 설정용 클래스인 동시에 스스로도 빈이기 때문에, AppContext에서 직접 구현해도 무관
          • 컨테이너에 의해 빈 오브젝트로 만들어짐
        • public class AppContext implements sqlMapConfig {
              ...
          
              @Override
              public Resource getSqlMapResource() {
                  return new ClassPathResource("sqlmap.xml", UserDao.class);
              }
          }
    • @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")
    반응형

    댓글

Designed by Tistory.