문서 읽는 데 37분 · B5

B-5: 트랜잭션·NULL·관계와 조인의 다리

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

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

안녕하세요, 홍순구 튜터입니다. 지난 시간엔 한 표에 몰려 있던 정보를 정규화로 차근차근 나눠, 종이 위 인스타그램 설계도를 거의 완성했어요. 회원·게시물·좋아요를 따로 떼어 깔끔하게 정리했죠. 오늘은 그 설계도에 마지막 마감칠을 합니다. 표를 안전하게 다루는 약속(트랜잭션), 칸이 비었을 때의 의미(NULL), 그리고 정규화로 흩어 놓은 표들을 다시 잇는 다리(조인)까지요.

세 주제가 따로 노는 것 같지만, 사실 한 줄기예요. 정규화로 데이터를 잘게 나눴기 때문에, 그걸 안전하게 바꾸려면 트랜잭션이 필요하고, 비어 있는 칸을 제대로 보려면 NULL을 알아야 하고, 흩어진 표를 다시 모으려면 조인이 필요하거든요. 오늘로 모델링(B 카테고리)을 마무리하고, 다음 시간부터는 종이 설계를 진짜 SQL로 구현하기 시작해요.

텍스트
 오늘의 여정
   ① 트랜잭션      여러 작업을 하나로  전부 되거나, 전부 안 되거나
   ② ACID 4총사    원자성 · 일관성 · 고립성 · 지속성
   ③ NULL          '빈칸'의 정체 — 0도 빈 글자도 아니다
   ④ NULL 의 함정   알 수 없음(3치 논리) 과 IS NULL
   ⑤ 본질식별자     식별자 4조건 다시 보기 — 존재성과 NULL
   ⑥ 조인의 다리    흩어진 표를 키로 다시 잇기  첫 JOIN 미리보기

💡 오늘 수업의 핵심 — "정규화로 나눈 표를, 안전하게(트랜잭션) 다루고 정확하게(NULL) 읽고 다시 잇는다(조인)"

🎯 학습 목표

  • 트랜잭션이 무엇인지, 그리고 트랜잭션이 지켜야 할 ACID 네 가지 성질을 인스타 시나리오로 이해한다. (SQLD 1과목 단골)
  • NULL이 '빈칸'이 아니라 '알 수 없음'이라는 걸 알고, 비교·연산에서 생기는 함정을 피한다.
  • 정규화로 흩어진 표가 외래키와 조인으로 어떻게 다시 이어지는지, 첫 SELECT 한 줄로 직접 확인한다.

Step 1: "여러 작업을 하나로 묶기 — 트랜잭션이란"

질문 하나로 시작할게요. 인스타에서 게시물을 하나 삭제하면 무슨 일이 일어날까요? 화면에선 게시물 하나가 사라지는 것뿐이지만, 속을 들여다보면 그 게시물에 달렸던 댓글도, 좋아요도 함께 사라져야 해요. 정규화로 회원·게시물·댓글·좋아요를 따로 떼어 놨으니까요. 즉 '게시물 삭제'는 사실 표 여러 개를 건드리는 여러 작업이에요.

여기서 문제가 생겨요. 게시물은 지웠는데 댓글 삭제가 중간에 실패하면? 주인 없는 댓글이 둥둥 떠다녀요. 데이터가 앞뒤가 안 맞는 상태가 되죠. 그래서 이 여러 작업을 떼어낼 수 없는 하나의 단위로 다뤄야 해요. 이렇게 묶은 작업 단위를 트랜잭션(transaction, 거래·처리)이라고 불러요.

텍스트
 게시물 하나 삭제 = 사실은 표 여러 개를 건드리는 여러 작업

   ┌─ 게시물 삭제
   ├─ 그 게시물의 댓글 삭제
   └─ 그 게시물의 좋아요 삭제

   이 셋을 한 트랜잭션으로  전부 되거나, 전부 안 되거나

가장 유명한 비유는 계좌이체예요. A가 B에게 1만 원을 보내면, (1) A 잔액에서 1만 원을 빼고 (2) B 잔액에 1만 원을 더해요. 두 작업이 다 돼야 정상이죠. 만약 (1)만 되고 (2)가 실패하면? 돈이 허공으로 사라져요. 그래서 둘을 한 트랜잭션으로 묶고, "둘 다 성공하면 확정, 하나라도 실패하면 전부 취소"로 다뤄요.

이 '확정'을 COMMIT(커밋), '취소·되돌리기'를 ROLLBACK(롤백)이라고 해요. 지금은 이름만 기억해 두세요. 실제로 어떻게 쓰는지는 SQL을 본격적으로 다루는 다음 카테고리에서 배워요.

💡 한 줄 정리

트랜잭션은 떼어낼 수 없는 하나의 논리적 작업 단위로, 묶인 작업이 전부 확정(COMMIT)되거나 전부 취소(ROLLBACK)된다.

🙋 학생 질문 — "튜터님, 게시물 하나 지우는 게 왜 여러 작업이에요?"

화면에서 보면 게시물 하나가 사라지는 것뿐이라 하나의 작업처럼 느껴지죠. 하지만 우리는 정규화로 댓글과 좋아요를 게시물과 다른 표에 따로 떼어 놨어요. 그래서 게시물을 진짜로 지우려면 게시물 표만이 아니라 댓글 표, 좋아요 표까지 손대야 해요. 보이지 않을 뿐, 뒤에서는 표 여러 개를 건드리는 여러 작업이 동시에 일어나는 거예요. 그 여러 작업을 안전하게 묶어 주는 약속이 트랜잭션입니다.


Step 2: "ACID ① — 원자성과 일관성"

트랜잭션이 제 역할을 하려면 네 가지 성질을 지켜야 해요. 네 성질의 영어 머리글자를 따서 ACID(애시드)라고 불러요. 한 번에 넷을 다 보면 헷갈리니, 오늘은 둘씩 나눠 볼게요. 먼저 원자성과 일관성.

원자성(Atomicity)은 '더 쪼갤 수 없다'는 뜻이에요. 트랜잭션 안의 작업은 전부 반영되거나, 전부 안 되거나 둘 중 하나예요. 중간만 살짝 반영되는 일은 없어요. 계좌이체에서 (1)만 되고 끝나는 사고를 막아 주는 게 바로 원자성이에요. 흔히 'All or Nothing'(전부 아니면 전무)이라고 표현해요.

텍스트
 원자성: 중간은 없다 — 전부(All) 또는 전무(Nothing)

   성공 경로:  (1) OK  (2) OK    COMMIT    둘 다 반영
   실패 경로:  (1) OK  (2) 실패  ROLLBACK  (1)도 취소

   ✗ 막아야 할 것:  (1)만 반영되고 (2)는 안 됨 (돈 증발)

일관성(Consistency)은 트랜잭션 전후로 데이터베이스가 정해 둔 규칙을 늘 만족한다는 뜻이에요. 여기서 규칙이란 우리가 설계에서 걸어 둔 제약이에요. 예를 들어 "게시물의 작성자는 반드시 회원 표에 있는 회원이어야 한다"는 외래키 규칙이 있다면, 트랜잭션이 끝난 뒤에도 이 규칙이 깨져선 안 돼요. 트랜잭션은 데이터를 올바른 상태에서 또 다른 올바른 상태로만 옮겨 놓아야 해요. ★빈출 — ACID 각 글자의 뜻을 짝짓는 문제는 SQLD에 꾸준히 나와요.

💡 한 줄 정리

원자성은 트랜잭션의 작업이 전부 반영되거나 전부 취소되는 성질(All or Nothing)이고, 일관성은 트랜잭션 전후로 데이터가 정해진 규칙을 늘 만족하는 성질이다.

🙋 학생 질문 — "튜터님, 원자성이랑 일관성이 비슷해 보여요. 뭐가 달라요?"

비슷해 보이지만 보는 각도가 달라요. 원자성은 '과정'을 봐요. 작업을 도중에 끊지 않고 전부 하든가 전부 안 하든가, 둘 중 하나로만 끝낸다는 약속이죠. 일관성은 '결과 상태'를 봐요. 트랜잭션이 끝난 뒤 데이터가 우리가 정한 규칙을 안 깨고 있느냐를 보는 거예요.

원자성을 잘 지키면 일관성도 지키기 쉬워져요. 중간에 끊기지 않으니 어중간하게 규칙이 깨진 상태로 멈출 일이 없으니까요. 하지만 둘이 같은 건 아니에요. '전부 또는 전무'(원자성)와 '규칙이 안 깨짐'(일관성)은 다른 약속이에요.


Step 3: "ACID ② — 고립성과 지속성"

이어서 나머지 둘, 고립성과 지속성을 볼게요.

고립성(Isolation, 격리성)은 여러 트랜잭션이 동시에 돌아도 서로 간섭하지 않는다는 뜻이에요. 각자 혼자 데이터베이스를 쓰는 것처럼 보이게 해 주죠. 인스타로 치면, 두 사람이 같은 게시물에 거의 동시에 좋아요를 눌러도 좋아요 수가 꼬이지 않아야 해요. 한쪽 트랜잭션이 끝나기 전의 어중간한 중간 결과를 다른 쪽이 보고 잘못 계산하면 안 되니까요.

지속성(Durability)은 한번 COMMIT된 트랜잭션은 영구히 보존된다는 뜻이에요. 시스템이 갑자기 꺼지거나 고장 나도, 커밋이 끝난 데이터는 사라지지 않아요. 안전한 저장 공간에 확실히 기록된 뒤에 "확정됐다"고 알려 주거든요. COMMIT을 경계로 '이제부터 영구'가 보장되는 것, 이게 지속성의 핵심이에요.

성질 영어 한 줄 뜻 인스타 예
원자성 Atomicity 전부 또는 전무 게시물·댓글·좋아요 삭제가 다 되거나 다 안 되거나
일관성 Consistency 규칙을 늘 만족 작성자는 항상 회원 표에 있는 회원
고립성 Isolation 동시 실행끼리 간섭 차단 동시 좋아요에도 수가 안 꼬임
지속성 Durability 커밋 후 영구 보존 올린 게시물은 서버가 꺼져도 남음

⚠️ 함정 — 고립성을 완벽하게 지킬수록 여러 트랜잭션이 서로 기다려야 해서 동시 처리 속도는 떨어져요. 그래서 실제 데이터베이스는 '어디까지 격리할지'를 몇 단계로 조절할 수 있게 해 둬요(격리 수준). 자세한 단계는 이 과목 범위를 넘으니, 지금은 "고립성과 성능은 맞바꾸는 관계"라는 점만 기억하세요.

💡 한 줄 정리

고립성은 동시에 실행되는 트랜잭션끼리 서로 간섭하지 못하게 하는 성질이고, 지속성은 한번 COMMIT된 결과가 시스템 장애에도 영구히 남는 성질이다.

🙋 학생 질문 — "튜터님, 지속성은 당연한 거 아니에요? 저장하면 남는 거잖아요."

저장만 하면 남는다고 느끼기 쉽지만, COMMIT 전에는 아직 '임시' 상태예요. 작업하다가 ROLLBACK하면 없던 일이 되어야 하니까요. 그래서 트랜잭션이 진행 중인 데이터는 아직 영구히 남는다는 보장이 없어요.

지속성이 보장하는 건 정확히 'COMMIT한 다음'이에요. 커밋이 끝났다고 알려 준 순간부터는, 바로 다음에 정전이 되든 서버가 죽든 그 데이터는 살아남아요. '언제부터 안전한가'의 경계를 COMMIT으로 딱 그어 주는 게 지속성의 진짜 의미예요.


Step 4: "빈칸의 정체 — NULL이란 무엇인가"

이제 트랜잭션을 떠나 다른 주제로 가요. 우리 회원 표를 다시 떠올려 볼게요. B-1에서 자기소개(bio)는 안 써도 된다고 했고(선택 속성), B-2에서 그 칸은 비어 있어도 되는 도메인이라고 했죠. 실제로 우리 데이터에는 자기소개를 안 쓴 회원이 있어요. 박승우·최도연·한나래의 bio가 비어 있죠. 이 '비어 있음'을 NULL(널)이라고 불러요.

NULL은 '값이 아직 없음' 또는 '알 수 없음'이에요. 여기서 꼭 짚어야 할 게 있어요. NULL은 0도 아니고, 빈 문자열('')도 아니에요. 0은 '숫자 영(零)'이라는 값이 분명히 있는 거고, 빈 문자열은 '길이가 0인 글자'라는 값이 있는 거예요. NULL은 아예 값 자체가 없는 상태고요.

텍스트
 같은 '비어 보임'이라도 속은 다르다

   bio = '백엔드 공부 중'    값이 있음
   bio = NULL               값이 아예 없음 (모름 / 안 씀)
   bio = ''                 '길이 0인 글자' 라는 값은 있음

   NULL ≠ 0,   NULL ≠ ''

설문지로 비유하면 딱 맞아요. '나이' 칸에 0이라고 쓰면 '0살'이라는 답을 한 거예요. 그 칸을 그냥 비워 두면 '답을 안 함'(모름)이고요. 둘은 완전히 다른 상태죠. 데이터베이스의 NULL은 이 '답 안 함'에 해당해요.

⚠️ 함정 — NULL을 0이나 빈 문자열과 같다고 착각하는 건 SQLD 단골 함정이에요. "값이 없다(NULL)"와 "값이 0이다", "값이 빈 글자다"는 셋 다 다른 상태라는 걸 분명히 해 두세요.

💡 한 줄 정리

NULL은 '값이 없음 / 알 수 없음'을 뜻하며, 숫자 0이나 빈 문자열('')과는 다른 상태다.

🙋 학생 질문 — "튜터님, 그냥 비어 있는 건데 왜 이렇게 따지나요?"

그냥 빈칸으로 두고 넘어가도 될 것 같지만, NULL은 계산이나 비교에서 보통의 값과 다르게 동작해요. 그래서 정확히 알아 두지 않으면 다음 Step에서 볼 함정에 그대로 빠져요. 예를 들어 "자기소개를 안 쓴 회원을 찾아 줘" 같은 조건을 잘못 쓰면 한 명도 안 걸리는 일이 생겨요.

지금 NULL의 정체를 분명히 해 두면, 다음 시간에 NULL을 다루는 SQL을 배울 때 훨씬 수월해요. 그래서 '빈칸 하나'를 이렇게 진지하게 짚는 거예요.


Step 5: "NULL의 함정 — 알 수 없음(3치 논리)"

NULL이 '알 수 없음'이라는 점이 재미있는 결과를 낳아요. NULL이 끼면 그 결과도 '알 수 없음'이 되거든요.

먼저 연산부터 볼게요. NULL + 100은 얼마일까요? '모르는 값'에 100을 더해도 여전히 모르죠. 그래서 답은 NULL이에요. 더하기든 곱하기든, 연산에 NULL이 한 번 끼면 결과는 NULL로 번져요.

다음은 비교예요. 보통 비교의 결과는 참(TRUE) 아니면 거짓(FALSE), 둘 중 하나죠. 그런데 NULL이 끼면 '알 수 없음(UNKNOWN)'이라는 세 번째 결과가 생겨요. 이렇게 결과가 셋이 되는 걸 3치 논리(three-valued logic)라고 해요.

텍스트
 보통의 비교는 둘:     참(TRUE) / 거짓(FALSE)
 NULL 이 끼면 셋:      참 / 거짓 / 알 수 없음(UNKNOWN)

   나이 = NULL     알 수 없음  아무 행도 안 걸림  (함정)
   나이 IS NULL    NULL 인 행을 정확히 가려냄      (올바른 방법)

   NULL + 100 = NULL    (연산에 NULL 이 끼면 결과도 NULL)

그래서 "자기소개가 비어 있는 회원"을 찾으려고 bio = NULL이라고 쓰면 안 돼요. 등호 비교의 결과가 '알 수 없음'이 되어 한 명도 안 걸리거든요. NULL인지 확인할 때는 등호(=)가 아니라 IS NULL이라는 전용 표현을 써요. ★빈출 — 자세한 사용법은 다음 시간(C-1)에 배우지만, "NULL 비교는 =이 아니라 IS NULL"이라는 한 가지는 지금 새겨 두세요.

이게 설계에도 영향을 줘요. 어떤 칸을 비워도 되게 둘지, 반드시 채우게 막을지를 정해야 하거든요. 반드시 채워야 하는 칸에는 NOT NULL(널 불가) 제약을 걸어요. 다음 Step에서 이 NOT NULL이 식별자와 어떻게 연결되는지 볼게요.

💡 한 줄 정리

NULL이 끼면 연산 결과도 NULL이 되고 비교 결과는 '알 수 없음'이 되므로, NULL 여부는 =이 아니라 IS NULL로 확인해야 한다.

🙋 학생 질문 — "튜터님, 왜 NULL = NULL도 참이 아니에요? 둘 다 비었잖아요."

직관적으로는 '둘 다 비었으니 같다'고 느껴지죠. 그런데 NULL은 '비었다'가 아니라 '모른다'예요. '모르는 값'과 또 다른 '모르는 값'이 서로 같은지는, 둘 다 모르니 알 수가 없어요. 그래서 NULL = NULL의 결과도 참이 아니라 '알 수 없음'이에요.

이 점 때문에 NULL끼리도 등호로 비교하면 원하는 대로 안 걸려요. 두 값이 모두 NULL인지 보고 싶을 때조차 IS NULL을 각각 확인해야 해요. NULL을 '모름'으로 읽는 습관을 들이면 이 함정이 자연스럽게 이해돼요.


Step 6: "본질식별자 다시 보기 — 존재성과 NOT NULL"

NULL을 배웠으니, B-3에서 다룬 식별자 이야기를 한 번 더 정리하면 딱 맞아떨어져요. 주식별자가 갖춰야 할 네 가지 조건, 기억하시죠?

텍스트
 주식별자가 갖춰야 할 네 가지 (B-3)

   유일성   다른 행과 겹치지 않는다
   최소성   꼭 필요한 칸만으로 식별한다
   불변성   값이 잘 바뀌지 않는다
   존재성   항상 값이 있다 — NULL 불가 = NOT NULL

방금 NULL을 배운 지금, 네 번째 존재성이 또렷해져요. 존재성이란 주식별자에는 반드시 값이 있어야 한다는 조건이에요. 누구인지 가리키는 키가 비어 있으면(NULL이면) 그 행을 식별할 수가 없으니까요. 그래서 주식별자는 늘 NOT NULL이어야 해요. Step 5에서 본 NOT NULL 제약이 바로 이 존재성을 지키는 장치예요.

이제 본질식별자인조식별자를 다시 봐요. 본질식별자는 업무에서 자연히 나오는 속성으로 식별하는 거예요(예: 이메일, username). 인조식별자는 식별만을 위해 새로 만든 번호예요(member_id). B-3에서 우리는 인조식별자(member_id)를 주식별자로 골랐죠. 왜였을까요?

본질식별자는 위험 요소가 있어요. 이메일은 사람이 바꿀 수 있고(불변성 위협), 가입 방식에 따라 비어 있을 수도 있어요(존재성 위협). NULL을 배운 지금 보면, 본질식별자가 NULL이 될 수 있다는 점이 인조식별자를 택하는 또 하나의 분명한 이유예요. 안 바뀌고 항상 값이 있는 번호를 주식별자로 삼는 게 안전하니까요.

💡 한 줄 정리

주식별자의 존재성 조건은 'NULL 불가(NOT NULL)'와 같은 말이며, 본질식별자가 NULL·변경의 위험을 가질 수 있어 안전한 인조식별자를 주식별자로 쓴다.

🙋 학생 질문 — "튜터님, username도 유일하고 안 비는데 그냥 그걸 주식별자로 쓰면 안 되나요?"

쓸 수 있어요. 그렇게 자연 속성으로 식별하는 게 본질식별자예요. 다만 두 가지가 걸려요. 첫째, 사람이 username을 바꾸고 싶어질 수 있어요. 주식별자가 바뀌면 그걸 가리키던 다른 표들도 전부 따라 고쳐야 해서 불변성이 흔들려요. 둘째, username은 글자라 길어서, 다른 표에서 이 키를 참조할 때마다 무거워져요.

그래서 실무에선 보통 짧고 안 바뀌는 인조식별자(member_id 같은 번호)를 주식별자로 두고, username에는 '유일해야 한다'는 제약만 따로 걸어요. 둘 다 챙기는 거죠. 식별은 가벼운 번호로, 중복 방지는 username에 직접.


Step 7: "흩어진 표를 키로 다시 잇기 — 관계가 조인으로"

이제 오늘의 마지막 다리예요. 지난 시간 정규화로 한 표에 몰려 있던 정보를 회원·게시물 같은 여러 표로 쪼갰어요. 깔끔해졌지만 숙제가 하나 생겼죠. "이 게시물을 누가 썼나"를 알려면, 게시물 표에 있는 작성자 칸을 들고 회원 표로 찾아가야 해요.

여기서 그 작성자 칸이 외래키(Foreign Key, FK)예요. 외래키는 다른 표의 주식별자를 가리키는 칸이에요. 게시물 표의 작성자 칸은 회원 표의 member_id(주식별자)를 가리키죠.

텍스트
 회원 표 (참조되는 쪽)
   member_id (PK) · username · nickname · bio

 게시물 표 (참조하는 쪽)
   post_id (PK) · member_id (FK) · caption

   게시물 표의 member_id(FK) ── 회원 표의 member_id(PK) 를 가리킴
   두 키를 맞대어 이으면 = 조인(JOIN)

여기서 이름을 하나 정리할게요. 지난 시간 설계도에선 게시물의 작성자를 writer_id라고 불러, 좋아요를 누른 사람(member_id)과 구분했어요. 그런데 실제 우리 데이터의 게시물 표에서는 작성자 칸 이름이 member_id예요. A-1에서 처음 만났던 바로 그 칸이죠. 이름이 writer_idmember_id든, '이 게시물을 쓴 회원을 가리키는 외래키'라는 역할은 똑같아요. (좋아요 표는 다음 카테고리에서 만들 거라, 지금 실제로 이어 볼 수 있는 건 회원·게시물 두 표예요.)

흩어진 두 표를 다시 모으려면 어떻게 할까요? 게시물 표의 member_id와 회원 표의 member_id를 맞대어, 같은 회원끼리 줄을 이어 주면 돼요. 이렇게 키를 맞대어 여러 표를 다시 잇는 것이 조인(JOIN)이에요. 두 명단을 학번으로 맞춰보는 일과 똑같아요. 한쪽 명단의 학번을 다른 명단에서 찾아 같은 줄에 붙이는 거죠.

💡 한 줄 정리

외래키는 다른 표의 주식별자를 가리키는 칸이며, 이 키를 맞대어 흩어진 표들을 다시 잇는 것이 조인(JOIN)이다.

🙋 학생 질문 — "튜터님, 외래키랑 주식별자랑 뭐가 달라요? 둘 다 member_id잖아요."

값은 같은 member_id인데 역할이 달라요. 주식별자는 '내 표 안에서 나를 가리키는 키'예요. 회원 표의 member_id는 그 표에서 회원 한 명을 콕 집는 주식별자죠.

외래키는 '남의 표의 주식별자를 가리키는 칸'이에요. 게시물 표의 member_id는 회원 표의 주식별자를 가리키는 외래키예요. 같은 회원 번호를 담고 있지만, 회원 표에서는 '나를 식별하는 키', 게시물 표에서는 '저쪽 회원을 가리키는 손가락'인 거죠. 이 손가락(외래키)이 있어서 두 표를 조인으로 다시 이을 수 있어요.


Step 8: "첫 조인 SELECT 미리보기 — 게시물과 작성자 잇기"

말로만 하면 감이 안 오니, 실제로 한 번 이어 볼게요. 회원 표와 게시물 표를 member_id로 맞대어, 각 게시물 옆에 그 작성자의 닉네임을 붙여 보는 거예요.

SQL
SELECT m.nickname, p.caption
FROM   post p
JOIN   member m ON p.member_id = m.member_id;

읽는 법은 이래요. 게시물 표(p)와 회원 표(m)를, 게시물의 member_id와 회원의 member_id같은 것끼리(ON 뒤의 조건) 이어 붙여라. 그러면 각 게시물 줄에 그 작성자의 닉네임이 딱 따라붙어요.

텍스트
 회원 표 ⨝ 게시물 표  (member_id 로 맞댐)

   김재훈   post 1, 2, 3
   이민지   post 4, 5, 6
   박승우   post 7, 8
   정하루   post 9, 10
   최도연   post 11
   강지수   post 12
   ----------------------------
   합계 12 행  (게시물 12개 = 결과 12행)

실제로 실행하면 정확히 12행이 돌아와요. 게시물이 12개니 각 게시물마다 한 줄씩, 작성자 닉네임이 붙어서요. 그런데 여기서 눈여겨볼 게 있어요. 회원은 8명인데, 결과에 작성자로 등장하는 건 6명뿐이에요. 게시물이 0개인 윤태양(7번)·한나래(8번)는 어디로 갔을까요?

이 조인은 양쪽에 짝이 있는 줄만 이어 줘요. 이런 조인을 안쪽 조인(INNER)이라고 해요. 게시물이 하나도 없는 회원은 게시물 표에 짝이 없으니, 이을 상대가 없어 결과에서 빠진 거예요. "게시물이 없는 회원도 빠뜨리지 말고 보여 줘"라고 하려면 다른 종류의 조인이 필요한데, 그건 다음에 본격적으로 다뤄요.

⚠️ 함정 — INNER 조인은 양쪽에 짝이 있는 줄만 남겨요. 짝이 없는 줄(게시물 0개 회원)은 조용히 사라지죠. "분명 회원이 8명인데 왜 6명만 나오지?" 하는 함정이 여기서 나와요. C-5에서 짝 없는 쪽도 살리는 OUTER 조인을 배우면 이 함정이 풀려요.

💡 한 줄 정리

두 표를 키로 맞대는 INNER 조인은 양쪽에 짝이 있는 행만 남기므로, 게시물이 0개인 회원은 결과에서 빠진다(짝 없는 쪽까지 보려면 OUTER 조인이 필요).

🙋 학생 질문 — "튜터님, 옛날 코드에서 FROM post, member WHERE ... 처럼 쉼표로 쓴 것도 봤어요."

맞아요, 같은 조인을 쉼표로 쓰는 옛날 방식이에요. FROM post p, member m WHERE p.member_id = m.member_id라고 쓰면 결과는 똑같이 12행이 나와요. 두 표를 키로 맞댄다는 뜻은 같거든요.

다만 요즘은 JOIN ... ON으로 쓰는 걸 권해요. '무슨 표를 잇는지(JOIN)'와 '무슨 조건으로 잇는지(ON)'가 한눈에 또렷하게 갈라져 보이니까요. 쉼표 방식은 표가 여러 개로 늘어나면 조건이 WHERE에 뒤섞여 읽기 어려워져요. 두 방식 모두 다음 시간에 제대로 비교하며 배워요.


마무리

오늘로 B 카테고리, 데이터 모델링을 마무리했어요. 종이 위 인스타그램 설계도가 이제 정말 완성됐죠. 다섯 박스를 잇고, 각 행을 키로 가리키고, 속성을 정규화로 잘 나눠 담고, 마지막으로 그 표들을 안전하게 다루고(트랜잭션) 정확하게 읽고(NULL) 다시 잇는(조인) 법까지 봤으니까요. 오늘 세 주제는 따로인 것 같아도, 결국 "잘게 나눈 표를 어떻게 안전하고 정확하게 다루느냐"라는 한 줄기였어요.

오늘 배운 핵심 세 가지

  • 💡 하나 — 트랜잭션은 떼어낼 수 없는 하나의 작업 단위이고, ACID(원자성·일관성·고립성·지속성) 네 성질로 안전을 보장한다. 원자성은 전부 또는 전무, 일관성은 규칙 유지, 고립성은 동시 실행 격리, 지속성은 커밋 후 영구 보존이다.
  • 💡 — NULL은 '값이 없음 / 알 수 없음'이며 0이나 빈 문자열과 다르다. NULL이 끼면 연산은 NULL, 비교는 '알 수 없음'이 되므로 NULL 여부는 =이 아니라 IS NULL로 확인한다.
  • 💡 — 외래키는 다른 표의 주식별자를 가리키는 칸이고, 이 키를 맞대어 흩어진 표를 다시 잇는 것이 조인이다. INNER 조인은 양쪽에 짝이 있는 행만 남긴다.

다음 시간 예고

이제 종이 설계가 끝났으니, 다음 시간부터는 이 설계도를 진짜 SQL로 옮겨요. C 카테고리의 첫걸음은 CREATE TABLE로 회원·게시물 표를 실제로 만들고, SELECT로 조회하고, WHERE로 원하는 행만 골라내는 거예요. 오늘 배운 NULL도 그때 IS NULL로 직접 다뤄 보고, 조인의 짝 없는 쪽을 살리는 OUTER 조인은 조인을 전문으로 파는 모듈(C-5)에서 본격적으로 만나요. 종이에 그린 설계도가 화면에서 살아 움직이는 걸 보게 될 거예요.


과제

오늘 배운 트랜잭션·NULL·조인을 직접 따져 보는 과제예요. SQL 문법을 외우는 게 아니라, 개념을 시나리오에 적용해 보는 게 목적이에요.

[기초] 트랜잭션이 깨지면 무슨 일이 생길까

인스타에서 회원이 자기 게시물을 삭제하는 상황을 떠올려 보세요. 이 삭제는 (1) 게시물 표에서 게시물 삭제 (2) 댓글 표에서 그 게시물의 댓글 삭제 (3) 좋아요 표에서 그 게시물의 좋아요 삭제, 이렇게 세 작업으로 이뤄져요. (1)은 됐는데 (2)에서 오류가 나 멈췄다고 해봐요. (가) 이때 데이터가 어떤 이상한 상태가 되는지 한 줄로 적고, (나) 트랜잭션의 어떤 성질이 이 사고를 막아 주는지, 그 성질이 어떻게 작동해 데이터를 원래대로 되돌리는지 설명해 보세요.

[응용] NULL을 어떻게 다뤄야 할까

회원 표에는 자기소개(bio)가 비어 있는 회원이 있어요(NULL). (가) "자기소개를 안 쓴 회원을 찾아 줘"라는 조건을 bio = NULL로 쓰면 왜 한 명도 안 걸리는지, NULL의 성질로 설명하고 올바른 표현은 무엇인지 적어 보세요. (나) 회원 표의 username(아이디)·nickname(표시 이름)·bio(자기소개) 세 칸 중 어떤 칸을 NOT NULL로 두고 어떤 칸을 NULL 허용으로 둘지 정하고, 각각의 이유를 한 줄씩 붙여 보세요.

[심화] 조인으로 표를 다시 이어 보기

회원 표(8명)와 게시물 표(게시물 12개, 작성자가 6명)를 member_id로 잇는 INNER 조인을 한다고 해봐요. (가) 결과가 몇 행이 나올지 예상하고 그 이유를 적어 보세요. (나) 게시물이 0개인 회원 2명(7·8번)이 결과에 안 나오는 이유를, INNER 조인의 동작으로 설명해 보세요. (다) 만약 "게시물이 없는 회원도 빠뜨리지 말고 한 줄씩 보여 줘"라고 하려면 어떤 종류의 조인이 필요할지, 이름만 떠올려 적어 보세요.


생각해볼 주제

1. ACID를 완벽하게 지키면 항상 좋을까

트랜잭션의 고립성을 완벽하게 지키면 데이터는 안전하지만, 여러 트랜잭션이 서로 기다리느라 동시 처리 속도가 떨어져요. 안전과 속도가 맞바꾸는 관계인 거죠. 그렇다면 모든 서비스가 무조건 가장 강한 고립성을 골라야 할까요? 좋아요 수를 세는 일과 계좌이체처럼, 데이터가 잠깐 어긋나도 큰일이 아닌 경우와 한 푼도 틀리면 안 되는 경우를 떠올리며, 상황마다 다르게 골라야 하는 이유를 생각해 보세요.

2. 비어도 되는 칸을 다 막아 버리면 안전할까

NULL이 함정을 만드니, "그럼 모든 칸을 NOT NULL로 막아서 빈칸을 아예 없애면 안전하지 않나?"라는 생각이 들 수 있어요. 정말 그럴까요? 자기소개를 아직 안 쓴 회원, 아직 카테고리를 안 정한 게시물처럼 '아직 값이 없는 게 자연스러운' 경우를 떠올려 보세요. 모든 칸을 NOT NULL로 막으면 이런 회원·게시물은 어떻게 될지, NULL을 허용하는 것과 막는 것이 각각 무엇을 얻고 무엇을 잃는지 생각해 보세요.

3. 왜 거의 항상 인조식별자를 주식별자로 쓸까

실무에서는 회원을 식별할 때 이메일이나 아이디(본질식별자) 대신, 의미 없는 번호(member_id 같은 인조식별자)를 주식별자로 두는 경우가 압도적으로 많아요. 그냥 이메일을 주식별자로 쓰면 표 하나가 줄어 더 간단할 텐데도요. 그럼에도 번호를 새로 만들어 주식별자로 삼는 이유는 무엇일지, 식별자 4조건(특히 불변성·존재성)과 "주식별자가 바뀌면 그걸 가리키던 다른 표들은 어떻게 되나"를 함께 떠올리며 생각해 보세요.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안이에요. 정답을 그대로 베끼기보다, 왜 그렇게 답하는지 흐름을 따라와 주세요. 모델링 개념을 시나리오에 적용하는 손이 목표예요.


🎯 [과제 1 예시답안] 트랜잭션이 깨지면 무슨 일이 생길까

채점 포인트

항목 배점 핵심
(가) 깨진 상태 묘사 40% 게시물은 지워졌는데 그 게시물의 댓글(또는 좋아요)이 남아 '주인 없는 데이터'가 된다
(나) 막아 주는 성질 40% 원자성(Atomicity) — 전부 또는 전무
되돌리는 작동 20% 중간 실패 시 ROLLBACK으로 이미 된 (1)까지 취소해 처음 상태로 복구

풀이 예시

(가) 어떤 이상한 상태가 되나

(1) 게시물 삭제는 성공했는데 (2) 댓글 삭제에서 멈췄으니, 게시물은 사라졌지만 그 게시물에 달렸던 댓글은 그대로 남아요. 가리킬 게시물이 없는 '주인 없는 댓글'이 떠돌게 되죠. 데이터가 앞뒤가 안 맞는 상태가 된 거예요. (좋아요도 마찬가지로 남을 수 있어요.)

(나) 어떤 성질이 막아 주나

트랜잭션의 원자성(Atomicity)이 막아 줘요. 원자성은 묶인 작업이 전부 반영되거나 전부 안 되거나, 둘 중 하나로만 끝난다는 약속이에요(All or Nothing). 세 작업을 한 트랜잭션으로 묶어 두면, (2)에서 오류가 난 순간 데이터베이스는 "이 트랜잭션은 실패"로 판정해요. 그리고 ROLLBACK으로 이미 끝낸 (1)까지 되돌려, 게시물 삭제 자체를 없던 일로 만들어요. 그래서 결과는 '셋 다 삭제됨' 또는 '셋 다 그대로', 둘 중 하나만 남죠. 어중간하게 게시물만 사라지는 상태는 절대 생기지 않아요.

💡 튜터의 한마디 — 원자성의 진짜 가치는 '실패할 때' 드러나요. 잘 될 때야 어느 방식이든 똑같지만, 중간에 문제가 터졌을 때 데이터를 깔끔하게 처음으로 되돌려 주는 것이 트랜잭션의 핵심이에요. "여러 표를 건드리는 작업은 트랜잭션으로 묶는다"를 습관으로 새겨 두세요.


🎯 [과제 2 예시답안] NULL을 어떻게 다뤄야 할까

채점 포인트

항목 배점 핵심
(가) bio = NULL 함정 50% NULL 비교는 '알 수 없음' → 아무 행도 안 걸림 / 올바른 표현은 IS NULL
(나) 세 칸의 NULL 정책 50% username·nickname=NOT NULL / bio=NULL 허용, 각 이유

풀이 예시

(가) 왜 한 명도 안 걸리나

NULL은 '알 수 없음'이라, NULL을 등호(=)로 비교하면 결과가 참도 거짓도 아닌 '알 수 없음(UNKNOWN)'이 돼요. 조건이 참이어야 행이 걸리는데, '알 수 없음'은 참이 아니라서 어떤 행도 안 걸려요. 그래서 bio = NULL은 항상 빈 결과를 줘요. 올바른 표현은 bio IS NULL이에요. IS NULL은 NULL인 행을 정확히 가려내는 전용 표현이거든요.

(나) 세 칸의 NULL 정책

정책 이유
username NOT NULL 로그인·식별에 쓰는 핵심 값이라 비면 안 됨 (유일 제약도 함께)
nickname NOT NULL 화면에 보이는 이름이라 항상 있어야 함
bio NULL 허용 자기소개는 안 써도 서비스에 지장 없음 — '아직 안 씀'이 자연스러운 칸

핵심은 "이 값이 없으면 서비스가 곤란한가"를 기준으로 가르는 거예요. 없으면 안 되는 값은 NOT NULL로 막고, 없어도 되는 값은 NULL을 허용해요.

💡 튜터의 한마디 — NULL 허용 여부는 단순한 기술 선택이 아니라 '이 값이 업무상 꼭 필요한가'라는 설계 판단이에요. 무조건 다 막는 것도, 다 허용하는 것도 답이 아니에요. 칸마다 의미를 따져 정하는 습관이 좋은 설계의 시작이에요.


🎯 [과제 3 예시답안] 조인으로 표를 다시 이어 보기

채점 포인트

항목 배점 핵심
(가) 결과 행 수 40% 12행 — 게시물 12개 각각에 작성자가 붙어서
(나) 7·8번이 빠지는 이유 40% INNER 조인은 양쪽에 짝 있는 행만 — 게시물 0개면 짝이 없음
(다) 짝 없는 쪽도 보려면 20% OUTER 조인

풀이 예시

(가) 몇 행이 나올까 — 12행

게시물 표의 행 하나하나가 자기 작성자를 회원 표에서 찾아 짝을 지어요. 게시물이 12개고, 12개 모두 작성자(member_id)가 회원 표에 있으니, 12개 게시물 각각에 작성자 닉네임이 붙어 12행이 나와요.

SQL
SELECT m.nickname, p.caption
FROM   post p
JOIN   member m ON p.member_id = m.member_id;

실제로 실행하면 정확히 12행이 돌아와요(김재훈 3 + 이민지 3 + 박승우 2 + 정하루 2 + 최도연 1 + 강지수 1 = 12).

(나) 왜 7·8번은 안 나오나

INNER 조인은 양쪽 표에 짝이 있는 행만 이어 줘요. 윤태양(7번)·한나래(8번)는 게시물이 0개라, 게시물 표에 이들을 가리키는 행이 하나도 없어요. 이을 상대가 없으니 결과에서 빠지는 거예요.

(다) 짝 없는 회원도 보려면

OUTER 조인이 필요해요. OUTER 조인은 짝이 없는 쪽도 버리지 않고 한 줄로 남겨 줘요(게시물 칸은 빈칸, 즉 NULL로). 자세한 사용법은 다음에 조인을 전문으로 다룰 때 배워요.

💡 튜터의 한마디 — "회원이 8명인데 결과는 6명만?"이라는 의문이 들면 조인을 제대로 이해하기 시작한 거예요. 조인은 단순히 표를 붙이는 게 아니라 '어떤 줄을 남기고 어떤 줄을 버리느냐'를 정하는 일이에요. INNER는 짝 있는 것만, OUTER는 짝 없는 것까지. 이 차이를 분명히 알아 두면 조인이 훨씬 쉬워져요.


🤔 [생각해볼 주제 1] ACID를 완벽하게 지키면 항상 좋을까

문제 상황 요약

고립성을 강하게 지키면 데이터는 안전하지만 동시 처리 속도가 떨어진다. 그렇다면 모든 서비스가 항상 가장 강한 고립성을 골라야 할까?

튜터의 가이드 및 해설

핵심은 "안전과 속도는 맞바꾸는 관계"라는 거예요. 고립성을 강하게 걸면 트랜잭션끼리 서로 기다려야 해서, 같은 시간에 처리할 수 있는 양이 줄어요. 반대로 느슨하게 풀면 빠르지만 데이터가 잠깐 어긋날 위험이 생기죠.

그래서 정답은 "상황마다 다르다"예요. 계좌이체는 한 푼도 틀리면 안 되니 강한 고립성을 골라야 해요. 돈은 정확성이 속도보다 훨씬 중요하니까요. 반면 게시물의 좋아요 수 표시는, 잠깐 100개로 보였다가 99개로 보정돼도 사용자가 크게 불편하지 않아요. 이런 곳은 고립성을 조금 풀어 속도를 얻는 게 합리적이에요.

결국 "이 데이터가 잠깐 어긋나면 얼마나 큰일인가"를 기준으로 고립성의 세기를 고르는 거예요. 무조건 가장 강하게 거는 게 정답이 아니라, 데이터의 성격에 맞춰 안전과 속도의 균형점을 찾는 게 설계예요.

🎯 SQLD는 이렇게 나온다

ACID 각 성질의 정의를 짝짓는 문제가 단골이에요. 특히 고립성(Isolation)을 "동시에 실행되는 트랜잭션이 서로 영향을 주지 않음"으로 정확히 고를 수 있어야 해요. 원자성과 헷갈리게 내는 함정이 있으니, 원자성='전부 또는 전무', 고립성='동시 실행 격리'로 분명히 갈라 두세요.

💡 실무에선

데이터베이스는 격리 수준을 여러 단계로 제공해요(읽기 일관성을 어디까지 보장할지). 돈·재고처럼 정확성이 생명인 곳은 강한 수준을, 조회 위주에 약간의 오차가 허용되는 곳은 느슨한 수준을 골라 성능을 확보해요. "기본은 안전하게, 성능이 정말 발목 잡는 곳만 신중히 완화"가 흔한 전략이에요.


🤔 [생각해볼 주제 2] 비어도 되는 칸을 다 막아 버리면 안전할까

문제 상황 요약

NULL이 함정을 만드니, 모든 칸을 NOT NULL로 막아 빈칸을 아예 없애면 안전하지 않을까? 정말 그럴까?

튜터의 가이드 및 해설

언뜻 그럴듯하지만, 모든 칸을 NOT NULL로 막으면 오히려 문제가 생겨요. 세상에는 '아직 값이 없는 게 자연스러운' 경우가 많거든요. 자기소개를 아직 안 쓴 회원, 카테고리를 아직 안 정한 게시물처럼요.

이런 칸까지 NOT NULL로 막으면 어떻게 될까요? 자기소개를 안 쓴 회원은 가입 자체가 안 되거나, 억지로 빈 문자열('')이나 '없음' 같은 가짜 값을 채워 넣게 돼요. 그러면 '진짜로 없음'과 '없다고 적어 둠'이 뒤섞여, 나중에 "자기소개를 안 쓴 회원"을 구분할 수가 없어져요. NULL을 없애려다 데이터의 의미를 흐리는 거죠.

그래서 NULL은 '함정'이기도 하지만 '필요한 표현'이기도 해요. '값이 아직 없음'을 정직하게 나타내는 유일한 방법이거든요. 핵심은 칸마다 따져 정하는 거예요. 없으면 서비스가 곤란한 값(아이디·표시 이름)은 NOT NULL로 막고, 없어도 되는 값(자기소개)은 NULL을 허용해 '아직 없음'을 정직하게 두는 거죠.

🎯 SQLD는 이렇게 나온다

NULL을 0이나 빈 문자열('')과 같다고 보는 선택지가 함정으로 나와요. 셋은 모두 다른 상태라는 걸 분명히 해야 해요. 또 "NULL은 무조건 나쁘니 없애야 한다"는 식의 단정도 오답이에요. NULL은 '값 없음'을 표현하는 정상적인 수단이라는 관점이 정답에 가까워요.

💡 실무에선

NOT NULL 여부는 요구사항에서 결정돼요. "이 값은 반드시 입력받아야 하나?"를 기획·업무 규칙과 함께 정하죠. 무심코 다 NULL 허용으로 두면 빈 데이터가 쌓여 품질이 떨어지고, 반대로 다 막으면 가짜 값이 들어와요. 칸마다 의미를 따져 정하는 게 데이터 품질의 출발점이에요.


🤔 [생각해볼 주제 3] 왜 거의 항상 인조식별자를 주식별자로 쓸까

문제 상황 요약

이메일·아이디(본질식별자) 대신 의미 없는 번호(인조식별자)를 주식별자로 두는 경우가 압도적으로 많다. 이메일을 쓰면 더 간단할 텐데, 왜 굳이 번호를 새로 만들까?

튜터의 가이드 및 해설

식별자 4조건 중 불변성존재성이 핵심 열쇠예요. 본질식별자(이메일·아이디)는 둘 다 흔들릴 수 있어요. 사람은 이메일을 바꾸고 싶어 하고(불변성 위협), 가입 방식에 따라 비어 있을 수도 있어요(존재성 위협). 반면 인조식별자(번호)는 한번 부여하면 바뀔 일이 없고, 시스템이 항상 채워 주니 비는 일도 없어요.

특히 무서운 건 '주식별자가 바뀌는' 상황이에요. 주식별자는 다른 표들이 외래키로 가리키는 대상이에요. 만약 회원의 주식별자를 이메일로 뒀는데 누군가 이메일을 바꾸면? 그 회원을 가리키던 게시물·댓글·좋아요 표의 외래키를 전부 따라 고쳐야 해요. 하나라도 놓치면 데이터가 어긋나죠. 인조식별자(번호)는 바뀔 일이 없으니 이런 연쇄 수정 사고가 아예 없어요.

그래서 실무는 보통 짧고 안 바뀌는 번호를 주식별자로 두고, 이메일·아이디에는 '유일해야 한다'는 제약만 따로 걸어요. 식별은 안전한 번호에 맡기고, 중복 방지는 본질 속성에 맡기는 거죠. 표 하나 줄이는 간단함보다, 안 바뀌고 항상 있는 안정성이 훨씬 값지니까요.

🎯 SQLD는 이렇게 나온다

주식별자 4조건(유일성·최소성·불변성·존재성)을 고르는 문제, 그리고 본질식별자와 인조식별자(대리키)의 차이를 묻는 문제가 나와요. "인조식별자를 쓰는 이유"로 불변성·존재성 확보를 고를 수 있어야 해요. 존재성을 'NOT NULL'과 연결 짓는 관점도 함께 챙겨 두세요.

💡 실무에선

대부분의 표가 의미 없는 일련번호를 주식별자로 둬요. 이메일·아이디 같은 본질 속성에는 유일 제약을 따로 걸어 중복을 막고요. 주식별자를 '업무 의미가 없는 안정된 값'으로 두면, 업무 규칙이 바뀌어도 식별 체계가 흔들리지 않아요. 이게 설계를 오래 견디게 하는 비결이죠.

전체 목록 데이터베이스