Page History
Info |
---|
JPA는 SQL문을 통해 어플리케이션을 작성했을때보다, 수많은 귀찮은 일을 하지 않아도된다는것을 실습을 통해 파악을 하였습니다. 하지만 실제 그것이 어떠한 SQL문을 수행하는지 또한 그 SQL문이 성능적으로 문제가 없는지 데이터베이스를 병행해서 공부해야하는 과제가 있습니다. 실행계획을 예상하고 측정하는것은 아주 광범위한 주제입니다. 여기서는 다소 자연스럽지 못하더라도 데이터베이스를 공부했을때 일반적인 사항과 JPA의 내용과 엮어보겠습니다. |
스캔 실행계획
데이터베이스를 공부했을때 성능에 관련된 실행계획은 SQL과 더불어 JPA에서도 이해해야하는 항목으로 생각됩니다. |
Table of Contents |
---|
실행계획 고려하기
SQL문을 해석하고 , 원하는 데이터 집합을 IO에서 빠르게 가져오기위한 노력으로
DB엔진은 구문분석후 효과적인 결과를 내기위해 관계형 데이터베이스의 종류는 많으며, SQL문을 빠르게 작동시키기위해 실행계획을 세우게 되며
이것은 각각의 데이터베이스마다 다르며 풀스캔/인덱스스캔등의 차이에따라도 다른 실행계획이 나옵니다.
또한 DB가 IO장치를 어떻게 활용할것인가? 까지 포함하면 더욱더 상이하고 복잡한 실행계획이 수행됩니다.
데이터베이스마다 차이가있습니다.
SQL문에서 이러한 실행 계획에대한 옵션을 직접 할수 SQL문에서 이러한 실행계획을 조정할수 없을뿐더러, 전적으로 DataBase에게 위임하게 됩니다.
즉, JPA도 이러한 실행계획에 관여할수 없으며 이러한 실행계획을 확인하는 방법은 SQL문을 실제
실행하여 어떻게 작동되는지 확인하는것입니다.
이름 | 명령어 |
---|---|
Oracle | set autotrace traceonly |
MSSQL | SET SHOWPLAN_TEXT_ON |
DB2 | EXPLAIN ALL WITH SNAPSHOT FOR SQL 구문 |
PostreSQL | EXPAIN SQL 구문 |
MySQL | EXPLAIN EXTENDED SQL 구문 |
JPA Query Method TO SQL
JPA | SQL |
---|---|
studentList = findByGroupInfoName("학생") | SELECT * FROM user ui left join spring.group_info gi on gi.group_id = ui.group_id and gi.name="학생" -- 위구문을 어플리케이션사용하기위해서는 실제는 몇가지 더 변환과정이 필요합니다. dataset = runsql(..) javaobj = datasetToObj(result) jsonobj = objToJson(javaobj) |
앞장에서 이미 설명된 컨셉이며, JPA가 SQL에 비해 어플리케이션에서 단순하고 일관성있게 사용하는지
보여주는 예입니다. 반환타잎은 Java오브젝트뿐만아니라,웹에서 바로 사용가능한 Json 결과로 뿌려줄수 있기떄문에
JPA에서 사용자정보에서 학생정보만 추출하는 과정이 이 한줄이외에 더 추가되는것은 개발 생산성 낭비로
측정하고 있습니다. 하지만 SQL문을 직접 작성하지 않을뿐 이러한 JPA 인터페이스가 어떤 SQL문으로 변환되어
실행이 되고 실제 데이터베이스에서 실행계획이 어떻게 되는지 점검할 필요가 있습니다.
...
JPA에 익숙하다면, 자신이 작성한 함수 인터페이스가,
어떠한 SQL문으로 변환되어 작동이 될지 예측할수도 있지만, JPA를 믿지 못하겠다고 가정해봅시다.
이럴때 아래옵션을 통해, 함수호출시마다 SQL문을 확인할수가 있습니다.
이것을 켜는 이유는 JPA가 어떠한 엉뚱한 SQL문을 만들어낼지?
불필요하게 중복호출을 하지 않는지?(사실 이게 가장 중요합니다.) 또는 그 SQL문이
제대로된 실행계획을 가지고 실행이되는지 검증을 하기 위함입니다.
JPA-SQL문 Trace
- spring.jpa.properties.hibernate.show_sql=true
- spring.jpa.properties.hibernate.use_sql_comments=true
- spring.jpa.properties.hibernate.format_sql=true
어플리케이션의 JPA테스트코드와 SQL 실행을 Trace한 예
쿼리실행을 JPA를 통해 간단하게 할수 있기때문에 , 데이터베이스를 조회하고 변경하는 코드에대해 JPA를 통해 작성된 인테페이스를 통해 어플리케이션에서 작동되는 실제 쿼리를 수행을 간단하게 할수가 있으며
어플리케이션 레벨에서 쿼리에관련된 TestCase 작성을 쉽게하고 또한 실제 작동되는 SQL문도 체크할수 있습니다.
이것은 유닛테스트 작성에 시간을 줄여, SQL/SP에는 도저히 코드변경점이 많아 시도해볼수 없는
많을것들을 할수 있게하며, 궁극적으로 어플리케이션이 DB 호출 최적화를 할수 있는 기회를 가질수가 있습니다.
대부분의 성능문제는 쿼리 자체 최적화문제가 아닌, 어플리케이션이 불필요하게 많은 쿼리를 호출하는데 발생합니다.
물론 실행계획까지 개발툴에 통합되어 포함되었으면 하는 바램도 있습니다.
실행계획 조사하기
번거롭지만, JPA를 학습하고 사용하는동안, JPA사용을 통해 단축된 개발시간을 이러한 습관을 가지는 시간을
확보하는것이 중요해보입니다.
JOIN에 따른 성능 측정
이것을 정리한 다시 이유는, JOIN및 조건절에 따라 드라이븐이 시작되는 위치가 달라질수도 있고
절차형 프로그래밍에서 이야기하는 루프의 형태가 달라질수 있다란 의미입니다.
소모적 반복적 SQL문 작성시간을 줄이고, 수많은 케이스를 유닛테스트로 전환될수 있을 가능성을 확인할수 있으며
이것은 데이터중심에서 메시지중심으로의 설계를 가능하게합니다.
실행계획 조사하기
Trace된 SQL문을 그대로 복사하여, 실행계획을 조사합니다. 매번 이러한 과정을 거칠필요는 없을듯보이며
SQL방식이던 JPA방식이던..., 성능에 의심이가는 부분에대해서는 개발방식과는 별개로 측정되고 분석되어야하는
사항입니다.
실행계획 콘솔모드
실행계획 시각화
참고:DB Management Tool에 따라서 실행계획 시각화기능을 제공하기도합니다.(MSSQL)
실행계획의 출력포맺은 DB마다 다르지만 공통적인 요소가 있습니다.
- 조작대상 객체
- 객체에 대한 조작의 종류
- 조작 대상이 되는 레코드 수
- 호출 계층
순차 풀스캔 VS 인덱스 스캔
엔티티의 설계
Code Block | ||||
---|---|---|---|---|
| ||||
@Entity
public class User {
private String name;
@ManyToOne
@JoinColumn(name = "GROUP_ID",nullable = true)
private GroupInfo groupInfo;
} |
ManyToOne은 JPA에서 관계도 형성을 하는 어노테이션으로서 실제 데이터베이스에서는
아래와같이 테이블생성시 외래키 설정및 인덱스 설정이 자동으로 수행되게 됩니다.
KEY `FKa36i4ekojwk70bxen390i6tek` (`group_id`) USING BTREE,
CONSTRAINT `FKa36i4ekojwk70bxen390i6tek` FOREIGN KEY (`group_id`) REFERENCES `group_info` (`group_id`)
순차풀스캔
JPA | SQL |
---|---|
findByName("Minsu") | SELECT * FROM user where name = 'minsu2'; |
Name필드에는 아무런 인덱스가 없기때문에, minsu2를 찾기위해 순차적으로
끝까지 조회를 해야합니다. (중복 사용자포함)
데이터량이 많아질시 탐색시간이 같이 선형적으로 증가합니다.
인덱스 스캔
JPA | SQL |
---|---|
findByGroupId(1) | SELECT * FROM user where group_id = 2; |
인덱스가 설정된 필드는, 이미 특정 탐색 알고리즘을 위해 데이터 분류가 되어 있습니다.
이경우 모두 순차 비교검사 할필요는 없이, 탐색 회수를 줄일수 있습니다.
적은량의 데이터에서 탐색시 효과가 없을수 있으나, 데이터량이 늘어남에따라 효과를볼수가 있습니다.
인덱스가 걸려있다고 항상 인덱스 스캔이 되는것은 아닙니다.
원하는 데이터를 뽑기위해 SQL문은 더 복잡해질수 있으며 실행계획은 DBMS가 세우며
옵티마이져 통계에의해 어떠한 테이블에서 풀스캔이 더 효율성이 좋다고 판단되면
인덱스를 무시하고 풀스캔이 진행될수도 있습니다. 이것은 실행계획을 조사하면
나오는 Tip으로 (Mysql은 Extra) 알수가 있습니다.
인덱스 최상위 Type은 페이지 물리적 재배열여부에따라 클러스터와 논클러스터로 나뉠수 있으며
동일 데이터라고 해도 스캔의 횟수가 달라질수 있습니다.
클러스터 인덱스 VS 논클러스터 인덱스
비교 | 클러스터 인덱스 | 논클러스터 인덱스 |
---|---|---|
차이 | 물리적으로 행을 재배열 | 물리적으로 재배열 하지않음 |
스캔 방식 | ||
페이지크기 | 작음 | 큼 |
선택도 |
|
|
페이지 논리갯수 | 테이블당1 | 테이블당 249 |
지정방법 (DB마다 다를수있음) |
정정: 테이블당 유니크한키로 한번만 지정가능합니다. PRIMARY KEY에 걸려버렸다고 치면...이후 다른키에 또 걸수 없습니다. ( 기본이냐 옵션이냐의 차이는 DB마다다름) | 명시해서 지정 |
키의이점 | 데이터 값의 범위가 크며, 키에따른 Order규칙이 키가 조인에 자주사용됨 | |
장단점 |
|
|
스캔 범위/방식에따른 실행계획 전략
성능을 위한 스캔 범위 Type은 3가지정도로 요약할수 대표적인 스캔 타잎은 3가지로 분류할수 있습니다.
- 이중루프(Nested Loops) : 한쪽테이블을 읽으면서 결합조건에 맞는 레코드를 다른쪽에서 모두 찾음
- Sort Merge : 결합할 두 테이블을 정렬을 하고 순차적으로 결합
- Hash : 결합 키값을 해시로 맵핑
이중루프중에도 양쪽풀스캔을(곱하기)하고 랜덤스캔을 하는 케이스가 성능적으로 가장 나쁜 케이스라고 보시면됩니다.
조건이 잘못되거나,필터(파티션)를 고려하지 않거나, 인덱스설정에따라 조인의 실행계획은 최악이 될수가 있습니다.
권한 이양의 죄악
이러한 실행계획 개입은, SQL문을 통해 할수있는게 아니고, RDB에 모든것을 이양해야하고
실제 작동을 시켜 실행계획을 파악해야합니다. 테이블 설계가 동일하다고 가정했을시
코드가 길어도 실행계획이 띄어난 SQL문 VS 간결한 SQL문, 두가지 SQL문이 있다고 하면
전자를 만들수 있는 SQL개발자가 더 뛰어난 개발자로 분류하였습니다.
실행계획이 띄어난 SQL문을 생성하기위해 RDB가 숨기고 있는 내부절차를 깊숙히 파악해야하며
몇가지 일반적으로 최악이 아닌 일반화는 있지만, 표준화가 되기 어려운내용입니다.
JPA의 경우도 이러한 권한을 받을수가 없습니다. 다만 최악의 실행계획을 수행하는 코드를 일반적으로 생성하지 않습니다.
대부분의 경우 JPA의 기본 인테페이스가 이해하는 SQL문이, 우리가 이해하는 SQL문보다 성능적으로 나쁘지 않는 SQL문을 만들어내며
테이블도 자동으로 생성해줄수 있습니다. 하지만 이것만으로 충분하지 않습니다.
JPA 는 SQL문이 하지못하는 몇가지 성능에 관련된 전략을 사용할수가 있습니다.
같은 쿼리라도 달라지는 RDB에의해 변경이되는 복잡한 실행계획에만 의존하지 않겠다는 전략입니다.
어느것이 항상빠르며,실행계획이 고정이된다라고 볼수 없으나
Sort Merge가 되는방식이 일반적으로 성능에서 안정적입니다.(편차가 크게 없이 빠름)
스캔범위및 실행 계획이 복잡해지지 않게 하기 위한 가장 간단한 규칙이지만,
우리가 원하는 집합을 만들다보면 이 규칙을 깨트리기 쉽고, 쿼리능력자에따라 차이가 날수있습니다.
- 스캔 범위 줄이기
- 조인을 덜 복잡하게 만들기
참고 : https://community.modeanalytics.com/sql/tutorial/sql-performance-tuning/
우리가 원하는 집합을 만들기위해, 2개의 테이블만 조작하여 JOIN문을 사용하면 좋겠지만
실제는 그렇지 않습니다.
JPA 튜닝 포인트
JPA에서는 실행계획이 틀어지고, 예측하기 어려운 튜닝포인트를 쿼리가 아닌
OOP중심, 즉 어플리케이션서 관여할수 있는 계층을 하나더 추가하였습니다.
JPA에서 이루어지는 성능에 관련된 기능을 간단하게 설명을하면...
No Format |
---|
User user1 = findByName("Minsu");
User user2 = findByName("Minsu");
user1.GetName()
user2.GetName() |
위와같은 코드는 일반적인 SQL/SP 호출 어플케이션에서는, 두번의 Select문을 DB에게 요청을 하였을것입니다.
JPA에서는 , 동일 트랙잰션에서 위와같이 동일처리라고 판단되는 사항에대해
SELECT SQL한번만 조회를하고, DB 호출없이 오브젝트 재사용전략을 택합니다.
DB에서는 상황에따라 옵티마이져가 불필요한 IO를 접근하지 않고, 메모리를 재활용할수 있는 상황입니다.
JPA 어플리케이션 레이아웃에서는, SQL호출 자체도 하지 않겠다는 의미입니다.
SQL호출을 하지 않는다란 말은 다음과정이 내포되어있기때문에 호출횟수에따라 아주 큰 비용이 될수도 있습니다.
SQL문요청 → 네트워크 전송 → DB엔진이 구문분석 → IO Read → 네트워크로 결과 반환
JPA의 이러한 캐시처리같은 메카니즘이 사용 중복 호출실수를 잡으려고 이렇게 작동되는것은 아니며
어플케이션 레이아웃에서 관여할수있는 성능에 관련된 몇가지 JPA컨셉을 추가로 학습을 해야합니다.
매번 SQL문을 호출하고 즉시 답을 얻는 방식과 달리, 아래와같은 몇가지 컨셉이 존재합니다.
Read전략Read전략:
- 즉시읽기( Eager loading) :
- 지연읽기 ( Lazy loading ) :
영속성 전이 전략
...
- ALL : 부모의 변화가 자식에게 모두 전가
- PERSIST : 부모의 영속화 될때 자식도 영속화가됨
- MERGE : 트랜젝션이 종료되고 변경사항이 merge()수행시 변경사항적용
- REMOVE : 부모삭제시 연관된 자식도 삭제
- REFRESH : 부모가 변경되면 자식도 변경
- DETACH : 부모가 DETACH되면, 연관된 ENTITY도 DEATCH되어 변경반영 죽시 안됨
- orphanRemoval : 연관관계가 끊어진 자식을 자동으로 제거
JPA는 객체지향과 데이터모델링 사이에 간극을 최소화하고 편리한 방식임에는 분명하지만
개발 난이도를 줄였다라고 판단할수는 없을듯 합니다.
오히려 SQL/SP 중심적으로 개발했을때는 신경쓰지 못한 부분에대해
더 깊이 RDB를 이해하고 JPA의 추가적인 영속성 특성을같이 잘 활용해야 사용측면에서도
성능 측면에서도 실무에 적용이 될것으로 기대해봅니다.
이 부분은 개념과함께 구체적인 사용사례가 필요한 부분이며, 샘플을 좀더 준비할예정입니다.
Expand | ||
---|---|---|
| ||
|
위 전략은 모두, 어플리케이션에서 DB와 연관된 불필요한 Read/Write등을 줄이고 그것을 어플리케이션에
반영할수 있는 기회를 제공해줍니다. 이것은 케이스별로 사용해야할 전략이 다르기 때문에
...
|