✅ XML 설정에서 Java-based-Configuration 설정으로
지금까지 XML 설정이 아닌 Java-based-Configuration 설정을 사용해 왔습니다. 그래서 크게 바꿀 것이 없고, 다만 복습한다는 생각으로 다시 한 번 코드를 구성해보겠습니다.
- pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>toby</groupId> <artifactId>toby</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client --> <!-- MariaDB로 교체함. --> <dependency> <groupId>org.mariadb.jdbc</groupId> <artifactId>mariadb-java-client</artifactId> <version>3.1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.20</version> </dependency> <!-- Spring test, JUnit과 함께 사용하기 위해 선언 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.20</version> <!-- <scope>test</scope> --> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.8</version> </dependency> <!-- For @EnableTransactionManagement!!! --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-dao</artifactId> <version>2.0.8</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.20</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.3.20</version> </dependency> <!-- OXM 추상화 API --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>5.3.20</version> </dependency> <!-- 내장형 DB, h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>2.1.214</version> <!-- <scope>test</scope> --> </dependency> <!-- xml 바인딩 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <!-- JAXB API ContextFactory --> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <version>2.3.2</version> </dependency> <!-- For @PostConstruct annotation!!!--> <dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>1.3.5</version> </dependency> <!-- Java Mail --> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>1.6.2</version> </dependency> <!-- JUnit Test --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <!-- <scope>test</scope> --> </dependency> <!-- Mockito framework --> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.2.0</version> <!-- <scope>test</scope> --> </dependency> </dependencies> <build> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <release>11</release> <!-- JUnit4를 호환하기 위한 클래스들을 사용하지 않도록 설정함 --> <excludes> <exclude>**/VintageTest*.java</exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
@EnableTransactionManagement 어노테이션을 사용하기 위해 의존성을 추가합니다. 트랜잭션을 처리해주는 어노테이션으로 기존의 xml의 <tx:annotation-driven /> 을 대체하는 어노테이션입니다.
👉 기존의 설정을 JavaConfig 설정으로 해왔기 때문에 여기서는 통합하는 과정을 쭉 살펴보면서 넘어가겠습니다.
- TestApplicationContext
@Configuration public class TestApplicationContext {
- UserDaoTest
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestApplicationContext.class}) public class UserDaoTest {
UserDaoTest는 이제 TestApplicationContext 설정 정보를 참조하여 Spring IoC 컨테이너를 생성하고 Bean 객체를 운용합니다.
🔹Bean으로 전환
- DataSource
@Configuration public class TestApplicationContext { /** * DB연결과 트랜잭션 */ @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(Driver.class); ds.setUrl("jdbc:mariadb://localhost:3306/testdb?characterEncoding=UTF-8"); ds.setUsername("root"); ds.setPassword("1234"); return ds; }
@Bean 메소드 내부에서는 빈의 구현 클래스에 맞는 프로퍼티 값 주입이 필요하기 때문에 인터페이스 타입의 변수로 받는 것이 아니라 구현 클래스 타입으로 변수를 만들어서 사용해야 합니다.
- transactionManager
@Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; }
- testUserService
@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 testUserService() { TestUserService testService = new TestUserService(); testService.setUserDao(userDao()); testService.setMailSender(mailSender()); return testService; } @Bean public MailSender mailSender() { return new DummyMailSender(); }
testUserSerivce는 다른 여러 의존관계를 지니고 있기 때문에 이러한 의존관계에 있는 객체들도 빈으로 등록해주어야 합니다. 이때 userDao()의 sqlService() 부분에 에러가 발생하는데 이는 SqlService 빈을 아직 등록하는 메소드가 존재하지 않기 때문입니다.
- SqlService
@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 jaxb2Marshaller = new Jaxb2Marshaller(); jaxb2Marshaller.setContextPath("vol1.jhcode.ch7.user.sqlservice.jaxb"); return jaxb2Marshaller; } @Bean public DataSource embeddedDatabase() { return new EmbeddedDatabaseBuilder() .setName("embeddedDatabase") .setType(EmbeddedDatabaseType.H2) .addScript("classpath:vol1/jhcode/ch7/user/sqlservice/updatable/sqlRegistrySchema.sql") .build(); }
책에서는 XML 설정을 JavaConfig 설정으로 옮기고 있어, XML로 등록된 빈에서 EmbeddedDatabase 객체를 찾아서 @Resource 어노테이션으로 주입 받고, 해당 객체를 다시 SqlService에게 전달하는 코드를 보여주고 있습니다. 하지만 저는 XML 설정이 없기 때문에 그냥 빈을 구성했습니다.
EmbeddedDatabase와 같이 로우 레벨에서 복잡한 로직을 통해서 등록되는 빈을 원래 XML에서는 <tx:anootation-driven /> 을 사용했는데 이를 대신해서 @EnableTransactionManagement를 사용합니다. @EnableTransactionManagement 어노테이션은 일반적으로 @Configuration 어노테이션이 적용된 클래스에 선언합니다. 이 어노테이션을 사용하면 스프링은 해당 클래스 내부에서 트랜잭션과 관련된 기능을 활성화하고, @Transactional 어노테이션이 적용된 메서드들을 트랜잭션으로 래핑합니다.
@EnableTransactionManagement public class TestApplicationContext {
- TestApplicationContext
package vol1.jhcode.ch7; import javax.sql.DataSource; import org.mariadb.jdbc.Driver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import org.springframework.mail.MailSender; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import vol1.jhcode.ch7.user.dao.UserDao; import vol1.jhcode.ch7.user.dao.UserDaoJdbc; import vol1.jhcode.ch7.user.service.DummyMailSender; import vol1.jhcode.ch7.user.service.UserService; import vol1.jhcode.ch7.user.service.UserServiceImpl; import vol1.jhcode.ch7.user.service.UserServiceTest.TestUserService; import vol1.jhcode.ch7.user.sqlservice.OxmSqlService; import vol1.jhcode.ch7.user.sqlservice.SqlRegistry; import vol1.jhcode.ch7.user.sqlservice.SqlService; import vol1.jhcode.ch7.user.sqlservice.updatable.EmbeddedDbSqlRegistry; @Configuration @EnableTransactionManagement public class TestApplicationContext { /** * DB연결과 트랜잭션 */ @Bean public DataSource dataSource() { SimpleDriverDataSource ds = new SimpleDriverDataSource(); ds.setDriverClass(Driver.class); ds.setUrl("jdbc:mariadb://localhost:3306/testdb?characterEncoding=UTF-8"); ds.setUsername("root"); ds.setPassword("1234"); return ds; } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager tm = new DataSourceTransactionManager(); tm.setDataSource(dataSource()); return tm; } /** * 애플리케이션 로직 & 테스트용 빈 */ @Autowired public SqlService sqlService; @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 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 jaxb2Marshaller = new Jaxb2Marshaller(); jaxb2Marshaller.setContextPath("vol1.jhcode.ch7.user.sqlservice.jaxb"); return jaxb2Marshaller; } @Bean public DataSource embeddedDatabase() { return new EmbeddedDatabaseBuilder() .setName("embeddedDatabase") .setType(EmbeddedDatabaseType.H2) .addScript("classpath:vol1/jhcode/ch7/user/sqlservice/updatable/sqlRegistrySchema.sql") .build(); } }
✅ 빈 스캐닝과 자동와이어링
🔹@Autowired
@Autowired
는 스프링 프레임워크에서 제공하는 어노테이션으로, 자동으로 의존성을 주입하는데 사용됩니다. 스프링은 빈(Bean)으로 등록된 객체들을 관리하고, @Autowired
어노테이션을 사용하면 해당 필드, 생성자 또는 메서드 파라미터에 자동으로 빈을 주입하여 의존성을 해결합니다.
- UserDaoJdbc
@Autowired private SqlService sqlService; @Autowired public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
@Autowired가 붙은 필드는 스프링이 동일한 타입의 빈을 찾고, 만약 두 개 이상이라면 변수의 명과 동일한 빈의 이름을 가진 빈을 찾아서 주입합니다.
@Autowired가 붙은 setter 메소드는 스프링이 파라미터의 타입을 보고 주입 가능한 타입의 빈을 모두 찾습니다. 하나라면 그대로 주입하고, 두 개라면 파라미터의 이름과 빈의 이름이 동일한 빈을 주입합니다. JdbcTemplate에 @Autowired를 붙이지 않는 이유는 JdbcTemplate은 DataSource 객체를 통해서 초기화되서 저장되어야 하기 때문입니다.
- TestApplicationContext
@Bean public UserDao userDao() { return new UserDaoJdbc(); }
@Autowired 어노테이션을 사용했기 때문에 관계 설정하는 부분이 사라지게 됩니다. 필드의 접근 제한자가 private일 때 외부에서 값을 넣을 수 없지만 스프링은 리플렉션 API를 이용해 제약조건을 우회해서 값을 넣어줍니다.
- TestApplicationContext
🔹@Component
@Component
어노테이션은 스프링 프레임워크에서 제공하는 어노테이션 중 하나로, 클래스를 빈(Bean)으로 등록하기 위해 사용됩니다. 스프링은 빈으로 등록된 객체들을 컨테이너에 보관하고, 필요한 곳에서 해당 빈을 가져와서 사용할 수 있도록 해줍니다.
- UserDaoJdbc
@Component("userDao") public class UserDaoJdbc implements UserDao {
- TestApplicationContext
@ComponentScan(basePackages = "vol1.jhcode.ch7.user") public class TestApplicationContext { //... 생략 @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 어노테이션을 사용해서 빈을 생성하면 클래스의 타입과 클래스의 이름의 앞글자가 소문자인 형식으로 빈이 등록됩니다. 현재 등록된 빈은 다음과 같습니다.
key : userDaoJdbc value : UserDaoJdbc
UserDao 타입으로 @Autowired 받았지만, UserDaoJdbc가 UserDao 인터페이스를 구현했기 때문에 다형성을 통해 상위 타입으로 받을 수 있습니다. 그러나, 현재 userDaoJdbc라는 이름으로 등록되었기 때문에 userDao 필드명에는 주입이 될 수도 있고 안 될 수도 있습니다. 동일한 타입의 빈이 또 있다면 되지 않을 것입니다. 그렇기 때문에 @Component의 name 속성을 사용하거나 @Qualifier 어노테이션을 통해서 명시적으로 관계 설정을 해주는 것이 좋습니다.
- UserServiceImpl
@Service("userService") public class UserServiceImpl implements UserService { public static final int MIN_LOGCOUNT_FOR_SILVER = 50; public static final int MIN_RECCOMEND_FOR_GOLD = 30; @Autowired private UserDao userDao; @Autowired private MailSender mailSender;
@Service 어노테이션은 @Component 어노테이션을 확장하여 만든 어노테이션이라고 생각하면 쉽습니다. 일종의 성격을 분류하기 쉽게 확장되었습니다. 서비스 계층의 빈을 구별할 수 있게 해줍니다.
- TestApplicationConetext
@Bean public UserService userService() { return new UserServiceImpl(); } @Bean public UserService testUserService() { return new TestUserService(); } @Bean public MailSender mailSender() { return new DummyMailSender(); }
- TestApplicationConetext
📖 토비 스프링 3.1 -p645~682
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T