1. 저장 프로그램 소개
- 일반적으로 데이터베이스는 데이터 저장과 처리를 담당하고, 모든 비즈니스 로직과 절차는 애플리케이션 코드(Java, Python 등)에 있었습니다.
- 그런데 만약, 자주 사용되는 복잡한 작업 절차 자체를 데이터베이스 안에 하나의 '프로그램'처럼 저장해두고, 필요할 때마다 그 프로그램의 이름만 불러서 실행할 수 있으면 어떨까요?
1.1 문제 상황
- 쇼핑몰에서 새로운 회원이 가입할 때, 시스템은 다음과 같은 여러 작업을 순서대로 처리해야 합니다.
users테이블에 새로운 고객 정보를INSERT합니다.user_profiles테이블에 고객의 상세 프로필 정보를INSERT합니다.coupons테이블에 신규 가입 축하 쿠폰을INSERT합니다.logs테이블에 신규 회원이 가입했다는 기록을INSERT합니다.
- 이 4개의 SQL 문 묶음은 '회원 가입'이라는 하나의 비즈니스 로직을 구성합니다.
- 이 로직을 애플리케이션 코드에 둘 수도 있지만, 데이터베이스 안에 저장해두고
회원가입_처리('네이트', ...)와 같이 간단하게 호출할 수는 없을까요? - 이러한 요구사항을 해결하기 위해 데이터베이스가 제공하는 기능이 바로 **저장 프로시저(Stored Procedure), 저장 함수(Stored Function), 그리고 트리거(Trigger)**입니다.
- 이들은 데이터베이스 내에 저장되어 실행되는 작은 프로그램 조각들입니다.
1.2 저장 프로그램의 종류
1.2.1 저장 프로시저 (Stored Procedure)
- 정의: 이름이 부여된 일련의 SQL 작업 묶음입니다.
- 특징: 파라미터를 받아 로직을 처리할 수 있고,
IF문이나LOOP문 같은 제어문도 사용할 수 있습니다. 여러 개의INSERT,UPDATE,DELETE작업을 포함하는 복잡한 비즈니스 로직을 하나의 단위로 처리하는데 사용됩니다. - 호출 방식:
CALL 프로시저이름(파라미터1, 파라미터2);와 같이CALL명령어로 독립적으로 호출됩니다.
1.2.2 저장 함수 (Stored Function)
- 정의: 특정 계산을 수행하고 반드시 '하나의 값'을 반환하는 프로그램입니다.
- 특징: 프로시저와 달리, 반드시 하나의 값을 반환(
RETURN)해야 한다는 점이 다릅니다. 이 특징 때문에,SUM()이나COUNT()같은 내장 함수처럼 일반적인SELECT쿼리문 안에서 값의 일부로 사용될 수 있습니다. - 사용 방식:
SELECT name, 나의함수(컬럼명) FROM 테이블;처럼 쿼리의 일부로 사용됩니다.
1.2.3 트리거 (Trigger)
- 정의: 특정 테이블에 특정 이벤트(Event)가 발생했을 때, 자동으로 실행되도록 약속된 프로그램입니다.
- 특징
- 개발자가 직접 호출하는 것이 아니라, 특정 조건이 만족되면 데이터베이스에 의해 '방아쇠(Trigger)'가 당겨지듯 자동으로 실행됩니다.
- 여기서 '이벤트'란
INSERT,UPDATE,DELETE같은 데이터 변경 작업을 의미합니다.
- 사용 예시
orders테이블에 새로운 주문(INSERT)이 들어올 때마다, 자동으로shipments(배송) 테이블에 새로운 배송 준비 데이터를 생성하거나,logs테이블에 감사(Audit) 기록을 남기는 용도로 사용될 수 있 습니다.
1.3 저장 프로그램이 존재하는 이유
- 과거에는 이런 기능들이 자주 사용되었습니다.
1.3.1 성능
- 애플리케이션 서버의 성능이 좋지 않고 네트워크 속도가 느렸던 시절, 여러 개의 SQL을 네트워크를 통해 주고받는 것보다, 데이터베이스 안에 로직을 넣어두고 한 번만 호출하는 것이 훨씬 더 효율적이었습니다.
1.3.2 코드 재사용과 중앙화
- 여러 종류의 애플리케이션(Java 웹, Python 분석 스크립트, 엑셀 연동 툴 등)이 동일한 데이터베이스 로직을 사용해야 할 때, 로직을 데이터베이스의 프로시저에 중앙화시켜두면 모두가 일관된 로직을 재사용할 수 있습니다.
1.3.3 보안
- 사용자에게 테이블에 대한 직접적인 수정 권한을 주지 않고, 특정 프로시저를 실행할 수 있는
EXECUTE권한만 부여할 수 있습니다. - 이를 통해 미리 정의된 안전한 방식으로만 데이터를 변경하도록 강제할 수 있습니다.
저장 프로그램의 강력함
데이터베이스에 로직을 저장하는 기능들은 그 자체로 매우 강력하며, 분명한 장점들을 가지고 있습니다.
실무 이야기
최근 실무에서는 이런 기능들을 예전처럼 잘 사용하지 않습니다. 그 이유는 이 문서의 뒷부분에서 자세히 설명하겠습니다.
이런 이유로 여기서는 프로시저, 함수, 트리거에 대해서 깊이있게 이해하기 보다는 "아, 이런 개념이고 이런 방식으로 사 용하는구나" 정도로 가볍게 살펴보고 넘어가겠습니다.
만약 실무에서 필요하게 되면 그때 관련 내용을 따로 학습하는 것을 추천합니다.
2. 저장 프로시저 (Stored Procedure)
- 특정 고객의 주소를 변경하고, 그 변경 이력을
logs라는 별도의 테이블에 기록하는 두 가지 작업을 하나의 프로시저로 묶어보겠습니다.
2.1 준비: logs 테이블 생성
DROP TABLE IF EXISTS logs;
CREATE TABLE logs (
id INT AUTO_INCREMENT PRIMARY KEY,
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
2.2 프로시저 생성 (CREATE PROCEDURE)
구분자(Delimiter) 변경
MySQL 클라이언트에서 여러 줄의 명령어로 이루어진 프로시저를 만들 때는, 명령어의 끝을 알리는 구분자(delimiter)를 세미콜론(;)이 아닌 다른 기호(예: // 또는 $$)로 잠시 변경해야 합니다.
프로시저의 BEGIN ... END 블록 안에 여러 개의 SQL 문이 세미콜론으로 끝나기 때문입니다.
-- 구분자를 // 로 변경
DELIMITER //
CREATE PROCEDURE sp_change_user_address(
IN user_id_param INT,
IN new_address_param VARCHAR(255)
)
BEGIN
-- 1. users 테이블의 주소 업데이트
UPDATE users
SET address = new_address_param
WHERE user_id = user_id_param;
-- 2. logs 테이블에 변경 이력 기록
INSERT INTO logs (description)
VALUES (CONCAT('User ID ', user_id_param, ' 주소 변경 ', new_address_param));
END //
-- 구분자를 다시 ; 로 원상 복구
DELIMITER ;
sp_change_user_address프로시저는 두 개의 **입력 파라미터(IN Parameter)**를 받습니다.user_id_param: 주소를 변경할 사용자의 ID입니다.new_address_param: 새로 변경할 주소 값입니다.
- 프로시저의 본문(
BEGIN ... END)에서는 두 가지 SQL 문이 순차적으로 실행됩니다.
UPDATE users SET address = new_address_param WHERE user_id = user_id_param;users테이블에서user_id_param에 해당하는 사용자의 주소(address)를new_address_param값으로 변경합니다.
INSERT INTO logs (description) VALUES (CONCAT('User ID ...'));logs테이블에 주소 변경 이력을 삽입합니다.CONCAT함수를 사용하여 로그 메시지를 동적으로 생성합니다.- 이 과정은
UPDATE가 성공적으로 수행된 후에만 실행됩니다.
저장 프로시저의 핵심
저장 프로시저는 여러 SQL 문을 하나의 논리적인 단위로 묶어 처리할 수 있게 해줍니다.
2.3 프로시저 호출 (CALL)
- 이제
user_id가 2번인 '네이트' 고객의 주소를 '경기도 성남시2'로 변경하는 프로시저를 호출해 보겠습니다.
CALL sp_change_user_address(2, '경기도 성남시2');
2.4 결과 확인
users테이블과logs테이블을 조회하여 두 작업이 모두 잘 처리되었는지 확인합니다.
SELECT address FROM users WHERE user_id = 2;
실행 결과
| address |
|---|
| 경기도 성남시2 |
SELECT * FROM logs;
실행 결과
| id | description | created_at |
|---|---|---|
| 1 | User ID 2 주소 변경 경기도 성남시2 | ... |
2.5 프로시저 삭제 (DROP PROCEDURE)
DROP PROCEDURE IF EXISTS sp_change_user_address;
3. 저장 함수 (Stored Function)
- 상품의 정가(
price)와 할인율(discount_rate)을 받아, 최종 판매가를 계산해서 반환하는 함수fn_get_final_price를 만들어보겠습니다.
3.1 준비: stored_items 테이블 생성
DROP TABLE IF EXISTS stored_items;
CREATE TABLE stored_items (
item_id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price INT NOT NULL,
discount_rate DECIMAL(5, 2)
);
INSERT INTO stored_items (name, price, discount_rate) VALUES
('고성능 노트북', 1500000, 10.00),
('무선 마우스', 25000, 20.00),
('기계식 키보드', 120000, 30.00),
('4K 모니터', 450000, 40.00),
('전동 높이조절 책상', 800000, 50.00);
3.2 함수 생성 (CREATE FUNCTION)
- 함수는 프로시저와 달리 반드시
RETURNS로 반환 타입을 명시해야 합니다.
DELIMITER //
CREATE FUNCTION fn_get_final_price(
price_param INT,
discount_rate_param DECIMAL(5, 2)
)
RETURNS DECIMAL(10, 2)
DETERMINISTIC
BEGIN
-- 최종 가격을 계산 (가격 * (1 - 할인율/100))
RETURN price_param * (1 - discount_rate_param / 100);
END //
DELIMITER ;
fn_get_final_price함수는 두 개의 입력 파라미터(price_param,discount_rate_param)를 받아, 최종 판매가를 계산하여 반환합니다.RETURNS DECIMAL(10, 2): 함수가DECIMAL(10, 2)타입의 값을 반환할 것임을 명시합니다. 함수는 반드시 반환 타입을 지정해야 합니다.DETERMINISTIC: 이 함수가 동일한 입력에 대해 항상 동일한 결과를 반환한다는 것을 MySQL에게 알려주는 키워드입니다.- 예를 들어, 2 + 3은 항상 5인 것처럼, 함수의 결과가 입력 값에만 의존하고 외부 요인(현재 시간, 랜덤 값 등)에 영향을 받지 않을 때 사용합니다.
- MySQL은 이 정보를 활용하여 쿼리 최적화에 도움을 받을 수 있습니다.
RETURN price_param * (1 - discount_rate_param / 100);: 계산된 최종 가격을 반환합니다.
저장 함수의 특징
함수는 특정 계산을 수행하고 하나의 결과 값을 반환하는 데 특화되어 있으며, SELECT 문 등 SQL 쿼리 내에서 마치 내장 함수처럼 활용될 수 있다는 것이 가장 큰 특징입니다.