JdbcTemplate - 이름 지정 파라미터 3
JdbcTemplateV2Config
JdbcTemplateItemRepositoryV2 을 스프링 빈으로 등록
@Bean
public ItemRepository itemRepository(){
return new JdbcTemplateItemRepositoryV2(dataSource); //V1에서 V2로 변경
}
ItemServiceApplication 에서 해당 설정 파일을 사용하도록 변경
@Import(JdbcTemplateV1Config.class) -> @Import(JdbcTemplateV2Config.class)
JdbcTemplate - SimpleJdbcInsert
JdbcTemplate은 INSERT SQL를 직접 작성하지 않아도 되도록 SimpleJdbcInsert 라는 편리한 기능을 제공한다.
JdbcTemplateItemRepositoryV3 클래스
/**
* SimpleJdbcInsert
*/
@Slf4j
@Repository
public class JdbcTemplateItemRepositoryV3 implements ItemRepository {
private final NamedParameterJdbcTemplate template;
private final SimpleJdbcInsert jdbcInsert;
public JdbcTemplateItemRepositoryV3(DataSource dataSource) {
this.template = new NamedParameterJdbcTemplate(dataSource);
this.jdbcInsert = new SimpleJdbcInsert(dataSource)
.withTableName("item")
.usingGeneratedKeyColumns("id")
//.usingColumns("item_name", "price", "quantity"); //생략 가능
}
@Override
public Item save(Item item) {
SqlParameterSource param = new BeanPropertySqlParameterSource(item);
Number key = jdbcInsert.executeAndReturnKey(param);
item.setId(key.longValue());
return item;
}
...
- SimpleJdbcInsert 는 생성할 때 데이터소스를 필요로 한다. 생성자를 통해 의존관계를 주입받고 생성해준다. 스프링에서는 JdbcTemplate 관련 기능을 사용할 때 관례상 이 방법을 많이 사용한다. 물론 SimpleJdbcInsert 을 스프링 빈으로 직접 등록하고 주입받아도 된다. (그러나 생성할 때 테이블명도 지정하기 때문에 사용할 곳이 정해져 있다. 따라서 빈으로 등록하고 사용하는 것은 비추)
- withTableName : 데이터를 저장할 테이블 명을 지정한다.
- usingGeneratedKeyColumns : key 를 생성하는 PK 컬럼 명을 지정한다.
- usingColumns : INSERT SQL에 사용할 컬럼을 지정한다. 특정 값만 저장하고 싶을 때 사용한다. 생략할 수 있다.
- SimpleJdbcInsert 는 생성 시점에 데이터소스를 통해 데이터베이스 테이블의 메타 데이터를 조회하기 때문에 usingColumns 을 생략할 수 있다. 만약 특정 컬럼만 지정해서 저장하고 싶다면 usingColumns 를 사용하면 된다.
- save() : jdbcInsert.executeAndReturnKey(param) 을 사용해서 파라미터 소스만 전달해주면 자동으로 INSERT SQL을 실행하고, 생성된 키 값도 KeyHoler 를 직접 사용하지 않고 매우 편리하게 조회할 수 있다.
INSERT SQL 에만 적용할 수 있기 때문에 나머지는 코드 부분은 기존과 같다.
SimpleJdbcInsert 실행 로그 (INSERT SQL)
Compiled insert object : ~
o.s.jdbc.core.simple.SimpleJdbcInsert :
Compiled insert object: insert string is [INSERT INTO item (ITEM_NAME, PRICE, QUANTITY) VALUES(?, ?, ?)]
JdbcTemplateV3Config
JdbcTemplateItemRepositoryV3 을 스프링 빈으로 등록
@Bean
public ItemRepository itemRepository() {
return new JdbcTemplateItemRepositoryV3(dataSource); //V2에서 V3로 변경
}
ItemServiceApplication 에서 해당 설정 파일을 사용하도록 변경
@Import(JdbcTemplateV2Config.class) -> @Import(JdbcTemplateV3Config.class)
JdbcTemplate 기능 정리
- JdbcTemplate : 순서 기반 파라미터 바인딩을 지원한다.
- NamedParameterJdbcTemplate : 이름 기반 파라미터 바인딩을 지원한다. (권장)
- SimpleJdbcInsert : INSERT SQL을 편리하게 사용할 수 있다.
- SimpleJdbcCall : 스토어드 프로시저를 편리하게 호출할 수 있다.
참고: 스토어드 프로시저를 사용하기 위한 SimpleJdbcCall 에 대한 자세한 내용은 다음 스프링 공식 메뉴얼을 참고하자. https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#jdbc-simple-jdbc-call-1
JdbcTemplate 사용법 정리
JdbcTemplate에 대한 사용법은 스프링 공식 메뉴얼에 자세히 소개되어 있다.
참고: 스프링 JdbcTemplate 사용 방법 공식 메뉴얼
단건 조회 - T queryForObject()
숫자 조회
int rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
조회 대상이 객체가 아니라 단순 데이터 하나라면 타입을 Integer.class , String.class 와 같이 지정해주면 된다.
문자 조회, 파라미터 바인딩
String lastName = jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?", String.class, 1212L);
객체 조회, 파라미터 바인딩
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
}, 1212L);
결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를 사용했다.
참고: RowMapper 인터페이스
@FunctionalInterface
public interface RowMapper<T> {
@Nullable
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
목록 조회 - List<T> query()
객체 조회
List<Actor> actors = jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
결과를 객체로 매핑해야 하므로 RowMapper 를 사용해야 한다. 여기서는 람다를 사용했다.
객체 조회
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor",
actorRowMapper);
위 예제와 결과가 동일하나 여기서는 RowMapper 를 분리했다. 이렇게 하면 RowMapper 를 재사용 할 수 있다.
변경(INSERT, UPDATE, DELETE) - jdbcTemplate.update()
반환값: int, SQL 실행 결과에 영향받은 로우 수
기타 기능 - execute()
임의의 SQL을 실행할 때 사용한다. 테이블을 생성하는 DDL에 사용할 수 있다.
DDL (테이블 생성)
jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
스토어드 프로시저 호출
jdbcTemplate.update("call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", Long.valueOf(unionId));
정리
실무에서 가장 간단하고 실용적인 방법으로 SQL을 사용하려면 JdbcTemplate을 사용하면 된다.
JPA와 같은 ORM 기술을 사용하면서 동시에 SQL을 직접 작성해야 할 때가 있는데, 그때도 JdbcTemplate을 함께 사용하면 된다.
그런데 JdbcTemplate의 최대 단점이 있는데, 바로 동적 쿼리 문제를 해결하지 못한다는 점이다. -> MyBatis 에서 해결
그리고 SQL을 자바 코드로 작성하기 때문에 SQL 라인이 코드를 넘어갈 때 마다 문자 더하기를 해주어야 하는 단점도 있다.
참고: JOOQ라는 기술도 동적쿼리 문제를 편리하게 해결해주지만 사용자가 많지 않아서 강의에서 다루지는 않는다.
3. 데이터 접근 기술 - 테스트
테스트 - 데이터베이스 연동
데이터 접근 기술은 실제 데이터베이스에 접근해서 데이터를 잘 저장하고 조회할 수 있는지 확인하는 것이 필요하다.
지금부터 테스트를 실행할 때 실제 데이터베이스를 연동해서 진행해보자.
테스트 케이스는 src/test 에 있기 때문에, 실행하면 src/test 에 있는 application.properties 파일이 우선순위를 가지고 실행된다. 테스트에서 데이터베이스에 접근할 수 있도록 테스트용 설정에도 데이터베이스 연결 설정을 추가하자.
데이터베이스 연결 설정
src/test/resources/application.properties
spring.profiles.active=test
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
logging.level.org.springframework.jdbc=debug
테스트 실행 - 로컬DB
@SpringBootTest
@SpringBootTest 는 @SpringBootApplication 를 찾아서 설정으로 사용한다.
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {}
즉, JdbcTemplateV3Config.class 를 스프링부트 설정으로 사용한다.
따라서 테스트도 JdbcTemplate 을 통해 실제 데이터베이스를 호출하게 된다.
ItemRepositoryTest
테스트 실행
ItemRepositoryTest 테스트 전체를 실행하자. (원래 MemoryItemRepository 를 사용하던 테스트)
실행 결과
updateitem() : 성공
save() : 성공
findItems() : 실패
@Autowired
ItemRepository itemRepository;
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
}
JdbcTemplateV3Config 설정 대로 JdbcTemplateItemRepositoryV3 를 주입 받으면서 각 테스트 실행 후 기존의 데이터가 지워지지 않는다.
H2 데이터베이스에 이미 과거에 서버를 실행하면서 저장했던 데이터가 보관되어 있다. 이 데이터가 현재 테스트에 영향을 주고 있다.
참고: TestDataInit 은 프로필이 local 일 때만 동작하므로, 프로필 test 와는 연관이 없다. 물론 현재는 local 과 test 에서 같은 데이터베이스 url 을 사용하고 있으므로 local 에서 실행했던 결과가 남아있을 수는 있다.
테스트 - 데이터베이스 분리
로컬에서 사용하는 애플리케이션 서버와 테스트에서 같은 데이터베이스를 사용하고 있으니 테스트에서 문제가 발생한다.
이런 문제를 해결하려면 테스트를 다른 환경과 철저하게 분리해야 한다.
가장 간단한 방법은 테스트 전용 데이터베이스를 별도로 운영하는 것이다.
H2 데이터베이스를 용도에 따라 2가지로 구분
- jdbc:h2:tcp://localhost/~/test local에서 접근하는 서버 전용 데이터베이스
- jdbc:h2:tcp://localhost/~/testcase test 케이스에서 사용하는 전용 데이터베이스
데이터베이스 파일 생성 방법
- 데이터베이스 서버를 종료하고 다시 실행한다.
- 사용자명은 sa 입력
- JDBC URL에 다음 입력, jdbc:h2:~/testcase (최초 한번)
- ~/testcase.mv.db 파일 생성 확인
- 이후부터는 jdbc:h2:tcp://localhost/~/testcase 로 접속
테이블 생성하기
testcase 데이터베이스에도 item 테이블을 생성하자.
drop table if exists item CASCADE;
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);
test - application.properties 변경
spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
테스트의 원칙
- 격리성: 테스트는 다른 테스트와 격리해야 한다.
- 반복성: 테스트는 반복해서 실행할 수 있어야 한다.
이전에 실패한 findItems() 테스트를 단독으로 실행하면 처음에는 실행에 성공한다.
그런데 같은 findItems() 테스트를 다시 실행하면 테스트에 실패한다.
처음 테스트를 실행할 때 저장한 데이터가 계속 남아있기 때문에 두번째 테스트에 영향을 준 것이다.
이 문제를 해결하려면 각각의 테스트가 끝날 때 마다 해당 테스트에서 추가한 데이터를 삭제해야 한다.
테스트가 끝날 때 마다 추가한 데이터에 DELETE SQL 을 사용해도 되겠지만, 이 방법도 궁극적인 해결책은 아니다.
만약 테스트 과정에서 데이터를 이미 추가했는데, 테스트가 실행되는 도중에 예외가 발생하거나 애플리케이션이 종료되어 버려서 테스트 종료 시점에 DELETE SQL 을 호출하지 못할 수 있기 때문이다.
테스트 - 데이터 롤백
트랜잭션과 롤백 전략
테스트가 끝나고 나서 트랜잭션을 강제로 롤백해버리면 데이터가 깔끔하게 제거된다.
테스트를 하면서 데이터를 이미 저장했는데, 중간에 테스트가 실패해서 롤백을 호출하지 못해도 괜찮다.
트랜잭션을 커밋하지 않았기 때문에 데이터베이스에 해당 데이터가 반영되지 않는다.
이렇게 트랜잭션을 활용하면 테스트가 끝나고 나서 데이터를 깔끔하게 원래 상태로 되돌릴 수 있다.
테스트를 트랜잭션 단위로 실행
트랜잭션 시작 -> 테스트 A 실행 -> 트랜잭션 롤백 -> 트랜잭션 시작 -> 테스트 B 실행 -> 트랜잭션 롤백
ItemRepositoryTest - 추가
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach(){
//트랜잭션 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
//트랜잭션 롤백
transactionManager.rollback(status);
}
...
- 트랜잭션 관리자는 PlatformTransactionManager 를 주입 받아서 사용한다. (스프링 부트는 자동으로 적절한 트랜잭션 매니저를 스프링 빈으로 등록해준다)
- @BeforeEach : 각각의 테스트 케이스를 실행하기 직전에 호출된다. 따라서 여기서 트랜잭션을 시작하면 된다. 그러면 각각의 테스트를 트랜잭션 범위 안에서 실행할 수 있다. transactionManager.getTransaction(new DefaultTransactionDefinition()) 로 트랜잭션을 시작한다.
- @AfterEach : 각각의 테스트 케이스가 완료된 직후에 호출된다. 따라서 여기서 트랜잭션을 롤백하면 된다. 그러면 데이터를 트랜잭션 실행 전 상태로 복구할 수 있다. transactionManager.rollback(status) 로 트랜잭션을 롤백한다.
실행 로그
JdbcTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
JdbcTransactionManager : Acquired Connection [HikariProxyConnection@1680634575 wrapping conn0: url=jdbc:h2:tcp://localhost/~/testcase user=SA] for JDBC transaction
JdbcTransactionManager : Switching JDBC Connection [HikariProxyConnection@xxx wrapping conn0: url=jdbc:h2:tcp://localhost/~/testcase user=SA] to manual commit
# 테스트 수행
JdbcTransactionManager : Initiating transaction rollback
JdbcTransactionManager : Rolling back JDBC transaction on Connection [HikariProxyConnection@1680634575 wrapping conn0: url=jdbc:h2:tcp://localhost/~/testcase user=SA]
JdbcTransactionManager : Releasing JDBC Connection [HikariProxyConnection@xxx wrapping conn0: url=jdbc:h2:tcp://localhost/~/testcase user=SA] after transaction
JdbcTemplate, JdbcInsert 등 데이터소스를 주입받아 커넥션을 사용하는 객체들은 트랜잭션이 시작되면 트랜잭션 동기화 매니저에서 커넥션을 받아서 사용하게 된다.
이제부터는 여러번 반복해서 실행해도 테스트가 성공하는 것을 확인할 수 있다.
테스트 - @Transactional
스프링은 테스트 데이터 초기화를 위해 트랜잭션을 적용하고 롤백하는 방식을 @Transactional 애노테이션 하나로 깔끔하게 해결해준다.
ItemRepositoryTest - 변경
기존의 트랜잭션 관련 코드 주석 처리 -> 클래스에 @Transactional 애노테이션 추가
@Transactional
@SpringBootTest
class ItemRepositoryTest {
@Autowired
ItemRepository itemRepository;
/* 트랜잭션 관련 코드 -> @Transactional 로 대체
@Autowired
PlatformTransactionManager transactionManager;
TransactionStatus status;
@BeforeEach
void beforeEach(){
//트랜잭션 시작
status = transactionManager.getTransaction(new DefaultTransactionDefinition());
}
*/
@AfterEach
void afterEach() {
//MemoryItemRepository 의 경우 제한적으로 사용
if (itemRepository instanceof MemoryItemRepository) {
((MemoryItemRepository) itemRepository).clearStore();
}
//트랜잭션 롤백
//transactionManager.rollback(status);
}
...
@Transactional 원리
스프링이 제공하는 @Transactional 애노테이션은 로직이 성공적으로 수행되면 트랜잭션 커밋, 예외가 발생하면 롤백하도록 동작한다.
그런데 @Transactional 애노테이션을 테스트에서 사용하면
@Test 단위로 트랜잭션을 시작하고 테스트가 끝나면 자동으로 트랜잭션 롤백을 수행한다.
클래스에 붙이면 클래스 내 모든 @Test 에 적용
메서드에 붙이면 해당 메서드만 트랜잭션 적용
참고: 테스트 케이스의 메서드나 클래스에 @Transactional 을 직접 붙여서 사용할 때만 이렇게 동작한다.
그리고 트랜잭션을 테스트에서 시작하기 때문에 서비스, 리포지토리에 있는 @Transactional 도 테스트에서 시작한 트랜잭션에 참여한다. (-> 트랜잭션 전파)
지금은 테스트에서 트랜잭션을 실행하면 테스트 실행이 종료될 때 까지 테스트가 실행하는 모든 코드가 같은 트랜잭션 범위에 들어간다고 이해하면 된다. 같은 범위라는 뜻은 쉽게 이야기해서 같은 트랜잭션을 사용한다는 뜻이다. 그리고 같은 트랜잭션을 사용한다는 것은 같은 커넥션을 사용한다는 뜻이기도 하다.
정리
테스트가 끝난 후 개발자가 직접 데이터를 삭제하지 않아도 되는 편리함을 제공한다.
테스트 실행 중에 데이터를 등록하고 중간에 테스트가 강제로 종료되어도 걱정이 없다. 이 경우 트랜잭션을 커밋하지 않기 때문에, 데이터는 자동으로 롤백된다. (보통 데이터베이스 커넥션이 끊어지면 자동으로 롤백되어 버린다.)
트랜잭션 범위 안에서 테스트를 진행하기 때문에 동시에 다른 테스트가 진행되어도 서로 영향을 주지 않는 장점이 있다.
@Transactional 애노테이션으로 테스트의 원칙(격리성,반복성)을 모두 충족할 수 있게 되었다.
강제로 커밋하기 - @Commit
@Transactional 을 테스트에서 사용하면 테스트가 끝나면 바로 롤백되기 때문에 테스트 과정에서 저장한 모든 데이터가 사라진다.
당연히 이렇게 되어야 하지만, 데이터베이스에 데이터가 잘 보관되었는지 최종 결과를 눈으로 확인하고 싶을 때도 있다.
다음과 같이 @Commit 을 클래스 또는 메서드에 붙이면 테스트 종료후 롤백 대신 커밋이 호출된다.
참고로 @Rollback(value = false) 를 대신 사용해도 된다.
import org.springframework.test.annotation.Commit;
@Commit
@Transactional
@SpringBootTest
class ItemRepositoryTest {}
- @Transactional : 트랜잭션 사용 + 테스트 끝나면 롤백
- @Transactional + @Commit 또는 @Rollbakc(false) : 트랜잭션 사용 + 테스트 끝나면 커밋
테스트 - 임베디드 모드 DB
테스트 케이스를 실행하기 위해서 별도의 데이터베이스를 설치하고, 운영하는 것은 상당히 번잡한 작업이다.
단순히 테스트를 검증할 용도로만 사용하기 때문에 테스트가 끝나면 데이터베이스의 데이터를 모두 삭제해도 된다.
더 나아가서 테스트가 끝나면 데이터베이스 자체를 제거해도 된다.
임베디드 모드
H2 데이터베이스는 자바로 개발되어 있고, JVM안에서 메모리 모드로 동작하는 특별한 기능을 제공한다.
그래서 애플리케이션을 실행할 때 H2 데이터베이스도 해당 JVM 메모리에 포함해서 함께 실행할 수 있다.
DB를 애플리케이션에 내장해서 함께 실행한다고 해서 임베디드 모드(Embedded mode)라 한다.
물론 애플리케이션이 종료되면 임베디드 모드로 동작하는 H2 데이터베이스도 함께 종료되고, 데이터도 모두 사라진다.
애플리케이션에서 자바 메모리를 함께 사용하는 라이브러리처럼 동작하기 때문이다.
임베디드 모드 직접 사용
ItemServiceApplication - 추가
@Slf4j
@Import(JdbcTemplateV3Config.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ItemServiceApplication.class, args);
}
@Bean
@Profile("test")
public DataSource dataSource(){
log.info("메모리 데이터베이스 초기화");
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
}
- @Profile("test") : 프로필이 test 인 경우에만 데이터소스를 직접 생성하여 스프링 빈으로 등록한다.
- jdbc:h2:mem:db : 이 부분이 중요하다. 데이터소스를 만들 때 url 을 이렇게 적으면 임베디드 모드(메모리 모드)로 동작하는 H2 데이터베이스를 사용할 수 있다.
- DB_CLOSE_DELAY=-1 : 임베디드 모드에서는 데이터베이스 커넥션 연결이 모두 끊어지면 데이터베이스도 종료되는데, 그것을 방지하는 설정이다.
이 데이터소스를 사용하면 테스트 케이스에서 메모리 DB를 사용할 수 있다.
H2 데이터베이스 서버가 꺼져있어도 테스트가 정상적으로 동작한다.
실행 결과 - 오류
org.springframework.jdbc.BadSqlGrammarException: PreparedStatementCallback; bad SQL grammar []; nested exception is org.h2.jdbc.JdbcSQLSyntaxErrorException:
Table "ITEM" not found; SQL statement: insert into item (item_name, price, quantity) values (?, ?, ?) [42102-200]
...
# 가장 밑에 있는 에러 메세지가 핵심적인 원인이다
Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "ITEM" not found; SQL statement:
insert into item (item_name, price, quantity) values (?, ?, ?) [42102-200]
데이터베이스 접속 성공
SQL 실행 실패 ( ITEM 테이블이 없기 때문에 )
테스트를 실행하기 전에 테이블을 먼저 생성해주어야 한다.
수동으로 할 수도 있지만 스프링 부트는 이 문제를 해결할 아주 편리한 기능을 제공해준다.
기본 SQL 스크립트를 사용해서 데이터베이스를 초기화하는 기능
메모리 DB는 애플리케이션이 종료될 때 함께 사라지기 때문에, 애플리케이션 실행 시점에 데이터베이스 테이블도 새로 만들어주어야 한다.
JDBC나 JdbcTemplate를 직접 사용해서 테이블을 생성하는 DDL을 호출해도 되지만, 너무 불편하다.
src/test/resources/schema.sql
drop table if exists item CASCADE;
create table item
(
id bigint generated by default as identity,
item_name varchar(10),
price integer,
quantity integer,
primary key (id)
);
- 파일 이름은 규칙이다.
- 테스트에서 사용하기 때문에 테스트 리소스 디렉터리에 생성하였다.
- 스프링 부트는 애플리케이션 로딩 시점에 해당 SQL 스크립트를 실행하여 데이터베이스를 초기화한다.
jdbc 로그 옵션
기본 SQL 스크립트가 잘 실행되는지 로그로 확인하려면 다음이 추가되어 있는지 확인하자.
# src/test/resources/application.properteis
logging.level.org.springframework.jdbc=debug
SQL 스트립트 로그 (애플리케이션 로딩 시점)
..init.ScriptUtils : Executing SQL script from URL [file:.../resources/test/schema.sql]
..init.ScriptUtils : 0 returned as update count for SQL: drop table if exists item CASCADE
..init.ScriptUtils : 0 returned as update count for SQL: create table item ( id bigint generated by
default as identity, item_name varchar(10), price integer, quantity integer, primary key (id) )
테스트 - 스프링 부트와 임베디드 모드
스프링 부트가 임베디드 데이터베이스에 대한 설정도 기본으로 제공한다.
스프링 부트는 데이터베이스에 대한 별다른 설정이 없으면 임베디드 데이터베이스를 사용한다.
테스트에서 데이터베이스 접근을 위한 기존 코드를 모두 삭제한다.
- ItemServiceApplication 에서 메모리DB 용 데이터소스를 스프링 빈으로 등록하는 코드 -> 삭제 or 주석처리
/*
@Bean
@Profile("test")
public DataSource dataSource(){
log.info("메모리 데이터베이스 초기화");
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:db;DB_CLOSE_DELAY=-1");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
*/
- test/resources/application.properties 에서 데이터베이스 접근 정보 -> 삭제 or 주석처리
spring.profiles.active=test
#spring.datasource.url=jdbc:h2:tcp://localhost/~/testcase
#spring.datasource.username=sa
#spring.datasource.password=
logging.level.org.springframework.jdbc=debug
이렇게 별다른 정보가 없으면 스프링 부트는 임베디드 모드로 접근하는 데이터소스( DataSource )를 만들어서 제공한다.
직접 만든 데이터소스와 비슷하다 생각하면 된다.
실행
ItemRepositoryTest 를 실행해보면 테스트가 정상 수행되는 것을 확인할 수 있다.
로그를 보면 jdbc:h2:mem 뒤에 임의의 데이터베이스 이름이 들어가 있다.
여러 데이터소스가 사용될 때 같은 데이터베이스를 사용하면서 발생하는 충돌을 방지하기 위해 스프링 부트가 임의의 이름을 부여한 것이다.
(기본적으로 히카리 커넥션 풀 사용)
HikariProxyConnection@162752456 wrapping conn0: url=jdbc:h2:mem:d8fb3a29-caf7-4b37-9b6c-b0eed9985454
참고: 임베디드 데이터베이스 이름을 스프링 부트가 기본으로 제공하는 이름 jdbc:h2:mem:testdb 로 고정하고 싶으면 application.properties 에 다음 설정을 추가하자.
spring.datasource.generate-unique-name=false
참고: 임베디드 데이터베이스에 대한 스프링 부트의 더 자세한 설정