순수한 서비스 계층
- 서비스 계층은 특정 기술에 종속적이지 않게 개발해야 한다. 즉, 기술에 종속적인 부분은 프레젠테이션 계층(UI), 데이터 접근 게층에서 가지고 간다.
- 이렇게 해야, 나중에 기술이 변경되더라도 비즈니스 로직을 담당하는 서비스 계층은 수정하지 않을 수 있다.
트랜잭션 동기화 매니저
서비스 계층에서의 트랜잭션 시작
비즈니스 로직 실행
- 스프링은 트랜잭션 동기화 매니저를 제공해서, 한 트랜잭션 내에서 동일한 커넥션을 사용하도록 한다.
- 트랜잭션 매너지가 데이터소스를 통해 커넥션을 만들면 트랜잭션 동기화 매니저에 보관하고, 레포지토리가 이 트랜잭션 동기화 매니저에 저장된 해당 커넥션을 가져다 쓰는 방식으로 동일한 커넥션을 유지하는 것.
- 덕분에 파라미터로 커넥션을 넘기지 않아도 된다.
- 트랜잭션 동기화 매너지가 관리하는 커넥션이 없으면, 새로운 커넥션을 만들어서 반환한다. 즉, 트랜잭션이 걸리지 않은 비즈니스 로직이더라도 데이터베이스에 접근해서 데이터를 잘 가져올 수 있는 것이다.
트랜잭션 종료
- DataSourceUtils.releaseConnection() 을 사용하면 커넥션을 바로 닫는 것이 아니다.
- 트랜잭션을 사용하기 위해 동기화된 커넥션은 커넥션을 닫지 않고 그대로 유지해준다. (서비스 계층에서 아직 사용하기 때문에 서비스 계층에서 닫아주어야 하므로)
- 트랜잭션 동기화 매니저가 관리하는 커넥션이 없는 경우 해당 커넥션을 닫는다. (그렇지 않은 경우)
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV3_1 {
// private final DataSource dataSource;
private final PlatformTransactionManager transactionManager;
private final MemberRepositoryV3 repository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
// 트랜잭션 시작
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
bizLogic(fromId, toId, money);
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw new IllegalStateException(e);
}
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = repository.findById(fromId);
Member toMember = repository.findById(toId);
repository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
repository.update(toId, toMember.getMoney() + money);
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체 중 예외 발생");
}
}
}
@Slf4j
public class MemberRepositoryV3 {
private final DataSource dataSource;
public MemberRepositoryV3(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
} else {
throw new NoSuchElementException("member not found memberId=" + memberId);
}
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(conn, pstmt, rs);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money = ? where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}" , resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("resultSize={}" , resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
}
}
private void close(Connection conn, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
// 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 함.
DataSourceUtils.releaseConnection(conn, dataSource);
}
private Connection getConnection() throws SQLException {
// 트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야 함.
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={}, class={}", con, con.getClass());
return con;
}
}
TransactionTemplate
- 트랜잭션을 시작하고, 커밋하고 롤백하는 코드가 제거될 수 있다.
- 하지만 비즈니스 로직을 담당하는 코드가 아닌, 트랜잭션 기술에 대한 코드가 아직 남아있다. 이는 AOP로 해결될 수 있다.
@Slf4j
public class MemberServiceV3_2 {
// private final DataSource dataSource;
private final TransactionTemplate txTemplate;
private final MemberRepositoryV3 repository;
public MemberServiceV3_2(PlatformTransactionManager transactionManager, MemberRepositoryV3 repository) {
this.txTemplate = new TransactionTemplate(transactionManager);
this.repository = repository;
}
// 트랜잭션 시작, 커밋, 롤백 코드가 제거됨
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
txTemplate.executeWithoutResult(status -> {
try {
bizLogic(fromId, toId, money);
} catch (SQLException e) {
throw new IllegalStateException(e);
}
});
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = repository.findById(fromId);
Member toMember = repository.findById(toId);
repository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
repository.update(toId, toMember.getMoney() + money);
}
private static void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체 중 예외 발생");
}
}
}
스프링 AOP 트랜잭션 흐름
스프링 AOP가 적용된 트랜잭션 흐름
- 선언적 트랜잭션 관리: @Transactional 어노테이션 하나로 선언해서 트랜잭션을 적용하는 것
- 프로그래밍 방식의 트랜잭션 관리: 트랜잭션 매니저 또는 트랜잭션 템플릿 등을 사용해서 트랜잭션 관련 코드를 직접 작성해서 트랜잭션을 적용하는 것.
spring.application.name=jdbc
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
- 스프링 부트는 DataSource와 PlatformTransactionManager를 빈으로 자동 등록해준다.