
프로젝트 도중 약 600개의 정적 데이터를 DB에 넣고, 다른 CUD 과정이 없이 Read만 이루어졌기 때문에 JPA가 아닌 JdbcTemplate을 사용하여 구현하였다.
따라서 @Entity로 모델링을 하지 않았기 때문에 DTO로 데이터를 매핑하여 받는 과정을 진행하였다.
@Repository
public class DefaultEmotionRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public ColorResponse getColorByEmotion(String emotion){
String sql = "SELECT red, blue, green, type FROM default_emotion WHERE emotion = ?";
// RowMapper를 구현하여 ResultSet의 각 행을 ColorResponse 객체로 매핑합니다.
return jdbcTemplate.queryForObject(sql, new Object[]{emotion}, (rs, rowNum) -> {
ColorResponse colorResponse = new ColorResponse();
colorResponse.setRed(rs.getInt("red"));
colorResponse.setBlue(rs.getInt("blue"));
colorResponse.setGreen(rs.getInt("green"));
int type = Integer.parseInt(rs.getString("type"));
colorResponse.setType(EmotionType.values()[type]);
return colorResponse;
});
}
}
JPA로 매핑하여 사용해도 JPQL을 통해 DTO로 매핑할 수도 있다. 하지만 JdbcTemplate으로 구현하는 것이 속도 면에서도 성능이 좋다고 하여,, 갑자기 든 생각.
가 떠올랐다..🧐
우선 개념 먼저 짚고 넘어가자
우선 JDBC이 무엇인지 알아보자.
JDBC란 Java Database Connectivity의 약자로
Java에서 데이터베이스에 연결하고 SQL 쿼리를 실행하는 표준 API이다.
동작 흐름) Java 애플리케이션 -> JDBC API -> JDBC Driver -> DB
각 JDBC Driver는 특정 DBMS에 맞춰져 있으며, JDBC 인터페이스를 통해 일관된 방식으로 데이터베이스와 통신한다.
yml 파일에서 datasource를 설정하는 아래와 같은 부분이 JDBC 환경설정을 구성하고 데이터베이스를 연결한다.
spring:
datasource:
url: jdbc:mariadb://localhost:3306
driver-class-name: org.mariadb.jdbc.Driver
JdbcTemplate은 말 그래도 JDBC를 사용하는 템플릿인데,
JDBC를 사용하려면 반복적이고 번거로운 코드가 필요하기 때문에 이러한 문제를 해결하고자, 스프링 프레임워크에서 제공하는 JDBC를 간소화하고 편리하게 사용할 수 있도록 도와주는 클래스이다.
해당 프로젝트에서는 JdbcTemplate에서 제공하는 메서드인 queryForObject와 RowMapper<T> 를 사용하여 query의 결과를 담는 ResultSet을 DTO 객체로 매핑한다.
JPA는 Java Persistence API의 약자로
Java 객체를 DB에 자동으로 매핑해주는 ORM 프레임워크이자 인터페이스의 모음이다.
즉 실제로 동작은 하지 않는다.
ORM이란?
Object-Relational-Mapping의 약자.
객체는 객체대로 설계하고, 관계형 데이터베이스는 관계형 데이터베이스대로 설계
이를 ORM 프레임워크(Hibernate)가 중간에서 매핑해주는 것을 뜻함
실제로 동작하는 부분은 바로 JDBC API이다.
JPA는 JDBC를 추상화한 기술로, Java 애플리케이션과 JDBC 사이에서
동작하며 개발자 -> JPA -> JDBC API -> SQL 호출 방식으로 이루어진다.

개발자는 SQL 대신 객체와 어노테이션을 다루고, 내부적으로는 JDBC가 실제 SQL을 실행한다.
그럼 마지막으로 JPQL은 무엇이냐.
Java Persistence Query Language의 약자로, SQL과 문법은 비슷하지만 JPA에서 사용하는 엔티티 기반 쿼리 언어이다.
테이블이 아닌 엔티티(클래스)와 그 속성(필드)를 기준으로 작성하며
SQL
SELECT * FROM default_emotion WHERE emotion = '기쁨';
JPQL
@Query("SELECT d FROM DefaultEmotion d WHERE d.emotion = :emotion")
DefaultEmotion findByEmotion(@Param("emotion") String emotion);
처럼 @Query 어노테이션 안에 JPQL을 작성하고,
default_emotion(테이블 명) ->DefaultEmotion(엔티티 클래스 명)
emotion(칼럼 명) ->d.emotion(클래스의 필드 명)
:emotion(파라미터 바인딩 문법)
으로 변경하여 작성한다.
JPQL을 언급한 이유는 new 키워드를 통해 DTO로 바로 반환할 수 있기 때문이다.
따라서 나는 DB에서 데이터를 불러오는 과정에서 3가지의 테스트를 진행해보려고 한다.
JdbcTemplate을 사용하여 DTO 에 바로 매핑@Entity를 사용하여 JPA로 호출 후 서비스에서 DTO 매핑@Entity를 사용하여 모델링 후 JPQL로 DTO 바로 매핑default_emotion 테이블에서 emotion 값과 일치하는 레코드를 조회 후 ColorResponse 객체에 매핑하여 반환@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ColorResponse {
private int red;
private int blue;
private int green;
private EmotionType type;
}
SpringBootTest 에서 각각 조회 코드를 메서드로 나누고, System.currentTimeMillis()로 조회 속도 측정
콜드 스타트 조회 속도 측정
3회 조회 후 평균 속도 측정
5회 각각 속도 측정
default_emotion 및 DefaultEmotion 구조CREATE TABLE default_emotion
(
id BIGINT NOT NULL AUTO_INCREMENT,
emotion VARCHAR(100) NOT NULL,
red INT NOT NULL,
green INT NOT NULL,
blue INT NOT NULL,
type VARCHAR(100) NOT NULL,
PRIMARY KEY (id)
)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class DefaultEmotion {
@Id
private Long id;
private String emotion;
private int red;
private int green;
private int blue;
@Enumerated(EnumType.ORDINAL)
private EmotionType type;
}
DefaultEmotionRepository 에서 getColorByEmotion 호출
@Repository
public class DefaultEmotionRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
public ColorResponse getColorByEmotion(String emotion){
String sql = "SELECT red, blue, green, type FROM default_emotion WHERE emotion = ?";
// RowMapper를 구현하여 ResultSet의 각 행을 ColorResponse 객체로 매핑합니다.
return jdbcTemplate.queryForObject(sql, new Object[]{emotion}, (rs, rowNum) -> {
ColorResponse colorResponse = new ColorResponse();
colorResponse.setRed(rs.getInt("red"));
colorResponse.setBlue(rs.getInt("blue"));
colorResponse.setGreen(rs.getInt("green"));
int type = Integer.parseInt(rs.getString("type"));
colorResponse.setType(EmotionType.values()[type]);
return colorResponse;
});
}
}
EmotionService 내에서 DefaultRepository 호출 후 DTO 매핑
@Repository
public interface DefaultRepository extends JpaRepository<DefaultEmotion, Long> {
Optional<DefaultEmotion> findByEmotion(String emotion);
}
@Service
@RequiredArgsConstructor
public class EmotionService {
private final DefaultRepository defaultRepository;
public ColorResponse getColorByEmotion(String emotion) {
DefaultEmotion entity = defaultRepository.findByEmotion(emotion)
.orElseThrow(() -> new EntityNotFoundException("Emotion not found: " + emotion));
ColorResponse response = new ColorResponse();
response.setRed(entity.getRed());
response.setBlue(entity.getBlue());
response.setGreen(entity.getGreen());
response.setType(entity.getType()); // enum 인덱스로 변환
return response;
}
}
DefaultRepository에서 findColorByEmotion 호출
@Repository
public interface DefaultRepository extends JpaRepository<DefaultEmotion, Long> {
@Query("SELECT new com.emotionmaster.emolog.color.dto.response.ColorResponse(d.red, d.blue, d.green, d.type) " +
"FROM DefaultEmotion d WHERE d.emotion = :emotion")
ColorResponse findColorByEmotion(@Param("emotion") String emotion);
(Native Query와 QueryDSL은 나중에 생각나서 추가해보았다!)
emotion = '머쓱하다'
JdbcTemplate: 17ms JPA + Service: 154ms JPQL: 106ms Native Query + Service : 109ms QueryDSL : 213ms
emotion = '사랑스럽다'
JdbcTemplate: 21ms JPA + Service: 184ms JPQL: 131ms Native Query + Service : 142ms QueryDSL : 254ms
emotion = '머쓱하다'
JdbcTemplate: 3 ms JPA + Service: 138 ms JPQL: 93 ms
emotion = '사랑스럽다'
JdbcTemplate: 4 ms JPA + Service: 72 ms JPQL: 45 ms
emotion = '머쓱하다'
JdbcTemplate: 13ms - 1ms - 0ms - 0ms - 0ms JPA + Service: 138ms - 3ms - 2ms - 2ms - 3ms JPQL: 108ms - 2ms - 1ms - 1ms - 1ms - 1ms
emotion = '사랑스럽다'
JdbcTemplate: 14ms - 0ms - 0ms - 0ms - 0ms JPA + Service: 130ms - 2ms - 2ms - 2ms - 2ms JPQL: 127ms - 2ms - 1ms - 1ms - 1ms - 1ms
결과적으로 JdbcTemplate > JPQL > Native Query > JPA > QueryDSL 순으로 조회가 빨랐으며, JdbcTemplate가 압도적으로 빠른 성능을 보였다.
emotion의 값을 변경해봐도 비슷한 수치를 보였다.
버퍼 풀에 캐시를 적재한 후의 조회는 얼핏 비슷해보였지만 여전히 JdbcTemplate 조회는 거의 0ms로 시간이 거의 걸리지 않는 수준이였다.
JdbcTemplate이 가장 빠른 이유?
RowMapper으로 DTO 변환을 즉시 수행하기 때문에 불필요한 로직 없이 바로 반환이 가능하다JPQL이 JPA보다 빠른 이유?
우선 애초에 JPA 내부에서 JDBC API가 실행되는 과정이기 때문에 JPA와 JPQL이 JdbcTemplate보다 느린 속도를 보일 수 밖에 없다.
나의 첫 예측은 JPA가 더 빠르다고 생각했다. 이유는 쿼리를 파싱 -> 분석 -> JPQL -> SQL 번역 -> 실행 단계를 진행하는 과정에서 시간이 더 걸릴 것 같았다.
하지만 JPQL이 확실하게 더 빠른 성능을 보였고, 그 이유는 DTO에 필요한 부분만 가져오는 게 큰 몫을 한다. 아래에 서브로 진행한 테스트를 보자
서브 테스트
JPQL을 DTO로 반환하는 것이 아닌 Entity 자체로 반환하도록 수정해보자
@Query("SELECT d FROM DefaultEmotion d WHERE d.emotion = :emotion")
DefaultEmotion findColorByEmotion(@Param("emotion") String emotion);
그러면 테스트 때 사용하던 JPA 구문과 똑같은 동작을 하게 된다.
Optional<DefaultEmotion> findByEmotion(@Param("emotion") String emotion);
1. 콜드 스타트 속도 측정
JPA : 139ms 수정 전 JPQL: 131ms 수정 후 JPQL : 182ms
2. 3회 평균 속도 측정
JPA : 42ms 수정 전 JPQL: 45 ms 수정 후 JPQL : 47ms
3. 5회 각각 속도 측정
JPA : 128ms - 2ms - 2ms - 1ms - 2ms 수정 전 JPQL: 127ms - 2ms - 1ms - 1ms - 1ms - 1ms 수정 후 JPQL : 181ms - 3ms - 2ms - 2ms - 2ms
결과적으로, 수정 후의 JPQL 쿼리가 전보다 훨씬 느려져서 JPA 보다 느려진 수치를 확인할 수 있고 DTO를 매핑한 JPQL이 JPA보다 빠르다는 사실이 입증되었다!
객체 중심의 설계를 지향하기 때문에 JPA를 많이 사용하지만 거기에서 더 나아가 JDBC, JPQL, QueryDSL, Native Query 등 적절히 섞어서 사용하는 것이 중요한 것 같다. 상황에 맞춰 유동적으로 또 다양하게 설계할 수 있는 힘을 길러야겠다 !!