'토비의 스프링 3.1'을 읽으며 코드를 구현하고 있었습니다.
책의 예시와 다르게 특정 테스트에서 @MockBean
, @SpyBean
을 사용했더니 Spring Context가 새로 생성되는 문제가 일어났고, Context의 캐싱을 사용하지 못하는 것은 물론 특정 빈의 @PostConstruct
가 중복해서 동작하는 상황을 발견할 수 있었습니다.
class OxmSqlService {
...
@PostConstruct
public void loadSql() {
baseSqlService.setSqlReader(oxmSqlReader);
baseSqlService.setSqlRegistry(sqlRegistry);
baseSqlService.loadSql();
}
...
}
OxmSqlService에서는 baseSqlService.loadSql();
을 호출하며, 이로 인해schema.sql
에 있는 쿼리를 통해 테이블 생성 및 sqlmap.xml
에 적힌 값들을 내장 DB에 넣습니다.
CREATE TABLE SQLMAP (
KEY_ VARCHAR(100) PRIMARY KEY,
SQL_ VARCHAR(100) NOT NULL
);
<?xml version="1.0" encoding="UTF-8"?>
<sqlmap>
<sql key="userAdd">insert into users(id, name, password, level, login, recommend, email) values (?,?,?,?,?,?,?)</sql>
<sql key="userDeleteAll">delete from users</sql>
<sql key="userGet">select * from users where id = ?</sql>
<sql key="userGetAll">select * from users order by id</sql>
<sql key="userGetCount">select count(*) from users</sql>
<sql key="userUpdate">update users set name = ?, password = ?, level = ?, login = ?, recommend = ?, email = ? where id = ?</sql>
</sqlmap>
내장 DB에 sql로 선언된 테이블이 생성되며, xml로 작성된 내용을 받아 key-value로 기입합니다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "classpath:testApplicationContext.xml")
class UserServiceTest {
@Autowired
ApplicationContext context;
@Autowired
UserService userService;
@SpyBean
UserDao userDao;
@SpyBean
NormalLevelUpgradePolicy userLevelUpgradePolicy;
@MockBean
MailSender mailSender;
@Autowired
UserService testUserService;
...
UserServiceTest는 @MockBean
과 @SpyBean
을 사용하는데, 해당 부분에서 생성되는 Context와 다른 테스트코드에서 생성되는 Context가 다르므로 OxmSqlService의 @PostConstruct
가 두 번 동작하며, 이는 내장 DB에 해당 테이블과 데이터를 다시금 반영하려 합니다.
그러나 이미 해당하는 테이블과 데이터들이 있기 때문에 예외가 발생하게 됩니다.
근본적인 문제는 Embedded DB가 테스트 동작 후 shut down되지 않기 때문입니다. Embedded DB는 컨텍스트가 유효하다면 종료되지 않고 대기합니다.
@MockBean
, @SpyBean
을 사용하게 되면 기존 Context의 원본 Bean이 아닌, 테스트 더블을 위한 Proxy Bean이 그 자리를 대체합니다.
이후의 테스트들에서는 기존의 Context가 오염되었다고 판단하여 다시금 Context를 만들게 되는데, 이 때 Embedded DB의 종료가 이뤄지지 않습니다(기존의 Context를 삭제하고 다시 만드는 게 아닌, 새 객체를 만드는 형태이기 때문으로 추측됩니다).
이러한 상황에서 Context가 재생성되며 Embedded DB 생성 쿼리를 동작시킬 경우 기존의 데이터와 충돌하여 예외가 발생하게 된 것입니다.
유효 Context와 Embedded DB의 상태를 맞춰주도록 작성해야 합니다.
Embedded DB가 소멸되지 않았을 때 유효 Context가 생길 경우 문제가 발생한다는 것을 알 수 있었습니다.
<jdbc:embedded-database id="embeddedDatabase" type="H2" generate-name="true">
<jdbc:script location="classpath:db/schema.sql"/>
</jdbc:embedded-database>
generate-name="true"
설정으로 Embedded DB가 생성될 때 커넥션 url을 매번 다르게 생성하여 각각의 테스트가 각각의 다른 DB를 참조하게 하면 이러한 문제를 해결할 수 있습니다.
@Bean
을 이용해 만드는 상황이라면
@Bean
public DataSource embeddedDatabase() {
return new EmbeddedDatabaseBuilder()
.generateUniqueName(true)
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:db/schema.sql")
.build();
}
generateUniqueName(true)
설정을 기입하면 됩니다.