본문 바로가기

Programing/JVM(Java, Kotlin)

[Java] 참조(Reference)와 주소의 관계

자바 카페에서 2020-07-24 에 논쟁을 했던 내용이다.

어떤 자바를 공부하는 사람이 아래와 같은 질문을 했다.            

 

질문 내용: equals(), == 질문드립니다.

- 질문 내용 :  안녕하세요.
자바에서 equals(), ==는 주소값이 같은지 확인하는거라 들었는데요.

class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(!(obj instanceof Person)){
            return false;
        }else{
            return this.name == ((Person)obj).name;
        }
    }
}

위 예제 처럼 equals를 오버라이딩 했습니다.

Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
p1.equals(p2); // true

를 하면 true값이 나오는데요,

 

여기서 의문점은 마지막

return this.name ==((Person)obj).name; 입니다.

== 는 주소값을 비교한다고 했는데

this.name 이랑 매개변수로 받은 name의 주소값은 같지 않은데 어떻게 true가 나올 수 있는지 모르겠습니다.


일단 위의 질문이 궁금할 수 있으니 답변부터하면...

사전지식: 문자열 생성 방법의 2가지 방식

인스턴스를 생성할 때 문자열("홍길동")을 객체 생성이 아닌 리터럴의 형태를 사용했다.

문자열을 아래와 같이 생성하는 것은 일반적인 객체 생성의 방식이다.

String hong = new String("홍길동");

반면의 아래의 방식을 리터럴 형태의 쓰임라고 한다.

String hong = "홍길동";

즉 new 연산자를 이용하지 않고 그냥 문자열의 그대로 (" 로 시작해서 "로 끝나는 리터럴) 사용을 햇다는 것이다.

다시 사용 코드를 보면

Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
p1.equals(p2); // true

이해를 돕기위해 그림으로 그려보면 아래와 같다.

결국 p1 이라는 레퍼런스가 가지고 있는 내부 객체의 name은 어떤 "문자열 리터럴"들을 보관하고 있는 곳(JVM에서는 Run-Time Constant Pool라고 부른다)의 값이다.

그런데 p2에서도 리터럴 형태로 사용했으므로 p1과 p2의 객체는 새로운 문자열이 만들어지지 않고 이전에 가리켰던 "홍길동"을 가리키게 된다.

실행시점(Run-Time)의 스냅샷을 개념적으로 그려보면 위와 같다.

결국 p1.equals(p2) 의 결과는 true 가 된다.

오버라이드한 equals는 p1.name == p2.name 가 수행이 되기 때문이다.


만약 == 연산의 결과를 false로 만드려면 어떻게 해야 할까?

아래처럼 문자열 객체를 직접 만들어주면 된다.

Person p1 = new Person(new String("홍길동"));
Person p2 = new Person(new String("홍길동"));
p1.equals(p2); // false

String 객체가 각각 만들어짐을 알 수 있다.

따라서 p1.equals(p2) 의 결과는 false가 된다. (내부적으로는 p1.name == p2.name 이 수행되므로)


이제부터 하고 싶은 이야기.

 

질문을 읽으면서 한가지 마음에 걸린 표현이 있었다.

자바에서 equals(), ==는 주소값이 같은지 확인하는거라 들었는데요.

바로, '주소값'이다.

 

사실 자바언어명세(Java Language Specification)를 읽다보면(Java13기준) address 단어가 총 4번 나오는데 한번은 인용하다의 의미의 addresse이고, 나머지 3군데는 final을 설명하기 위해서이다.

오히려 객체(Object)에 대해 설명할 때는 address(주소)라는 말대신 reference(참조)라는 용어를 사용한다.

참조 값(또는 그냥 참조)는 이러한 객체들(null 참조 포함) 가리키는 것이다.

사실 명세의 정의가 썩 이해하기 좋지는 않다. 대신 카이호스트만의 책을 인용하면 아래와 같다.

참조는 실제 객체를 찾아내는 구현체 고유의 방법이다.

위의 표현이 좋은 이유는 '주소'라는 구현체의 방법을 명시하지 않았다는 것이다.

예를 들어 참조를 구현하는 구현체가 ID를 통한 방법(IdReference)이다. 기타 다른 방법(OtherReference)이 있다고 가정하자.

클래스 다이어그램으로 그려보면 아래와 같다.

참조의 구현을 주소를 이용할 수 있다. 그리고 많은 JVM의 구현체가 이 방식을 사용하는 것으로 알고 있다.

하지만 다른 구현체의 방벙이 없다고 하위 구현체가 상위 개념을 커버하는 것은 좀 이상하다.

 

이는 상표명(구현체)이 마치 제품 자체가 되어 버린 경우와 유사하다.

셀로판테이프를 스카치테이프라 부르는 것이나, 스테이플러를 호치키스, 복사하다를 제록스하다 등으로 이야기 하는 식이다.

 

블랙스완이라는 용어처럼 백조는 하얗다라는 개념은 흑고니(흑조)가 발견됨으로 명제가 깨질 수 있는 것이다.

흑조의 경우 원래 자연계에 있던 것을 나중에 발견한 케이스이다.

하지만, 구현체는 사람이 인공적으로 만드는 것이기에 상황에 따라 다른 방식의 참조 방식을 만들 수 있는 여지는 훨씬 크다.


그리고 나는 그 카페에서 2020.07.24. 17:45에 강제탈퇴가 되었다.

사유는 참 재미있다.

"이곳은 제가 독자들을 위해 서비스하는 곳입니다."

근데 말이지 나도 당신 책을 가지고 있으니 읽었으니 '독자'인데

내가 책을 샀다고 저자에게 알려주어 댓글을 받기까지 했으니 독자라는 것은 이미 알고 있을 것이다.

논쟁에서의 논리적 오류는 메세지까지 옅볼 수 있다.

 

아니면 (내 책이 잘못되었다고 이야기 하지 않는) 이 생략되어 있는 것일까?