본문 바로가기

[JAVA] 내가 쌍둥이를 낳았어 근데 첫째 둘째 생긴거 같으면 동일인물임?

@Salieri2026. 2. 7. 09:09

Reference 값에 대해서 이해를 모른 상태로 질문을 하는 애들이 많아져서 정리하고자 글을 쓴다 

이 개념은 모든 프로그래밍의 개념을 포괄하니까, 

알아두면 정말 좋을 것 같아 정리한 글 

 

제목은 어그로


 

 

 

1. 개요: 객체 생성과 인스턴스의 구분

Person이라는 클래스(설계도)를 통해 속성(Property) 값이 완전히 동일한 두 개의 객체(Instance)를 생성하는 상황을 가정해 보면

 

public class Person {
    String name;
    int age;
    String hobby;

    // 생성자: 이름, 나이, 취미
    // constructor : 는 코드를 구성하는 요소인데, 이것도 필요하다면 나중에 포스팅함
    public Person(String name, int age, String hobby) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }
}
//가 있다고 가정해보자.

 

그러면 이제 main method 에서 instantiate 가 가능하다

// 생성자: 이름, 나이, 취미
Person person1 = new Person("김철수", 20, "코딩"); 
Person person2 = new Person("김철수", 20, "코딩");

 

 

 

 

여기서 제기되는 핵심적인 질문은

person1과

person2는

동일한 객체인가?

 

 

그러니까, 김철수라는 사람이... 나이도 똑같고, 취미도 똑같은데.. 그러면 "나" 라는 자아가 똑같은 "김철수"를 보았을때 "같은 존재"라고 할 수 있는가. 그러니까 내가 살아온 세상이, 다 같은거야, 거울을 보면서 생각하는거지. 거울에 비친 "나"는 과연 "나"의 본질을 비추고 있다고 할 수 있는가?  심지어 "부모"도 같다고 말 할 수 있지않나? 그렇다면 나의 진정한 값은 .... 

 

 

뭐 여기까지 깊게 들어가게 되면 철학적인 존재적 가치에 대한 심오한 토론과 "나"의 가치성을 따지는 것이 되어버리니까. 

간단하게 생각해보면, 그러니까. 우리 "개발자" 들의 시야로 보면

 

"거울" "나" 를 두가지 객채로 분리하고,

거울속 "나" 는 다른 존재지.

그럼 다른거야

 

 

 


 

Java의 관점

  • == 연산자: 두 객체는 서로 다릅니다(False).
  • .equals() 메서드: 개발자의 정의에 따라 논리적으로 동등하다고 판단할 수 있습니다(True).

 

 

이것을 좀 더 원리적으로 이해하기 위해서는 여럿서적에서도 이야기 하고 지나가는데 ( 개인적으로 JAVA 의 정석 정말 좋다고 생각함, 물론 첫 입문자에겐 언어의 사용이 난해함)

 

 

2. 메모리 할당 구조: Stack과 Heap의 역할

이러한 현상이 발생하는 원인을 이해하기 위해서는 Java의 메모리 관리 메커니즘을 파악해야 한다

new 키워드를 사용한 객체 생성 시 메모리에서는 다음과 같은 절차가 수행됩니다.

메모리 적재 구조 시각화

다시봐도 뭔 소리인지

저번에 말했던 JVM을 보면, 크게 봣을때

 

  1. Heap (힙) 영역: 객체의 실제 데이터(이름, 나이, 취미 등)가 저장되는 공간입니다. new 연산자가 호출될 때마다 독립적인 메모리 공간이 할당됩니다.
  2. Stack (스택) 영역: 메서드 내에서 선언된 변수(person1, person2)가 위치하는 공간입니다. 이곳에는 객체 자체가 아닌, 객체가 위치한 메모리 주소(Reference Value)가 저장된다
[Stack 영역 (참조 변수)]                 [Heap 영역 (인스턴스)]
+------------------------+              +-----------------------------+
| 변수명 |  값 (참조 주소)  |              | 주소  |  실제 데이터 (객체)     |
+-------+----------------+              +-------+---------------------+
| person1|    0x100       | -------------> | 0x100 | "김철수", 20, "코딩"  |
+------------------------+              +-----------------------------+
                                                 (물리적으로 분리된 공간)
+------------------------+              +-----------------------------+
| person2|    0x200       | -------------> | 0x200 | "김철수", 20, "코딩"  |
+------------------------+              +-----------------------------+

 

상세 분석

  • 객체 생성 (new Person(...)):
    • JVM은 Heap 영역의 가용 공간(예: 0x100)을 확보하여 데이터를 저장합니다.
    • 해당 메모리의 주소값(0x100)을 반환합니다.

예시 : person1() 으로 부르면, 김철수의 값을 던짐

  • 참조 할당 (Person person1 = ...):
    • Stack 영역의 변수 person1에는 반환된 주소값 0x100이 저장됩니다.
  • 추가 객체 생성:
    • 속성 값이 동일하더라도 new 연산자가 다시 호출되면, JVM은 예외 없이 새로운 메모리 공간(예: 0x200)을 할당합니다.
    • 변수 person2에는 새로운 주소값 0x200이 저장됩니다.

따라서 person1 == person2 연산은 실질적으로 0x100 == 0x200을 비교하는 것이므로, 결과는 거짓(False)이 된다

 

 

 

3. JVM 런타임 데이터 영역의 구조적 이해

JVM(Java Virtual Machine)의 메모리 구조를 거시적인 관점에서 도식화해서 깊게 들어가면

JVM Runtime Data Areas

[ Method Area (클래스 정보) ]     [ Stack Area (실행 제어) ]        [ Heap Area (데이터 저장) ]
+-------------------------+    +-----------------------+      +-----------------------+
|                         |    | main() Stack Frame    |      |                       |
|  Person 클래스 메타데이터  |    |                       |      |  +-----------------+  |
|  (Static, 메서드 코드)    |    |  [ person1 (참조) ]   |      |  | Person 인스턴스 A |  |
|                         |    |   Value: 0x100 -------+------>|  | "홍길동", 20    |  |
|  "Person의 구조 정의"     |    |                       |      |  +-----------------+  |
|                         |    |  [ person2 (참조) ]   |      |                       |
|                         |    |   Value: 0x200 -------+------>|  +-----------------+  |
+-------------------------+    |                       |      |  | Person 인스턴스 B |  |
                               +-----------------------+      |  | "홍길동", 20    |  |
                                                              |  +-----------------+  |
                                                              +-----------------------+
  1. Method Area (메서드 영역): 클래스 로더에 의해 로딩된 클래스 파일(Person.class)의 바이트코드와 메타데이터가 저장되는 공용 공간입니다.
  2. Stack Area (스택 영역): 메서드 호출 시 생성되는 스택 프레임(Stack Frame)이 위치하며, 지역 변수 및 참조 변수가 이곳에서 수명 주기를 갖습니다.
  3. Heap Area (힙 영역): 런타임에 동적으로 생성되는 모든 객체와 배열이 저장되는 공간입니다. 더 이상 참조되지 않는 객체는 가비지 컬렉터(Garbage Collector)에 의해 메모리에서 해제됩니다.

4. 객체 비교의 두 가지 기준

① 동일성 (Identity) 비교: == 연산자

== 연산자는 두 피연산자의 **원시 값(Primitive Value)**을 비교합니다. 참조형 변수에서 원시 값은 메모리 주소를 의미합니다.

if (person1 == person2) {
    // 실행되지 않음 (0x100 != 0x200)
    System.out.println("두 참조 변수는 동일한 메모리 주소를 가리킵니다.");
} else {
    // 실행됨
    System.out.println("두 참조 변수는 서로 다른 메모리 주소를 가리킵니다.");
}

② 동등성 (Equality) 비교: .equals() 메서드

.equals() 메서드는 객체의 논리적 내용이 같은지를 판단하기 위해 사용됩니다. 기본적으로 Object 클래스의 .equals()는 == 연산과 동일하게 주소값을 비교하도록 구현되어 있습니다. 따라서 값의 비교를 원할 경우, 해당 클래스에서 메서드를 **오버라이딩(Overriding)**하여 재정의해야 합니다.

5. 구현 예제 및 검증

다음은 Person 클래스에서 equals 메서드를 재정의하여 논리적 동등성을 구현한 예제입니다.

class Person {
    String name;
    int age;
    String hobby;

    public Person(String name, int age, String hobby) {
        this.name = name;
        this.age = age;
        this.hobby = hobby;
    }

    /**
     * 객체의 논리적 동등성을 비교하기 위해 equals 메서드를 재정의합니다.
     * 주석을 해제하면 내용 기반 비교가 활성화됩니다.
     */
    /*
    @Override
    public boolean equals(Object o) {
        // 1. 메모리 주소가 동일하면 즉시 true 반환 (성능 최적화)
        if (this == o) return true;
        
        // 2. null이거나 클래스 타입이 다르면 false 반환
        if (o == null || getClass() != o.getClass()) return false;
        
        // 3. 필드 값 정밀 비교
        Person person = (Person) o;
        return age == person.age && 
               name.equals(person.name) && 
               hobby.equals(person.hobby);
    }
    */
}

public class ReferenceTest {
    public static void main(String[] args) {
        // 1. 객체 생성 (Heap 영역의 서로 다른 주소에 할당)
        Person person1 = new Person("홍길동", 20, "축구");
        Person person2 = new Person("홍길동", 20, "축구");
        
        // 2. 참조 할당 (Shallow Copy)
        // person1의 참조 주소(0x100)를 myself 변수에 복사
        Person myself = person1; 

        System.out.println("=== 1. == 연산자 (동일성: 주소 비교) ===");
        // 서로 다른 주소이므로 false 반환
        System.out.println("person1 == person2 : " + (person1 == person2)); 
        
        // 동일한 주소를 공유하므로 true 반환
        System.out.println("person1 == myself : " + (person1 == myself)); 

        System.out.println("\n=== 2. equals 메서드 (동등성: 내용 비교) ===");
        // Person 클래스 내 equals 오버라이딩 여부에 따라 결과가 달라짐
        System.out.println("person1.equals(person2) : " + person1.equals(person2));
        
        // 시스템 식별 해시코드(Identity Hash Code) 확인
        System.out.println("\n=== [참고] 메모리 주소 기반 식별자 ===");
        System.out.println("person1 식별자: " + System.identityHashCode(person1));
        System.out.println("person2 식별자: " + System.identityHashCode(person2));
        System.out.println("-> 식별자가 상이함은 서로 다른 메모리 공간을 점유하고 있음을 의미합니다.");
    }
}

6. 결론 요약

Java 프로그래밍에서 객체를 다룰 때는 동일성동등성의 개념을 명확히 구분해야 합니다.

구분비교 수단비교 대상의미

동일성 (Identity) a == b Stack의 참조 주소 두 변수가 물리적으로 동일한 메모리 위치를 가리키는가?
동등성 (Equality) a.equals(b) Heap의 인스턴스 데이터 두 객체가 논리적으로 동일한 정보를 포함하고 있는가?

핵심 요약

  1. 메모리 구조: Method Area는 클래스 정보를, Stack은 참조 주소를, Heap은 실제 인스턴스를 관리합니다.
  2. 객체 생성: new 연산자는 항상 Heap 영역에 새로운 메모리 공간을 할당합니다.
  3. 비교 원칙: 객체의 물리적 일치를 확인할 때는 ==를 사용하며, 논리적 내용의 일치를 확인할 때는 equals() 메서드를 재정의하여 사용해야 합니다.

 


 

결어

앞서 설명 된 "도플갱어의 역설" 에 대한 답을, 잠깐 적는다면. 모든 것은 관점에 따라 어디에 가중치를 두냐에 따라서 달라진다. 사회의 시선에서 정의된 "나"라는 값과, 가족 내부에서 자식으로 정의된 "나" 라는 값과 연인의 관계속에서 타인에 의해 정의된 "나"라는 어떻게 보이고 싶고 어떻게 판단되는지 달라지기 때문이다. 쉬운 예시로, 회사에서의 "김부장"은 "회사"에서의 가치이지만, 김부장님도 부모를 만나면 "자식"으로써의 가치가 존재한다. 

 

설령, 이 모든 값이 같은 도플갱어가 나타났다 하더라도,
그것을 "인지"하고 "해석"하고 고유한 가치 값을 가진 것은 "나 자신" 일뿐이다.

 

개발자의 역할은 이러한 관점의 차이를 "컴퓨터" 가 이해할 수 있도록 해석하게 해주는 것이라고 생각한다. 사람들은 본인이 이해하고 있는 세상을 "컴퓨터" 도 이해하고 있다고 생각하지만, 사실상 "컴퓨터" 또한 사람이 만들어낸 존재이기 때문에 그 본질의 값은 "사람"과 굉장히 닮아있기 때문에 , 어떤 관점으로 어디 Data 를 Reference 하고 Memory 를 할당하고 설계하는 가치는 있다고 생각한다. 

물론, 이제 여기서 더 깊게 들어가게 되면, JAVA 같은 언어도 아니라 C++ 같은 언어도 아니라, 근본 언어인 Interpreter 언어 같은거 해야한다고 생각하지때문에. 사람의 언어도 다양하게 존재하고 각기 지역에 따라 다르게 사용하고 규칙이 존재하는데 내가 그래서 AI 등장으로 개발자의 시장이 넓어졋다고 생각하지 "대체가능한" 이라고 생각은 안함, 물론 내가 틀릴수잇음

 

객체를 생성하고 없애고 하는 Backend 개발자를 지향하는 사람으로써(장기적으로는 DevOps/MLops를 희망함)는 이런거 배우는게 너무나 재밌기 때문에 "박사"는 시기상조가 아닐까 싶다.

 

애휴

 

 

이거 어려우면 Python 하러가...

 

 

 

 

Salieri
@Salieri :: 살리에리의 인생살이 채널

평소에 내가 좋아하는것과 싫어하는 것들 그리고 왜 안되나 싶은 것들을 업로드 하는 블로그입니다 인생살이도 업로드 하고있어요. 제가 걸어온 길이 당신에게 있어서 도움이 되는 이정표였으면 좋겠네요. 질문은 항상 넓은 마음으로 받고있답니다. 대답하기 곤란한 질문이 아닌 이상 제에게 이메일을 써주세요 트위터 : https://twitter.com/@Salieri1845799 깃허브 : https://github.com/salieri009

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차