안드로이드 개발을 하면서 data class를 많이 써왔지만 내가 data class를 왜 써야되는지? 어떤 기능이 있는지를 알고 써왔던가? 라는 생각을 해봤다.
그래서 늦었지만 지금이라도 알고 쓰고 싶었다.
data class의 주목적은 데이터 보관용 객체를 손쉽게 만들기 위함이다.
예를 들면 기본 클래스에서 정의되어있는 3가지 기능들을 보자
class는 기본적으로 Java Object를 상속받고 있다. Object를 상속 받으면
3가지 함수를 override 할 수 있는데
1.toString()
2.equals()
3.hashcode()
하지만 위에 기능은 데이터 보관용으로 사용하기엔 부족한 부분이 많다.
간단하게 테스트 해보면서 알아보자
과연 뭐가 나올까? 기본적으로 보면 user1과 user2는 프로퍼티 값이 같으니깐 같은 데이터라고 봐도된다. 그러면 true가 나올 것 같지만
모두가 예상하듯이? false가 나온다. 그 이유는 Object에서 제공하는 equals()의 함수는 객체의 주소값을 비교하기 때문이다. 그래서 false가 나온 것이다.
이런 괴상한 값이 나온다. 그 이유도 역시
클래스명@해시코드(16진법 변환) 이렇게 나오도록 구현이 되어있기 때문이다.
또 괴상한 값이 나왔다. 이 역시 객체의 주소기반으로 해시코드로 변환한 값이다.
결국 객체의 주소가 달라지면 해시코드의 값도 달라진다!
위에 같이 알아본 기본 기능들은 사실 데이터를 다루는 객체에게 있어서 아무 소용도 없는 기능이다. 그래서 데이터 보관용 객체로 사용하기엔 부적절하다.
그래서 나온 것이... data class이다
data class는 위에 알아본 3가지 기능 + copy() 라는 메소드를 자동으로 만들어주는 class이다.
응? 일반 클래스도 자동으로 생성해주는 거 아닌가?
data class는 3가지 메소드에서 데이터 보관용 객체에 알맞게 재정의 한 것이다.
위에 3가지 기능과 copy()까지 총 4가지 메소드를 같이 알아보자
아까 위에서 본 코드랑 별반 다를게 없다. 다른거라곤 class앞에 data가 붙은 것이다.
근데 결과는 정반대이다.
true가 나온다. 왜일까.. 뭐가 다른걸까
기본 class에서 equals()은 주소값을 비교한다면 data class에서 재정의한 equals()은 생성자의 값이 같은 지를 비교하기 때문이다!
물론 기본 class에서 equals()를 override하면 data class처럼 생성자의 값을 비교하도록 만들 수 있다. 하지만 보일러 플레이트 코드가 엄청 많아질 것이다. data class는 이걸 컴파일 단계에서 재정의 해준다.
과연 결과는?
👏👏👏 정말 보기 쉽게 잘 나오는 것 같다. 이런게 바로 data class를 사용할 수 밖에 없는 이유인 것 같다.
기본 class에선 객체의 주소기반으로 해시코드로 변환한다고 말했었다. 하지만 data class에선 프로퍼티에 값들의 기반으로 해시코드로 변환하기 때문에..!! 결과는
같다!.. 그렇기에 equals()를 해도 true가 나온다.프로퍼티 값이 같다는 건 같은 객체라고 본다는 뜻이다.
그렇기에 data class에서 쉽게 같은 값을 비교하여 찾아내거나 삭제할 수 있다.
copy()는 메소드 이름에서 알 수 있듯이 객체를 그대로 복사하는 것이다.
하지만 copy()는 얕은 복사(Shallow Copy)이기 때문에 잘 사용해야된다.
얕은 복사(Shallow Copy)란 간단하게 말해서 프로퍼티의 값을 복사하는 것이 아니라 주소값을 복사하는 걸 말한다. 그러니깐 같은 주소값을 바라보면 다음과 같은 문제가 발생한다.
이렇게 Array를 추가해보자. !
결과는
friends의 값을 user1만 바꿨음에도 user2에 friends 값이 같이 바뀌었다.
그 이유는 위에 말한 것과 같이 얕은 복사가 일어났기 때문이다.
주소값이 복사돼서 값이 같이 바뀐 것이다. 이건 우리가 원하는 동작이 아니다.
그래서 만약 Array나 MutableList같이 가변 객체를 생성자로 사용할 때는 copy를 재정의 해줄 필요가 있다.
기본 copy는 final이라 재정의가 안됨으로 새로 deepCopy를 만들어서 사용하면
우리가 원하는 결과를 얻을 수 있다.
흠.... 근데 알아보기전에 data class에 equals()를 override 할 이유가 있을까라는 의문이 들 것이다.
data class에서 재정의해주는 equals()에선 주 생성자의 프로퍼티 값으로만 판단하기 때문에 만약 주 생성자 이외에 멤버변수의 값도 같이 비교하기 위해 재정의를 해줘야된다.
이렇게 재정의를 해준다면
gender라는 값이 다르기때문에 false가 나오는 걸 볼 수있다.
그래! equals()를 override 했다 근데 다른 걸 왜해줘야되는데?
물론 toString()도 override해서 name, age 뿐만 아니라 gender까지 나오게 하면 더할나위가 없을 것이다. 하지만 toString()은 옵셔널이지만 hashCode()는 필수로 override를 해줘야된다. 그 이유가 뭘까?
사실 hashCode를 재정의 안해줘도 equals() 함수만 재정의해줘도 큰 문제는 없다. 하지만 어디서 문제가 나타나냐면 바로 해시기반의 자료구조에서 문제가 나타난다.
해시기반에 자료구조인 HashMap, HashSet이 있는데 왜 문제가 될까?
바로 해시기반에 자료구조에서 값을 비교하는 동작원리를 보면 이해가 될 것이다.
HashMap이나 HashSet에서 값을 비교할 때 equals()를 쓴다. 응? equals()는 재정의를 해줬으니 문제가 없지않을까라는 생각을 한다. 하지만 equals()를 하기전에 hashCode가 같은지 먼저 확인하는 로직이 있어서 문제가 발생하는 것이다.
hashCode가 다르면 우선 다른 객체라고 보기때문에 equals()을 호출도 안할 것이다. 그렇기에 탐색 속도가 월등히 빠를 것이다. 만약 hashCode를 재정의 안해준다면 gender에 값이 달라도 hashCode에서 판단하지 못하고 객체의 프로퍼티를 하나씩 비교하는 작업을 통해 판단할 수 있을것이다.
지금까지는 현실적인 이유였다. 그니깐 주생성자 이외에 멤버변수 때문에 override한 이유이고 좀 더 간단한 이유를 보자면
equals() override를 해서 나는 name으로만 판단한다고 한다.
그럼 age가 달라져도 결과는
가 나올 것이다. 하지만 hashCode에 값을 보면 다르다.
hashMap을 예를 들어보면
위에서 말했듯이 제거가 안되는 걸 볼 수있다.
해시기반의 자료구조는 equals()전에 hashCode가 같은지 먼저 확인하는 로직때문에 name이 같아도 제거하지 못하는 오류를 범할 수 있다. 그래서 equals()와 hashCode()는 같이 override하는 걸 권장하는 이유이다!