이 글은 코딩 팩토리님의 equatable 강의를 바탕으로 참고했습니다.
이제 회사에서 새로운 앱을 만들기 앞서,
필요한 플러터 지식을 쌓기 위해 고전해야겠다고 생각이 들어
필요할 것 같은 플러그인을 찾고 있는 중이다.
그중에 Formz라는 로그인 기능 구현 시 유용한 플러그인을 발견하게 되었는데,
같이 사용할 수 있는 Equatable이라는 플러그인을 알게 되었다.
Formz를 배우기 앞서 Equatable에 대한 개념을 잡고 가고자 한다.
우선 인스턴스에 대해 알아볼 필요가 있다.
객체의 인스턴스에 대한 개념은 지난 싱글톤 글에 대해서 설명한 적이 있다.
간단히 설명하면 객체는 하나의 인스턴스를 가지게 되는데,
같은 객체를 두 개 생성한다고 하더라도,
두 객체는 각각의 인스턴스를 가지게 된다.
두 객체를 비교하는 코드를 짜 보았다.
...
class Person {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
}
class Screen extends StatefulWidget {
@override
_ScreenState createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
final person1 = Person(id: 1, name: 'sooyeol', age: 26);
final person2 = Person(id: 1, name: 'sooyeol', age: 26);
renderText(String text) {
return Row(
children: [
Text(
text,
style: TextStyle(
fontSize: 20.0
)
)
]
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('equatable'),
),
body: Column(
children: [
renderText('person1.id == person2.id : ${person1.id == person2.id}'),
renderText('person1.name == person2.name : ${person1.name == person2.name}'),
renderText('person1.age == person2.age : ${person1.age == person2.age}'),
renderText('person1 == person2 : ${person1 == person2}'),
]
),
);
}
}
}
Person이라는 모델 클래스를 만들어 주고,
build 메서드 내부의 id, name, age가 똑같은 두 개의 Person객체를 생성하였다.
이 두 개의 멤버 변수와 객체 자체를 비교해보면 결과는 다음과 같다.
id, name, age의 값은 같지만(true)
객체 자체를 비교할 땐 false라고 나오는 것이다.
그렇다면 위에서 설명했던 같은 객체라도 서로의 인스턴스(메모리의 저장되는 위치)는
다르기 때문에 이런 결과가 나오는 것이다.
그렇다면 우리가 만약에 로그인 기능을 구현한다고 가정할 때,
유저가 하나의 모델을 가지고 있다고 했을 때,
중복되지 않게 한 사람당 모델을 부여한다고 조건을 추가하면,
위에 처럼 객체 자체를 비교할 때는 성립되지 않을 것이다.
컴퓨터공학적으로는 저 두 객체는 다르지만,
명시적으로(앱에서 사용되고 우리에게 보일 땐)
저 두 객체가 같다고 어떻게 표현할 수 있을까?
코딩 팩토리님은 아래와 같은 방법을 알려주셨다.
class Person {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
bool operator ==(Object other) {
// TODO: implement ==
return super == other;
}
@override
// TODO: implement hashCode
int get hashCode => super.hashCode;
}
모델 클래스 안에 operator와 hashCode 함수를 넣어주는 것이다.
명시적으로 비교할 때 이 두 함수를 쓰게 된다.
어째서 아무것도 상속되지 않은 클래스 안에서 이 두 가지에 메서드
호출이 가능한 것일까?
이유는 보이지는 않지만 Person class는 Object 클래스를 상속받고 있다.
다만 생략했을 때는 보이지 않고 자동으로 상속이 되어있다.
이는 dart 언어가 객체지향 프로그래밍(Object-Oriented Programming 이하 OOP)이기
때문에 Object가 제일 상위 클래스로 상속이 되는 것이다.
class Person (extends Object){ //보이지 않는다는 의미로 괄호를 넣었을 뿐 실제로 이렇게 사용하지 않습니다.
...
}
그렇다면 명시적으로 객체가 같다는 것을 어떻게 알 수 있을까?
아래와 같이 코드를 짜 보자.
class Person {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
bool operator ==(Object other) {
return other is Person;
}
@override
// TODO: implement hashCode
int get hashCode => super.hashCode;
}
operator == 메서드를 적용했기 때문에,
다른 객체가 Person 모델이면 true로 반환될 수 있게
도와줍니다.
다음과 같은 결과가 나옵니다.
그렇다면, 다른 멤버 변수인 id, name, age도 비교할 수 있게 조건을 추가해줍시다.
기존 코드에서는 코드 상에 표시된 1, sooyeol, 26이라는 값으로,
임시로 비교해 주었고,
객체 자체를 비교한 것과 마찬가지로 hashCode를 사용하여 비교해주는 편이 훨씬 더 좋습니다.
그럼 여기서 hashCode에 개념을 잠시 익혀보겠습니다.
HashCode는 보통 Map이나 Set에서 키의 역할을 해주는데,
Map이나 Set에서는 키가 중복으로 저장될 수 없습니다.
만약 Hashcode에서
class Person {
...
@override
bool operator ==(Object other) {
return other is Person && other.id == this.id;
}
@override
int get hashCode {
return this.id;
}
}
id만을 같은 hashCode로 정의한다면,
Map이나 Set안에서는 중복이 없기 때문에
하나의 hashCode만이 찍히게 됩니다.
왜냐하면 operator == 메서드에서
Person객체 이거나 같은 id를 가진다는 조건이 맞으면
hashCode메서드에서 하나의(같은) HashCode가 생성되기 때문입니다.
저도 이해하기 어려웠는데, 계속 보니 어느 정도(저도 어렵습니다) 이해가 갑니다.
class Person {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
bool operator ==(Object other) {
return other is Person && other.id == this.id;
}
// 만약 객체의 멤버변수나 객체 자체를 비교했을 때, operator == 메서드가 실행이되고,
// 비교하는 두 객체 1과 2가 id가 같고, Person 클래스로 되어있으면(위에 return문이 true일때)
// 아래 hashCode 메서드가 실행이 되고, this.id값이 리턴이 됩니다. 이 this.id값은 hashCode로
// 변환이 되어 반환합니다. ex) 111023332 <= 해시코드(예를 든 겁니다.)
// 그렇다면, id의 값이 1이고, 비교하는 id의 값이 1이면,
// 둘다 해시코드가 111023332이므로, 둘은 같은 값을 값게 되고,
// Set이나 Map에 이 person값을 담으면 1개의 값만 담기는 것을 알 수 있게 됩니다.
@override
int get hashCode {
return this.id;
}
}
주석 처리로 적어놓았지만 한 번 더 적겠습니다.
만약 객체의 멤버 변수나 객체 자체를 비교했을 때, operator == 메서드가 실행이 되고,
비교하는 두 객체 1과 2가 id가 같고, Person 클래스로 되어있으면(위에 return문이 true일 때)
아래 hashCode 메서드가 실행이 되고, this.id값이 리턴이 됩니다. 이 this.id값은 hashCode로
변환이 되어 반환합니다. ex) 111023332 <= 해시 코드(예를 든 겁니다.)
그렇다면, id의 값이 1이고, 비교하는 id의 값이 1이면,
둘 다 해시 코드가 111023332이므로, 둘은 같은 값을 갖게 되고,
Set이나 Map에 이 person값을 담으면 1개의 값만 담기는 것을 알 수 있게 됩니다.
그렇다면 한번 Map에 담아 프린트를 찍어보겠습니다.
Person person1;
Person person2;
@override
initState() {
person1 = Person(id: 1, name: 'sooyeol', age: 26);
person2 = Person(id: 1, name: 'inyeong', age: 22);
Map map = <Person, bool> {
person1: true,
person2: true,
};
print(map.length);
super.initState();
}
두 객체는 id값만 같지, name값과 age값이 다릅니다.
하지만 해시코드는 id값과 Person객체인지 아닌지에 체크를 하고,
이게 맞으면,
id값으로 만들어집니다.
고로,
두 객체는 같은 해시 코드를 가지게 되고,
중복 값을 담을 수 없는 Map은 하나의 객체를 가지게 됩니다.
그렇다면, 좀 더 자유롭게 조건을 넣어도 되겠죠??
아래와 같이 바꿀 수 있습니다.
즉, operator는 비교 알고리즘을 자유롭게 변경할 수 있습니다.
class Person {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
bool operator ==(Object other) {
return other is Person &&
other.id == this.id &&
other.name == this.name &&
other.age == this.age;
}
@override
int get hashCode {
return this.id + this.name.hashCode + this.age;
}
}
이번에는 객체 자체와 id, name, age값을 모두 중복 체크를 하고,
저 모든 조건에 충족이 되면(중복 값이 생기면)
중복되는 객체의 해시 코드를 비교해 중복을 없애는 형식으로 바꾸어 줄 수 있습니다.
해시 코드는 id값 + name값 + age값으로 바꾸어 주어, id, name, age 모두를 비교해주는 값으로
설정할 수 있습니다.
name 같은 경우는 String이기 때문에 숫자열로 바꾸어주는 hashCode함수를 붙여주었습니다.
결과적으로 다시 한번 map.length를 프린트해 보면
2라는 값이 나오는 것을 볼 수 있습니다.
name값과 age값이 다르기 때문에
위에 조건과 맞지 않기 때문에(중복이 없기 때문에)
두 개의 고유의 Person객체가 맵에 들어가게 된 거죠.
이해가 되셨나요?
그럼 이 hashCode를 쓰는 이유는 무엇일까요?
hashCode를 이해하느라.. 이 녀석을 쓰는 이유를 잊으셨을까 봐
다시 적어드리려 합니다.
식별을 위한 것입니다.
예를 들어 내 앱 서비스의 유저가 같은 이름, 나이, 성별(물론 이것보다 휴대폰 번호 등등 여러 정보가 더 많지만)로만
가입을 할 때, 같은 유저가 두 아이디를 가지지 못하게 하기 위해
저 해시 코드를 부여하여, 중복을 없애기 위해서입니다.
이밖에도 여러 용도로 사용할 수 있으니, 알아두시면 좋을 것 같다고 생각이 듭니다.
하지만, hashCode에 대해 이해는 되었지만,
만약 이것을 쓰게 되었을 때,
너무 복잡하고 번거롭기도 합니다.
그래서 더 간단히 해결할 수 있는 방법을 알려드리겠습니다.
이 방법을 소개해 드리기 위해 설명이 길어졌네요..
개념 잡는데 도움이 되실 겁니다.
바로 Equatable입니다.
바로 위에서 사용했던 operator == 함수와
hashCode 함수를 응용했던 값 비교를
이 패키지에서 간단히 한 코드로 자동화해줍니다.
놀랍지 않나요..
우선 아래 패키지를
pubspec.yaml 파일에 넣어줍니다.
https://pub.dev/packages/equatable/install
그러고 나서 아까 사용했던, Person모델에 Equatable을 상속시켜 줍니다.
class Person extends Equatable {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
// TODO: implement props
List<Object> get props => throw UnimplementedError();
}
그러면 Person아래에 빨간 줄이 생기고,
함수 하나를 override 해주라고 합니다.
overriding을 하게 되면,
@override
// TODO: implement props
List <Object> get props => throw UnimplementedError();
아래와 같은 List <Object> 타입에 props 메서드가 생성됩니다.
그리고 이곳에 리스트 값을 다음과 같이 넣어줍니다.
class Person extends Equatable {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
List<Object> get props => [this.id, this.name, this.age];
}
...
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'sooyeol', age: 26);
final person2 = Person(id: 1, name: 'sooyeol', age: 26);
...
}
이렇게 넣어주면,
id, name, age 값을 모두 비교해 줍니다.
심지어 모든 값이 같으면 같은 값을 가진 객체인지 아닌지도 구분해 줍니다.
엄청나지 않나요?
한 번 값이 같은 Person 객체를 비교해 보겠습니다.
모두 true가 나오는 것을 볼 수 있습니다.
그러면 모델의 멤버 변수의 값을 변경해 보겠습니다.
class Person extends Equatable {
final int id;
final String name;
final int age;
Person({
@required this.id,
@required this.name,
@required this.age
});
@override
List<Object> get props => [this.id, this.name, this.age];
}
...
@override
Widget build(BuildContext context) {
final person1 = Person(id: 1, name: 'sooyeol', age: 26);
final person2 = Person(id: 1, name: 'inyeong', age: 22);
...
}
id는 같게 하고, 이름과 나이만 다르게 입력했습니다.
결과는 다음과 같습니다.
id를 제외한 나머지 값은 false가 나왔으며,
값이 다 다르니 두 객체의 비교는 false가 나왔습니다.
이렇게 위에서 사용했던 operator == 메서드와 HashCode함수를 사용하지 않아도,
이리 간단히 객체와 객체의 멤버 변수까지 비교해주는 Equatable 플러그인을 알아보았습니다.
잘 유용하게 사용하시면 좋겠습니다.
감사합니다.