[JAVA]List 중복 객체 제거(Set,Collection Stream)

홍헌·2024년 5월 18일
0

java

목록 보기
4/4

쓰게 된 계기

프로젝트 진행 중 유저들이 방문횟수가 아닌 데일리로 방문을 했는지 여부만 확인할 수 있게 해달라는 요청이 있었다. 이 요청만 있었으면 크게 문제가 되지 않았겠지만 중간에 비지니스 로직이 끼어있어서 그냥 서비스단에서 처리하기로 했다.

1. 처음 생각한 코드

고객들의 정보는 리스트 형식으로 되어있었다. 아주 간단하게 만든 예시를 보자.

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을 선택했다.

2. Set선택 이유

Set을 선택한 이유는 두 가지였다. 첫번째는 순서가 상관없으면서 set이 Stream보다 성능상 낫고, 두번째는 코드가 길거 같지 않아서였다.

우선 Set내부 구조를 보기위해 실제로 구현한 HashSet을 예시로 보자.

HashSet은 내부적으로 HashMap을 사용하고 있다. 이를 통해 해시테이블을 만들어서 키-값 형식으로 저장한다. (해시테이블은 키를 해시코드로 변환하고 해당 코드를 인덱스로 값을 저장한다.)

위와 같이 map에 값을 넣는 방식을 이용하는 것이다. 반면 Stream의 경우 distinct()로 중복을 제거할 때 equals를 이용하여 많은 오버헤드를 발생시킨다고 알고 있어 성능상 좋지 못하다고 들었기 때문이다.
(distinct()가 어떻게 동작하는지는 클래스 파일에 들어가서 직접 볼 수 없었기 때문에 javadevcentral이나 java공식문서에서 확인했다.)
또한 stream을 사용하는 이유 중의 하나가 코드를 간결하게 보기 위해서도 있는데 set을 이용한다고 코드가 길어질 것 같지 않아서였다.

3. 문제점

다만 코드를 작성하다보니 내가 간과한 게 있었다. 해당 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());
	}

}

따라서 출력해보면


중복이 제거 되지 않는다.

4. 해결방법

이런 경우 해결 방법을 생각해 본 적이 없어서 찾아보니 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메소드로 비교해서 저장해야 하기 때문에 어떤 기준으로 객체가 같은지 판별할 수 있어야 하기 때문이다.

5. 내용 추가(06/12)

비슷한 내용의 코드를 만들어야 하는 경우가 생겨 만들던 중 코드에 오류 발생 여지가 있다는 점을 알게 되었다. 내가 예시로 올린 코드는 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()로 비교하면 된다.

0개의 댓글