diff --git a/README.md b/README.md index 879627617d..da097f9dcd 100644 --- a/README.md +++ b/README.md @@ -46,4 +46,34 @@ - [x] 공유자원 Singleton vs static - [x] JdbcTemplate 테스트 작성 - [x] JdbcTemplate의 executeQuery 중복 제거 -- [x] TransactionTemplate catch 절 final \ No newline at end of file +- [x] TransactionTemplate catch 절 final + +### 3차 +- [x] EOF 해결(TransactionTemplateTest, PreparedStatementCreatorTest) + +## 4단계 리뷰 +- [x] AppUserService 생성자 빈칸 제거 +- [x] TxUserService TransactionTemplate 생성자로 받도록 수정 +- [x] TransactionTemplate 반환값 없는 메서드 만들기 +- [x] Nullable TypeQualifierNickname 어떤 효과? +- [x] TransactionSyncrhonizationManager 중복되는 get() 제거 +- [x] ConnectionManager 제거 리팩토링 + - 현재 흐름 + - Transaction + 1. TransactionTemplate에게 요청 + 2. DataSourceUtils -> Connection 생성 및 ThreadLocal 등록 + 3. TransactionTemplate 트랜잭션 시작하며 로직 수행 -> JdbcTemplate 호출 + 4. JdbcTemplate은 ConnectionManager에게 Connection 요청 + 5. ConnectionManager는 ThreadLocal의 Connection 반환 + 6. JdbcTemplate DB 접근 끝 + 7. TransactionTemplate 로직 끝 -> 커밋 + 8. DataSourceUtils 통해 Connection 자원 반환, ThreadLocal에서도 제거 + - Transaction X + 1. JdbcTemplate은 ConnectionManager에게 Connection 요청 + 2. ConnectionManager는 Connection 새로 생성해서 반환 + 3. JdbcTemplate DB 접근 끝 + 4. ConnectionManager가 Connection 자원 반환 + +### 2차 +- [x] TransactionCallback 대신 Supplier 사용 +- [x] isTransactional 문제 해결 diff --git a/app/src/main/java/com/techcourse/dao/UserDao.java b/app/src/main/java/com/techcourse/dao/UserDao.java index b9f18f3552..dee6890cf1 100644 --- a/app/src/main/java/com/techcourse/dao/UserDao.java +++ b/app/src/main/java/com/techcourse/dao/UserDao.java @@ -1,6 +1,8 @@ package com.techcourse.dao; import com.techcourse.domain.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -18,6 +20,8 @@ public class UserDao { return new User(id, account, password, email); }; + private static final Logger log = LoggerFactory.getLogger(UserDao.class); + private final JdbcTemplate jdbcTemplate; public UserDao(final DataSource dataSource) { diff --git a/app/src/main/java/com/techcourse/service/AppUserService.java b/app/src/main/java/com/techcourse/service/AppUserService.java new file mode 100644 index 0000000000..1dfc9bdb08 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,33 @@ +package com.techcourse.service; + +import com.techcourse.dao.UserDao; +import com.techcourse.dao.UserHistoryDao; +import com.techcourse.domain.User; +import com.techcourse.domain.UserHistory; + +public class AppUserService implements UserService { + + private final UserDao userDao; + private final UserHistoryDao userHistoryDao; + + public AppUserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { + this.userDao = userDao; + this.userHistoryDao = userHistoryDao; + } + + public User findById(final long id) { + return userDao.findById(id) + .orElseThrow(() -> new IllegalArgumentException("유저가 존재하지 않습니다.")); + } + + public void insert(final User user) { + userDao.insert(user); + } + + public void changePassword(final long id, final String newPassword, final String createBy) { + final var user = findById(id); + user.changePassword(newPassword); + userDao.update(user); + userHistoryDao.log(new UserHistory(user, createBy)); + } +} diff --git a/app/src/main/java/com/techcourse/service/TxUserService.java b/app/src/main/java/com/techcourse/service/TxUserService.java new file mode 100644 index 0000000000..2ddc473083 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,30 @@ +package com.techcourse.service; + +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionTemplate; + +public class TxUserService implements UserService { + + private final UserService userService; + private final TransactionTemplate transactionTemplate; + + public TxUserService(final UserService userService, final TransactionTemplate transactionTemplate) { + this.userService = userService; + this.transactionTemplate = transactionTemplate; + } + + @Override + public User findById(final long id) { + return transactionTemplate.executeWithTransaction(() -> userService.findById(id)); + } + + @Override + public void insert(final User user) { + transactionTemplate.executeWithoutResult(() -> userService.insert(user)); + } + + @Override + public void changePassword(final long id, final String newPassword, final String createBy) { + transactionTemplate.executeWithoutResult(() -> userService.changePassword(id, newPassword, createBy)); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 04304f373e..42d01bf760 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,42 +1,10 @@ package com.techcourse.service; -import com.techcourse.config.DataSourceConfig; -import com.techcourse.dao.UserDao; -import com.techcourse.dao.UserHistoryDao; import com.techcourse.domain.User; -import com.techcourse.domain.UserHistory; -import org.springframework.transaction.support.TransactionTemplate; -import javax.sql.DataSource; +public interface UserService { -public class UserService { - - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; - private final TransactionTemplate transactionTemplate; - - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - final DataSource dataSource = DataSourceConfig.getInstance(); - this.transactionTemplate = new TransactionTemplate(dataSource); - } - - public User findById(final long id) { - return userDao.findById(id) - .orElseThrow(() -> new IllegalArgumentException("유저가 존재하지 않습니다.")); - } - - public void insert(final User user) { - userDao.insert(user); - } - - public void changePassword(final long id, final String newPassword, final String createBy) { - transactionTemplate.executeWithTransaction(() -> { - final var user = findById(id); - user.changePassword(newPassword); - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); - }); - } + User findById(final long id); + void insert(final User user); + void changePassword(final long id, final String newPassword, final String createBy); } diff --git a/app/src/test/java/com/techcourse/service/UserServiceTest.java b/app/src/test/java/com/techcourse/service/UserServiceTest.java index dbf94783e6..835ad51f23 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -9,6 +9,9 @@ import org.junit.jupiter.api.Test; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.sql.DataSource; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; @@ -18,11 +21,14 @@ class UserServiceTest { private JdbcTemplate jdbcTemplate; private UserDao userDao; + private TransactionTemplate transactionTemplate; @BeforeEach void setUp() { - this.jdbcTemplate = new JdbcTemplate(DataSourceConfig.getInstance()); + DataSource dataSource = DataSourceConfig.getInstance(); + this.jdbcTemplate = new JdbcTemplate(dataSource); this.userDao = new UserDao(jdbcTemplate); + this.transactionTemplate = new TransactionTemplate(dataSource); DatabasePopulatorUtils.execute(DataSourceConfig.getInstance()); final var user = new User("gugu", "password", "hkkang@woowahan.com"); @@ -32,7 +38,7 @@ void setUp() { @Test void testChangePassword() { final var userHistoryDao = new UserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + final var userService = new AppUserService(userDao, userHistoryDao); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -47,7 +53,10 @@ void testChangePassword() { void testTransactionRollback() { // 트랜잭션 롤백 테스트를 위해 mock으로 교체 final var userHistoryDao = new MockUserHistoryDao(jdbcTemplate); - final var userService = new UserService(userDao, userHistoryDao); + // 애플리케이션 서비스 + final var appUserService = new AppUserService(userDao, userHistoryDao); + // 트랜잭션 서비스 추상화 + final var userService = new TxUserService(appUserService, transactionTemplate); final User user = userDao.findById(1L).get(); final var oldPassword = user.getPassword(); diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionManager.java b/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionManager.java deleted file mode 100644 index c32e04f9b4..0000000000 --- a/jdbc/src/main/java/org/springframework/jdbc/core/ConnectionManager.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.springframework.jdbc.core; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.transaction.support.TransactionContext; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.SQLException; - -public class ConnectionManager { - - private static final Logger log = LoggerFactory.getLogger(ConnectionManager.class); - - private final DataSource dataSource; - - public ConnectionManager(final DataSource dataSource) { - this.dataSource = dataSource; - } - - public Connection getConnection() { - if (TransactionContext.isEmpty()) { - return getNewConnection(); - } - return TransactionContext.get(); - } - - private Connection getNewConnection() { - try { - return dataSource.getConnection(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } - } - - public void closeNotTransactional(Connection connection) { - if (isTransactional(connection)) { - return; - } - close(connection); - } - - private boolean isTransactional(final Connection connection) { - if (TransactionContext.isEmpty()) { - return false; - } - return TransactionContext.get() == (connection); - } - - private void close(final Connection connection) { - try { - connection.close(); - } catch (SQLException e) { - log.error(e.getMessage(), e); - throw new RuntimeException(e); - } - } -} diff --git a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java index 9343c3acdb..2aa5ecb886 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java +++ b/jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; @@ -17,10 +18,10 @@ public class JdbcTemplate { private static final Logger log = LoggerFactory.getLogger(JdbcTemplate.class); private final PreparedStatementCreator preparedStatementCreator = new PreparedStatementCreator(); - private final ConnectionManager connectionManager; + private final DataSource dataSource; public JdbcTemplate(final DataSource dataSource) { - this.connectionManager = new ConnectionManager(dataSource); + this.dataSource = dataSource; } public Optional queryForObject(final String sql, final RowMapper rowMapper, final Object... args) { @@ -28,14 +29,14 @@ public Optional queryForObject(final String sql, final RowMapper rowMa } private T executeQuery(final String sql, final SqlExecutor executor, final Object... args) { - final Connection connection = connectionManager.getConnection(); + final Connection connection = DataSourceUtils.getConnection(dataSource); try (final PreparedStatement preparedStatement = preparedStatementCreator.createPreparedStatement(connection, sql, args)) { return executor.execute(preparedStatement); } catch (final SQLException e) { log.error(e.getMessage(), e); throw new RuntimeException(e); } finally { - connectionManager.closeNotTransactional(connection); + DataSourceUtils.closeNotTransactional(dataSource, connection); } } diff --git a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java index 3c40bfec52..95ed9947b8 100644 --- a/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java +++ b/jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceUtils.java @@ -13,13 +13,12 @@ public abstract class DataSourceUtils { private DataSourceUtils() {} public static Connection getConnection(DataSource dataSource) throws CannotGetJdbcConnectionException { - Connection connection = TransactionSynchronizationManager.getResource(dataSource); - if (connection != null) { - return connection; + if (TransactionSynchronizationManager.hasResource(dataSource)) { + return TransactionSynchronizationManager.getResource(dataSource); } try { - connection = dataSource.getConnection(); + Connection connection = dataSource.getConnection(); TransactionSynchronizationManager.bindResource(dataSource, connection); return connection; } catch (SQLException ex) { @@ -27,11 +26,23 @@ public static Connection getConnection(DataSource dataSource) throws CannotGetJd } } - public static void releaseConnection(Connection connection, DataSource dataSource) { + public static void releaseConnection(Connection connection) { try { connection.close(); } catch (SQLException ex) { throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); } } + + public static void closeNotTransactional(final DataSource dataSource, final Connection connection) { + try { + if (connection.getAutoCommit()) { + TransactionSynchronizationManager.unbindResource(dataSource); + releaseConnection(connection); + } + } catch (SQLException e) { + throw new CannotGetJdbcConnectionException("Failed to close JDBC Connection"); + } + } + } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/Nullable.java b/jdbc/src/main/java/org/springframework/transaction/support/Nullable.java new file mode 100644 index 0000000000..02ce941e66 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/Nullable.java @@ -0,0 +1,14 @@ +package org.springframework.transaction.support; + +import javax.annotation.Nonnull; +import javax.annotation.meta.When; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Nonnull(when = When.MAYBE) +public @interface Nullable { +} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionCallback.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionCallback.java deleted file mode 100644 index c85947e3ee..0000000000 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionCallback.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.springframework.transaction.support; - -@FunctionalInterface -public interface TransactionCallback { - - void execute(); -} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionContext.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionContext.java deleted file mode 100644 index c8bd2738d1..0000000000 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.springframework.transaction.support; - -import java.sql.Connection; - -public class TransactionContext { - - private static final ThreadLocal status = new ThreadLocal<>(); - - public static void set(final Connection connection) { - status.set(connection); - } - - public static Connection get() { - return status.get(); - } - - public static void remove() { - status.remove(); - } - - public static boolean isEmpty() { - return status.get() == null; - } -} diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java index 715557fc66..c33fec26bc 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionSynchronizationManager.java @@ -2,22 +2,32 @@ import javax.sql.DataSource; import java.sql.Connection; +import java.util.HashMap; import java.util.Map; public abstract class TransactionSynchronizationManager { - private static final ThreadLocal> resources = new ThreadLocal<>(); + private static final ThreadLocal> resources = ThreadLocal.withInitial(HashMap::new); private TransactionSynchronizationManager() {} + private static Map currentResource() { + return resources.get(); + } + public static Connection getResource(DataSource key) { - return null; + return currentResource().get(key); } public static void bindResource(DataSource key, Connection value) { + currentResource().put(key, value); + } + + public static void unbindResource(DataSource key) { + currentResource().remove(key); } - public static Connection unbindResource(DataSource key) { - return null; + public static boolean hasResource(DataSource dataSource) { + return currentResource().containsKey(dataSource); } } diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java index b390ae4b91..b24ab46983 100644 --- a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -3,10 +3,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.exception.UndeclaredThrowableException; +import org.springframework.jdbc.datasource.DataSourceUtils; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.util.function.Supplier; public class TransactionTemplate { @@ -18,14 +20,15 @@ public TransactionTemplate(final DataSource dataSource) { this.dataSource = dataSource; } - public void executeWithTransaction(final TransactionCallback transactionCallback) { + @Nullable + public T executeWithTransaction(final Supplier supplier) { Connection connection = null; try { - connection = dataSource.getConnection(); - TransactionContext.set(connection); + connection = DataSourceUtils.getConnection(dataSource); connection.setAutoCommit(false); - transactionCallback.execute(); + final T result = supplier.get(); connection.commit(); + return result; } catch (final RuntimeException e) { if (connection != null) { rollback(connection); @@ -37,13 +40,8 @@ public void executeWithTransaction(final TransactionCallback transactionCallback } throw new UndeclaredThrowableException(e); } finally { - TransactionContext.remove(); - try { - if (connection != null) { - connection.close(); - } - } catch (final SQLException e) { - throw new RuntimeException(e); + if (connection != null) { + closeTransactional(connection); } } } @@ -56,4 +54,16 @@ private void rollback(final Connection connection) { throw new RuntimeException(e); } } + + private void closeTransactional(final Connection connection) { + TransactionSynchronizationManager.unbindResource(dataSource); + DataSourceUtils.releaseConnection(connection); + } + + public void executeWithoutResult(final Runnable action) { + executeWithTransaction(() -> { + action.run(); + return null; + }); + } } diff --git a/jdbc/src/test/java/org/springframework/jdbc/core/PreparedStatementCreatorTest.java b/jdbc/src/test/java/org/springframework/jdbc/core/PreparedStatementCreatorTest.java index ebf4db8e93..17df80ae60 100644 --- a/jdbc/src/test/java/org/springframework/jdbc/core/PreparedStatementCreatorTest.java +++ b/jdbc/src/test/java/org/springframework/jdbc/core/PreparedStatementCreatorTest.java @@ -45,4 +45,4 @@ void createPreparedStatement() throws SQLException { () -> assertThat(parameterMetaData.getParameterClassName(2)).contains("Integer") ); } -} \ No newline at end of file +} diff --git a/jdbc/src/test/java/org/springframework/transaction/support/TransactionTemplateTest.java b/jdbc/src/test/java/org/springframework/transaction/support/TransactionTemplateTest.java index d91d31b110..064f8f88ee 100644 --- a/jdbc/src/test/java/org/springframework/transaction/support/TransactionTemplateTest.java +++ b/jdbc/src/test/java/org/springframework/transaction/support/TransactionTemplateTest.java @@ -7,6 +7,7 @@ import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; +import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; @@ -27,10 +28,10 @@ void setUp() throws SQLException { @Test void executeWithTransaction() throws SQLException { // given - TransactionCallback transactionCallback = () -> System.out.println("성공"); + Supplier supplier = () -> "성공"; // when - transactionTemplate.executeWithTransaction(transactionCallback); + transactionTemplate.executeWithTransaction(supplier); // then verify(connection, times(1)).setAutoCommit(false); @@ -42,12 +43,12 @@ void executeWithTransaction() throws SQLException { void executeWithTransaction_RuntimeExceptionThrown_RollbackAndSameExceptionThrown() throws SQLException { // given RuntimeException exception = new DataAccessException("실패"); - TransactionCallback transactionCallback = () -> { + Supplier supplier = () -> { throw exception; }; // when, then - assertThatThrownBy(() -> transactionTemplate.executeWithTransaction(transactionCallback)) + assertThatThrownBy(() -> transactionTemplate.executeWithTransaction(supplier)) .isInstanceOf(exception.getClass()); verify(connection, times(1)).setAutoCommit(false); verify(connection, times(1)).rollback(); @@ -55,4 +56,4 @@ void executeWithTransaction_RuntimeExceptionThrown_RollbackAndSameExceptionThrow } -} \ No newline at end of file +} diff --git a/study/src/test/java/transaction/stage1/Stage1Test.java b/study/src/test/java/transaction/stage1/Stage1Test.java index 8c29944d4e..94bd3d8a28 100644 --- a/study/src/test/java/transaction/stage1/Stage1Test.java +++ b/study/src/test/java/transaction/stage1/Stage1Test.java @@ -58,10 +58,10 @@ private void setUp(final DataSource dataSource) { * Read phenomena | Dirty reads * Isolation level | * -----------------|------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | - + * Repeatable Read | - + * Serializable | - */ @Test void dirtyReading() throws SQLException { @@ -69,6 +69,7 @@ void dirtyReading() throws SQLException { // db에 새로운 연결(사용자A)을 받아와서 final var connection = dataSource.getConnection(); + System.out.println("connection = " + connection); // 트랜잭션을 시작한다. connection.setAutoCommit(false); @@ -81,7 +82,7 @@ void dirtyReading() throws SQLException { final var subConnection = dataSource.getConnection(); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_READ_COMMITTED; // 트랜잭션 격리 레벨을 설정한다. subConnection.setTransactionIsolation(isolationLevel); @@ -111,10 +112,10 @@ void dirtyReading() throws SQLException { * Read phenomena | Non-repeatable reads * Isolation level | * -----------------|--------------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | + + * Repeatable Read | - + * Serializable | - */ @Test void noneRepeatable() throws SQLException { @@ -130,7 +131,7 @@ void noneRepeatable() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_REPEATABLE_READ; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); @@ -173,16 +174,17 @@ void noneRepeatable() throws SQLException { * Read phenomena | Phantom reads * Isolation level | * -----------------|-------------- - * Read Uncommitted | - * Read Committed | - * Repeatable Read | - * Serializable | + * Read Uncommitted | + + * Read Committed | + + * Repeatable Read | + + * Serializable | - */ @Test void phantomReading() throws SQLException { // testcontainer로 docker를 실행해서 mysql에 연결한다. final var mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0.30")) + .withUrlParam("allowMultiQueries", "true") .withLogConsumer(new Slf4jLogConsumer(log)); mysql.start(); setUp(createMySQLDataSource(mysql)); @@ -197,7 +199,7 @@ void phantomReading() throws SQLException { connection.setAutoCommit(false); // 적절한 격리 레벨을 찾는다. - final int isolationLevel = Connection.TRANSACTION_NONE; + final int isolationLevel = Connection.TRANSACTION_SERIALIZABLE; // 트랜잭션 격리 레벨을 설정한다. connection.setTransactionIsolation(isolationLevel); diff --git a/study/src/test/java/transaction/stage2/FirstUserService.java b/study/src/test/java/transaction/stage2/FirstUserService.java index 9a1d415c18..1ab86ec269 100644 --- a/study/src/test/java/transaction/stage2/FirstUserService.java +++ b/study/src/test/java/transaction/stage2/FirstUserService.java @@ -66,7 +66,7 @@ public Set saveAndExceptionWithRequiredNew() { throw new RuntimeException(); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithSupports() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -77,7 +77,7 @@ public Set saveFirstTransactionWithSupports() { return of(firstTransactionName, secondTransactionName); } -// @Transactional(propagation = Propagation.REQUIRED) + @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithMandatory() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -88,7 +88,7 @@ public Set saveFirstTransactionWithMandatory() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNotSupported() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -99,7 +99,7 @@ public Set saveFirstTransactionWithNotSupported() { return of(firstTransactionName, secondTransactionName); } - @Transactional(propagation = Propagation.REQUIRED) +// @Transactional(propagation = Propagation.REQUIRED) public Set saveFirstTransactionWithNested() { final var firstTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); userRepository.save(User.createTest()); @@ -130,7 +130,10 @@ private Set of(final String firstTransactionName, final String secondTra private void logActualTransactionActive() { final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); + final var synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); final var emoji = actualTransactionActive ? "✅" : "❌"; + final var emoji2 = synchronizationActive ? "✅" : "❌"; log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); + log.info("\n{} is Synchronization Transaction Active : {} {}", currentTransactionName, emoji2, synchronizationActive); } } diff --git a/study/src/test/java/transaction/stage2/SecondUserService.java b/study/src/test/java/transaction/stage2/SecondUserService.java index 0d240fe854..4c9101daf0 100644 --- a/study/src/test/java/transaction/stage2/SecondUserService.java +++ b/study/src/test/java/transaction/stage2/SecondUserService.java @@ -70,7 +70,10 @@ public String saveSecondTransactionWithNever() { private void logActualTransactionActive() { final var currentTransactionName = TransactionSynchronizationManager.getCurrentTransactionName(); final var actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive(); + final var synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive(); final var emoji = actualTransactionActive ? "✅" : "❌"; + final var emoji2 = synchronizationActive ? "✅" : "❌"; log.info("\n{} is Actual Transaction Active : {} {}", currentTransactionName, emoji, actualTransactionActive); + log.info("\n{} is Synchronization Transaction Active : {} {}", currentTransactionName, emoji2, synchronizationActive); } } diff --git a/study/src/test/java/transaction/stage2/Stage2Test.java b/study/src/test/java/transaction/stage2/Stage2Test.java index e522bf4365..caca6f5a7c 100644 --- a/study/src/test/java/transaction/stage2/Stage2Test.java +++ b/study/src/test/java/transaction/stage2/Stage2Test.java @@ -45,13 +45,14 @@ void testRequired() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithRequired"); } /** * 생성된 트랜잭션이 몇 개인가? * 왜 그런 결과가 나왔을까? + * REQUIRED는 기존 트랜잭션에 합류한다. */ @Test void testRequiredNew() { @@ -59,8 +60,9 @@ void testRequiredNew() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(2) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew", + "transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew"); } /** @@ -69,17 +71,19 @@ void testRequiredNew() { */ @Test void testRequiredNewWithRollback() { - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(0); assertThatThrownBy(() -> firstUserService.saveAndExceptionWithRequiredNew()) .isInstanceOf(RuntimeException.class); - assertThat(firstUserService.findAll()).hasSize(-1); + assertThat(firstUserService.findAll()).hasSize(1); } /** * FirstUserService.saveFirstTransactionWithSupports() 메서드를 보면 @Transactional이 주석으로 되어 있다. * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. + * 찾아 보니 기존의 트랜잭션이 있으면 참여하고 없으면 트랜잭션 없이 진행한다는데, 기존의 트랜잭션 없이 해도 트랜잭션으로 진행하는 거 같다. + * Javadoc에 의하면 'SUPPORTS is slightly different from no transaction at all'라고 한다. 트랜잭션이 아예 없는 건 아닌듯 */ @Test void testSupports() { @@ -87,14 +91,15 @@ void testSupports() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithSupports"); } /** * FirstUserService.saveFirstTransactionWithMandatory() 메서드를 보면 @Transactional이 주석으로 되어 있다. * 주석인 상태에서 테스트를 실행했을 때와 주석을 해제하고 테스트를 실행했을 때 어떤 차이점이 있는지 확인해보자. * SUPPORTS와 어떤 점이 다른지도 같이 챙겨보자. + * TRANSACTION이 필수로 필요하다. 없으면 IllegalTransactionStateException 발생 */ @Test void testMandatory() { @@ -102,8 +107,8 @@ void testMandatory() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.FirstUserService.saveFirstTransactionWithMandatory"); } /** @@ -119,8 +124,8 @@ void testNotSupported() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNotSupported"); } /** @@ -133,8 +138,8 @@ void testNested() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNested"); } /** @@ -146,7 +151,7 @@ void testNever() { log.info("transactions : {}", actual); assertThat(actual) - .hasSize(0) - .containsExactly(""); + .hasSize(1) + .containsExactly("transaction.stage2.SecondUserService.saveSecondTransactionWithNever"); } }