트랜잭션이 없는 비즈니스 로직
@RequiredArgsConstructor
public class MemberServiceV1 {
private final MemberRepositoryV1 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member member) {
if (member.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
@Slf4j
public class MemberRepositoryV1
{
private final DataSource dataSource;
public MemberRepositoryV1(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member (member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.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(con, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.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(con, pstmt, rs);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("result size = {}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("result size = {}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
private Connection getConnection() throws SQLException {
Connection conn = dataSource.getConnection();
log.info("conn = {}, conn.class = {}", conn, conn.getClass());
return conn;
}
private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
}
formId 회원에서 toId 회원으로 money 만큼 계좌 이체하는 비즈니스 로직
toId 가 "ex"이면 예외가 발생하여 toId 회원의 잔고가 증가하는 로직은 실행되지 않음
- 결국
fromId 회원의 잔고만 감소하는 문제가 발생
트랜잭션이 있는 비즈니스 로직
- 앞서 발생한 문제는 트랜잭션을 통해 해결할 수 있음
- 그렇다면 트랜잭션을 애플리케이션의 어느 계층에 걸어야 하는가? → 서비스 계층
- 비즈니스 로직에서 문제가 발생하면 해당 비즈니스 로직에서 수행된 작업들을 롤백해야하기 때문
비즈니스 로직과 트랜잭션
- 트랜잭션을 시작하기 위해 서비스 계층에서 커넥션을 생성하고 트랜잭션 커밋 후 종료해야 함
- 애플리케이션에서 DB 트랜잭션을 사용하려면 같은 세션을 유지해야 하고, 이를 위해 트랜잭션을 사용하는 동안 같은 커넥션을 유지해야 함
- 커넥션을 파라미터로 전달하여 사용함으로써 동일 커넥션을 유지할 수 있음
커넥션을 파라미터로 받는 Repostitory
public class MemberRepositoryV2 {
private final DataSource dataSource;
public MemberRepositoryV2(DataSource dataSource) {
this.dataSource = dataSource;
}
public Member save(Member member) throws SQLException {
String sql = "insert into member (member_id, money) values (?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.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(con, pstmt, null);
}
}
public Member findById(String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.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(con, pstmt, rs);
}
}
public Member findById(Connection con, String memberId) throws SQLException {
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = con.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 {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
public void update(String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("result size = {}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
public void update(Connection con, String memberId, int money) throws SQLException {
String sql = "update member set money=? where member_id=?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
int resultSize = pstmt.executeUpdate();
log.info("result size = {}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
JdbcUtils.closeStatement(pstmt);
}
}
public void delete(String memberId) throws SQLException {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
int resultSize = pstmt.executeUpdate();
log.info("result size = {}", resultSize);
} catch (SQLException e) {
log.error("db error", e);
throw e;
} finally {
close(con, pstmt, rs);
}
}
private Connection getConnection() throws SQLException {
Connection conn = dataSource.getConnection();
log.info("conn = {}, conn.class = {}", conn, conn.getClass());
return conn;
}
private void close(Connection con, Statement stmt, ResultSet rs) throws SQLException {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
JdbcUtils.closeConnection(con);
}
}
- 기존
MemberRepositoryV1과는 다르게 findById(), update() 에서 커넥션을 파라미터로 받음
- 커넥션 유지가 필요하기 때문에 새로 커넥션을 획득하는
getConnection() 메서드를 사용하면 안 된다
- 또한, 비즈니스로직 내 다른 작업에서 동일한 커넥션을 사용해야하기 때문에
close() 메서드로 커넥션을 닫으면 안 된다
Service 계층에서 트랜잭션 실행
@RequiredArgsConstructor
public class MemberServiceV2 {
private final DataSource dataSource;
private final MemberRepositoryV2 memberRepository;
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
Connection con = dataSource.getConnection();
try {
con.setAutoCommit(false); // 트랜잭션 시작
businessLogic(con, fromId, toId, money);
con.commit(); // 성공 시 커밋
} catch (Exception e) {
con.rollback();
throw new IllegalStateException(e);
} finally {
release(con);
}
}
private void businessLogic(Connection con, String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(con, fromId);
Member toMember = memberRepository.findById(con, toId);
memberRepository.update(con, fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(con, toId, toMember.getMoney() + money);
}
private void release(Connection con) {
if (con != null) {
try {
con.setAutoCommit(true);
con.close();
} catch (Exception e) {
log.info("error", e);
}
}
}
private void validation(Member member) {
if (member.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
Connection con = dataSource.getConnection();
- 트랜잭션을 시작하기 위해 비즈니스 로직 실행 전 커넥션 획득
con.setAutoCommit(false);
- 트랜잭션 시작을 위해 수동 커밋 모드로 변경
- 커넥션을 통해 DB 세션에
set autocommit false 가 전달
- 트랜잭션 시작
businessLogic(con, fromId, toId, money);
- 트랜잭션이 시작된 커넥션을 파라미터로 전달하면서 비즈니스 로직 수행
- 비즈니스 로직 내부의
memberRepository.update(con, ... )에서 파라미터로 받은 커넥션을 사용
con.commit();
- 비즈니스 로직이 정상적으로 완료되면 트랜잭션을 커밋
con.rollback();
- 비즈니스 로직 실행 중 예외가 발생하면 트랜잭션을 롤백
release(con);
finally { ... } 구문을 사용하여 예외 발생과 상관없이 커넥션을 안전하게 종료
- 단, 커넥션 풀을 사용하면 커넥션이 종료되는 것이 아니라 '반환'됨
- 현재 수동 커밋 모드로 동작하기 떄문에 반환할 때는 기본값인 자동 커밋 모드로 되돌려 반환하는 것이 안전