반응형

1. Index, 인덱스란?


 

인덱스(Index)란 데이터베이스 테이블에서 데이터를 빠르게 검색할 수 있도록 도와주는 자료구조이다.

 

인덱스는 테이블의 특정 컬럼에 대해 별도의 데이터 구조(주로 B-Tree, Hash 등)를 만들어, 전체 테이블을 일일이 탐색하지 않고도 원하는 데이터를 빠르게 찾을 수 있게 해준다.

 

SQL에서 인덱스는 다음처럼 생성, 삭제, 재구성이 가능하다.

 

sql

CREATE INDEX 인덱스명 ON 테이블명(컬럼명); -- 인덱스 생성

DROP INDEX 인덱스명; -- 인덱스 삭제

ALTER INDEX 인덱스명 REBUILD; -- 인덱스 재구성

데이터베이스는 기본적으로 전체 테이블을 순차 탐색(Full Table Scan) 한다.

 

인덱스를 사용하면 특정 조건(Where)에 맞는 데이터를 테이블을 풀 스캔하지 않고 INDEX → ROWID → TABLE 순으로 빠르게 접근하기 때문에 데이터들을 빠르게 찾을 수 있다.

 

(여기서 ROWID란 테이블에서 하나의 Row(행)가 물리적으로 저장된 주소 값이다.)

 

 

 

 

 

2. Index와 Table의 구조적 차이


[출처] mangkyu.tistory.com/96

 

구분 Table Index
목적 데이터를 실제로 저장 데이터를 빠르게 찾기 위한 포인터 목록
저장형태 테이블스페이스 내 데이터블록에 로우(행) 단위 저장 테이블과 별도 공간에 Key + RowID 쌍 저장
구조 Heap Table, Cluster Table 등 다양한 구조 주로 B-Tree, Bitmap, Hash 구조
읽는 방식 전체 스캔 또는 인덱스를 통해 접근 인덱스 트리 탐색 → RowID로 테이블 접근

 

 

Table

이름, 컬럼, 제약조건 같은 구조를 설명하는 틀로, 데이터베이스 내부적으로 스키마 객체(USER_TABLES, DBA_TABLES) 같은 메타 정보로 관리된다. 실제로 디스크에 저장되는 테이블 데이터로 테이블 스페이스 -> 데이터파일 -> 데이터 블록으로 실제 값이 저장된다.

 

 

Index

별도의 저장 공간에 생성되며(B-Tree / Bitmap, Hash) 같은 구조를 사용하여 Key + RowID를 쌍으로 하는 포인터 형태로 테이블과 분리된 파일 안에 저장된다. DB가 데이터를 검색할 때 인덱스를 먼저 탐색하고 RowID를 통해 테이블에서 실제 데이터를 읽는다.

 

따라서 비유로 쉽게 Table이 이라면, Index는 책의 목차라고 표현하는 것이다.

 

책은 실제 내용을 담고 있고, 목차는 찾기 위한 주소 목록이다.

 

인덱스가 없으면 전체 책을 처음부터 끝까지 읽어야 하지만, 인덱스가 있으면 바로 찾아갈 수 있다.

 

 

 

 

 

3. 인덱스의 종류와 동작 원리


 

🔹인덱스 종류

종류 설명 특징
B-Tree Index 가장 일반적인 인덱스. 범위 검색 가능 WHERE 조건에 적합
Unique Index 중복을 허용하지 않음 PK, Unique 제약조건에 사용
Composite Index 여러 컬럼을 묶어서 만든 인덱스 다중 조건 WHERE 최적화
Hash Index 해시값 기반. 정확한 값 검색에 적합 NoSQL에서 자주 사용
Bitmap Index 값 종류가 적고 중복 많은 컬럼용 DW, OLAP 환경에서 유용
Full-Text Index
텍스트 검색 특화 검색엔진, 게시판 검색 등

 

 

🔹인덱스 스캔 

방식 설명
Full Scan 인덱스 전체 탐색
Range Scan 범위 조건에 맞는 값만 탐색
Unique Scan 고유 인덱스를 이용해 단일 Row 조회
Skip Scan 선두 컬럼 없이도 인덱스 탐색 가능

 

 

 

 

🔹B-Tree 동작 원리

[출처] coding-factory.tistory.com/746

 

 

인덱스 유형 중에서 가장 많이 사용하는 인덱스의 구조는 B-Tree(균형 트리, Balanced Tree) 구조이다.

 

B-Tree 기반으로 B+Tree 또는 B*Tree 형태로 변형하여 사용하기도 하며, B*Tree 인덱스는 오라클을 비롯한 대부분의 DBMS에서 사용하고 있는 보편적인 인덱스이다.

 

B-Tree는 루트 노드 → 브랜치 노드 → 리프 노드로 구성되어 있으며, 각 노드에는 데이터값과 해당 데이터가 실제 저장된 위치를 가리키는 포인터가 함께 저장된다.

 

검색 시, 루트 노드에서 시작해 조건에 맞는 방향으로 브랜치를 따라 내려가고, 최종적으로 리프 노드에서 포인터를 통해 실제 데이터를 찾아내는 방식이다.

 

B-Tree는 BST(Binary Search Tree, 이진 탐색 트리) 와 비슷해 보이지만, BST는 노드당 자식이 최대 2개인 반면, B-Tree는 노드 안에 데이터가 n개 존재하면 자식 노드의 수는 항상 n+1개가 되는 구조이다. (초기 root의 경우나 leaf노드 제외)

 

따라서 B-Tree 자료구조가 균형 트리라고 불리는 이유는 값이 추가될 때마다 트리는 스스로 노드 분할(페이지 분할)을 통해 균형을 맞추고, 모든 리프 노드는 항상 동일한 깊이를 가지게 되기 때문이다.

 

[출처] stackoverflow.com/questions/74256305/when-exactly-should-root-split-in-a-b-tree

 

덕분에 데이터가 아무리 많아도 탐색 시 전체 테이블 스캔을 피하고 log₂N 시간 복잡도로 일정한 검색 성능을 보장한다.

 

하지만 인덱스 파일은 생성 후 삽입, 수정, 삭제 등이 오랫동안 일어나다 보면 트리의 한쪽이 무거워져 전체적으로 트리의 깊이가 깊어지게 되고 검색 속도가 떨어지므로 주기적으로 리빌딩 작업을 거치는 것이 좋다.

 

B+Tree는 B-Tree의 확장 개념으로, 특정 키값이 한 노드에만 존재하는 것이 아닌 leaf노드와 부모노드에 공존하게 되어있는 형태이다.

 

B*Tree또한 B-Tree의 문제점인 '구조를 유지하기 위해 추가적으로 연산을 해야한다는 것' 을 보안하기 위해 모든 리프는 같은 레벨에 있으며 노드 분할 시 인접 노드와 공간을 먼저 공유한 뒤 분할하여 노드 분할을 최소화하는 방식의 형태이다.

 

그 외에 B+-Tree도 존재한다.

 

 

B-Tree VisualizationB+Tree Visualization 에서 생성과정을 눈으로 확인해볼 수 있다.

 

 

 

 

 

4. 인덱스의 타입


인덱스는 크게 두 가지로 나뉘며, 대부분 B-Tree 저장 구조로 이루어져 있다.

  • 클러스터형 인덱스(Clustered Index) = 오라클에서는 Index Organized Table(IOT)
  • 보조 인덱스(Non-Clustered Index)

 

 

🔹 클러스터형 인덱스 (Clustered Index, IOT)

일반적으로 Index와 Table분리형이다. 인덱스에 먼저 접근해서 rowid를 찾고 rowid를 이용해 테이블에서 데이터를 다시 조회한다.

 

반면 Clustered Index(IOT)Table과 Index가 결합된 형태로, 인덱스에 따로 접근할 필요 없이 테이블이 인덱스 순서대로 물리적으로 정렬된다.

 

테이블당 1개만 존재할 수 있으며 PK를 기준으로 자동 생성된다.

 

테이블 자체가 인덱스 구조로 만들어지기 때문에 검색 속도가 빠르지만 데이터를 삽입/수정/삭제할 때, 기존 데이터를 밀어내며 위치를 유지해야 하므로 성능에 영향을 준다.

 

예를 들어 ID 컬럼을 PK로 사용해 클러스터형 인덱스를 만들었다고 가정해보자, 이미 ID = 1,3,4 가 존재하는 테이블에 ID = 2를 삽입하면, 2보다 큰 모든 데이터는 한 칸씩 밀리고 ID = 2가 그 자리를 차지해야 한다. 데이터가 수십, 수백만 건이라면 이 과정은 엄청난 비용을 발생시킨다.

 

따라서 보통 Primary Key로는 순차적으로 증가하는 정수형 ID + Auto Increment 를 사용하는 이유이며, 새로운 데이터가 항상 맨 뒤에 추가되어 정렬 변경을 피할 수 있어 성능 저하를 방지할 수 있다.

 

⚠️ 오라클에서 사용하는 Cluster Index (Table)와는 다른 개념이다.

Cluster Index는 인덱스와 테이블이 느슨하게 연결된 구조로 인덱스와 데이터가 1:1이 아닌 1:N의 관계이다.
기존의 분리형 Index ↔ Table관계와, 일체형 Clurstered Index의 중간 역할을 하는 구조이다.

 

 

 

 

🔹 보조 인덱스 (Non-Clustered Index)

Non-Clustered Index는 이름 그대로 클러스터형 인덱스가 아닌 일반 인덱스를 의미한다.

 

테이블 데이터와 인덱스분리된 형태로 저장되고, 인덱스의 리프 노드에는 실제 데이터를 찾기 위한 포인터(rowid) 가 저장된다.

 

보조 인덱스의 특징은 다음과 같다.

 

  • 테이블 당 여러 개 생성 가능하다
  • 데이터는 테이블에 저장되고, 인덱스는 데이터 위치(RowID)를 참조한다
  • 조회 시 인덱스 + 테이블을 두 번 접근하므로 I/O가 발생한다
  • 데이터가 삽입/수정/삭제될 때마다 인덱스도 별도로 갱신해야 한다
  • 데이터 정렬을 보장하지 않는다
  • 클러스터형 인덱스보다 삽입/수정/삭제 성능이 좋다

 

 

 

 

 

 

5. 장단점 및 주의 사항


 

장점

1️⃣ 검색 성능 향상

WHERE, JOIN, ORDER BY, GROUP BY 등에서 빠른 데이터 접근이 가능하다. 인덱스를 사용하면 선형 탐색(O(N)) 대신 로그 탐색(O(log N)) 또는 해시 기반 탐색(O(1))이 가능하다.

 

2️⃣ 정렬/그룹핑 최적화

인덱스 자체가 정렬된 구조라서 별도의 ORDER BY 없이도 정렬된 결과를 반환 가능하다.

 

 

 

 

단점

1️⃣ 쓰기 성능 저하

INSERT, UPDATE, DELETE 시 테이블 뿐 아니라 인덱스도 갱신해야 하기 때문에 오버헤드 발생

 

2️⃣ 저장 공간 증가

인덱스는 테이블과 별개의 물리적 저장 공간을 차지한다. 컬럼 값이 길거나 Row 수가 많을수록 인덱스 크기도 커진다. 인덱스를 관리하기 위해 추가 관리가 필요하며 DB의 약 10%에 해당하는 저장 공간이 필요하다.

 

 

 

 

 

⚠️ 주의사항

인덱스는 잘 활용하면 성능을 크게 향상시키지만, 무분별하게 사용하거나 구조를 잘못 잡으면 오히려 성능을 떨어뜨릴 수 있다.

 

1️⃣ 인덱스를 과도하게 생성하면, DML(INSERT, UPDATE, DELETE) 작업 시 인덱스까지 함께 관리해야 하므로 쓰기 성능이 저하된다.

 

 

 

2️⃣ WHERE 조건으로 대부분의 데이터를 반환하는 경우, 옵티마이저는 인덱스를 사용하지 않고 Full Table Scan을 선택한다. 즉, 인덱스를 타지 않고 테이블 전체를 읽는 것이 더 빠를 수 있다.

 

 

 

3️⃣ WHERE조건에 함수가 포함되면 인덱스를 사용할 수 없다.

sql

WHERE UPPER(NAME) = 'JOHN' // → 인덱스 무시

 

 

 

4️⃣ 인덱스는 내부적으로 정렬된 상태를 유지하기 때문에, 후방 일치 검색(LIKE '%abc')처럼 정렬 정보를 활용할 수 없는 경우 인덱스를 사용할 수 없다.

sql

WHERE JOB LIKE '%ist'   -- 후방 검색, 인덱스 사용 불가

 

 

 

5️⃣복합 인덱스(Composite Index) 순서 주의

INDEX (A, B) WHERE A = ? 일 때 효율적이나,WHERE B = ? 만 사용하면 비효율적이다.

INDEX (A, B)가 다음과 같은 형태로 저장될 때

 

A B ROWID
1 a ...
1 b ...
1 c ...
2 a ...
2 d ...
3 b ...

 

sql

SELECT * FROM 테이블명 WHERE B = 'b'

B 값은 인덱스 내에서 A에 묶여있기 때문에 인덱스를 처음부터 끝까지 Full Scan해야 한다.

 

 

 

 

 

따라서 인덱스를 사용할 땐, 인덱스를 쓰는 목적과 기준을 다시 한번 생각해볼 필요가 있다.

 

 

 

 

 

6. 목적과 선택 기준


인덱스 쓰는 목적

  • 조회 속도 개선: WHERE, JOIN, ORDER BY 속도 향상
  • 정렬 최소화: 이미 인덱스가 정렬되어 있어 ORDER BY 시 별도 정렬 불필요
  • 중복 체크: Unique Index를 통해 데이터 무결성 유지

 

 

인덱스 선택 기준

  • 자주 WHERE, JOIN, ORDER BY, GROUP BY에 등장하는 컬럼
  • 데이터 분포가 넓은 컬럼 <예: 성별(X), 사용자 ID(O)>
  • 데이터가 10~20% 이상 WHERE로 추출될 경우 → Full Scan이 더 빠름
  • 자주 검색/조인/정렬되는 컬럼에만 인덱스 생성: 불필요한 인덱스는 피함
  • 저카디널리티 컬럼은 인덱스 비추천: 중복값이 많은 컬럼은 인덱스 효과가 적음
  • 너무 넓은(large) 컬럼 인덱스는 피함: 인덱스 크기 증가로 오히려 성능 저하
  • 적절한 인덱스 개수 유지: 읽기/쓰기 성능 균형 고려

 

 

 

인덱스를 사용할 때는 단순히 생성하는 것보다, 실제 쿼리가 어떻게 동작하는지를 확인하는 과정이 중요하다.
이를 위해 실행계획(Execution Plan) 을 반드시 확인해야 하며, 실행계획을 통해 쿼리가 인덱스를 제대로 사용하는지,
또는 Full Scan을 수행하는지를 사전에 파악하고 튜닝하는 습관이 필요하다.

 

 

반응형

1. ROW_NUMBER()


ROW_NUMBER() 함수는 각 행에 고유한 순번을 부여하는 윈도우 함수로, 데이터 정렬, 순위 매기기, 페이징 처리 등에 사용된다.

정렬 기준이 필요하므로 ORDER BY와 필수로 사용되며 그룹별로 순번을 따로 지정하고 싶은 경우 PARTITION BY를 선택적으로 사용할 수 있다.

sql

SELECT 
  EMPNO, ENAME, SAL,
  ROW_NUMBER() OVER (ORDER BY SAL DESC)
FROM
    EMP

 

 

 

 

2. OVER()


OVER()는 윈도우 함수를 사용할 때 필수 절로, 집합화된 row들의 정렬 순서를 지정하는 데 사용된다.

OVER()를 사용할 때는 ORDER BY 절을 사용하여 정렬 순서를 지정할 수 있다.

ORDER BY 절은 출력되는 로우의 순서와 일치하지 않아도 된다.

sql

SELECT
    DEPTNAME,
    EMPNO,
    SAL,
    RANK() OVER (PARTITION BY DEPTNAME ORDER BY SAL DESC)
FROM
    EMP

 

 

 

 

3. PARTITION BY


PARTITION BY는 데이터를 그룹으로 분할하여 계산하는 데 사용된다. 마치 GROUP BY처럼 동작하지만, 집계하지 않고 원본 데이터를 유지하면서 그룹 내에서만 순번이나 계산을 적용할 수 있다.

 

즉, GROUP BY는 그룹화된 데이터 묶음을 집계함수를 이용하여 필요한 값을 계산하고, 이를 하나의 행으로 집약시키지만, PARTITION BY는 계산된 값을 그룹화된 데이터의 각각 행에 모두 별도의 컬럼으로 추가한다.

 

 

예시 1. GROUP BY - 부서 별 평균 급여

sql

SELECT 
    DEPTNO,
    AVG(SAL) AS AVG_SAL
FROM
    EMP
GROUP BY
    DEPTNO

- DEPTNO가 10, 20, 30이면 3행만 출력됨 (요약된 값)

 

 

예시 2. PARTITION BY - 부서별 평균 급여를 모든 행에 표시

sql

SELECT 
  EMPNO, ENAME, DEPTNO, SAL,
  AVG(SAL) OVER(PARTITION BY DEPTNO) AS AVG_SAL
FROM
    EMP

 

 

 

 

4. Trouble Shoot


실무 중 입금 내역을 조회하는 화면에서 약정금이 중복으로 표시되고 집계되는 문제가 있었다.

 

 

  • 하나의 약정금에 대해 입금이 여러 차례 나눠서 이루어지는 경우가 있다.
  • 이 경우 입금이 2번, 3번 나뉘어져 있다면, 같은 약정금이 2번, 3번 반복해서 조회되는 상황이 발생한다.
  • 이로 인해 소계, 총합 계산 시에도 약정금이 실제보다 더 크게 집계되는 오류가 발생한다.

 

즉, 회차별로 한 번만 보여야 할 약정금이 입금 횟수만큼 반복되어 나오는 문제였다.

 

동일 회차의 입금 행 중 첫 번째 행만 약정금을 보여주고, 나머지는 0으로 처리하기 위해 Sheet 렌더링 과정에서 셀 병합(Merge) 처리 하려 했지만, 소계 열 생성 내부 메서드와 같이 동작하지 않았다.

 

또한, 조회 후 Js에서 후처리하는 방법으로 동일 회차의 약정금을 Merge 처리하도록 작성하였다. 시간 복잡도를 계산했을 때 O(n)이 걸림에도 내부적으로 DOM을 조작과 렌더링 관련 오버헤드 때문에 브라우저 성능 저하가 심하여 정상적으로 동작하지 않았다.

 

앞서 해결 방법들이 잘못됐다 생각이 들었고, 조회 SQL 단계에서 해결하는 것이 단순히 시각적으로 보정하는 것보다 데이터 자체를 논리적으로 통제할 수 있을 것이라 생각했고 다음과 같이 처리했다.

 

sql

SELECT
  입금자, 품목, 구분, 회차, 입금액, 입금일, ...
  CASE
    WHEN ROW_NUMBER() OVER (
      PARTITION BY 입금자, 품목, 구분, 회차, ...
      ORDER BY 입금액 DESC, 입금일, ... ASC
    ) = 1 THEN 약정금
    ELSE 0
  END AS 약정금
FROM 입금내역
...

반응형

1. PL/SQL


PL/SQL은 Procedural Language/Structured Query Language의 약자로, 데이터베이스에서 SQL을 확장하여 절차적 프로그래밍 기능을 제공하는 언어이다.

DECLARE 선언부, BEGIN 시작부, EXCEPTION 예외부, END 끝 등의 구조로 이루어져있다.

 

 

 

1.1 주요 특징

  • 절차적 프로그래밍: 조건문(IF, CASE), 반복문(LOOP, WHILE), 예외 처리 등 절차적 요소를 포함하여 논리적인 흐름을 제어할 수 있다.
  • 블록 구조: PL/SQL은 선언부(DECLARE), 실행부(BEGIN-END), 예외 처리부(EXCEPTION)로 구성된 블록 구조를 사용한다.
    • 익명 블록(Anonymous Block)
    • 저장 프로시저(Stored Procedure)
    • 함수(Function)
    • 패키지(Package)
  • SQL 통합: SQL 문과 함께 동작하며, 프로시저, 함수, 트리거 등을 정의하고 실행할 수 있다.
  • 모듈화 및 재사용성: 패키지를 통해 관련 함수와 프로시저를 묶어 관리할 수 있어 코드의 재사용성과 유지보수성을 높일 수 있다.
  • 파라미터 지원: IN, OUT, INOUT 파라미터를 사용하여 외부에서 값을 전달받거나, 실행 결과를 반환할 수 있다.
    • IN : 읽기 전용 입력 값 (기본값)
    • OUT : 프로시저 실행 후 결과 반환
    • INOUT : 입력 값도 받고 결과 값도 반환

 

 

 

1.2 CURSOR

Oracle이 SQL 문을 실행할 때 생성하는 메모리 공간인 Context Area에서 쿼리 실행 상태 정보, 결과집합, 처리 행 수 등의 정보가 저장되고 Cursor는 이 Context Area를 참조하여 데이터를 가져오는 도구이다.

 

Context Area : SQL 실행 결과(결과집합)와 실행 상태 정보를 저장하는 메모리 영역

Cursor : Context Area를 가리키고 데이터를 가져오는 도구

 
 
 
암시적 커서

sql

DECLARE
    v_count NUMBER;
BEGIN
    SELECT COUNT(*) INTO v_count FROM employees;
    DBMS_OUTPUT.PUT_LINE('총 직원 수: ' || v_count);
END;
  • SELECT COUNT(*) 실행 → Oracle이 자동으로 Context Area 생성
  • Context Area에 결과값(COUNT(*)) 저장
  • v_count 변수에 저장 후 Context Area 자동 해제

⚡ 암시적 커서의 특징

  • Context Area를 자동 관리
  • SQL%FOUND, SQL%NOTFOUND, SQL%ROWCOUNT 같은 속성으로 상태 확인 가능
 
 
 
명시적 커서

sql

DECLARE
    CURSOR emp_cursor IS SELECT emp_name FROM employees;
    v_emp_name employees.emp_name%TYPE;
BEGIN
    OPEN emp_cursor;  -- Context Area 생성 및 결과 저장
    LOOP
        FETCH emp_cursor INTO v_emp_name;  -- Context Area에서 한 행 가져오기
        EXIT WHEN emp_cursor%NOTFOUND;
        DBMS_OUTPUT.PUT_LINE(v_emp_name);
    END LOOP;
    CLOSE emp_cursor;  -- Context Area 해제
END;
  • OPEN emp_cursorContext Area 생성
  • FETCH emp_cursorContext Area에서 한 행씩 가져옴
  • CLOSE emp_cursorContext Area 해제

⚡ 명시적 커서의 특징

  • Context Area를 직접 관리해야 함
  • OPEN, FETCH, CLOSE 등을 사용

 

 

 

1.3 장단점

✅ 장점

  • 성능 향상: SQL 문을 미리 컴파일하고 저장하여 실행 속도가 빠름
  • 네트워크 트래픽 감소: DB 내에서 로직을 처리하여 애플리케이션과의 통신량 감소
  • 보안 강화: 데이터베이스 내부에서 실행되므로 직접 SQL을 실행하는 것보다 보안이 강화됨
  • 트랜잭션 제어 가능: COMMIT, ROLLBACK을 활용해 안정적인 트랜잭션 관리 가능

❌ 단점

  • 유지보수 어려움: 비즈니스 로직이 DB에 집중되면 코드 관리가 어려워짐
  • 디버깅 불편: 일반 애플리케이션 코드보다 디버깅 및 로깅이 어려움
  • 이식성 부족: DBMS에 종속적이므로 다른 데이터베이스로 이전 시 코드 변경 필요
  • 병목 발생 가능: DB 서버에서 많은 로직을 처리하면 서버 부하 증가

 

 

 

1.4 Example

sql

CREATE OR REPLACE PROCEDURE EXAMPLE."PEXC_EXAMPLE_Q" 
(
      p_example1  VARCHAR2  DEFAULT 'Q'   
     ,p_example2  VARCHAR2                                                                                           
     ,p_example3  VARCHAR2                                                                                        
     ,RTN_CURSOR  OUT SYS_REFCURSOR                                                                               
) 
IS
    ---------------------------------------------------------------------------------------------------------------------------
    -- ▩▩ [ 1. 변수선언 ]
    ---------------------------------------------------------------------------------------------------------------------------
    ERR_CODE  NUMBER := -20009; -- ERROR 코드
    ERR_MSG   VARCHAR2(200);    -- ERROR Message
    USER_EXCEPTION EXCEPTION;

    v_sql_query VARCHAR2(1000); -- 동적 SQL 저장 변수
    v_table_name VARCHAR2(50);  -- 테이블명 동적 결정 변수

    v_count NUMBER; -- 데이터 존재 여부 체크용
BEGIN
    ---------------------------------------------------------------------------------------------------------------------------
    -- ▩▩ [ 2. 수행전 Check Logic ]
    ---------------------------------------------------------------------------------------------------------------------------
    -- 테이블 존재 여부 체크 (p_example1 기준)
    SELECT COUNT(*) INTO v_count 
    FROM all_tables 
    WHERE table_name = UPPER('TABLE_EXAMPLE1');

    IF v_count = 0 THEN
        RAISE_APPLICATION_ERROR(-20001, 'TABLE_EXAMPLE1 테이블이 존재하지 않습니다.');
    END IF;

    ---------------------------------------------------------------------------------------------------------------------------
    -- ▩▩ [ 3. Business Logic 처리 ]
    ---------------------------------------------------------------------------------------------------------------------------
    DBMS_OUTPUT.PUT_LINE('example');    

    -- 동적 테이블명 설정
    IF p_example1 = 'Q' THEN
        v_table_name := 'TABLE_EXAMPLE1';
    ELSE
        v_table_name := 'TABLE_EXAMPLE3';
    END IF;

    -- 동적 SQL 생성
    v_sql_query := 'SELECT * FROM ' || v_table_name;

    -- 반복문을 활용한 디버깅 메시지 출력
    FOR i IN 1..3 LOOP
        DBMS_OUTPUT.PUT_LINE('Loop iteration: ' || i);
    END LOOP;

    -- 동적 SQL 실행
    OPEN RTN_CURSOR FOR v_sql_query;

    ---------------------------------------------------------------------------------------------------------------------------
    -- ▩▩ [ 4. Exception 처리 ]
    ---------------------------------------------------------------------------------------------------------------------------
EXCEPTION
    WHEN USER_EXCEPTION THEN
        RAISE_APPLICATION_ERROR(ERR_CODE, ERR_MSG);
    WHEN OTHERS THEN
        RAISE_APPLICATION_ERROR(ERR_CODE, SQLERRM);
END;

예시 구조를 보면 PL/SQL 코드가, Business Logic을 처리하는 Service Layer와 데이터 접근을 담당하는 Repository의 역할을 함께 수행하며, 하나의 Method처럼 활용한다.

 

그럼 Java단에서는 ? 추가적인 전처리나 후처리 작업이 없다면 파라미터만 넘겨 리턴 받으면 끝이다.

 

PL/SQL 같은 프로시저 기반 개발 방식에서는 계층형 아키텍처객체지향 설계를 쓰지 않는다.

 

 

 

2. Procedure vs Java


먼저 간략히 말하자면 우리 회사는 Stored Procedure를 사용한다.

입사 후 첫 프로젝트를 마치며 프로시저의 단점을 너무 많이 느꼈고, 왜 객체지향 개발을 하지않을까 의문이 생겼다.

그렇게 프로시저 개발을 채택하는 이유와 Java 프로그래밍에 대한 차이를 찾아보던 중 도움되고 공감되는 글이있었고, 두 개발 방식의 차이를 잘 정리해놓아 가져와보았다.

 

 

 

🔹 기술적 차이

   Java : 컴파일된 소스를 어플리케이션 서버에 배포하여 실행하는 객체지향 프로그래밍. 비교적 간단한 쿼리로 데이터를 조회한 후 자바 객체를 이용해 로직을 구현

   Procedure : DB 서버에서 소스를 컴파일 후 실행하는 절차지향 프로그래밍. 쿼리 집합. 순차적으로 쿼리를 수행하여 로직 구현. 문법이 DBMS 마다 다르다.

 

 

 

🔹 성능/부하 관련

   Java :

  • 어플리케이션 서버의 사양에 영향이 크고 DB 서버 사양에 비교적 영향을 덜 받는다.
  • 쿼리 성능 외에 네트워크 IO 횟수에도 영향을 받는다.
  • 객체지향다운 구현과 단순하고 효율적인 쿼리를 사용하여 DB 접속 네트워크 I/O 횟수를 줄이는 것이 좋다.
  • 수행 쿼리 로그를 콘솔과 파일로 남길 경우 그를 위한 추가 자원과 시간이 필요하다. 네트워크 I/O 보다 로깅이 더 많은 시간을 잡아먹는다. 최신 로깅 라이브러리가 지원하는 비동기 로깅을 고려해볼 수 있다.
  • 캐싱 기법으로 동일 쿼리 재수행을 최소화할 수 있다.
  • 멀티 쓰레드를 활용하여 성능 향상을 기대할 수 있다. 단, 데드락에 주의해야 함. 또한 스프링 트랜잭션 전파는 멀티 쓰레드에 적용되지 않는다는 것도 주의.
  • 성능 이슈가 발생할 경우 로깅 파일 살펴보면 응답이 느린 쿼리를 찾아내기 쉽다.

   Procedure :

  • DB 성능이 매우 중요하다. 그러므로 DBMS 선택도 중요하고 DB 서버 사양도 중요함.
  • 로직이 복잡할 경우 복잡한 쿼리를 사용하게 되어 쿼리 작성 실력과 튜닝이 더욱 중요해짐.
  • 트랜잭션 격리 수준에 따라 다르지만, 동일 쿼리 반복 수행시 실제로 데이터를 조회하므로 비효율적인 쿼리 수행이 많음.
  • 프로시저 자체에서 멀티쓰레드를 지원하진 않지만, 어플리케이션에서 멀티 쓰레드로 프로시저를 실행할 수는 있다.
  • 성능 이슈가 발생할 경우 프로시저 내에서 속도가 느린 쿼리를 찾아내기 어렵다. DB 관리자의 지원이 절대적으로 필요.

 

 

 

🔹 형상관리 (버전관리)

   Java : IDE 툴을 통해 모든 소스가 SVN, GIT 등으로 형상 관리가 되고 그것이 그대로 서버에 배포되므로 변경 추적과 원복이 용이하다. 동시에 같은 소스를 다른 개발자가 따로 수정할 경우 conflict 오류가 발생하여 서로 알아채고 merge 하기 쉽다.

   Procedure : 별도로 소스 변경 내역을 수동으로 백업하며 관리하여야 하고 그것이 DB 서버에 실제로 반영되었음을 보장하지 않으므로 변경 추적과 원복이 어렵다. 동시에 같은 소스를 다른 개발자가 따로 수정할 경우 아무런 오류 없이 나중에 컴파일된 소스가 반영되므로 서로 알아채기 어렵다.

 

 

 

🔹 배포관리

   Java : 변경 소스를 반영하기 위해 어플리케이션 서버를 무중단 서버로 구축하거나 재시작 해야한다.

   Procedure : DB 에서 프로시저 컴파일만 하면 되므로 권한이 있는 사람이면 아무 때나 수정 배포할 수 있다. 단, 어플리케이션 서버에서 콜하는 프로시저이고 파라미터 갯수나 타입 등이 바뀔 때에는 어플리케이션 소스도 변경 배포되어야 하므로 두 작업이 되도록 동시에 이루어지도록 타이밍을 잘 맞추어야 한다.

 

 

 

🔹 디버깅/테스트

   Java : 개발자들이 개발시 사용하는 IDE 툴로 쉽게 디버깅이 가능하다. 로깅 설정으로 로그 파일을 남기면 실제 수행된 쿼리와 바인딩된 변수 확인이 가능하므로 운영 환경에서도 문제 추적이 용이하다. 소스 일부 테스트와 테스트 자동화가 용이하다.

   Procedure : 디버깅 지원하는 DB 툴이 있어야 하며, 파라미터를 수동으로 입력하고 실행하여 디버깅을 해야 한다. 잘못된 파라미터 입력으로 버그나 오류가 발생했는데 애꿎은 프로시저만 디버깅하느라 삽질하는 경우가 종종 있다. DB 서버 자체에 쿼리 수행 로그가 남지만 개발자에게 접근 권한을 주는 경우는 거의 없으므로 운영 환경에서 문제 추적이 어렵다. 프로시저 내에 특정 부분만 테스트할 수 없고 프로시저 단위로 수행해야 하므로 테스트 자동화가 까다롭다.

 

 

 

🔹 에러 처리

   Java : 다양한 추상화 기법으로 일관된 에러 메시지 생성과 처리가 용이함.

   Procedure : 발생한 에러 메시지를 어플리케이션에 OUT 변수로 전달해야 한다. 경우에 따라 어플리케이션에서는 이를 다시 특정 Exception 으로 포장하거나 분기 처리해야 하므로 비교적 다루기 복잡함.

 

 

 

🔹트랜잭션 관리

   Java : 스프링 등의 선언적 트랜잭션 관리 기법으로 코딩이 아닌 설정을 통한 다양한 트랜잭션 전략 활용 용이하고, 그에 따라 트랜잭션을 조건에 따라 다양하게 옵션화할 때도 동일한 소스를 수정없이 재사용할 수 있다. 단, 일부 특수한 트랜잭션 설정은 DBMS 에서 지원해주는 것만 가능하다.

   Procedure : 기본적으로 어플리케이션에서 트랜잭션을 관리하지만, 프로시저에서 수동 커밋 롤백이 가능함. 프로그래밍적으로 직접 커밋 롤백 명령문을 작성해야 하므로 트랜잭션 분리 전략을 다양하게 활용하기 어렵다.

 

 

 

🔹 DB 의존도

   Java : 특정 DBMS 특화 문법이나 함수 사용을 지양하고 표준 ANSI SQL 위주로 개발하면 DB 의존도를 최소화하여 변경 비용을 크게 줄일 수 있다.

   Procedure : DBMS 마다 프로시저 문법이 다르므로 의존도가 매우 높다. 양이 많고 복잡하다면 한 번 프로시저로 개발하면 DBMS 변경이 현실적으로 거의 불가능하다.

 

 

 

🔹 소스 보안 (안티 리버싱)

   Java : 자바 특성상 디컴파일이 쉽기 때문에 완벽한 비공개가 어렵다. 전문 상용 난독화 툴을 적용하면 분석이 거의 불가능하게 만들 수 있기는 하다.

   Procedure : DBMS 에 따라 자체 제공하는 소스 암호화 방법이 있다. 그 외 접근 제한을 통해 소스를 보호할 수도 있다.

 

 

 

🔹 개발/수정 생산성

   Java : 객체지향 개발 특성상 개발 초기에는 자바 소스를 여럿 작성해야 해서 시간이 걸리지만, 일단 개발이 완료되고 나면 수정 변경에 유연하고 견고한 시스템을 구축할 수 있다.

   Procedure : 개발 초기에는 프로시저만 작성하면 되어 금방 만들 수 있지만, 절차 지향 특성상 로직이 복잡해지고 소스가 길어질수록 점점 수정 변경이 어려워지고 리스크가 커진다.

 

 

 

 

Spring Boot부터 시작해서 OOP, JPA로 개발을 배웠던 나로서는 PROCEDURE의 단점이 너무 많다고 느낀다.

동적 SQL이나 프로시저 내 임시 테이블을 이용한 로직, 새로운 프로젝트마다 덕지덕지 붙어서 돌아가는 SP들은 가독성도 좋지 않고, 디버깅이 힘들어 유지 보수 측면에서 너무 불편하다.

 

하지만 한편으론, 진짜 이런 방식이 ‘무조건 나쁜 걸까?’ 하는 생각도 든다.

과연 Java로 모든 로직을 옮기면 정말 더 깔끔하고 유지 보수하기 쉬운 구조가 될까?

10년 넘는 도메인 지식을 가진 설계자들이 반드시 Spring과 JPA를 잘 알아야 할까?

결국, 기술 선택에는 비용과 리소스, 그리고 누가 그것을 다룰 수 있느냐가 전부 포함되어 있다.

비즈니스 적으로도 모두 다 해당하는 개발자의 인건비와 빠른 개발 속도를 포기하고 Java를 택하기엔 수지 타산이 맞지 않다.

 

결국 중요한 건 "무엇이 더 낫냐"가 아니라, "이 상황에서 어떤 선택이 더 효율적인가?"라는 질문이다.

현재로서 내가 할 수 있는 일은 지금 쓰고 있는 구조 안에서 최대한 문제를 줄이는 것뿐.

이런 고민을 하다 보면, 나는 도대체 어떤 개발자가 되고 싶은 걸까 하는 질문에 자꾸 멈춰 서게 된다.

옛 방식을 고수하면 도태될까 두렵고, 그렇다고 무작정 신기술만 밀어붙이는 마이웨이 개발자가 되는 것도 싫다.

나는 어디쯤에 서야 할까? 아직은 나도 내 스탠스를 정확히 정하지 못했다.

반응형

0. Atlas


MongoDB Atlas에서 Cluster 생성 후 Connect → Drivers → Java로 맞춘다음 uri 를 복사한다.

password만 Replace라고 나와있지만 아래처럼

 

uri

mongodb+srv://<**UserName**>:<**password**>@<Cluster>.ergif.mongodb.net/<databaseName>?retryWrites=true&w=majority

<>안에 있는 항목인 UserName, password, databaseName을 적어준다.

 

 

1. build.gradle


gradle

implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

 

 

 

2. Application.yml


yml

spring:
    data:
    mongodb:
      uri: mongodb+srv://UserName:password@cluster.ergif.mongodb.net/database?retryWrites=true&w=majority

 

 

 

3. Application.java


java

@SpringBootApplication
@EnableJpaAuditing
@EnableJpaRepositories(basePackages = {
        "com.challenge.chat.domain.chat.repository.mysql",
        "com.challenge.chat.domain.member.repository"
})
@EnableMongoRepositories(basePackages = "com.challenge.chat.domain.chat.repository.mongo")
public class ChatApplication {

    public static void main(String[] args) {
        SpringApplication.run(ChatApplication.class, args);
    }
}

만약 database를 여러개 사용할 경우 위와 같이

@EnableJpaRepositories에 basePackages 설정과

@EnableMongoRepositories에 basePackages 설정을 해줘야 한다.

 

 

 

4. Document


java

@Getter
@Document(collection = "chat")
@NoArgsConstructor
public class Chat {
    @Id
    private String id;

    private MessageType type;

    private String sender;

    private String userId;

    private String roomId;

    private String message;

    private String date;

    public Chat(ChatDto chatDto, MessageType messageType) {
        this.type = messageType;
        this.sender = chatDto.getSender();
        this.userId = chatDto.getUserId();
        this.roomId = chatDto.getRoomId();
        this.message = chatDto.getMessage();
        this.date =  chatDto.getDate();
    }
}

MongoDB는 Document 모델의 NoSQL DataBase다. 따라서 @Entity 대신 @Document를 사용한다.
Id또한 String으로하는 이유는 @Id 로 PK를 설정하면 자동으로 중복되지 않은 ObjectId를 생성하며 String 값이기 때문이다.

MessageType 은 Enum 클래스이며 Enum, Long, Date, List 모두 잘 들어간다.

 

 

 

5. Repository


java

@Repository
public interface ChatRepository extends MongoRepository<Chat, String> {

    List<Chat> findByRoomId(String roomId);
}

Spring Data MongoDB 를 지원하기 때문에 어느정도 메서드 쿼리를 사용할 수 있다.

@query 어노테이션을 사용할 수 있으며 MongoDB Json 쿼리를 직접 작성할 수 있다. 공식문서

 

 

 

6. Config


java

@Configuration
public class MongoConfig {
    // MongoDB 에 "_class" 들어가지 않게 설정
    @Autowired
    private MongoMappingContext mongoMappingContext;

    @Bean
    public MappingMongoConverter mappingMongoConverter(
            MongoDatabaseFactory mongoDatabaseFactory,
            MongoMappingContext mongoMappingContext
    ) {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDatabaseFactory);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext);
        converter.setTypeMapper(new DefaultMongoTypeMapper(null));
        return converter;
    }
}

꼭 필요한 설정은 아니지만 위와 같이 설정을 해주지 않으면 _class 필드가 같이 저장된다.

반응형

1. Intro


채팅 시스템 프로젝트 당시, MySQL 단일로 사용하고 있었다. Java 서버는 Scale Up/Out을 진행하며 트래픽에 안정적인 모습을 보여주나 DB에서 부하가 발생하기 시작했다. 따라서 Kafka와 같은 MQ의 도입 및 아키텍처 수정과 함께 DB를 전환하기 위해 NoSQL을 조사했다.

 
RDBMS의 한계

많은 데이터량과 데이터 처리량이 계속적으로 증가한다면

  1. 스키마 문제 : 빅데이터를 RDB의 스키마에 맞춰 변경해서 넣으려면 매우 긴 시간의 down time이 발생한다.
  2. Scale up(서버 자체의 성능을 증가)의 한계 : RDBMS는 애초에 스케일 아웃을 염두에 두고 설계하지 않았다. 관계형 모델과 트랜잭션의 연산, 일관성, 속성을 유지하면서 분산환경(Scale Out)에서 RDBMS를 조작하는것은 어렵다.
  3. RDBMS에 채팅 내용을 담기에 I/O 비용이 크다. ( I/O : 데이터를 넣고 빼는 리소스(cpu, disk, memory) 사용량이 크다.
SQL NoSQL
규격이 정해져 있다. 정해진 규격이 없다.
Join이 가능하다. Join이라는 개념 자체가 없다.
트랜잭션을 사용한다. 트랜잭션이 없다. → MongoDB 어느정도 가능
분산처리가 어렵다. 분산처리가 쉽다.
ACID를 구현한다.
ACID를 완벽히 구현하지 않고 "Eventual consistency" 개념이 도입되었다.

 

 

결론
  • 테이블 형식의 데이터 저장이 필요 없다.
  • Join, where 과 같은 sql 연산이 필요 없다.
  • 빠른 읽기/쓰기 지연, 데이터를 자주 변경하지 않는다.
  • 캐시 데이터 저장 가능
  • 수직, 수평적 확장 가능 → 분산처리 용이

 

 

 

2. NoSQL이란?


💡 Not Only SQL의 약자로 기존 RDBM 형태의 관계형 데이터베이스가 아닌 다른 형태의 데이터 저장 기술을 의미한다.
  • 기존의 정형화된 데이터뿐만 아니라 메신저 텍스트, 음성 등 반정형화, 비정형화된 데이터도 저장하고 다뤄야 하는 수요가 생겼다.
  • 빅데이터를 다룰 때, RDBMS로만 트래픽을 감당하기 어려워졌고, NoSQL은 분산 환경을 빠르게 처리하기 위해서 개발되었다.
  • NoSQL의 핵심Horizontal Scalability(수평확장)과 High Availability(고가용성)이다.
  • RDBMS 가 클라이언트/서버 환경에 맞는 데이터 저장기술이라면, NoSQL은 클라우드 환경에 맞는 저장 기술이다.

 

 

 

3. CAP


 

분산 데이터베이스는 방대한 데이터를 다루기에 유용한 시스템이다. 분산 데이터베이스는 수평 확장할 수 있기 때문에 트래픽이 증가하더라도 낮은 지연 시간을 유지할 수 있고, 일부 노드 장애에 적절히 대응할 수 있다. NoSQL 데이터베이스는 대표적인 분산 데이터베이스이며, 유연하게 데이터를 다룰 수 있다는 장점이 있다.

 

분산형 구조일관성(Consistency), 가용성(Availability), 분산 허용(Partitioning Tolerance)의 3가지 특징을 가지고 있다. CAP이론은 이 중 2가지만 만족할 수 있다는 이론인데, NoSQL은 대부분 이 CAP이론을 따른다.

 
 
3.1 일관성(Consistency)

데이터는 항상 일관성 있는 상태를 유지해야 하고 데이터의 조작 후에도 무결성을 해치지 말아야 한다는 속성이다.

 

쓰기 동작이 완료된 후 발생하는 읽기 동작은 마지막으로 쓰여진 데이터를 리턴해야 한다. 일관성동시성 또는 동일성이라고도 하며 다중 클라이언트에서 같은 시간조회하는 데이터항상 동일한 데이터임보증하는 것을 의미한다.

 

이것은 관계형 데이터베이스지원하는 가장 기본적인 기능이지만 일관성을 지원하지 않는 NoSQL을 사용한다면 데이터의 일관성느슨하게 처리되어 동일한 데이터가 나타나지 않을 수 있다. 느슨하게 처리된다는 것은 데이터의 변경시간의 흐름에 따라 여러 노드전파하는 것을 말한다. 이러한 방법을 최종적으로 일관성유지된다고 하여 최종 일관성 또는 궁극적 일관성을 지원한다고 한다.

 

 

각 NoSQL들은 분산 노드 간데이터 동기화를 위해서 두 가지 방법을 사용한다.

  1. 첫번째로 데이터저장 결과클라이언트응답하기 전모든 노드데이터저장하는 동기식 방법이 있다. 그만큼 느린 응답시간을 보이지만 데이터의 정합성보장한다.
  2. 두번째로 메모리임시 파일기록하고 클라이언트먼저 응답한 다음 특정 이벤트 또는 프로세스사용하여 노드데이터동기화하는 비동기식 방법이 있다. 빠른 응답시간을 보인다는 장점이 있지만, 쓰기 노드장애발생하였을 경우 데이터손실될 수 있다.

 

분산 시스템에서 일관성유지하기 위해서는 희생이 따른다. 가용성분할 허용성지원하는 카산드라(Cassandra DB)최종 일관성(Eventual Consistency)을 지원한다. 또한 설정값조절하여 강한 일관성지원할 수 있다. (최종 일관성이란 특정 노드에서 데이터를 읽을 때 최신 데이터가 아닐 수도 있다는 뜻이다)

 

많은 NoSQL 솔루션은 읽기와 쓰기의 성능 향상을 위해 데이터메모리임시기록한 다음 클라이언트에 응답하고 백그라운드 쓰레드(혹은 프로세스)로 해당 데이터를 디스크에 기록한다.

 

방금 전에 언급했듯이 이 경우에는 데이터 손실의 위험존재하게 되는데 카산드라와 HBase에서는 이러한 손실을 방지하기 위해 메모리저장하기 커밋로그 및 WAL파일에 먼저 정보기록하는 방법을 사용하고 있다. Redis에도 AOF(Append Only File)라는 기능이 존재한다.

 
 
3.2 가용성(Availability)

특정 노드가 장애가 나도 서비스가 가능해야 한다라는 의미를 가진다. 데이터 저장소에 대한 모든 동작(read, write 등)항상 성공적으로 리턴되어야 한다.

 

명확해 보이는 단어이기는 하지만 분산 시스템에서의 특징을 말하는 것이기 때문에 서비스가 가능하다성공적으로 리턴이라는 표현이 애매하다. 얼마동안 기다리는 것 까지를 성공적이라고 할 수 있느냐에 대한 문제가 남아있다.

 

"20시간정도 기다렸더니 리턴이 왔어! Availability가 있는 시스템이야!" 라고 할 수 없기 때문이다.

 

다시한번 성공적으로 리턴 에 대해서 보면, 모든 동작에 대해서 시스템이 Fail 이라는 리턴을 성공적으로 보내준다면 그것을 Availability가 있다고 해야 하느냐에 대해서도 애매하다. CAP를 설명하는 문서들 중 Fail 이라고 리턴을 하는 경우도 성공적인 리턴 이라고 설명하는 것을 보았다.

 

NoSQL은 클러스터 내에서 몇 개의 노드망가지더라도 정상적인 서비스가능하다. 몇몇 NoSQL은 가용성보장하기 위해 데이터 복제(Replication)을 사용한다. 동일한 데이터다중 노드중복 저장하여 그 중 몇 대의 노드고장나도 데이터가 유실되지 않도록 하는 방법이다.

 

데이터 중복 저장 방법에는 동일한 데이터를 가진 저장소하나 더 생성하는 Master-Slave 복제 방법데이터 단위중복 저장하는 Peer-to-Peer 복제 방법이 있다.

 
 
3.3 분산 허용(Partitioning Tolerance)

분할 허용성이란 지역적으로 분할된 네트워크 환경에서 동작하는 시스템에서 두 지역 간의 네트워크가 단절되거나 네트워크 데이터의 유실이 일어나더라도 각 지역 내의 시스템은 정상적으로 동작해야 함을 의미한다.

 

가용성(Availability)과의 차이점은 Availability특정 노드장애발생한 상황에 대한 것이고 'Tolerance to network Partitions'노드의 상태정상이지만 네트워크 등의 문제서로 간연결끊어진 상황에 대한 것이다.

 

 

 

4. PACELC 이론


PACELC 이론은 CAP 이론으로 부족한 부분을 보완하기위해 네트워크 장애 상황과 정상 상황으로 나누어서 설명하는 이론이다.

 

P(네트워크 파티션)상황에서 A(가용성)과 C(일관성)의 상충 관계와 E(else, 정상)상황에서 L(지연 시간)과 C(일관성)의 상충 관계를 설명한다. PACELC 이론에서는 장애 상황, 정상 상황에서 어떻게 동작하는지에 따라 시스템을 PC/EC, PC/EL, PA/EC, PA/EL로 나눌 수 있다.

 

장애상황 정상상황 유형 특성 구현 시스템
P+A E+L • 장애 상황 시 가용 노드만 반영, 복구 시 전체 반영
• 정상 상황 시 Latency 우선 고려
• PA 가능한 노드에만 데이터 반영, 복구 시 Eventual Consistency 적용
• EL Latency 위해 모든 노드에 데이터를 반영 안함
Cassndra
DynamoDB
P+A E+C • 장애 상황 시 가용 노드만 반영, 복구 시 전체 반영
• 정상 상황 시 모든 노드 동일 메시지 보장
• PA 장애 상황일때 C를 포기 가능한 노드에만 데이터 반영
• EC 모든 노드에서 같은 메시지를 볼 수 있도록 쓰기연산 수행
MongoDB
Hazelcast IMDG
P+C E+L • 장애 상황 시 Timeline Consistency 수준 보장
• 정상 상황 시 Latency 우선 고려
• PC 평소만큼의 C를 보장하기 위해 A를 희생
• EL 평소에도 L을 위해 C를 희생(Timeline Consistency)
PNUTS
P+C E+C • 장애 상황 시 일관성 우선 보장
• 정상 상황 시 모든 노드 동일 메시지 보장
• PC 장애 상황일 때 C를 위해 A를 희생
• EC 그렇지 않은 경우에도 C를 위해 L을 희생
VoltDB
HBase

 

 

 

5. NoSQL 종류와 특징


5.1 Key-Value

Key-Value의 쌍으로 데이터가 저장되는 유형이다. 키는 값에 접근하기 위한 용도로 사용되며, 어떠한 형태데이터라도 담을 수 있다. (이미지나 비디오도 가능하다)

  • 간단한 API를 제공하는 만큼 질의의 속도가 굉장히 빠른 편이다.
  • 단순한 저장구조로 인하여 복잡한 조회 연산을 지원하지 않는다.
  • 고속 읽기와 쓰기에 최적화된 경우가 많다.
  • 하나의 서비스 요청에 다수의 데이터 조회 및 수정 연산이 발생하면 트랜잭션 처리가 불가능하여 데이터 정합성을 보장할 수 없다.
  • Dynamo DB, Redis, Riak, Memcache … 등이 있다.
 
5.2 Column-Based

 

Column 별로 연속적으로 저장하는 유형이다.

  • 하나의 키에 여러 개의 컬럼 이름과 컬럼 값의 쌍으로 이루어진 데이터를 저장하고 조회한다.
  • 모든 컬럼은 항상 타임 스탬프 값과 함께 저장된다.
  • 저장의 기본 단위는 컬럼으로 컬럼은 컬럼 이름과 컬럼 값, 타임스탬프로 구성된다.
  • 컬럼형 NoSQL은 구글의 Big Table 영향을 받았다. → Big Table DB라고도 함
  • 이러한 컬럼들의 집합이 Row(Row)이며, Row키(Row key)는 각 Row를 유일하게 식별하는 값이다.
  • Cassandra DB, HBase, ScyllaDB 등이 있다.

대부분의 컬럼 모델 NoSQL은 쓰기와 읽기 중에 쓰기에 더 특화되어 있다. 그렇기 때문에 읽기 연산 대비 쓰기 연산이 많은 서비스나 빠른 시간 안에 대량의 데이터를 입력하고 조회하는 서비스를 구현할 때 가장 좋은 성능을 보인다.

 

채팅 내용 저장, 실시간 분석을 위한 데이터 저장소 등의 서비스 구현에 적합하다. 또한 값들은 일련의 바이너리 데이터로 존재하기 때문에 어떤 형태의 데이터라도 저장될 수 있다. 다만 위의 두 모델들과는 다르게 Blob 단위의 쿼리가 불가능하며 로우와 컬럼의 초기 디자인이 중요하다. Schema Less이긴 하지만 새로운 필드를 만드는 데 드는 비용이 크기 때문에 사실상 결정된 스키마를 변경하는 것이 어렵다.

 
5.3 Document-Oriented

데이터가 key와 document 형태로 저장되는 JSON, XML과 같은 Collection 데이터 모델 구조이다. key는 문서에 대한 ID 로 표현된다. key-value 모델과 달리 value는 계층적인 형태의 document로 저장된다. 이를 통해 하나의 객체를 여러 개의 테이블에 나눠 저장할 필요가 없어지게 된다.

  • 객체를 document의 형태로 바로 저장할 수 있기 때문에 객체-관계 매핑을 할 필요가 없다.
  • key-value 모델과 동일하게 검색에 최적화되어 있다.
    • 문서 ID 에 대한 인덱스를 사용하여 O(1) 시간 안에 문서를 조회할 수 있다.
  • 중앙 집중식 로그 저장, 타임라인 저장, 통계 정보 저장 등에 사용된다.
  • Document 데이터베이스의 질의 결과가 JSON, xml 형태로 출력된다.
  • Mongo DB, Cough DB 등이 있다.

대부분의 문서 모델 NoSQL 은 B 트리 인덱스를 사용하여 2 차 인덱스를 생성한다. B 트리는 크기가 커지면 커질수록 새로운 데이터를 입력하거나 삭제할 때 성능이 떨어지게 된다. 읽기와 쓰기의 비율이 7:3 정도일 때 가장 좋은 성능을 보인다.

 
5.4 Graph

Nodes, Relationship, Key-Value 데이터 모델을 채용하고 있다.

  • 데이터 간의 관계를 그래프로 형성하고 통합 검색을 빠르게 할 수있다.
  • 개체와 관계를 그래프 형태로 표현한 것이므로 관계형 모델이라고 할 수 있다.
  • 페이스북이나 트위터 같은 소셜 네트워크, 연관된 데이터를 추천해주는 추천 엔진이나 패턴 인식 등의 데이터베이스로 적합하다.
  • Neo4J, OreientDB 등이 있다.

 

 

6. 프로젝트에 맞는 NoSQL 선택


6.1 NoSQL 종류와 특성

 
6.2 Top 3

Key-Value 제품을 포함하여 전체 NoSQL제품들 중 현재 시장에서 가장 많이 인기가 있는 제품들은

  1. MongoDB(Document)
  2. HBase(Wide Columnar Store)
  3. Cassandra(Wide Columnar Store)

출처: https://www.samsungsds.com/kr/insights/1232564_4627.html

 
[ Cassandra & HBase ]
  • 카산드라 클러스터 설정 및 구성이 HBase 클러스터 구성보다 훨씬 쉽다.
  • 카산드라가 일반적으로 write시 5배 이상의 더 나은 성능, read시 4배 이상의 성능을 보인다.
[ Cassandra & MongoDB ]
  • Cassandra 노드가 추가될수록 MonogoDB 보다 훨씬 나은 선형적인 성능 향상을 보인다.
  • 다중 Index가 필요한 구조라면 MongoDB를 선택하고, 데이터 항목 변경이 많고 unique access가 많은 경우라면 Cassandra가 적합하다.

    비교 관련자료
 

 

6.3 Discord의 DB

현 프로젝트는 Discord와 같은 채팅 서비스를 생각하고 진행 중이라서 가장 먼저 Discord의 데이터베이스 모델을 찾아봤다.

ScyllaDB의 경우 현재 프로젝트에 오버스펙이라 판단했고, MongoDB와 Cassandra 중 선택하기로 결정되었다.

 
 
6.4 결론
  • 관련 자료를 쉽게 얻을 수 있는 것 → MongoDB
  • CP(MongoDB) or AP(Cassandra) → MongoDB
  • 채팅 시스템 데이터 모델 → Cassandra
  • 페이지네이션, 인덱스 검색 성능 → MongoDB
  • 모니터링 & Cloud 지원 → MongoDB

관련 자료와 모니터링 및 클라우드 지원 측면에서는 MongoDB가 압도적으로 유리했다.

 

Discord와 같이 하루에 몇 십억의 채팅이 발생하는 시스템이라면 가용성을 제일 중시하여 Cassandra를 선택했겠지만, 현재 프로젝트 목표와 테스트 시나리오 상 하나의 채팅방에 최대 100명의 접속자와 초당 최대 5000건의 성능 테스트를 진행하고 있었기 때문에 일관성이 중요했다.

 

MongoDB는 가용성보다는 일관성을 보장하는 분산 시스템이고 Cassandra는 가용성을 추구하는 시스템으로 분류된다. 이 분류는 단지 두 시스템 모두의 기본 동작만을 설명할 뿐 이지만, 테스트의 신뢰성을 높이는 측면에선 MongoDB가 유리했다.

 

선택을 하지 못한 우리는 결국 각각의 Insert 성능을 테스트해서 비교해보고 선택하기로 결정했다.

 

테스트 기준 : MongoDB(M10, 3 replica) vs Cassandra(3 nodes)

테스트 결과

  • 1000 Requests / sec
    • MongoDB: 에러율 0%, 응답 평균 시간: 9.42 ms
    • Cassandra: 에러율 7.5%, 응답 평균 시간: 97.92 ms
  • 2000 Requests / sec
    • MongoDB: 에러율 1.1%, 응답 평균 시간 16.66 ms
    • Cassandra: 에러율 25.66%, 응답 평균 시간: 303.88 ms

 

MongoDB가 Cassandra보다 더 좋은 성능 지표가 나왔고, 이를 분석해본 결과, Cassandra는 node 6개를 기반으로 설계가 되어있는 db라고 한다. 또한 cpu의 성능도 많이 좋아야 했기에 default 환경을 갖추는데 많은 투자가 필요하다.

 

운영 비용 측면에서 MongoDB M10 보다 3배의 비용이 발생하기 때문에 MongoDB를 채택하게 되었다.

반응형
반응형

+ Recent posts