Java System.arraycopy 완벽 가이드
안녕하세요.
이번 포스팅은 Java에서 배열을 효율적으로 복사하는 핵심 메서드인 System.arraycopy에 대해 상세히 알아보겠습니다. 대용량 데이터를 다루거나 성능이 중요한 애플리케이션을 개발할 때 배열 복사 방법은 프로그램의 속도와 메모리 사용에 큰 영향을 미칩니다. 이 글에서는 System.arraycopy의 기본 개념부터 고급 활용법, 성능 분석까지 모든 것을 다루어 여러분의 코딩 실력을 한 단계 업그레이드하는 데 도움을 드리겠습니다.

목차
- System.arraycopy란?
- 기본 문법과 매개변수
- System.arraycopy 사용 예제
- 다양한 데이터 타입 복사하기
- 성능 분석: 다른 복사 방법과 비교
- System.arraycopy의 내부 동작 원리
- 주의사항과 제한 사항
- 실전 활용 사례
- 자주 묻는 질문(FAQ)
- 마무리
#1. System.arraycopy란?
System.arraycopy는 Java의 표준 라이브러리에 포함된 네이티브 메서드로, 한 배열에서 다른 배열로 요소를 빠르게 복사하는 기능을 제공합니다. 이 메서드는 JDK 1.0부터 제공되어 왔으며, Java의 가장 기본적이면서도 강력한 배열 조작 도구 중 하나입니다.
주요 특징
- 네이티브 메서드: C/C++로 구현되어 JVM의 최적화된 성능을 제공
- 빠른 속도: 자바의 다른 배열 복사 방법보다 일반적으로 더 빠름
- 메모리 효율성: 새 배열을 생성하지 않고 기존 배열로 직접 복사 가능
- 부분 복사: 배열의 특정 부분만 선택적으로 복사 가능
- 다양한 타입 지원: 기본 데이터 타입과 객체 배열 모두 지원
#2. 기본 문법과 매개변수
System.arraycopy 메서드의 기본 문법은 다음과 같습니다:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
각 매개변수의 의미는 다음과 같습니다:
No | 매개변수 | 설명 |
1 | src | 원본 배열 (복사할 데이터가 있는 배열) |
2 | srcPos | 원본 배열에서 복사를 시작할 인덱스 |
3 | dest | 대상 배열 (데이터가 복사될 배열) |
4 | destPos | 대상 배열에서 데이터를 저장할 시작 인덱스 |
5 | length | 복사할 요소의 개수 |
간단한 예시
int[] source = {1, 2, 3, 4, 5};
int[] destination = new int[5];
// source 배열의 내용을 destination 배열로 복사
System.arraycopy(source, 0, destination, 0, source.length);
// 결과: destination = {1, 2, 3, 4, 5}
#3. System.arraycopy 사용 예제
실제 코드를 통해 System.arraycopy의 다양한 사용법을 알아보겠습니다.
예제 1: 전체 배열 복사
public class ArrayCopyExample1 {
public static void main(String[] args) {
int[] sourceArray = {1, 2, 3, 4, 5};
int[] destArray = new int[sourceArray.length];
// 전체 배열 복사
System.arraycopy(sourceArray, 0, destArray, 0, sourceArray.length);
// 결과 출력
System.out.print("복사된 배열: ");
for (int value : destArray) {
System.out.print(value + " ");
}
// 출력 결과: 복사된 배열: 1 2 3 4 5
}
}
예제 2: 배열의 일부분만 복사
public class ArrayCopyExample2 {
public static void main(String[] args) {
int[] sourceArray = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int[] destArray = new int[5];
// 원본 배열의 인덱스 3부터 5개 요소를 대상 배열의 인덱스 0부터 복사
System.arraycopy(sourceArray, 3, destArray, 0, 5);
// 결과 출력
System.out.print("부분 복사된 배열: ");
for (int value : destArray) {
System.out.print(value + " ");
}
// 출력 결과: 부분 복사된 배열: 4 5 6 7 8
}
}
예제 3: 동일한 배열 내에서 요소 이동
public class ArrayCopyExample3 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
// 인덱스 0부터 3개 요소를 같은 배열의 인덱스 1로 이동
// 주의: 이렇게 하면 array[0]의 값은 유지됨
System.arraycopy(array, 0, array, 1, 3);
// 결과 출력
System.out.print("요소 이동 후 배열: ");
for (int value : array) {
System.out.print(value + " ");
}
// 출력 결과: 요소 이동 후 배열: 1 1 2 3 5
}
}
예제 4: 배열의 특정 요소 삭제
public class ArrayCopyExample4 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
int removeIndex = 2; // 인덱스 2(값 3)를 삭제
// 삭제할 요소 다음부터 끝까지의 요소들을 한 칸씩 앞으로 이동
System.arraycopy(array, removeIndex + 1, array, removeIndex, array.length - removeIndex - 1);
// 마지막 요소는 0으로 설정 (또는 적절한 기본값)
array[array.length - 1] = 0;
// 결과 출력
System.out.print("요소 삭제 후 배열: ");
for (int value : array) {
System.out.print(value + " ");
}
// 출력 결과: 요소 삭제 후 배열: 1 2 4 5 0
}
}
#4. 다양한 데이터 타입 복사하기
System.arraycopy는 기본 데이터 타입과 객체 배열 모두에 사용할 수 있습니다.
기본 데이터 타입 배열 복사
// byte 배열 복사
byte[] srcBytes = {1, 2, 3, 4, 5};
byte[] destBytes = new byte[srcBytes.length];
System.arraycopy(srcBytes, 0, destBytes, 0, srcBytes.length);
// char 배열 복사
char[] srcChars = {'a', 'b', 'c', 'd', 'e'};
char[] destChars = new char[srcChars.length];
System.arraycopy(srcChars, 0, destChars, 0, srcChars.length);
// boolean 배열 복사
boolean[] srcBooleans = {true, false, true, false, true};
boolean[] destBooleans = new boolean[srcBooleans.length];
System.arraycopy(srcBooleans, 0, destBooleans, 0, srcBooleans.length);
// double 배열 복사
double[] srcDoubles = {1.1, 2.2, 3.3, 4.4, 5.5};
double[] destDoubles = new double[srcDoubles.length];
System.arraycopy(srcDoubles, 0, destDoubles, 0, srcDoubles.length);
객체 배열 복사 (얕은 복사)
public class ObjectArrayCopyExample {
static class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public static void main(String[] args) {
// 객체 배열 생성
Person[] sourcePeople = {
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 35)
};
// 동일한 크기의 대상 배열 생성
Person[] destPeople = new Person[sourcePeople.length];
// 객체 배열 복사 (얕은 복사)
System.arraycopy(sourcePeople, 0, destPeople, 0, sourcePeople.length);
// 결과 출력
System.out.println("원본 배열과 복사된 배열의 참조 비교:");
for (int i = 0; i < sourcePeople.length; i++) {
System.out.println("sourcePeople[" + i + "] == destPeople[" + i + "]: "
+ (sourcePeople[i] == destPeople[i]));
}
// 원본 배열의 객체 수정
sourcePeople[0].name = "Alice Modified";
// 복사된 배열에서도 변경 확인
System.out.println("\n원본 배열 객체 수정 후 복사된 배열:");
for (Person person : destPeople) {
System.out.println(person);
}
}
}
위 예제에서 객체 배열을 복사할 때는 참조만 복사되는 얕은 복사(shallow copy)가 발생합니다. 즉, 원본 배열과 대상 배열의 각 요소는 동일한 객체를 참조합니다.
#5. 성능 분석: 다른 복사 방법과 비교
Java에서 배열을 복사하는 방법은 여러 가지가 있습니다. 각 방법의 성능을 비교해보겠습니다.
배열 복사 방법들
- System.arraycopy(): 네이티브 메서드로 가장 빠른 성능 제공
- Arrays.copyOf(): 내부적으로 System.arraycopy 호출, 새 배열 생성
- Arrays.copyOfRange(): 부분 배열 복사, 내부적으로 System.arraycopy 사용
- clone(): 배열의 복제본 생성, 새 배열 반환
- 반복문을 사용한 수동 복사: 요소별로 직접 복사
성능 비교 코드
public class ArrayCopyPerformanceTest {
private static final int ARRAY_SIZE = 10_000_000;
private static final int ITERATIONS = 100;
public static void main(String[] args) {
int[] sourceArray = new int[ARRAY_SIZE];
for (int i = 0; i < ARRAY_SIZE; i++) {
sourceArray[i] = i;
}
// 1. System.arraycopy
long startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
int[] destArray = new int[ARRAY_SIZE];
System.arraycopy(sourceArray, 0, destArray, 0, ARRAY_SIZE);
}
long systemArrayCopyTime = System.nanoTime() - startTime;
// 2. Arrays.copyOf
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
int[] destArray = Arrays.copyOf(sourceArray, ARRAY_SIZE);
}
long arraysCopyOfTime = System.nanoTime() - startTime;
// 3. Arrays.copyOfRange
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
int[] destArray = Arrays.copyOfRange(sourceArray, 0, ARRAY_SIZE);
}
long arraysCopyOfRangeTime = System.nanoTime() - startTime;
// 4. clone
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
int[] destArray = sourceArray.clone();
}
long cloneTime = System.nanoTime() - startTime;
// 5. Manual copy with loop
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
int[] destArray = new int[ARRAY_SIZE];
for (int j = 0; j < ARRAY_SIZE; j++) {
destArray[j] = sourceArray[j];
}
}
long manualLoopTime = System.nanoTime() - startTime;
// 결과 출력 (밀리초 단위로 변환)
System.out.println("배열 크기: " + ARRAY_SIZE);
System.out.println("반복 횟수: " + ITERATIONS);
System.out.println("System.arraycopy 시간: " + systemArrayCopyTime / 1_000_000 + " ms");
System.out.println("Arrays.copyOf 시간: " + arraysCopyOfTime / 1_000_000 + " ms");
System.out.println("Arrays.copyOfRange 시간: " + arraysCopyOfRangeTime / 1_000_000 + " ms");
System.out.println("clone 시간: " + cloneTime / 1_000_000 + " ms");
System.out.println("수동 반복문 시간: " + manualLoopTime / 1_000_000 + " ms");
}
}
성능 비교 결과
실행 환경에 따라 결과가 다를 수 있지만, 일반적으로 다음과 같은 성능 순서를 보입니다:
- System.arraycopy(): 가장 빠름 (네이티브 구현)
- clone(): System.arraycopy보다 약간 느림
- Arrays.copyOf(): System.arraycopy를 내부적으로 사용하지만 새 배열 생성 오버헤드 있음
- Arrays.copyOfRange(): copyOf와 유사하지만 범위 계산 오버헤드 추가
- 수동 반복문: 가장 느림 (JVM 최적화 부족)
배열 크기가 클수록 System.arraycopy의 성능 이점이 더 두드러집니다.
#6. System.arraycopy의 내부 동작 원리
System.arraycopy는 Java Native Interface(JNI)를 통해 구현된 네이티브 메서드입니다. 내부적으로는 C/C++로 작성되어 있어 JVM이 직접 최적화된 메모리 복사 작업을 수행합니다.
주요 동작 원리
- 메모리 블록 복사: 배열은 연속된 메모리 공간에 저장되므로, 시스템 수준에서 효율적인 메모리 블록 복사 연산이 수행됩니다.
- 타입 검사: JVM은 복사 과정에서 배열 타입의 호환성을 검사합니다. 호환되지 않는 타입의 배열 간 복사를 시도하면 ArrayStoreException이 발생합니다.
- 경계 검사: 원본 및 대상 배열의 인덱스와 길이가 유효한지 검사합니다. 배열 범위를 벗어나면 IndexOutOfBoundsException이 발생합니다.
OpenJDK 구현
OpenJDK의 System.arraycopy 구현은 다음과 같이 동작합니다:
- HotSpot JVM에서는
Unsafe.copyMemory()
또는 최적화된 메모리 복사 함수를 사용 - 원시 타입 배열은 직접 메모리 복사로 처리
- 객체 배열은 각 요소의 참조를 복사하고 필요한 경우 타입 검사를 수행
#7. 주의사항과 제한 사항
System.arraycopy를 사용할 때 알아두어야 할 주의사항들입니다.
1. 타입 호환성
원본 배열과 대상 배열은 타입이 호환되어야 합니다. 예를 들어, int[] 배열에서 double[] 배열로 직접 복사할 수 없습니다.
int[] sourceInts = {1, 2, 3};
double[] destDoubles = new double[3];
// 오류 발생: ArrayStoreException
System.arraycopy(sourceInts, 0, destDoubles, 0, 3);
객체 배열의 경우, 하위 타입에서 상위 타입으로는 복사가 가능하지만, 역방향은 불가능합니다.
// 가능: String은 Object의 하위 타입
String[] sourceStrings = {"A", "B", "C"};
Object[] destObjects = new Object[3];
System.arraycopy(sourceStrings, 0, destObjects, 0, 3);
// 불가능: Object에서 String으로 직접 복사 시도
Object[] sourceObjects = {"A", "B", "C"};
String[] destStrings = new String[3];
// 다음 코드는 런타임 오류 발생 가능성 있음
System.arraycopy(sourceObjects, 0, destStrings, 0, 3);
2. 배열 범위 문제
배열 범위를 벗어나는 복사 시도는 IndexOutOfBoundsException을 발생시킵니다.
int[] source = {1, 2, 3, 4, 5};
int[] dest = new int[3];
// 오류 발생: 원본 배열 크기(5)가 대상 배열 크기(3)보다 큼
System.arraycopy(source, 0, dest, 0, 5);
// 오류 발생: srcPos(3) + length(5) > source.length(5)
System.arraycopy(source, 3, dest, 0, 5);
3. null 배열 처리
원본 또는 대상 배열이 null인 경우 NullPointerException이 발생합니다.
int[] source = {1, 2, 3};
int[] dest = null;
// 오류 발생: NullPointerException
System.arraycopy(source, 0, dest, 0, 3);
4. 얕은 복사 주의
객체 배열 복사 시 얕은 복사(shallow copy)가 수행됩니다. 즉, 객체 자체가 아닌 객체에 대한 참조만 복사됩니다.
// Person 객체 배열
Person[] people1 = {new Person("Alice"), new Person("Bob")};
Person[] people2 = new Person[2];
// 얕은 복사 수행
System.arraycopy(people1, 0, people2, 0, 2);
// 원본 배열의 객체 수정이 복사본에도 영향을 미침
people1[0].name = "Charlie";
System.out.println(people2[0].name); // 출력: Charlie
#8. 실전 활용 사례
1. 동적 배열 구현 (ArrayList와 유사)
public class DynamicArray {
private int[] array;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public DynamicArray() {
array = new int[DEFAULT_CAPACITY];
size = 0;
}
public void add(int element) {
if (size == array.length) {
// 배열 크기 조정 필요
resize();
}
array[size++] = element;
}
private void resize() {
// 기존 배열 크기의 두 배로 새 배열 생성
int[] newArray = new int[array.length * 2];
// System.arraycopy로 기존 요소 복사
System.arraycopy(array, 0, newArray, 0, size);
// 새 배열로 참조 업데이트
array = newArray;
}
public void remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
// 삭제할 요소 이후의 모든 요소를 한 칸씩 앞으로 이동
System.arraycopy(array, index + 1, array, index, size - index - 1);
// 마지막 요소 정리 및 크기 감소
array[--size] = 0;
}
public int get(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
return array[index];
}
public int size() {
return size;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < size; i++) {
sb.append(array[i]);
if (i < size - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
DynamicArray dynamicArray = new DynamicArray();
// 요소 추가
for (int i = 0; i < 15; i++) {
dynamicArray.add(i * 10);
}
System.out.println("초기 배열: " + dynamicArray);
// 요소 삭제
dynamicArray.remove(5);
System.out.println("인덱스 5 삭제 후: " + dynamicArray);
// 첫 번째 요소 삭제
dynamicArray.remove(0);
System.out.println("첫 번째 요소 삭제 후: " + dynamicArray);
}
}
2. 큐(Queue) 구현
public class ArrayQueue {
private int[] elements;
private int front;
private int rear;
private int size;
private int capacity;
public ArrayQueue(int capacity) {
this.capacity = capacity;
elements = new int[capacity];
front = 0;
rear = -1;
size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
public void enqueue(int item) {
if (isFull()) {
throw new IllegalStateException("Queue is full");
}
rear = (rear + 1) % capacity;
elements[rear] = item;
size++;
}
public int dequeue() {
if (isEmpty()) {
throw new IllegalStateException("Queue is empty");
}
int item = elements[front];
front = (front + 1) % capacity;
size--;
return item;
}
public int peek() {
if (isEmpty()) {
throw new IllegalStateException("Queue is empty");
}
return elements[front];
}
public int size() {
return size;
}
// 큐 내용을 새 배열로 복사하여 반환
public int[] toArray() {
int[] result = new int[size];
if (front <= rear) {
// 단순한 경우: front에서 rear까지 연속된 영역
System.arraycopy(elements, front, result, 0, size);
} else {
// 복잡한 경우: front부터 배열 끝까지, 그리고 0부터 rear까지
int firstPart = capacity - front;
System.arraycopy(elements, front, result, 0, firstPart);
System.arraycopy(elements, 0, result, firstPart, rear + 1);
}
return result;
}
public static void main(String[] args) {
ArrayQueue queue = new ArrayQueue(5);
// 큐에 요소 추가
queue.enqueue(10);
queue.enqueue(20);
queue.enqueue(30);
// 앞쪽 요소 확인
System.out.println("큐의 첫 번째 요소: " + queue.peek());
// 요소 제거
int removed = queue.dequeue();
System.out.println("큐에서 제거된 요소: " + removed);
// 현재 큐 상태 확인
System.out.print("현재 큐 내용: ");
for (int item : queue.toArray()) {
System.out.print(item + " ");
}
}
}
3. 배열 합치기
public class ArrayMergeExample {
public static int[] mergeArrays(int[] arr1, int[] arr2) {
int[] result = new int[arr1.length + arr2.length];
// 첫 번째 배열 복사
System.arraycopy(arr1, 0, result, 0, arr1.length);
// 두 번째 배열 복사
System.arraycopy(arr2, 0, result, arr1.length, arr2.length);
return result;
}
public static void main(String[] args) {
int[] first = {1, 2, 3, 4, 5};
int[] second = {6, 7, 8, 9, 10};
int[] merged = mergeArrays(first, second);
System.out.print("합쳐진 배열: ");
for (int value : merged) {
System.out.print(value + " ");
}
}
}
#9. 자주 묻는 질문(FAQ)
Q1: System.arraycopy와 Arrays.copyOf의 차이점은 무엇인가요?
A: 주요 차이점은 다음과 같습니다:
- 반환 값: System.arraycopy는 void를 반환하며 기존 배열에 복사하고, Arrays.copyOf는 새 배열을 생성하여 반환합니다.
- 기능: System.arraycopy는 더 세밀한 제어(시작 위치, 길이 등)가 가능하고, Arrays.copyOf는 보다 간단한 인터페이스를 제공합니다.
- 내부 구현: Arrays.copyOf는 내부적으로 System.arraycopy를 호출합니다.
- 성능: Arrays.copyOf는 새 배열 생성 오버헤드 때문에 System.arraycopy보다 약간 느립니다.
Q2: 배열을 복사할 때 발생할 수 있는 예외는 무엇인가요?
A: System.arraycopy 사용 시 발생할 수 있는 주요 예외는 다음과 같습니다:
- ArrayStoreException: 호환되지 않는 타입의 배열 간 복사 시도
- IndexOutOfBoundsException: 배열 범위를 벗어나는 인덱스 사용
- NullPointerException: 원본 또는 대상 배열이 null인 경우
Q3: 객체 배열의 깊은 복사(deep copy)를 하려면 어떻게 해야 하나요?
A: System.arraycopy는 얕은 복사만 수행합니다. 깊은 복사를 구현하려면 각 객체를 개별적으로 복제해야 합니다:
// 깊은 복사 예제
Person[] original = {new Person("Alice"), new Person("Bob")};
Person[] deepCopy = new Person[original.length];
// 각 객체를 복제하여 새 배열에 저장
for (int i = 0; i < original.length; i++) {
deepCopy[i] = original[i].clone(); // Person 클래스에 clone 메서드 구현 필요
}
또는 직렬화/역직렬화를 사용할 수도 있습니다.
Q4: 다차원 배열도 System.arraycopy로 복사할 수 있나요?
A: System.arraycopy는 1차원 배열 복사에 최적화되어 있습니다. 다차원 배열의 경우 첫 차원만 얕은 복사가 됩니다. 완전한 복사를 위해서는 중첩 반복문이나 재귀를 사용해야 합니다:
// 2차원 배열 복사 예제
int[][] original = {{1, 2}, {3, 4}, {5, 6}};
int[][] copy = new int[original.length][];
// 각 하위 배열 복사
for (int i = 0; i < original.length; i++) {
copy[i] = new int[original[i].length];
System.arraycopy(original[i], 0, copy[i], 0, original[i].length);
}
Q5: System.arraycopy가 clone() 메서드보다 항상 더 빠른가요?
A: 대부분의 경우 System.arraycopy가 clone()보다 빠르지만, 매우 작은 배열에서는 그 차이가 미미할 수 있습니다. 또한 최신 JVM은 clone() 메서드 호출도 많이 최적화했기 때문에 성능 차이가 줄어들었습니다. 그러나 대용량 배열이나 성능이 중요한 애플리케이션에서는 여전히 System.arraycopy가 선호됩니다.
#10. 마무리
이 글에서는 Java의 System.arraycopy 메서드에 대해 상세히 알아보았습니다. 기본 사용법부터 성능 분석, 실전 활용 사례까지 다양한 측면에서 이 강력한 배열 복사 메서드를 살펴보았습니다.
System.arraycopy는 Java의 가장 기본적인 배열 조작 도구 중 하나이면서도, 최적화된 성능을 제공하는 강력한 메서드입니다. 특히 대용량 데이터를 다루거나 성능에 민감한 애플리케이션에서는 이 메서드의 특성을 잘 이해하고 활용하는 것이 중요합니다.
긴 글 읽어주셔서 감사합니다.
끝.
참고 자료
- Java API Documentation - System.arraycopy
- Java API Documentation - Arrays
- Effective Java (Joshua Bloch)
- Java Performance: The Definitive Guide (Scott Oaks)
'■Development■ > 《Java》' 카테고리의 다른 글
[Java] System.arraycopy vs Arrays.copyOfRange 차이점 비교 (0) | 2025.03.25 |
---|---|
[Java] throw와 throws의 차이 (0) | 2024.07.24 |
[Java] Java Class 파일 DeCompile (0) | 2020.04.08 |
[Java] StringBuffer vs StringBuilder (0) | 2019.09.26 |
[Java] Handler 완벽 가이드 (0) | 2019.09.05 |