벤치마크: easy-query vs jOOQ
I’m happy to translate the article for you, but I’ll need the full text of the post (the parts you’d like translated) pasted here. Could you please provide the article’s content? Once I have the text, I’ll keep the source line unchanged and translate everything else into Korean while preserving the original formatting, markdown, and code blocks.
개요
이 문서는 easy‑query, jOOQ, Hibernate를 비교한 JMH 벤치마크 결과를 제시합니다. 소스‑코드 분석을 통해 easy‑query가 뛰어난 성능을 달성하는 이유를 설명합니다.
테스트 환경
| 구성 | 값 |
|---|---|
| OS | Windows 10 (Build 26200) |
| JDK | OpenJDK 21.0.9+10‑LTS |
| Database | H2 2.2.224 (In‑Memory) |
| Connection Pool | HikariCP 4.0.3 |
| JMH | 1.37 |
| easy‑query | 3.1.66‑preview3 |
| jOOQ | 3.19.1 |
| Hibernate | 6.4.1.Final |
테스트 매개변수
- 워밍업: 10회 반복, 각 5 s |
- 측정: 15회 반복, 각 5 s |
- 포크: JVM 프로세스 3개 |
- 스레드: 단일 스레드
벤치마크 결과
쿼리 연산
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| ID로 선택 | 298 303 ops/s | 132 786 ops/s | 264 571 ops/s | 🏆 easy‑query (2.25× vs jOOQ) |
| 리스트 선택 | 247 088 ops/s | 68 773 ops/s | 141 050 ops/s | 🏆 easy‑query (3.59× vs jOOQ) |
| COUNT | 382 545 ops/s | 197 704 ops/s | 385 362 ops/s | Hibernate ≈ easy‑query |
복합 쿼리
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| JOIN 쿼리 | 138 963 ops/s | 5 859 ops/s | 56 437 ops/s | 🏆 easy‑query (23.72× vs jOOQ) |
| 서브쿼리 (GROUP BY + HAVING) | 100 725 ops/s | 5 296 ops/s | 15 594 ops/s | 🏆 easy‑query (19.01× vs jOOQ) |
쓰기 연산
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| 단일 삽입 | 63 866 ops/s | 50 257 ops/s | 57 385 ops/s | 🏆 easy‑query (1.27× vs jOOQ) |
| 배치 삽입 (1000) | 72.06 ops/s | 43.54 ops/s | 70.66 ops/s | 🏆 easy‑query (1.66× vs jOOQ) |
| ID로 업데이트 | 125 902 ops/s | 100 361 ops/s | 92 470 ops/s | 🏆 easy‑query (1.25× vs jOOQ) |
시각화
SELECT BY ID (ops/s – higher is better)
EasyQuery █████████████████████████████████████████████████ 298,303
Hibernate ████████████████████████████████████████████ 264,571
jOOQ ████████████ 132,786
JOIN QUERY (ops/s – higher is better)
EasyQuery █████████████████████████████████████████████████ 138,963
Hibernate ████████████████ 56,437
jOOQ █ 5,859
Source: …
왜 easy‑query가 이렇게 빠른가?
easy‑query는 fastjson2와 같은 고성능 프레임워크의 기법을 차용하여 여러 단계에서 깊은 최적화를 구현합니다.
1. 리플렉션 대신 람다 함수 매핑
핵심 성능 향상은 리플렉션 메서드 호출을 LambdaMetafactory가 생성한 함수로 교체한 데서 옵니다.
전통적인 리플렉션 (느림)
Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = (Integer) methodGetId.invoke(bean); // 매 호출마다 오버헤드 발생
리플렉션이 초래하는 비용:
- JVM 네이티브 메서드 스택 진입
- JNI 레이어 전환
- C 수준 체크
easy‑query의 해결책 – getter를 직접 호출하는 람다를 생성:
// sql-core/src/main/java/com/easy/query/core/common/bean/DefaultFastBean.java
/**
* Fast implementation of bean get/set methods.
* Lambda getter calls have virtually zero overhead.
* Setters are also zero‑overhead except for first‑time lambda creation and caching.
*/
public class DefaultFastBean implements FastBean {
private Property getLambdaProperty(FastBeanProperty prop) {
Class propertyType = prop.getPropertyType();
Method readMethod = prop.getReadMethod();
String getFunName = readMethod.getName();
final MethodHandles.Lookup caller = MethodHandles.lookup();
// Generate function mapping with LambdaMetafactory
final CallSite site = LambdaMetafactory.altMetafactory(
caller,
"apply",
MethodType.methodType(Property.class),
methodType.erase().generic(),
caller.findVirtual(beanClass, getFunName, MethodType.methodType(propertyType)),
methodType,
FLAG_SERIALIZABLE);
return (Property) site.getTarget().invokeExact();
}
}
성능 차이 (fastjson2 벤치마크)
| 메서드 | 10 000회 호출 시간 |
|---|---|
Method.invoke (리플렉션) | 25 ms |
| 람다 함수 매핑 | 1 ms |
≈ 25배 빠름!
주요 장점:
- 생성된 함수는 캐시되어 재사용될 수 있습니다.
- 이후 호출은 일반 Java 메서드 호출과 동일합니다.
- 실행이 완전히 Java 스택에서 이루어지므로 JNI 오버헤드가 없음.
2. 프로퍼티‑액세서 캐싱
시작 시점(또는 최초 사용 시) easy‑query는 각 엔티티 프로퍼티마다 람다를 생성하고 이를 캐시에 저장합니다:
// Getter cache
public Property getBeanGetter(FastBeanProperty prop) {
// In practice, with caching:
// return EasyMapUtil.computeIfAbsent(propertyGetterCache, prop.getName(),
// k -> getLambdaProperty(prop));
return getLambdaProperty(prop);
}
// Setter cache
public PropertySetterCaller getBeanSetter(FastBeanProperty prop) {
// return EasyMapUtil.computeIfAbsent(propertySetterCache, prop.getName(),
// k -> getLambdaPropertySetter(prop));
return getLambdaPropertySetter(prop);
}
// Constructor cache
public Supplier getBeanConstructorCreator() {
// return EasyMapUtil.computeIfAbsent(constructorCache, beanClass,
// k -> getLambdaCreate());
return getLambdaCreate();
}
- 첫 번째 접근 – 람다 생성 (≈ 50 µs).
- 그 이후 접근 – 캐시된 람다 사용 (≈ 0.1 µs).
3. 컴파일‑타임 프록시 생성 (APT)
런타임에 동적 프록시를 만들지 않고, easy‑query는 **Annotation Processing Tool (APT)**을 이용해 컴파일 단계에서 프록시 클래스를 생성합니다:
// Compile‑time generated proxy class knows all properties
@EntityProxy
public class User {
// fields, getters, setters, etc. are generated ahead of time
}
- 런타임 바이트코드 생성이 없으므로 시작 비용이 낮아짐.
- 모든 프로퍼티 메타데이터가 일반 Java 코드로 존재하므로 JIT 최적화에 유리합니다.
요약
- Lambda 기반 접근자 생성은 무거운 리플렉션 비용을 없앱니다.
- 캐싱은 최초 오버헤드를 일회성 비용으로 전환합니다.
- 컴파일 시점 프록시 생성은 런타임 프록시 생성을 완전히 방지합니다.
이러한 기술들을 결합하면 easy‑query가 jOOQ보다 지속적으로 성능이 뛰어나며, 특히 복잡한 쿼리와 배치 쓰기 작업에서 Hibernate보다 종종 우수합니다.
1. 엔티티 정의 및 프록시 (생성됨)
// Entity class
public class User {
private String id;
private String name;
private Integer age;
}
// Generated proxy class UserProxy
public class UserProxy {
public StringProperty id() { /* ... */ }
public StringProperty name() { /* ... */ }
public IntegerProperty age() { /* ... */ }
}
4. 직접 JDBC 실행
easy-query는 JDBC API를 직접 사용합니다:
// easy-query directly creates PreparedStatement
PreparedStatement ps = EasyJdbcExecutorUtil.createPreparedStatement(
connection, sql, parameters, easyJdbcTypeHandler);
ResultSet rs = ps.executeQuery();
5. 경량 SQL 생성
SQL은 간단한 문자열 연결을 통해 생성됩니다:
// Clean SQL building
easyEntityQuery.queryable(User.class)
.where(u -> u.age().ge(25))
.orderBy(u -> u.username().desc())
.limit(10)
.toList();
생성된 SQL
SELECT id, username, email, age, phone, address
FROM t_user
WHERE age >= ?
ORDER BY username DESC
LIMIT ?
복잡한 추상화 계층이 없습니다—직접 SQL을 생성합니다.
6. ResultSet 매핑 최적화
Lambda‑function 매핑과 결합하면 ResultSet‑to‑object 변환에 오버헤드가 전혀 발생하지 않습니다:
// Using cached Setter Lambda to set property values
PropertySetterCaller setter = fastBean.getBeanSetter(prop);
setter.call(entity, resultSet.getObject(columnIndex));
이는 entity.setName(value)를 직접 호출하는 것과 동일하며, 리플렉션 오버헤드가 없습니다.
easy‑query 최적화 요약
| 최적화 | easy‑query |
|---|---|
| 리플렉션 대신 람다 함수 매핑 | ✅ |
| 컴파일 시점 프록시 클래스 (APT) | ✅ |
| 직접 JDBC 실행 | ✅ |
| 프로퍼티 접근자 캐싱 | ✅ |
| 경량 SQL 생성 | ✅ |
결론
easy-query는 다음을 통해 높은 성능을 달성합니다:
- 핵심 기술 –
LambdaMetafactory를 사용하여 함수 매핑을 생성합니다 (fastjson2와 동일한 기법). - 컴파일 시 최적화 – APT가 생성한 프록시 클래스가 런타임 동적 프록시를 제거합니다.
- 경량 아키텍처 – 추가 추상화 레이어 없이 직접 JDBC를 사용합니다.
- 캐싱 전략 – 함수 매핑과 메타데이터를 재사용을 위해 캐시합니다.
“Lambda는
LambdaMetafactory를 사용하여 리플렉션 대신 함수 매핑을 생성합니다. 생성된 함수는 재사용을 위해 캐시될 수 있으며, 이후 호출은 전적으로 Java 스택 호출 내에서 이루어져 사실상 네이티브 메서드 호출과 동일합니다.” – Fastjson2 저자
easy-query는 이 철학을 ORM 전체에 적용하여 강력한 타입 지정, Lambda 구문 및 암시적 조인을 제공하면서도 거의 네이티브 수준의 JDBC 성능을 제공합니다.
관련 링크
- 벤치마크 코드:
- easy‑query GitHub:
- jOOQ: