순수한 서비스 계층

  • 서비스 계층은 특정 기술에 종속적이지 않게 개발해야 한다. 즉, 기술에 종속적인 부분은 프레젠테이션 계층(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를 빈으로 자동 등록해준다.