JDBC Template vs JPA

HW·2024년 5월 20일
0

서론

Repository에서 DB로 데이터를 읽어오는 방법에는 여러 가지 있으며
성능적으로 차이가 난다.

스프링의 JdbcTemplate과 JpaRepository를 사용하여 총 4가지 방법의 성능을 비교해보겠다.

본론

메서드

먼저 성능 비교 대상의 네 가지 메서드를 살펴보자.

__readById Method

public Optional<Member> __readById(long id) {
        String sql = "SELECT id, auth_provider, created_at, username, role FROM member WHERE id = ?";
        try (PreparedStatement ps = jdbcTemplate.getDataSource().getConnection().prepareStatement(sql)) {
            ps.setLong(1, id);
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                Member member = Member.builder()
                        .id(rs.getLong("id"))
                        .authProvider(rs.getInt("auth_provider"))
                        .createdAt(rs.getDate("created_at"))
                        .username(rs.getString("username"))
                        .role(rs.getInt("role"))
                        .build();
                return Optional.of(member);
            } else {
                return Optional.empty();
            }
        } catch (SQLException e) {
            // Handle SQL exceptions appropriately
            throw new RuntimeException(e); // Or a more specific exception type
        }
    }

PreparedStatement를 사용하는 메서드다.
PreparedStatement를 사용하면 다음과 같은 이점이 있다.

1. 보안 향상
SQL 쿼리에 사용자 입력을 바인딩할 때,
사용자 입력이 쿼리 문자열에 직접 포함되는 대신 PreparedStatement를 통해 전달된다.
이는 악의적인 사용자가 SQL 쿼리를 조작하여 데이터베이스를 공격하는 것을 방지한다.

2. 성능 향상
PreparedStatement는 쿼리를 미리 컴파일하고, 데이터베이스에 캐시되어 쿼리 실행 속도를 향상시킨다.
동일한 쿼리를 여러 번 실행해야 하는 경우, 미리 컴파일된 PreparedStatement를 재사용함으로써 데이터베이스 부하를 줄일 수 있다.

_readById Method

public Optional<Member> _readById(long id) {
        String sql = "SELECT id, auth_provider, created_at, username, role FROM member WHERE id = ? LIMIT 1";
        try {
            Member member = jdbcTemplate.queryForObject(
                    sql,
                    new Object[]{id},
                    (rs, rowNum) -> Member.builder()
                            .id(rs.getLong("id"))
                            .authProvider(rs.getInt("auth_provider"))
                            .createdAt(rs.getDate("created_at"))
                            .username(rs.getString("username"))
                            .role(rs.getInt("role"))
                            .build()
            );
            return Optional.ofNullable(member);
        } catch (EmptyResultDataAccessException e) {
            return Optional.empty();
        }
    }

람다 함수를 사용하는 메서드다.
람다 함수를 사용하면 다음과 같은 이점이 있다.
1. 간결한 코드
코드의 가독성을 향상시킬 수 있고, 더 간결한 코드를 작성할 수 있다.

readById Method

public Optional<Member> readById (long id)
    {
        String sql = "SELECT id, auth_provider, created_at, username, role FROM member WHERE id = ? LIMIT 1";
        List<Member> member = jdbcTemplate.query(sql, (rs, rowNum) -> Member.builder()
                .id(rs.getLong("id"))
                .authProvider(rs.getInt("auth_provider"))
                .createdAt(rs.getDate("created_at"))
                .username(rs.getString("username"))
                .role(rs.getInt("role"))
                .build()
                , id);
        return member.stream().findFirst();
    }

성능 개선 이전에 불필요한 예외처리 사용을 줄이기 위해 사용한 메서드다.
예외처리 사용을 줄이는 대신 List와 stream을 사용하게 되어 오버헤드가 발생할 수 있다.

JpaRepository.findById Method

@Repository
public interface MemberJpaRepository extends JpaRepository<Member, Long> {

    @Override
    Optional<Member> findById(Long aLong);
}

JPA Repository를 사용하는 메서드다.
JPA는 다음과 같은 이점이 있다.
1. 고수준의 추상화
2. 타입 안전한 쿼리
잘못된 쿼리 구문이나 타입 불일치로 인한 런타임 오류를 줄여준다.
3. 페이징, 정렬 지원
아직 사용해보지 못했지만 복잡한 SQL 쿼리를 작성하지 않고도
페이징과 정렬 기능을 쉽게 구현할 수 있다.

결과

Time taken by readById(1L): 4262948 nanoseconds
Time taken by _readById(1L): 1896983 nanoseconds
Time taken by __readById(1L): 2262450 nanoseconds
Time taken by _findById(1L): 33946033 nanoseconds

Time taken by readById(1L): 1042659 nanoseconds
Time taken by _readById(1L): 1259606 nanoseconds
Time taken by __readById(1L): 1225161 nanoseconds
Time taken by _findById(1L): 2170267 nanoseconds

Time taken by readById(1L): 840699 nanoseconds
Time taken by _readById(1L): 872879 nanoseconds
Time taken by __readById(1L): 942781 nanoseconds
Time taken by _findById(1L): 1845246 nanoseconds

Time taken by readById(1L): 3848028 nanoseconds
Time taken by _readById(1L): 1072384 nanoseconds
Time taken by __readById(1L): 863421 nanoseconds
Time taken by _findById(1L): 1796104 nanoseconds

Time taken by readById(1L): 763224 nanoseconds
Time taken by _readById(1L): 868220 nanoseconds
Time taken by __readById(1L): 959683 nanoseconds
Time taken by _findById(1L): 1721944 nanoseconds

Time taken by readById(1L): 759036 nanoseconds
Time taken by _readById(1L): 3119310 nanoseconds
Time taken by __readById(1L): 868972 nanoseconds
Time taken by _findById(1L): 1764735 nanoseconds

Time taken by readById(1L): 1042338 nanoseconds
Time taken by _readById(1L): 1062466 nanoseconds
Time taken by __readById(1L): 1345207 nanoseconds
Time taken by _findById(1L): 2281486 nanoseconds

Time taken by readById(1L): 1088756 nanoseconds
Time taken by _readById(1L): 1217126 nanoseconds
Time taken by __readById(1L): 1343313 nanoseconds
Time taken by _findById(1L): 3570998 nanoseconds

Time taken by readById(1L): 947650 nanoseconds
Time taken by _readById(1L): 1041065 nanoseconds
Time taken by __readById(1L): 1049421 nanoseconds
Time taken by _findById(1L): 2266909 nanoseconds

Time taken by readById(1L): 877037 nanoseconds
Time taken by _readById(1L): 834878 nanoseconds
Time taken by __readById(1L): 1087112 nanoseconds
Time taken by _findById(1L): 1909968 nanoseconds

10개의 케이스의 결과에서 평균값을 구한다.

readById:
시간 총합: 4262948 + 1042659 + 840699 + 3848028 + 763224 + 759036 + 1042338 + 1088756 + 947650 + 877037 = 16815574 nanoseconds
평균 시간: 16815574 / 10 = 1681557.4 nanoseconds

_readById:
시간 총합: 1896983 + 1259606 + 872879 + 1072384 + 868220 + 3119310 + 1062466 + 1217126 + 1041065 + 834878 = 14587117 nanoseconds
평균 시간: 14587117 / 10 = 1458711.7 nanoseconds

__readById:
시간 총합: 2262450 + 1225161 + 942781 + 863421 + 959683 + 868972 + 1345207 + 1343313 + 1049421 + 1087112 = 12241711 nanoseconds
평균 시간: 12241711 / 10 = 1224171.1 nanoseconds

_findById:
시간 총합: 33946033 + 2170267 + 1845246 + 1796104 + 1721944 + 1764735 + 2281486 + 3570998 + 2266909 + 1909968 = 28017890 nanoseconds
평균 시간: 28017890 / 10 = 2801789 nanoseconds

결론

PreparedStatement를 사용하는 메서드가 성능이 제일 좋다.

이전 글에서 예외처리를 시작할 때
객체가 생성되니 불필요한 객체생성을 방지하기 위해 예외처리 방식을 피했는데
알고보니 이게 성능이 좋은 방식이었다.

메서드 호출 순서에 따라서 소요시간이 달라지는 경우가 있었는데
이 경우를 배제하고도 평균적으로 PreparedStatement를 사용하는 메서드가 소요시간이 짧았다.

요구사항에 따라 성능과 함께 가독성과 유지보수성도 고려하여
두 가지 방법 중 적절한 것을 선택해야겠다.

profile
예술융합형 개발자🎥

0개의 댓글