반응형

인덱스 ROWID = 논리적 주소

DBA = 데이터파일번호 + 블록번호 : 디스크 상에서 블록을 찾기 위한 주소 정보

버퍼캐시 : I/O 성능을 높이기 위해 사용된다

 

인덱스를 이용해 테이블 블록을 찾아가는 과정

 

인덱스를 통해 찾은 ROWID를 통해 데이터 가져올 때 버퍼캐시에서 데이터를 찾기 위해 DBA(데이터파일번호 + 블록번호)를 해시 함수에 입력하여 해시 체인을 찾은 후 해시 체인에서 버퍼 헤더를 찾는다

버퍼 헤더는 메모리의 주소값을 가지고 있기 때문에 그 포인터를 이용해 버퍼 블록을 찾는다

 

인덱스로 테이블 블록을 엑세스할 때는 리프 블록에서 읽은 ROWID를 분해하여 DBA 정보를 얻는다

테이블 Full Scan을 할 때는 익스텐트 맵을 통해 읽을 블록들의 DBA 정보를 얻는다

 

ROWID가 가리키는 테이블 블록을 버퍼캐시에서 먼저 찾고, 못 찾으면 디스크에서 블록을 읽어 버퍼캐시에 적재한 후 읽는다

 

인덱스 클러스터링 팩터(CF)

특정 컬럼을 기준으로 같은 값을 갖는 데이터가 서로 모여있는 정보

CF가 좋은 컬럼에 생성한 인덱스는 검색 효율이 매우 좋다

 

인덱스 클러스터링 팩터가 가장 좋은 상태

 

인덱스 클러스터링 팩터가 가장 안 좋은 상태

 

CF가 좋은 컬럼에 생성한 인덱스가 검색 효율이 좋은 이유

인덱스 ROWID로 테이블을 액세스할 때, 래치 획득과 해시 체인 스캔 과정을 거쳐 어렵게 찾아간 테이블 블록에 대한 포인터를 바로 해제하지 않고 일단 유지하는데, 이를 버퍼 Pinning이라고 한다

이를 통해 다음 인덱스 레코드를 읽을 때 같은 테이블 블록인 경우 래치 획득과 해시 체인 스캔 과정을 생략하고 바로 테이블 블록을 읽을 수 있어 논리적인 블록 I/O 과정을 생략할 수 있기 땜누에 블록 I/O가 적게 발생한다

 

인덱스 손익 분기점

Index Range Scan에 의한 테이블 액세스가 Table Full Scan보다 느려지는 지점

 

인덱스를 이용한 테이블 액세스가 Table Full Scan 보다 더 느려지게 만드는 가장 핵심적인 두 가지 요인

  • Table Full Scan은 시퀀셜 액세스이나 인덱스 ROWID를 이용한 테이블 액세스는 랜덤 액세스 방식
  • Table Full Scan은 Multiblock I/O인 반면 인덱스 ROWID를 이용한 테이블 액세스는 SingleBlock I/O 방식

인덱스 손익 분기점은 보통 5 ~ 20%의 낮은 수준에서 결정된다(CF에 따라 크게 달라진다)

 

온라인 프로그램 : 소량 데이터를 읽고 갱신, 인덱스를 효과적으로 활용하는 것이 중요

배치 프로그램 튜닝 : 항상 전체범위 처리 기준으로 튜닝 필요, 처리대상 집합 중 일부를 빠르게 처리하는 것이 아닌 전체를 빠르게 처리하는 것을 목표로 한다

 

Covered 쿼리 : 인덱스만 읽어서 처리하는 쿼리

이때 사용하는 인덱스를 Covered 인덱스라고 부른다

 

Include 인덱스

 

include로 포함된 컬럼은 리프 블록에만 저장한다

따라서 수직탐색에는 사용할 수 없다

다만, 수평탐색에서는 필터조건으로 사용할 수 있다(테이블 랜덤 액세스 횟수 줄이는데 사용 가능)

위에 두 인덱스가 있을 때 인덱스 스캔량은 emp_x02가 더 적다, sal 도 액세스 조건으로 사용하기 때문

emp_x02는 소트 연산 생략이 가능하지만 emo_x01은 불가능하다

include 인덱스는 랜덤 액세스를 줄이는 용도로 개발되었다

 

인덱스 구조 테이블(오라클에서는 IOT(Index-Organized Table), MS 는 클러스터형 인덱스)

테이블을 인덱스 구조로 만드는 구문

 

일반 테이블은 heap 구조

힙구조는 데이터를 입력할 때 랜덤 방식을 사용(순서 없음), IOT 구조는 인덱스 구조 테이블이므로 정렬 상태를 유지(인위적으로 클러스터 팩터를 좋게 만드는 방법)

 

클러스터 테이블

인덱스 클러스터 테이블

클러스터 키 값이 같은 레코드를 한 블록에 모아서 저장하는 구조, 한 블록에 다 담을 수 없을 때는 새로운 블록을 할당해서 클러스터 체인으로 연결

여러 테이블 레코드를 같은 블록에 저장할 수 있는데, 이런 경우는 “다중 테이블 클러스터” 라고 부른다

일반 테이블은 하나의 데이터 블록을 여러 테이블이 공유할 수 없음

 

인덱스 클러스터 테이블 구성

1. 클러스터 생성

 

2. 인덱스 생성

 

3. 클러스터 테이블 생성

 

클러스터 인덱스도 B*Tree 인덱스 구조이지만 테이블 레코드를 일일이 가리키지 않고 해당 키 값을 저장하는 첫 번째 데이터 블록을 가리킨다

일반 테이블 인덱스 레코드는 테이블 레코드와 1:1 이지만 클러스터 인덱스는 테이블 레코드와 1:M 관계를 갖는다

클러스터 인덱스의 키 값은 항상 Unique 하다

 

해시 클러스터 테이블

인덱스를 사용하지 않고 해시 알고리즘을 사용해 클러스터를 찾아간다

클러스터 생성 시 아래와 같이 hashkeys ‘숫자’ 를 입력하여 생성한다

 

부분 범위 처리

전체 쿼리 결과집합을 쉼 없이 연속적으로 전송하지 않고 사용자로부터 Fetch Call이 있을 때마다 일정량씩 나누어 전송

 

정렬 조건이 있을 때 부분 범위 처리

정렬이 있는 경우 모든 데이터를 읽어 정렬을 마치고 나서야 전송을 시작할 수 있기 때문에 전체 범위 처리가 된다

단, 정렬절에 있는 컬럼이 선두인 인덱스가 존재한다면 부분 범위 처리가 가능하다.(인덱스가 정렬되어있기 때문에 정렬을 하지 않아도 되므로 가능)

 

모든 DBMS는 데이터를 조금씩 나누어 전송한다. 다만 부분 범위 처리를 프로그램에 적용하고 안하고는 개발자의 목이다

 

부분 범위 처리가 의미가 있기 위해서는 멈출 수 있어야 한다

DB에 직접 접속하는 2-Tier 환경에서는 구현이 가능하지만, 클라이언트와 DB 사이에 WAS, AP 서버 등 이 존재하는 n-Tier 환경에서는 클라이언트가 특정 DB 커넥션을 독점할 수 없기 때문에 SQL 조회 결과를 클라이언트에게 모두 전송하고 커서를 닫아야 한다

 

배치 I/O

읽는 블록마다 건건이 I/O Call을 발생시키는 것이 아닌 테이블 블록에 대한 디스크 I/O Call을 미루었다가 읽을 블록이 일정량 쌓이면 한꺼번에 처리하는 방식

* 데이터 순서를 보장할 수 없다

반응형
반응형

데이터를 찾는 방법은 두가지가 존재한다.

  1. 테이블 전체 스캔
  2. 인덱스 이용

인덱스 튜닝의 두 가지 핵심 요소

  1. 인덱스 스캔 효율화 튜닝
    - 인덱스 스캔 과정에서 발생하는 비효율을 줄이는 것
  2. 랜덤 액세스 최소화 튜닝⭐️
    - 테이블 액세스 횟수를 줄이는 것

SQL 튜닝은 랜덤 1/0와의 전쟁

데이터베이스 성능이 느린 이유는 디스크 I/O 때문!

 

OLTP(OnLine Transaction Processing)에서는 디스크 I/O 중에서도 랜덤 I/O가 특히 중요

 

인덱스는 정렬되어있기 때문에 범위 스캔(Range Scan) 가능 하다

DBMS 의 인덱스는 일반적으로 B*Tree(Balanced Tree) 인덱스를 사용한다

 

인덱스 내에는 가장 왼쪽 첫 번째 레코드를 가리키는 LMC(LeftMost Child) 레코드가 존재한다

LMC가 가리키는 주소의 블록에는 키 값을 가진 첫 번째 레코드 보다 작거나 같은 레코드가 저장되어있다

리프 블록에 저장된 각 레코드는 키 값 순으로 정렬돼 있을 뿐 아니라 테이블 레코드를 가리키는 주소값(ROWID)을 갖는다

ROWID = 데이터 블록 주소 + 로우 번호

ROWID를 알면 테이블 레코드를 찾아갈 수 있다

 

ROWID = 데이터 블록 주소 + 로우 번호

데이터 블록 주소 = 데이터 파일 번호 + 블록 번호

블록 번호 = 데이터파일 내에서 부여한 상대적 순번

로우 번호 = 블록 내 순번

 

인덱스 탐색 : 인덱스 탐색은 두 가지로 나뉜다

  1. 수직적 탐색
    - 인덱스 스캔 시작지점을 찾는 과정
    - 수직적 탐색 과정에서 찾고자 하는 값보다 크거나 같은 값을 만나면, 바로 직전 레코드가 가리키는 하위 블록으로 이동 한다
  2. 수평적 탐색
    - 데이터를 찾는 과정
    - 스캔 시작점으로 부터 찾고자 하는 데이터가 더 안 나타날 때까지 인텍스 리프 블록을 수평적으로 스캔

결합 인덱스 : 두 개 이상의 컬럼을 결합해서 만든 인덱스

 

인덱스는 B*Tree(Balanced Tree, 루트에서 리프 블록까지 읽는 블록 수 가 같음) 이므로 컬럼 순서에 따라 일량의 차이가 나지는 않는다

 

인덱스의 기본 사용 방법은 인덱스를 Range Scan 하는 방법을 의미

  • 인덱스 컬럼을 가공하지 않아야 인덱스를 정상적으로 사용(Range Scan)할 수 있다
  • Like를 이용한 중간 값 검색을 할 때도 Range Scan할 수 없다
  • OR 으로 검색하는 경우 Range Scan 불가능 → Or Expansion을 사용하면 Range Scan 가능(use_concat 힌트) or 를 union all sql로 변환 하여 실행하기 때문

* 이유: 인덱스 스캔 시작점을 찾을 수 없기 때문!

 

IN 조건절의 경우 OR 조건을 표현하는 다른 방식으로 SQL 옵티마이저가 IN 개수 만큼 Index Range Scan을 반복한다.(IN-List Iterator)

 

인덱스를 정상적으로 사용한다 = 리프 블록에서 스캔 시작점을 찾아서 스캔하다가 중간에 멈추는 것을 의미

인덱스 Range Scan을 위한 첫 번째 조건 : 선두 컬럼이 가공되지 않은 상태로 조건절에 있어야 한다

 

인덱스는 데이터가 정렬되어있기 때문에 인덱스를 이용하여 소팅 연산을 생략하는 효과를 볼 수 있다

 

ORDER BY 의 정렬 요소를 가공하면 정렬 연산을 생략할 수 없다

 

ORDER BY 에 인덱스 요소를 가공하여 사용하였기 때문에 별도 정렬 연산이 발생한다

 

ORDER BY 에 인덱스인 주문번호가 들어간 것 같지만 실제로는 TO_CHAR로 변환된 주문번호가 들어가기 때문에 소팅 연산이 발생 한다 이 경우 테이블 alias 인 A를 ORDER BY 주문번호에 붙여주면 정렬 연산을 생략할 수 있다

 

인덱스는 정렬되어 있기 때문에 인덱스 구성요소에 대한 MIN ,MAX를 구하는 경우 인덱스 리프블록의 왼쪽(MIN), 오른쪽(MAX)에서 레코드 하나(FIRST ROW)만 읽고 멈춘다

 

가공 후 MIN, MAX를 사용하는 경우에 대해서는 정상적으로 동작하지 못한다

 

스칼라 서브쿼리의 수 만큼 테이블을 여러번 읽어야하기 때문에 비효율적이고 SQL도 복잡해진다

 

인덱스를 사용하기 위해 조건절을 지정하는 경우 자동 형변환을 조심해야한다

 

이 경우 인덱스를 정상적으로 사용하지 못하는데, 그 이유는 생년월일 데이터가 TO_NUMBER 로 자동 형변환이 일어났고 이에 따라서 인덱스 컬럼이 가공되어 인덱스를 정상적으로 사용하지 못한 것이다

 

자동 형변환

문자 < 숫자

문자 < 날짜

LIKE를 사용하는 경에는 문자형 기준으로 숫자형 컬럼이 변환

 

자동 형변화 시 주의사항

숫자형 컬럼과 문자형 컬럼을 비교하면 문자형 컬럼이 숫자로 변환된다

이때 문자형이 숫자로 변환이 불가능하는 경우에는 에러가 발생한다

 

Index Range Scan

B*Tree 인덱스의 가장 일반적이고 정상적인 형태의 액세스 방식

인덱스 루트에서 리프 블록까지 수직적 탐색 후 필요한 범위만 스캔

* 인덱스의 선두 컬럼을 가공하지 않은 상태로 조건절에 사용!

 

Index Full Scan

수직적 탐색 인덱스 리프 블록을 처음부터 끝까지 수평적으로 탐색하는 방식

데이터 검색을 위한 최적의 인덱스가 없을 때 차선으로 선택된다

인덱스 선두 컬럼이 조건절에 없는 상황에서 조회 대상 테이블이 대용량 테이블인 경우 Full Scan에 따른 부담이 크다면 옵티마이저가 Index Full Scan을 고려한다

 

Index Full Scan을 하면 Range Scan과 마친가지고 결과 집합이 인덱스 컬럼 순으로 정렬되어있다. 따라서 Sort 연산을 생략할 목적으로 사용할 수 있다

 

Index Full Scan을 사용할 때 아래와 같이 대부분의 레코드에 대해 테이블 엑세스가 발생하는 경우 Full Scan 보다 오히려 불리할 수 있다 다만 아래의 경우는 결과 집합의 일부만 빠르게 출력할 목적으로 first_rows 힌트를 사용했기 때문에 Index Full Scan을 사용했다

* 부분범위 처리가 가능한 상황에서는 극적인 성능 개선 효과를 가져다 줄 수 있다

 

Index Unique Scan

수직적 탐색만으로 데이터를 찾는 스캔 방식으로 Unique 인덱스를 = 조건으로 탐색하는 경우 동작(인덱스에 포함된 모든 컬럼에 대해 조건절에 = 조건으로 검색할 때)

 

Index Skip Scan

인덱스 선두 컬럼이 조건절에 없어도 사용할 수 있는 방법(9i에 등장) 조건절에 빠진 인덱스 선두 컬럼의 Distinct Value 개수가 적고 후행 컬럼의 Distinct Value 개수가 많은 때 유용 Distinct Value가 적은 두 개의 선두컬럼이 모두 조건절에 없는 경우, 중간 컬럼이 없는 경우도 사용 가능

 

Index Fast Full Scan

논리적인 인덱스 트리 구조를 무시하고 인덱스 세그먼트 전체를 Multiblock I/O 방식으로 스캔하는 방법(물리적으로 디스크에 저장된 순서대로 인덱스 리프 블록들을 읽는다.)

- 결과 집합이 정렬되지 않음.

- 쿼리에 사용한 컬럼이 모두 인덱스에 포함되어있을 때만 사용 가능

- 인덱스가 파티션 돼 있지 않아도 병렬 쿼리가 가능

병렬 쿼리 시 에는 Direct Path I/O 방식 사용으로 I/O 속도가 더 빨라진다

 

Index Range Scan Descending

Index Range Scan과 동일 방식으로 스캔하나 인덱스 뒤에서 부터 앞쪽으로 스캔하기 때문에 결과 집합이 내림차 순으로 된 결과를 얻을 수 있다.(index_desc 힌트로 유도 가능)

반응형
반응형

SQL은 Structed Query Language : SQL은 구조적, 집합적, 선언적 질의어

 

DBMS 내부에서 프로시저를 작성하고 컴파일해서 실행 하능한 상태로 만드는 전 과정 = SQL 최적화

 

SQL 최적화

  1. SQL 파싱 (SQL 파서가 수행)
    1) 파싱 트리 생성 : SQL을 개별 구성 요소로 분석 및 파싱 트리 생성
    2) Syntax 체크 : 문법적 오류 확인 : 사용할 수 없는 키워드, 순서, 누락된 키워드 확인
    3) Semantic 체크 : 의미상 오류 없는지 확인. 오브젝트 권한, 존재하지 않는 테이블 또는 컬럼 사용 여부 확인
  2. SQL 최적화 (SQL 옵티마이저가 수행)
    - 미리 수집된 시스템 및 오브젝트 통계정보를 바탕으로 실행 경로 생성 및 선택(1개)
  3. 로우 소스 생성 (로우 소스 생성기가 수행)
    - 실행 경로를 바탕으로 실행 가능한 코드 및 프로시저 형태로 포맷팅

SQL 옵티마이저 : 가장 효율적으로 수행할 수 있는 최적의 데이터 엑세스 경로를 선택해주는 DBMS 핵심 엔진

1. 후보군이 될 만한 실행계획들을 찾아낸다

2. 데이터 딕셔너리에 미리 수집해 둔 오브젝트 통계 및 시스템 통계정보를 이용해 각 실행계획의 예상 비용 산정

3. 최저 비용을 나타내는 실행계획 선택

 

서버 프로세스는 클라이언트와 서버와의 통신을 위해 존재하는 프로세스로 커넥션 당 1개씩 생성 된다. 백그라운드 프로세스 는 DBWR, LGWR, PMON, SMON 같이 공통 기능을 위한 프로세스 이다

 

비용(Cost) : 쿼리를 수행하는 동안 발생할 것으로 예상하는 I/O 횟수 또는 예상 소요시간을 표현한 값

 

* 실측치가 아니므로 실제 수행할 때 발생하는 I/O 또는 시간과 많은 차이가 난다

 

옵티마이저 힌트 (지시 강제임) : 옵티마이저가 정확하지 않으므로 힌트를 통해 효율적인 액세스 경로를 선택할 수 있다
1. /*+ 힌트 */ 추천
2. —+ 힌트 비추천

 

주의 사항

1. 힌트 사이에 , 사용하면 , 앞에 까지만 적용 됨

2. alias 사용한 경우 alias로 힌트를 사용해야 적용 됨

 

힌트를 쓸꺼면 빈틈없이 사용!

 

System Global Area(SGA)

- DB Buffer Cache

- Redo Log Buffer

- Shared Pool
   - Library Cache
   - Data Dictionary Cache

 

소프트 파싱 : SQL을 캐시(Library Cache)에서 찾아 곧바로 실행단계로 넘어 가는 것

 

하드 파싱 : 캐시(Library Cache)에서 찾는 것을 실패하여 최적화 및 로우 소스 생성 단계 까지 모두 거치는 것

하드 파싱할 때는 많은 연산이 발생

 

파싱 기준 정보

테이블 컬럼 인덱스 구조

오브젝트 통계

시스템 통계

옵티마이저 관련 파라미터

 

사용자 정의 함수/프로시저, 트리거, 패키지 등은 생성할 때 이름을 갖고 컴파일 상태로 딕셔너리에 저장 되어 영구 보관 된다

 

SQL은 이름이 따로 없고 전체 SQL 텍스트가 이름 역할을 한다

처음 실행할 때 최적화 과정을 거쳐 생성한 내부 프로시저를 라이브러리 캐시에 적재하여 여러 사용자가 공유하며 재사용, 캐시 공간이 부족하면 버려졌다가 다음 실행 때 최적화 과정을 거쳐 캐시에 적재

 

SQL 텍스트가 변하면 SQL ID도 변하고 새로운 SQL로 인식하여 최적화 과정을 수행 및 캐시에 적재 한다

 

실행할 때마다 프로시저를 생성하면 비효율적,

파라미터를 받는 프로시저를 공유하면서 재사용 하는 파라미터 Driven 방식으로 바인드 변수 사용 ⇒ 하드 파싱 한번만 일어나고, 캐싱된 SQL을 공유 재사용

 

SQL이 느린 이유는 디스크 I/O 때문

 

데이터베이스 저장 구조(중요)

  • 데이터 파일 : 디스크상 물리적인 OS파일
  • 테이블 스페이스 : 세그먼트를 담는 컨테이너
    • 세그먼트(테이블, 인덱스, 파티션, LOB) : 데이터 저장공간이 필요한 오브젝트(물리적인 공간을 점유하는 오브젝트) 세그먼트는 익스텐트 맵을 세그먼트 헤더에 관리
      • 익스텐트 : 공간을 확장하는 단위, 연속된 블록 집합
        • 블록 : 데이터를 읽고 쓰는 단위
          • 로우

세그먼트 공간이 부족해지면 테이블스페이스로부터 익스텐트를 추가로 할당받는다. 단, 세그먼트에 할당된 모든 익스텐트가 같은 데이터파일에 위치하지 않을 수 있다

  • 익스텐트 내 블록은 서로 인접한 연속된 공간
  • 익스텐트 끼리는 연속된 공간이 아닐 수 있다

DBA (Data Block Address)

인덱스를 이용해 테이블 레코드를 읽을 때 인덱스 ROWID를 이용하는데, ROWID는 DBA + 로우 번호(블록 내 순번) 으로 구성되어있다

테이블을 스캔할 때는 테이블 세그먼트 헤더에 저장된 익스텐트 맵을 이용한다. 익스텐트 맵을 통해 각 익스텐트의 첫 번째 블록 DBA를 알 수 있다

 

블록 : 데이터를 읽고 쓰는 단위

테이블 뿐 아니라 인덱스도 블록 단위로 데이터를 읽고 쓴다

테이블 또는 인덱스 블록을 액세스(읽는) 방식

  • 시퀀셜 액세스 : 논리적 또는 물리적으로 연결된 순서에 따라 차례로 블록을 읽는 방식 리프 블록은 앞뒤를 가리키는 주소값을 통해 논리적으로 서로 연결되어 있다. 이 주소 값에 따라 앞 또는 뒤로 순차적으로 스캔하는 방식
  • 랜던 액세스 : 논리적, 물리적 순서를 따르지 않고, 레코드 하나를 읽기 위해 한 블록씩 접근 하는 방식

논리적 I/O : SQL 문을 처리하는 과정에서 메모리 버퍼캐시에서 발생한 총 블록 I/O Direct I/O가 발생할 수 있어 논리적 I/O가 메모리 I/O가 정확히 같은 의미는 아니지만, 일반적으로 같다

물리적 I/O : 디스크에서 발생한 총 블록 I/O

물리적 I/O = 논리적 I/O * (100 * BCHR)

 

SQL 트레이스에서

Query + Current = DB 버퍼 캐시에서 읽은 총 블록 개수 (Disk 포함됨)

Disk = 물리적으로 읽은 블록 수

 

블록을 읽을 때는 해당 블록을 먼저 버퍼 캐시에서 찾아보고 없을 때만 디스크에서 읽는다. 이때 디스크에서 곧 바로 읽는 것이 아닌 버퍼 캐시에 먼저 적재 후 읽는다

 

BCHR = (1 - (Disk / (Query + Current))) * 100

 

Single Block I/O

- 한 번에 한 블록씩 요청해서 메모리에 적재하는 방식

- 인덱스를 이용할 때는 인덱스와 테이블 블록 모두 Single Block I/O로 동작

  • 인덱스 루트 블록을 읽을 때
  • 인덱스 루트 블록에서 얻은 주소로 브랜치 블록을 읽을 때
  • 인덱스 브랜치 블록에서 얻은 주소로 리프 블록을 읽을 때
  • 인덱스 리프 블록에서 읽은 주소로 테이블 블록을 읽을 때

Multiblock I/O

- 한 번에 여러 블록씩 요청해서 메모리에 적재하는 방식

- 디스크 상에 그 블록과 인접한 블록들을 한꺼번에 읽어 캐시에 미리 적재 (인접한 블록은 익스텐트에 속한 블록) : 익스텐트의 결계를 넘지 못한다

index ffs, full

multiblock readcount에 의해 몇 개를 읽어올지 결정 된다

 

캐시 탐색 메커니즘

Direct Path I/O를 제외한 모든 블록 I/O는 메모리 버퍼 캐시를 경유한다

  • 인덱스 루트 블록을 읽을 때
  • 인덱스 루트 블록에서 얻은 주소로 브랜치 블록을 읽을 때
  • 인덱스 브랜치 블록에서 얻은 주소로 리프 블록을 읽을 때
  • 인덱스 리프 블록에서 읽은 주소로 테이블 블록을 읽을 때
  • 테이블 블록을 Full Scan 할 때

메모리 공유자원에 대한 액세스 직렬화

SGA는 공유 자원으로 여러 프로세스가 접근 가능하므로 두 개 이상 프로세스가 동시에 접근하려고 하면 블록 경합이 발생할 수 있다

따라서 직렬화 메커니즘을 통해 이를 해결한다(래치)

SGA를 구성하는 서브 캐시마다 별도이 래치가 존재 버퍼캐시에는 캐시버퍼 체인 래치, 캐시버퍼 LRU 체인 래치 등이 작동

 

버퍼 Lock : 오라클은 캐시버퍼 체인 래치를 해제하기 전에 먼저 버퍼 헤더에 버퍼 Lock을 설정함으로 써 버퍼 블록 자체에 대한 직렬화 문제 해결

반응형
반응형

서비스를 개발 하거나 운영하면서 OOM(Out Of Memory)을 만나는 경우가 있다.

이런 경우 단편적 해결 방법으로는 서비스를 재기동하는 방법이 있겠다.

 

하지만 우리는 Out Of Memory가 왜 발생했는지 알고 원인을 해결헤야 하는데,

이 과정에서 heap dump를 분석할 필요가 있다.

 

heap dump란?

 

우선 heap에 대해 알아야하는데, heap은 JVM에서 관리하는 메모리 공간으로 Object들이 저장되는 공간이다.

primitive type의 경우 stack area에 저장되나 객체의 경우는 stack에는 참조를 저장하고 실제 Object는 heap에 저장되게 된다.

이러한 부분을 이해한 상태에서 대량의 객체가 생성된다면, 그 객체는 heap에 생성이 된다.

 

하지만 heap은 한정된 자원으로 계속해서 객체가 생기면 더 이상 생성할 공간이 부족해지게 되고, OOM(Out Of Memory) Error가 발생하게 된다.

이때, heap의 상태를 snapshot으로 저장된 것이 heap dump파일이다.

Heap Dump 테스트

Heapdump 테스트를 위해 Intellij의 JVM heap을 임의로 설정 후 객체를 무한정 생성해 본다

 

1. 프로젝트의 Run/Debug Configuration 에서 JVM Option으로 최소, 최대 힙사이즈를 1Gb로 부여하고 heap dump를 생성하는 옵션과 heap dump path를 지정해준다

* OOM 발생 시 heap dump 파일을 생성하기 위해서는 "-XX:+HeapDumpOnOutOfMemoryError" JVM 옵션을 꼭 지정해줘야 한다

 

2. 테스트 코드 작성

import java.util.ArrayList;
import java.util.List;

public class OomTest {

    public static void main(String[] args) {

        List<DummyDto> dummyDtoList = new ArrayList<>();
        long num = 0;

        while (true) {
            num++;
            dummyDtoList.add(new DummyDto("더미에요 " + num));
        }
    }

    static class DummyDto {
        private String text;

        public DummyDto(String text) {
            this.text = text;
        }
    }
}

 

3. 위 코드를 수행해보면 아래처럼 OOM이 발생하고 프로그램이 종료된다

그리고 힘 덤프 파일을 log 디렉토리에서 **.hprof로 얻을 수 있다

 

Heap Dump 분석

heap dump를 분석기 위해서는 여러 분석 프로그램을 이용할 수 있는데,

MAT을 사용하여 분석하는 방법을 다룬다

 

MAT (Memory Analzer)

설치 : https://eclipse.dev/mat/

 

MAT을 다운 받고 heapdump 파일을 열면 Overview를 볼 수 있다

 

Overview에서 Leak Suspects로 이동하면 대략적인 OOM의 원인을 알 수 있다

이 건은 테스트 이기 때문에 전체 용량의 대부분을 Object[] 가 차지하는 것을 알 수 있지만 실제 운영 중인 시스템에서는 그 비율 다를 수 있다

 

Leak Suspects로만 정확하게 알 수 없을 수 있다

그런 경우 다른 정보를 참고하여 문제를 일으키는 친구를 찾아내어야한다

아래는 dominator tree인데 해당 데이터를 보면 Retained Heap를 보면 많이 차지하고 있는 포인트를 유추할 수 있고 해당 포인트의 계층구조를 타고 들어가면 우리가 찾던 문제의 원인이 되는 객체를 발견할 수 있고 해당 객체의 어떤 값이 들어있는지도 확인할 수 있다

 

추가로 stacktrace를 확인하면 어떤 로직을 수행하는 도중에 문제가 발생했는지 확인할 수 있다

원인을 찾았으니 해당 객체를 발생시키는 부분의 코드를 수정해주면 된다!

 

OOM을 발생시키는 원인

  • JVM 메모리 할당 문제
  • 많은 수의 동시 요청
  • 비효율적인 데이터 조회

보통 데이터 조회 시 조건을 잘못지정하거나 페이징을 적용하지 않는 등 데이터를 과하게 가져오게 되면서 발생하는 경우가 대부분이기 때문에 개발할 때 이 부분을 신경쓰는 것이 중요하다

반응형

'공부 > Java' 카테고리의 다른 글

getParameter, getReader 값 못 읽는 문제  (0) 2025.06.25
Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
[Java] HashMap get 메서드에 관하여  (0) 2021.09.13
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
반응형

Dependency Inversion Principle은 의존성 역전의 원칙이라고 한다.

해당 부분은 이해하기 어려울 수 도 있는데,

고수준의 모듈이 저수준의 모듈에 의존하지 않도록 인터페이스나 추상클래스에 의존하도록 하는 것을 말한다.

우선 Dependency Inversion Principle을 준수하지 않은 경우에 대한 예제를 보자

public class Led {
	public void on() {
		System.out.println("LED on");
	}

	public void off() {
		System.out.println("LED off");
	}
}

public class Switch {
	private Led led;
	private boolean isOn;
	
	public Switch(Led led) {
		this.led = led;
		this.isOn = false;
	}

	public void operate() {
		if (isOn) {
			led.off();
		} else {
			led.on();
		}
	}
}

위 코드의 Switch는 Led 타입의 변수를 가지고 있다.

이는 Switch 클래스가 Led에 의존성을 가진다고 볼 수 있다.

이 경우 Switch클래스의 전구는 Led외에는 사용할 수 없게 된다는 문제가 발생 한다.

 

이번에는 Dependency Inversion Principle을 준수하는 예를 보자

interface Light {
	void on();
	void off();
}

public class Led implements Light {
	public void on() {
		System.out.println("LED on");
	}

	public void off() {
		System.out.println("LED off");
	}
}

public class Switch {
	private Light light;
	private boolean isOn;
	
	public Switch(Light light) {
		this.light = light;
		this.isOn = false;
	}

	public void operate() {
		if (isOn) {
			light.off();
		} else {
			light.on();
		}
	}
}

위 경우는 Switch가 하위 클래스에 대한 의존성을 갖는 것이 아닌 인터페이스에 대한 의존성을 갖고 있다.

이 이야기는 인터페이스를 구현하는 하위 클래스를 사용할 수 있다는 이야기가 되며, 이를 통해 하나의 타입에 대한 의존성 문제를 해결할 수 있다.

즉, Dependency Inversion Principle은 하나의 클래스에 대해 의존성이 발생하지 않도록 하는 원칙이다.

반응형
반응형

Spring의 핵심 요소

- 제어 역전(IoC, Inversion of Control)

- 관점 지향 프로그래밍(AOP, Aspect Oriented Programming)

- 서비스 추상화(PSA, Portable Service Abstraction)

라고 볼 수 있다.

 

오늘 볼 것은 IoC, 제어의 역전을 위한 DI(Dependency Injection)에 대해서 알아보고자 한다.

참고로 DI는 스프링에만 있는 것이 아니다. DI는 디자인 패턴 중 하나이다.

DI, 의존성 주입은 무엇일까, DI에 대해 이야기하기 전에 먼저 의존성이라는 것에 대해 알아야한다.

의존성이랑 A객체 내 B객체가 사용될 때 A는 B에 대해 의존성을 가진다고 한다. 이유인 즉슨 B가 바뀌게 되거나 다른 객체를 사용해야할 때 그 영향을 A도 받기 때문이다. 이런 의존성은 개발 또는 유지보수를 힘들게 한다.

따라서 우리는 결합도는 낮고 응집도는 높게 개발할 필요가  있다.

 

그러면 어떻게 의존성을 줄일 수 있을까, 그것은 객체를 직접 만들지 않고 외부에서 만들어서 객체를 넣어주는 방식을 사용하면 된다. 이런 것을 우리는 DI라고 부른다.

 

그러면 DI의 방법은 무엇이 있을까

- 생성자 주입

- Setter 주입

 

생성자 주입의 예

B b = new B();
A a = new A(b);

a를 생성하기 전 객체 b를 생성하여 a를 생성할 때 b를 생성자를 통해 주입해준다.

 

Setter 주입의 예

B b = new B();
A a = new A();
a.setB(b);

이번에는 객체 b를 생성하고 객체 a를 생성한 후 a의 setB메서드를 통해 객체 b를 주입해준다.

 

이렇게 되면 우리가 주입한 객체에 따라 a가 동작하게 되고 a는 b가 다른 객체가 주입되면 다른 객체에 맞게 동작할 수 있다.

 

그런데 어떻게 다른 객체를 주입할 수 있을까

그것은 인터페이스를 이용하면 된다.

우리가 a 내에 b를 주입 받을 필드를 B클래스의 레퍼런스 변수를 지정하면 우리는 B밖에 주입할 수 없다.

그러나 B 대신 B가 구현하고 있는 인터페이스 SuperB를 사용해서 레퍼런스 변수를 만든다면 우리는 SuperB를 구현하고 있는 객체를 주입할 수 있게 된다.

 

SuperB를 구현한 Class인 B와 BB 그리고 BBB가 있을 때 우리는 a에 B, BB, BBB를 주입할 수 있다.

이렇게 되면 우리의 프로그램이 필요한 객체를 SuperB인터페이스에 맞게 구현해서 주입한다면, A 클래스를 수정하지 않아도 주입된 객체에 따라 A의 객체를 동작을 변경할 수 있게 된다.

 

JAVA를 처음 배울 때 인터페이스가 왜 필요하지라는 생각을 많이 갖고 있었는데, 스프링이나 디자인 패턴들을 공부할 때마다 인터페이스의 활용도가 높다는 것을 느낀다.

 

 

반응형

'공부 > Spring' 카테고리의 다른 글

[Spring] IoC란 무엇인가?  (0) 2021.09.16
@Transactional 동작하지 않는 경우  (0) 2021.09.10
[Spring] AOP란?  (0) 2021.09.08
[Spring] Filter & Interceptor  (0) 2021.05.05
Servlet Working Flow  (0) 2021.05.04
반응형

Spring의 핵심 요소

- 제어 역전(IoC, Inversion of Control)

- 관점 지향 프로그래밍(AOP, Aspect Oriented Programming)

- 서비스 추상화(PSA, Portable Service Abstraction)

라고 볼 수 있다.

 

IoC는 우리가 제어의 역전이라고 알고 있다.

그렇다면 제어의 역전이라는 것은 무엇일까, 일반적으로 프로그램을 개발할 때 개발자(주체)가 객체의 생명 주기를 관리하게 된다. 제어의 역전이란 외부(컨테이너, 주체)에서 객체의 생명 주기를 관리하는 것이다.

이러한 IoC를 위해 DI(Dependency Injection)이 사용된다.

 

Spring IoC컨테이너가 우리가 필요한 인터페이스 자리에 객체를 생성해서 넣어준다(DI).

 

Spring에서는 IoC 컨테이너ApplicationContext 인터페이스를 구현한 오브젝트(객체)이다.

추가로 ApplicationContext는 BeanFactory를 상속한 하위 인터페이스이다.

 

IoC컨테이너를 통해 Bean이 관리된다.

Bean이란 IoC컨테이너가 관리하는 대상(객체)이다.

 

IoC컨테이너가 객체를 관리하기 위해서는 Bean을 IoC컨테이너에 등록해 주어야 한다.

등록하는 방법은 xml을 통한 방법과 java를 이용하는 방법이 있다.

 

방법은 추가로 등록하겠습니다.

반응형

'공부 > Spring' 카테고리의 다른 글

[Spring] DI란?  (0) 2021.09.20
@Transactional 동작하지 않는 경우  (0) 2021.09.10
[Spring] AOP란?  (0) 2021.09.08
[Spring] Filter & Interceptor  (0) 2021.05.05
Servlet Working Flow  (0) 2021.05.04
반응형

자바를 사용하면서 HashMap은 빼놓을 수 없는 컬렉션 중 하나이다.

HashMap은 Map 인터페이스를 구현한 컬렉션으로 Key, Value를 가진다.

Key와 Value가 짝을 이루어 입력되고 후에 찾을 때 get 메서드에 Key를 전달함으로서 Value 값을 얻을 수 있다.

 

아래는 String, Integer를 Key, Value로 갖는 예제이다.

public class HashMapTest {
	
	public static HashMap<String, Integer> map;
	
	public static void init() {
    
		map.put("a", 1);
		map.put("b", 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		String test = "a";
		
		System.out.println("##: " + map.get(test));
	}
}

결과:

##: 1

 

("a", 1)과 ("b", 2)가 들어있는 HashMap에서 "a"에 대한 Value값을 get 메서드를 통해 가져오는 예제이다.

"a"를 get에 넣으니 정상적으로 1이 나왔다.

 

 

그렇다면 Key에 우리가 만든 클래스를 사용한다면 어떤 결과가 나올까

public class HashMapTest {
	
	public static HashMap<Foo, Integer> map;
	
	public static void init() {
    
		map.put(new Foo("a"), 1);
		map.put(new Foo("b"), 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		Foo c = new Foo("b");
		
		System.out.println("##: " + map.get(c));
	}
	
	static class Foo{
    
		String key;
		
		public Foo(String key) {
			this.key = key;
		}
	}
}

위 경우 우리가 기대하는 결과 값은

##: 2

일 것이다.

 

하지만 결과는

##: null

이 나오게 된다.

 

이유는 간단하다.

우리가 만든 클래스에 hashCode와 equals를 Override하지 않아 발생한 문제이다.

위 String이나 Integer에서는 해당 문제가 발생하지 않는 이유도 String, Integer 등 클래스에는 이미 hashCode와 equals를 Override하고 있기 때문이다.

 

우선 HashMap의 put 메서드를 보자

public V put(K key, V value) {
	return putVal(hash(key), key, value, false, true);
}

입력받은 key에 대해 hash메서드를 호출한다.

 

HashMap내 hash메서드는 아래와 같다.

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

key에 대한 hashCode를 호출하게 된다. 즉, 직접 만든 class내에 hashCode 메서드를 Override하지 않으면 Object의 hashCode 메서드를 호출하게 된다.

 

이런 경우 다음과 같은 결과가 나온다.

public class HashCodeTest {

	public static void main(String[] args) {
		
		System.out.println("##: " + new Foo("a").hashCode());
		System.out.println("##: " + new Foo("b").hashCode());
		System.out.println("##: " + new Foo("b").hashCode());
		
	}
	
	static class Foo{
		String key;
		
		public Foo(String key) {
			this.key = key;
		}
	}
}

결과:

##: 32374789

##: 1973538135

##: 1023487453

 

즉, 기본 hashCode 메서드를 호출하게 되면 Foo 클래스 내 key가 같아도 다른 hash값이 나오게 된다.

 

그리고 HashMap의 get 메서드를 보자

public V get(Object key) {
	Node<K,V> e;
	return (e = getNode(key)) == null ? null : e.value;
}

getNode는 아래와 같다.

final Node<K,V> getNode(Object key) {
	Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
	if ((tab = table) != null && (n = tab.length) > 0 &&
		(first = tab[(n - 1) & (hash = hash(key))]) != null) {
		if (first.hash == hash && // always check first node
			((k = first.key) == key || (key != null && key.equals(k))))
			return first;
		if ((e = first.next) != null) {
			if (first instanceof TreeNode)
				return ((TreeNode<K,V>)first).getTreeNode(hash, key);
			do {
				if (e.hash == hash &&
					((k = e.key) == key || (key != null && key.equals(k))))
					return e;
			} while ((e = e.next) != null);
		}
	}
	return null;
}

간단하게 보면 key에 대한 hash값을 구한 뒤 hash값이 같고 equals 메서드를 통해 비교한 결과가 true인 경우 해당 값을 반환하도록 되어있다.

 

그러기 때문에 커스텀 클래스를 만든 경우 hashCode와 equals를 오버라이드해주지 않으면 HashMap에서 정상적으로 동작하지 않게 된다.

 

꼭, hashCodeequals오버라이드 하자!

 

마지막으로 hashCode와 equals를 오버라이드 한 후 HashMap을 사용한 결과이다.

public class HashMapTest {
	
	public static HashMap<Foo, Integer> map;
	
	public static void init() {
    
		map.put(new Foo("a"), 1);
		map.put(new Foo("b"), 2);
	}

	public static void main(String[] args) {
		
		map = new HashMap<>();
		init();
		
		Foo c = new Foo("c");
		
		c.key = "b";
		
		System.out.println("##: " + map.get(c));
	}
	
	static class Foo{
    
		String key;
		
		public Foo(String key) {
			this.key = key;
		}

		@Override
		public int hashCode() {
			return key.hashCode();
		}

		@Override
		public boolean equals(Object obj) {

	        if(this == obj) return true;    
	        if(obj == null || getClass() != obj.getClass()) return false;
	        
			Foo t = (Foo) obj;
			return this.key.equals(t.key);
		}
	}
}

결과:

##: 2

 

반응형

'공부 > Java' 카테고리의 다른 글

Java SSL 인증서 검증 흐름 및 확인  (0) 2025.06.17
Out Of Memory 문제 분석  (0) 2025.05.31
JavaAgnet  (0) 2021.05.09
Java Virtual Machine(JVM)  (0) 2021.05.08
Primitive, Reference Type  (0) 2021.05.07

+ Recent posts