컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
db · 01

관계형 DB 기초

DB관계형기본키

관계형 데이터베이스(RDBMS)는 데이터를 표(테이블) 형태로 저장합니다. 엑셀 스프레드시트를 떠올리면 됩니다 — 세로줄(컬럼)마다 어떤 종류의 정보를 담을지 정해두고, 가로줄(행)마다 실제 데이터 한 건씩을 담습니다. 여기에 기본키(PK)로 각 행을 구별하고, 외래키(FK)로 서로 다른 테이블을 연결하면 관계형 모델의 뼈대가 완성됩니다.

핵심 개념

테이블(Table)

하나의 주제(예: 사용자, 주문)에 대한 데이터를 모아두는 표입니다. 컬럼(열)과 레코드(행)로 이루어져 있습니다.

핵심 개념

컬럼(Column)

테이블의 세로줄이며, 하나의 속성(attribute)을 의미합니다. id, name, email처럼 어떤 값을 저장할지 미리 정해집니다.

핵심 개념

레코드(Row)

테이블의 가로줄이며, 실제 데이터 한 건입니다. 모든 컬럼에 대응하는 값을 하나씩 가지고 있습니다.

핵심 개념

기본키(Primary Key)

각 레코드를 유일하게 식별하는 컬럼입니다. 값이 중복될 수 없고(UNIQUE), 비어있을 수도 없습니다(NOT NULL).

핵심 개념

외래키(Foreign Key)

다른 테이블의 기본키를 그대로 저장해 두 테이블을 연결하는 컬럼입니다. 존재하지 않는 값은 참조할 수 없습니다.

핵심 개념

관계(Relationship)

외래키 덕분에 한 사용자가 여러 주문을 가지는 것처럼(1:N), 서로 다른 테이블의 데이터를 자연스럽게 이어 볼 수 있습니다.

왜 표로 나눠서 저장할까? — 사용자 정보와 주문 정보를 한 표에 몰아넣으면, 같은 사용자가 주문할 때마다 이름·이메일을 계속 반복 저장하게 됩니다. 테이블을 나누고 외래키로 연결하면 이런 중복을 줄일 수 있습니다(정규화의 출발점).
기본키는 보통 자동으로 채워집니다 — 실무에서는 id 값을 직접 정하기보다, DB가 1, 2, 3, ...처럼 자동으로 증가시키는 방식(AUTO_INCREMENT, IDENTITY 등)을 많이 사용합니다.
외래키는 "약속"을 강제합니다 — 외래키 제약 조건이 걸려 있으면, 존재하지 않는 사용자 id를 가진 주문을 실수로 저장하는 상황 자체를 데이터베이스가 막아 줍니다.
이 시각화에서 확인할 것 — "테이블 구조" 탭에서 열·행·셀 용어를 먼저 익히고, "기본키" 탭에서 중복/빈값이 왜 거부되는지, "외래키 관계" 탭에서 두 테이블이 어떻게 화살표로 연결되는지 직접 확인하세요.

관계형 모델이 나온 배경

관계형 모델은 에드거 코드(E. F. Codd)가 1970년 논문 "A Relational Model of Data for Large Shared Data Banks"에서 제안했습니다. 데이터를 값의 집합으로 다루고 저장 방식과 분리해서 보게 하자는 생각이 바탕입니다. 교재에서는 테이블을 릴레이션(relation), 행을 튜플(tuple), 컬럼을 어트리뷰트(attribute)라고 부릅니다. 이 토픽의 테이블·행·컬럼은 같은 개념을 실무에서 쓰는 이름으로 부른 것입니다.

엑셀과 다른 점

컬럼마다 데이터 타입이 정해집니다 — 정수, 문자열, 날짜처럼 컬럼의 타입을 선언하고, 타입에 맞지 않는 값은 저장되지 않습니다. 엑셀은 한 칸에 아무 값이나 넣을 수 있습니다.
제약 조건을 DB가 강제합니다 — 기본키, 외래키, NOT NULL, UNIQUE, CHECK 같은 제약 조건을 선언하면, 어기는 입력이나 수정을 DB가 거부합니다. 사람이 조심하는 것에 기대지 않습니다.
여러 사용자가 동시에 쓰는 것을 전제로 합니다 — 트랜잭션과 락으로 동시 접근을 다룹니다. 이 부분은 트랜잭션, 락 토픽에서 이어집니다.

키의 종류

후보키와 기본키 — 행을 유일하게 식별할 수 있는 최소한의 컬럼 조합을 후보키라고 하고, 후보키 중에서 대표로 고른 것이 기본키입니다. 컬럼 하나일 수도 있고 여러 컬럼을 묶은 복합 키일 수도 있습니다.
자연키와 대리키 — 주민 번호나 이메일처럼 업무에서 의미가 있는 값을 키로 쓰면 자연키, 의미 없이 DB가 붙여 주는 번호를 쓰면 대리키(surrogate key)입니다. 이 사이트의 테이블은 BIGSERIAL(자동 증가 번호)을 기본키로 둔 대리키 방식입니다.
외래키는 참조 대상이 정해져 있습니다 — PostgreSQL 문서에 따르면 외래키가 가리키는 컬럼은 기본키이거나 UNIQUE 제약이 걸린 컬럼이어야 합니다.

자주 하는 오해

외래키는 반드시 기본키만 참조한다 — 기본키를 참조하는 것이 가장 흔하지만, UNIQUE 제약이 걸린 컬럼도 참조할 수 있습니다.
기본키는 숫자 하나여야 한다 — 타입은 숫자로 정해져 있지 않고, 컬럼 여러 개를 묶은 복합 기본키도 만들 수 있습니다. 숫자 번호를 쓰는 것은 관행입니다.
외래키를 걸면 조회가 빨라진다 — 외래키는 정합성을 지키는 제약 조건이고, 조회 속도를 높이는 장치가 아닙니다. PostgreSQL은 외래키를 거는 쪽 컬럼에 인덱스를 자동으로 만들지 않으며, 참조하는 쪽 컬럼에 인덱스를 따로 거는 것이 좋을 수 있다고 문서에서 안내합니다.
NULL은 0이나 빈 문자열과 같다 — NULL은 값이 없거나 알 수 없다는 표시이고, 0이나 빈 문자열은 값입니다. 그래서 NULL과의 비교는 참도 거짓도 아닌 '알 수 없음'이 되어 = NULL이 아니라 IS NULL로 확인합니다.
테이블을 나누면 항상 좋다 — 중복은 줄지만 데이터를 한 번에 보려면 테이블을 합쳐야 합니다. 어디까지 나눌지는 정규화 토픽에서 이어집니다.

면접에서 이렇게 물어요

면접 질문

기본키와 UNIQUE 제약은 무엇이 다른가요?

둘 다 값의 중복을 막습니다. 기본키는 테이블에 하나만 둘 수 있고 NULL이 안 됩니다. UNIQUE는 테이블에 여러 개 둘 수 있고, PostgreSQL에서는 NULL을 여러 개 넣을 수 있습니다(제품마다 다릅니다).

면접 질문

외래키를 거는 이유와 걸지 않는 경우를 말해 보세요.

존재하지 않는 부모 행을 가리키는 자식 행이 저장되는 것을 DB가 막아 주므로 정합성을 지킬 수 있습니다. 대신 입력과 삭제 때 DB가 참조를 확인하는 비용이 듭니다.

면접 질문

1:N과 N:M 관계는 어떻게 표현하나요?

1:N은 N쪽 테이블에 1쪽의 기본키를 외래키로 둡니다. N:M은 양쪽 기본키를 외래키로 가지는 연결 테이블(교차 테이블)을 따로 만들어 두 개의 1:N으로 바꿉니다.

면접 질문

외래키가 없어도 JOIN을 할 수 있나요?

할 수 있습니다. JOIN은 조건식으로 행을 잇는 연산이어서 외래키가 선언되어 있는지와 상관없이 쓸 수 있습니다. 외래키는 그 연결이 깨지지 않도록 지켜 주는 제약일 뿐입니다.

실제 시스템에서는

이 사이트의 태그 — 토픽의 태그는 topic 테이블에 쉼표로 이어 붙이지 않고 topic_tag 테이블로 나누고, topic_id를 외래키로 topic에 연결했습니다. 한 토픽에 태그가 여러 개인 1:N 관계입니다.
PostgreSQL — 기본키를 선언하면 그 컬럼에 유일성을 보장하는 인덱스가 자동으로 만들어집니다. psql에서 \d 테이블이름을 실행하면 컬럼, 기본키, 외래키를 볼 수 있습니다.
제약 조건 이름 — 제약 조건에 이름을 붙여 두면, 위반했을 때 오류 메시지에 이름이 나와서 어느 규칙을 어겼는지 알기 쉽습니다.

함께 보면 좋은 토픽

SQL JOIN — 외래키로 이은 테이블을 조회에서 합치는 방법을 봅니다.
정규화 — 중복을 줄이도록 테이블을 나누는 규칙을 봅니다.
인덱스 — 기본키와 UNIQUE가 인덱스로 구현되는 이유와 조회 속도를 봅니다.
트랜잭션 — 여러 행의 변경을 하나로 묶는 방법을 봅니다.
커넥션 풀 — DB 연결을 미리 만들어 두고 빌려 쓰며 연결 비용을 줄이는 방법을 봅니다.
OLAP vs OLTP — 짧은 트랜잭션 처리와 대량 이력 분석, 두 업무의 차이를 봅니다.

참고 자료

교재 — Silberschatz, Korth, Sudarshan, "Database System Concepts" 7판, "Introduction to the Relational Model" 장

SQL — 기본키(PK) / 외래키(FK) 정의

-- USERS: id를 기본키로 지정 (자동 증가)
CREATE TABLE users (
    id     BIGINT       PRIMARY KEY AUTO_INCREMENT,
    name   VARCHAR(50)  NOT NULL,
    email  VARCHAR(100) NOT NULL
);

-- ORDERS: user_id를 외래키로 지정 (USERS.id를 참조)
CREATE TABLE orders (
    id        BIGINT      PRIMARY KEY AUTO_INCREMENT,
    user_id   BIGINT      NOT NULL,
    product   VARCHAR(50) NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

PRIMARY KEY는 id가 유일하고(UNIQUE) 비어있지 않음(NOT NULL)을 DB 차원에서 보장합니다.
FOREIGN KEY ... REFERENCES는 orders.user_id에 users.id에 없는 값을 넣으려는 시도를 DB가 직접 거부하게 만듭니다 — 시각화의 "외래키 관계" 탭에서 본 거부 동작이 바로 이 제약 조건입니다.

Java — JPA Entity로 기본키 / 외래키 표현

@Entity
@Table(name = "users")
@Getter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)   // 기본키 — DB가 자동 채번
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;
}

@Entity
@Table(name = "orders")
@Getter
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", nullable = false)   // 외래키 — USERS.id 참조
    private User user;

    @Column(nullable = false)
    private String product;
}

User.id는 @Id + @GeneratedValue로 기본키를, Order.user는 @JoinColumn(name = "user_id")로 외래키를 표현합니다.
객체지향 코드에서는 숫자 id 대신 User user 필드를 직접 들고 있는 것처럼 보이지만, 실제 테이블에는 SQL 예시와 동일하게 user_id 컬럼(외래키 값)만 저장됩니다.

Java — 기본키로 조회하기 (Repository / Service)

public interface OrderRepository extends JpaRepository<Order, Long> {

    // LEFT JOIN FETCH: 연관된 User를 한 번에 가져와 LazyInitializationException 방지
    @Query("SELECT o FROM Order o LEFT JOIN FETCH o.user WHERE o.id = :id")
    Optional<Order> findByIdWithUser(@Param("id") Long id);
}

@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;

    public Order getOrder(Long id) {
        return orderRepository.findByIdWithUser(id)
                .orElseThrow(() -> new NotFoundException("주문을 찾을 수 없습니다: id=" + id));
    }
}

기본키(id)로 조회할 땐 결과가 없을 수도 있으므로 Optional로 감싸고, Optional.get() 대신 orElseThrow()로 없을 경우를 명시적으로 처리합니다.
LEFT JOIN FETCH로 외래키가 가리키는 User까지 한 번의 쿼리로 함께 가져오는 것이 이 프로젝트의 표준 패턴입니다.

Q1

관계형 데이터베이스에서 기본키(Primary Key)의 역할은?

같은 값을 가진 행이 두 개 있으면 안 됩니다.
기본키는 테이블의 각 행(레코드)을 유일하게 식별하는 컬럼(또는 컬럼 조합)으로, 중복되거나 비어있을 수 없습니다.

Q2

외래키(Foreign Key)의 역할은?

테이블끼리 서로 연결하는 다리 역할입니다.
외래키는 다른 테이블의 기본키 값을 참조해 두 테이블 간의 관계(1:N 등)를 표현하고, 참조 무결성을 보장합니다.

Q3

테이블, 컬럼, 레코드(행)의 관계로 옳은 것은?

엑셀 시트를 떠올려보세요. 컬럼은 열, 레코드는 행입니다.
테이블은 여러 컬럼(열)으로 구성된 스키마를 가지며, 레코드(행)는 그 컬럼들 각각에 대한 값을 담은 한 줄의 데이터입니다.