2025년 9월 28일 작성
JPA 성능 최적화하기 (N+1 해결, Fetch 전략, Cache 활용)
JPA는 개발 생산성을 높이지만 잘못 사용하면 심각한 성능 문제를 야기하기 때문에, N+1 query 해결, 적절한 fetch 전략, cache 활용 등, 체계적인 성능 최적화 전략이 필요합니다.
JPA 성능 최적화
- JPA는 개발 생산성을 크게 향상시키지만, 잘못 사용하면 심각한 성능 문제를 야기할 수 있습니다.
- N+1 query, 불필요한 data loading, 비효율적인 cache 사용 등이 주요 성능 저해 요소입니다.
- 따라서 JPA 성능 최적화는 N+1 query 해결, 적절한 fetch 전략, cache 활용이 핵심입니다.
- monitoring과 profiling을 통해 성능 병목 지점을 정확히 파악해야 합니다.
- 체계적인 접근을 통해 단계별로 성능을 개선하고, 지속적인 monitoring으로 성능을 유지해야 합니다.
- database schema 설계와 query 최적화도 JPA 성능에 큰 영향을 미칩니다.
JPA 성능 문제의 주요 원인
- ORM 추상화의 부작용 : 개발자가 실제 실행되는 SQL을 인지하지 못해 비효율적인 query가 생성됩니다.
- 간단해 보이는 객체 탐색이 수십 개의 SQL을 실행할 수 있습니다.
- lazy loading과 eager loading의 잘못된 설정이 성능 문제를 유발합니다.
- entity 연관 관계 탐색 시 예상치 못한 추가 query가 실행됩니다.
- persistence context 오용 : entity lifecycle 관리의 오해로 인한 성능 저하가 발생합니다.
- 장시간 유지되는 persistence context로 인한 memory 누수가 발생할 수 있습니다.
- 대용량 data 처리 시 1차 cache가 오히려 성능을 저하시킬 수 있습니다.
- dirty checking overhead가 read-heavy application에서 불필요한 부담을 줍니다.
- database 설계와의 불일치 : JPA mapping과 database 최적화 간의 mismatch가 문제를 야기합니다.
- 객체 지향 설계와 관계형 database 최적화 사이의 trade-off를 고려해야 합니다.
- index 설계와 JPA query pattern이 맞지 않아 성능이 저하될 수 있습니다.
- 정규화된 schema와 JPA의 object graph 탐색이 충돌할 수 있습니다.
성능 최적화 전략
- 측정 기반 최적화 : 추측이 아닌 실제 측정 data를 바탕으로 최적화를 수행해야 합니다.
- SQL logging을 통해 실제 실행되는 query를 확인하고 분석합니다.
- application performance monitoring을 통해 bottleneck을 정확히 파악합니다.
- load testing을 통해 실제 운영 환경과 유사한 조건에서 성능을 검증합니다.
- 단계별 접근 : 가장 효과가 큰 최적화부터 단계적으로 적용합니다.
- N+1 query 해결을 최우선으로 하여 가장 큰 성능 향상을 달성합니다.
- fetch 전략 최적화를 통해 불필요한 data loading을 제거합니다.
- cache 적용으로 반복적인 database 접근을 줄입니다.
- 지속적인 관리 : 성능 최적화는 일회성이 아닌 지속적인 관리가 필요합니다.
- 정기적인 performance review를 통해 성능 regression을 방지합니다.
- 새로운 기능 개발 시 성능 영향도를 사전에 검토합니다.
- monitoring alert을 설정하여 성능 문제를 조기에 발견하고 대응합니다.
N+1 Query 문제 해결
- N+1 query 문제는 JPA 사용 시 가장 흔히 발생하는 성능 문제입니다.
- 연관 entity 조회 시 예상보다 많은 query가 실행되는 현상으로, 심각한 성능 저하를 초래할 수 있습니다.
- fetch join, batch fetching, entity graph 등 다양한 해결 방법을 상황에 맞게 선택해야 합니다.
N+1 Query 발생 원인과 문제점
- lazy loading의 부작용 : 연관 entity를 개별적으로 loading할 때 발생합니다.
- 하나의 query로 N개의 entity를 조회한 후, 각 entity의 연관 data를 위해 N번의 추가 query가 실행됩니다.
@OneToMany,@ManyToOne등의 연관 관계에서 주로 발생합니다.- 개발 단계에서는 작은 dataset으로 인해 문제가 드러나지 않을 수 있습니다.
// N+1 문제 발생 예제
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class)
.getResultList(); // 1번의 query로 orders 조회
for (Order order : orders) {
System.out.println(order.getUser().getName()); // 각 order마다 user 조회 query 실행 (N번)
}
// 총 1 + N번의 query 실행
- eager loading의 cartesian product : 여러 연관 관계를 eager loading할 때 발생합니다.
- 두 개 이상의 collection을 동시에 fetch하면 cartesian product가 생성됩니다.
- 결과 set이 기하급수적으로 증가하여 memory와 network 사용량이 급증합니다.
- database에서 중복된 data를 반복적으로 전송하게 됩니다.
- 문제의 심각성 : production 환경에서 치명적인 성능 저하를 야기할 수 있습니다.
- 10개의 order가 있으면 11번의 query가 실행되지만, 1000개가 있으면 1001번이 실행됩니다.
- database connection pool 고갈과 network traffic 급증을 초래할 수 있습니다.
- application response time이 수십 배 증가할 수 있습니다.
Fetch Join을 통한 해결
- JPQL fetch join : 가장 확실한 N+1 문제 해결 방법입니다.
JOIN FETCHkeyword를 사용하여 연관 entity를 한 번에 조회합니다.- 하나의 SQL query로 필요한 모든 data를 가져와 추가 query 실행을 방지합니다.
- inner join과 left join을 적절히 선택하여 data 무결성을 보장해야 합니다.
// fetch join을 사용한 N+1 해결
List<Order> orders = entityManager.createQuery(
"SELECT o FROM Order o JOIN FETCH o.user", Order.class)
.getResultList();
for (Order order : orders) {
System.out.println(order.getUser().getName()); // 추가 query 없이 접근 가능
}
// 여러 연관 관계 fetch
List<Order> ordersWithItems = entityManager.createQuery(
"SELECT DISTINCT o FROM Order o " +
"JOIN FETCH o.user " +
"JOIN FETCH o.orderItems", Order.class)
.getResultList();
- fetch join 제약 사항과 해결책 : fetch join 사용 시 주의해야 할 점들이 있습니다.
- 두 개 이상의 collection을 동시에 fetch join하면 cartesian product 문제가 발생합니다.
DISTINCTkeyword를 사용하여 중복 제거를 하되, application level에서 처리해야 합니다.- pagination과 함께 사용할 때는 memory에서 sorting이 발생할 수 있습니다.
// 단계별 fetch join으로 cartesian product 방지
List<User> users = entityManager.createQuery(
"SELECT DISTINCT u FROM User u JOIN FETCH u.orders", User.class)
.getResultList();
// 두 번째 query에서 addresses fetch
users = entityManager.createQuery(
"SELECT DISTINCT u FROM User u JOIN FETCH u.addresses WHERE u IN :users", User.class)
.setParameter("users", users)
.getResultList();
Batch Fetching 전략
- @BatchSize annotation : 연관 entity를 batch 단위로 loading합니다.
- entity나 collection에
@BatchSize(size = 20)을 설정하여 batch loading을 활성화합니다. - N+1 query를 N/batch_size + 1 query로 줄여 성능을 크게 향상시킵니다.
- memory 사용량과 query 횟수의 적절한 balance를 찾아 batch size를 조정해야 합니다.
- entity나 collection에
@Entity
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
@BatchSize(size = 20)
private User user;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
@BatchSize(size = 15)
private List<OrderItem> orderItems = new ArrayList<>();
}
// 사용 예제
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class)
.setMaxResults(50)
.getResultList();
for (Order order : orders) {
order.getUser().getName(); // batch loading으로 효율적으로 처리
}
- global batch size 설정 : application 전체에 적용되는 batch size를 설정합니다.
hibernate.default_batch_fetch_sizeproperty로 global 설정이 가능합니다.- 개별 entity의
@BatchSizeannotation이 global 설정보다 우선순위가 높습니다. - 적절한 batch size는 보통 4, 8, 16, 32 등의 2의 거듭제곱 값을 사용합니다.
- JDBC driver와 database가 2의 거듭제곱 단위로 최적화된 경우가 많기 때문입니다.
# hibernate properties
hibernate.default_batch_fetch_size=16
Entity Graph 활용
- JPA 2.1 entity graph : 동적으로 fetch 전략을 제어할 수 있는 강력한 기능입니다.
@NamedEntityGraphannotation으로 미리 정의하거나 runtime에 동적으로 생성할 수 있습니다.- 상황에 따라 다른 loading 전략을 적용하여 flexibility를 확보합니다.
- query hint로 entity graph를 지정하여 사용합니다.
@Entity
@NamedEntityGraphs({
@NamedEntityGraph(
name = "Order.withUser",
attributeNodes = @NamedAttributeNode("user")
),
@NamedEntityGraph(
name = "Order.withUserAndItems",
attributeNodes = {
@NamedAttributeNode("user"),
@NamedAttributeNode(value = "orderItems", subgraph = "items")
},
subgraphs = @NamedSubgraph(
name = "items",
attributeNodes = @NamedAttributeNode("product")
)
)
})
public class Order {
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
}
// 사용 방법
List<Order> orders = entityManager.createQuery("SELECT o FROM Order o", Order.class)
.setHint("javax.persistence.fetchgraph",
entityManager.getEntityGraph("Order.withUser"))
.getResultList();
Fetch 전략 최적화
- 적절한 fetch 전략 선택은 JPA 성능에 직접적인 영향을 미칩니다.
- lazy vs eager loading의 trade-off를 이해하고 사용 pattern에 맞게 설정해야 합니다.
- projection과 DTO pattern을 활용하여 필요한 data만 효율적으로 조회할 수 있습니다.
Lazy vs Eager Loading 최적화
- 기본 전략 조정 : JPA의 기본 fetch type을 사용 pattern에 맞게 조정해야 합니다.
@OneToOne,@ManyToOne은 기본적으로 EAGER이지만 대부분 LAZY로 변경하는 것을 권장합니다.@OneToMany,@ManyToMany는 기본적으로 LAZY이며 이를 유지하는 것이 좋습니다.- 80% 이상 함께 사용되는 연관 관계만 EAGER loading을 고려합니다.
@Entity
public class Order {
// 기본값이 EAGER이지만 LAZY로 변경 권장
@ManyToOne(fetch = FetchType.LAZY)
private User user;
// 기본값이 LAZY (권장)
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> orderItems = new ArrayList<>();
// 항상 함께 사용되는 경우에만 EAGER 고려
@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private OrderSummary summary;
}
- 사용 pattern 분석 : 실제 application에서 entity가 어떻게 사용되는지 분석해야 합니다.
- 각 연관 관계별 접근 빈도를 측정하여 적절한 fetch 전략을 결정합니다.
- admin 기능과 일반 user 기능에서 서로 다른 fetch 요구 사항이 있을 수 있습니다.
- business logic 변화에 따라 fetch 전략도 지속적으로 조정해야 합니다.
Projection 활용
- JPQL projection : 필요한 field만 선택하여 조회하는 기법입니다.
- entity 전체가 아닌 특정 field만 조회하여 network traffic과 memory 사용량을 줄입니다.
- constructor expression을 사용하여 DTO 객체를 직접 생성할 수 있습니다.
- 대용량 entity나 BLOB field가 있는 경우 특히 효과적입니다.
// DTO class
public class UserSummary {
private String username;
private String email;
private Long orderCount;
public UserSummary(String username, String email, Long orderCount) {
this.username = username;
this.email = email;
this.orderCount = orderCount;
}
}
// Constructor expression 사용
List<UserSummary> summaries = entityManager.createQuery(
"SELECT NEW com.example.dto.UserSummary(u.username, u.email, " +
"(SELECT COUNT(o) FROM Order o WHERE o.user = u)) " +
"FROM User u WHERE u.active = true", UserSummary.class)
.getResultList();
// 특정 field만 조회
List<Object[]> results = entityManager.createQuery(
"SELECT u.username, u.email FROM User u WHERE u.active = true")
.getResultList();
- interface 기반 projection : Spring Data JPA에서 제공하는 projection 기능입니다.
- interface를 정의하여 필요한 property만 노출할 수 있습니다.
- dynamic projection을 통해 runtime에 projection type을 결정할 수 있습니다.
- nested projection을 사용하여 연관 entity의 일부 정보도 포함할 수 있습니다.
Read-Only Query 최적화
- @Transactional(readOnly = true) : 조회 전용 query의 성능을 최적화합니다.
- Hibernate의 flush mode가 MANUAL로 변경되어 dirty checking이 비활성화됩니다.
- session-level cache의 overhead가 줄어들어 memory 사용량이 감소합니다.
- database connection pool에서도 read-only connection을 별도로 관리할 수 있습니다.
@Service
@Transactional(readOnly = true)
public class UserQueryService {
public List<User> findActiveUsers() {
// read-only transaction에서 실행
// dirty checking 비활성화로 성능 향상
return userRepository.findByActiveTrue();
}
public UserStatistics getUserStatistics() {
// 복잡한 집계 query도 read-only에서 최적화
return userRepository.calculateUserStatistics();
}
}
Cache 전략
- 적절한 cache 전략은 JPA 성능 향상의 핵심 요소입니다.
- 1차 cache와 2차 cache의 특성을 이해하고 효과적으로 활용해야 합니다.
- query result cache를 통해 더욱 세밀한 성능 최적화가 가능합니다.
1차 Cache 활용
- persistence context cache : transaction 범위 내에서 entity의 uniqueness와 caching을 보장합니다.
- 같은 primary key로 조회 시 database 접근 없이 memory에서 반환합니다.
- entity의 identity를 보장하고 consistent한 상태를 유지합니다.
- transaction 종료 시 자동으로 clear되므로 별도 관리가 불필요합니다.
@Transactional
public void demonstrateFirstLevelCache() {
// 첫 번째 조회: database에서 loading
User user1 = entityManager.find(User.class, 1L);
// 두 번째 조회: 1차 cache에서 반환 (database 접근 없음)
User user2 = entityManager.find(User.class, 1L);
// 같은 instance임을 보장
assert user1 == user2; // true
}
- 1차 cache 관리 : 적절한 cache 관리를 통해 memory 사용량을 제어합니다.
entityManager.clear()로 전체 persistence context를 정리할 수 있습니다.- 대용량 batch processing에서는 정기적인 cache 정리가 필요합니다.
entityManager.detach(entity)로 특정 entity만 cache에서 제거할 수 있습니다.
2차 Cache 설정
- 2차 cache 개념 : 여러 transaction 간에 공유되는 application level cache입니다.
- EntityManagerFactory level에서 관리되는 shared cache입니다.
- 자주 조회되는 reference data나 변경이 적은 entity에 적용합니다.
- cache provider로 Ehcache, Hazelcast, Infinispan 등을 선택할 수 있습니다.
# hibernate cache 설정
hibernate.cache.use_second_level_cache=true
hibernate.cache.region.factory_class=org.hibernate.cache.jcache.JCacheRegionFactory
hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
- cache concurrency strategy : entity의 사용 pattern에 맞는 cache 전략을 선택합니다.
- READ_ONLY : 읽기 전용 data에 사용하며 가장 빠른 성능을 제공합니다.
- NONSTRICT_READ_WRITE : 가끔 변경되는 data에 사용하며 cache consistency를 느슨하게 관리합니다.
- READ_WRITE : 자주 변경되는 data에 사용하며 strong consistency를 보장합니다.
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class Country {
@Id
private String code;
private String name;
// reference data는 READ_ONLY 전략 적합
}
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User {
@Id
private Long id;
private String username;
private String email;
// 자주 변경되는 user data는 READ_WRITE 전략
}
Query Result Cache
- query cache 활성화 : JPQL query 결과를 cache하여 반복 실행 시 성능을 향상시킵니다.
hibernate.cache.use_query_cache=true설정으로 query cache를 활성화합니다.- query별로
setCacheable(true)또는 hint를 사용하여 cache를 적용합니다. - query string과 parameter를 key로 사용하여 cache entry를 관리합니다.
// Query cache 사용
List<User> activeUsers = entityManager.createQuery(
"SELECT u FROM User u WHERE u.active = true ORDER BY u.username", User.class)
.setHint("org.hibernate.cacheable", true)
.getResultList();
// 또는 setCacheable 사용
List<Product> expensiveProducts = entityManager.createQuery(
"SELECT p FROM Product p WHERE p.price > :price", Product.class)
.setParameter("price", new BigDecimal("1000"))
.setCacheable(true)
.getResultList();
- cache 효과 측정 : cache hit ratio를 monitoring하여 cache 효과를 검증합니다.
- Hibernate statistics를 통해 cache hit/miss ratio를 확인할 수 있습니다.
- cache hit ratio가 80% 이상이 되도록 cache 전략을 조정합니다.
- parameter가 자주 변경되는 query는 cache 효과가 제한적입니다.
Bulk Operation
- 대용량 data 처리에서는 개별 entity 단위 작업보다 bulk operation이 훨씬 효율적입니다.
- JPQL bulk update/delete와 batch processing을 활용하여 성능을 대폭 향상시킬 수 있습니다.
- bulk operation 사용 시 persistence context와의 동기화 문제를 주의해야 합니다.
JPQL Bulk Update/Delete
- bulk update : 여러 entity를 한 번에 update하는 효율적인 방법입니다.
- 개별 entity를 조회하고 수정하는 것보다 수십 배 빠른 성능을 제공합니다.
- 하나의 UPDATE SQL로 조건에 맞는 모든 record를 한 번에 처리합니다.
- persistence context를 bypass하므로 entity lifecycle event가 발생하지 않습니다.
// 비효율적인 방법: 개별 entity update
@Transactional
public void updateUserStatusIndividually(UserStatus oldStatus, UserStatus newStatus) {
List<User> users = entityManager.createQuery(
"SELECT u FROM User u WHERE u.status = :oldStatus", User.class)
.setParameter("oldStatus", oldStatus)
.getResultList();
for (User user : users) {
user.setStatus(newStatus); // 각 user마다 UPDATE SQL 실행
}
// N번의 UPDATE SQL 실행
}
// 효율적인 방법: bulk update
@Transactional
public int updateUserStatusBulk(UserStatus oldStatus, UserStatus newStatus) {
return entityManager.createQuery(
"UPDATE User u SET u.status = :newStatus, u.lastModified = :now " +
"WHERE u.status = :oldStatus")
.setParameter("newStatus", newStatus)
.setParameter("oldStatus", oldStatus)
.setParameter("now", LocalDateTime.now())
.executeUpdate(); // 1번의 UPDATE SQL로 모든 record 처리
}
- persistence context 동기화 : bulk operation 후 persistence context 정리가 필요합니다.
- bulk operation은 persistence context를 bypass하므로 cache된 entity가 outdated될 수 있습니다.
entityManager.clear()를 호출하여 persistence context를 정리해야 합니다.- 이후 entity 조회 시 database에서 최신 data를 가져옵니다.
@Transactional
public void updateAndRefresh() {
// Bulk update 실행
int updatedCount = entityManager.createQuery(
"UPDATE User u SET u.lastLogin = :now WHERE u.active = true")
.setParameter("now", LocalDateTime.now())
.executeUpdate();
// Persistence context 정리 (중요!)
entityManager.clear();
// 이제 entity 조회 시 최신 data가 반환됨
User user = entityManager.find(User.class, 1L);
}
Batch Processing
- JDBC batch size 설정 : 여러 DML 연산을 batch로 묶어서 실행합니다.
hibernate.jdbc.batch_sizeproperty로 batch 크기를 설정합니다.- network round trip 횟수를 줄여 성능을 향상시킵니다.
- 적절한 batch size는 보통 10-50 정도이며, 환경에 따라 tuning이 필요합니다.
# hibernate.properties
hibernate.jdbc.batch_size=25
hibernate.order_inserts=true
hibernate.order_updates=true
hibernate.jdbc.batch_versioned_data=true
@Transactional
public void batchInsertUsers(List<User> users) {
int batchSize = 25;
for (int i = 0; i < users.size(); i++) {
entityManager.persist(users.get(i));
// batch size마다 flush하여 batch 실행
if (i % batchSize == 0) {
entityManager.flush();
entityManager.clear();
}
}
// 남은 entity들 처리
entityManager.flush();
entityManager.clear();
}
Performance Monitoring
- 지속적인 monitoring과 profiling을 통해 성능 병목 지점을 파악해야 합니다.
- SQL logging과 statistics 수집으로 실제 database 접근 pattern을 분석합니다.
- APM tool과 database monitoring을 통해 종합적인 성능 관리가 필요합니다.
Hibernate Statistics
- statistics 활성화 : Hibernate 내부 통계를 수집하여 성능을 분석합니다.
hibernate.generate_statistics=true설정으로 statistics 수집을 활성화합니다.- query 실행 횟수, cache hit ratio, connection 사용량 등을 monitoring합니다.
- JMX를 통해 runtime에 statistics를 확인하고 관리할 수 있습니다.
# Statistics 활성화
hibernate.generate_statistics=true
hibernate.stats.fetch_statistics=true
@Service
public class HibernateStatsService {
@PersistenceContext
private EntityManager entityManager;
public void logStatistics() {
SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
.unwrap(SessionFactory.class);
Statistics stats = sessionFactory.getStatistics();
System.out.println("Entity fetch count: " + stats.getEntityFetchCount());
System.out.println("Entity load count: " + stats.getEntityLoadCount());
System.out.println("Query execution count: " + stats.getQueryExecutionCount());
System.out.println("Cache hit count: " + stats.getSecondLevelCacheHitCount());
System.out.println("Cache miss count: " + stats.getSecondLevelCacheMissCount());
// Cache hit ratio 계산
long hitCount = stats.getSecondLevelCacheHitCount();
long missCount = stats.getSecondLevelCacheMissCount();
double hitRatio = (double) hitCount / (hitCount + missCount) * 100;
System.out.println("Cache hit ratio: " + String.format("%.2f%%", hitRatio));
}
public void clearStatistics() {
SessionFactory sessionFactory = entityManager.getEntityManagerFactory()
.unwrap(SessionFactory.class);
sessionFactory.getStatistics().clear();
}
}
- JMX monitoring : JMX Bean을 통해 runtime에 성능 지표를 monitoring합니다.
- Spring Boot Actuator와 연동하여 HTTP endpoint로 통계를 확인할 수 있습니다.
- Micrometer와 통합하여 Prometheus, Grafana 등으로 시각화가 가능합니다.
- 임계값 기반 alert을 설정하여 성능 문제를 조기에 감지합니다.
SQL Logging
- SQL logging 설정 : 실제 실행되는 SQL을 확인하여 성능 문제를 진단합니다.
show_sql=true와format_sql=true로 SQL을 readable하게 출력합니다.use_sql_comments=true로 JPQL과 SQL의 연관성을 파악할 수 있습니다.- parameter binding을 확인하여 prepared statement의 효과를 검증합니다.
# SQL logging 설정
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.use_sql_comments=true
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE
- P6Spy 활용 : 더 자세한 SQL 정보를 확인할 수 있는 도구입니다.
- 실행 시간과 함께 실제 parameter 값이 binding된 SQL을 확인할 수 있습니다.
- slow query 감지와 performance 분석에 유용합니다.
- production 환경에서는 overhead를 고려하여 선택적으로 사용해야 합니다.
# P6Spy 설정
spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver
spring.datasource.url=jdbc:p6spy:h2:mem:testdb
# spy.properties
appender=com.p6spy.engine.spy.appender.Slf4JLogger
logMessageFormat=com.p6spy.engine.spy.appender.MultiLineFormat
Application Performance Monitoring
- APM tool 활용 : 종합적인 application 성능 monitoring을 위해 APM tool을 활용합니다.
- New Relic, AppDynamics, Datadog 등의 commercial APM solution을 사용할 수 있습니다.
- Micrometer와 Spring Boot Actuator를 활용한 custom monitoring도 가능합니다.
- database connection pool, query execution time, throughput 등을 실시간으로 monitoring합니다.
@Component
public class JpaPerformanceMetrics {
private final MeterRegistry meterRegistry;
private final Timer.Sample queryTimer;
public JpaPerformanceMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
@EventListener
public void handleQueryExecution(QueryExecutionEvent event) {
Timer.builder("jpa.query.execution")
.tag("query.type", event.getQueryType())
.tag("entity", event.getEntityName())
.register(meterRegistry)
.record(event.getExecutionTime(), TimeUnit.MILLISECONDS);
}
@EventListener
public void handleCacheEvent(CacheEvent event) {
Counter.builder("jpa.cache." + event.getType())
.tag("region", event.getRegionName())
.register(meterRegistry)
.increment();
}
}
- performance baseline 설정 : 정상적인 성능 기준을 설정하고 deviation을 monitoring합니다.
- 평균 응답 시간, throughput, error rate 등의 baseline을 설정합니다.
- 성능 regression을 조기에 발견하기 위한 alert 규칙을 정의합니다.
- load testing 결과를 바탕으로 realistic한 성능 목표를 설정합니다.
Performance Tuning Checklist
- 체계적인 성능 점검을 통해 JPA application의 성능을 지속적으로 관리해야 합니다.
- 단계별 checklist를 활용하여 놓치기 쉬운 최적화 지점(point)을 확인합니다.
- 정기적인 성능 review를 통해 성능 regression을 방지하고 지속적인 개선을 달성합니다.
개발 단계 Checklist
- entity 설계 검토 : entity 설계가 성능에 미치는 영향을 사전에 검토합니다.
- 모든 연관 관계가 LAZY loading으로 설정되어 있는가.
@BatchSizeannotation이 적절히 설정되어 있는가.- entity graph나 fetch join 전략이 계획되어 있는가.
- 대용량 field(BLOB, CLOB)가 별도 entity로 분리되어 있는가.
- query 작성 검토 : JPQL과 native query 작성 시 성능을 고려합니다.
- N+1 query가 발생할 가능성이 있는 코드를 확인했는가.
- fetch join이나 entity graph를 적절히 사용했는가.
- bulk operation이 필요한 곳에서 개별 entity 수정을 하고 있지 않은가.
- projection을 활용하여 불필요한 data loading을 방지했는가.
- cache 전략 수립 : entity별 적절한 cache 전략을 수립합니다.
- reference data에 대해 2차 cache가 설정되어 있는가.
- 자주 실행되는 query에 대해 query cache가 고려되었는가.
- cache concurrency strategy가 적절히 선택되었는가.
- cache region별로 TTL과 size limit이 설정되어 있는가.
Test 단계 Checklist
- 성능 test 실행 : 실제 운영 환경과 유사한 조건에서 성능을 검증합니다.
- 실제 data volume과 유사한 환경에서 테스트했는가.
- SQL logging을 통해 실행되는 query를 확인했는가.
- 예상 load에서 응답 시간이 목표치를 만족하는가.
- memory usage와 connection pool 사용량이 적정한가.
- monitoring 설정 : 성능 지표를 수집하고 분석할 수 있는 환경을 구축합니다.
- Hibernate statistics가 활성화되어 있는가.
- APM tool이나 custom metric 수집이 설정되어 있는가.
- database monitoring과 연동되어 있는가.
- alert 규칙이 정의되어 성능 문제를 조기에 감지할 수 있는가.
운영 단계 Checklist
- 정기적인 성능 review : 운영 중인 system의 성능을 지속적으로 관리합니다.
- 주간/월간 성능 report를 통해 trend를 파악하고 있는가.
- slow query 발생 현황을 정기적으로 검토하고 있는가.
- cache hit ratio가 목표치를 유지하고 있는가.
- connection pool 사용률과 wait time이 정상 범위에 있는가.
- 성능 문제 대응 : 성능 문제 발생 시 신속한 대응 체계를 운영합니다.
- 성능 문제 발생 시 escalation 절차가 정의되어 있는가.
- query plan 분석과 index 추가 등의 대응 방안이 준비되어 있는가.
- 긴급 상황에서 cache clear나 connection pool 재설정이 가능한가.
- 성능 문제 해결 후 근본 원인 분석과 재발 방지 대책이 수립되는가.
- 지속적인 개선 : 성능 최적화는 일회성이 아닌 지속적인 process입니다.
- 새로운 기능 개발 시 성능 영향도 검토가 포함되어 있는가.
- database schema 변경 시 JPA mapping과의 영향도를 검토하는가.
- 정기적으로 query 최적화와 index tuning을 수행하고 있는가.
- 성능 best practice가 개발팀 내에서 공유되고 있는가.
긴급 상황 대응 Checklist
- 성능 장애 발생 시 : 신속한 문제 해결을 위한 체계적인 접근이 필요합니다.
- 현재 실행 중인 slow query를 확인했는가.
- connection pool 상태와 database lock 상황을 점검했는가.
- cache 상태와 hit ratio를 확인했는가.
- application memory 사용량과 GC 상황을 점검했는가.
- 임시 조치 및 복구 : service 영향을 최소화하면서 문제를 해결합니다.
- 문제가 되는 기능의 일시 비활성화를 고려했는가.
- cache warm-up이나 connection pool 재설정을 시도했는가.
- query timeout 설정이나 batch size 조정을 고려했는가.
- database connection limit 증설이나 read replica 활용을 검토했는가.
Reference
- https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html
- https://vladmihalcea.com/tutorials/hibernate/
- https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
- https://www.baeldung.com/jpa-hibernate-performance
- https://blog.jooq.org/tag/jpa-performance/