문서 읽는 데 39분 · D7

D-7: DCL & 절차형 SQL — 권한·뷰·시퀀스로 데이터를 "운영"하기

목차 21
전체 24강 중 19강 · 데이터베이스
난이도 · 입문

ℹ️Oracle SQL로 SQLD 자격증을 대비하며 실전 SQL을 쌓는 트랙이에요. 프로그래밍 경험이 없어도 시작할 수 있고, 부트캠프에서는 자바 기초 다음·스프링 부트 직전에 배치하길 권해요.

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 데이터를 가로로 눕히고(PIVOT), 여러 줄을 한 줄로 모으고(LISTAGG), 정렬한 결과의 위만 떼어 내는(Top-N) 세 기법으로 인기글 리포트를 뽑았어요. 분석 쿼리의 큰 줄기를 거의 다 잡은 셈이죠.

오늘은 한 발 물러서요. 지금까지는 데이터를 "조회하고 분석" 했다면, 오늘부터는 그 데이터를 "안전하고 편하게 관리" 하는 쪽으로 무대를 옮겨요. 분석가에서 운영자로 시선을 옮기는 거예요.

누가 어떤 데이터를 보거나 고칠 수 있는지 정하는 권한(GRANT·REVOKE), 복잡한 조회를 한 번 정의해 두고 이름만으로 부르는 뷰, post_id 같은 번호를 자동으로 매겨 주는 시퀀스, 그리고 여러 단계를 하나로 묶는 프로시저 개념까지 — 운영의 도구를 하나씩 익혀요.

텍스트
 오늘의 여정
   ① 권한이 왜 필요할까 — DCL 첫 만남
   ② 권한 주기 — GRANT 와 역할(ROLE)
   ③ 권한 회수 — REVOKE 와 WITH GRANT OPTION
   ④ 복잡한 조회를 이름으로 — 뷰(VIEW)
   ⑤ 뷰로 보안까지 — 컬럼 가리기와 갱신 제약
   ⑥ 자동 번호표 — 시퀀스(SEQUENCE)
   ⑦ 여러 단계를 한 번에 — 프로시저·트리거·함수 개념
   ⑧ 운영의 세 기둥 종합

💡 오늘 수업의 핵심 — "권한(GRANT/REVOKE)으로 누가 무엇을 할지 정하고, 뷰로 복잡한 조회를 이름 하나로 다시 부르며, 시퀀스로 번호를 자동으로 매긴다"

🎯 학습 목표

  • GRANT·REVOKE 로 권한을 주고 회수하며, ROLE 로 여러 권한을 묶어서 다룬다. (SQLD 2과목 'DCL', ★빈출)
  • 뷰(CREATE VIEW)로 복잡한 조회를 재사용하고, 컬럼을 가려 보안에 쓰며, 갱신 제약을 안다. (SQLD 2과목 'DML')
  • 시퀀스(CREATE SEQUENCE/NEXTVAL/CURRVAL)로 자동 번호를 만들고, 프로시저·트리거·함수의 개념을 구분한다. (SQLD 2과목 '절차형 SQL')

Step 1: "데이터를 안전하게 — 권한 관리의 첫걸음"

C-1 첫 시간에 SQL 명령어를 네 갈래로 나눠 봤던 것 기억하시죠? 표를 만들고 바꾸는 DDL, 데이터를 넣고 빼는 DML, 그 변경을 확정하거나 되돌리는 TCL, 그리고 마지막 하나 — 누가 무엇을 할 수 있는지 정하는 DCL 이었어요. 지금까지 앞의 셋은 실컷 써 왔는데, 오늘 드디어 네 번째 DCL 차례예요.

DCL 은 Data Control Language, 데이터 "제어" 언어예요. 명령은 딱 두 개뿐이에요. 권한을 주는 GRANT, 권한을 거두는 REVOKE.

왜 권한이 필요할까요? 인스타그램 데이터베이스를 떠올려 보세요. 회원 비밀번호, 비공개 계정의 게시물 같은 민감한 데이터가 한곳에 모여 있어요. 분석만 하는 직원에게 회원 정보를 통째로 삭제할 권한까지 주면 큰 사고로 이어질 수 있어요. 그래서 "이 사람은 조회만, 저 사람은 입력까지" 처럼 할 수 있는 일의 범위를 미리 정해 둬요.

텍스트
 권한은 GRANT 로 주고 REVOKE 로 거둔다
   관리자 ─[ GRANT  SELECT ON post ] 사용자 :  post 조회 가능
   관리자 ─[ REVOKE SELECT ON post ] 사용자 :  post 조회 불가

⚠️ DCL 의 명령은 GRANTREVOKE 둘뿐이에요. DDL(CREATE/DROP)·DML(INSERT/UPDATE)과 헷갈리지 않게 "권한을 다루면 DCL" 로 기억하세요. SQLD 1과목에서 명령어 분류를 묻는 문제가 ★빈출이에요.

💡 한 줄 정리 — DCL 은 권한을 다루는 명령으로, 주는 GRANT 와 거두는 REVOKE 둘뿐이다. 누가 어떤 데이터에 무엇을 할 수 있는지 정해 사고를 막는다.

🙋 학생 질문 — "튜터님, 지금까지 권한 없이도 쿼리가 다 됐는데요?"

지금까지 우리는 표를 직접 만든 주인(스키마 소유자)으로 접속해서 실습했어요. 내가 만든 표는 당연히 내가 다 할 수 있으니 권한을 의식할 일이 없었죠. 권한이 문제가 되는 건 "다른 사람" 이 내 표를 건드리려 할 때예요. 회사에선 한 데이터베이스를 여러 사람·여러 프로그램이 함께 쓰니, 각자 할 수 있는 일을 나눠 두는 게 기본이에요.


Step 2: "권한 주기 — GRANT 와 역할(ROLE)" ★빈출

권한을 주는 명령은 형태가 늘 똑같아요. "무슨 권한을, 어느 표에, 누구에게" 세 가지를 끼워 넣어요.

SQL
-- (관리자가 실행) 조회 전용 직원 reader 에게 post 표의 SELECT 권한만 준다
GRANT SELECT ON post TO reader;

이렇게 주면 reader 는 게시물 50건을 조회할 수 있게 돼요. 단, SELECT 만 줬으니 입력·수정·삭제는 못 해요. 줄 수 있는 권한은 이렇게 나뉘어요.

권한 할 수 있는 일
SELECT 조회
INSERT 행 추가
UPDATE 행 수정
DELETE 행 삭제
ALL PRIVILEGES 위 전부

그런데 직원이 여러 명이고, 줘야 할 권한도 여러 개라면 한 명씩 일일이 주기가 번거로워요. 이럴 때 권한을 하나의 "역할(Role)" 에 모아 두고, 그 역할을 사람에게 통째로 줘요. 역할은 권한을 담는 바구니라고 생각하면 돼요.

SQL
-- 1) 역할(권한 묶음)을 만든다
CREATE ROLE read_only;
-- 2) 역할에 권한을 담는다
GRANT SELECT ON member TO read_only;
GRANT SELECT ON post   TO read_only;
-- 3) 역할을 사람에게 통째로 준다
GRANT read_only TO reader;

이제 readerread_only 역할 하나로 회원 12명과 게시물 50건을 모두 조회할 수 있어요. 직원이 새로 와도 권한을 일일이 챙길 필요 없이 역할만 주면 끝이라, 사람이 늘수록 진가가 드러나요.

⚠️ 권한은 두 갈래예요. 특정 표를 다루는 객체 권한(SELECT ON post 등)과, 접속·표 생성 같은 시스템 권한(CREATE SESSION 등)이에요. 둘을 구분하는 문제가 SQLD 에 ★빈출이에요.

💡 한 줄 정리GRANT 권한 ON 객체 TO 대상 으로 권한을 준다. ROLE 은 여러 권한을 묶어 두는 바구니라, 사람이 많을수록 관리가 편해진다.

🙋 학생 질문 — "튜터님, reader 같은 사용자는 어떻게 만드나요?"

사용자를 새로 만드는 건 CREATE USER 명령이고, 이건 보통 데이터베이스 전체를 관리하는 관리자 계정만 할 수 있어요. 표를 가진 우리(스키마 주인)는 "내 표에 대한 권한을 누구에게 줄지" 를 정하는 GRANT·REVOKE 를 맡고요. 그래서 오늘은 사용자가 이미 있다고 보고, 그 사용자에게 권한을 주고 거두는 흐름에 집중해요.


Step 3: "권한 회수하기 — REVOKE 와 WITH GRANT OPTION"

준 권한은 언제든 거둘 수 있어요. GRANTTO 로 줬다면, REVOKEFROM 으로 거둬요.

SQL
-- (관리자가 실행) reader 에게 줬던 post 조회 권한을 거둔다
REVOKE SELECT ON post FROM reader;

권한을 거두자마자 reader 가 게시물을 조회하면 "그런 표가 없다" 며 막혀요. 조금 전까지 잘 보이던 표가, 권한을 잃자 아예 존재하지 않는 것처럼 보이는 거예요. 거꾸로 권한만 다시 주면 표는 원래대로 다시 보여요.

⚠️ 함정: GRANTTO, REVOKEFROM 이에요. 방향을 바꿔 적으면 문법 오류가 나요. SQLD 에서 보기의 TO/FROM 을 슬쩍 바꿔 놓는 ★빈출 함정이에요.

권한을 줄 때 WITH GRANT OPTION 을 붙이면, 받은 사람이 그 권한을 또 다른 사람에게 넘길 수 있어요.

SQL
-- reader 가 받은 권한을 다른 사람에게 다시 넘길 수 있게 허용
GRANT SELECT ON post TO reader WITH GRANT OPTION;

이렇게 주면 reader 가 또 다른 직원에게 같은 SELECT 권한을 넘겨줄 수 있어요. 그리고 관리자가 reader 의 권한을 회수하면, reader 가 넘겨준 권한까지 줄줄이 함께 사라져요. 권한이 사슬처럼 이어지는 거죠.

⚠️ 함정: WITH GRANT OPTION 으로 넘어간 권한은 원 권한을 REVOKE 하면 연쇄로 사라져요. "내가 직접 준 사람만 잃는 게 아니라, 그 사람이 넘긴 사람까지" 라는 점이 SQLD 단골 함정이에요.

💡 한 줄 정리REVOKEFROM 으로 권한을 거둔다. WITH GRANT OPTION 으로 넘어간 권한은 원 권한을 회수하면 연쇄로 함께 사라진다.

🙋 학생 질문 — "튜터님, 권한을 회수하면 그 사람이 넣어 둔 데이터도 사라지나요?"

아니에요. 회수되는 건 "할 수 있는 일(권한)" 뿐이에요. 그 사람이 권한을 가졌을 때 이미 넣어 둔 데이터는 표에 그대로 남아 있어요. 권한과 데이터는 별개라고 생각하면 돼요. 권한은 "문을 열 수 있는 열쇠" 이고, 데이터는 "방 안의 물건" 인 셈이죠. 열쇠를 거둬도 방 안 물건은 그대로예요.


Step 4: "복잡한 조회를 한 번 정의해 두기 — 뷰"

지난 시간 마지막에 인기글 리포트를 만들었죠. 좋아요 Top-3 게시물에 해시태그를 한 줄로 붙이던 그 긴 쿼리요. 그때 한 학생이 물었어요. "이 긴 쿼리를 인기글을 볼 때마다 매번 다시 적어야 하나요?" 오늘 그 답을 드릴게요. 아니에요. 한 번 정의해 두고 이름만으로 다시 부르면 돼요. 그게 뷰(View)예요.

뷰는 "저장된 SELECT 문" 이에요. 자주 쓰는 복잡한 조회에 이름을 붙여 두면, 다음부터는 그 이름을 진짜 표처럼 쓸 수 있어요. 그래서 뷰를 "가상 표" 라고도 불러요. 진짜 데이터를 따로 저장하는 게 아니라, 부를 때마다 안에 적어 둔 SELECT 가 실행돼요.

SQL
-- sql/ddl/05_dcl_views_sequences.sql
CREATE VIEW popular_posts_with_tags AS
SELECT t.rnk, t.post_id, p.caption, t.likes,
       NVL(LISTAGG(h.name, ', ') WITHIN GROUP (ORDER BY h.hashtag_id), '(태그 없음)') AS tags
FROM (SELECT post_id, COUNT(*) AS likes,
             ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC) AS rnk
      FROM post_like GROUP BY post_id) t
JOIN post p          ON p.post_id  = t.post_id
LEFT JOIN post_hashtag ph ON ph.post_id = t.post_id
LEFT JOIN hashtag h       ON h.hashtag_id = ph.hashtag_id
WHERE t.rnk <= 3
GROUP BY t.rnk, t.post_id, p.caption, t.likes;

CREATE VIEW 이름 AS 뒤에 지난 시간의 그 긴 쿼리를 그대로 넣었어요. 이제부터 인기글 리포트는 이렇게 한 줄이면 끝이에요.

SQL
SELECT rnk, post_id, likes, tags FROM popular_posts_with_tags ORDER BY rnk;
rnk post_id likes tags
1 12 6 여행, 제주, 바다
2 1 5 안녕, 일상, 첫게시물
3 40 4 카페, 먹스타그램, 디저트

긴 쿼리는 뷰 안에 숨고, 우리는 popular_posts_with_tags 라는 이름만 기억하면 돼요. 뷰는 진짜 표처럼 WHERE 도 걸 수 있어요. "1위만 보여 줘" 라면 WHERE rnk = 1 을 붙이면 되고요.

뷰가 필요 없어지면 DROP VIEW popular_posts_with_tags; 로 지워요. 이때 사라지는 건 "조회 정의" 뿐이고, 원본 post·post_like·hashtag 표의 데이터는 한 줄도 다치지 않아요. 뷰는 그 위에 얹힌 창문일 뿐이니까요.

⚠️ 뷰는 데이터를 따로 저장하지 않아요. 부를 때마다 안의 SELECT 가 다시 실행돼서, 원본이 바뀌면 뷰 결과도 자동으로 최신이에요. 새 인기글이 생기면 같은 뷰를 불러도 순위가 갱신돼 나와요.

💡 한 줄 정리 — 뷰는 저장된 SELECT(가상 표)다. 복잡한 조회에 이름을 붙여 재사용하고, DROP VIEW 로 지워도 원본 데이터는 그대로다.

🙋 학생 질문 — "튜터님, 뷰랑 결과를 새 표로 복사해 두는 거랑 뭐가 달라요?"

핵심은 "언제 데이터를 보느냐" 예요. 결과를 새 표로 복사해 두면, 그 순간의 데이터가 사진처럼 멈춰 있어요. 원본이 나중에 바뀌어도 복사본은 옛날 그대로죠. 반면 뷰는 부를 때마다 SELECT 를 다시 돌리니 항상 최신이에요. "지금 이 순간의 인기글" 을 보고 싶으면 뷰가 맞고, "특정 날짜의 스냅샷" 을 남겨 두고 싶으면 복사가 맞아요. 이 차이는 오늘 생각해볼 주제로 더 깊게 다뤄요.


Step 5: "뷰로 보안까지 — 컬럼 가리기와 갱신 제약"

뷰의 또 다른 쓸모는 보안이에요. 회원 표에는 자기소개(bio) 같은 정보까지 들어 있는데, 외부에 공개할 명단에는 이름만 보이면 충분하죠. 보여 줄 컬럼만 고른 뷰를 만들어 두면, 그 뷰에만 권한을 주고 원본 표는 감출 수 있어요.

SQL
-- sql/ddl/05_dcl_views_sequences.sql
CREATE VIEW member_public AS
SELECT member_id, username, nickname
FROM member;

member_public 은 회원 12명을 그대로 담되, 컬럼은 세 개(member_id·username·nickname)뿐이에요. bio 는 뷰에 아예 없으니, 이 뷰로는 절대 보이지 않아요. 여기에 조회 권한을 주면 이렇게 돼요.

SQL
-- 공개 명단 뷰만 조회 권한을 준다 (원본 member 표는 못 봄)
GRANT SELECT ON member_public TO reader;

readermember_public 으로 회원 이름은 보지만, 자기소개가 담긴 원본 member 표는 손도 못 대요. 민감한 컬럼을 뷰로 가린 거예요. 뷰에 준 권한과 원본 표 권한은 별개라, 딱 필요한 만큼만 열어 줄 수 있어요.

그런데 뷰에는 조심할 점이 하나 있어요. 단순히 한 표에서 컬럼만 추린 뷰는 INSERT·UPDATE 도 통하지만, 집계·조인·DISTINCT 가 들어간 뷰는 입력·수정이 막혀요.

SQL
-- 집계·조인이 들어간 인기글 뷰에 INSERT 시도 → 막힌다
INSERT INTO popular_posts_with_tags (post_id) VALUES (999);
-- ORA-01779: cannot modify a column which maps to a non key-preserved table

인기글 뷰는 여러 표를 조인하고 집계해서 만든 거라, 결과 한 줄이 원본 어느 표의 어느 행에 대응하는지 분명하지 않아요. 그래서 입력·수정이 막혀요. 뷰는 "조회" 가 본업이라고 생각하면 편해요.

뷰의 정의를 바꾸고 싶을 땐 CREATE OR REPLACE VIEW 로 통째로 새로 써요. DROP 후 다시 CREATE 하면 줬던 권한이 사라지지만, CREATE OR REPLACE 는 권한을 유지한 채 안의 SELECT 만 갈아끼워 줘요.

⚠️ 함정: 집계·조인·DISTINCT 가 들어간 복합 뷰는 INSERT·UPDATE 가 막혀요. "뷰면 무조건 수정도 된다" 가 아니라 "단순 뷰만 수정된다" 가 SQLD 포인트예요.

💡 한 줄 정리 — 뷰로 보여 줄 컬럼만 골라 보안에 쓴다. 단순 뷰는 수정도 되지만, 집계·조인 뷰는 조회 전용이다.

🙋 학생 질문 — "튜터님, 그럼 뷰는 항상 못 고치는 건가요?"

아니에요. 한 표에서 컬럼만 추린 단순한 뷰(예: 방금 만든 member_public)는 INSERT·UPDATE 도 돼요. 막히는 건 집계(GROUP BY·COUNT)나 조인이 낀 복합 뷰예요. 결과 한 줄이 원본 표의 정확히 한 행과 1:1 로 이어질 때만 수정이 허용된다고 보면 돼요. 인기글 뷰는 여러 줄을 묶고 합쳐서 한 줄을 만들기 때문에 그 1:1 이 깨져 막히는 거예요.


Step 6: "자동 번호표 — 시퀀스"

C-1 에서 회원과 게시물을 만들 때, member_idpost_id 에 1, 2, 3… 하고 번호를 우리가 직접 적어 넣었어요. 그런데 실제 서비스에선 새 게시물이 1초에도 수십 개씩 올라와요. 그때마다 "다음 번호가 몇이더라?" 하고 사람이 셀 수는 없죠. 이 번호를 자동으로 매겨 주는 장치가 시퀀스(Sequence)예요. 은행 창구의 번호표 발급기를 떠올리면 딱 맞아요.

SQL
-- sql/ddl/05_dcl_views_sequences.sql
CREATE SEQUENCE post_id_seq
    START WITH 51
    INCREMENT BY 1
    NOCACHE
    NOCYCLE;

게시물이 이미 post_id 1~50까지 차 있으니, 다음 번호인 51부터 시작(START WITH 51)하고 1씩 늘어나게(INCREMENT BY 1) 만들었어요. 번호를 뽑는 명령은 두 가지예요.

SQL
SELECT post_id_seq.NEXTVAL FROM dual;   -- 51 (다음 번호를 뽑고 +1)
SELECT post_id_seq.CURRVAL FROM dual;   -- 51 (방금 뽑은 번호를 다시 확인, 증가 안 함)
  • NEXTVAL — 다음 번호를 하나 뽑아요. 부를 때마다 1씩 올라가요.
  • CURRVAL — 방금 뽑은 번호가 몇이었는지 다시 확인해요. 번호를 새로 뽑진 않아요.

실전에선 INSERT 할 때 PK 값으로 NEXTVAL 을 넣어요.

SQL
INSERT INTO post (post_id, member_id, caption)
VALUES (post_id_seq.NEXTVAL, 1, '시퀀스로 자동 채번된 새 게시물');

이렇게 하면 post_id 에 51이 자동으로 들어가고, 다음 INSERT 는 52를 받아요. 번호 충돌을 사람이 신경 쓸 필요가 없어진 거예요. 이 시점에 새 게시물이 51번을 받아 실제로 들어가는 걸 확인했어요.

⚠️ 함정: 접속을 새로 한 직후 CURRVAL 부터 부르면 "아직 이 세션에서 번호를 뽑은 적이 없다" 며 막혀요(ORA-08002). CURRVAL 은 "방금 뽑은 값" 이라, 그 세션에서 NEXTVAL 을 최소 한 번은 부른 뒤에야 쓸 수 있어요. ★빈출 함정이에요.

🌟 NOCYCLE(기본값)은 번호가 최대값에 닿아도 1로 되돌아가지 않게 해요. CYCLE 을 주면 되돌아가는데, PK 처럼 겹치면 안 되는 번호엔 절대 쓰면 안 돼요. 같은 번호가 두 번 나오면 PK 충돌이 나니까요.

💡 한 줄 정리 — 시퀀스는 자동 번호 발급기다. NEXTVAL 로 다음 번호를 뽑고(+1), CURRVAL 로 방금 값을 확인한다. CURRVALNEXTVAL 뒤에만 쓸 수 있다.

🙋 학생 질문 — "튜터님, 시퀀스를 1부터 다시 시작하려면요?"

가장 깔끔한 건 시퀀스를 지웠다(DROP SEQUENCE) 원하는 시작값으로 다시 만드는 거예요. ALTER SEQUENCE 로 증가폭 같은 옵션을 바꿀 수도 있고요. 다만 한 번 뽑은 번호는 되돌릴 수 없다는 점은 기억하세요. 트랜잭션을 ROLLBACK 해도 이미 올라간 시퀀스 번호는 그대로라, 중간중간 번호가 비는(건너뛰는) 일이 생길 수 있어요. PK 는 "겹치지만 않으면 된다" 라서 번호가 좀 비어도 문제는 없어요.


Step 7: "여러 단계를 한 번에 — 프로시저·트리거·함수 개념"

지금까지 우리는 SQL 한 문장씩 따로따로 실행했어요. 그런데 "좋아요가 눌리면, 좋아요 표에 한 줄 넣고, 게시물의 좋아요 수도 1 올린다" 처럼 여러 단계가 늘 함께 일어나야 하는 일도 있어요. 이런 여러 단계를 하나로 묶어 두는 도구가 절차형 SQL 이에요. 종류는 셋이에요.

이번 Step 은 문법을 외우기보다 "셋이 무엇이고 어떻게 다른지" 만 또렷이 구분하면 충분해요. 깊은 작성법은 이 강의 범위를 넘어서요.

  • 프로시저(Procedure) — 여러 SQL 단계를 한 이름으로 묶어 두고, 그 이름을 부르면 단계들이 줄줄이 실행돼요. 값을 돌려주진 않아요. "좋아요 처리" 같은 작업 묶음이죠.
  • 함수(Function, 사용자 정의 함수) — 프로시저처럼 묶되, 끝에 값 하나를 돌려줘요. 값을 돌려주니 SELECT 절 안에서 우리가 써 온 LENGTH·ROUND 처럼 쓸 수 있어요.
  • 트리거(Trigger) — 우리가 직접 부르지 않아도, 특정 사건(INSERT·UPDATE·DELETE)이 일어나면 자동으로 실행돼요.
텍스트
 트리거 — 사건이 일어나면 자동 실행
   post_like 에 INSERT  ──  (트리거 자동 발동)  ──  post 의 좋아요 수 + 1
   사람이 따로 부르지 않아도 데이터베이스가 알아서 실행한다

비유하자면 프로시저는 "여러 일을 순서대로 적어 둔 체크리스트", 함수는 "재료를 넣으면 결과값 하나를 내주는 자판기", 트리거는 "문이 열리면 저절로 켜지는 센서등" 같아요.

⚠️ 함정: 프로시저와 함수의 가장 큰 차이는 "값을 돌려주느냐" 예요. 함수는 값을 돌려주니 SELECT 안에서 쓸 수 있고, 프로시저는 값을 돌려주지 않아 따로 호출해요. 둘을 바꿔 적은 보기가 SQLD ★빈출이에요.

💡 한 줄 정리 — 프로시저는 여러 단계를 묶고(값 없음), 함수는 묶되 값 하나를 돌려주며, 트리거는 사건이 나면 자동으로 실행된다.

🙋 학생 질문 — "튜터님, 이런 걸 실무에서 많이 쓰나요?"

트리거·프로시저는 강력하지만 호불호가 갈려요. 규칙이 데이터베이스 안에 숨어 있어서, 나중에 "왜 좋아요 수가 저절로 바뀌지?" 하고 원인을 찾기 어려울 수 있거든요. 그래서 요즘은 핵심 규칙을 데이터베이스 밖(프로그램 코드)에 두는 흐름도 많아요. 어디에 두는 게 좋은지는 정답이 하나가 아니라, 오늘 생각해볼 주제로 같이 고민해 봐요. SQLD 에선 "각각이 무엇인지" 까지가 출제 범위라, 개념만 또렷이 잡으면 충분해요.


Step 8: "운영의 세 기둥 종합 — 권한·뷰·시퀀스"

오늘 배운 셋을 한 흐름으로 엮어 볼게요. 운영자가 인기글 화면을 새로 연다고 상상해 봐요.

먼저 인기글 리포트는 매번 길게 짜지 않고, Step 4에서 만든 뷰를 불러요.

SQL
SELECT * FROM popular_posts_with_tags ORDER BY rnk;

한 줄로 3건(post_id 12·1·40)이 태그까지 붙어 나와요. 다음으로, 이 화면을 보는 직원에겐 뷰 조회 권한만 줘요.

SQL
GRANT SELECT ON popular_posts_with_tags TO reader;

직원은 인기글은 보지만 원본 표는 건드리지 못해요. 마지막으로, 새 게시물이 올라오면 post_id 는 시퀀스가 자동으로 매겨요.

SQL
INSERT INTO post (post_id, member_id, caption)
VALUES (post_id_seq.NEXTVAL, 1, '오늘 올라온 새 게시물');

세 도구가 각자 한 축을 맡아요. 뷰는 복잡한 조회를 이름 하나로, 권한은 누가 무엇을 할지, 시퀀스는 번호를 자동으로. 조회·분석만 하던 데서, 데이터를 안전하고 편하게 굴리는 운영의 영역으로 한 걸음 들어온 거예요.

💡 한 줄 정리 — 뷰(복잡 조회 재사용)·권한(접근 통제)·시퀀스(자동 번호)가 운영의 세 기둥이다. 셋을 엮으면 안전하고 편한 데이터 운영이 된다.

🙋 학생 질문 — "튜터님, 뷰에만 권한을 주면 원본 표는 정말 안 보이나요?"

네, 별개예요. 뷰에 SELECT 권한을 줘도 원본 표의 권한은 따로 주지 않는 한 그대로 닫혀 있어요. 그래서 "인기글 화면은 보여 주되, 원본 게시물 표 전체는 막고 싶다" 같은 요구를 뷰 하나로 깔끔하게 풀 수 있어요. 보여 줄 만큼만 창문(뷰)을 내주고, 방(원본 표)의 문은 잠가 두는 셈이죠.


마무리

오늘은 조회·분석에서 한 걸음 나아가, 데이터를 안전하고 편하게 다루는 운영의 도구를 배웠어요. 권한으로 접근을 나누고, 뷰로 복잡한 조회를 이름 하나에 담고, 시퀀스로 번호를 자동으로 매겼어요. 절차형 SQL 의 세 종류도 개념으로 구분했고요.

오늘 배운 핵심 세 가지

  • 💡 권한(DCL)GRANT ... TO 로 주고 REVOKE ... FROM 으로 거둔다. ROLE 로 권한을 묶고, WITH GRANT OPTION 으로 넘긴 권한은 연쇄로 회수된다.
  • 💡 뷰(VIEW) — 저장된 SELECT(가상 표). 복잡한 조회를 재사용하고 컬럼을 가려 보안에 쓴다. 집계·조인 뷰는 조회 전용이다.
  • 💡 시퀀스(SEQUENCE) — 자동 번호 발급기. NEXTVAL 로 다음 번호(+1), CURRVAL 로 방금 값. CURRVALNEXTVAL 뒤에만 쓸 수 있다.

다음 시간 예고

지금까지 우리가 짠 SQL 은 모두 Oracle 문법이었어요. 그런데 같은 일을 다른 데이터베이스에서는 조금씩 다르게 적어요. 예를 들어 Oracle 의 DECODE 를 표준 SQL 에선 CASE 로, NVLCOALESCE 로 쓰죠. 다음 시간엔 지금까지 배운 SQL 을 표준(ANSI) 문법과 나란히 놓고 비교하면서, 어느 데이터베이스에서도 통하는 SQL 감각을 길러요.


과제

오늘 배운 권한·뷰·시퀀스를 직접 다뤄 보는 과제예요. 명령을 실행하기 전에 "결과가 어떻게 나올지" 를 먼저 머릿속으로 그려 본 뒤, 실행해서 맞는지 확인해 보세요.

[기초] 권한을 주고 거둬 보기

권한을 역할로 묶어 주고, 일부만 회수해 보세요. (가) memberpostSELECT 권한을 담은 read_only 역할을 만들고, 한 사용자에게 부여하세요. (나) 그 사용자가 게시물을 수정할 수 있도록 postUPDATE 권한을 따로 더 주세요. (다) 다시 그 UPDATE 권한만 회수했을 때, SELECT 권한은 어떻게 되는지 확인하고 그 이유를 한 줄로 설명해 보세요.

[응용] 회원별 게시물 수 뷰 만들기

회원마다 게시물을 몇 개 썼는지 보여 주는 뷰를 만들어 보세요. (가) memberpost 를 이어 회원별 게시물 수를 담은 member_post_count 뷰를 만드세요. 게시물이 한 개도 없는 회원도 빠지지 않게 하는 게 핵심이에요. (나) 게시물이 0개인 회원이 누구인지 뷰를 조회해 확인하세요. (다) 이 뷰에 새 행을 INSERT 하려고 하면 어떻게 되는지 해 보고, 왜 그런지 설명해 보세요.

[심화] 시퀀스로 새 회원 자동 채번하기

새 회원의 member_id 를 시퀀스로 자동으로 매겨 보세요. (가) 기존 회원이 1~12번이니, 13부터 시작하는 member_id_seq 시퀀스를 만드세요. (나) NEXTVAL 로 새 회원 한 명을 INSERT 하고, member_id 가 13으로 들어갔는지 확인하세요. (다) 접속을 새로 한 직후 NEXTVAL 없이 CURRVAL 부터 부르면 어떻게 되는지 확인하고, 그 이유를 설명해 보세요.


생각해볼 주제

1. 뷰는 부를 때마다 다시 계산하는데, 왜 쓸까

뷰는 데이터를 따로 저장하지 않고, 부를 때마다 안의 SELECT 가 다시 실행돼요. 인기글처럼 무거운 쿼리라면 매번 다시 계산하는 게 부담일 수도 있어요. 그런데도 결과를 새 표로 복사해 두지 않고 뷰를 쓰는 이유는 무엇일까요? "복사본은 그 순간에 멈춘 데이터" 라는 점과 함께 견줘 보세요.

2. 권한을 사람마다 줄까, 역할로 묶을까

직원이 세 명일 땐 한 명씩 권한을 줘도 괜찮아요. 그런데 직원이 수십 명으로 늘고, 줘야 할 권한도 여러 개라면 어떨까요? 새 직원이 올 때마다 권한을 일일이 챙기는 것과, 역할(ROLE)에 묶어 두고 역할만 주는 것 중 어느 쪽이 관리하기 편할지, "권한 정책이 바뀌는 순간" 까지 그려 보세요.

3. 자동 반응(트리거)을 데이터베이스에 둘까, 프로그램에 둘까

"좋아요가 눌리면 게시물의 좋아요 수를 자동으로 1 올린다" 는 규칙을, 데이터베이스 트리거로 둘 수도 있고 프로그램 코드로 둘 수도 있어요. 트리거에 두면 어떤 경로로 데이터가 들어와도 빠짐없이 실행되는 대신, 그 규칙이 데이터베이스 안에 숨어 눈에 잘 안 띄어요. 둘 중 어디에 두는 게 좋을지, 나중에 그 규칙을 고치거나 원인을 추적해야 하는 상황까지 떠올려 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다 "이건 권한(DCL)인가 뷰인가 시퀀스인가", "권한은 TO 로 주고 FROM 으로 거두는가", "이 뷰는 수정이 되는가 안 되는가" 를 떠올리는 감각을 기르는 게 목표예요.


🎯 [과제 1 예시답안] 권한을 주고 거둬 보기

채점 포인트

항목 배점 핵심
(가) read_only 역할 생성·부여 40% CREATE ROLE + GRANT SELECT + GRANT 역할 TO 사용자
(나) UPDATE 권한 추가 25% GRANT UPDATE ON post TO ...
(다) UPDATE 만 회수·SELECT 유지 설명 35% 권한은 하나씩 따로 관리됨

풀이 예시

(가) read_only 역할로 조회 권한 묶어 주기

권한을 사람에게 직접 주는 대신, 역할에 담아 한 번에 줘요. memberpost 의 조회 권한을 read_only 역할에 담고, 사용자에게 역할을 부여해요.

SQL
CREATE ROLE read_only;
GRANT SELECT ON member TO read_only;
GRANT SELECT ON post   TO read_only;
GRANT read_only TO reader;        -- 사용자에게 역할 통째로 부여

이제 reader 는 회원 12명과 게시물 50건을 조회할 수 있어요.

(나) 게시물 수정 권한 따로 더 주기

조회만으로는 게시물을 고칠 수 없으니, UPDATE 권한을 따로 줘요. 역할과 별개로 사용자에게 바로 줄 수도 있어요.

SQL
GRANT UPDATE ON post TO reader;

이제 reader 는 게시물을 조회도 하고 수정도 할 수 있어요.

(다) UPDATE 권한만 회수하기

SQL
REVOKE UPDATE ON post FROM reader;

UPDATE 만 거뒀더니, reader 가 게시물을 수정하려 하면 "권한이 없다" 며 막혀요. 하지만 조회는 여전히 잘 돼서 게시물 50건이 그대로 보여요. 권한은 SELECT·UPDATE 가 각각 따로 관리되기 때문에, 하나를 거둬도 나머지는 그대로 남아요.

💡 튜터의 한마디 — 권한은 "한 덩어리" 가 아니라 종류별로 따로 켜고 끄는 스위치예요. SELECT·INSERT·UPDATE·DELETE 를 각각 주고 거둘 수 있죠. 그래서 "조회는 되는데 수정은 막힌" 상태를 정확히 만들 수 있어요. 딱 필요한 권한만 주는 이 습관이 사고를 막는 첫걸음이에요.


🎯 [과제 2 예시답안] 회원별 게시물 수 뷰 만들기

채점 포인트

항목 배점 핵심
(가) 회원별 게시물 수 뷰 (0개 포함) 45% member LEFT JOIN post · GROUP BY · COUNT(post_id)
(나) 게시물 0개 회원 확인 25% post_cnt = 0 → 3명
(다) 집계 뷰 INSERT 불가 설명 30% 집계·조인 뷰는 조회 전용

풀이 예시

(가) 회원별 게시물 수 뷰

memberpost 를 잇되, 게시물이 0개인 회원도 빠지지 않게 LEFT JOIN 을 써요. 0개 회원을 0으로 세려면 COUNT(*) 가 아니라 COUNT(post_id)(게시물 쪽 컬럼)를 세야 해요. C-4 에서 다룬 "COUNT(*)COUNT(컬럼) 의 차이" 가 여기서 살아나요.

SQL
CREATE VIEW member_post_count AS
SELECT m.member_id, m.username, COUNT(p.post_id) AS post_cnt
FROM member m
LEFT JOIN post p ON p.member_id = m.member_id
GROUP BY m.member_id, m.username;

뷰를 조회하면 회원 12명이 모두 나오고, 각자 쓴 게시물 수가 붙어요.

(나) 게시물이 0개인 회원

SQL
SELECT member_id, username, post_cnt
FROM member_post_count
WHERE post_cnt = 0
ORDER BY member_id;
member_id username post_cnt
7 taeyang 0
8 narae 0
10 minsu 0

세 명이 게시물 0개로 나와요. INNER JOIN 을 썼다면 이 세 명은 결과에서 통째로 사라졌을 텐데, LEFT JOIN 덕분에 0으로 남았어요.

(다) 집계 뷰에 INSERT 시도

SQL
INSERT INTO member_post_count (member_id) VALUES (999);
-- ORA-01779: cannot modify a column which maps to a non key-preserved table

막혀요. 이 뷰는 GROUP BY 로 집계하고 두 표를 조인해서 만든 복합 뷰라, 결과 한 줄이 원본 어느 행과 1:1 로 이어지는지 분명하지 않거든요. 그래서 입력·수정이 허용되지 않아요. 집계·조인 뷰는 "조회 전용" 이라고 기억하면 돼요.

💡 튜터의 한마디 — 이 과제는 오늘의 뷰와 지난 모듈의 LEFT JOIN·집계가 함께 모인 종합 문제예요. "관계를 빠짐없이 보려면 LEFT JOIN, 0을 세려면 COUNT(컬럼), 그걸 재사용하려면 뷰" — 이 흐름이 익숙해지면 실무 리포트 뷰는 어렵지 않아요. 그리고 집계 뷰는 못 고친다는 제약도 함께 잡아 두세요.


🎯 [과제 3 예시답안] 시퀀스로 새 회원 자동 채번하기

채점 포인트

항목 배점 핵심
(가) 13부터 시작하는 시퀀스 30% CREATE SEQUENCE ... START WITH 13
(나) NEXTVAL 로 INSERT → 13 35% PK 값으로 NEXTVAL
(다) 새 세션 CURRVAL 함정 35% NEXTVAL 선행 없으면 ORA-08002

풀이 예시

(가) 13부터 시작하는 시퀀스

기존 회원이 member_id 1~12번까지 있으니, 다음 번호인 13부터 시작하게 만들어요. PK 채번이라 NOCYCLE 로 두고요.

SQL
CREATE SEQUENCE member_id_seq
    START WITH 13
    INCREMENT BY 1
    NOCACHE
    NOCYCLE;

(나) NEXTVAL 로 새 회원 INSERT

PK 값으로 NEXTVAL 을 넣으면 13이 자동으로 들어가요.

SQL
SELECT member_id_seq.NEXTVAL FROM dual;   -- 13
INSERT INTO member (member_id, username, nickname, bio)
VALUES (member_id_seq.CURRVAL, 'newbie', '새내기', '시퀀스로 가입');

NEXTVAL 로 13을 뽑은 뒤, 바로 이어 CURRVAL(방금 뽑은 13)을 INSERT 의 PK 로 넣었어요. 새 회원 newbie(새내기)가 member_id 13번으로 들어가, 회원이 13명이 됐어요. 다음에 또 NEXTVAL 을 부르면 14가 나와요.

(다) 새 세션에서 CURRVAL 부터 부르면

SQL
-- 접속을 새로 한 직후, NEXTVAL 없이
SELECT member_id_seq.CURRVAL FROM dual;
-- ORA-08002: sequence MEMBER_ID_SEQ.CURRVAL is not yet defined in this session

막혀요. CURRVAL 은 "이 세션에서 방금 뽑은 번호" 를 뜻하는데, 새로 접속한 세션에선 아직 NEXTVAL 을 한 번도 부르지 않았으니 "방금 뽑은 값" 이 없거든요. 그래서 CURRVAL 은 같은 세션에서 NEXTVAL 을 최소 한 번 부른 뒤에야 쓸 수 있어요.

💡 튜터의 한마디 — 시퀀스의 두 함정을 같이 기억하세요. 하나는 방금 본 CURRVAL 의 세션 제약이고, 다른 하나는 "뽑은 번호는 ROLLBACK 해도 안 돌아온다" 는 점이에요. 그래서 시퀀스 번호는 중간이 비는(건너뛰는) 일이 생겨요. PK 는 겹치지만 않으면 되니 번호가 좀 비어도 괜찮다는 것까지 알면, 시퀀스는 끝이에요.


🤔 [생각해볼 주제 1] 뷰는 부를 때마다 다시 계산하는데, 왜 쓸까

문제 상황 요약

뷰는 데이터를 따로 저장하지 않고, 부를 때마다 안의 SELECT 가 다시 실행돼요. 인기글처럼 무거운 쿼리라면 매번 다시 계산하는 게 부담일 수도 있죠. 그런데도 결과를 새 표로 복사해 두지 않고 뷰를 쓰는 이유는 무엇일까요?

튜터의 가이드 및 해설

핵심은 "최신성" 이에요. 뷰는 부를 때마다 SELECT 를 다시 돌리니, 원본이 바뀌면 결과도 자동으로 최신이에요. 새 좋아요가 눌리면 인기글 뷰의 순위도 곧바로 갱신돼 나오죠. 반면 결과를 새 표로 복사해 두면, 그 순간의 데이터가 사진처럼 멈춰요. 원본이 나중에 바뀌어도 복사본은 옛날 그대로라, "지금 이 순간" 을 봐야 하는 화면엔 맞지 않아요.

또 뷰는 저장 공간을 거의 안 써요. 정의(SELECT 문)만 보관하지 데이터를 중복으로 쌓지 않거든요. 그래서 "항상 최신이어야 하고, 데이터를 두 벌로 두기 싫을 때" 뷰가 잘 맞아요. 다만 정말 무거운 집계를 수없이 반복 조회한다면, 결과를 미리 계산해 저장해 두는 방식(구체화 뷰)을 쓰기도 해요. "최신성을 택할까, 계산 비용을 아낄까" 의 저울질이에요.

🎯 SQLD는 이렇게 나온다

뷰는 "데이터를 저장하지 않는 가상 테이블이며, 조회할 때마다 정의된 SELECT 가 실행된다" 는 성질이 정오 판단의 단골이에요. 뷰의 정의는 데이터 딕셔너리에 저장된다는 점, 단순 뷰는 갱신이 되지만 집계·조인·DISTINCT 가 들어간 복합 뷰는 갱신이 막힌다는 점이 함께 나와요. "뷰는 실제 데이터를 저장한다" 같은 보기는 틀린 설명이에요.

💡 실무에선

자주, 그리고 무겁게 조회하는 리포트는 결과를 미리 계산해 따로 저장(요약 테이블 등)해 두고 주기적으로 갱신해요. 반대로 최신성이 중요하고 조회가 가끔이면 그냥 뷰로 둬요. 같은 "조회 재사용" 이라도 데이터의 신선도와 계산 비용을 보고 갈라 쓰는 거죠.


🤔 [생각해볼 주제 2] 권한을 사람마다 줄까, 역할로 묶을까

문제 상황 요약

직원이 세 명일 땐 한 명씩 권한을 줘도 괜찮아요. 그런데 직원이 수십 명으로 늘고, 줘야 할 권한도 여러 개라면 어떨까요? 새 직원마다 권한을 일일이 챙기는 것과, 역할에 묶어 두고 역할만 주는 것 중 어느 쪽이 편할까요?

튜터의 가이드 및 해설

소수일 땐 직접 줘도 큰 차이가 없어요. 문제는 규모가 커질 때예요. 직원이 30명이고 줘야 할 권한이 10개라면, 직접 주는 방식은 따져야 할 조합이 수백 개로 불어나요. 새 직원이 올 때마다 그 10개를 빠짐없이 챙겨야 하고, 빠뜨리면 곧 사고죠.

역할(ROLE)로 묶으면 이게 단순해져요. "조회 전용", "운영자" 같은 역할에 권한을 한 번 담아 두고, 새 직원에겐 역할만 주면 끝이에요. 더 큰 이점은 "정책이 바뀔 때" 드러나요. 조회 전용 직원 전원에게 새 표 권한을 추가해야 한다면, 직접 방식은 30명을 일일이 손대야 하지만 역할 방식은 역할 하나만 고치면 전원에게 자동 반영돼요. "사람이 많고 정책이 변한다" 면 역할이 정답이에요.

🎯 SQLD는 이렇게 나온다

ROLE 은 "권한들을 모아 둔 집합이며, 사용자에게 부여할 수 있다" 는 정의가 핵심이에요. 권한을 객체 권한(특정 표의 SELECT 등)과 시스템 권한(CREATE SESSION 등)으로 나누는 문제, 그리고 권한을 다시 넘길 수 있게 하는 옵션이 객체 권한은 WITH GRANT OPTION, 시스템 권한·역할은 WITH ADMIN OPTION 으로 갈린다는 점이 함정으로 나와요.

💡 실무에선

직무별로 역할을 설계해 두고(읽기 전용·운영·관리자 등), 사람은 역할에 배정해요. "이 사람에게 무슨 권한이 있나" 가 아니라 "이 사람은 무슨 역할인가" 로 관리하면 입·퇴사와 인사이동을 훨씬 깔끔하게 처리할 수 있어요. 바탕에는 "딱 필요한 만큼만 준다" 는 최소 권한 원칙이 깔려 있고요.


🤔 [생각해볼 주제 3] 자동 반응(트리거)을 데이터베이스에 둘까, 프로그램에 둘까

문제 상황 요약

"좋아요가 눌리면 게시물의 좋아요 수를 자동으로 1 올린다" 는 규칙을, 데이터베이스 트리거로 둘 수도 있고 프로그램 코드로 둘 수도 있어요. 어디에 두는 게 좋을까요?

튜터의 가이드 및 해설

트리거의 장점은 "빠짐없음" 이에요. 어떤 경로로 데이터가 들어와도 데이터베이스가 알아서 실행하니, 개발자가 깜빡해도 규칙이 지켜져요. 단점은 "숨어 있음" 이에요. 규칙이 데이터베이스 안에 들어가 있어, 나중에 "왜 좋아요 수가 저절로 바뀌지?" 하고 원인을 찾을 때 트리거의 존재를 모르면 한참을 헤매요. 추적과 디버깅이 어렵고, 다른 데이터베이스로 옮길 때 손이 많이 가고요.

프로그램 코드에 두면 반대예요. 로직이 코드에 드러나 있어 읽기·테스트가 쉽지만, 좋아요를 넣는 경로가 여러 군데라면 어느 한 곳에서 "+1" 을 빠뜨릴 위험이 있죠. 정답은 하나가 아니에요. 보통은 핵심 비즈니스 규칙을 코드에 두어 눈에 보이게 하고, 빠지면 큰일 나는 정합성 보정이나 감사 기록 같은 건 트리거로 안전망을 까는 식으로 섞어 써요.

🎯 SQLD는 이렇게 나온다

트리거는 "특정 이벤트(INSERT·UPDATE·DELETE)가 일어나면 자동으로 실행되며, 사용자가 직접 호출할 수 없다" 는 점이 핵심이에요. 프로시저·함수와 묶어 "값을 반환하는 것은 함수, 반환하지 않는 것은 프로시저, 이벤트로 자동 실행되는 것은 트리거" 라는 구분을 묻는 문제가 ★빈출이에요. 셋을 바꿔 적은 보기를 골라내면 돼요.

💡 실무에선

핵심 로직은 애플리케이션에 두어 눈에 보이게 하고, 트리거는 감사 로그나 정합성 보정처럼 "조용히 빠짐없이" 돌아야 하는 곳에 제한적으로 써요. 트리거에 무거운 로직을 잔뜩 넣으면 성능과 추적성이 함께 나빠지니, "꼭 자동이어야 하는가" 를 따져 보고 최소한으로 쓰는 게 요령이에요.

전체 목록 데이터베이스