SQL 데이터 삭제 명령어: DELETE, DROP, TRUNCATE 비교 분석
1. 데이터 삭제 명령어의 종류와 특징
SQL에서 데이터를 삭제하는 명령어는 크게 DELETE, DROP, TRUNCATE 세 가지로 나눌 수 있으며, 각각의 역할과 동작 방식에 차이가 있다.
DELETE
DELETE는 데이터 조작 언어(DML)에 속하며, 테이블의 데이터를 행(row) 단위로 삭제하는 명령어이다.
- 동작 방식:
WHERE절을 사용하여 특정 조건을 만족하는 행만 선택적으로 삭제할 수 있다. 만약WHERE절을 생략하면 테이블의 모든 행을 삭제한다. - 트랜잭션:
DELETE작업은 트랜잭션 로그에 각 행의 삭제 기록을 남기므로, 작업 속도가 상대적으로 느리다. 하지만 이 덕분에ROLLBACK을 통해 데이터를 복구하는 것이 가능하다. - 특징: 테이블의 구조나
AUTO_INCREMENT와 같은 속성은 변경하지 않고, 오직 데이터만 삭제한다.
DROP
DROP은 데이터 정의 언어(DDL)에 속하며, 테이블 자체를 완전히 삭제하는 명령어이다.
- 동작 방식: 테이블의 모든 데이터뿐만 아니라, 테이블의 구조, 인덱스, 제약 조건, 트리거 등 모든 객체를 영구적으로 삭제한다.
- 트랜잭션: DDL 명령어이므로
ROLLBACK으로 복구할 수 없다. 실행 즉시 데이터베이스에 반영되므로 사용에 매우 신중해야 한다. - 특징: 테이블의 존재 자체가 사라진다.
TRUNCATE
TRUNCATE 또한 데이터 정의 언어(DDL)로 분류되며, 테이블의 모든 데이터를 한 번에 삭제하는 명령어이다.
- 동작 방식:
WHERE절을 사용할 수 없으며, 테이블의 모든 행을 삭제한다. 내부적으로는 테이블을 삭제하고 재생성하는 것과 유사하게 동작하여 매우 빠르다. - 트랜잭션:
DELETE와 달리 트랜잭션 로그를 거의 남기지 않아ROLLBACK이 불가능하거나 제한적이다. - 특징: 테이블이 사용하던 저장 공간을 해제하고,
AUTO_INCREMENT속성을 초기값으로 리셋시킨다.
2. DELETE와 TRUNCATE의 비교 및 사용 사례
두 명령어 모두 테이블의 데이터를 삭제하지만, 다음과 같은 명확한 차이점이 존재하여 상황에 맞게 사용해야 한다.
| 구분 | DELETE | TRUNCATE |
|---|---|---|
| 종류 | DML (데이터 조작어) | DDL (데이터 정의어) |
| 삭제 단위 | 행(Row) 단위 | 테이블 전체 (모든 행) |
WHERE 절 |
사용 가능 | 사용 불가능 |
| 속도 | 느림 (각 행을 로깅) | 빠름 (최소한의 로깅) |
ROLLBACK |
가능 | 불가능 (또는 제한적) |
AUTO_INCREMENT |
유지됨 | 초기화됨 |
| 트리거 | 각 행마다 실행 | 실행되지 않음 |
DELETE를 사용해야 할 때
- 일부 데이터만 삭제해야 할 경우:
WHERE절을 사용하여 특정 조건을 만족하는 행만 삭제해야 할 때 반드시DELETE를 사용해야 한다. - 데이터 복구 가능성이 필요할 경우: 작업 실수 등을 대비하여
ROLLBACK을 통해 데이터를 복원할 필요가 있을 때 사용한다. - 삭제 트리거를 실행해야 할 경우: 각 행이 삭제될 때마다 특정 비즈니스 로직(트리거)이 실행되어야 할 때 적합하다.
TRUNCATE를 사용해야 할 때
- 테이블의 모든 데이터를 빠르게 삭제해야 할 경우: 대용량 테이블의 모든 데이터를 비우고 초기 상태로 되돌리고 싶을 때 가장 효율적이다.
- 데이터 복구가 필요 없을 경우: 로그 데이터나 테스트 데이터처럼 완전히 삭제해도 무방한 데이터를 처리할 때 사용한다.
AUTO_INCREMENT값을 초기화하고 싶을 경우: 테이블의 ID 값을 1부터 다시 시작하게 만들고 싶을 때 유용하다.
3. 내 프로젝트 주문 API에서 TRUNCATE를 사용하지 않은 이유
프로젝트의 주문 생성 API에서는 장바구니에 담긴 상품을 주문한 후, 해당 상품들을 장바구니 테이블에서 삭제하는 로직이 포함되어 있다. 이때 TRUNCATE가 아닌 DELETE 명령어가 사용되었다.
-- 장바구니에서 주문한 항목 삭제
const deleteCartSql = `DELETE FROM carts WHERE user_id = ? AND id IN (?)`;
await conn.query(deleteCartSql, [userId, cart_item_ids]);
TRUNCATE를 사용하지 않은 이유는 명확하다.
첫째, 특정 조건의 데이터만 삭제해야 하기 때문이다.
TRUNCATE는 테이블의 모든 데이터를 삭제하는 명령어이며, WHERE 절을 지원하지 않는다. 주문 로직에서는 장바구니 테이블(carts)의 모든 데이터를 삭제하는 것이 아니라, 현재 주문을 진행한 특정 사용자(user_id = ?)의 장바구니 항목 중에서도, 이번에 주문한 상품들(id IN (?))만을 선별하여 삭제해야 한다. 이처럼 특정 조건을 만족하는 데이터만 삭제하기 위해서는 DELETE 명령어 사용이 필수적이다.
둘째, 트랜잭션의 원자성을 보장해야 하기 때문이다.
주문 생성 과정은 여러 단계(주문 정보 생성, 주문 상세 정보 생성, 장바구니 항목 삭제 등)로 구성되어 있으며, 이 모든 과정은 하나의 트랜잭션(Transaction)으로 묶여 있다. 만약 중간에 오류가 발생할 경우, conn.rollback()을 통해 이전의 모든 작업을 취소하고 데이터 일관성을 유지해야 한다.
DELETE는 DML이므로 트랜잭션의 일부로 완벽하게 동작하며,ROLLBACK이 가능하다.- 반면,
TRUNCATE는 DDL이므로 대부분의 데이터베이스에서 암묵적인COMMIT을 유발하여 트랜잭션을 깨뜨리고ROLLBACK을 불가능하게 만든다.
따라서 주문 과정의 원자성(Atomicity)을 보장하고 데이터의 일관성을 지키기 위해서는 트랜잭션 제어가 가능한 DELETE를 사용하는 것이 올바른 선택이다.
4. 심화: "지옥에서 온 conn.query"와 현대적인 해결책
앞선 프로젝트 예시처럼 현대적인 Node.js 개발에서는 conn.query를 직접 사용하되, async/await와 같은 구조화된 방식을 사용한다. "지옥에서 온 conn.query"라는 표현은, 이러한 안전장치 없이 conn.query를 날것 그대로 사용할 때 마주하는 문제점들을 의미한다.
conn.query가 만들어내는 '지옥'의 종류
콜백 지옥 (Callback Hell)
mysql라이브러리의 기본query는 콜백 기반으로 동작한다. 여러 쿼리를 순차적으로 실행해야 할 경우, 콜백 함수가 계속 중첩되어 코드의 가독성이 급격히 떨어지고 유지보수가 어려워진다.// 콜백 지옥의 예시 conn.query('SELECT...', (err, result1) => { if (err) throw err; conn.query('INSERT...', (err, result2) => { if (err) throw err; // 코드가 계속해서 오른쪽으로 파고든다. }); });트랜잭션 관리 지옥
콜백 기반 코드에서BEGIN,COMMIT,ROLLBACK을 수동으로 관리하는 것은 매우 복잡하다. 에러 발생 시 각 단계마다ROLLBACK처리를 신경 써야 하며, 하나라도 놓치면 데이터 정합성이 깨지는 심각한 문제가 발생할 수 있다.유지보수 지옥
비즈니스 로직 코드 곳곳에 SQL 쿼리 문자열이 흩어져 있으면, 테이블 구조가 변경될 때마다 관련된 모든 코드를 찾아 수정해야 하는 유지보수의 악몽이 시작된다.
'지옥'에서 벗어나는 방법 (프로젝트 적용 사례)
내 프로젝트에서는 이러한 '지옥'을 피하기 위해 다음과 같은 현대적인 해결책을 적용했다.
mysql2/promise와async/await를 통한 트랜잭션 관리
콜백 지옥과 트랜잭션 관리 지옥을 해결하기 위해Promise를 지원하는mysql2/promise라이브러리를 도입했다. 이를 통해 복잡한 주문 생성 과정을async/await와try...catch...finally구문으로 명확하고 안정적으로 구현했다.이 구조는 복잡한 비동기 흐름과 에러 처리를 매우 직관적으로 만들어주며, 데이터의 정합성을 강력하게 보장한다.// orders.repository.js의 create 함수 exports.create = async ({ userId, delivery_info, cart_item_ids }) => { const conn = await dbPool.getConnection(); // 1. 커넥션 풀에서 연결을 가져온다. try { await conn.beginTransaction(); // 2. 트랜잭션 시작 // 3. 주문 관련 쿼리들을 순차적으로 실행 const itemsToOrder = await findCartItemsForOrder(conn, userId, cart_item_ids); // ... (주문 생성, 주문 상세 생성) ... const deleteCartSql = `DELETE FROM carts WHERE user_id = ? AND id IN (?)`; await conn.query(deleteCartSql, [userId, cart_item_ids]); await conn.commit(); // 4. 모든 작업이 성공하면 커밋 return { order_id: orderId, message: "주문이 성공적으로 완료되었습니다." }; } catch (err) { await conn.rollback(); // 5. 중간에 에러 발생 시 롤백 throw err; } finally { conn.release(); // 6. 성공/실패 여부와 관계없이 연결을 풀에 반환 } };리포지토리 패턴 (Repository Pattern)을 통한 관심사 분리
유지보수 지옥을 피하기 위해 리포지토리 패턴을 적용했다. 프로젝트의 모든 데이터베이스 관련 코드는orders.repository.js,carts.repository.js와 같은 리포지토리 파일에 격리시켰다.- 적용 사례:
- 주문 생성과 관련된 모든 SQL 쿼리와 트랜잭션 로직은
orders.repository.js의create함수 안에 모여 있다. - 서비스 계층(Service Layer)에서는 이 리포지토리의
create함수를 호출하기만 하면 된다. 서비스는 복잡한 SQL 쿼리나 트랜잭션의 존재 자체를 알 필요가 없다.
- 주문 생성과 관련된 모든 SQL 쿼리와 트랜잭션 로직은
- 장점:
- 중앙 관리: 데이터베이스 로직이 한 곳에 모여 있어 테이블 구조 변경 시 수정할 부분이 명확하다.
- 재사용성: 동일한 쿼리를 여러 서비스에서 재사용하기 용이하다.
- 관심사 분리: 서비스 계층은 비즈니스 로직에만 집중하고, 데이터 접근은 리포지토리에 위임하여 코드의 역할이 명확해진다.
- 적용 사례:
결론적으로, conn.query 자체는 죄가 없다. 하지만 이를 어떻게 구조화하고 추상화하여 사용하느냐에 따라 개발 경험은 '지옥'이 될 수도, '천국'이 될 수도 있다. 내 프로젝트는 async/await와 리포지토리 패턴을 통해 conn.query의 단점을 극복하고, 안정적이고 유지보수하기 좋은 코드를 구현한 좋은 사례이다.
'Programmers' 카테고리의 다른 글
| [과제]Express Book Market API: JWT 인증 기능 테스트 시나리오 (0) | 2025.10.29 |
|---|---|
| [35일차]Node.js 에러 핸들링: 기본부터 실전 프로젝트 적용까지 (0) | 2025.10.29 |
| [33일차]Node.js의 논블로킹 I/O와 비동기 처리 방식 (1) | 2025.10.27 |
| [32일차]나의 book_market 프로젝트 추가 기술 정리 보고서 (0) | 2025.10.24 |
| [31일차]데이터베이스 무결성 관리, 스키마 개선 작업 결과 (0) | 2025.10.23 |