문제인식
첫 궁금증은 쏙쏙 들어오는 함수형 코딩이라는 책을 읽으면서 시작되었다.

Copy on Write라는 기법은 어플리케이션 레벨에서 불변성을 지키기 위해 객체를 복사하고 수정을 하는 것이라 알고 있었지만 깊은복사인줄 알았던 방법이 얕은 복사를 사용하여 적용을 한다는 것이었다!
우테코 프리코스를 참가할때 불변성이라는 키워드를 지키기 위해서 VO(Value Object)를 사용하고 값이 변경될때마다 new 키워드로 새 객체를 생성해주던 나는 잘 이해가 안가서 Copy on Write를 좀더 탐구해보기로 하였다.
불변성을 지키기 위한 Copy on Write
먼저 자바 코드로 예를 들어보자.
public class MutableExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Ramen");
list.add("Keyboard");
// 수정 시 원본 데이터를 직접 변경 (In-place modification)
list.set(0, "Sushi");
System.out.println(list); // [Sushi, Keyboard] -> 원본 "Ramen"이 사라짐
}
}
위 코드와 같이 리스트를 직접 set으로 수정하면 불변성이 깨지게 된다.어디서도 수정이 가능하기 때문에 디버깅하기 어려워지고 데이터의 일관성이 없어진다.
또한 스레드에 안전하지 못하기 때문에 Sychronized나 따로 락을 걸어줘야 한다고 한다.
public class ShallowCopyExample {
public static void main(String[] args) {
// 1. 원본 리스트 생성
List<Food> original = new ArrayList<>();
original.add(new Food("Ramen"));
// 2. 얕은 복사 수행 (생성자 이용 혹은 addAll(original); 메서드를 이용)
List<Food> copy = new ArrayList<>(original);
// 3. 구조적 수정
copy.add(new Food("Sushi"));
System.out.println("Original List: " + original); // [Ramen]
System.out.println("Copy List: " + copy); // [Ramen, Sushi]
}
}
위 코드를 보면 새롭게 ArrayList를 선언하고 리스트를 수정한다.
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
public class CoWExample {
public static void main(String[] args) {
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Ramen");
cowList.add("Keyboard");
// 1. 데이터를 수정하는 순간, 내부적으로 배열의 복사본이 생성됨
// 2. 새로운 배열에 "Sushi"를 쓰고, 원본 참조를 교체함
cowList.set(0, "Sushi");
// 수정 후에도 이전에 얻은 Iterator는 '이전 상태(불변)'를 유지함
System.out.println(cowList); // [Sushi, Keyboard]
}
또한 자바의 concurrent 패키지에 있는 CopyOnWriteArrayList를 활용하여 좀 더 쉽게 구현할수도 있다.
이런식의 얕은 복사를 시키고 수정하는 것을 Copy on Write라고 하는데 이 방법엔 여러가지 장점이 있다.
1. 효율적인 자원 관리 (구조적 공유)

Copy-on-Write(CoW) 기법은 수정이 필요할 때만 복사본을 만든다. 이때 모든 데이터를 새로 복사하는 '깊은 복사'가 아니라, 바뀌지 않는 부분은 원본 객체의 주소를 그대로 가리키는 얕은 복사(Shallow Copy)를 활용한다.
- 이득: 전체 데이터를 매번 새로 생성하는 것에 비해 메모리 공간을 획기적으로 아낄 수 있으며(공간 복잡도 이득), 변경이 없는 요소들을 재사용하므로 불필요한 인스턴스 생성을 줄여준다.
- 주의: 단, 수정 시 배열의 주소값 목록을 새로 복사해야 하므로, 직접 수정(Mutation)보다는 O(N)만큼의 비용이 더 발생한다는 점을 인지해야 한다.
2. 멀티스레드 환경에서의 안정성 (Lock-Free Read) 가변(Mutable) 데이터를 여러 스레드가 동시에 수정하면 데이터가 깨지거나 ConcurrentModificationException이 발생할 수 있다. 하지만 CoW는 기존 데이터는 건드리지 않고 새로운 메모리 공간에 복사본을 만들어 수정한 뒤, 참조 주소를 한 번에 교체(Atomic Swap)한다.
- 이득: 읽기 작업에는 별도의 락(Lock)을 걸 필요가 없어 성능 저하가 없다. 쓰기 스레드가 작업을 하는 동안에도 읽기 스레드들은 '수정 전의 안전한 원본'을 계속 볼 수 있어 스레드 안전(Thread-Safety)이 보장된다.
3. 데이터의 불변성(Immutability) 유지 CoW의 가장 큰 철학적 이점은 "한 번 생성된 데이터의 상태는 변하지 않는다"는 원칙을 지키는 것이다.
- 예측 가능한 코드: 어떤 함수나 스레드가 데이터를 읽고 있을 때, 외부의 영향으로 인해 데이터가 중간에 변할 리 없다는 것을 보장한다. 이는 사이드 이펙트(Side Effect)를 방지하여 디버깅을 쉽게 만든다.
- 스냅샷 보존: 수정을 위해 복사를 수행하더라도 원본 데이터는 메모리에 그대로 남아있다(참조가 끊기기 전까지). 이는 마치 데이터의 '특정 시점 상태(Snapshot)'를 보존하는 것과 같아서, 이전 상태로 되돌리거나(Undo) 데이터의 변경 이력을 관리하기에 매우 유리하다. 이는 저장장치에서의 몇몇 파일시스템이나 데이터베이스 서버에서도 사용되는 개념인데 깊은복사와 같은 전체 백업을 대체할수는 없지만 저장 공간을 아낄수 있다는 장점이 있다
운영체제에서의 Copy on Write
사실 CoW(Copy on write) 기법은 사실 프로그래밍 언어보다 훨씬 이전부터 운영체제(OS)가 메모리를 관리하는 핵심 전략이었는데,
가장 대표적인 예가 바로 프로세스를 생성하는 fork() 시스템 콜이다.

새로운 프로세스를 만들 때 부모 프로세스의 메모리를 전부 복사하는 건 엄청난 낭비이다. 메모리 용량도 문제지만, 그 데이터를 다 옮기는 동안 컴퓨터에 엄청난 부하가 걸릴것이다.
그렇기 때문에 OS에서 내놓은 해결책이 있는데 일단 부모와 자식이 같은 메모리 페이지를 공유하게 만든다. (이때는 읽기 전용)

그러다 어느 한쪽이 데이터를 수정(Write)하려고 하면, 그때야 비로소 해당 페이지를 복사해서 독자적인 공간을 만들어준다.
덕분에 프로세스 생성 속도가 비약적으로 빨라지고, 실제로 수정되지 않는 메모리 영역은 계속 공유하면서 자원을 아낄 수 있게 된다.
결론
처음엔 그저 "함수형 프로그래밍에서 불변성을 지키기 위한 코딩 기법"인 줄로만 알았던 Copy-on-Write(CoW)가 알고 보니 운영체제, 데이터베이스, 파일 시스템, 그리고 공부하다 알게 된 사실이지만 도커 컨테이너와 같은 클라우드 인프라에 이르기까지 계층을 구분하지 않고 자원을 극한으로 효율화하기 위해 사용되는 거대한 철학이였다는걸 깨달았다. 앞으로 이런 철학을 새기면서 어떻게 자원을 효율적으로 사용하고 불변성을 지킬수 있는지 되새기는 프로그래머가 되어야겠다.
'탐구' 카테고리의 다른 글
| 노가다에서 벗어나기: ModelMapper보다 MapStruct를 선택한 이유 (1) | 2025.12.30 |
|---|---|
| 영속성 컨테이너의 플러시 타이밍 문제에 관하여... (0) | 2025.07.01 |
| CORS가 협업을 자꾸 힘들게해요 (0) | 2025.04.12 |