사용자 이력(UserHistoryDao) ➡️ 비즈니스 로직
: 데이터베이스와는 상관이 없다. 단순히 DB 테이블하고 1대1관계로 테이블을 다루는 곳이기 때문에 비즈니스 로직을 DAO에 두지 않는다. 그럼 둘 곳이 Controller밖에 없다. 하지만 Controller에 넣기에는 역할이 비즈니스 역할이 아니다. 그렇기 때문에 비즈니스 역할을 담당할 객체를 하나 더 만들어야 한다.
UserDao ➡️ CRUD
: 비즈니스 로직과 하는 일이 다르기 때문에 같이 묶는것은 적합하지 않다.
RegisterController ➡️ Presentation
UserService ➡️ Service & 비즈니스 로직
: DAO를 불러서 처리
: Controller는 UserService만 주입 받아서 사용하면 된다.
: 트랜잭션을 사용하기에도 적합함. 예를 들어, 회원가입을 할려면 UserDao에서 insert()
와 UserHistoryDao에서 insertUserHistory()
를 하나의 트랜잭션에 묶어서 테스트를 해야 한다. 둘다 통과되면 회원가입이 진행
➡️ Controller에서도 할 수 있지만 너무 복잡해지고 불필요한 기능들이 섞이게 됨. 그래서 별도의 service 객체를 둬서 그 기능들만 사용할 수 있게 두는 것이 적합하다.
✔️ DAO의 각 메서드는 개별 Connection을 사용
✔️ 같은 트랜잭션내에서 같은 Connection을 사용할 수 있게 관리
deleteUser()
를 보면, Connection을 가져온다.
: 메서드마다 각각의 Connection을 따로 만들면서 처리한다.
하나의 트랜잭션은 하나의 Connection으로만 이뤄져야 한다.
: deleteUser()
두번 호출하면 각각의 Connection이 된다. 둘 다 커밋한 상태에서 하나의 deleteUser()가 rollback을 한다면 되돌릴 수 없게 된다. 개별 Connection에서 실행이 되기 때문이다. 이 문제를 해결하려면, 개별 Connection을 1개의 Connection으로 쓰게 해 줘야 한다. 즉, deleteUser()
는 각각의 Connection이 아닌 같은 Connection을 써줘야 한다.
❗️ DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils를 사용해야 한다.
DataSourceUtils.getConnection(ds);
부분을 잘 확인해 봐야 한다.TransactionManager를 생성
DefalutTransactionDefinition
을 이용해서 Transaction을 얻어온다.
: 원래는 setting해줘야 하는데 defalut속성으로 처리함
트랜잭션 시작
: 성공 or 실패
❗️ __DefalutTransactionDefinition
: 트랜잭션의 속성을 정의
<tx:annotation-driven>
이 있어야 @Transaction
사용 가능✔️ AOP를 이용한 핵심 기능과 부가 기능의 분리
insert()
하는 부분이 핵심 기능. 나머지는 부가 기능✔️ @Transaction은 클래스나 인터페이스에도 붙일 수 있음
❗️@Transactional 은 테스트 케이스에 선언시 테스트 시작전에 트랜잭션을 시작하고, 테스트 완료 후 항상 롤백을 하여, 다음 테스트에 영향을 주지 않는다.
✔️ 트랜잭션의 경계가 어떻게 설정됨에 따라서 결과가 달라진다.
REQUIRED & REQUIRES_NEW 중요
REQUIRED_NEW
: 트랜잭션안에 다른 트랜잭션 생성
NESTED
: 트랜잭션안에 sub트랜잭션 생성 ➡️ 같은 트랜잭션안에서 처리
: save point가 있다.
✔️ 트랜잭션에 기존에 있으면 새로운 트랜잭션을 만들지 않는다.
하나의 트랜잭션인 것처럼 만들어진다.
B2에서 에러가나면 처음인 A1부터 롤백된다.
REQUIRED는 기본값이여서 생략해도 된다.
✔️ 새로운 트랜잭션을 만든다.
2개의 트랜잭션이 만들어진다.
A2에서 예외가 뜨면 다시 A1으로 rollback되지만 B1,B2 는 취소되지 않는다. 별도의 트랜잭션이기 때문
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@Repository
public class A1Dao {
@Autowired
DataSource dataSource;
public int insert(int key, int value) throws Exception {
Connection connection = null;
PreparedStatement ps = null;
try {
connection = dataSource.getConnection();
ps = connection.prepareStatement("insert into a1 values(?, ?)");
ps.setInt(1, key);
ps.setInt(2, value);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
close(connection, ps);
}
}
private void close(AutoCloseable... acs) {
for(AutoCloseable ac :acs)
try {
if(ac != null) {
ac.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
package kr.ac.jipark09;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
@Autowired
A1Dao a1Dao;
@Test
public void insertTest() throws Exception {
a1Dao.insert(1, 100); // 성공
a1Dao.insert(1, 200); // 실패
}
}
✔️ 트랜잭션 묶기
package kr.ac.jipark09;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import javax.sql.DataSource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
@Autowired
A1Dao a1Dao;
@Autowired
DataSource dataSource;
@Test
public void insertTest() throws Exception {
// 트랜잭션을 생성
PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 시작
try {
// 하나의 트랜잭션으로 바꿈
a1Dao.insert(1, 100);
a1Dao.insert(1, 200);
tm.commit(status);
} catch (Exception e) {
tm.rollback(status);
} finally {
}
}
}
package kr.ac.jipark09;
import org.apache.taglibs.standard.tag.common.sql.DataSourceUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import javax.xml.crypto.Data;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@Repository
public class A1Dao {
@Autowired
DataSource dataSource;
public int insert(int key, int value) throws Exception {
Connection connection = null;
PreparedStatement ps = null;
try {
// connection = dataSource.getConnection();
connection = DataSourceUtils.getConnection(dataSource);
ps = connection.prepareStatement("insert into a1 values(?, ?)");
ps.setInt(1, key);
ps.setInt(2, value);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
// close(connection, ps); 닫으면 종료되니까 ㄴㄴ
close(ps);
// TransactionManager가 닫아야되는지 아닌지 판단한다.
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
private void close(AutoCloseable... acs) {
for(AutoCloseable ac :acs)
try {
if(ac != null) {
ac.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
}
✔️ TransactionManager를 직접 생성하지 않고 주입받아서 처리
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven/>
<tx:annotation-driven/>
@Transactional
어노테이션을 쓰기 위해 등록package kr.ac.jipark09;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import javax.sql.DataSource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class A1DaoTest {
@Autowired
A1Dao a1Dao;
@Autowired
DataSource dataSource;
// 빈으로 등록한 DataSourceTransactionManager를 주입
@Autowired
DataSourceTransactionManager tm;
@Test
public void insertTest() throws Exception {
// 트랜잭션을 생성
// PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
// 트랜잭션 시작
try {
// 하나의 트랜잭션으로 바꿈
a1Dao.deleteAll();
a1Dao.insert(1, 100);
a1Dao.insert(2, 200);
tm.commit(status);
} catch (Exception e) {
tm.rollback(status);
} finally {
}
}
}
create table b1 select * from a1 where false; -- 테이블만 생성
create table b1 select * from a1; -- 테이블만 생성 & 데이터도 복사
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.stereotype.Repository;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@Repository
public class B1Dao {
@Autowired
DataSource dataSource;
public int insert(int key, int value) throws Exception {
Connection connection = null;
PreparedStatement ps = null;
try {
connection = DataSourceUtils.getConnection(dataSource);
System.out.println(connection);
ps = connection.prepareStatement("insert into b1 values(?, ?)");
ps.setInt(1, key);
ps.setInt(2, value);
return ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
close(ps);
DataSourceUtils.releaseConnection(connection, dataSource);
}
}
private void close(AutoCloseable... acs) {
for(AutoCloseable ac :acs)
try {
if(ac != null) {
ac.close();
}
} catch(Exception e) {
e.printStackTrace();
}
}
public void deleteAll() throws Exception {
Connection connection = dataSource.getConnection();
String sql = "delete from a1";
PreparedStatement ps = connection.prepareStatement(sql);
ps.executeUpdate();
close(ps);
}
}
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TxService {
@Autowired
A1Dao a1Dao;
@Autowired
B1Dao b1Dao;
public void insertA1WithoutTx() throws Exception {
a1Dao.insert(1, 100);
a1Dao.insert(1, 200);
}
}
package kr.ac.jipark09;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/root-context.xml"})
public class TxServiceTest {
@Autowired
TxService txService;
@Test
public void insertA1WithoutTest() throws Exception {
txService.insertA1WithoutTx();
}
}
✔️ @Transactional 주입
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TxService {
@Autowired
A1Dao a1Dao;
@Autowired
B1Dao b1Dao;
@Transactional
public void insertA1WithoutTx() throws Exception {
a1Dao.insert(1, 100);
a1Dao.insert(1, 200);
}
}
❗️ @Transactional(rollbackFor = Exception.class)
: 예외가 뜨면 rollback 하라는 의미
:@Transactional
은 RuntimeException과 Error만 rollback 한다.
✔️ REQUIRED 활용
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TxService {
@Autowired
A1Dao a1Dao;
@Autowired
B1Dao b1Dao;
@Transactional(propagation = Propagation.REQUIRED)
public void inserA1WithTx() throws Exception {
a1Dao.insert(1, 100); // 성공
insertB1WithTx();
a1Dao.insert(2, 200); // 성공
}
@Transactional(propagation = Propagation.REQUIRED)
public void insertB1WithTx() throws Exception {
b1Dao.insert(1, 100); // 성공
b1Dao.insert(1, 200); // 실패
}
}
✔️ REQUIRES_NEW 활용
❗️ 같은 클래스에 속한 메서드끼리의 호출(내부 호출)은 @Transactional이 동작하지 않는다. 그 이유는, 프록시 방식(defalut)의 AOP는 내부 호출인 경우, Advice가 적용되지 않는다. 그래서 트랜잭션이 적용되지 않는 것.
➡️ 두 메서드를 별도의 클래스로 분리하면 트랜잭션이 적용된다.
package kr.ac.jipark09;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import javax.sql.DataSource;
@Service
public class TxService {
@Autowired
A1Dao a1Dao;
@Autowired
B1Dao b1Dao;
@Autowired
DataSource dataSource;
// @Transactional(propagation = Propagation.REQUIRED)
// public void inserA1WithTx() throws Exception {
// a1Dao.insert(1, 100); // 성공
// insertB1WithTx();
// a1Dao.insert(2, 200); // 성공
// }
public void insertA1WithTx() throws Exception {
PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = tm.getTransaction(txd);
try {
a1Dao.insert(1, 100); // 성공
insertB1WithTx();
a1Dao.insert(1, 200); // 실패
tm.commit(status);
} catch (Exception e) {
tm.rollback(status);
} finally {
}
}
public void insertB1WithTx() throws Exception {
PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
DefaultTransactionDefinition txd = new DefaultTransactionDefinition();
txd.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = tm.getTransaction(txd);
try {
b1Dao.insert(1, 100); // 성공
b1Dao.insert(2, 200); // 성공
tm.commit(status);
} catch (Exception e) {
tm.rollback(status);
} finally {
}
}
// @Transactional(propagation = Propagation.REQUIRES_NEW)
// public void insertB1WithTx() throws Exception {
// b1Dao.insert(1, 100); // 성공
// b1Dao.insert(1, 200); // 실패
// }
}
Reference
: https://fastcampus.co.kr/dev_academy_nks