깊은 복사(deep copy)와 얕은 복사(shallow copy)

깊은 복사(deep copy)와 얕은 복사(shallow copy)

들어가며

프로그래밍 언어마다 데이터를 처리하는 방법이 다릅니다. 그 가운데 깊은 복사(deep copy)와 얕은 복사(shallow copy)에 대해 알아고자 합니다. JavaScript를 시작으로 깊은 복사와 얕은 복사를 어떻게 지원하는지 알아보겠습니다. 또한 Haskell은 왜 깊은 복사와 얕은 복사를 지원하지 않는지도 알아보겠습니다.

정의

깊은 복사와 얕은 복사는 프로그래밍에서 객체를 복사하는 두 가지 방법입니다. 깊은 복사는 원래 객체의 모든 값이 복사되고 상호 연결되지 않고 기존 독자적으로 존재하는 새 객체를 만듭니다. 반면에 얕은 복사는 원본 객체와 일부 값을 공유하는 새로운 객체를 만듭니다.

자바스크립트(JavaScript)

Deep copy in JavaScript

JavaScript에서는 JSON.stringify()JSON.parse() 메서드를 사용하여 객체의 전체 복사본을 만들 수 있습니다. 이 메서드는 객체를 JSON 문자열로 변환한 다음 다시 새 객체로 구문 분석합니다.

let source = {
    a: 0,
    b: {
        c: 0
    }
};

// deep copy
let target = JSON.parse(JSON.stringify(source));

console.log("source object: ", source);
console.log("target object: ", target);

source.b.c = 100;

console.log("source object: ", source);
console.log("target object: ", target);
source object: { a: 0, b: { c: 0 } }
target object: { a: 0, b: { c: 0 } }
source object: { a: 0, b: { c: 100 } }
target object: { a: 0, b: { c: 0 } }

Shallow copy in JavaScript

JavaScript에서는 Object.assign() 메서드를 사용하여 객체의 얕은 복사본을 만들 수 있습니다. 이 메서드는 하나 이상의 소스 객체에서 대상 객체로 열거 가능한 모든 자체 속성을 복사합니다.

let source = {
    a: 0,
    b: {
        c: 0
    }
};

// shallow copy
let target = Object.assign({}, source);

console.log("source object: ", source);
console.log("target object: ", target);

source.b.c = 100;

console.log("source object: ", source);
console.log("target object: ", target);
source object: { a: 0, b: { c: 0 } }
target object: { a: 0, b: { c: 0 } }
source object: { a: 0, b: { c: 100 } }
target object: { a: 0, b: { c: 100 } }

파이썬(Python)

Deep copy in Python

Python에서는 복사 모듈을 사용하여 객체의 깊은 복사본 또는 얕은 복사본을 만들 수 있습니다. 깊은 복사본은 copy.deepcopy()로 만듭니다.

import copy

source = {
    'a': 0,
    'b': {
        'c': 0
    }
}

# deep copy
target = copy.deepcopy(source)

print('source dictionary: ', source)
print('target dictionary: ', target)

source['b']['c'] = 100

print('source dictionary: ', source)
print('target dictionary: ', target)
source dictionary: {'a': 0, 'b': {'c': 0}}
target dictionary: {'a': 0, 'b': {'c': 0}}
source dictionary: {'a': 0, 'b': {'c': 100}}
target dictionary: {'a': 0, 'b': {'c': 0}}

Shallow copy in Python

얕은 복사본은 copy.copy()로 만듭니다.

import copy

source = {
    'a': 0,
    'b': {
        'c': 0
    }
}

# shallow copy
target = copy.copy(source)

print('source dictionary: ', source)
print('target dictionary: ', target)

source['b']['c'] = 100

print('source dictionary: ', source)
print('target dictionary: ', target)
source dictionary: {'a': 0, 'b': {'c': 0}}
target dictionary: {'a': 0, 'b': {'c': 0}}
source dictionary: {'a': 0, 'b': {'c': 100}}
target dictionary: {'a': 0, 'b': {'c': 100}}

자바(Java)

Deep copy in Java

deep copy란 객체의 실제 값을 새로운 객체로 복사하는 것으로, 원본 객체와 복사된 객체가 서로 다른 메모리 주소를 가지고 있습니다. 따라서 원본 객체의 값이 변경되어도 복사된 객체의 값에는 영향을 미치지 않습니다.

Java에서 객체를 deep copy하는 방법은 다음과 같습니다.

  • clone() 메서드 사용하기: clone() 메서드는 Object 클래스에 정의된 메서드로, 자신을 복제하여 새로운 인스턴스를 반환합니다. 이때 clone() 메서드는 얕은 복사를 수행하므로, 내부에 참조 타입의 필드가 있는 경우에는 해당 필드도 clone() 메서드를 호출해야 합니다. 또한 clone() 메서드를 사용하려면 Cloneable 인터페이스를 구현하고 CloneNotSupportedException을 처리해야 합니다.

  • 복사 생성자 (Copy Constructor) 사용하기: 복사 생성자란 매개변수로 같은 타입의 다른 객체를 받아서 그 객체의 값을 복사하여 새로운 인스턴스를 생성하는 생성자입니다. 이때 복사 생성자는 내부에 참조 타입의 필드가 있는 경우에는 해당 필드도 새로운 인스턴스로 생성해야 합니다.

  • 복사 팩토리 (Copy Factory) 사용하기: 복사 팩토리란 매개변수로 같은 타입의 다른 객체를 받아서 그 객체의 값을 복사하여 새로운 인스턴스를 반환하는 정적 메서드입니다. 이때 복사 팩토리는 내부에 참조 타입의 필드가 있는 경우에는 해당 필드도 새로운 인스턴스로 생성해야 합니다.

  • 직렬화 (Serialization) 사용하기: 직렬화란 객체의 상태 정보를 바이트 스트림으로 변환하는 과정입니다. 직렬화된 바이트 스트림을 역직렬화하면 원래의 객체와 동일한 값을 가진 새로운 인스턴스가 생성됩니다. 이때 직렬화하려면 Serializable 인터페이스를 구현하고 transient 키워드가 붙은 필드는 직렬화 대상에서 제외됩니다.

  • 외부 라이브러리 사용하기: 외부 라이브러리 중에는 깊은 복사를 제공하는 것들이 있습니다. 예를 들어 Gson, Jackson, Apache Commons Lang 등이 있습니다.

아래 예제는 복사 생성자 (Copy Constructor) 사용하여 객체의 깊은 복사본으로 새 객체를 만듭니다.


class Source {
    int a;
    B b;

    public Source(int a, B b) {
        this.a = a;
        this.b = b;
    }

    public Source(Source source) {
        this.a = source.a;
        this.b = new B(source.b);
    }

    @Override
    public String toString() {
        return "{a=" + this.a + ", b=" + this.b + "}";
    }
}

class B {
    int c;

    public B(int c) {
        this.c = c;
    }

    public B(B b) {
        this.c = b.c;
    }

    @Override
    public String toString() {
        return "{c=" + this.c + "}";
    }
}
public class App {

    public static void main(String[] args) {

        B b = new B(0);
        Source source = new Source(0, b);
        Source target = new Source(source);

        System.out.println("source object: " + source);
        System.out.println("target object: " + target);

        source.b.c = 100;

        System.out.println("source object: " + source);
        System.out.println("target object: " + target);
    }
}
source object: {a=0, b={c=0}}
target object: {a=0, b={c=0}}
source object: {a=0, b={c=100}}
target object: {a=0, b={c=0}}

Shallow copy in Java

clone() 메서드, 복사 생성자, 복사 팩토리, 직렬화, 그리고 외부 라이브러리 없이 생성자로 객체를 만들면 얕은 복사가 됩니다.

class Source {
    int a;
    B b;

    public Source(int a, B b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public String toString() {
        return "{a=" + this.a + ", b=" + this.b + "}";
    }
}

class B {
    int c;

    public B(int c) {
        this.c = c;
    }

    @Override
    public String toString() {
        return "{c=" + this.c + "}";
    }
}
public class App {

    public static void main(String[] args) {

        B b = new B(0);
        Source source = new Source(0, b);
        Source target = new Source(source.a, source.b);

        System.out.println("source object: " + source);
        System.out.println("target object: " + target);

        source.b.c = 100;

        System.out.println("source object: " + source);
        System.out.println("target object: " + target);
    }
}
source object: {a=0, b={c=0}}
target object: {a=0, b={c=0}}
source object: {a=0, b={c=100}}
target object: {a=0, b={c=100}}

스칼라(Scala)

Deep copy in Scala

클래스에 deepCopy 메소드를 추가해 새로운 B 인스턴스를 생성합니다. 생성한 인스턴스로 깊은 복사본을 만들 수 있습니다. Scala에서 val 은 불변 변수이므로 한 번 할당하면 값을 변경할 수 업습니다. 그래서 B 클래스의 cvar 로 선언합니다.

case class B(var c: Int)
case class Source(a: Int, b: B) {
  def deepCopy(): Source = {
    val copiedB = B(b.c)
    Source(a, copiedB)
  }
}

@main def main: Unit =
  val source = Source(0, B(0))

  // deep copy
  val target = source.deepCopy()

  println("source object: " + source)
  println("target object: " + target)

  source.b.c  = 100

  println("source object: " + source)
  println("target object: " + target)
source object: Source(0,B(0))
target object: Source(0,B(0))
source object: Source(0,B(100))
target object: Source(0,B(0))

Shallow copy in Scala

case class B(var c: Int)
case class Source(a: Int, b: B)

@main def main: Unit =
  val source = Source(0, B(0))

  // shallow copy
  val target = source.copy()

  println("source object: " + source)
  println("target object: " + target)

  source.b.c  = 100

  println("source object: " + source)
  println("target object: " + target)
source object: Source(0,B(0))
target object: Source(0,B(0))
source object: Source(0,B(100))
target object: Source(0,B(100))

좀 더 안전한 방법

클래스의 val 값을 var 로 바꾸어 기존 인스턴스를 가변으로 만드는 것보다 완전 새로운 인스턴스의 값을 바꾸는 방법이 더 안전합니다.

case class B(c: Int)
case class Source(a: Int, b: B)

@main def main: Unit =
  val source = Source(0, B(0))

  // shallow copy
  val target = source.copy()

  println("source object: " + source)
  println("target object: " + target)

  val newSource = source.copy(b = source.b.copy(c = 100))

  println("source object: " + source)
  println("newSource object: " + newSource)
  println("target object: " + target)
source object: Source(0,B(0))
target object: Source(0,B(0))
source object: Source(0,B(0))
newSource object: Source(0,B(100))
target object: Source(0,B(0))

오캐믈(OCaml)

Deep copy in OCaml

OCaml 프로그래밍 언어는 깊은 복사와 얕은 복사를 모두 지원합니다. 깊은 복사는 객체가 가진 모든 멤버를 새로운 객체로 복사하는 것을 말하고, 얕은 복사는 객체가 가진 참조값만을 복사하는 것을 말합니다. OCaml에서는 copy라는 함수를 사용하여 깊은 복사를 할 수 있습니다.

type b = {
  mutable c : int;
}
type source = {
  a : int;
  b : b;
}

let source = {
  a = 0;
  b = {
    c = 0;
  }
}

(* deep copy *)
let target = { 
  a = source.a; 
  b = { 
    c = source.b.c; 
  } 
}

let () =
  Printf.printf "source object: %d %d\n" source.a source.b.c;
  Printf.printf "target object: %d %d\n" target.a target.b.c;

  source.b.c <- 100;

  Printf.printf "source object: %d %d\n" source.a source.b.c;
  Printf.printf "target object: %d %d\n" target.a target.b.c
source object: 0 0
target object: 0 0
source object: 0 100
target object: 0 0

Shallow copy in OCaml

아래 코드 sourcetarget은 같은 객체를 가리키게 됩니다.

type b = {
  mutable c : int;
}
type source = {
  a : int;
  b : b;
}

let source = {
  a = 0;
  b = {
    c = 0;
  }
}

(* shallow copy *)
let target = source

let () =
  Printf.printf "source object: %d %d\n" source.a source.b.c;
  Printf.printf "target object: %d %d\n" target.a target.b.c;

  source.b.c <- 100;

  Printf.printf "source object: %d %d\n" source.a source.b.c;
  Printf.printf "target object: %d %d\n" target.a target.b.c
source object: 0 0
target object: 0 0
source object: 0 100
target object: 0 100

러스트(Rust)

Deep copy in Rust

Rust는 객체를 복사할 때 참조를 복사하는 것이 아니라 값 자체를 복사합니다. 따라서 Rust에서는 깊은 복사할 필요가 없습니다. 하지만 포인터나 힙 데이터와 같은 복잡한 타입을 복사하려면 Clone 트레잇(trait)을 구현해야 합니다. Clone 트레잇은 clone() 메서드를 제공합니다. 이 메서드는 명시적으로 객체의 전체 내용을 새로운 객체로 만듭니다.

#[derive(Debug)]
struct Source {
    a: i32,
    b: B,
}

#[derive(Debug)]
struct B {
    c: i32,
}

impl Clone for Source {
    fn clone(&self) -> Self {
        Self {
            a: self.a,
            b: self.b.clone(),
        }
    }
}

impl Clone for B {
    fn clone(&self) -> Self {
        Self { c: self.c }
    }
}

fn main() {
    let mut source = Source { a: 0, b: B { c: 0 } };
    let target = source.clone();

    println!("source object: {:?}", source);
    println!("target object: {:?}", target);

    source.b.c = 100;

    println!("source object: {:?}", source);
    println!("target object: {:?}", target);
}
source object: Source { a: 0, b: B { c: 0 } }                                                                             
target object: Source { a: 0, b: B { c: 0 } }                                                                             
source object: Source { a: 0, b: B { c: 100 } }                                                                           
target object: Source { a: 0, b: B { c: 0 } }

Not exist shallow copy in Rust

Rust에서는 얕은 복사라는 개념이 없습니다. Rust에서 객체를 복사할 때 참조를 복사하는 것이 아니라 값 자체를 복사하거나 소유권을 이전합니다. 따라서 JavaScript의 Object.assign과 같은 함수가 Rust에는 존재하지 않습니다. Rust에서는 참조 카운터나 공유 참조와 같은 타입을 사용하여 데이터를 공유할 수 있습니다.

#[derive(Debug)]
struct Source {
    a: i32,
    b: B,
}

#[derive(Debug)]
struct B {
    c: i32,
}

impl Clone for Source {
    fn clone(&self) -> Self {
        Self {
            a: self.a,
            b: self.b.clone(),
        }
    }
}

impl Clone for B {
    fn clone(&self) -> Self {
        Self { c: self.c }
    }
}

fn main() {
    let mut source = Source { a: 0, b: B { c: 0 } };
    let target = &source;

    println!("source object: {:?}", source);
    println!("target object: {:?}", target);

    source.b.c = 100;

    println!("source object: {:?}", source);
    // 아래 코드는 동작하지 않습니다.
    // println!("target object: {:?}", target);
}
source object: Source { a: 0, b: B { c: 0 } }                                                                             
target object: Source { a: 0, b: B { c: 0 } }                                                                             
source object: Source { a: 0, b: B { c: 100 } }

하스켈(Haskell)

Not exist deep copy & shallow copy in Haskell

Haskell 프로그래밍 언어는 깊은 복사본과 얕은 복사본을 지원하지 않습니다. Haskell은 순수 함수형 언어이기 때문에 모든 값이 불변(immutable)입니다. 따라서 값을 복사할 필요가 없습니다. Haskell에서는 참조 투명성(referential transparency)이 보장되므로 같은 값은 같은 메모리 주소를 가리킵니다.

Haskell에서는 값을 변경하거나 새로운 값을 생성하기 위해 모나드(monad)라는 개념을 사용합니다. 모나드는 상태(state), 입출력(IO), 예외(exception) 등의 부수 효과(side effect)를 다루기 위한 추상화(abstraction)입니다. 모나드는 타입 생성자(type constructor)로서, 특정한 성질들을 만족하는 함수들을 가지고 있습니다. 모나드의 성질들은 다음과 같습니다.

  • return: 임의의 타입 a를 받아서 모나드 타입 M a로 변환하는 함수입니다. 예를 들어, Maybe 모나드에서 return 5Just 5가 됩니다.

  • (>>=): 바인딩(bind) 또는 체이닝(chain)이라고도 합니다. 모나드 타입 M a와 함수 a -> M b를 받아서 모나드 타입 M b를 반환하는 함수입니다. 예를 들어, Maybe 모나드에서 Just 5 >>= (\x -> return (x + 1))Just 6이 됩니다.

  • (>>): 순차(sequence)라고도 합니다. 모나드 타입 M aM b를 받아서 M b를 반환하는 함수입니다. 예를 들어, Maybe 모나드에서 Just 5 >> NothingNothing이 됩니다.

모나드는 do 표기법(do notation)을 사용하여 간편하게 작성할 수 있습니다. do 표기법은 다음과 같은 규칙으로 변환됩니다:

data B = B { c :: Int
           } deriving (Show)

data Source = Source { a :: Int
                     , b :: B
                     } deriving (Show)

source :: Source
source = Source { a = 0, b = B { c = 0 } }

target :: Source    
target = source

source' :: Source
source' = source { b = (b source) { c = 100 } }

someFunc :: IO ()
someFunc = do
  putStrLn $ "source object: " ++ show source
  putStrLn $ "target object: " ++ show target

  putStrLn $ "source object: " ++ show source'
  putStrLn $ "target object: " ++ show target
source object: Source {a = 0, b = B {c = 0}}                                                                              
target object: Source {a = 0, b = B {c = 0}}                                                                              
source object: Source {a = 0, b = B {c = 100}}                                                                            
target object: Source {a = 0, b = B {c = 0}}

나가며

저는 프로그래밍 언어가 처리하는 깊은 복사와 얕은 복사 같은 방법(how to)에 대한 내용은 목적에 따라 감추거나 몰라도 된다고 봅니다.

예를 들어 하드웨어 제어를 목적으로 한 임베디드 프로그래밍을 할 때 얕은 복사같은 메커니즘을 프로그래머에게 제공하는 것은 좋은 선택입니다. 프로그래머가 하드웨어 자원 최적화를 할 수 있는 모든 가능성을 둬야 합니다.

하지만 데이터 처리 같은 경우 자원 최적화보다 대용량의 데이터 처리를 일관되고 신뢰할 수 있는 결과을 빠르게 얻는 것이 목표가 될 겁니다. 얕은 복사나 깊은 복사 개념을 알려주면 혼란만 있습니다. 불변성을 유지하며 처리하는 방법을 사용하는 것이 장기적으로 비용을 줄이는 방법입니다.

본인이 사용하는 프로그래밍 언어가 주로 어떠한 영역에 사용되는지 먼저 인지합시다. 프로그래밍 언어가 제공하는 다양한 기능 가운데 혼란이 줄 수 있는 기능이 있기도 합니다. 우리는 이러한 기능을 식별하고 검토한 뒤 어떠한 상황에 사용할지 결정해야 합니다. 이 방법은 당장 나타나는 협업을 쉽게 하고 나중에 나타날 결함을 줄여줄 겁니다.