인프런 커뮤니티 질문&답변

이한빈님의 프로필 이미지
이한빈

작성한 질문수

자바 ORM 표준 JPA 프로그래밍 - 기본편

실전 예제 6 - 값 타입 매핑

프록시 관련한 equals()오버라이딩, 제가 이해한 것이 맞나요?

해결된 질문

작성

·

797

5

덕분에 스프링, JPA 말고도 자바 전반에 걸쳐서 정말 많은 걸 재밌게 배우고 있어서 감사드립니다.

공부를 해도 의문이 해소되지 않는 부분이 있고, 제가 이해하는 것이 맞는지 확신이 안 가는 것들이 있어 질문드립니다.

 

1. 왜 2:23초 정도의 equals() 오버라이딩에서, 클래스 비교를 instance of가 아니라 !=로 하였는지

프록시로 생성된 객체는 원본 클래스를 상속한, 생성된 클래스에 속하기에

equals 오버라이딩 할 때, ==이나 !=를 이용한다면 적절한 구현이 이루어질 수 없지 않나요?

1) 기본적으로 instance of를 써야 프록시로 생성된 객체가 원본 클래스와 적절하게 동등성 비교가 가능하다고 알고 있었습니다.

2) 한편, 실전예제에서 instance of를 사용하지 않은 것이 값 타입이라서 그런것이 아닌가 추측했습니다.

왜냐면 값 타입은 식별자를 갖지 않기 때문에 getReference() 메서드로 프록시를 가질 일 자체가 없어서 위의 문제가 발생하지 않으리라는게 제 추측이었습니다.

이와 관련해서 의문이 들었던 부분이:

 

2. 이전 강의(값 타입 컬렉션)에서 값 타입 컬렉션에 대해

값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함: null 입력X, 중복 저장X

이라고 정리했었는데,

그렇다면 값 타입 컬렉션의 경우 PK가 존재하는 것인가요? 또, 그걸 통해서 find나 getReference가 가능한 건가요?

별개로, 값 타입이 불변 타입으로 사용되기에 equals 오버라이딩이 필요하다는 점은 이해했습니다.

 

마지막으로

3. getter를 사용해서 equals나 hashCode 메서드를 구현하는 것이 프록시 문제를 피해갈 수 있다는 것이 이해가 가지 않습니다.

오히려 저는 getter 사용이 아니라 클래스 판별 부분을 ==로 하지 않고 instance of로 하는 것이 더 맞다고 생각했습니다.

그 이유는

1) 제가 보기에, 위에서 언급했듯이, 프록시가 비교에서 문제가 발생하는 부분은,

프록시로 얻은 객체는 원본 객체와는 다른 클래스(JPA에 의해 생성된, 원본 클래스를 extends 하는 클래스)라는 점이기에 그렇습니다.

2) getter 사용이 아니라 instance of 사용이 프록시와 연관이 있다고 IntelliJ가 안내문으로말해주는 것 같아보였습니다. 단축키로 equals와 hashCode 메서드를 생성하려고 하면 다음과 같은 안내문이 나왔었는데요:

 

generate equals() and hashCode()

  • Accept subclasses as parameter to equals() method

While generally incompliant to Object.equals() specification accepting subclasses might be nessesary for generated method to work correctly with frameworks, which generate Proxy subclasses like Hibernate.

생성된 메서드를, 하이버네이트처럼 프록시 서브클래스들을 만드는 프레임워크들과 호환시키고 싶으시다면, 일반적인 Object.equals() 규격과 다르게, 서브클래스들을 포함시키는 것이 필요할 수 있습니다.

  • Use getters during code generation
  • 해당 클래스에 getter 만들지 않으면 당연히 위 옵션을 체크하더라도 getter 메서드 사용하지 않고 구현된다.

 

제가 잘못 번역한 것인지는 모르겠습니다만 위의 안내문은 제가 위에서 생각했던 것처럼 서브클래스를 포함시키는 것

(!=으로 동등 클래스만 포함시키는 것이 아니라 instance of로 서브클래스까지 포함시키는 것)

이 하이버네이트 등의 프록시 서브클래스 문제를 해결해준다고 말하는 것 같았기 때문입니다.

 

공부가 얕아서 질문이 많았습니다. 하이버네이트에서 실제 프록시를 생성하거나 할 때 무언가 제가 모르는 부분이 있는 것은 아닌가 해서 하이버네이트 라이브러리에서 em을 extends 하는 Session이나 SessionImpl을 뒤져 보기도 했는데, 도통 답을 찾기가 어렵네요. 도움이 필요합니다..

 

답변 2

4

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. 이한빈님 좋은 질문입니다.

 

1. 왜 2:23초 정도의 equals() 오버라이딩에서, 클래스 비교를 instance of가 아니라 !=로 하였는지

프록시로 생성된 객체는 원본 클래스를 상속한, 생성된 클래스에 속하기에

equals 오버라이딩 할 때, ==이나 !=를 이용한다면 적절한 구현이 이루어질 수 없지 않나요?

-> 네 생각하신 내용이 맞습니다. 프록시를 정확하게 비교하려면 생각하신 것 처럼 instance of를 사용하는 것이 맞습니다. 강의에서 제가 누락했네요.

3번에서 답을 드리지만 getter도 함께 추가해야 문제가 발생하지 않습니다.

 

2. 이전 강의(값 타입 컬렉션)에서 값 타입 컬렉션에 대해

값 타입 컬렉션을 매핑하는 테이블은 모든 컬럼을 묶어서 기본 키를 구성해야함: null 입력X, 중복 저장X

이라고 정리했었는데,

그렇다면 값 타입 컬렉션의 경우 PK가 존재하는 것인가요? 또, 그걸 통해서 find나 getReference가 가능한 건가요?

별개로, 값 타입이 불변 타입으로 사용되기에 equals 오버라이딩이 필요하다는 점은 이해했습니다.

-> 불가능합니다. 여기서 PK는 데이터베이스 기준의 PK를 뜻합니다. 값타입은 @Id가 없습니다. 따라서 단독으로 조회하는 것이 불가능합니다.

 

3. getter를 사용해서 equals나 hashCode 메서드를 구현하는 것이 프록시 문제를 피해갈 수 있다는 것이 이해가 가지 않습니다.

-> 여기서 getter를 사용하는 이유는 instance of와는 무관합니다. 1번에 말씀드린 것 처럼 instance of를 사용해야 프록시 인스턴스를 비교할 때 적절한 결과가 나오겠지요?

그런데 여기서 getter를 사용하는 이유는 바로 프록시의 특성 때문입니다.

 

필드 직접 접근

equals(Object o){

  Objects.equals(this.city, o.city)

}

 

여기에서 o.city가 문제가 됩니다. o.city를 호출하게 되면 프록시의 필드에 직접 접근하게 됩니다. 그런데 프록시는 필드에 값이 없습니다. 진짜 target에 접근해야 값을 가져올 수 있습니다. 프록시를 통해서 target에 접근하려면 항상 메서드를 호출해야 합니다. 따라서 o.city는 null을 반환하게 됩니다.

 

getter를 통한 접근

equals(Object o){

  Objects.equals(this.getCity(), o.getCity())

}

 

o.getCity()를 호출하면 프록시의 getCity()를 호출하게 되고, 프록시는 다시 target의 getCity()를 호출해서 원본 값을 가져올 수 있습니다.

추가로 다음 질문도 읽어보시면 도움이 되실거에요.

https://www.inflearn.com/questions/20180

도움이 되셨길 바래요.

0

이한빈님의 프로필 이미지
이한빈
질문자

친절한 답변 감사드립니다. 덕분에 이해가 되었습니다.

다만, 관련해서, 추가적으로 궁금한 것들이 있습니다.

1.

getter로 프록시의 필드에 접근하려고 하면 프록시의 getName() 메서드가 프록시 내부의 target.getName 메서드를 호출하는 방식으로 오버라이딩 되어 있어서,

3에 대한 답변처럼, 필드 접근방식과 다르게 원본 객체의 값을 받아올 수 있는 것일까요? (반면 필드 값의 경우 이 방식의 해결이 불가능하기에 null을 반환하는 것이고)

 

2. 현 강의의 범위에서는 약간 벗어나지만, getter/setter 메서드 자체에 대한 의문입니다.

자바빈 프로퍼티 규약 때문에 getter와 setter를 사용하는 것은 알겠습니다.

그런데 왜 getter와 setter를 사용하는가에 대해서는 약간 의문이 남아 있습니다.

public으로 공개한 getter와 setter 메서드를 만드는 것은 결과적으로 필드 값 자체에 어디에서든 접근 가능하다는 면에서

클래스 자체의 은닉에는 의미가 없지 않나 하는 의문입니다.

 

그래서 제가 getter/setter 사용에 대해 생각하는 봐를 정리해 봤는데, 다음과 같습니다:

 

1) 필드접근과 다르게, getter/setter 접근은 오버라이드를 이용해서 상황에 맞출 수 있기 때문(제가 위의 1번 물음에서 하이버네이트가 getter를 해결할 때 사용한 방식이라고 생각한 것입니다.)

2) 변수 자체에 대한 접근을 통제할 수 있는 필드 접근 제한자에 비해, 불변객체 등을 만들 때처럼 getter만 만들고 setter는 만들지 않는 식으로 읽기/쓰기 중 하나만 제약할 수 있어서(물론 컬렉션의 경우에는 변경 가능성이 남아 있지만)

3) getter/setter라는 명명이 개발에서 가장 자주 쓰이는 필드 변수에 대한 읽기/쓰기 변수의 명명법 상 일관성을 줘서

3)의 경우에는 의문점이 있는데, 그렇다면 IntelliJ 단축키로 만들어지는 것과 같은 기본적인 getter/setter 이상으로 추가적인 무언가가 있거나, 변화되었다면 getter/setter 명명법을 사용하지 않는 편이 나을까요?

4) getter는 side effect에 대해 안전하지만, setter는 그렇지 않기 때문에, setter 사용은 가급적 피하는 것이 좋다.

 

3. Hibernate.unproxy()메서드

공부하다가 Hibernate.unproxy()의 존재에 대해 알게 되었는데요. 참고한 글 링크, 글에 대한 간단 번역

위 글에서는 프록시로 로딩된 객체와 원본 객체의 동등성 문제를 unproxy를 이용해서 해결하던데,

이런 방법이 많이 쓰이는지도, 동등성 문제라면 equals() hashCode()오버라이딩 하는 것에 비해, 굳이 이 방법을 쓸 메리트가 있는지도 의문이 들었습니다. 하이버네이트에 종속적이기도 하구요.

 

동등성 비교 외에 프록시 객체로 인해 문제 발생할 이유가 더 있을까요? unproxy + 형변환 조합이 존재하는 것에는 분명 이유가 있을 것 같은데, 굳이 .class()를 맞춰주는 방식이 필요한 상황이 있을까요?

 

김영한님의 프로필 이미지
김영한
지식공유자

1. 네 생각하신 내용이 맞습니다.

2. 생각하신 이유들이 맞습니다. 여기에 추가로 자바빈 프로퍼티 접근법이라는 방식이 오래동안 정착되었습니다. 특정 자바 기술들은 프로퍼티 기반으로 데이터를 접근하기도 합니다. JSON 데이터를 만드는 라이브러리 등등

3. 이렇게 하면 Hibernate에 종속적이어서 가급적 사용하지 않는 것이 좋습니다. 거의 사용하지 않는다고 생각하시면 됩니다.

감사합니다.

이한빈님의 프로필 이미지
이한빈
질문자

덕분에 의문사항이 다 해결되었습니다. 친절한 답변 감사합니다.

이한빈님의 프로필 이미지
이한빈

작성한 질문수

질문하기