-
[Spring Boot] REST API 만들기(9) - Transaction 적용Spring Boot/2.7.x - REST API 만들기 2022. 10. 11. 18:04반응형
REST API 만들기(9) - Transaction 적용
1. Transaction 란?
모든 작업이 정상적으로 완료되면 Commit을 실행하고, 작업 처리 중 에러가 발생하면 Rollback하는 방식으로 처리하는 일련의 작업들을 하나의 단위로 묶어서 처리하는 것을 트랜잭션이라고 합니다.
2. Transaction의 기본 원칙
Atomicity (원자성)
트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야 합니다. (All or Nothing)
Consistency (일관성)
트랜잭션이 성공적으로 완료하면 모든 데이터는 일관성을 유지해야 합니다.
Isolation (독립성, 격리성)
트랜잭션은 독립적으로 처리되며, 처리되는 중간에 외부에서의 간섭은 없어야 합니다.
Durablility (영속성, 지속성)
성공적으로 완료된 트랜잭션의 결과는 영구적으로 지속되어야 한다.3. Transaction 적용
3_1. DataSourceConfig.java 수정
선언적 트랜잭션 처리를 활성화하기 위해서 @EnableTransactionManagement을 추가한 후 PlatformTransactionManager의 구현 클래스 중 JDBC 기반 트랜잭션 관리자인 DataSourceTransactionManager 클래스를 빈으로 등록하세요.
DataSourceConfig.java 수정package com.example.springbootrestapi.config; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; 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 org.springframework.transaction.annotation.EnableTransactionManagement; @EnableTransactionManagement @ComponentScan(basePackages = "com.example.springbootrestapi.service") @MapperScan( basePackages = "com.example.springbootrestapi.mapper", sqlSessionFactoryRef = "sqlSessionFactory" ) @Configuration public class DataSourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource.hikari") public DataSource dataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .build(); } @Bean public SqlSessionFactory sqlSessionFactory( DataSource dataSource, ApplicationContext applicationContext ) throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml")); return sessionFactory.getObject(); } @Bean public SqlSessionTemplate sqlSession( SqlSessionFactory sqlSessionFactory ) throws Exception { return new SqlSessionTemplate(sqlSessionFactory); } @Bean public DataSourceTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } }
3_1. @Transactional 추가
트랜잭션을 적용하려는 클래스 및 메소드에 @Transactional 추가하세요.TodoService.java
package com.example.springbootrestapi.service; import com.example.springbootrestapi.domain.Todo; import com.example.springbootrestapi.mapper.TodoMapper; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; @RequiredArgsConstructor @Service public class TodoService { private final TodoMapper todoMapper; /** * To-Do 조회 */ @Transactional(readOnly = true) public List<Todo.Response> getTodos(Todo.Request todoRequest) { return todoMapper.getTodos(todoRequest); } /** * To-Do 상세 조회 */ @Transactional(readOnly = true) public Todo.Response getTodoById(int id) { return todoMapper.getTodoById(id); } /** * To-Do 저장 */ @Transactional public Todo.Response insertTodo(Todo.Request todoRequest) { Todo.Response todoResponse = Todo.Response.builder().build(); int result = todoMapper.insertTodo(todoRequest); if (result > 0 && !ObjectUtils.isEmpty(todoRequest.getId())) { todoResponse = todoMapper.getTodoById(todoRequest.getId()); } return todoResponse; } /** * To-Do 수정 */ @Transactional public Todo.Response updateTodo(Todo.Request todoRequest) { Todo.Response todoResponse = Todo.Response.builder().build(); int result = todoMapper.updateTodo(todoRequest); if (result > 0) { todoResponse = todoMapper.getTodoById(todoRequest.getId()); } return todoResponse; } /** * To-Do 삭제 */ @Transactional public Todo.Response deleteTodoById(int id) { Todo.Response todoResponse = Todo.Response.builder().build(); int result = todoMapper.deleteTodoById(id); if (result > 0) { todoResponse = todoMapper.getTodoById(id); } return todoResponse; } /** * To-Do 저장 시 title이 #으로 시작하는 경우 RuntimeException 발생 */ @Transactional public int insertTodosFailed(List<Todo.Request> todoRequests) { int result = 0; for (Todo.Request todoRequest : todoRequests) { if (todoRequest.getTitle().startsWith("#")) { throw new RuntimeException("title이 #으로 시작"); } result += todoMapper.insertTodo(todoRequest); } return result; } }
TodoServiceTest.javapackage com.example.springbootrestapi.service; import com.example.springbootrestapi.domain.Todo; import com.example.springbootrestapi.domain.Todo.Response; import com.example.springbootrestapi.mapper.TodoMapper; import java.util.ArrayList; import java.util.List; import javax.annotation.Resource; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; @SpringBootTest @ActiveProfiles("local") class TodoServiceTest { @Resource TodoService todoService; @Resource TodoMapper todoMapper; @Transactional @DisplayName("getTodos_To-Do 목록 조회") @Test void 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); Todo.Request todoRequest = Todo.Request.builder() .title("Title Junit Test Insert") .description("Description Junit Test Insert") .completed(true) .build(); // When List<Response> todoResponses = todoService.getTodos(todoRequest); // Then Assertions.assertTrue(!todoResponses.isEmpty()); } @Transactional @DisplayName("getTodoById_To-Do 상세 조회") @Test void testGetTodoById() { // Given String title = "Title Junit Test Insert"; String description = "Description Junit Test Insert"; boolean completed = false; int insertId = insertTodo(title, description, completed); // When Todo.Response todoResponse = todoService.getTodoById(insertId); // Then Assertions.assertEquals(title, todoResponse.getTitle()); Assertions.assertEquals(description, todoResponse.getDescription()); Assertions.assertEquals(completed, todoResponse.getCompleted()); } @Transactional @DisplayName("insertTodo_To-Do 저장") @Test void testInsertTodo() { // Given Todo.Request todoRequest = Todo.Request.builder() .title("Title Junit Test Insert") .description("Description Junit Test Insert") .completed(false) .build(); // When Todo.Response todoResponse = todoService.insertTodo(todoRequest); // Then Assertions.assertEquals(todoRequest.getTitle(), todoResponse.getTitle()); Assertions.assertEquals(todoRequest.getDescription(), todoResponse.getDescription()); Assertions.assertEquals(todoRequest.getCompleted(), todoResponse.getCompleted()); } @Transactional @DisplayName("updateTodo_To-Do 수정") @Test void testUpdateTodo() { // Given String title = "Title Junit Test Insert"; String description = "Description Junit Test Insert"; boolean completed = false; int insertId = insertTodo(title, description, completed); Todo.Request todoRequest = Todo.Request.builder() .id(insertId) .title("Title Junit Test Update") .description("Description Junit Test Update") .completed(true) .build(); // When Todo.Response todoResponse = todoService.updateTodo(todoRequest); // Then Assertions.assertEquals(todoRequest.getTitle(), todoResponse.getTitle()); Assertions.assertEquals(todoRequest.getDescription(), todoResponse.getDescription()); Assertions.assertEquals(todoRequest.getCompleted(), todoResponse.getCompleted()); } @Transactional @DisplayName("deleteTodoById_To-Do 삭제") @Test void testDeleteTodoById() { // Given int insertId = insertTodo("Title Junit Test Insert", "Description Junit Test Insert", false); // When Todo.Response todoResponse = todoService.deleteTodoById(insertId); // Then Assertions.assertNull(todoResponse); } /** * To-Do 저장 */ int insertTodo( String title, String description, Boolean completed) { Todo.Request todoRequest = Todo.Request.builder() .title(title) .description(description) .completed(completed) .build(); todoMapper.insertTodo(todoRequest); return todoRequest.getId(); } @DisplayName("insertTodosFailed_To-Do 저장 시 title이 #으로 시작하는 경우 RuntimeException 발생") @Test void testInsertTodosFailed() { // Given List<Todo.Request> todoRequests = new ArrayList<>(); todoRequests.add(Todo.Request.builder() .title("Title Junit Test Insert 01") .description("Description Junit Test Insert 02") .completed(false) .build()); todoRequests.add(Todo.Request.builder() .title("Title Junit Test Insert 02") .description("Description Junit Test Insert 02") .completed(false) .build()); todoRequests.add(Todo.Request.builder() .title("#Title Junit Test Insert 03") .description("Description Junit Test Insert 03") .completed(false) .build()); // When int result = 0; try { result = todoService.insertTodosFailed(todoRequests); } catch (Exception e) { e.printStackTrace(); } // Then Assertions.assertEquals(0, result); } }
4. 결과 확인
TodoServiceTest.testInsertTodosFailed() 실행 시 RuntimeException(Unchecked Exception)이 발생하여 정상적으로 롤백되는지 확인하세요.- Error, Checked Exception : 예외 상황이 발생할 경우 트랜잭션 커밋
- Unchecked Exception : 예외 상황이 발생할 경우 트랜잭션 롤백
소스 코드는 Github Repository - https://github.com/tychejin1218/springboot-rest-api (branch : section09) 를 참조하세요.
GitHub에서 프로젝트 복사하기(Get from Version Control) - https://tychejin.tistory.com/325반응형'Spring Boot > 2.7.x - REST API 만들기' 카테고리의 다른 글
[Spring Boot] REST API 만들기(8) - Interceptor 적용 (0) 2022.10.07 [Spring Boot] REST API 만들기(7) - Controller 구현 및 단위 테스트(Junit5) (0) 2022.10.05 [Spring Boot] REST API 만들기(6) - Service 구현 및 단위 테스트(Junit5) (0) 2022.10.03 [Spring Boot] REST API 만들기(5) - Mapper 구현 및 단위 테스트(Junit5) (0) 2022.09.25 [Spring Boot] REST API 만들기(4) - Log4jdbc 설정 (0) 2022.09.15