사전 참조
개념

참조

gabury1고친 사람 github-actions[bot]

참조는 데이터를 통째로 건네는 대신 그 데이터가 있는 곳을 알려 줍니다. 데이터는 한 곳에 한 벌만 둡니다. 필요한 쪽은 참조를 따라가 꺼내 씁니다. 그래서 한 곳을 고치면 그것을 가리키는 모두가 고친 내용을 봅니다.

쉽고 빠른 이해

참조는 데이터 대신 데이터가 있는 곳을 건네는 값입니다. 주문 기록에 상품 정보를 전부 적지 않고 상품 번호만 적어 두는 것이 참조입니다.

같은 데이터를 여러 벌 복사해 두면 한 벌을 고쳤을 때 나머지가 낡습니다. 큰 데이터를 넘길 때마다 통째로 옮기는 품도 듭니다. 데이터를 한 벌만 두고 나머지는 그것을 가리키게 하면 두 문제가 사라집니다.

어떻게 도나:

  1. 데이터는 한 곳에만 둡니다
  2. 쓰는 곳은 데이터 대신 그것이 있는 곳을 담은 값을 들고 다닙니다
  3. 데이터가 필요해지면 그 값을 따라가 꺼냅니다

대가는 둘입니다. 같은 데이터를 함께 보는 곳이 있는 줄 모르고 고치면 남이 보는 값도 바뀝니다. 가리키던 데이터가 사라지면 그 참조를 따라가는 일이 어긋납니다. 오류가 나기도 합니다. 무슨 일이 일어날지 아예 정해져 있지 않기도 합니다.

상세

친구에게 맛집을 알려 줄 때 가게를 통째로 옮겨 주지는 않습니다. 주소 한 줄을 건넵니다. 주소를 받은 친구가 열 명이어도 가게는 하나입니다.

참조는 다른 데이터를 직접 담지 않고 그 데이터에 닿는 길을 담은 값입니다. 이때 가리켜지는 데이터를 대상이라 부릅니다. 변수 하나가 메모리 어딘가에 놓인 데이터 묶음을 가리키는 것이 가장 흔한 모습입니다.

참조에는 언제나 두 쪽이 있습니다. 가리키는 참조와 가리켜지는 대상입니다. 둘은 따로 놓입니다. 그래서 참조를 몇 개 만들어도 대상은 하나로 남습니다.

참조를 따라가 대상을 꺼내 읽는 동작의 이름은 역참조(dereference)입니다. 참조를 들고만 있을 때는 대상이 지금 어떤 상태인지 모릅니다. 따라가서 읽어야 그 순간의 값을 봅니다.

참조를 다루는 방식은 두 갈래입니다. 참조를 하나의 값으로 다루는 갈래가 있습니다. 참조를 대상의 다른 이름으로 묶는 갈래도 있습니다. 묶는 갈래에서는 한 번 묶인 참조를 다른 대상에 다시 묶지 못합니다.

값으로 다루는 참조는 그 자체가 하나의 값입니다. 변수에 담을 수 있습니다. 함수에 넘길 수도 있습니다. 다른 참조로 덮어쓸 수도 있습니다.

값으로 다루는 참조를 덮어쓰면 가리키는 대상이 바뀝니다. 앞서 가리키던 대상은 손대지 않은 채 남습니다. 그림의 「덮어쓰기 전」 칸과 「덮어쓴 뒤」 칸을 나란히 놓으면 참조가 가리키는 곳만 바뀐 것이 보입니다.

flowchart TD
    subgraph 전["덮어쓰기 전"]
        R1["참조"] -- 역참조 --> D1["대상 A"]
    end
    subgraph 후["덮어쓴 뒤"]
        R2["참조"] -- 역참조 --> D2["대상 B"]
        D3["대상 A · 손대지 않은 채 남음"]
    end

값으로 다루는 참조 둘이 같다는 말은 둘이 같은 대상을 가리킨다는 뜻입니다. 내용이 똑같은 대상 둘을 따로 가리키는 참조 둘은 같은 참조가 아닙니다. 내용을 비교하려면 두 대상까지 따라가 하나씩 맞춰 봐야 합니다.

flowchart TD
    subgraph 같음["같은 참조"]
        A1["참조 1"] --> AO["대상"]
        A2["참조 2"] --> AO
    end
    subgraph 다름["같은 참조가 아님"]
        B1["참조 1"] --> BO1["대상 · 내용 같음"]
        B2["참조 2"] --> BO2["대상 · 내용 같음"]
    end

가리키는 방법은 쓰는 곳마다 다릅니다. 메모리 위치로 가리킬 수 있습니다. 이름으로 가리킬 수도 있습니다. 키 값으로 가리킬 수도 있습니다. 어느 쪽이든 대상을 옮겨 담지 않는다는 점은 같습니다.

포인터도 데이터가 있는 곳을 값으로 들고 다닙니다. 이 항목에서 「참조」는 넓은 뜻입니다. 가리키는 값이면 포인터도 참조입니다.

좁은 뜻으로 쓰는 곳도 있습니다. 포인터는 담은 값이 메모리 주소라는 것을 드러냅니다. 주소에 수를 더해 옆 칸을 가리키게 하는 주소 산술도 허용합니다. 주소를 감추고 그런 셈을 막는 언어 기능에 「참조」라는 이름이 붙은 곳에서는, 참조와 포인터가 서로 맞선 이름이 됩니다.

배경

같은 데이터를 여러 곳에서 써야 할 때 가장 쉬운 방법은 복사입니다. 쓰는 곳마다 한 벌씩 나눠 주면 됩니다. 문제는 고칠 때 생깁니다. 한 벌을 고쳐도 나머지 복사본은 옛 값을 들고 있습니다.

데이터가 크면 넘기는 데도 품이 듭니다. 함수를 한 번 부를 때마다 큰 묶음 전체를 새로 만들어 넘겨야 합니다. 여러 곳이 같은 하나를 함께 보며 고쳐야 하는 일은 복사로는 아예 안 됩니다.

필요했던 것은 데이터를 한 벌만 두고 그것에 닿는 길만 나눠 주는 방법입니다. 길을 담은 값은 작아서 데이터가 커져도 넘기는 양이 늘지 않습니다. 한 벌을 고치면 길을 따라오는 모두가 고친 값을 봅니다. 이 길을 담은 값의 이름이 참조입니다.

flowchart TD
    subgraph 복사["복사 · 한 벌씩 나눠 준다"]
        U1["쓰는 곳 1"] --> C1["복사본 · 고친 값"]
        U2["쓰는 곳 2"] --> C2["복사본 · 옛 값"]
    end
    subgraph 길["참조 · 닿는 길만 나눠 준다"]
        U3["쓰는 곳 1"] --> D["데이터 한 벌 · 고친 값"]
        U4["쓰는 곳 2"] --> D
    end

예시

참조는 쓰는 곳마다 가리키는 대상도, 가리키는 방법도 다릅니다. 세 곳에서 하나씩 봅니다. 자바 프로그램의 변수, Git 저장소의 브랜치 이름, PostgreSQL 테이블의 외래 키입니다.

쓰는 곳 참조를 담는 것 가리키는 대상 무엇으로 가리키나
자바 프로그램 변수 객체 객체가 있는 곳을 담은 값
Git 저장소 .git/refs 아래의 파일 커밋이나 다른 ref 커밋의 해시 값, 또는 다른 ref 의 이름
PostgreSQL 외래 키 열 다른 테이블의 행 그 행의 열 값

자바의 객체 참조

자바에서 객체는 변수에 들어가지 않습니다. 변수에는 그 객체를 가리키는 값이 들어갑니다. 자바는 이 값을 참조 값이라 부르고, 흔히 줄여서 참조라고 합니다.

아무 객체도 가리키지 않는 특별한 참조도 하나 있습니다. null 참조입니다.

한 객체를 가리키는 참조는 여럿일 수 있습니다. 두 변수가 같은 객체의 참조를 담으면, 한 변수로 객체의 상태를 고친 것이 다른 변수로도 보입니다. 기본형 int 변수 둘과 Value 변수 둘을 나란히 놓은 코드입니다.

Java
class Value { int val; }

class Test {
    public static void main(String[] args) {
        int i1 = 3;
        int i2 = i1;
        i2 = 4;
        System.out.print("i1==" + i1);
        System.out.println(" but i2==" + i2);
        Value v1 = new Value();
        v1.val = 5;
        Value v2 = v1;
        v2.val = 6;
        System.out.print("v1.val==" + v1.val);
        System.out.println(" and v2.val==" + v2.val);
    }
}

이 프로그램은 이렇게 찍습니다.

i1==3 but i2==4
v1.val==6 and v2.val==6

i2 = i1 은 수 3 을 복사합니다. 그래서 i2 를 4 로 바꿔도 i1 은 3 입니다. v2 = v1 은 객체가 아니라 참조를 복사합니다. new 식이 하나뿐이라 Value 객체도 하나뿐입니다. v1.val 과 v2.val 은 그 한 객체의 같은 필드라서 둘 다 6 입니다.

flowchart TD
    subgraph 기본형["int 변수 · 수를 직접 담는다"]
        I1["i1 · 3"]
        I2["i2 · 4"]
    end
    subgraph 참조형["Value 변수 · 참조를 담는다"]
        V1["v1"]
        V2["v2"]
    end
    V1 --> O["Value 객체 · val 6"]
    V2 --> O

이 동작이 참조를 쓰는 까닭이면서 위험입니다. v2 를 받은 코드가 v1 도 같은 객체를 본다는 것을 모르고 고치면, v1 으로 보는 값도 말없이 바뀝니다.

두 참조를 == 로 비교하면 같은 객체를 가리키는지를 봅니다. 둘 다 null 이어도 참입니다. 글자가 똑같은 String 객체 둘이어도 서로 다른 객체면 거짓입니다. 내용을 비교하려면 s.equals(t) 처럼 메서드를 부릅니다.

Git 의 ref

Git 은 커밋마다 SHA-1(Secure Hash Algorithm 1) 해시 값을 이름으로 붙입니다. 1a410efbd13591db07496601ebc7a059dd55cfe9 같은 문자열이라 사람이 외우기 어렵습니다. 그래서 이 값을 파일에 적어 둡니다. 그 뒤로는 그 파일 이름으로 커밋을 부릅니다. Git 은 이 단순한 이름을 참조, 줄여서 ref 라 부릅니다.

ref 파일은 .git/refs 디렉터리 아래에 있습니다. 기술적으로는 파일 하나에 해시 한 줄을 적는 것만으로 ref 가 하나 생깁니다.

터미널
$ echo 1a410efbd13591db07496601ebc7a059dd55cfe9 > .git/refs/heads/master
$ git log --pretty=oneline master
1a410efbd13591db07496601ebc7a059dd55cfe9 Third commit
cac0cab538b970a37ea1e769cbbde608743bc96d Second commit
fdf4fc3344e67ab068f836878b6c4951e3b15f3d First commit

첫 줄은 해시 한 줄을 .git/refs/heads/master 파일에 적습니다. 그 뒤로는 해시 대신 master 라는 이름으로 커밋을 부를 수 있습니다. Git 의 브랜치가 기본적으로는 이것입니다. 한 작업 줄기의 맨 앞 커밋을 가리키는 단순한 참조입니다. 그 줄기에 커밋을 더하면 맨 앞이 달라집니다. 브랜치 ref 가 가리키는 곳도 따라 옮겨 갑니다.

HEAD 는 지금 꺼내 둔 브랜치를 가리키는 ref 입니다. 그래서 한 단계를 더 거칩니다. .git/HEAD 파일에는 보통 해시가 아니라 다른 ref 의 이름이 들어 있습니다.

터미널
$ cat .git/HEAD
ref: refs/heads/master

이렇게 다른 ref 를 가리키는 ref 의 이름은 심볼릭 참조(symbolic ref)입니다. git checkout test 를 하면 이 파일이 ref: refs/heads/test 로 바뀝니다. 심볼릭 참조를 쓰면 Git 은 가리키는 ref 를 거듭 따라가 커밋까지 닿습니다.

태그도 ref 입니다. 태그에는 두 가지가 있습니다. 경량 태그(lightweight tag)와 주석 태그(annotated tag)입니다. 경량 태그는 커밋을 가리키는 ref 하나뿐입니다. 브랜치 ref 와 달리 움직이지 않습니다. 언제나 같은 커밋을 가리킵니다. git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d 를 부르면 경량 태그가 하나 생깁니다. 주석 태그는 태그 객체를 따로 만듭니다. 그 ref 는 커밋이 아니라 태그 객체를 가리킵니다.

HEAD 는 master 를 거쳐 커밋에 닿습니다. 경량 태그는 커밋을 바로 가리킵니다.

flowchart TD
    H["HEAD · ref: refs/heads/master"] --> M["refs/heads/master · 1a410ef…"]
    M --> C3["커밋 1a410ef · Third commit"]
    T["refs/tags/v1.0 · 경량 태그"] --> C2["커밋 cac0cab · Second commit"]

PostgreSQL 의 외래 키

외래 키는 한 열의 값이 다른 테이블 어떤 행의 값과 맞아야 한다고 정하는 제약입니다. 상품 테이블 products 와 주문 테이블 orders 로 봅니다.

SQL
CREATE TABLE products (
    product_no integer PRIMARY KEY,
    name text,
    price numeric
);

CREATE TABLE orders (
    order_id integer PRIMARY KEY,
    product_no integer REFERENCES products (product_no),
    quantity integer
);

orders 의 행은 상품의 이름이나 가격을 담지 않습니다. product_no 값 하나로 products 의 한 행을 가리킵니다. 이 외래 키가 걸리면 products 에 없는 product_no 로는 주문 행을 만들 수 없습니다. product_no 가 NULL 인 주문은 이 검사에서 빠집니다.

이때 orders 를 참조하는 테이블(referencing table), products 를 참조되는 테이블(referenced table)이라 부릅니다. 두 테이블의 행이 서로 어긋나지 않게 지키는 성질의 이름은 참조 무결성입니다.

flowchart TD
    subgraph ORD["orders · 참조하는 테이블"]
        O1["order_id"]
        O2["product_no"]
        O3["quantity"]
    end
    subgraph PRD["products · 참조되는 테이블"]
        P1["product_no"]
        P2["name"]
        P3["price"]
    end
    O2 -- 외래 키 --> P1

경계

자바에서 객체를 메서드에 넘기면 참조에 의한 전달인가. 아닙니다. 객체를 넘길 때도 자바는 값에 의한 전달입니다. 넘어가는 값이 참조 값일 뿐입니다.

두 낱말부터 가릅니다. 값에 의한 전달(pass by value)은 인자의 값을 복사해 새 매개변수 변수에 담습니다. 참조에 의한 전달(pass by reference)은 호출한 쪽 변수 자체를 매개변수로 묶습니다. 그래서 매개변수에 무엇을 대입하면 호출한 쪽 변수가 바뀝니다.

자바는 앞쪽입니다. 메서드를 부르면 매개변수 변수가 새로 만들어집니다. 인자로 쓴 식을 계산한 값이 그 새 변수에 담깁니다. 메서드 본문은 그 뒤에 돕니다. 객체를 넘기면 이때 담기는 값이 그 객체를 가리키는 참조 값입니다.

그래서 매개변수는 호출한 쪽 변수와 다른 새 변수입니다. 같은 객체를 가리키는 참조의 복사본을 담을 뿐입니다. 메서드 안에서 무엇을 하느냐에 따라 호출한 쪽이 보는 결과가 갈립니다.

메서드 안에서 하는 일 호출한 쪽에서 보면 까닭
매개변수를 거쳐 객체의 필드를 고친다 고친 값이 보인다 두 변수가 같은 객체를 가리킨다
매개변수에 새 객체를 대입한다 호출한 쪽 변수는 바뀌지 않는다 바뀐 것은 새로 만든 매개변수 변수뿐이다

참조에 의한 전달이 언어 기능으로 있는 경우와 나란히 놓으면 차이가 드러납니다. C++ 에서는 void f(double& a) { a += 3.14; } 처럼 매개변수를 참조로 선언할 수 있습니다. 이 참조는 대상 객체의 다른 이름처럼 동작합니다. 값으로 다루는 참조가 아니라 대상의 다른 이름으로 묶는 참조입니다.

그래서 a 에 무엇을 더하는 것은 다른 대상을 가리키게 하는 일이 아니라 대상 자체를 고치는 일입니다. double d = 0; 뒤에 f(d) 를 부르면 d 에 3.14 가 더해집니다. 자바의 매개변수는 언제나 새로 만든 변수라서 이렇게 호출한 쪽 변수에 직접 닿지 않습니다.

flowchart TD
    subgraph JAVA["자바 · 값에 의한 전달"]
        J1["호출한 쪽 변수"] --> JO["객체"]
        J2["매개변수 변수 · 참조 값의 복사본"] --> JO
    end
    subgraph CPP["C++ · 참조 매개변수"]
        CP["한 대상 · d 와 a 라는 두 이름이 붙는다"]
    end

실패

참조와 대상은 따로 놓입니다. 그래서 참조는 남아 있는데 대상이 없는 일이 생깁니다. 거꾸로 치워져야 할 대상이 참조에 붙들려 안 치워지는 일도 생깁니다. 깨지는 조건과 그때 일어나는 일을 네 곳에서 봅니다.

쓰는 곳 깨지는 조건 그때 일어나는 일
자바 아무것도 안 가리키는 참조로 필드를 읽거나 메서드를 부른다 예외가 던져진다
C 이미 사라진 대상을 가리키는 포인터를 쓴다 언어가 결과를 정해 두지 않았다
Swift 두 인스턴스가 서로를 붙들고 놓지 않는다 둘 다 메모리에서 안 치워질 수 있다
PostgreSQL 다른 행이 가리키는 행을 지운다 기본 설정이면 대개 오류가 난다

자바의 null 참조

null 참조로 객체가 있어야 하는 일을 하면 NullPointerException 이 던져집니다. 다음은 그런 경우 가운데 다섯입니다.

  • null 로 인스턴스 메서드를 부른다
  • null 의 필드를 읽거나 고친다
  • null 을 배열처럼 보고 길이를 잰다
  • null 을 배열처럼 보고 원소를 읽거나 고친다
  • null 을 예외처럼 던진다

목록은 여기서 닫히지 않습니다. null 을 그 밖에 잘못 쓴 경우에도 같은 예외를 던지도록 되어 있습니다.

C 의 수명이 끝난 대상

할당해 받은 메모리 한 덩이와 그것을 가리키는 포인터 하나를 놓고 봅니다. 대상이 먼저 없어졌을 때 그 포인터가 어떻게 되는지를 봅니다.

C 에서 객체에는 수명이 있습니다. 수명은 그 객체를 위한 공간이 확보되어 있다고 보장되는 실행 구간입니다. 할당해 받은 메모리를 해제하면 그 공간은 더 이상 확보되어 있지 않습니다. 대상의 수명이 거기서 끝납니다.

수명 밖에서 대상을 가리켜 쓰면 동작이 정의되지 않습니다. 정의되지 않은 동작(undefined behavior)은 그때 무슨 일이 일어날지를 언어 표준이 정해 두지 않았다는 뜻입니다.

대상의 수명이 끝나면 그것을 가리키던 포인터의 값도 정해지지 않은 값이 됩니다. 포인터는 그대로 남아 있지만 더는 믿을 수 없는 값을 담고 있습니다.

해제한 메모리를 다시 가리켜 쓰는 결함의 이름은 해제 후 사용(use after free)입니다. 해제된 메모리는 나중에 다시 할당되어 다른 포인터에 담길 수 있습니다. 그러면 옛 포인터는 새 할당 안의 어딘가를 가리킵니다. 그 메모리는 이제 새 포인터를 쓰는 코드의 몫입니다. 그 뒤로 옛 포인터로 하는 일은 모두 올바르지 않습니다. 이런 옛 포인터를 댕글링 포인터(dangling pointer)라고도 합니다.

flowchart TD
    subgraph 해제전["해제 전"]
        P1["옛 포인터"] --> M1["대상"]
    end
    subgraph 해제뒤["해제 뒤"]
        P2["옛 포인터"] --> M2["해제된 메모리"]
    end
    subgraph 재할당["다시 할당된 뒤"]
        P3["옛 포인터"] --> M3["새 할당"]
        P4["새 포인터"] --> M3
    end

Swift 의 강한 참조 순환

참조의 수를 세다가 0 이 되면 대상을 치우는 방식의 이름은 참조 카운팅입니다. Swift 는 클래스 인스턴스에 이 방식을 씁니다. Swift 에서 부르는 이름은 ARC(Automatic Reference Counting, 자동 참조 카운팅)입니다.

인스턴스를 살려 두는 참조는 강한 참조입니다. 두 인스턴스가 서로를 강한 참조로 붙들면 각자 상대를 살려 둡니다. 그러면 어느 쪽도 강한 참조가 0 개가 되는 때에 이르지 못할 수 있습니다. 이 고리의 이름은 강한 참조 순환(strong reference cycle)입니다.

flowchart TD
    A["인스턴스 A"] -- 강한 참조 --> B["인스턴스 B"]
    B -- 강한 참조 --> A

약한 참조(weak)와 미소유 참조(unowned)는 대상을 살려 두지 않습니다. 두 인스턴스 사이의 관계 가운데 일부를 강한 참조 대신 이 둘로 정의하면 고리가 풀립니다.

PostgreSQL 에서 참조되는 행 지우기

외래 키는 없는 상품을 가리키는 주문을 못 만들게 막습니다. 그런데 주문이 이미 가리키고 있는 상품 행을 지우면 어떻게 되나. 외래 키에 붙이는 ON DELETE 동작이 이것을 정합니다.

기본 동작은 NO ACTION 입니다. 따로 적지 않아도 됩니다. NO ACTION 은 참조되는 테이블에서 행을 지우는 일 자체를 막지 않습니다. 다만 외래 키 제약은 여전히 지켜져야 합니다. 그래서 이 작업은 대개 오류로 끝납니다.

외래 키 제약 검사는 트랜잭션 뒤쪽으로 미뤄 둘 수도 있습니다. 미뤄 두었다면 검사가 돌기 전에 다른 명령이 상황을 바로잡을 수 있습니다. 참조되는 테이블에 맞는 행을 새로 넣거나, 참조하는 테이블에 남은 행을 지우는 것입니다.

RESTRICT 는 NO ACTION 보다 엄격합니다. 참조되는 행을 지우는 일을 아예 막습니다. 검사를 트랜잭션 뒤쪽으로 미루는 것도 허용하지 않습니다.

남은 동작은 셋입니다.

동작 참조되는 행을 지울 때
CASCADE 그 행을 가리키던 행도 함께 지웁니다
SET NULL 가리키던 열을 NULL 로 바꿉니다
SET DEFAULT 가리키던 열을 기본값으로 바꿉니다. 그 기본값이 외래 키를 못 맞추면 작업이 실패합니다

관련 항목

참조와 같은 일을 하는 다른 이름

포인터 · 핸들 · 디스크립터 · 식별자 · 심볼릭 링크 · URL · 별칭

참조를 따라가거나 풀어내는 연산

역참조 · 널 검사 · 얕은 복사 · 깊은 복사 · 복사 · 조인 · 주소 산술

참조가 가리키는 대상과 그것이 담기는 구조

힙 · 스택 · 메모리 · 객체 · 인스턴스 · 필드 · 레코드 · 노드 · 테이블

참조를 세어 대상을 치우는 방식

가비지 컬렉션 · 참조 카운팅 · ARC · 루트 집합 · 도달 가능성 · 약한 참조 · 강한 참조 · 미소유 참조 · 소유권

참조가 끊어지거나 엉킬 때 나는 오류

널 참조 · 널 포인터 · NullPointerException · 댕글링 포인터 · 해제 후 사용 · 정의되지 않은 동작 · 순환 참조 · 강한 참조 순환 · 이중 해제 · 메모리 누수 · 세그멘테이션 폴트

인자를 매개변수로 넘기는 방식

값에 의한 전달 · 참조에 의한 전달 · 매개변수 · 참조 매개변수 · 값 타입 · 참조 타입

저장소에서 커밋을 가리키는 이름

브랜치 · 태그 · 경량 태그 · 주석 태그 · HEAD · 심볼릭 참조 · 커밋 · SHA-1 · 원격 추적 브랜치 · origin

참조를 주고받는 저장소 명령

푸시 · 페치 · 풀 · git update-ref · 원격 저장소 · 로컬 저장소 · Git

데이터베이스에서 다른 행을 가리키는 값

외래 키 · 기본 키 · 참조 무결성 · ON DELETE · CASCADE · 정규화 · 행 · 관계형 데이터베이스 · 문서 데이터베이스

참조와 값을 가르는 언어 기능

스마트 포인터 · 옵셔널 · 불변 · 객체 동일성 · equals

다른 이름: reference · 레퍼런스