Java & Spring/토비의 스프링 3.1

7장) 7.6 스프링 3.1의 DI

Zin0_0 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")
반응형