2026.06.23 기준 Oracle Java 공식 문서와 Java Language Specification 내용을 확인해 정리했습니다.
자바를 공부하다 보면 void 메서드인데도 객체 값이 바뀌는 상황을 자주 만납니다.
분명히 반환값이 없는데, 메서드 호출 후 객체 안의 필드 값이 달라져 있으면 “자바가 객체를 참조로 전달하는 건가?”라는 생각이 들 수 있습니다.
결론부터 말하면, 자바는 항상 값에 의한 전달(pass by value) 입니다. 다만 객체를 넘길 때는 객체 자체가 아니라 객체를 가리키는 참조값의 복사본이 전달되기 때문에, 그 복사된 참조값으로 같은 객체의 내부 필드를 바꿀 수 있습니다. Oracle Java Tutorial도 참조형 매개변수 역시 값으로 전달되며, 메서드 안에서 객체 필드는 변경될 수 있다고 설명합니다.
자바 객체 참조 전달 개념 먼저 정리
🔉 1. 자바는 참조 전달이 아니라 값 전달이다
자바 메서드 호출에서 매개변수로 전달되는 것은 항상 값입니다.
int, double, boolean 같은 기본형은 실제 값이 복사됩니다. 반면 객체는 객체가 통째로 복사되는 것이 아니라, 객체 위치를 가리키는 참조값이 복사됩니다.
쉽게 말하면 객체는 집이고, 참조값은 집 주소라고 볼 수 있습니다. 메서드에 객체를 넘긴다는 것은 집을 새로 복사해서 주는 것이 아니라, 같은 집 주소가 적힌 종이를 하나 더 만들어서 주는 것에 가깝습니다.
그래서 메서드 안에서 그 주소를 따라가 집 안의 가구를 바꾸면, 메서드 밖에서도 바뀐 상태가 보입니다.
🔉 2. void 메서드인데 값이 바뀌는 이유
void 메서드는 값을 반환하지 않는 메서드일 뿐, 객체의 상태 변경까지 막는다는 뜻은 아닙니다.
예를 들어 다음 코드를 보면 changeName()은 반환값이 없습니다. 하지만 user.name 값은 바뀝니다.
class User {
String name;
User(String name) {
this.name = name;
}
}
public class Main {
static void changeName(User user) {
user.name = "Kim";
}
public static void main(String[] args) {
User user = new User("Lee");
changeName(user);
System.out.println(user.name); // Kim
}
}
여기서 중요한 부분은 changeName(user)가 호출될 때 user 객체 자체가 복사되는 것이 아니라는 점입니다.
메서드 안의 user 매개변수는 바깥의 user 변수와 다른 변수입니다. 하지만 두 변수 모두 같은 User 객체를 바라보고 있습니다.
그래서 user.name = "Kim"처럼 객체 내부 필드를 수정하면 원본 객체의 상태가 바뀝니다.
🔉 3. 참조값 복사와 객체 필드 변경은 다르다
많이 헷갈리는 부분은 “참조값을 복사했다”는 말과 “객체가 복사됐다”는 말을 구분하지 못할 때 생깁니다.
자바에서 객체 변수를 메서드에 넘기면 참조값만 복사됩니다. 따라서 메서드 안과 밖의 변수는 서로 다르지만, 그 변수들이 바라보는 객체는 같을 수 있습니다.
이 차이를 이해하면 void 메서드에서 객체 값이 바뀌는 현상이 자연스럽게 보입니다.
기본형과 참조형 매개변수 차이
🔉 1. 기본형은 값 자체가 복사된다
기본형 매개변수는 값 자체가 복사되기 때문에 메서드 안에서 값을 바꿔도 바깥 변수에는 영향이 없습니다.
public class Main {
static void changeNumber(int number) {
number = 20;
}
public static void main(String[] args) {
int number = 10;
changeNumber(number);
System.out.println(number); // 10
}
}
changeNumber() 안에서 number = 20으로 바꿨지만, main()의 number는 그대로 10입니다.
메서드 안의 number와 바깥의 number는 이름만 같아 보일 수 있지만 실제로는 별개의 변수입니다.
🔉 2. 참조형은 참조값이 복사된다
참조형은 객체를 가리키는 값이 복사됩니다.
class Box {
int value;
Box(int value) {
this.value = value;
}
}
public class Main {
static void changeBox(Box box) {
box.value = 20;
}
public static void main(String[] args) {
Box box = new Box(10);
changeBox(box);
System.out.println(box.value); // 20
}
}
이 코드는 기본형 예제와 결과가 다릅니다.
box.value = 20은 매개변수 box 자체를 바꾸는 것이 아니라, box가 가리키는 객체 안의 value 필드를 바꾸는 코드입니다.
즉, 메서드 안에서 같은 객체의 내부 상태를 수정했기 때문에 호출한 쪽에서도 변경된 값을 보게 됩니다.
🔉 3. 값 변경 여부 비교표
자바 메서드 매개변수에서 가장 먼저 구분해야 할 기준은 “변수 자체를 바꾸는지, 객체 내부를 바꾸는지”입니다.
| 구분 | 전달되는 값 | 메서드 안 변경 코드 | 호출 후 원본 영향 | 핵심 이유 |
|---|---|---|---|---|
| 기본형 | 실제 값 복사 | number = 20 | 영향 없음 | 복사된 지역 변수만 변경 |
| 참조형 필드 변경 | 참조값 복사 | box.value = 20 | 영향 있음 | 같은 객체의 필드 변경 |
| 참조형 재할당 | 참조값 복사 | box = new Box(20) | 영향 없음 | 매개변수만 새 객체를 가리킴 |
| 컬렉션 요소 추가 | 참조값 복사 | list.add("A") | 영향 있음 | 같은 리스트 객체 변경 |
| 문자열 재할당 | 참조값 복사 | str = "new" | 영향 없음 | 매개변수 재할당이며 String은 불변 |
메서드 안에서 객체를 새로 할당하면 왜 원본이 안 바뀔까
🔉 1. 매개변수 재할당은 원본 변수 변경이 아니다
객체 필드 변경과 객체 재할당은 완전히 다른 동작입니다.
다음 예제를 보면 메서드 안에서 box = new Box(20)을 실행했지만, 호출한 쪽의 box.value는 그대로 10입니다.
class Box {
int value;
Box(int value) {
this.value = value;
}
}
public class Main {
static void changeBox(Box box) {
box = new Box(20);
}
public static void main(String[] args) {
Box box = new Box(10);
changeBox(box);
System.out.println(box.value); // 10
}
}
이 코드에서 메서드 안의 box는 바깥의 box와 다른 변수입니다.
처음에는 두 변수가 같은 객체를 가리키지만, box = new Box(20)을 실행하는 순간 메서드 안의 box만 새 객체를 가리키게 됩니다.
바깥의 box는 여전히 처음 만든 Box(10) 객체를 가리키고 있으므로 결과는 10입니다.
🔉 2. 객체 내부 변경과 참조 재할당을 구분해야 한다
자바 객체 참조 전달 문제는 대부분 이 두 줄의 차이를 이해하면 풀립니다.
box.value = 20; // 같은 객체의 내부 값을 변경
box = new Box(20); // 매개변수 box가 새 객체를 가리키도록 변경
첫 번째 코드는 객체 안으로 들어가 값을 바꿉니다. 그래서 원본 객체 상태가 변경됩니다.
두 번째 코드는 메서드 안의 지역 변수 box가 바라보는 대상을 바꿉니다. 그래서 호출한 쪽 변수에는 영향이 없습니다.
제가 공식 문서를 확인하면서 가장 헷갈렸던 부분도 바로 이 지점이었습니다. “객체를 넘기면 바깥 값이 바뀐다”가 아니라, 더 정확히는 “같은 객체를 바라보는 참조값 복사본으로 객체 내부를 수정하면 바깥에서도 변경된 상태가 보인다”라고 이해하는 것이 안전합니다.
void 메서드 객체 값 변경 오류 해결 방법
🔉 1. 의도하지 않은 값 변경은 방어 복사로 막는다
객체 값이 바뀌면 안 되는 상황이라면 메서드에 원본 객체를 그대로 넘기지 않는 것이 좋습니다.
대표적인 방법은 방어 복사입니다. 원본 객체를 직접 넘기는 대신 복사본을 만들어 메서드에 전달하면, 메서드 안에서 값을 바꿔도 원본 객체는 유지됩니다.
class User {
String name;
User(String name) {
this.name = name;
}
User copy() {
return new User(this.name);
}
}
public class Main {
static void changeName(User user) {
user.name = "Kim";
}
public static void main(String[] args) {
User user = new User("Lee");
changeName(user.copy());
System.out.println(user.name); // Lee
}
}
방어 복사는 특히 회원 정보, 주문 정보, 설정값처럼 원본 상태가 중요할 때 유용합니다.
단, 객체 안에 또 다른 객체가 들어 있는 경우에는 얕은 복사와 깊은 복사를 구분해야 합니다. 단순히 바깥 객체만 복사하면 내부 객체는 여전히 공유될 수 있습니다.
🔉 2. 불변 객체로 설계하면 변경 가능성을 줄일 수 있다
객체 상태가 바뀌면 안 된다면 불변 객체로 설계하는 방법도 있습니다.
불변 객체는 생성 후 내부 값을 바꾸지 못하도록 만든 객체입니다. 필드를 private final로 두고, 값을 바꾸는 setter를 제공하지 않는 방식이 대표적입니다.
class User {
private final String name;
User(String name) {
this.name = name;
}
String getName() {
return name;
}
}
이렇게 만들면 메서드에 User 객체를 전달하더라도 name 값을 직접 바꿀 수 없습니다.
쉽게 말하면, 객체를 전달해도 안쪽 값에 손댈 수 없게 설계하는 방식입니다.
🔉 3. setter 사용 범위를 줄이면 디버깅이 쉬워진다
객체 값이 어디서 바뀌는지 찾기 어렵다면 setter 사용 범위를 먼저 확인하는 것이 좋습니다.
모든 필드에 무조건 setter를 열어두면 어느 메서드에서 값이 바뀌었는지 추적하기 어려워집니다. 특히 여러 서비스 메서드가 같은 객체를 공유하는 구조에서는 의도하지 않은 상태 변경이 버그로 이어질 수 있습니다.
객체 값 변경이 필요한 경우에는 메서드 이름에 의도를 드러내는 것도 도움이 됩니다.
user.changeName("Kim");
order.cancel();
account.withdraw(10000);
setName(), setStatus()처럼 단순히 값을 넣는 이름보다, 어떤 비즈니스 동작인지 드러나는 이름이 코드 이해에 유리합니다.
자바 객체 값 변경 확인 순서
🔉 1. 먼저 필드 변경인지 재할당인지 확인한다
자바 객체 참조 문제를 디버깅할 때 가장 먼저 볼 부분은 메서드 안에서 어떤 변경이 일어났는지입니다.
아래처럼 점을 찍고 필드에 접근하면 객체 내부 변경입니다.
user.name = "Kim";
box.value = 20;
list.add("A");
반대로 아래처럼 매개변수 자체에 새 값을 넣으면 재할당입니다.
user = new User("Kim");
box = new Box(20);
list = new ArrayList<>();
필드 변경은 원본 객체에 영향을 줄 수 있고, 재할당은 호출한 쪽 변수에 영향을 주지 않습니다.
🔉 2. 오류 해결 체크리스트
객체 값이 예상과 다르게 바뀐다면 아래 순서대로 확인하면 원인을 좁히기 쉽습니다.
| 확인 항목 | 점검 내용 | 해결 방향 |
|---|---|---|
| 1단계 | 메서드 안에서 객체.필드 = 값 형태가 있는지 확인 | 의도한 변경인지 검토 |
| 2단계 | list.add(), map.put() 같은 컬렉션 변경이 있는지 확인 | 원본 컬렉션 공유 여부 확인 |
| 3단계 | 매개변수 재할당인지 필드 변경인지 구분 | 재할당은 원본 변수에 영향 없음 |
| 4단계 | setter가 여러 곳에서 호출되는지 확인 | setter 범위 축소 또는 메서드명 개선 |
| 5단계 | 원본 보존이 필요한 객체인지 확인 | 방어 복사 또는 불변 객체 적용 |
| 6단계 | 테스트 코드로 호출 전후 값을 비교 | 변경 지점 명확히 추적 |
🔉 3. 컬렉션은 특히 원본 변경에 주의해야 한다
List, Map, Set 같은 컬렉션은 객체 참조 전달 개념을 이해하지 못하면 실수하기 쉽습니다.
import java.util.ArrayList;
import java.util.List;
public class Main {
static void addItem(List<String> items) {
items.add("Java");
}
public static void main(String[] args) {
List<String> items = new ArrayList<>();
addItem(items);
System.out.println(items); // [Java]
}
}
addItem()은 void 메서드입니다. 하지만 items.add("Java")는 같은 리스트 객체에 요소를 추가하는 동작이므로 호출 후 리스트 내용이 바뀝니다.
원본 리스트가 바뀌면 안 된다면 새 리스트를 만들어 넘기거나, 메서드 안에서 복사본을 사용해야 합니다.
addItem(new ArrayList<>(items));
이 방식은 원본 리스트를 보호하는 데 도움이 됩니다.
자바 객체 참조 전달을 정확히 이해하는 핵심
🔉 1. 객체를 넘긴다는 말은 참조값 복사본을 넘긴다는 뜻이다
자바에서 객체를 메서드에 전달한다는 말은 객체 자체를 넘긴다는 뜻이 아닙니다.
정확히는 객체를 가리키는 참조값이 복사되어 매개변수에 들어갑니다. Java Language Specification은 메서드 호출이 평가되고 실행되는 방식과 인자 값이 매개변수로 전달되는 구조를 규정하고 있습니다.
그래서 메서드 안의 매개변수는 호출한 쪽 변수와 별개입니다. 그러나 두 변수가 같은 객체를 가리키고 있다면, 객체 내부 상태 변경은 공유되어 보입니다.
이 한 문장만 기억해도 대부분의 혼란이 정리됩니다.
자바는 참조 전달이 아니라 값 전달이며, 객체의 경우 참조값이 복사되기 때문에 같은 객체의 내부 값은 변경될 수 있습니다.
🔉 2. void는 상태 변경 여부를 판단하는 기준이 아니다
void는 반환값이 없다는 의미입니다.
따라서 void 메서드라고 해서 아무 변화도 만들지 않는다는 뜻은 아닙니다. 파일을 저장하거나, 로그를 남기거나, 리스트에 값을 추가하거나, 객체 필드를 변경하는 메서드는 모두 void일 수 있습니다.
객체 값 변경 여부를 판단할 때는 반환 타입보다 메서드 내부 코드를 봐야 합니다.
객체.필드 = 값, 객체.setXxx(), list.add(), map.put() 같은 코드가 있다면 원본 객체 상태가 바뀔 가능성이 있습니다.
🔉 3. 안전한 코드를 위해 변경 가능성을 명확히 드러내야 한다
객체 값을 바꾸는 메서드는 이름과 구조에서 변경 의도가 드러나야 합니다.
예를 들어 단순 조회처럼 보이는 메서드 안에서 객체 값을 바꾸면 나중에 디버깅이 어려워집니다.
// 혼란을 줄 수 있는 이름
checkUser(user);
// 변경 의도가 드러나는 이름
updateUserStatus(user);
좋은 코드는 “어디서 값이 바뀌는지”를 읽는 사람이 쉽게 예상할 수 있어야 합니다.
자바 객체 참조 전달 문제는 문법 자체보다 코드 설계 습관과 연결되는 경우가 많습니다. 객체를 변경할지, 복사해서 사용할지, 불변으로 만들지를 미리 정하면 void 메서드에서 값이 바뀌는 문제를 훨씬 안정적으로 다룰 수 있습니다.
자주 묻는 질문
🔉 질문 1
Q. 자바는 객체를 참조 전달하나요, 값 전달하나요?
A. 자바는 항상 값 전달입니다. 객체를 넘길 때도 객체 자체가 아니라 객체를 가리키는 참조값의 복사본이 전달됩니다. 그래서 같은 객체의 필드는 바뀔 수 있지만, 매개변수를 새 객체로 재할당해도 호출한 쪽 변수는 바뀌지 않습니다.
🔉 질문 2
Q. 자바 void 메서드에서 객체 값이 변경되는 이유는 무엇인가요?
A. void는 반환값이 없다는 뜻이지 객체 상태 변경이 없다는 뜻은 아닙니다. 메서드 안에서 user.name = "Kim"처럼 객체 필드를 수정하면, 호출한 쪽에서도 같은 객체의 변경된 상태를 보게 됩니다.
🔉 질문 3
Q. 자바에서 메서드 안에 new 객체를 넣었는데 원본이 안 바뀌는 이유는 무엇인가요?
A. box = new Box(20)처럼 매개변수를 재할당하면 메서드 안의 지역 변수만 새 객체를 가리킵니다. 호출한 쪽의 변수는 여전히 기존 객체를 가리키므로 원본 값은 바뀌지 않습니다.
🔉 질문 4
Q. 자바 List를 void 메서드에 넘겼을 때 값이 추가되는 것도 같은 원리인가요?
A. 맞습니다. List도 객체이므로 참조값의 복사본이 전달됩니다. 메서드 안에서 list.add()를 실행하면 같은 리스트 객체에 요소가 추가되기 때문에 호출 후에도 변경된 내용이 보입니다.
🔉 질문 5
Q. 자바 객체 값 변경 오류를 막으려면 어떻게 해야 하나요?
A. 원본 객체가 바뀌면 안 되는 상황에서는 방어 복사, 불변 객체, setter 제한을 고려하는 것이 좋습니다. 특히 컬렉션은 new ArrayList<>(list)처럼 복사본을 넘기면 의도하지 않은 원본 변경을 줄일 수 있습니다.
0 댓글