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..a8d453b094 --- /dev/null +++ b/app/src/main/java/com/techcourse/service/AppUserService.java @@ -0,0 +1,35 @@ +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; + } + + @Override + public User findById(final long id) { + return userDao.findById(id); + } + + @Override + public void insert(final User user) { + userDao.insert(user); + } + + @Override + 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..101410662a --- /dev/null +++ b/app/src/main/java/com/techcourse/service/TxUserService.java @@ -0,0 +1,31 @@ +package com.techcourse.service; + +import com.techcourse.config.DataSourceConfig; +import com.techcourse.domain.User; +import org.springframework.transaction.support.TransactionTemplate; + +public class TxUserService implements UserService { + + private final UserService userService; + + public TxUserService(UserService userService) { + this.userService = userService; + } + + @Override + public User findById(long id) { + return TransactionTemplate.query(() -> userService.findById(id), DataSourceConfig.getInstance()); + } + + @Override + public void insert(User user) { + TransactionTemplate.execute(() -> userService.insert(user), DataSourceConfig.getInstance()); + } + + @Override + public void changePassword(long id, String newPassword, String createBy) { + TransactionTemplate.execute( + () -> userService.changePassword(id, newPassword, createBy), DataSourceConfig.getInstance() + ); + } +} diff --git a/app/src/main/java/com/techcourse/service/UserService.java b/app/src/main/java/com/techcourse/service/UserService.java index 205ff1adfe..b14dbcacbf 100644 --- a/app/src/main/java/com/techcourse/service/UserService.java +++ b/app/src/main/java/com/techcourse/service/UserService.java @@ -1,55 +1,12 @@ 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.dao.DataAccessException; -import org.springframework.jdbc.datasource.DataSourceUtils; -import java.sql.SQLException; +public interface UserService { -public class UserService { + User findById(final long id); - private final UserDao userDao; - private final UserHistoryDao userHistoryDao; + void insert(final User user); - public UserService(final UserDao userDao, final UserHistoryDao userHistoryDao) { - this.userDao = userDao; - this.userHistoryDao = userHistoryDao; - } - - public User findById(final long id) { - return userDao.findById(id); - } - - 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); - - final var dataSource = DataSourceConfig.getInstance(); - final var connection = DataSourceUtils.getConnection(dataSource); - try { - connection.setAutoCommit(false); - - userDao.update(user); - userHistoryDao.log(new UserHistory(user, createBy)); - - connection.commit(); - } catch (SQLException e) { - try { - connection.rollback(); - throw new DataAccessException(e); - } catch (SQLException e2) { - throw new DataAccessException(e2); - } - } finally { - DataSourceUtils.releaseConnection(dataSource); - } - } + 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 83bc1d3505..91f9bf60df 100644 --- a/app/src/test/java/com/techcourse/service/UserServiceTest.java +++ b/app/src/test/java/com/techcourse/service/UserServiceTest.java @@ -31,7 +31,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"; @@ -46,7 +46,9 @@ 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); + final var newPassword = "newPassword"; final var createBy = "gugu"; diff --git a/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java new file mode 100644 index 0000000000..51bf4f42a3 --- /dev/null +++ b/jdbc/src/main/java/org/springframework/transaction/support/TransactionTemplate.java @@ -0,0 +1,47 @@ +package org.springframework.transaction.support; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.datasource.DataSourceUtils; + +import javax.sql.DataSource; +import java.sql.SQLException; +import java.util.function.Supplier; + +public class TransactionTemplate { + + private TransactionTemplate() { + } + + public static void execute(Runnable transactionCallback, DataSource dataSource) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + + transactionCallback.run(); + + connection.commit(); + } catch (SQLException e) { + try { + connection.rollback(); + throw new DataAccessException(e); + } catch (SQLException e2) { + throw new DataAccessException(e2); + } + } finally { + DataSourceUtils.releaseConnection(dataSource); + } + } + + public static T query(Supplier transactionCallback, DataSource dataSource) { + final var connection = DataSourceUtils.getConnection(dataSource); + try { + connection.setAutoCommit(false); + + return transactionCallback.get(); + } catch (SQLException e) { + throw new DataAccessException(e); + } finally { + DataSourceUtils.releaseConnection(dataSource); + } + } +} diff --git a/study/src/test/java/aop/stage0/Stage0Test.java b/study/src/test/java/aop/stage0/Stage0Test.java index 079cc6b5a0..907efdc4de 100644 --- a/study/src/test/java/aop/stage0/Stage0Test.java +++ b/study/src/test/java/aop/stage0/Stage0Test.java @@ -15,6 +15,8 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.PlatformTransactionManager; +import java.lang.reflect.Proxy; + import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; @@ -45,7 +47,11 @@ void setUp() { @Test void testChangePassword() { final var appUserService = new AppUserService(userDao, userHistoryDao); - final UserService userService = null; + final UserService userService = (UserService) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {UserService.class}, + new TransactionHandler(platformTransactionManager, appUserService) + ); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -59,7 +65,11 @@ void testChangePassword() { @Test void testTransactionRollback() { final var appUserService = new AppUserService(userDao, stubUserHistoryDao); - final UserService userService = null; + final UserService userService = (UserService) Proxy.newProxyInstance( + getClass().getClassLoader(), + new Class[] {UserService.class}, + new TransactionHandler(platformTransactionManager, appUserService) + ); final var newPassword = "newPassword"; final var createBy = "gugu"; diff --git a/study/src/test/java/aop/stage0/TransactionHandler.java b/study/src/test/java/aop/stage0/TransactionHandler.java index 2aa8445aba..858fcbad6c 100644 --- a/study/src/test/java/aop/stage0/TransactionHandler.java +++ b/study/src/test/java/aop/stage0/TransactionHandler.java @@ -1,15 +1,38 @@ package aop.stage0; +import aop.DataAccessException; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionDefinition; + import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class TransactionHandler implements InvocationHandler { + private final PlatformTransactionManager transactionManager; + + private final Object target; + + public TransactionHandler(PlatformTransactionManager transactionManager, Object target) { + this.transactionManager = transactionManager; + this.target = target; + } + /** * @Transactional 어노테이션이 존재하는 메서드만 트랜잭션 기능을 적용하도록 만들어보자. */ @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { - return null; + final var transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition()); + + try { + final var ret = method.invoke(target, args); + + transactionManager.commit(transactionStatus); + return ret; + } catch (Exception e) { + transactionManager.rollback(transactionStatus); + throw new DataAccessException(e); + } } } diff --git a/study/src/test/java/aop/stage1/Stage1Test.java b/study/src/test/java/aop/stage1/Stage1Test.java index 113b2e7d03..3976e8c350 100644 --- a/study/src/test/java/aop/stage1/Stage1Test.java +++ b/study/src/test/java/aop/stage1/Stage1Test.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.aop.framework.ProxyFactoryBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.PlatformTransactionManager; @@ -41,7 +42,17 @@ void setUp() { @Test void testChangePassword() { - final UserService userService = null; + final var target = new UserService(userDao, userHistoryDao); + + final var proxyFactoryBean = new ProxyFactoryBean(); + proxyFactoryBean.setTarget(target); + + proxyFactoryBean.setProxyTargetClass(true); + + final var pointcut = new TransactionPointcut(); + final var advice = new TransactionAdvice(platformTransactionManager); + proxyFactoryBean.addAdvisor(new TransactionAdvisor(pointcut, advice)); + final UserService userService = (UserService) proxyFactoryBean.getObject(); final var newPassword = "qqqqq"; final var createBy = "gugu"; @@ -54,7 +65,17 @@ void testChangePassword() { @Test void testTransactionRollback() { - final UserService userService = null; + final var target = new UserService(userDao, stubUserHistoryDao); + + final var proxyFactoryBean = new ProxyFactoryBean(); + proxyFactoryBean.setTarget(target); + + proxyFactoryBean.setProxyTargetClass(true); + + final var pointcut = new TransactionPointcut(); + final var advice = new TransactionAdvice(platformTransactionManager); + proxyFactoryBean.addAdvisor(new TransactionAdvisor(pointcut, advice)); + final UserService userService = (UserService) proxyFactoryBean.getObject(); final var newPassword = "newPassword"; final var createBy = "gugu"; diff --git a/study/src/test/java/aop/stage1/TransactionAdvice.java b/study/src/test/java/aop/stage1/TransactionAdvice.java index 03a03a84e5..346f517688 100644 --- a/study/src/test/java/aop/stage1/TransactionAdvice.java +++ b/study/src/test/java/aop/stage1/TransactionAdvice.java @@ -1,15 +1,33 @@ package aop.stage1; +import aop.DataAccessException; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionDefinition; /** * 어드바이스(advice). 부가기능을 담고 있는 클래스 */ public class TransactionAdvice implements MethodInterceptor { + private final PlatformTransactionManager platformTransactionManager; + + public TransactionAdvice(PlatformTransactionManager platformTransactionManager) { + this.platformTransactionManager = platformTransactionManager; + } + @Override public Object invoke(final MethodInvocation invocation) throws Throwable { - return null; + final var transactionStatus = platformTransactionManager.getTransaction(new DefaultTransactionDefinition()); + + try { + final var ret = invocation.proceed(); + platformTransactionManager.commit(transactionStatus); + return ret; + } catch (Exception e) { + platformTransactionManager.rollback(transactionStatus); + throw new DataAccessException(e); + } } } diff --git a/study/src/test/java/aop/stage1/TransactionAdvisor.java b/study/src/test/java/aop/stage1/TransactionAdvisor.java index 7abc27516c..34c2baeb16 100644 --- a/study/src/test/java/aop/stage1/TransactionAdvisor.java +++ b/study/src/test/java/aop/stage1/TransactionAdvisor.java @@ -10,14 +10,23 @@ */ public class TransactionAdvisor implements PointcutAdvisor { + private final Pointcut pointcut; + + private final Advice advice; + + public TransactionAdvisor(Pointcut pointcut, Advice advice) { + this.pointcut = pointcut; + this.advice = advice; + } + @Override public Pointcut getPointcut() { - return null; + return pointcut; } @Override public Advice getAdvice() { - return null; + return advice; } @Override diff --git a/study/src/test/java/aop/stage1/TransactionPointcut.java b/study/src/test/java/aop/stage1/TransactionPointcut.java index 29ff854890..27059da4ae 100644 --- a/study/src/test/java/aop/stage1/TransactionPointcut.java +++ b/study/src/test/java/aop/stage1/TransactionPointcut.java @@ -1,5 +1,6 @@ package aop.stage1; +import aop.Transactional; import org.springframework.aop.support.StaticMethodMatcherPointcut; import java.lang.reflect.Method; @@ -14,6 +15,6 @@ public class TransactionPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(final Method method, final Class targetClass) { - return false; + return method.isAnnotationPresent(Transactional.class); } } diff --git a/study/src/test/java/aop/stage2/AopConfig.java b/study/src/test/java/aop/stage2/AopConfig.java index 0a6e7f124e..0a9826fa4e 100644 --- a/study/src/test/java/aop/stage2/AopConfig.java +++ b/study/src/test/java/aop/stage2/AopConfig.java @@ -1,8 +1,44 @@ package aop.stage2; +import aop.Transactional; +import aop.stage1.TransactionAdvice; +import aop.stage1.TransactionAdvisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Service; +import org.springframework.transaction.PlatformTransactionManager; + +import java.lang.reflect.Method; @Configuration public class AopConfig { + private final PlatformTransactionManager platformTransactionManager; + + public AopConfig(PlatformTransactionManager platformTransactionManager) { + this.platformTransactionManager = platformTransactionManager; + } + + @Bean + public TransactionAdvice transactionAdvice() { + return new TransactionAdvice(platformTransactionManager); + } + + @Bean + public Pointcut transactionPointcut() { + return new TransactionPointcut(); + } + + @Bean + public TransactionAdvisor transactionAdvisor() { + return new TransactionAdvisor(transactionPointcut(), transactionAdvice()); + } + + @Bean + public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { + return new DefaultAdvisorAutoProxyCreator(); + } } diff --git a/study/src/test/java/aop/stage2/TransactionPointcut.java b/study/src/test/java/aop/stage2/TransactionPointcut.java new file mode 100644 index 0000000000..1824ae0d30 --- /dev/null +++ b/study/src/test/java/aop/stage2/TransactionPointcut.java @@ -0,0 +1,16 @@ +package aop.stage2; + +import aop.Transactional; +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Method; + +public class TransactionPointcut extends StaticMethodMatcherPointcut { + + @Override + public boolean matches(final Method method, final Class targetClass) { + return targetClass.isAnnotationPresent(Service.class) && + method.isAnnotationPresent(Transactional.class); + } +}