(10) Spring Official Guide - Managing Transactions

HEYDAY7·2022년 10월 29일
0

Learn Kotlin + Spring

목록 보기
11/25
post-custom-banner

Managing Transactions

https://spring.io/guides/gs/managing-transactions/

한줄 요약

DB의 핵심 개념 중 하나인 Transaction을 관리하는 방법을 배웁니다.

프로젝트 구성

initailizer를 이용해 Spring Data JDBC와 H2 Database를 Dependency로 넣어서 만들어주면 된다.

Creating a Booking Service

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")
        }
    }
}

Build an Application

먼저 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 이렇게 매겨뒀으니 차례차례 알아보자

  1. "Alice", "Bob", "Carol"은 아무 문제 없기 때문에 booking을 통해서 BOOKINGS[FIRST_NAME]에 잘 들어간다.
  2. "Chris"와 "Samuel"의 경우 "Chris"는 문제가 없지만 "Samuel"이 6자 이므로 등록에 실패한다. 여기서 핵심은 book() function 안에 있는 코드로 실제적으로는 "Chris"의 경우 Table에 등록되었었지만, Transaction의 특징상 RollBack을 통해서 원 상태로 복구가 된다.
  3. "Buddy"와 null의 경우 "Buddy"는 문제가 없지만 null이 FIRST_NAME field에 들어갈 수 없기 때문에 결국 Buddy 또한 들어가지 못하게 된다.

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을 통해서 처리 된다는 것도 알게 되었다. 아마 추후에 실제로 프로젝트를 짜거나 할 때 안전한 설계를 위해 꼭 고려하게 될 것 같으며, 그 때 깊게 다시한번 파봐야 할 것 같다.

코드는 여기서 확인하면 된다.

profile
(전) Junior Android Developer (현) Backend 이직 준비생
post-custom-banner

0개의 댓글