성공적으로 완료가 되어 반영되는 것을 커밋이라 하고, 문제가 하나로도 발생되어 원래의 상태로 돌아가는 것을 롤백이라 한다. 이를 통해 데이터의 일관성을 유지할 수 있다.
여러 RDB에서는 Stored Procedure
(저장함수)하는 개념을 제공하여 해결을 돕고 있다.
DELIMITER //
CREATE PROCEDURE TransferMoney(IN account_id1 INT, IN account_id2 INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
-- 하나의 쿼리라도 에러가 발생하면 롤백!
ROLLBACK;
END;
START TRANSACTION;
UPDATE bank_accounts SET balance = balance - 1000 WHERE account_id = account_id1;
UPDATE bank_accounts SET balance = balance + 1000 WHERE account_id = account_id2;
-- 두 UPDATE 쿼리 모두 성공하면 커밋
COMMIT;
END//
DELIMITER ;
이렇게 송금 계좌와 수신 계좌의 ID만 알면 Stored Procedure는 일관적으로 동작할 수 있다.
메시와 호날두의 사례를 적용하여 Stored Procedure 호출을 할 수 있다.
메시 계좌의 ID는 291036, 호날두 계좌의 ID는 918225번
CALL TransferMoney(918225, 291036);
Repeatable Read (MySQL 기본 설정)
한 트랜잭션 내에서 같은 데이터를 반복해서 읽어도 동일한 결과를 보장
1) 고객 A와 고객 B가 동시에 같은 상품의 재고를 확인하고 구매를 시도할 수 있다.
2) 하지만 실제로 재고를 감소시키는 업데이트는 한 명의 고객만 성공할 수 있다.
3) 고객 A가 먼저 결제를 완료하면, 고객 B는 실패하거나 잠금 대기 상태에 빠질 수 있다.
4) 데이터 일관성은 유지되지만, 고객 B는 결제 실패나 지연을 경험할 수 있다.
5) 성능은 Serializble보다 좋지만 동시 접근으로 인한 충돌 처리가 필요하다.
Read Committed (MySQL을 제외한 다른 DB들의 기본 설정)
한 트랜잭션 내에서 커밋 완료된 데이터만 읽을 수 있는 것을 보장
1) 고객 B가 고객 A의 트랜잭션이 커밋된 후에만 그 변경된 데이터를 읽을 수 있다.
2) 즉, 고객 B는 자신의 트랜잭션 중 언제든 고객 A의 결제로 인해 변경된 재고를 볼 수 있다.
3) Dirty Read는 방지되지만 Non-Repeatable Read 문제가 발생할 수 있다.
4) 각 쿼리마다 가장 최근에 커밋된 데이터를 읽기 때문에 성능과 일관성이 절충된다.
트랜잭션 내에서 같은 데이터를 여러 번 읽을 때, 그 사이에 다른 트랜잭션이 해당 데이터를 수정하거나 삭제하여 이전에 읽은 데이터와 다른 결과를 얻게 되는 현상을 말해요. 운이 좋아 해당 데이터가 커밋 전이라면 문제가 없으나 커밋이 되었다면 골치가 아프겠죠.
예를 들자면, 이런 상황이 발생할 수 있어요.
Read Uncommitted (가장 낮은 Level)
한 트랜잭션 내에서 커밋 완료되지 않은 데이터도 읽을 수 있다.
1) 고객 B가 고객 A의 결제가 끝나기 전에 변경된 재고를 확인할 수 있다.
2) 즉, 고객 A가 아직 결제를 완료하지 않았는데도 고객 B는 변경된 재고 상태를 볼 수 있다.
3) 고객 A의 트랜잭션이 롤백되더라도 고객 B는 잘못된 재고 정보를 바탕으로 결정을 내릴 수 있다.
4) 이로 인해 Dirty Read 문제가 발생할 수 있고, 일관성이 깨진다. 하지만 성능은 좋다.
트랜잭션이 커밋되기 전 커밋되지 않은 변경사항을 읽는 것을 말합니다.
예를 들자면, 이런 상황이 발생할 수 있어요.
접근제어는 특정 사용자가 데이터베이스의 어느 부분에 접근할 수 있는지를 정의하는 보안 메커니즘이다.
CREATE USER 'messi'@'localhost' IDENTIFIED BY 'goat';
이 쿼리는 localhost에서 접근할 수 있는 messi라는 이름의 새로운 사용자를 등록하고 'goat'라는 비밀번호로 이 사용자를 인증하겠다는 쿼리이다. MySQL 접속 시 ID: messi, PW: goat를 입력하여 인증할 수 있다.
GRANT SELECT, INSERT, DELETE ON soccer.* TO 'messi'@'localhost';
이 쿼리는 messi라는 유저에게 soccer라는 데이터베이스에서는 어떤 테이블이건 SELECT, INSERT, DELETE할 권한을 허락한다는 의미이다. UPDATE 쿼리는 수행할 수 없고, soccer를 제외한 다른 데이터베이스에선 아무런 권한이 없다.
사용자마다 항상 최소 권한의 법칙을 적용하여 해당 사용자가 컨트롤할 수 있는 영역에 대해서만 명확하게 권한을 주어야 한다.
프로그램에서 쿼리 인자를 입력할 땐 반드시 Prepared statement를 사용해야 한다.
웹에서 사용자 이름과 비밀번호를 입력받아 로그인을 처리하는 쿼리
"SELECT * FROM Users WHERE Username='" + username + "' AND Password='" + password + "';"
여기서 악의적인 공격자가 admin'; --
와 같은 값을 username에 넘기면 쿼리는 이렇게 된다.
"SELECT * FROM Users WHERE Username='admin'; --' AND Password='...';"
--
는 SQL에서 주석을 나타내므로, ' AND Password='...'
부분은 실행되지 않게 된다.
즉 이 쿼리는 비밀번호 검사를 생략하게 되고, 공격자가 관리자 계정에 액세스할 수 있게 된다.
이러한 공격 방법을 SQL Injection이라고 한다.
Prepared statement라는 개념으로 이러한 취약점에 대비할 수 있다.
const mysql = require('mysql');
const connection = mysql.createConnection({
host : 'localhost',
user : 'user',
password : 'password',
database : 'database'
});
connection.connect();
const username = "admin'; --";
const password = "password";
// 아래의 쿼리가 Prepared statement 입니다. 원래 인자값의 위치가 ?로 대체되어 있죠.
const sql = "SELECT * FROM Users WHERE Username = ? AND Password = ?";
connection.query(sql, [username, password], function(error, results, fields) {
if (error) {
throw error;
}
console.log(results);
});
connection.end();
connection.query 함수에 인자(username, password)를 따로 입력하게 되는 형태이다. 이렇게 되면 사용자 입력이 SQL 코드로 해석되는 것을 방지할 수 있어 공격을 방어할 수 있다.