ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Kotlin으로 REST API 만들기(9) - Transaction 적용
    Spring Boot/Kotlin으로 REST API 만들기 2022. 11. 3. 21:00
    반응형

    Kotlin으로 REST API 만들기(9) - Transaction 적용

    1. Transaction 란?
    모든 작업이 정상적으로 완료되면 Commit을 실행하고, 작업 처리 중 에러가 발생하면 Rollback하는 방식으로 처리하는 일련의 작업들을 하나의 단위로 묶어서 처리하는 것을 트랜잭션이라고 합니다.
     
    2. Transaction의 기본 원칙
    Atomicity (원자성)
    트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 합니다. (All or Nothing)
    Consistency (일관성)
    트랜잭션이 성공적으로 완료하면 모든 데이터는 일관성을 유지해야 합니다.
    Isolation (독립성, 격리성)
    트랜잭션은 독립적으로 처리되며, 처리되는 중간에 외부에서의 간섭은 없어야 합니다.
    Durablility (영속성, 지속성)
    성공적으로 완료된 트랜잭션의 결과는 영구적으로 지속되어야 한다.
     
    3. Transaction 적용
    3_1. DataSourceConfig.kt 수정
    선언적 트랜잭션 처리를 활성화하기 위해서 @EnableTransactionManagement을 추가한 후 PlatformTransactionManager의 구현 클래스 중 JDBC 기반 트랜잭션 관리자인 DataSourceTransactionManager 클래스를 빈으로 등록하세요. 

    DataSourceConfig.kt 수정

    package com.example.springbootrestapi.config
    
    import com.zaxxer.hikari.HikariDataSource
    import org.apache.ibatis.session.SqlSessionFactory
    import org.mybatis.spring.SqlSessionFactoryBean
    import org.mybatis.spring.SqlSessionTemplate
    import org.mybatis.spring.annotation.MapperScan
    import org.springframework.boot.context.properties.ConfigurationProperties
    import org.springframework.boot.jdbc.DataSourceBuilder
    import org.springframework.context.ApplicationContext
    import org.springframework.context.annotation.Bean
    import org.springframework.context.annotation.ComponentScan
    import org.springframework.context.annotation.Configuration
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver
    import org.springframework.jdbc.datasource.DataSourceTransactionManager
    import javax.sql.DataSource
    
    
    @ComponentScan(basePackages = ["com.example.springbootrestapi.service"])
    @MapperScan(
      basePackages = ["com.example.springbootrestapi.mapper"],
      sqlSessionFactoryRef = "sqlSessionFactory"
    )
    @Configuration
    class DataSourceConfiguration {
    
      @Bean
      @ConfigurationProperties(prefix = "spring.datasource.hikari")
      fun dataSource(): DataSource {
        return DataSourceBuilder.create()
          .type(HikariDataSource::class.java)
          .build()
      }
    
      @Bean
      fun sqlSessionFactory(
        dataSource: DataSource,
        applicationContext: ApplicationContext
      ): SqlSessionFactory {
        val sessionFactory = SqlSessionFactoryBean()
        sessionFactory.setDataSource(dataSource)
        sessionFactory.setMapperLocations(
          *PathMatchingResourcePatternResolver()
            .getResources("classpath:mapper/*.xml")
        )
        return sessionFactory.`object`!!
      }
    
      @Bean
      fun sqlSession(
        sqlSessionFactory: SqlSessionFactory
      ): SqlSessionTemplate {
        return SqlSessionTemplate(sqlSessionFactory)
      }
    
      @Bean
      fun transactionManager(): DataSourceTransactionManager {
        return DataSourceTransactionManager(dataSource())
      }
    }



    3_2. @Transactional 추가
    트랜잭션을 적용하려는 클래스 및 메소드에 @Transactional 추가하세요. 
    TodoService.kt

    package com.example.springbootrestapi.service
    
    import com.example.springbootrestapi.domain.TodoRequest
    import com.example.springbootrestapi.domain.TodoResponse
    import com.example.springbootrestapi.mapper.TodoMapper
    import org.springframework.stereotype.Service
    import org.springframework.transaction.annotation.Transactional
    
    
    @Service
    class TodoService(
      val todoMapper: TodoMapper
    ) {
    
      /** To-Do 조회  */
      fun getTodos(todoRequest: TodoRequest): MutableList<TodoResponse> {
        return todoMapper.getTodos(todoRequest)
      }
    
      /**
       * To-Do 상세 조회
       */
      fun getTodoById(id: Long): TodoResponse {
        return todoMapper.getTodoById(id)
      }
    
      /**
       * To-Do 저장
       */
      fun insertTodo(todoRequest: TodoRequest): TodoResponse {
        var todoResponse = TodoResponse()
        var result = todoMapper.insertTodo(todoRequest)
        if(result > 0){
          todoRequest.id?.let {
            todoResponse = todoMapper.getTodoById(it)
          }
        }
        return todoResponse
      }
    
      /**
       * To-Do 수정
       */
      fun updateTodo(todoRequest: TodoRequest): TodoResponse {
        var todoResponse = TodoResponse()
        var result = todoMapper.updateTodo(todoRequest)
        if (result > 0) {
          todoRequest.id?.let {
            todoResponse = todoMapper.getTodoById(it)
          }
        }
        return todoResponse
      }
    
      /**
       * To-Do 삭제
       */
      fun deleteTodoById(id: Long): TodoResponse {
        var todoResponse = TodoResponse()
        var result = todoMapper.deleteTodoById(id)
        if (result > 0) {
          todoResponse = todoMapper.getTodoById(id)
        }
        return todoResponse
      }
    
      /**
       * To-Do 저장 시 title이 #으로 시작하는 경우 RuntimeException 발생
       */
      @Transactional
      fun insertTodosFailed(todoRequests: List<TodoRequest>): Int {
        var result = 0
        for (todoRequest in todoRequests) {
          if (todoRequest.title.startsWith("#")) {
            throw RuntimeException("title이 #으로 시작")
          }
          result += todoMapper.insertTodo(todoRequest)
        }
        return result
      }
    }


    TodoServiceTest.kt

    package com.example.springbootrestapi.service
    
    import com.example.springbootrestapi.domain.TodoRequest
    import com.example.springbootrestapi.mapper.TodoMapper
    import org.junit.jupiter.api.Assertions.*
    import org.junit.jupiter.api.DisplayName
    import org.junit.jupiter.api.Test
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.boot.test.context.SpringBootTest
    import org.springframework.test.context.ActiveProfiles
    import org.springframework.transaction.annotation.Transactional
    
    
    @SpringBootTest
    @ActiveProfiles("local")
    class TodoServiceTest {
    
      @Autowired
      lateinit var todoService: TodoService
    
      @Autowired
      lateinit var todoMapper: TodoMapper
    
      @Transactional
      @DisplayName("getTodos_To-Do 목록 조회")
      @Test
      fun testGetTodos() {
    
        // Given
        insertTodo("Title Junit Test Insert 01", "Description Junit Test Insert 01", false)
        insertTodo("Title Junit Test Insert 02", "Description Junit Test Insert 02", true)
        insertTodo("Title Junit Test Insert 03", "Description Junit Test Insert 03", false)
        insertTodo("Title Junit Test Insert 04", "Description Junit Test Insert 04", true)
        insertTodo("Title Junit Test Insert 05", "Description Junit Test Insert 05", false)
    
        val todoRequest = TodoRequest().apply {
          this.title = "Title Junit Test Insert"
          this.description = "Description Junit Test Insert"
          this.completed = true
        }
    
        // When
        val todoResponses = todoService.getTodos(todoRequest)
    
        // Then
        assertTrue(todoResponses.isNotEmpty())
      }
    
      @Transactional
      @DisplayName("getTodoById_To-Do 상세 조회")
      @Test
      fun testGetTodoById() {
    
        // Given
        val title = "Title Junit Test Insert"
        val description = "Description Junit Test Insert"
        val completed = false
        val insertId = insertTodo(title, description, completed)
    
        // When
        val todoResponse = todoService.getTodoById(insertId)
    
        // Then
        assertEquals(title, todoResponse.title)
        assertEquals(description, todoResponse.description)
        assertEquals(completed, todoResponse.completed)
      }
    
      @Transactional
      @DisplayName("insertTodo_To-Do 저장")
      @Test
      fun testInsertTodo() {
    
        // Given
        val todoRequest = TodoRequest().apply {
          this.title = "Title Junit Test Insert"
          this.description = "Description Junit Test Insert"
          this.completed = true
        }
    
        // When
        val todoResponse = todoService.insertTodo(todoRequest)
    
        // Then
        assertEquals(todoRequest.title, todoResponse.title)
        assertEquals(todoRequest.description, todoResponse.description)
        assertEquals(todoRequest.completed, todoResponse.completed)
      }
    
      @Transactional
      @DisplayName("updateTodo_To-Do 수정")
      @Test
      fun testUpdateTodo() {
    
        // Given
        val insertId = insertTodo("Title Junit Test Insert", "Description Junit Test Insert", false)
    
        val todoRequest = TodoRequest().apply {
          this.id = insertId
          this.title = "Title Junit Test Update"
          this.description = "Description Junit Test Upate"
          this.completed = true
        }
    
        // When
        val todoResponse = todoService.updateTodo(todoRequest)
    
        // Then
        assertEquals(todoRequest.title, todoResponse.title)
        assertEquals(todoRequest.description, todoResponse.description)
        assertEquals(todoRequest.completed, todoResponse.completed)
      }
    
      @Transactional
      @DisplayName("deleteTodoById_To-Do 삭제")
      @Test
      fun testDeleteTodoById() {
    
        // Given
        val insertId = insertTodo("Title Junit Test Insert", "Description Junit Test Insert", false)
    
        // When
        val todoResponse = todoService.deleteTodoById(insertId)
    
        // Then
        assertNull(todoResponse)
      }
    
      fun insertTodo(
        title: String,
        description: String,
        completed: Boolean
      ): Long {
    
        val todoRequest = TodoRequest().apply {
          this.title = title
          this.description = description
          this.completed = completed
        }
    
        todoMapper.insertTodo(todoRequest)
    
        return todoRequest.id ?: 0
      }
    
      @DisplayName("insertTodosFailed_To-Do 저장 시 title이 #으로 시작하는 경우 RuntimeException 발생")
      @Test
      fun testInsertTodosFailed() {
    
        // Given
        val todoRequests = mutableListOf<TodoRequest>()
        todoRequests.add(
          TodoRequest().apply {
            this.title = "Title Junit Test Insert 01"
            this.description = "Description Junit Test Insert 01"
            this.completed = false
          }
        )
        todoRequests.add(
          TodoRequest().apply {
            this.title = "Title Junit Test Insert 02"
            this.description = "Description Junit Test Insert 02"
            this.completed = false
          }
        )
    
        todoRequests.add(
          TodoRequest().apply {
            this.title = "#Title Junit Test Insert 03"
            this.description = "Description Junit Test Insert 03"
            this.completed = false
          }
        )
    
        // When
        var result = 0
        try {
          result = todoService.insertTodosFailed(todoRequests)
        } catch (e: Exception) {
          e.printStackTrace()
        }
    
        // Then
        assertEquals(0, result)
      }
    }


    4. 결과 확인
    TodoServiceTest.testInsertTodosFailed() 실행 시 RuntimeException(Unchecked Exception)이 발생하여 정상적으로 롤백되는지 확인하세요.
    Error, Checked Exception : 예외 상황이 발생할 경우 트랜잭션 커밋
    Unchecked Exception : 예외 상황이 발생할 경우 트랜잭션 롤백

     

    소스 코드는 Github Repository - https://github.com/tychejin1218/kotlin-springboot-rest-api.git (branch : section08) 를 참조하세요.

    GitHub에서 프로젝트 복사하기(Get from Version Control) - https://tychejin.tistory.com/325
    단축키 기본 및 응용 - https://tychejin.tistory.com/364

    반응형

    댓글

Designed by Tistory.