프로젝트 진행 중 유저들이 방문횟수가 아닌 데일리로 방문을 했는지 여부만 확인할 수 있게 해달라는 요청이 있었다. 이 요청만 있었으면 크게 문제가 되지 않았겠지만 중간에 비지니스 로직이 끼어있어서 그냥 서비스단에서 처리하기로 했다.
고객들의 정보는 리스트 형식으로 되어있었다. 아주 간단하게 만든 예시를 보자.
Test.java
public class Test {
public static void main(String[] args) {
List<TestVo> list = List.of(
new TestVo("person1", "2024-05-03"),
new TestVo("person1", "2024-05-03"),
new TestVo("person2", "2024-05-03"),
new TestVo("person2", "2024-05-02"),
new TestVo("person2", "2024-05-02")
);
}
}
TestVO.java
public class TestVo {
private String userId;
private String date;
//~~getter,setter, constructor 생략
}
이러한 형식을 생각했을 때 내가 생각난 방법은 두 가지였다.
Stream과 Set이었다.
public class Test {
public static void main(String[] args) {
List<TestVo> list = List.of(
new TestVo("person1", "2024-05-03"),
new TestVo("person1", "2024-05-03"),
new TestVo("person2", "2024-05-03"),
new TestVo("person2", "2024-05-02"),
new TestVo("person2", "2024-05-02")
);
//Stream으로 중복 제거
List<TestVo> newList =
list.stream().distinct().collect(Collectors.toList());
//Set으로 중복 제거
Set<TestVo> newSet = new HashSet<>(list);
}
}
예를 들면 위와 같이 짤 수 있는데 필자는 Set을 선택했다.
Set을 선택한 이유는 두 가지였다. 첫번째는 순서가 상관없으면서 set이 Stream보다 성능상 낫고, 두번째는 코드가 길거 같지 않아서였다.
우선 Set내부 구조를 보기위해 실제로 구현한 HashSet을 예시로 보자.
HashSet은 내부적으로 HashMap을 사용하고 있다. 이를 통해 해시테이블을 만들어서 키-값 형식으로 저장한다. (해시테이블은 키를 해시코드로 변환하고 해당 코드를 인덱스로 값을 저장한다.)
위와 같이 map에 값을 넣는 방식을 이용하는 것이다. 반면 Stream의 경우 distinct()로 중복을 제거할 때 equals를 이용하여 많은 오버헤드를 발생시킨다고 알고 있어 성능상 좋지 못하다고 들었기 때문이다.
(distinct()가 어떻게 동작하는지는 클래스 파일에 들어가서 직접 볼 수 없었기 때문에 javadevcentral이나 java공식문서에서 확인했다.)
또한 stream을 사용하는 이유 중의 하나가 코드를 간결하게 보기 위해서도 있는데 set을 이용한다고 코드가 길어질 것 같지 않아서였다.
다만 코드를 작성하다보니 내가 간과한 게 있었다. 해당 list는 객체리스트였다. 이는 객체의 주소값이 달라지고 결국 해시코드가 다 달라져 모든 객체들이 담기게 된다.
public class Test {
public static void main(String[] args) {
List<TestVo> list = List.of(
new TestVo("person1", "2024-05-03"),
new TestVo("person1", "2024-05-03"),
new TestVo("person2", "2024-05-03"),
new TestVo("person2", "2024-05-02"),
new TestVo("person2", "2024-05-02")
);
List<TestVo> newList = list.stream().distinct().collect(Collectors.toList());
Set<TestVo> newSet = new HashSet<>(list);
System.out.println("newList");
for(TestVo test:newList)
System.out.println(test.getUserId()+" : "+ test.getDate());
System.out.println();
System.out.println("newSet");
for(TestVo test:newSet)
System.out.println(test.getUserId()+" : "+ test.getDate());
}
}
따라서 출력해보면
중복이 제거 되지 않는다.
이런 경우 해결 방법을 생각해 본 적이 없어서 찾아보니 hashcode 메소드와 equals 메소드를 오버라이드 해주는 걸로 간단히 해결 가능했다.
public class TestVo {
private String userId;
private String date;
//~~getter,setter, constructor 생략
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof TestVo))
return false;
TestVo temp = (TestVo) obj;
return this.userId.equals(temp.userId) && this.date.equals(temp.date);
}
@Override
public int hashCode() {
// 해시 충돌 방지
int result = 20 + this.userId.hashCode();
result = this.date.hashCode() / 3 + result * 3;
return result;
}
}
위와 같이 필드의 값들만 기준으로 해시코드를 계산하게 하면 동일한 값들이 있을 때는 동일한 해시코드를 리턴하여 간단하게 중복값을 지울 수 있게 되었다.
다만 여기서 equals메소드도 재정의하고, hashcode를
@Override
public int hashCode() {
// 해시 충돌 방지
return = this.userId.hashCode() + this.date.hashCode();
}
이런 식으로 간단하게 적지 않은 이유는 정확하고 빠르게 비교하기 위해서이다.
해시코드는 potentially equal이다. 즉 잠재적으로 같은지 비교하는 것이다. 해시코드가 다를 경우 절대 같은 객체일 수는 없지만, 해시코드가 같을 때 같거나 다른 객체일 수 있다. 따라서 해시코드가 다를 때는 무조건 다른 값으로 생각하고 해시테이블에 저장하는데, 해시코드가 같을 때는 equals메소드로 비교해서 저장해야 하기 때문에 어떤 기준으로 객체가 같은지 판별할 수 있어야 하기 때문이다.
비슷한 내용의 코드를 만들어야 하는 경우가 생겨 만들던 중 코드에 오류 발생 여지가 있다는 점을 알게 되었다. 내가 예시로 올린 코드는 hashcode와 equals를 사용할 때 내부 필드들이 null값이 아니라는 상황을 전제하에 한다. 따라서 해당 필드들이 null값인 경우 오류가 발생할 여지가 있다.
package methodArrangement.EX10;
import java.util.Objects;
public class TestVo {
private String userId;
private String date;
//~~getter,setter, constructor 생략
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof TestVo)) {
return false;
}
TestVo temp = (TestVo) obj;
//return this.userId.equals(temp.userId) && this.date.equals(temp.date);
return Objects.equals(this.date, temp.date)&&Objects.equals(this.userId, temp.userId);
}
@Override
public int hashCode() {
// 해시 충돌 방지
//int result = 20 + this.userId.hashCode();
//result = this.date.hashCode() / 3 + (result * 3);
//return result;
return Objects.hash(this.userId,this.date);
}
}
Objects.hash()는 Java7에서 도입되었다고 하는데 Objects.hashcode()와 달리 복수의 대상을 인자로 받아 hashcode를 반환한다. 이 hashcode가 잠재적으로 같은 경우 equals를 호출하고 이 equals 메소드에서 오류가 나지 않게 하기 위해서는 Objects.equals()로 비교하면 된다.