티스토리 뷰

본 글은 다크모드에 최적화되어 있습니다.

 

 

스프링에는 데이터 접근 계층(Repository)에서 사용하는 JdbcTemplate과 서비스 계층(Service)에서 사용하는 TransactionTemplated 이 존재합니다. 특별한 기능을 제공하는 것은 아니고 템플릿 콜백 패턴을 사용하여 반복되는 코드를 제거해 줍니다.

 

JdbcTemplate

JdbcTemplate은 데이터 접근 계층에서 DB에 접근할 때 반복되는 코드를 제거해 줍니다. 반복되는 코드란 아래의 목록들을 말합니다.

  • 커넥션 조회 및 동기화
  • PreparedStatement 생성 및 파라미터 바인딩
  • 쿼리 실행
  • ResultSet으로 결과 바인딩
  • 예외 발생 시 스프링 예외 변환기(DataAccessException) 실행
  • 리소스 종료

위의 목록들 중 일부를 알아봅시다.

커넥션 조회 및 동기화

서비스 계층에서 트랜잭션을 시작하면 DB에 접근하기 위한 커넥션을 생성하여 트랜잭션 동기화 매니저에 저장합니다. 데이터 접근 계층에서는 DataSourceUtils.getConnection()을 통해 트랜잭션 동기화 매니저에 저장된 커넥션을 가져와 DB에 접근합니다.

public class Repository {
	// ..생략
    public void 메서드() {
   	 // 동기화된 커넥션을 가져온다
	Connection conn = DataSourceUtils.getConnection(dataSource);
    }
}

PreparedStatement 생성 및 파라미터 바인딩

DB에 실행할 SQL문을 생성합니다.

public class Repository {
	// ..생략
    public void 메서드() {
    	String sql = "...";
        // PreparedStatement 생성
        PreparedStatement stmt = conn.prepareStatement(sql)
        // 파라미터 바인딩
        stmt.setString()
        stmt.setInt()
    }
}

스프링 예외 변환기

체크 예외를 언체크 예외로 변환하여 서비스 계층에 예외를 넘겨주기 위해서 스프링에서 제공하는 스프링 예외 변환기를 사용합니다.

public class Repository {
	// ..생략
    public void 메서드() {
    	try{
       // ..생략
        } catch(SQLException ex) {
        SQLExceptionTranslator exTranslator 
        		= new SQLErrorCodeSQLExceptionTranslator(dataSource);
        DataAccessException exceptionTranslate 
        		= exTranslator.translate("save", sql, ex);
        throw exceptionTranslate;
        }
    }
}

 

여기까지 보았는데 하나의 로직을 작성하는데 코드가 상당히 길어집니다. 사용자 생성, 조회, 삭제 등등 로직이 많아질수록 위의 코드를 모두 작성하여야 하는데 상당히 힘들 것입니다. 

 

이러한 문제를 해결하고자 JdbcTemplate을 사용할 수 있습니다. JdbcTemplate은 DB 접근 계층에서 반복되는 코드를 모두 삭제해 줍니다. 실행할 sql문만 작성하면 JdbcTemplate이 알아서 쿼리를 실행해 줍니다.

 

JdbcTemplate 적용

@Repository
@Primary
public class MemberRepositoryV5 implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;
    @Autowired
    public MemberRepositoryV5(DataSource dataSource) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values(?, ?)";
        jdbcTemplate.update(sql, member.getMemberId(), member.getMoney());
        return member;
    }

    @Override
    public Member findById(String memberId) {
        String sql = "select * from member where member_id = ?";

        return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
            return new Member(rs.getString("member_id"), rs.getInt("money"));
        }, memberId);
    }

    @Override
    public void update(String memberId, int money) {
        String sql = "update member set money = ? where member_id = ?";
        jdbcTemplate.update(sql, money, memberId);
    }

    @Override
    public void delete(String memberId) {
        String sql = "delete from member where member_id = ?";
        jdbcTemplate.update(sql, memberId);
    }
}

JdbcTemplate 미적용

@Repository
@RequiredArgsConstructor
public class MemberRepositoryV4_2 implements MemberRepository {

    private final DataSource dataSource;

    @Override
    public Member save(Member member) {
        String sql = "insert into member (member_id, money) values(?,?)";

        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = DataSourceUtils.getConnection(dataSource);
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, member.getMemberId());
            stmt.setInt(2, member.getMoney());
            stmt.executeUpdate();

            return member;
        } catch (SQLException ex) {
            SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException dataAccessException = exceptionTranslator.translate("insert", sql, ex);
            throw dataAccessException;
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(conn, dataSource);
        }
    }

    @Override
    public Member findById(String memberId) {
        String sql = "select * from member where member_id = ?";

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            conn = DataSourceUtils.getConnection(dataSource);
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, memberId);
            rs = stmt.executeQuery();
            if (rs.next())
                return new Member(rs.getString("member_id"), rs.getInt("money"));
            else
                throw new NoSuchElementException("사용자가 존재하지 않습니다. id = " + memberId);
        } catch (SQLException ex) {
            SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException dataAccessException = exceptionTranslator.translate("select", sql, ex);
            throw dataAccessException;
        } finally {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(conn, dataSource);
        }
    }

    @Override
    public void update(String memberId, int money) {
        String sql = "update member set money = ? where member_id = ?";

        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = DataSourceUtils.getConnection(dataSource);
            stmt = conn.prepareStatement(sql);
            stmt.setInt(1, money);
            stmt.setString(2, memberId);
            stmt.executeUpdate();
        } catch (SQLException ex) {
            SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException dataAccessException = exceptionTranslator.translate("update", sql, ex);
            throw dataAccessException;
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(conn, dataSource);
        }

    }

    @Override
    public void delete(String memberId) {
        String sql = "delete from member where member_id = ?";

        Connection conn = null;
        PreparedStatement stmt = null;

        try {
            conn = DataSourceUtils.getConnection(dataSource);
            stmt = conn.prepareStatement(sql);
            stmt.setString(1, memberId);
            stmt.executeUpdate();
        } catch (SQLException ex) {
            SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException dataAccessException = exceptionTranslator.translate("delete", sql, ex);
            throw dataAccessException;
        } finally {
            JdbcUtils.closeStatement(stmt);
            DataSourceUtils.releaseConnection(conn, dataSource);
        }

    }
}

 

TransactionTemplate

TransactionTemplate은 트랜잭션 매니저를 생성, 커밋, 롤백하는 과정을 제거해 줍니다. 즉 반복되는 코드를 삭제해 줍니다.

 

TransactionTemplate 적용

@RequiredArgsConstructor
public class MemberServiceV3_2 {

    private final MemberRepositoryV3 memberRepository;
    private final TransactionTemplate transactionTemplate;

    public void accountTransfer(String fromId, String toId, int money)  {

        transactionTemplate.executeWithoutResult((status) -> {
            try {
                Member fromMember = memberRepository.findById(fromId);
                Member toMember = memberRepository.findById(toId);

                memberRepository.update(fromId, fromMember.getMoney() - money);
                if (toId.equals("ex"))
                    throw new IllegalStateException("계좌이체 오류");
                memberRepository.update(toId, toMember.getMoney() + money);
            } catch (Exception ex) {
                throw new IllegalStateException(ex);
            }
        });
    }
}

TransactionTemplate 미적용

@RequiredArgsConstructor
public class MemberServiceV3_1 {

    private final PlatformTransactionManager transactionManager;
    private final MemberRepositoryV3 memberRepository;


    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
	// 트랜잭션 시작
        TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            Member fromMember = memberRepository.findById(fromId);
            Member toMember = memberRepository.findById(toId);

            memberRepository.update(fromMember.getMemberId(), fromMember.getMoney() - money);
            if (toId.equals("ex"))
                throw new IllegalStateException("계좌이체 오류");
            memberRepository.update(toMember.getMemberId(), toMember.getMoney() + money);
		// 트랜잭션 커밋
            transactionManager.commit(transactionStatus);
        } catch (Exception ex) {
       	 // 트랜잭션 롤백
            transactionManager.rollback(transactionStatus);
            throw new IllegalStateException();
        }
    }
}

 

 

@Transactional

위에서 트랜잭션 템플릿을 사용한 코드를 봅시다. 비즈니스 로직 안에 트랜잭션과 관련된 코드와 비즈니스 로직이 섞여 있습니다. 이는 좋은 코드라고 할 수가 없습니다. 비즈니스 로직에는 순수한 자바 코드만 담겨있어야 합니다. 

 

따라서 이와 같은 문제를 해결하고자 @Transactional 애너테이션을 사용해야 합니다.

아래 코드를 보면 비즈니스 로직에 순수한 자바 코드만 담겨 있습니다. 트랜잭션 시작, 커밋, 롤백을 @Transactional 애너테이션 하나로 해결해 준 것입니다.

@Service
@Slf4j
@RequiredArgsConstructor
public class MemberServiceV4 {

    private final MemberRepository memberRepository;

    @Transactional
    public void accountTransfer(String fromId, String toId, int money) {
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toId);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private void validation(String memberId) {
        if(memberId.equals("ex"))
            throw new IllegalStateException("계좌이체 오류 발생");
    }
}

결론

DB 접근 계층인 Repository에서는 JdbcTemplate을 사용하고, 서비스 계층인 Service에서는 @Transactional 애너테이션을 사용하는 것이 좋습니다.

Total
Today
Yesterday
최근에 올라온 글
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30