티스토리 뷰

스프링 3.1의 DI

어노테이션의 메타정보 활용

  • 장점
    • 다양한 메타 정보를 얻을 수 있음
    • XML 보다 작성 편리 (IDE 도움)
  • 단점
    • XML 과 달리 변경 사항이 있을 때마다 새로 컴파일해줘야 함

스프링 3.1 이후 DI 3요소

  • 핵심 로직을 담은 자바 코드
  • DI 프레임워크
  • DI를 위한 메타데이터로서의 자바 코드 (어노테이션)

자바 코드를 이용한 빈 설정

어노테이션과 자바코드로 XML 대체

config 파일

@Configuration
@ImportResource("/test-applicationContext.xml")
public class TestApplicationContext {
}
  • 기존 xml 파일과 호환을 위해 @ImportResource() 를 붙임

의 전환

  • @Bean
    • @Configuration 이 붙은 DI 설정용 클래스에서 주로 사용되는 것으로, 메소드를 이용해서 빈 오브젝트의 생성과 의존관계 주입을 직접 자바 코드로 작성할 수 있게 해준다.
    • 의 id 값 -> 메소드의 이름
    • 의 class -> 생성할 빈 오브젝트의 클래스
    • 변수 타입 : 구현 클래스 타입으로 (?)
      • Q. 생성자 DI 를 사용하면 인터페이스 타입으로 해도 되지 않나?
@Bean
public PlatformTransactionManager transactionManager() {
  //책에서는 DataSourceTransactionManager tm = new DataSourceTransactionManager();

    PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
    return transactionManager;
}

XML parent 속성

  • @Bean 메소드로 전환할 때는 부모 bean 설정을 참고해서 프로퍼티 값을 모두 넣어줘야 함
  • 자바 코드에서 inner 클래스를 참조할 때는 패키지가 다르면 public 으로 접근 제한자를 바꿔줘야 함을 주의.
    • public static class TestUserService extends UserServiceImpl

@Resource

  • @Autowired 와 유사하게 필드에 빈을 주입받을 때 사용
  • @Autowired
    • 필드 타입을 기준으로 빈을 탐색
  • @Resource
    • 필드 이름을 기준으로 탐색

XML 전용 태그 전환

내장형 DB 생성 태그

  1. 내장형 DB 생성
  2. 초기화 스크립트 실행
  3. DataSource 타입의 DB 커넥션 오브젝트를 빈으로 등록

위 로직을 자바 코드로 옮긴다.

@Bean
public DataSource embeddedDatabase() {
    return new EmbeddedDatabaseBuilder()
            .setName("embeddedDatabase")
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:springbook...")
            .build();
}

트랜잭션 태그

  • InfrastructureAdvisorAutoProxyCreator
  • AnnotationTransactionAttributeSource
  • TransactionInterceptor
  • BeanFactoryTransactionAttributeSourceAdvisor
  • 위 네 가지 클래스를 빈으로 등록 -> 매우 복잡. 자바 코드로 작성 어려움.

대안

  • @EnableTransactionManagement
  • 스프링은 XML에서 자주 사용되는 전용 태그를 @Enable 로 시작하는 어노테이션으로 대체할 수 있게 다양한 어노테이션을 제공.
public class TestApplicationContext {

    /**
     * DB연결과 트랜잭션
     */

    @Bean
    public DataSource dataSource() {
        SimpleDriverDataSource ds = 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 dao;
    }

    @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 EmbeddedDatabaseBuilder()
            .setName("embeddedDatabase")
            .setType(HSQL)
            .addScript("classpath:springbook/user/sqlservice/updatable/sqlRegistrySchema.sql")
            .build();
    }
}

빈 스캐닝과 자동 와이어링

@Autowired

  • 자동와이어링 기법을 이용해 조건에 맞는 빈을 찾아 자동으로 수정자 메소드나 필드에 넣어준다.
  • 장점
    • 컨테이너가 이름이나 타입을 기준으로 주입될 빈을 찾아주기 때문에 빈의 프로퍼티 설정을 직접해주는 자바 코드나 XML 의 양을 대폭 줄일 수 있다.
  • 단점
    • 빈 설정정보를 보고 다른 빈과 의존관계가 어떻게 맺어져 있는지 한눈에 파악하기 힘들다.

As-is

  @Autowired SqlService sqlService;

    @Bean 
    public UserDao userDao() {
        UserDaoJdbc dao = new UserDaoJdbc();
        dao.setDataSource(dataSource());
        dao.setSqlService(this.sqlService);
        return dao;
    }

To-be

public class UserDaoJdbc implements UserDao {

  //필드 주입
    @Autowired
    private SqlService sqlService;
    //...

  //Setter 주입
    @Autowired
  public void setDataSource(DataSource dataSource) {
      this.jdbcTemplate = new JdbcTemplate(dataSource);
  }


}

// @Autowired SqlService sqlService;

@Bean 
public UserDao userDao() {
    UserDaoJdbc dao = new UserDaoJdbc();
    //dao.setDataSource(dataSource());
    //dao.setSqlService(this.sqlService);
    return dao;
}

@Component 를 이용한 자동 빈 등록

@Component

  • 클래스에 부여
  • @Component 가 붙은 클래스는 빈 스캐너를 통해 자동으로 빈으로 등록됨 (정확히는 @Component 또는 @Component를 메타 어노테이션으로 갖고 있는 어노테이션이 붙은 클래스)

userDao() 메소드 제거

    @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;
    }
@Component
public class UserDaoJdbc implements UserDao {
    //...
}
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
public class TestApplicationContext {
    //...
}
  • @Component 어노테이션을 사용하려면 빈 스캔 기능을 사용하겠다는 어노테이션 정의 필요. -> @ComponentScan
    • 모든 클래스패스를 다 뒤져서 @Component 가 붙은 클래스를 찾는 것은 부담이 많이 가는 작업이므로 basePackages를 지정
  • 빈의 아이디는 클래스 이름의 첫 글자를 소문자로 바꿔서 사용.
  • 의존관계를 담은 프로퍼티를 따로 지정할 방법이 없으므로 @Autowired 와 같은 자동 와이어링 방식을 함께 적용
  • 빈 이름을 변경하고 싶다면 어노테이션에 이름을 넣어준다.
    • e.g. @Component(“userDao”)

userServiceImpl 에 @Component 적용

@Component
public class UserServiceImpl implements UserService {
    // ...
}
  • NoSuchBeanDefinitionException 발생
    • UserService type bean이 중복 -> userServiceImpl, testUserService
    • 해결 방법
      • UserServiceImpl의 빈 이름 수정
@Service("userService")
public class UserServiceImpl implements UserService {
    // ...
}

컨텍스트 분리와 @Import

  • 개선할 점
    • 어플리케이션이 동작하는데 필요한 DI 정보와 테스트를 수행하기 위한 DI 정보 혼재 -> 성격이 다른 DI 정보 분리

테스트용 컨텍스트 분리

  • 테스트에서만 필요한 빈 추출
    • testUserService
    • mailSender
@Configuration
public class TestAppContext {
    // Q. 생성자 주입으로는 간단하게 만들 수 없나..?
    @Bean
    public UserService testUserService() {
        TestUserService testService = new TestUserService();
        return testService;
    }

    @Bean
    public MailSender mailSender() {
        MailSender mailSender = new DummyMailSender();
        return mailSender;
    }
}
  • TestUserService 클래스에 @Component 를 붙여서 빈 자동등록을 할 수도 있겠지만
    • 어플리케이션을 위한 클래스, 테스트를 위한 클래스가 같은 패키지에 있어서 기준 패키지 설정 어려움
    • 테스트용 빈은 수동등록을 통해 빈 등록 정보를 쉽게 알 수 있게 하는 것이 좋음

@Import

  • SQL Service
    • 다른 빈들과 달리 독립적으로 개발되거나 변경될 가능성이 높음 -> 별도의 설정정보로 관리
@Configuration
public class SqlServiceContext {

    @Bean
    public SqlService sqlService() {
        SqlService sqlService = new OxmSqlService(unmarshaller(), sqlRegistry());
        return sqlService;
    }

    @Bean
    public SqlRegistry sqlRegistry() {
        SqlRegistry sqlRegistry = new EmbeddedDbSqlRegistry(embeddedDatabase());
        return sqlRegistry;
    }

    @Bean
    public Unmarshaller unmarshaller() {
        Unmarshaller unmarshaller = new Jaxb2Marshaller();
        return unmarshaller;
    }

    @Bean
    public DataSource embeddedDatabase() {
        return new EmbeddedDatabaseBuilder()
                .setName("embeddedDatabase")
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:springbook...")
                .build();
    }    
}
  • 위와 같이 Context 가 추가될 때마다 테스트용 설정정보 @ContextConfiguration 에 classes 내용을 수정해야 할까? -> No. 테스트용 설정정보는 어플리케이션 설정정보와 깔끔하게 분리되는 편이 낫다.
  • 반면 SqlServiceContext 는 AppContext 는 긴밀하게 결합시키는 것이 나음.
    • @Import 이용해서 SqlServiceContext 설정정보를 가져오도록 수정
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@Import(SqlServiceContext.class)
public class AppContext {
    //...
}

Profile

  • MailSender
    • 운영환경 : JavaMailSenderImpl
    • 테스트 : DummyMailSender
    • 두 개의 빈 충돌
  • 문제 해결
    • 운영 환경에서만 필요한 빈 설정 클래스 추가
@Configuration
@Profile("production")
public class ProductionAppContext {
    @Bean
    public MailSender mailSender() {
        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
        mailSender.setHost("localhost");
        return mailSender;
    }
}
@Configuration
@Profile("test")
public class TestAppContext {
}
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@Import({SqlServiceContext.class, TestAppContext.class, ProductionAppContext.class})
public class AppContext {
}
@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = AppContext.class)
class UserServiceTest {
  • @ActiveProfiles(“test”) 를 통해 test profile이 지정된 testAppContext 의 빈 설정은 포함되고, ProductionAppContext의 빈 설정은 production profile로 선언되어 있으므로 무시된다.

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

  • Profile 마다 빈 구성이나 구현 클래스에 어떤 차이가 있는지 한 눈에 비교하기 어려움.
  • 프로파일에 따라 분리했던 설정 정보를 하나의 파일로 모아보자.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@Import({SqlServiceContext.class})
public class AppContext {

    @Resource
    DataSource embeddedDatabase;

    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDaoJdbc(dataSource(), sqlService());
        return userDao;
    }


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

        dataSource.setDriverClass(Driver.class);
        dataSource.setUrl("jdbc:mysql://....");
        dataSource.setUsername("spring");
        dataSource.setPassword("book");

        return dataSource;
    }

    @Bean
    public UserService userService() {
        UserService userService = new UserServiceImpl(userDao(), mailSender());
        return userService;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        PlatformTransactionManager transactionManager = new DataSourceTransactionManager(dataSource());
        return transactionManager;
    }


    @Configuration
    public static class ProductionAppContext {
        @Bean
        public MailSender mailSender() {
            JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
            mailSender.setHost("localhost");
            return mailSender;
        }
    }

    @Configuration
    @Profile("test")
    public static class TestAppContext {
        @Bean
        public UserService testUserService() {
            TestUserService testService = new TestUserService(this.userDao, mailSender());
            return testService;
        }

        @Bean
        public MailSender mailSender() {
            MailSender mailSender = new JavaMailSenderImpl();
            return mailSender;
        }
    }

}
  • @Import 에 ProductionAppContext.class, TestAppContext.class 생략 가능.
    • static 중첩 클래스로 넣은 @Configuration 클래스는 스프링이 자동으로 포함해준다.

프로퍼티 소스

  • DB 연결정보와 같은 설정 정보를 xml 이나 프로퍼티 파일과 같은 텍스트 파일에 저장
  • database.properties
db.driverClass=com.mysql.jdbc.Driver
db.url=jdbc:mysql://localhost/springbook?characterEncoding=UTF-8
db.username=spring
db.password=book
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@Import(SqlServiceContext.class)
@PropertySource("/database.properties")
public class AppContext {

@Bean
public DataSource dataSource() {
    SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
    try {
        dataSource.setDriverClass((Class<? extends Driver>)) Class.forName(env.getProperty("db.driverClass"));
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
    dataSource.setUrl(env.getProperty("db.url"));
    //...
    return dataSource;
}

}
  • @PropertySource 로 등록한 리소스로부터 가져오는 프로퍼티 값은 컨테이너가 관리하는 Environment 타입의 환경 오브젝트에 저장됨.
  • 이것으로 DB 연결정보는 설정 클래스로부터 완전히 분리됨.

PropertySourcesPlaceholderConfigurer

  • @Value 를 이용해 Environment 오브젝트 대신 프로퍼티 값을 직접 DI 받는 방법
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@Import(SqlServiceContext.class)
@PropertySource("/database.properties")
public class AppContext {
    @Value("${db.driverClass}") Class<? extends Driver> driverClass;
    @Value("${db.url") String url;

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

      dataSource.setDriverClass(this.driverClass);
      dataSource.setUrl(this.url);
      return dataSource;
  }

  //빈 후처리기로 사용되는 빈. 반드시 static 메소드로 선언.
  @Bean
  public static PropertySourcesPlaceholderConfigurer   placeholderConfigurer() {
      return new PropertySourcesPlaceholderConfigurer();
  }

}
  • 장점
    • 타입 변환을 스프링이 알아서 처리
  • 단점
    • dataSource 빈에서만 사용하는 프로퍼티를 위해 클래스에 필드를 선언해야 함.

빈 설정의 재사용과 @Enable*

  • 개선사항
    • OxmSqlReader 내 sqlmap.xml 파일 위치 지정
      • UserDao.class 에 종속적
private Resource sqlmap = new ClassPathResource("sqlmap.xml", UserDao.class);
  • 인터페이스를 정의해서 의존성을 제거
public interface SqlMapConfig {
    Resource getSqlMapResource();
}

public class UserSqlMapConfig implements SqlMapConfig{
    @Override
    public Resource getSqlMapResource() {
        return new ClassPathResource("sqlmap.xml", UserDao.class);
    }
}

public class AppContext {
    @Bean
    public SqlMapConfig sqlMapConfig() {
        return new UserSqlMapConfig();
    }
    //...
}
  • 빈 설정정보를 위해 새로운 클래스 UserSqlMapConfig 를 추가한 것이 아쉽다. -> 간결하게 만들고 싶다.
    • AppContext 도 빈으로 취급되니 AppContext 가 SqlMapConfig 인터페이스를 구현하도록 만든다. -> 이후 SqlServiceContext 에서 SqlMapConfig 를 주입 받을 때 AppContext 를 빈으로 찾아낼 수 있음
public class AppContext implements SqlMapConfig{

    @Override
    public org.springframework.core.io.Resource getSqlMapResource() {
        return new ClassPathResource("sqlmap.xml", UserDao.class);
    }
}

@Enable* 어노테이션

  • @Import(SqlServiceContext.class) 를 다른 이름의 어노테이션으로 대체
    • SQL Service 를 사용하겠다는 의미가 더 잘 드러나고 깔끔하다.
@Import(value = SqlServiceContext.class)
public @interface EnableSqlService {
}

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example.tobyspring")
@EnableSqlService
@PropertySource("/database.properties")
public class AppContext implements SqlMapConfig{
}
  • 추가로, @EnableSqlService 에 엘리먼트를 넣어서 sqlmap.xml 의 위치를 담도록 수정할 수도 있다.
    • @EnableSqlService(“classpath:/springbook/user/sqlmap.xml”) -> vol.2 에서 다룸.
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2025/02   »
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
글 보관함