https://spring.io/guides/gs/managing-transactions/
DB의 핵심 개념 중 하나인 Transaction을 관리하는 방법을 배웁니다.
initailizer를 이용해 Spring Data JDBC와 H2 Database를 Dependency로 넣어서 만들어주면 된다.
BookingService라는 이름으로 서비스를 만들어 주고, book function 자체를 @Transactional annotation을 붙혀서 작성해줍니다.
## BookingService.kt
@Component
class BookingService(
@Autowired private val jdbcTemplate: JdbcTemplate
) {
private val logger = LoggerFactory.getLogger(BookingService::class.java)
@Transactional
fun book(vararg persons: String?) {
for (person in persons) {
logger.info("Booking $person in a seat...")
jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person)
}
}
fun findAllBookings(): List<String> {
return jdbcTemplate.query("select FIRST_NAME from BOOKINGS") { rs, _ ->
rs.getString("FIRST_NAME")
}
}
}
먼저 JDBC Table을 만들어주는 코드를 추가한다. 여기서 FIRST_NAME column은 Not null에 최대 5자 라는 것을 기억해두자.
// main/resources/schema.sql
drop table BOOKINGS if exists;
create table BOOKINGS(ID serial, FIRST_NAME varchar(5) NOT NULL);
CommandLineRunner로 AppRunner를 만들어 동작하게 만들어주고, Transaction 관리를 알아보기 위한 테스트 코드를 작성해준다.
## AppRunner.kt
@Component
class AppRunner(
@Autowired private val bookingService: BookingService
): CommandLineRunner {
private val logger = LoggerFactory.getLogger(AppRunner::class.java)
override fun run(vararg args: String?) {
// 1
bookingService.book("Alice", "Bob", "Carol")
Assert.isTrue(bookingService.findAllBookings().size == 3,
"First booking should work with no problem")
// 2
try {
bookingService.book("Chris", "Samuel")
} catch (e: RuntimeException) {
logger.info("v--- The following exception is expect because 'Samuel' is too " +
"big for the DB ---v")
logger.error(e.message)
}
for (person in bookingService.findAllBookings()) {
logger.info("So far, $person is booked.")
}
logger.info("You shouldn't see Chris or Samuel. Samuel violated DB constraints, " +
"and Chris was rolled back in the same TX")
Assert.isTrue(bookingService.findAllBookings().size == 3,
"'Samuel' should have triggered a rollback")
// 3
try {
bookingService.book("Buddy", null)
} catch (e: RuntimeException) {
logger.info("v--- The following exception is expect because null is not " +
"valid for the DB ---v")
logger.error(e.message)
}
for (person in bookingService.findAllBookings()) {
logger.info("So far, $person is booked.")
}
logger.info("You shouldn't see Buddy or null. null violated DB constraints, and " +
"Buddy was rolled back in the same TX")
Assert.isTrue(bookingService.findAllBookings().size == 3,
"'null' should have triggered a rollback")
}
}
사실 핵심은 log가 어떻게 찍히는 줄 알기만 하면 된다. 번호를 1,2,3 이렇게 매겨뒀으니 차례차례 알아보자
2번과 3번의 실제 동작을 확실히 알고 싶으면 아래 코드처럼 book()을 수정해보는 것을 추천한다. 그러면 실제로 "Chris"와 "Buddy"가 Table에 들어갔다가 RollBack 된다는 사실을 알 수 있다.
@Transactional fun book(vararg persons: String?) { for (person in persons) { logger.info("Booking $person in a seat...") jdbcTemplate.update("insert into BOOKINGS(FIRST_NAME) values (?)", person) logger.info( jdbcTemplate.query( "select FIRST_NAME from BOOKINGS") { rs, _ -> rs.getString("FIRST_NAME") }.toString() ) } }
결국 본 가이드의 핵심은 @Transaction을 이용해보는 것과, 실제로 어떻게 동작하는지를 눈으로 확인해보는 것에 있는 것 같다. 서버 쪽 작업을 제대로해보는 것은 처음이다 보니 머리로만 알던 Transaction 개념이 실제로 rollback을 통해서 처리 된다는 것도 알게 되었다. 아마 추후에 실제로 프로젝트를 짜거나 할 때 안전한 설계를 위해 꼭 고려하게 될 것 같으며, 그 때 깊게 다시한번 파봐야 할 것 같다.
코드는 여기서 확인하면 된다.