들어가며
스프링의 DI 설정 방식은 크게 세 단계를 거쳐 진화했습니다.
- XML 시대 — 모든 빈 설정을 XML에 기술
- Java Config 시대 (Spring 3.x) — XML을 @Configuration 클래스로 대체
- Auto Configuration 시대 (Spring Boot) — 관례 기반 설정과 의존성 감지로 설정 자동화
1. 어노테이션 기반 프로그래밍의 진화
1.1 메타정보로서의 어노테이션
어노테이션의 역할은 코드의 동작을 직접 기술하는 게 아니라, 프레임워크가 참조하는 메타정보를 제공하는 것입니다. XML은 모든 정보를 명시적으로 기술해야 했습니다.
<bean id="userService" class="com.example.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
어노테이션은 리플렉션 API를 통해 클래스 정보를 자동으로 수집합니다.
@Service
public class UserServiceImpl implements UserService { ... }
1.2 관례 기반(Convention over Configuration) 프로그래밍
Spring 3.x에서 Spring Boot로 넘어오면서 "개발자가 직접 선언"하던 것들이 "관례에 의해 자동 처리"되는 영역으로 바뀌었습니다.
| Spring 3.x | Spring Boot | |
| ComponentScan | @ComponentScan(basePackages="com.example") 직접 지정 |
메인 클래스 패키지 기준 자동 스캔 |
| 트랜잭션 | @EnableTransactionManagement 직접 선언 |
spring-boot-starter-data-jpa 추가만으로 자동 적용 |
| DataSource | @Bean으로 직접 생성 |
application.yml 값 선언만으로 자동 생성 |
2. DataSource 설정
가장 극적으로 달라진 부분입니다. DataSource 객체 생성부터 드라이버 로딩, TransactionManager 연결까지 Spring 3.x에서는 모두 개발자 몫이었습니다.
Spring 3.x
@Configuration
@EnableTransactionManagement
@PropertySource("classpath:/database.properties")
public class AppContext {
@Value("${db.url}") String url;
@Value("${db.username}") String username;
@Value("${db.password}") String password;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource ds = new SimpleDriverDataSource();
try {
ds.setDriverClass((Class<? extends Driver>)
Class.forName("com.mysql.cj.jdbc.Driver")); // 드라이버 직접 로딩
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
ds.setUrl(this.url);
ds.setUsername(this.username);
ds.setPassword(this.password);
return ds;
}
@Bean
public PlatformTransactionManager transactionManager() {
DataSourceTransactionManager tm = new DataSourceTransactionManager();
tm.setDataSource(dataSource()); // DataSource 직접 연결
return tm;
}
}
# database.properties
db.url=jdbc:mysql://localhost/mydb
db.username=root
db.password=1234
Spring Boot 3.x
최신의 스프링은 관례를 기반으로 구조에 맞춰 파일과 정보들을 작성해놓는다면 스프링 프레임워크 내부에서 자동으로 해당 위치에 있는 파일을 읽어 설정을 구성해줍니다.
@SpringBootApplication
// DataSource, TransactionManager, JdbcTemplate 모두 자동 구성
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
# application.yml — 값 선언만, 객체 생성은 Auto Configuration이 처리
spring:
datasource:
url: jdbc:mysql://localhost/mydb
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
달라진 내용: spring-boot-starter-jdbc 의존성이 클래스패스에 있으면 DataSourceAutoConfiguration이 application.yml 값을 읽어 HikariCP 기반 DataSource를 자동 생성합니다. SimpleDriverDataSource는 커넥션 풀이 없어 프로덕션에 부적합하므로 더 이상 권장되지 않습니다.
3. 컴포넌트 스캔과 의존성 주입
Spring 3.x — 수정자 주입 + @Autowired
이전 스프링 구버전 방식은 스캔 범위를 직접 지정하고 @Autowired를 통해 프레임워크에서 자동으로 주입될 수 있도록 구성을 하였습니다. 하지만 모든 의존성에 @Autowired를 통해 주입하는 방식이 불필요한 보일러 플레이트를 만들고 순환 의존성을 가질 수 있는 문제도 있었습니다.
@Configuration
@ComponentScan(basePackages = "com.example") // 스캔 범위 직접 지정
public class AppContext { ... }
@Repository
public class UserDaoJdbc implements UserDao {
private JdbcTemplate jdbcTemplate;
@Autowired // 수정자에 @Autowired 명시
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(String id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
userRowMapper(), id
);
}
}
@Service
public class UserServiceImpl implements UserService {
private UserDao userDao;
@Autowired // 수정자에 @Autowired 명시
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Transactional
public void register(User user) {
userDao.save(user);
}
}
Spring Boot 3.x — 생성자 주입, @Autowired 불필요
이제는 @Autowired를 통해 주입하는 것이 아니라 final을 통해 생성자를 통해 주입하는 방식으로 바뀌었습니다. Lombock의 @RequiredConstructer를 통해 더욱 더 쉽게 주입될 수 있게 변화하였습니다. 이를 통해 보일러 플레이트 코드를 줄이고 순환 의존 또한 컴파일 시점에 파악할 수 있게 되었습니다.
// @SpringBootApplication이 메인 클래스 패키지 기준으로 자동 스캔
// @ComponentScan 선언 불필요
@Repository
public class UserRepository implements UserDao {
private final JdbcTemplate jdbcTemplate; // final로 불변성 보장
// 단일 생성자 → @Autowired 없이 자동 주입
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findById(String id) {
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
userRowMapper(), id
);
}
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public void register(User user) {
userRepository.save(user);
}
}
달라진 핵심: 수정자 주입은 객체 생성 후 상태가 바뀔 수 있어 불변성이 깨집니다. 생성자 주입은 final 필드를 사용할 수 있고, 주입 누락을 컴파일 시점에 감지합니다. Spring 4.3부터 단일 생성자는 @Autowired 없이도 자동 주입됩니다.
4. 설정 클래스 분리와 모듈화
Spring 3.x — @Import로 설정 클래스 직접 연결
이전에는 독립적인 모듈 설정 클래스를 만들어 @Import를 통해 설정 정보를 주입하였습니다.
// 독립 모듈 설정 클래스
@Configuration
public class SqlServiceContext {
@Bean
public SqlService sqlService() {
OxmSqlService sqlService = new OxmSqlService();
sqlService.setUnmarshaller(unmarshaller());
sqlService.setSqlRegistry(sqlRegistry());
return sqlService;
}
@Bean
public SqlRegistry sqlRegistry() { ... }
@Bean
public Unmarshaller unmarshaller() { ... }
}
// 메인 설정에서 @Import로 직접 연결
@Configuration
@Import(SqlServiceContext.class)
public class AppContext { ... }
더 나아가 @Import를 메타 어노테이션으로 감싸 의미있는 이름을 부여했습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SqlServiceContext.class)
public @interface EnableSqlService {}
// 사용
@Configuration
@EnableSqlService
public class AppContext { ... }
Spring Boot 3.x — Auto Configuration + 필요한 경우만 @Configuration
@Import와 @Enable* 패턴 자체는 현재도 동일하게 유효합니다. 허지만 달라진 점이 Spring Boot의 @EnableJpaRepositories, @EnableCaching도 같은 패턴입니다.
// Spring 3.x: AppContext, SqlServiceContext, TestAppContext 모두 직접 작성
// Spring Boot: 기본값으로 불충분한 경우에만 작성
@Configuration
public class SqlServiceConfig {
// Auto Configuration이 처리 못하는 SqlService 커스텀 설정만 담당
@Bean
public SqlService sqlService(SqlServiceProperties props) {
OxmSqlService sqlService = new OxmSqlService();
sqlService.setSqlmap(new ClassPathResource(props.getSqlmapLocation()));
return sqlService;
}
}
테스트하고 싶은 레이어에서 필요한 설정만 주입하여 테스트할 수 있습니다. @WebMvcTest도 컨트롤러 레이어를 테스트할 때 사용하고는 합니다.
@DataJpaTest
class MyRepositoryTest {
@Autowired
private MyRepository myRepository;
@Test
void testSave() {
MyEntity entity = new MyEntity("Test");
myRepository.save(entity);
List<MyEntity> result = myRepository.findAll();
assertThat(result).isNotEmpty();
}
}
5. 프로파일 - @Profile의 현대적 활용
Spring 3.x — 중첩 static 클래스로 환경 분리
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "com.example")
@Import(SqlServiceContext.class)
public class AppContext {
// 운영 환경 빈
@Configuration
@Profile("production")
public static class ProductionAppContext {
@Bean
public MailSender mailSender() {
JavaMailSenderImpl ms = new JavaMailSenderImpl();
ms.setHost("smtp.mycompany.com");
return ms;
}
}
// 테스트 환경 빈
@Configuration
@Profile("test")
public static class TestAppContext {
@Bean
public MailSender mailSender() {
return new DummyMailSender(); // 실제 메일 전송 안 함
}
}
}
// 테스트에서 프로파일과 설정 클래스를 직접 지정
@RunWith(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@ContextConfiguration(classes = AppContext.class)
public class UserServiceTest { ... }
Spring Boot 3.x — @Profile + application-{profile}.yml 조합
@Configuration
@Profile("production")
public class ProductionMailConfig {
@Bean
public MailSender mailSender() {
JavaMailSenderImpl ms = new JavaMailSenderImpl();
ms.setHost("smtp.mycompany.com");
return ms;
}
}
@TestConfiguration // 테스트 전용 설정은 @TestConfiguration으로 분리
public class TestMailConfig {
@Bean
public MailSender mailSender() {
return new JavaMailSenderImpl();
}
}
# application-prod.yml — 운영 환경 인프라 설정
spring:
datasource:
url: jdbc:mysql://prod-db.mycompany.com/mydb
# application-test.yml — 테스트 환경 인프라 설정
spring:
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL
// 테스트
@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest { ... }
// 또는 슬라이스 테스트로 더 가볍게
@DataJpaTest // H2 자동 구성, JPA 계층만 로드
class UserRepositoryTest { ... }
달라진 핵심: Spring 3.x는 환경별 빈을 @Profile로만 분리했습니다. Spring Boot는 여기에 application-{profile}.yml이 더해져 인프라 프로퍼티(DB URL, 포트 등)도 코드 수정 없이 환경별로 전환됩니다.
6. 외부 설정 - @Value에서 @ConfigurationProperties로
Spring 3.x — @PropertySource + @Value
@Configuration
@PropertySource("classpath:/database.properties")
public class AppContext {
// 프로퍼티마다 @Value 선언 반복
@Value("${db.url}") String url;
@Value("${db.username}") String username;
@Value("${db.password}") String password;
@Value("${db.driverClass}") String driverClass;
@Bean
public DataSource dataSource() {
SimpleDriverDataSource ds = new SimpleDriverDataSource();
ds.setUrl(this.url);
ds.setUsername(this.username);
// ...
return ds;
}
}
Spring Boot 3.x — @ConfigurationProperties로 타입 안전하게 묶기
@ConfigurationProperties(prefix = "sql.service")
@Validated
public class SqlServiceProperties {
@NotNull
private String sqlmapLocation; // sql.service.sqlmap-location
private String registryType = "embedded"; // sql.service.registry-type
// getters/setters ...
}
@Configuration
@EnableConfigurationProperties(SqlServiceProperties.class)
public class SqlServiceConfig {
private final SqlServiceProperties props;
public SqlServiceConfig(SqlServiceProperties props) {
this.props = props;
}
@Bean
public SqlService sqlService() {
OxmSqlService sqlService = new OxmSqlService();
sqlService.setSqlmap(new ClassPathResource(props.getSqlmapLocation()));
return sqlService;
}
}
# application.yml
sql:
service:
sqlmap-location: /sqlmap.xml
registry-type: embedded
달라진 핵심: @Value는 프로퍼티가 많아질수록 선언이 분산되고 오타를 컴파일 시점에 잡지 못합니다. @ConfigurationProperties는 관련 프로퍼티를 하나의 클래스로 묶고, @Validated로 값 검증까지 적용할 수 있습니다.
7. 전체 구조 비교
Spring 3.x 최종 구조
@Configuration
@EnableTransactionManagement // 트랜잭션 직접 활성화
@ComponentScan(basePackages = "com.example") // 스캔 범위 직접 지정
@Import(SqlServiceContext.class) // 모듈 설정 직접 연결
@PropertySource("classpath:/database.properties") // 프로퍼티 직접 연결
public class AppContext {
// DataSource @Bean 직접 정의
// TransactionManager @Bean 직접 정의
// @Value로 프로퍼티 바인딩
// @Profile 중첩 클래스로 환경 분리
}
Spring Boot 3.x 최종 구조
@SpringBootApplication
// = @Configuration
// + @EnableAutoConfiguration → DataSource, TransactionManager 등 자동 구성
// + @ComponentScan → 메인 클래스 패키지 기준 자동 스캔
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// 커스텀이 필요한 경우만 별도 @Configuration 작성
| Spring 3.x | Spring Boot | |
| DataSource 생성 | @Bean으로 직접 생성 |
application.yml 선언 → 자동 생성 |
| TransactionManager | @Bean 직접 생성 + DataSource 연결 |
자동 구성 |
| ComponentScan | basePackages 직접 지정 |
메인 클래스 위치 기준 자동 |
| 프로퍼티 바인딩 | @PropertySource + @Value |
@ConfigurationProperties |
| 의존성 주입 스타일 | 수정자 주입 + @Autowired |
생성자 주입, @Autowired 불필요 |
| 환경 분리 | @Profile 중첩 클래스 |
@Profile + application-{profile}.yml |
| 테스트 설정 | @ContextConfiguration(classes=...) 직접 지정 |
@SpringBootTest 하나로 해결 |
맺음말
Spring 3.x에서 직접 작성하던 설정의 대부분은 Spring Boot Auto Configuration으로 대체됐습니다. 그러나 DI 컨테이너가 메타정보를 어떻게 활용하는지, 왜 관례 기반 프로그래밍이 코드를 단순하게 만드는지, 모듈을 어떻게 설계해야 독립성을 유지할 수 있는지에 대한 사고 방식은 동일합니다.
설정이 자동화될수록 내부 동작 원리를 이해하는 일이 더 중요해집니다. Auto Configuration이 무엇을 어떻게 처리하는지 모르면, 기본값이 맞지 않는 상황에서 어디를 재정의해야 할지 알 수 없기 때문입니다.