스프링의 현재와 미래 같은 챕터이다.
스프링의 핵심인 객체지향은 그대로지만, 이 객체지향을 지키기 위한 기술들이 많이 바뀌었다고 한다.
- 어노테이션의 메타정보 활용
자바는 소스코드를 컴파일 후 클래스 파일로 만들었다가, JVM에 의해 메모리에서 로딩되어 실행된다.
이런 자바코드를 리플렉션등과 같은 방법으로 클래스내의 인터페이스, 필드 등의 메타정보를 확인 할 수 있다.
자바5에 어노테이션이 등장했다.
자바의 클래스나 인터페이스, 필드와 메서드들은 그 자체로 직접 이용이 가능하지만 어노테이션은 그런 방식으로는 이용이 불가능하다.
리플렉션을 이용해 메타정보를 조회하고, 어노테이션에 설정된 값을 가져와서 참고하는 방법이 전부라고 한다.
그럼에도 이렇게 직접 영향을 주지 못하는 어노테이션은 빠르게 발전했다고 한다.
어노테이션이 IOC 방식의 프레임워크, 프레임워크가 참조하는 메타정보라는 이 구성방식에 잘 어울리기 때문이라고 한다.
기존에 작성하던 이 긴 xml을 어노테이션으로 교체가 가능하다.
@Seungkyu
public class SK{}
이렇게만 작성해도 클래스의 정보와 해당 클래스가 어떤 인터페이스를 구현하고 있는지 알아 올 수 있기 때문이다.
이런 방식을 사용하여 XML을 최대한 작성하지 않도록 만들 수 있었고, 스프링 3.1에서는 대부분의 기능을 이 어노테이션으로 대체가 가능하다고 한다.
기존에 작성했던 xml을 어노테이션을 사용하는 방법으로 리펙토링 해보도록 하자.
자바 코드를 이용한 빈 설정
우선 test-applicationContext.xml을 사용하는 테스트를 타깃으로 리펙토링 해보자.
테스트가 작성되어 있기에, 바로 확인이 가능하기 때문이다.
해당 xml은 @ContextConfiguration(locations="/test-applicationContext.xml")을 붙인 클래스들이 사용 중이다.
이거를 그대로 가져올 클래스를 먼저 만든다.
@Configuration
public class TestApplicationContext {
}
이제 이게 xml이다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestApplicationContext.class)
class UserDaoTest {
이제 테스트 클래스의 configuration을 위치가 아닌, 해당 클래스로 변경해준다.
일단 이거도 단계적으로 나아가기 위해 처음에는 해당 xml을 불러오는 것으로 시작한다.
@Configuration
@ImportResource("/test-applicationContext.xml")
public class TestApplicationContext {
}
이렇게하고 테스트를 실행하면 당연히 성공한다.
하나씩 고치면서 계속 테스트를 실행해보자.
우선 step1은 context:annotation-config라고 한다.
사실 이거
@PostConstruct를 위해서 작성했던 것인데, @Configuration을 사용하면 자동으로 등록해주기 때문에 바로 삭제해도 문제 없다고 한다.
step2는 가장 많을 bean이다.
현재 이 DataSource를 SimpleDriverDataSource 구현체로 주입받고 있다.
이거를 클라이언트가 SimpleDriverDataSource로 주입받는 것이 아니라, DataSource 인터페이스로 받도록 해야한다.
그렇기에 이런식으로 Bean을 생성해야 한다.
@Bean
public DataSource dataSource() {
return null;
}
이제 내용과 리턴 타입을 채워보자.
xml의 내용을 setter를 사용해 그대로 넣으면 된다.
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(Driver.class);
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("1204");
return dataSource;
}
이렇게 작성하게 테스트를 돌려보면
비록 DataSource를 찾지 못해 intellij의 xml에서는
이렇게 에러가 발생한다고 나오지만, 실제 실행하면 @Bean의 정보를 불러오기에 테스트는 정상적으로 동작한다.
마찬가지로 transactionManager도 아래와 같이 옮긴다.
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource());
return transactionManager;
}
만약 다른 빈의 주입이 필요하면 이렇게 빈의 메서드를 직접 호출해서 넣어주면 된다.
여기서 현재 sqlService()를 만들지 않아서 에러가 발생 중이다.
그럴 때는 다른 곳에 있는 SqlService 빈을 불러와야 하기에, Configuration 자체에 주입 받으면 된다.
이렇게 주입받아서 사용하고, 테스트를 수행해보니 정상적으로 수행되는 것을 볼 수 있었다.
<jdbc:embedded-database id="embeddedDatabase" type="H2">
<jdbc:script location="classpath:schema.sql"/>
</jdbc:embedded-database>
이것도 옮겨야 하는데 이 친구는 <bean>이 아니다.
하지만 저번에 EmbeddedDatabaseBuilder를 사용했던 것을 떠올리며, 결국 해당 타입으로 주입을 하게 되는 것이기에 그렇게 만들어서 넣어준다.
@Bean
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setName("embeddedDatabase")
.setType(EmbeddedDatabaseType.H2)
.addScript("/schema.sql").build();
}
메서드의 이름은 우리가 등록했던 bean의 id로 해준다.
마지막으로 트랜잭션 어노테이션을 위해 해당 어노테이션까지 TestApplicationContext에 붙여준다.
@EnableTransactionManagement
이렇게해서 모든 bean을 옮겼다.
초기 테스트를 위해서 붙였던 @ImportResource를 지금은 제거가 가능하다.
그렇게 수정하고 테스트를 돌려보니 성공하는 것을 볼 수 있었다.
빈 스캐닝과 자동와이어링
이렇게 만든 빈들을 생성자나 setter를 통해 주입해야한다.
스프링은 @Autowired가 붙은 수정자 메서드가 있으면 타입을 보고 주입 할 수 있는 빈을 주입한다.
만약 타입으로 주입 가능한 빈이 2개 이상이면, 그 중에서 해당 프로퍼티와 동일한 이름의 빈으로 주입한다.
그렇기에 이렇게 동일한 타입이 2개라도 각각의 이름을 통해서 빈을 주입 가능한 것이다.
@Bean
public DataSource dataSource() {}
@Bean
public DataSource embeddedDataSource() {}
@Autowired
public UserDaoImpl(
DataSource dataSource,
SqlService sqlService) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.sqlService = sqlService;
}
이렇게 DataSource로 받지만, 실제 사용하는건 JdbcTemplate과 같은 경우 JdbcTemplate을 주입받을 수는 없으니 반드시 setter나 생성자를 통해 주입 받아야 한다.
@Component를 통해서도 빈 등록이 가능하다.
이 @Component는 클래스에 사용하는 어노테이션이고, 이 어노테이션이 부여된 클래스는 자동으로 빈으로 등록된다.
현재 작성한 UserDaoImpl을 보면
@Component
public class UserDaoImpl implements UserDao {
//
}
이렇게 해당 클래스에 @Component를 붙이는 것 만으로도
@Bean
public UserDao userDao() {
return new UserDaoImpl(dataSource(), sqlService());
}
이거를 대체할 수 있다는 것이다.
대신에 @ComponentScan을 통해 @Component들을 검색할 범위를 지정해야 한다.
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "seungkyu")
public class TestApplicationContext { }
이렇게 범위를 지정해서 그 패키지들의 하위 클래스들을 검색하도록 해야한다.
@Component에서 클래스는 해당 클래스로 등록하고, 빈의 아이디는 특별하게 지정하지 않으면 클래스의 이름을 첫글자 소문자로 바꿔서 사용한다.
현재 UserDaoImpl이기에 빈의 아이디는 userDaoImpl로 지정이 되겠지만, UserDao의 구현체이기에 해당 UserDao의 타입으로도 주입이 되게 된다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface SeungkyuComponent {
}
이런 식으로 @Component 어노테이션을 가지고 있는 어노테이션은 빈으로 등록이 된다.
그런 빈 중에서도 @Service, @Repository와 같은 특별한 역할을 하는 빈들이 있기에 맞게 달아주도록 하자.
여기는 현재 데이터베이스에 접근하는 코드이니 @Repository를 붙여주도록 하겠다.
빈을 등록하다보면 다음과 같은 상황이 생길 수 있다.
타입으로 주입을 시도하려고 했지만, 같은 타입이 2개 있고 이름으로도 주입할 수 없는 상황이다.
기존에는 UserServiceImpl을 UserService로 id를 지정해서 빈을 등록했지만, 지금은 클래스의 이름으로 등록이 되었기에 저 중 어떤 id도 userService와 맞지 않아 어떤 빈을 주입할지 에러가 생긴 것이다.
이 문제를 해결해주기 위해서는 다시 UserServiceImpl을 UserService로 등록해주면 된다.
@Component("userService")
@RequiredArgsConstructor
public class UserServiceImpl implements UserService{}
이 방법으로 빈을 등록해주도록 하자.
그러면 나머지 이 testUserServiceImpl은 어떻게 사용하게 될까?
컨텍스트 분리와 @Import
이렇게 DI를 사용해왔던 이유는 언젠가 사용할 테스트 때문이다.
그리고 지금 이 testUserServiceImpl도 테스트 때에만 사용하기에 어떻게 처리를 할지 고민하는 것이다.
다른 애플리케이션 빈에서는 분리하는 것이 더 좋아보인다.
일단 테스트에서 사용할 빈들을 따로 빼두는 Configuration을 만들어보자.
@Configuration
public class TestAppContext {
@Autowired UserDao userDao;
@Bean
public UserService testUserService(){
return new TestUserServiceImpl(userDao, mailSender());
}
@Bean
public MailSender mailSender() {
return new MailSenderTest();
}
}
이제 테스트 환경에서는 이거를 주입하면 될 거 같다.
그렇다고 이거를 ComponentScan의 범위로 넣을 수는 없을 것이기에, 해당 테스트 환경에서만 주입해주도록 해야한다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {TestApplicationContext.class, TestAppContext.class})
class UserDaoTest {}
마지막으로 데이터베이스 연결과 같은 부분도 분리하도록 하겠다.
결국 Configuration에도 같은 관심사끼리 모아두는 것이 유지보수에는 유리할 것이다.
@Configuration
public class SqlServiceContext {
@Bean
public SqlRegistry sqlRegistry() {
return new ConcurrentHashMapSqlRegistry();
}
@Bean
public SqlReader sqlReader() {
return new YmlSqlReader("/sqlmap.yml");
}
@Bean
public SqlService sqlService() {
return new YmlSqlService(sqlReader(), sqlRegistry());
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setName("embeddedDatabase")
.setType(EmbeddedDatabaseType.H2)
.addScript("/schema.sql")
.build();
}
}
이렇게 일단 지금 가지고 있는건 SqlServiceContext, TestAppContext, TestApplicationContext 이렇게 3개이다.
일단 혼동되지 않게 TestApplicationContext를 AppContext로 바꿔주자.
이거를 모두 배열로 @ContextConfiguration에 넣어줘야 할까?
하지만 여기서 의존관계를 살펴보면 SqlServiceContext를 AppContext가 필요로하고, 솔직히 테스트 클래스나 TestAppContext는 관심이 없다.
그렇기에 SqlServiceContext를 AppContext에만 직접 넣어주도록 만들어주는 것이 좋을 것 같다.
@Import(SqlServiceContext.class)
public class AppContext {}
이 방법으로 직접 넣어주고 다른 부분은 수정하지 않도록 만들자.
프로파일
현재 방법은 다음과 같은 문제가 있다.
현재 운영, 테스트가 공통으로 사용하는 AppContext와 TestAppContext 이렇게 2개가 있고 여기 모두에서 UserService 타입의 빈이 정의되어있다.
하나는 MailSenderImpl 타입이고 다른 하나는 MailSenderTest이다.
그럼 MailSender를 주입 받을 때, 어떤 타입을 주입받게 될까.
지금도 이렇게 충돌이 일어나고 있고, 우리는 테스트 환경에서는 testUserService를 데리고 오고 싶은 것이다.
이렇게 테스트 환경과 운영환경에서 각각 다른 빈을 가져와야 하는 경우가 많다.
스프링은 이렇게 환경에 따라서 빈 설정정보가 달라져야 하는 경우에 간단하게 해당 설정들을 만들 수 있도록 방법을 제공한다.
프로파일은 클래스 단위로 지정한다.
@Profile 어노테이션을 사용해서 프로파일의 이름을 넣어주면 된다.
@Configuration
@Profile("test")
public class TestAppContext {
}
이제 이렇게 하고 실행을 해보면, 당연히 실패한다.
MailSender를 가져오지 못해 실패하는데, 따로 저 profile을 활성화해주지 않아 에러가 발생하는 것이다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppContext.class, TestAppContext.class, ProductionAppContext.class})
@ActiveProfiles("test")
class UserDaoTest {}
이렇게 ActiveProfiles로 test를 활성화 해줘야, test의 profile들을 불러오게 된다.
이렇게 설정을 하고 다시 테스트를 돌려보니, 다음과 같이 성공하는 것을 볼 수 있다.
프로퍼티 소스
현재 데이터베이스의 정보가 이렇게 코드로 들어가있다.
이런 정보가 깃에 올라가면 안되기도 하고, 배포서버와 개발서버에서 다른 데이터베이스를 연결하기에 이런 정보는 코드로 작성해서는 안된다.
프로퍼티 파일로 빼도록 하자.
database.yml라는 파일을 만들고 데이터베이스의 정보는 그곳에다가 저장하도록 하겠다.
그리고 AppContext 내의 코드를 다음과 같이 수정해준다.
@Autowired
Environment environment;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
try
{
dataSource.setDriverClass((Class<? extends java.sql.Driver>)Class.forName(environment.getProperty("db.driverClass")));
}
catch (ClassNotFoundException e)
{
throw new RuntimeException(e);
}
dataSource.setUrl(environment.getProperty("db.url"));
dataSource.setUsername(environment.getProperty("db.username"));
dataSource.setPassword(environment.getProperty("db.password"));
return dataSource;
}
Enviroment를 주입받고, 거기서 하나씩 값을 빼서 사용하는 것이다.
이건 뭐랄까, 한번에 많이 가져오는 방법이고 하나씩 가져오는 방법도 있다.
@Value("${db.driverClass}")
private Class<? extends Driver> driverClassName;
@Value("${db.url}")
private String url;
@Value("${db.username}")
private String username;
@Value("${db.password}")
private String password;
이렇게 @Value로 값을 명시하고 값을 가져올 수 있다.
이 중 원하는 방법으로 변수들을 빼오면 된다.
나는 아마 @Value...?
'Spring > 토비의 스프링' 카테고리의 다른 글
[토비의 스프링] 8.1 스프링의 정의 (6) | 2025.07.28 |
---|---|
[토비의 스프링] 7.5 DI를 이용해 다양한 구현 방법 적용하기 (2) | 2025.07.22 |
[토비의 스프링] 7.4 인터페이스 상속을 통한 안전한 기능확장 (1) | 2025.07.22 |
[토비의 스프링] 7.3 서비스 추상화 적용 (2) | 2025.07.21 |
[토비의 스프링] 7.2 인터페이스의 분리와 자기참조 빈 (2) | 2025.07.11 |