앞서, 내가 작성했던 코드 및 프로그램에서는 데이터를 파일로 저장하거나 인메모리 형태로 데이터를 임시로 저장해서 사용했었다.
사실, 데이터베이스를 배운 시점인 지금도 이 방법들은 나에겐 데이터를 가장 쉽게 접근하여 사용할 수 있는 방법이다.
그런데 규모가 큰 프로젝트에서는 이러한 방법들은 분명히 한계가 존재한다.
파일형태로 데이터를 보관하는 경우의 한계
파일을 읽고 쓰는 방식으로 데이터에 접근하는 방법이다. 그러나 이 방법에는 분명한 한계가 존재한다.
엑셀이나 CSV 같은 파일의 형태는 데이터가 필요할 때마다 전체 파일을 매번 읽어야 한다.
파일이 손상되거나 여러 개의 파일들을 동시에 다뤄야 하거나 하는 등 복잡하고 데이터량이 많아질수록 데이터를 불러들이는 작업이 점점 힘들어질 수 있다.
그런데, 관계형 데이터베이스에서는 하나의 CSV 파일이나 엑셀 시트를 한 개의 테이블로 저장할 수 있다. 한 번에 여러 개의 테이블을 가질 수 있기 때문에 SQL 을 활용해 데이터를 불러오기 수월하다.
인메모리 형태로 데이터를 보관하는 경우의 한계
여기서 말하는 인메모리 형태는 프로그램이 실행될 때만 존재하는 데이터를 말한다.
JavaScript에서 변수를 만들어 저장한 경우, 프로그램이 종료될 때 해당 프로그램이 사용하던 데이터도 사라지게 된다. 이 말은 변수 등에 저장한 데이터가 프로그램의 실행에 의존한다는 것이다.
예상하지 못한 상황으로부터 데이터를 보호할 수 없고, 프로그램이 종료된 상태라면 데이터를 원하는 시간에 받아올 수 없으므로, 데이터의 수명이 프로그램의 수명에 의존하게 된다.
이러한 이유들로 데이터베이스를 활용하면 기존에 파일이나 인메모리 형태로 데이터를 보관하던 방법에서 발생할 수 있는 문제들을 걱정할 필요가 없다.
데이터베이스는 관계형 데이터베이스와 비관계형 데이터베이스로 구분할 수 있다.
관게형 데이터베이스는 SQL을 기반으로 동작하며, 비관계형 데이터베이스는 NoSQL로 데이터를 다룬다.
관계형 데이터베이스에서는 테이블의 구조, 데이터 타입, 제약 조건을 사전에 정의하고 이에 맞춰 알맞은 형태의 데이터만 삽입할 수 있다. 또한 관계형 데이터베이스는 행, 열로 구성된 테이블(릴레이션)에 데이터를 저장한다.
특정한 형식을 지키기 때문에, 데이터를 정확하게만 입력한다면 SQL을 이용햬 데이터에 수월하게 접근할 수 있다. (MySQL, Oracle, SQLite, MariaDB...등)
NoSQL은 주로 데이터가 고정되어 있지 않은 데이터베이스를 의미한다. NoSQL이라 해서 NoSQL에 스키마가 없는 것은 아니다. 관계형 데이터베이스에서는 데이터를 입력할 때 스키마에 맞게 입력해야 하지만, NoSQL에서는 데이터를 읽을 때 스키마에 따라 데이터를 읽어온다.
즉 NoSQL은 데이터를 읽어 올때에만 데이터 스키마가 사용된다. 하지만 이 방법이 데이터를 쓸 때 정해진 방식이 없다는 것을 의미하진 않는다. 데이터를 입력하는 방식에 따라, 데이터를 읽어올 때 영향을 미치기 때문이다. (MongoDB, Casandra ... 등)
SQL은 데이터베이스 언어로, 주로 관계형 데이터베이스에서 사용한다. 예를 들어 MySQL, Oracle, SQLite, PostgreSQL 등 다양한 데이터베이스에서 SQL 구문을 사용할 수 있다.
SQL이란 데이터베이스 용 프로그래밍 언어이다.
데이터베이스에 쿼리(질의)를 보내 원하는 데이터를 가져오거나 삽입할 수 있다. 그리고 이름에서 유추할 수 있듯이, SQL을 사용하기 위해서는 데이터가 구조가 고정되어 있어야 한다.
(즉 SQL은 데이터가 구조화된 테이블을 사용하는 데이터베이스에서 활용할 수 있다.)
SQL을 사용할 수 있는 데이터베이스와 달리, 데이터의 구조가 고정되어 있지 않은 데이터베이스를 NoSQL이라고 한다. 관계형 데이터베이스와는 달리, 테이블을 사용하지 않고 데이터를 다른 형태로 저장한다.
NoSQL의 대표적인 예시는 MongoDB 와 같은 문서 지향 데이터베이스가 있다.
스키마는 데이터베이스에서 데이터가 구성되는 방식과 서로 다른 엔티티 간의 관계에 대한 설명이다.
즉, 데이터베이스의 설계도라고 볼 수 있다.
학교에서 필요한 데이터베이스를 구축한다고 가정한다면, 대표적으로 선생님, 수업, 학생과 같은 데이터를 저장할 수 있을 것이다.
여기서 선생님(Teachers), 수업(Classes), 학생(Students) 등을 엔티티라고 하는데, 엔티티는 고유한 정보의 단위이다. 그리고 데이터베이스에서는 이러한 엔티티를 테이블로 표현한다.
그리고 각각의 엔티티안의 Name, Department, Classes, Room Number, Email 등 엔티티를 구성하고 있는 요소를 필드(속성)이라고 하며 테이블에서 열을 구성하게 된다.
그리고 레코드는 테이블에 저장된 항목이다. 테이블에서 하나의 행(한 줄)을 의미한다.
그리고 이 테이블 사이에는 관계를 표현할 수 있다.
"선생님이 여러 수업을 담당하고 있다."
"한 개의 수업에는 여러 학생들이 참여할 수 있다."
이처럼 테이블 간에는 다양한 관계를 가질 수 있다.
그렇다면 테이블 사이에 다양한 관계를 어떻게 표현하는 걸까?
테이블 사이의 관계를 표현하기 위한 방법으로 기본 키(primary key)와 외래 키(primary key)를 사용한다.
기본 키란, 하나의 레코드(데이터)를 식별할 수 있는 유일한 식별자로 중복되지 않으며 NULL값을 가질 수 없다.
외래 키란, 테이블 간의 관계를 표현하기 위해 사용되는 키이다.
다른 테이블에서 현재의 테이블의 기본 키를 참조할 때 해당 값을 외래 키라고 한다.
아래의 그림에서 TeacherID 필드는 Teachers 테이블의 특정 레코드를 참조하고 있는 외래 키이다.
(참고로 1:N 관계에서는 N에 해당하는 테이블에 외래키 설정하는 것이 좋다.)
위의 예에서는 한 명의 선생님이 여러 수업을 담당할 수 있는 1:N의 관계를 표현했지만,
아래의 테이블들은 한 개의 수업은 여러 학생들이 참여할 수 있고, 한 명의 학생은 여러 수업을 참여할 수 있다와 같이 N:M의 관계를 가지고 있다.
이처럼 N:M의 관계를 표현하는 방법으로는 바로 1:N과 1:M의 관계로 분리하는 것이다.
위의 그림에서 Classes/Students 테이블은 기존의 N:M 관계를 1:N, 1:M의 관계로 표현할 수 있도록
중간에 위치하고 있다. 따라서 한 명의 학생이 참여하고 있는 수업을 찾고 싶다면 Classes/Student 테이블에서 학생의 ID를 찾고 이 ID에 매핑된 ClassID를 찾아서 Classes 테이블에서 해당하는 수업에 대한 정보를 가져오는 것이다.
이와같이 현실 세계를 표현하기 위한 데이터의 논리적인 구조를 스키마라고 한다.
(위의 구조와 더불어 제약조건등이 추가될 수 있다.)
트랜잭션이란 여러 개의 작업을 하나로 묶은 실행 단위이다.
각 트랜잭션은 하나의 특정 작업으로 시작을 해 묶여 있는 모든 작업을 완료해야 정상적으로 종료한다.
만약 하나의 트랜잭션에 속해있는 여러 작업 중에서 단 하나의 작업이라도 실패하면, 이 트랜잭션에 속한 모든 작업을 실패한 것으로 판단한다.
다시 말해 작업이 하나라도 실패를 하게 되면 트랜잭션도 실패이고,
모든 작업이 성공적이면 트랜잭션 또한 성공이다.
성공 또는 실패 라는 두 개의 결과만 존재하는 트랜잭션은, 미완료된 작업 없이 모든 작업을 성공해야 한다.
데이터베이스 트랜잭션은 ACID라는 특성을 가지고 있다.
ACID는 데이터베이스 내에서 일어나는 하나의 트랜잭션(transaction)의 안전성을 보장하기 위해 필요한 성질이다.
Atomicity(원자성)
원자성은 하나의 트랜잭션에 속해있는 모든 작업이 전부 성공하거나 전부 실패해서 결과를 예측할 수 있어야 한다.
하나의 단위로 묶여있는 여러 작업이 부분적으로 실행된다면, 업데이트가 일어났지만 누가 업데이트했는지 모르거나, 업데이트 날짜가 누락되는 등 데이터가 오염될 수 있다.
즉 트랜잭션의 작업들은 분리되어 동작할 수 없기 때문에 일부의 완료라는 것은 존재하지 않는다.
트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전혀 반영되지 않아야하는 성질이다.
예를 들어 계좌이체를 할 때에는 다음과 같은 두 단계가 있다.
1. A 계좌에서 출금한다.
2. B 계좌에 입금한다.
계좌이체를 하려는데 A 계좌에서는 출금이 이뤄지고, B 계좌에 입금되지 않았다. 이때 어디서 문제가 발생했는지 파악할 수 없다면, A 계좌에서 출금된 돈은 세상에서 사라지는 돈이 된다.
이때 A 계좌에서 출금은 성공했지만, B 계좌에 입금하는 작업에 실패한다면 계좌 A에서 출금하는 작업을 포함하여 모든 작업이 실패로 돌아가야 한다는 것이 Atomicity(원자성)임시저장이다.
Consistency(일관성)
Consistency(일관성)은 데이터베이스의 상태가 일관되어야 한다는 성질이다.
하나의 트랜잭션 이전과 이후, 데이터베이스의 상태는 이전과 같아야 한다.
다시 말해, 트랜잭션이 일어난 이후의 데이터베이스의 상태는 무결성이 유지되고 모순되지 말아야 한다는 성질이다.
예를 들어 '모든 고객은 반드시 이름을 가지고 있어야 한다'라는 데이터베이스의 제약 조건이 있다고 한다면,
다음과 같은 트랜잭션은 Consistency(일관성)를 위반하는 것이다.
1.이름 없는 새로운 고객을 추가하는 질의
2.기존 고객의 이름을 삭제하는 질의
데이터베이스의 유효한 상태는 다를 수 있지만, 데이터의 상태에 대한 일관성은 변하지 않아야 한다. 이 예시는 '이름이 있어야 한다' 라는 제약 조건을 위반하는 것이다.
따라서 예시 트랜잭션이 일어난 이후의 데이터베이스는 일관되지 않는 상태를 가지게 되므로 일관성을 지키지 못하는 것이다.
Isolation(격리성, 독립성)
Isolation 특징은 모든 트랜잭션은 다른 트랜잭션으로부터 독립되어야 한다는 뜻이다.
동시에 여러 개의 트랜잭션들이 수행될 때, 각 트랜잭션은 고립(격리) 되어 있어 연속으로 실행된 것과 동일한 결과를 나타낸다.
즉 같은 자원에 대해 여러 트랜잭션이 동시에 사용할 수 없어 하나의 트랜잭션이 실행되는 중간에는 다른 트랜잭션 연산이 침범하지 못하는 성질이다.
예를 들어 계좌에 5,000원이 있다고 가정한다면,
이 계좌로부터 B 계좌로 3천 원을, C 계좌로 3천 원을 동시에 이체하는 경우, 계좌 B에 먼저 송금한 뒤 계좌 C에 보내는 결과와 동일해야 한다.
동시에 트랜잭션을 실행한다고 해서 계좌 B와 C에 각각 3천 원씩 송금하여 마이너스 통장이 되는 것이 아니다.
각각의 송금 작업을 연속으로 실행하는 것과 동일한 결과가 나타나야 한다.
독립성을 지키는 각 트랜잭션은 철저히 독립적이기 때문에, 다른 트랜잭션의 작업 내용을 알 수 없다.
그리고 트랜잭션이 동시에 실행될 때와 연속으로 실행될 때의 데이터베이스 상태가 동일해야 한다.
Durability(지속성)
Durability(지속성)는 하나의 트랜잭션이 성공적으로 수행되었다면, 해당 트랜잭션에 대한 결과는 지속적으로 유지되어야 한다. 만약 런타임 오류나 시스템 오류가 발생하더라도, 해당 기록은 영구적이어야 한다는 뜻이다.
예를 들어 은행에서 계좌이체를 성공적으로 실행한 뒤에, 해당 은행 데이터베이스에 오류가 발생하더라도 트랜잭션의 결과는 기록으로 남아있어야 한다.
마찬가지로 계좌이체를 로그로 기록하기 전에 시스템 오류 등에 의해 종료가 된다면, 해당 이체 내역은 실패로 돌아가고 각 계좌들은 계좌이체 이전 상태들로 돌아가게 된다.
COMMIT(완료) : 트랜잭션 실행이 성공적으로 종료되었음을 알리는 연산으로 데이터의 값들은 영속성, 지속성이 보장되며, 데이터베이스의 상태가 일관성 있는 상태로 변화된 상태를 의미한다.
ROLLBACK(복귀) : 트랜잭션 실행이 실패하였음을 알리는 연산으로 트랜잭션이 수행한 결과를 원래의 상태로 복귀시켜야 하는 상태를 의미한다.
SELECT CustomerId, AVG(Total)
FROM invoices
WHERE CustomerId >= 10
GROUP BY CustomerId
HAVING SUM(Total) >= 30
ORDER BY 2
데이터를 조회하는 SELECT 문은 정해진 순서대로 동작한다. SELECT 문의 실행 순서는 다음과 같다.