관리 메뉴

개발공부기록

Shallow Copy와 Deep Copy의 차이?, 자바에서 Deep Copy를 하기 위해서는 무엇을 사용해야 할까? 본문

이론 직접 정리/자바

Shallow Copy와 Deep Copy의 차이?, 자바에서 Deep Copy를 하기 위해서는 무엇을 사용해야 할까?

소소한나구리 2025. 3. 13. 21:01
728x90

Shallow Copy

Shallow Copy(얕은 복사)

  • 객체를 복사할 때 객체가 가진 필드의 값을 그대로 복사하는 방법을 뜻한다
  • 기본 자료형의 경우 값 자체가 복사되며, 객체(참조형)의 경우 참조 주소(메모리 주소값)만 복사된다는 뜻이다

특징

  • 원본 객체와 복사본 객체는 서로 다른 객체지만 객체 내부에서 참조하고 객체는 동일한 객체를 가리키게 된다.
  • 실제 데이터가 아닌 참조값만 복사하기 때문에 복사 과정이 매우 빠르다
  • 기존 객체의 데이터와 공유되기 때문에 추가적인 메모리 사용량이 매우 적어 메모리 사용이 효율적이다

단점

  • 복사된 객체와 원본 객체가 내부 데이터를 공유한다는 특성을 모르고 접근할 경우 원본 데이터를 변경하면 복사된 객체도 영향을 받으므로 사이드 이펙트(side-effect) 문제가 발생할 수 있다(Mutable 객체의 경우)

Deep Copy

Deep Copy(깊은 복사)

  • 객체를 복사할 때 객체 내부에 존재하는 모든 참조형 필드들까지 독깁적인 새로운 객체로 생성하여 완전히 별개의 객체를 만드는 방법이다

특징

  • 복사된 객체와 원본 객체가 완전히 독립적이기 때문에 한쪽을 수정해도 다른 쪽에 영향을 주지 않는다
  • 원본과 복사된 객체간의 모든 데이터가 완전히 독립적이기 때문에 사이드 이펙트를 걱정할 필요가 없어 안전하다
  • 객체 내부 데이터가 외부에 노출되지 않아 데이터 캡슐화가 자연스럽게 유지된다

단점

  • 새로운 메모리를 할당하고 데이터를 복사해야 하기 때문에 데이터가 크고 복잡한 객체에 대해 복사하는 시간이 많이 걸리게 되어 빈번한 Deep Copy는 애플리케이션의 성능을 저하시킬 수 있다
  • 모든 데이터를 복제하기 때문에 메모리 소비량이 증가하게 된다
  • Deep Copy를 사용하려면 직접 모든 참조 필드를 일일히 복사하거나 직렬화(Seriallization) 같은 메커니즘을 사용해야 하며 객체 구조가 변경되면 복사 로직도 함께 수정해야 하기 때문에 유지보수 비용이 증가할 수 있다.

자바에서 Deep Copy를 하기 위한 방법

1. 복사 생성자(Copy Constructor) - 순수 자바일 때 간편하므로 권장

  • 객체를 복사할 때 같은 타입의 객체를 매개변수로 받아서 새 객체를 생성하는 생성자를 만들어서 Deep Copy를 한다.
  • CopyConstructor 클래스에는 String name과 int[] score가 필드로 존재할 때 일반적으로 값을 초기화하는 생성자는 매개변수의 값을 각 필드에 대입하면서 초기화한다.
  • 복사 생성자는 생성자의 매개변수로 동일 클래스 타입의 객체를 받아서 필드를 꺼내 복사한다.
    • 이때 name 필드는 String 타입이므로 이미 불변객체라 그대로 값이 복사된다.
    • 문제는 int[]인 socre인데 socre는 int[]의 데이터가 저장된 메모리 주소값을 가지고 있어 그냥 대입하면 Shallow Copy가 되기 때문에 clone()메서드를 사용하여 Deep Copy를 구현할 수 있다.

 

  • 실행 테스트를 위해 CopyConstructor student = new CopyConstructor(값 초기화, 배열 초기화)로 객체를 생성하고, new CopyConstructor(student)를 통해 생성자를 통해서 객체를 복사해서 출력해보면 서로 다른 주소값을 가지고 있는 것을 확인할 수 있다.
  • 복사된 studentCopy의 score[0]의 값을 변경해보면 원본 객체인 student의 score 값은 그대로이지만 studentCopy의 score값은 변경되어 서로 독립적으로 객체가 동작하는 것을 확인할 수 있다

2. Cloneable 인터페이스의 clone() 메서드 오버라이딩

  • 위에서 int[]인 score 변수를 복사할 때 사용했던 clone() 메서드를 직접 오버라이딩해서 구현할 수 있다.
  • 직접 구현하고자 하는 객체에 Coloneable 인터페이스를 구현하고 clone()메서드의 로직을 직접 작성하면 되는데 모든 필드별로 개발자가 직접 복제 로직을 작성하는 것이 번거롭기 때문에 불편하다.
  • 단순히 인터페이스의 추상 메서드를 구현하는 방식이기 때문에 예시는 생략한다.

3. 직렬화(Serialization)을 통한 복사

  • Java 내장 직렬화를 사용하여 DeepCopy를 구현할 수도 있다.
  • DeepCopy가 적용되어야 할 객체에 Serializable 인터페이스를 구현해둔다.
  • 해당 인터페이스는 마커 인터페이스로 특별한 기능은 없지만 이 인터페이스를 구현한 객체는 직렬화가 가능한 객체로 인식하게 되며, Serializable 인터페이스가 없는 객체를 직렬화할 경우 오류가 발생한다.(NotSerializableException)

 

 

  • Serializable를 상속받은 모든 객체를 직렬화를 이용한 Deep Copy를 할 수 있는 deepCopy() 메서드 이다.
  • 직렬화를 하기 위해 자바 객체를 바이트(byte)로 변환해야 하는데 메모리상에서 즉시 직렬화와 역직렬화를 처리하기 위해 ByteArrayOutputStream과 ByteArrayInputStream()을 사용하고 이를 통해서 ObjectOutputStream, ObjectInputStream을 생성한다.
  • oos.writeObejct(객체)로 객체를 직렬화후 바로 역직렬화를 하여 Deep Copy를할 수 있다

 

  • new SerializationCopy()로 Serializable을 구현한 객체를 생성하고, 해당 객체를 만들어 둔 deepCopy()메서드를 통해 복사를해서 실행해보면 Deep Copy가 성공적으로 된 것을 확인할 수 있다.
  • 복사된 studentCopy의 score[0]의 값을 변경해보면 원본에 영향이 없이 독립적으로 studentCopy의 score[0]의 값만 변경되었다.

4. JSON 직렬화 방식(Jackson)을 이용한 Deep Copy - 권장

  • 외부 라이브러리이기 때문에 직접 다운받아서 라이브러리를 IDE에 등록해주거나 만약 Maven이나 Gradle의 빌드를 통해서 프로젝트를 생성했다면 의존관계를 추가해야 한다.
  • 순수 자바 프로젝트인 경우 외부라이브러리이기 때문에 의존성을 추가할 때 버전 정보를 추가해야 한다.(최신 버전 권장)
  • 만약 스프링부트 프로젝트로 프로젝트를 생성했다면 Jackson 라이브러리를 내장하고 있어서 버전 정보 없이 의존성을 추가하면 된다
  • 스프링 부트를 사용한다면 보통 spring-web을 의존관계를 등록해서 생성할텐데 그 내부에 Jackson 라이브러리가 포함되어있다.

Maven의 경우

 <!-- 순수 자바 프로젝트 일 경우 명시 필수-->
<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.17.0</version>
    </dependency>
</dependencies>


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

Gradle의 경우

imimplementation 'org.springframework.boot:spring-boot-starter-web' // 스프링 부트 프로젝트
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.0'	// 순수 자바 프로젝트

 

보통 Jackson라이브러리를 사용하는 경우는 스프링 부트 환경일 것이기 때문에 스프링 부트로 프로젝트를 생성한 예제를 구현해보았다.

 

  • Jackson 라이브러리를 사용하려면 기본 생성자가 필요하다 - 이 방법을 권장한다.
  • 만약 기본 생성자를 사용하지 않을 것이라면 각 생성자의 필드에 @JsonProperty()를 통해 필드 매핑을 명확히 해줘야 Jackson이 역직렬화 할 때 오류가 발생하지 않는다.

 

  • 보통 실무에서는 Util클래스를 별도의 패키지로 Util 클래스들을 모아서 관리하지만 지금은 같은 패키지에 만들었다.
  • 스프링 부트 프로젝트에서는 Jackson 라이브러리를 사용하기 위한 ObjectMapper를 기본적으로 스프링 빈으로 등록되어있기 때문에 생성하여 스프링 빈으로 등록할 필요 없이 의존관계 주입을 통해 사용하면 된다
  • 주입 받은 mapper를 writeValueAsString(object)로 JSON 문자열로 직렬화 한 다음, mapper.readValue(직렬화, 클래스)를 통해서 역직렬화 하여 새로운 객체로 반환된다

 

  • 애플리케이션을 실행하는 main()메서드가 있는 Application클래스에서 DeepCopyUtil를 주입하면 준비가 모두 끝난다
  • 스프링 부트 프로젝트에서 콘솔 출력을 하기 위해 CommandLineRunner 인터페이스를 구현하여 run()메서드를 오버라이딩하고 객체를 deepCopy해보면 원본 Student 객체와  복제된 Student 객체가 독립적으로 별도의 주소값을 가지고 있는 것을 확인할 수 있다.
728x90