Embedded Database가 중복 설정되어 테스트가 실패하는 경우

eora21·2023년 12월 15일
0

문제를 어떻게 발견했나요?

'토비의 스프링 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) 설정을 기입하면 됩니다.

profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글