Java에서는 Apache Ant 빌드도구가 2000년에 등장 하였고 표준 빌드도구로 사용되고 있다가 Apache Maven이 등장하였다. Maven은 현 시점에서도 사실상 명실상부한 Java 표준 빌드도구라고 할 수 있다. 

그렇게 마땅한 적수가 없던 Maven이였지만, 어느 날 Gradle이라는 빌드도구가 세상에 나오게 되고 현재까지 Gradle은 견고했던 Maven의 아성을 무너트리고 있다.

 

Gradle이란?

Gradle은 Groovy를 기반으로 한 오픈소스 빌드 도구이다.

Ant의 자유도와 Maven의 관례를 통한 접근성을 바탕으로 이전 빌드툴의 단점을 보완하여 개선된 서비스를 제공한다.

현재 스프링 오픈소스 프로젝트와 안드로이드 스튜디오의 공식 빌드 시스템이기도 하다.

 

Gradle의 특징

  • Ant처럼 매우 유연한 범용 빌드 도구이다.
  • Maven과 같은 구조화 된 Build Framework이다. (구조의 전환이 가능)
  • Maven, Ivy등의 기존 저장소 인프라 또는 pom.xml 파일과 ivy.xml 파일에 대한 Migration의 편의성이 제공된다.
  • Muilti Project 빌드를 지원한다. 
  • Dependency 관리의 다양한 방법을 제공한다.
  • Build Script를 xml이 아닌 Groovy기반의 DSL(Domain Specific Language)을 사용한다.
  • 기존 Build를 구성하기 위한 풍부한 도메인 모델이 제공된다.
  • Gradle 설치 없이 Gradle Wrapper를 이용하여 빌드를 지원한다.

 

Gradle의 장점

Apache Ant, Apache Maven과 같은 기존의 빌드툴은 xml형식을 이용하여 정적인 설정정보를 구성한다.

Gradle은 Groovy라는 스크립트 언어를 이용하여 코드로서 설정정보를 구성하기 떄문에 구조적인 장점이 있다.

  • xml의 구조적인 틀을 벗어나 코딩에 의한 간결한 정의가 가능하다.
  • 프로젝트를 설정주입방식으로 정의하기 때문에 Maven의 상속 구조보다 재사용에 용의하다.

 

Gradle의 기본구조

모든 Gradle Script는 하나 이상의 project로 구성되며, 모든 프로젝트는 하나이상의 Task로 구성된다.

  • Project : 소스를 jar로 모으거나, Java Project를 컴파일하거나, 테스트를 실행하고, 어플리케이션을 배포하는 등의 업무로 구성된다.
  • Task : 작업의 최소단위. Task간 의존관계 설정과 함께 흐름에 따른 구성이 가능하며, 동적인 Task의 생성도 가능하다.

Gradle은 자바6버전 이상의 Virtual Machine 환경에서 사용이 가능하며, 직접 설치를 하거나 Gradle Wrapper를 이용하여 설치를 하지 않고 실행환경을 구성할 수 있다.

 

Gradle Buile Lifecycle

  • 초기화(Initialization) : 빌드 대상 프로젝트를 결정하고 각각에 대한 Project 객체를 생성. settings.gradle 파일에서 프로젝트를 구성한다. (Multi, Single Project를 구분) 
  • 구성(Configuration) : 빌드 대상이 되는 모든 프로젝트의 빌드 스크립트를 실행 (프로젝트 객체 구성)
  • 실행(Execution) : 구성 단계에서 생성하고 설정된 프로젝트의 Task 중에 실행 대상을 결정한다. gradle 명령행에서 지정한 Task이름 파라미터와 현재 디렉토리를 기반으로 Task를 결정하여 선택된 Task를 실행한다.

1. Hash Table 이란?

해시 테이블은 Key와 Value로 구성된 하나의 테이블을 말한다.

해시 함수를 사용하여 색인(index)을 bucket이나 slot의 배열로 계산한다.

해시테이블은 데이터를 담을 테이블을 미리 크게 확보해 놓은 후

입력받은 데이터를 해시하여 테이블 내의 주소를 계산하고 이 주소에 데이터를 담는다.

데이터를 찾는 과정은 다음과 같다.

1. Input Data는 Hash Function을 거쳐서 테이블내의 인덱스로 변환된다.

2 그 후, 구한 인덱스를 이용해 테이블 내의 요소에 접근하게 된다.

 

해시테이블의 요소 접근 과정

 

이러한 이유로 해시테이블은 탐색, 삽입, 삭제 3가지 연산들의 시간복잡도에 대해

적절한 가정하에 평균적으로 O(1)의 상수시간에 가까워 지고 충돌이 많이 발생할수록 O(n)에 가까워진다. 

다만, 해시 테이블은 데이터가 입력되지 않은 여유 공간이 많아야 제 성능을 유지할 수 있다.

통계적으로 해시 테이블의 공간 사용률이 70 ~ 80%에 이르면 성능 저하가 나타나기 시작한다.

 

 

2. Hashing 이란?

해싱이란 해시함수(hash function)를 이용해서 데이터를 해시테이블(hash table)에 저장하고 검색하는 기법을 말한다.

해시함수는 데이터가 저장되어 있는 곳을 알려 주기 때문에 다량의 데이터 중에서도 원하는 데이터를 빠르게 찾을 수 있다.

JDK내에서 해싱에서 사용하는 자료구조는 다음과 같이 배열(Array)연결리스트(LinkedList)의 조합으로 되어 있으며 이 방법은  Separate Chaining방식이라고 부른다.

Separate Chaining 방식이 나온 이유는 해시테이블의 고질적인 문제인 해시 충돌을 방지 하기위해 고안되었다. 

※ 해시 충돌(Hash Collision): 해싱된 키(Hash Key)가 중복되어 해당 버킷에 이미 레코드가 존재하는 현상을 말한다.

(JDK 1.8부터는 하위 데이터가 8개 이하일때 LinkedList를 이상일때는 TreeMap을 사용한다.)

Array 와 LinkedList로 구성

 

저장할 데이터의 키를 Hash function에 넣으면 배열의 한 요소를 얻게 되고, 그 곳에 연결되어 있는

LinkedList에 데이터를 저장하게 된다.

하나의 예를 들어보겠다.

어떤 동네의 동사무소 직원이 해당 지역 주민들의 데이터를 주민등록번호의 맨 앞자리의 생년을 기준으로 00년대생 부터 90년대생까지 분류 하게 되면 다음과 같이 데이터가 분류된다.

 

한가지 유의할 점은, LinkedList는 검색에 불리한 자료구조이기 때문에 LinkedList의 크기가 커질수록 검색속도가 떨어지게 된다.

이는, 실생활에서 하나의 책상서랍에 물건들이 많으면 많을수록 찾는데 오래걸리는 이유와 동일하다.

반면에, Array는 크기가 커져도, 원하는 요소가 몇 번째에 있는 지만 알면 아래의 공식에 의해서 빠르게 원하는 값을 찾을 수 있다.

배열의 n번째 요소의 주소 = 배열의 시작주소 + type의 size * n

그리하여, 해시테이블은 Array의 특징으로 인해 크기가 크면 클수록, LinkedList에 최소한의 데이터가 저장되기때문에 성능이 좋아진다는 결론이 나오게 된다.

 

3. Separate Chaining 방식의 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package Hash;
 
import java.util.LinkedList;
 
/**
 * 
 * Separate Chaining기법
 * Hash Collision 을 방지하기 위한 기법 중 하나로 
 * Array와 LinkedList를 이용하여 해시테이블을 구현한다.
 * 
 * 
 * @author USER
 *
 */
 
 
public class SeparateChainingHashTable {
    
    LinkedList<Node>[] data;
    
    
    
    //default 생성자 - LinkedList 생성
    @SuppressWarnings("unchecked")
    public SeparateChainingHashTable(int size) {
        
        this.data = new LinkedList[size];
        
    }
    
    
    int getHashCode(String Key) {
        
        int hashCode = 0;
        for(char c : Key.toCharArray()) {
            
            hashCode += c;
            
        }
        
        return hashCode;
    }
    
    int hashToIndex(int hashCode) {
        
        return hashCode % data.length;
        
    }
    
    Node selectKey(LinkedList<Node> list, String key) {
        
        if(list == null) {
            return null;
        }
        
        for(Node node : list) {
            if(key.equals(node.Key)) {
                
                return node;
            }
        }
        
        return null;
    }
    
    void put(String key, String value) {
        
        // 1. 해시코드를 구한다.
        int hashCode = getHashCode(key);
        System.out.println(hashCode);
        
        // 2. 해시코드를 인덱스 주소로 변환한다.
        int index = hashToIndex(hashCode);
        System.out.println(index);
        
        // 3. 반환된 주소의 연결리스트를 list에 담는다. 
        LinkedList<Node> list = data[index];
        System.out.println(list);
        
        // 4. 해당 주소에 연결리스트가 없으면 새로 생성
        if(list == null) {
            list = new LinkedList<Node>();
            data[index] = list;
        }
        
        Node node = selectKey(list, key);
        
        // 5. 해당 연결리스트에 데이터가 있으면 마지막에 새로운 노드를 추가하여 데이터를 insert
        if(node == null) {
            list.addLast(new Node(key, value));
        } else {
            node.setValue(value);
        }
    }
    
    public String get(String key) {
        
        int hashCode = getHashCode(key);
        int index = convertToIndex(hashCode);
        
        LinkedList<Node> list = data[index];
        Node node = selectKey(list, key);
        
        return node == null ? "Not Found" : node.getValue();
        
    }
    
    class Node{
        
        String Key;
        String Value;
        
        public Node(String Key, String Value) {
            this.Key = Key;
            this.Value = Value;
        }
        
        public String getValue() {
            return this.Value;
        }
        
        public void setValue(String Value) {
            this.Value = Value;
        }
    }
}
 
class Test {
    
    public static void main(String[] args) {
        
        SeparateChainingHashTable hashTable = new SeparateChainingHashTable(10);
        hashTable.put("Alpha" , "A");
        hashTable.put("Bravo" , "B");
        hashTable.put("Charlie" , "C");
        hashTable.put("Delta" , "D");
        hashTable.put("Foxtrot" , "F");
        hashTable.put("Echo" , "E");
        
        System.out.println(hashTable.get("Alpha"));
        System.out.println(hashTable.get("Bravo"));
        System.out.println(hashTable.get("Charlie"));
        System.out.println(hashTable.get("Delta"));
        System.out.println(hashTable.get("Foxtrot"));
        System.out.println(hashTable.get("Echo"));
        
        
        
    }
}
 
http://colorscripter.com/info#e" target="_blank" style="color:#4f4f4ftext-decoration:none">Colored by Color Scripter

 

인덱스도 엄연히 하나의 객체(object)이다

SQL의 성능을 튜닝하기 위해서 애써 인덱스를 생성하고 인덱스스캔을 수행하게 하였지만

인덱스 구성 컬럼만으로 결과 집합을 도출 할 수 없는 경우 반드시 테이블 랜덤 엑세스가 발생하기 된다

알다시피, 테이블 랜덤 액세스는 DBMS 성능의 큰 장애물이다

그러므로, 이번에는 인덱스라는 객체만을 FULL SCAN하여 원하는 데이터를 가져오는

인덱스 풀 스캔(INDEX FULL SCAN)을 알아보자

 

인덱스 풀 스캔이란?

인덱스 풀 스캔은 오로지 인덱스라는 객체만을 풀 스캔하여 원하는 데이터를 가져오는 것이다

이는 인덱스 튜닝에서 매우 중요한 부분을 차지한다

 

인덱스 풀 스캔의 종류

오라클의 인덱스 풀 스캔은 다음과 같이 두 종류가 있다.

1.인덱스 풀 스캔 (Index Full Scan)

2.인덱스 패스트 풀 스캔 (Index Fast Full Scan)

인덱스 풀 스캔의 두 종류의 특성에 대해 이해하기 전에 먼저 알아야 할 것 이 있다.

그것은 바로 블록단위 I/O이다.

 

블록단위 I/O의 의미와 유형

오라클은 데이터를 가져올 때 항상 블록단위로 가져오게 된다.

예를들어, 단 한건의 행만 가져온다고 해도 해당 행이 속해있는 블록 전체를 가져오게 된다.

이것이 바로 블록단위  I/O이다.

블록단위 I/O의 유형으로는 싱글 블록, 멀티 블록이 있다.

 

싱글 블록 I/O 읽기(Single Block I/O Read)

한번의 읽기를 통해 한 개의 블록을 읽는 방식으로, 일반적인 인덱스 스캔시 사용한다.

예를들어, 사용자가 요청한 데이터 결과를 출력하기 위해 읽어야 하는 블록이 32개라면 32번의 I/O 읽기가 발생한다.

또한, 한번 읽은 블록들은 데이터 버퍼 캐시의 맨 앞쪽에 위치하여 비교적 긴 시간 동안 데이터 버퍼 캐시에 해당 결과가 남게 된다. 그리하여, 데이터 버퍼 캐시에 남아있는 일정시간 동안에는 동일한 SQL문 호출 시 빠른 속도로 사용자에게 데이터를 전달하게 된다.

즉, 자주 수행되는 SQL문에 매우 유리한 블록단위 I/O이다.

 

멀티 블록 I/O 읽기(Multi Block I/O Read)

한번의 읽기를 통해 여러 개의 블록을 읽는 방식으로, 일반적으로 테이블 풀 스캔 시 사용한다.

한번의 읽기로 읽는 블록의 개수가 64개라면 단 한번의 I/O 읽기로 64개의 블록을 모두 읽게 된다.

만약, 효율적인 인덱스 스캔이 아니라면 오히려 테이블 풀 스캔이 성능이 좋을 수 있는 이유가 바로 이 멀티 블록 I/O Read 때문이다.

또한, 싱글 블록과는 다르게 한번 읽은 블록들은 데이터 버퍼 캐시의 맨 뒤쪽에 위치하여 얼마 지나지않아 데이터 버퍼 캐시에서 사라져 동일한 SQL문 호출 시 같은 연산을 반복하게 된다.

즉, 호출 빈도수가 잦은 SQL문에서는 부적합한 방법이 된다. 

 

 

 

 

이번에 다룰 내용은 랜덤 액세스에 대한 내용이다.

아마, 오라클 튜닝에 대해 관심이 있는 사람들이 인덱스와 더불어 가장 처음 접하게 될 내용이고

이해가 가지 않는 내용이라고 생각한다

나 또한 확실하게 개념을 잡기 위해 이렇게 정리한다

 

랜덤 액세스란?

랜덤 액세스는 데이터를 저장하는 블록을 한 번에 여러 개 액세스 하는 것이 아니라

한 번에 하나의 블록만을 액세스 하는 싱글 블록 I/O 방식이다.

반대로 테이블 풀 스캔(Table Full Scan)의 경우에는 한 번에 여러 개의 블록을 액세스 하는 멀티 블록 I/O 방식을 사용한다.

 

랜덤 액세스는 언제 발생될까?

인덱스 스캔 시 인덱스의 리프 블록에는 해당 테이블의 행을 가리키는 ROW ID가 존재한다. 

B-Tree INDEX 구조 출처 : 개발자를 위한 오라클 SQL 튜닝

위에 보이는 그림의 최하단 영역인 리프 블록 해당 테이블의 행을 가리키는 ROWID 가 보일 것이다.

인덱스 스캔이 완료되면 해당 데이터를 찾아가는 유일한 주소 값인 ROWID를 확인하여 테이블에 액세스 하게 된다.

이러한 일련의 과정들을 랜덤 액세스 또는 테이블 랜덤 액세스라고 한다.

 

랜덤 액세스의 종류

1. 확인 랜덤 액세스

확인 랜덤 액세스는 WHERE 조건에 의해 발생하게 된다.

예를 들어, WHERE 조건에 계좌번호와 주민등록번호 2개의 조건이 있고 해당 테이블의 인덱스는 계좌번호 칼럼이다.

SQL이 실행되게 되면 계좌번호 칼럼에 의해 인덱스를 액세스하고 처리 범위가 좁혀질 것이다.

그러나, 주민등록번호는 인덱스로 설정이 되어있지 않기 때문에

결국 계좌번호 조건을 만족하는 모든 데이터에 대해 테이블을 액세스 하여 거래일자 조건을 부합하는 값을 찾게 된다.

이처럼 WHERE 조건의 칼럼이 인덱스에 존재하지 않아 테이블 랜덤 액세스를 발생시키는 것을 확인 랜덤 액세스라고 한다. 

확인 랜덤 액세스의 특징은 랜덤 액세스의 횟수보다 최종 결과가 동일하거나 더 적게 추출된다는 것이다.

열심히 테이블을 액세스 한 후 버려지는 데이터가 존재하기 때문이다.

그러므로, 랜덤 액세스의 세 가지 종류 중에서도 확인 랜덤 액세스의 제거는 성능에 있어 매우 중요한 역할을 수행하게 된다.

 

2. 추출 랜덤 액세스

추출 랜덤 액세스는 SELECT 절에서 발생하게 된다.

예를 들어, WHERE 절의 칼럼들은 모두 인덱스에 존재하지만 SELECT 절의 칼럼이 인덱스에 존재하지 않는다면

실행 순서에 의해 인덱스 액세스 이후 테이블을 액세스 해야 한다.

이와 같은 현상이 추출 랜덤 액세스이다.

또한 추출 랜덤 액세스의 경우 SELECT 절의 칼럼들은 추출되는 데이터를 감소시키거나 증가시키지 못하므로

발생한 만큼 결과로 추출된다.

 

3. 정렬 랜덤 액세스

정렬 랜덤 액세스는 ORDER BY 절 등에 사용된 칼럼이 인덱스에 구성되어 있지 않아 테이블을 액세스 하여

정렬을 수행하기 위해 데이터를 액세스 하는 랜덤 액세스를 발생시키는 경우를 정렬 랜덤 액세스라고 한다

정렬 랜덤 액세스도 추출 랜덤 액세스와 같이 랜덤 액세스의 횟수와 추출되는 데이터 건수가 동일하다

 

위와 같이 세 가지의 랜덤 액세스 종류를 살펴보았다.

추출, 정렬 랜덤 액세스는 랜덤 액세스의 횟수와 추출되는 건수가 동일하지만

확인 랜덤 액세스는 추출되는 데이터의 건수가 감소할 수 있고 랜덤 액세스 중 가장 많은 부하를 발생시키기 때문에

SQL 튜닝을 하는 입장에서는 최우선적으로 확인 랜덤 액세스를 제거하기 위해 노력해야 할 것이다.

 

 

 

 

 

 

#1 블로그의 시작

2019.12.01 블로그의 시작일

어쩌면 이 블로그는 남들에게 유용한 정보를 제공해주는 용도가 아닌

나만의 개인 저장소로 사용 될 것 같다.

 

나는 IT 개발자를 직업으로 삼고 있기 때문에

이 블로그에 올릴 자료들은 IT 지식 공부한 것을 정리하는 느낌으로

업로드 할 예정이다.

 

이 블로그를 통해 내가 다시한번 열정을 가지고 앞으로 나아가길

이 블로그가 나에게 확고한 모티베이션을 주기를

이 블로그가 타인에게 유용한 정보제공처가 되기를 바란다.


+ Recent posts