개발/Spring

자바 컬렉션 Set

728x90

Collection을 확장한 배열과 비슷한 역할을 하는 3개의 인터페이스가 존재한다. 

 

  • List
  • Set
  • Queue

Set는 순서에 상관 없이, 어떤 데이터가 존재하는지를 확인하기 위한 용도로 많이 사용된다.

다시 말해서 중복되는 것을 방지하고, 원하는 값이 포함되어 있는지를 확인하는 것이 주 용도다.

 

예) 1분간 사용자가 요청한 로그가 있고, 1분간 동일한 서버에 요청하는 중복 사용자 수는 매우 많다. 이러한 경우 배열로 확인하려고 한다면,

indexOf()라는 메소드로 해당 객체가 존재하는지 확인후에 add() 메소드로 추가하는 작업을 반복해야만 한다.

 

하지만 Set을 구현한 클래스를 사용하면 그냥 데이터를 추가만 해주면 된다.

 

그러면 자동으로 데이터가 중복되지 않고 저장된다. 이 때 순서는 중요하지 않다.

 

이처럼 어떤 값이 존재하는지 존재 여부만 중요할 때 Set을 사용하면 된다.

 

 

자바에서 Set인터페이스를 구현한 주요 클래스는 HashSet, TreeSet, LinkedHashSet이 있다.

 

  • HashSet : 순서가 전혀 필요없는 데이터를 해시 테이블에 저장한다. Set중에 성능이 가장 좋다.
  • TreeSet : 저장된 데이터의 값에 따라서 정렬되는 셋이다. REd-black이라는 트리타입으로 값이 저장되며 HashSet보다 성능이 약간 느리다
  • LinkedHashSet : 연결된 목록 타입으로 구현된 해시 테이블에 데이터를 저장한다., 저장된 순서 따라서 값이 정렬된다. 대신 성능이 이 셋 중에서 가장 나쁘다.

HashSet은 별도의 정렬 작업이 없어 제일 빠르다, 하지만 몇백만건을 처리하지 않는 이상 성능차이를 알긴 힘들다.

 

TreeSetdms red-black트리는 각 노드를 붉은색 혹은 검정색으로 구분하여 데이터를 더 빠르고 쉽게 찾을 수 있는 구조의 이진트리를 말한다.

 

 

HashSet에 대해서 파해쳐 보자

 

상속관계는 아래와 같다.

 

java.lang.Object

java.util.AbstractCollection<E>

java.util.AbstractSet<E>

java.util.HashSet<E>

 

구현되어 있는 메소드는 Obejct 클래스에 선언되어 있는 eqials(), hashCode() 메소드와 이 클래스에서 추가한 removeAll()뿐이다.

 

Set은 무엇보다도 데이터가 중복되는 것을 허용하지 않으므로, 데이터가 같은지를 확인하는 작업이 핵심이다.

 

 

equals와 hashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다. 그렇기 때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있다.

boolean equals(Object obj)로 정의된 equals 메소드는 2개의 객체가 동일한지 검사하기 위해 사용된다. eqauls가 구현된 방법은 2개의 객체가 참조하는 것이 동일한지를 확인하는 것이다.

즉, 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 동일한 객체가 된다.

int hashCode()로 정의된 hashCode 메소드는 실행 중에(Runtime) 객체의 유일한 integer값을 반환한다. Object 클래스에서는 heap에 저장된 객체의 메모리 주소를 반환하도록 되어있다. (항상 그런 것은 아니다.)

 

  • Serializable 원격으로 객체를 전송하거나 파일에 저장할 수 있음을 지정
  • Clonable Object 클래스의 Clone 메소드가 제대로 수행될 수 있음을 지정, 즉 복제가 가능한 객체임을 의미
  • Iterable<E> 객체가 foreach문장을 사용할 수 있음을 지정
  • Collection<E> 여러개의 객체를 하나의 객체에 담아 처리할 때의 메소드를 지정
  • Set<E> 셋 데이터를 처리하는 것과 관련된 메소드를 지정

 

Set 인터페이스에 정의되어 있는 메소드는 대부분 List와 비슷하다. 

그렇다면 List에만 정의되어 있고, Set에 정의되어 있지 않은 메소드는 어떤 것이 있을 까?

 

Set은 순서가 없기 때문에, 순서가 매개변수로 넘어가는 메소드나, 수행 결과가 데이터의 위치와 관련된 메소드는 Set인터페이스에서는 필요가 없다. 그러므로 get (int index)index(Object o) 와 같은 메소드들은 Set에 존재하지 않는다.

 

 

 

HashSet의 생성자들도 여러 종류가 있다.

4개의 생성자가 존재한다.

 

 

로드 팩터 ->  데이터의 갯수 / 저장공간 

데이터의 갯수가 증가하여 로드팩터보다 커지면 저장공간의 크기는 증가되고 해시 재정리 작업을 해야만 한다. 데이터가 해시 재정리 작업에 들어가면, 내부에 갖고 있는 자료구조를 다시 생성하는 단계를 거쳐야 하므로 성능에 영향이 발생한다.

 

 

로드팩터라는 값이 클수록 공간은 넉넉해지지만, 데이터를 찾는 시간은 증가한다. 따라서 초기 공간 갯수와 로드 팩터는 데이터의 크기를 고려하여 산정하는 것이 좋다. 왜냐하면 초기 크기가 데이터의 갯수/로드팩터 보다 클 경우에는 데이터를 쉽게 찾기 위한 해시 재정리 작업이 발생하지 않기 때문이다. 따라서 대량의 데이터를 여기에 담아 처리할 때에는 초기크기와 로드팩터의 값을 조절해가면서 가장 적당한 크기를 찾아야만 한다.

 

 

HashSet의 주요 메소드를 살펴보자,

 

HashSet에 선언되어 있는 메소드는 그리 많지 않다. 부모 클래스인 AbstractSet과 Abstrafct Collection에 선언 및 구현되어 있는 메소드를 그대로 사용하는 경우가 많다.

 

HashSet에 선언되어 있는 메소드 목록을 살펴보자.

 

 

boolean add -> 데이터 추가

void clear -> 모든 데이터 삭제

Object clone -> HashSet 객체를 복사한다. 하지만 담겨 있는 데이터들은 복제하지 않는다.

boolean contains -> 지정한 객체가 존재하는지 확인한다.

boolean isEmpty -> 데이터가 있는지 확인한다.

Iterator<E> iterator -> 데이터를 꺼내기 위한 Iterator 객체를 리턴한다.

boolean remove -> 매개변수로 넘어온 객체를 삭제한다.

int size -> 데이터의 갯수를 리턴한다.

 

 

메서드의 갯수는 그렇게 많지 않다 그만큼 사용법도 그리 어렵지 않다. 

 

HashSet과 같은 Set을 사용하면 여러 중복되는 값들을 걸러내는데 매우 유용하다.

 

저장되어 있는 값을 꺼내려면 for 루프를 사용하는 것이 편하다.

 

하지만 데이터를 저장한 순서대로 출력하지 않는다. 보관되어 있는 순서가 전혀 중요하지 않을 때 사용해야만 한다.

 

몇천 몇만건의 데이터 중 중복된 것을 거르는 작업을 하기 위해서는 Set을 사용하지 않으면 매우 코드가 복잡해진다.

 

 

Queue는 왜 필요할까?

 

LinkedList에서는 앞에 있는 애와 뒤에 있는 애만 기억한다. 배열의 중간에 데이터가 지속적으로 삭제되고 추가될 경우에는 배열보다 메모리 공간 측면에서 훨씬 유리하다. 왜냐하면 배열과 같ㅌ은 ArrayList와 Vector는 각 위치가 정해져 있고 그 위치로 데이터를 찾는다.

 

그런데 맨 앞의 값을 삭제하면 그 뒤에 있는 값들은 하나씩 앞으로 위치를 이동해야 제대로 된 위치의 값을 가지게 된다.

 

그에 반해 LinkedList는 중간에 있는 데이터를 삭제하면 지운 데이터의 앞에 있는 데이터와 뒤에 있는 데이터를 연결하면 금나이다.

 

위치를 맞추기 위해 값을 이동하는 단계를 거치지 않아도 된다.

 

그리고 LinkedList는 List인터페이스 뿐만 아니라 Queue, Deque인터페이스도 구현하고 있다.

 

LinkedList 자체가 List이면서도 Queue, Deque도 된다. 이 중에서 Queue를 먼저 살펴보자.

 

LinkedList 클래스가 구현한 인터페이스 중에 자바6에서 추가된 Deque라는 것이 있다. Double Ended Queue의 약자이며, Queue 인터페이스를 확장하였기 때문에 큐의 기능을 모두 포함한다. 맨 앞에 값을 넣거나 뺴는 작업, 맨 뒤에 값을 넣거나 뺴는 작업을 수행하는데 용이하다.

 

LinkedList를 살펴보자

 

java.lang.Object

java.util.AbstractCollection<E>

java.util.AbstractList<E>

java.util.AbstractSequentailList<E>

java.util.LinkedList<E>

 

ArrayList 클래스나 Vector 클래스와 상속관계는 비슷하지만, AbstractSqeuntailListrk 부모이다.

 

AbstractList와 AbstractSequentailList의 차이점은 add, set, remove등의 메소드에 대한구현 내용이 상이하다는 저옫이다.

LinkedList 구현한 인터페이스 목록을 살펴보자

 

Serializable 원격으로 객체를 전송하거나 파일에 저장할 수 있음을 지정

Cloneable Object 클래스의 clone 메소드가 제대로 수행될 수 있음을 지정, 즉 복제가 가능한 객체임을 의미함

Iterable<E> 객체가 foreach문을 사용할 수 있음을 지정

Colletion<E> 여러 개의 객체를 하나의 객체에 담아 처리할 때의 메소드 지정

Deque<E> 맨 앞과 맨 뒤의 값을 용이하게 처리하는 큐와 관련된 메소드 지정

List<E> 목록형 데이터를 처리하는 것과 관련된 메소드 지정

Queue<E> 큐를 처리하는 것과 관련된 메소드 지정

 

LinkedList는 List도 되고 큐도 된다. 두 인퍼테이스를 모두 구현한 특이한 클래스라고 볼 수 있다. 게다가 Deque 인터페이스도 구현하므로 맨 앞과 끝의 데이터를 쉽게 처리할 수 있다.

 

 

LinkedList의 생성자와 주요 메소드를 살펴보자.

LinkedList는 일반적인 배열 타입과 클래스와 다르게, 생성자로 객체를 생성할 때 처음부터 크기를 지정하지 ㅇ낳는다. 왜냐하면, 각 데이터들이 앞 뒤로 연결되는 구조이기 때문에 미리 공간을 만들어 놓을 필요가 없다. 따라서 두 가지 생성자만 존재함.

 

LinkedList() -> 비어있는 LinkedList를 생성한다.

LinkedList(Collecion<? extends E> c)  -> 매개변수로 받은 컬렉션 객체의 데이터를 LinkedList에 담는다.

 

LinkedList 클래스는 구현한 인터페이스의 종류가 매우 많기 때문에, 메소드의 종류도 다양하다.

 

각 메소드의 이름을 보면 매우 중복된 기능을 수행하는 메소드가 많은 것을 알 수 있다.

 

 

LinkedList 객체의 가장 앞에 데이터를 추가한다.

void addFirst 

boolean offerFirst

void push

 

LinkedList 객체의 가장 뒤에 데이터를 추갛나다.

boolean add

void addLast

boolean offer

boolean offerLast

 

LinkedList 객체의 특정  위치에 있는 데이터를 수정ㅎ나다. 그리고 기존에 있던 데이터를 리턴한다.

void add

 

LinkedList 객체의 특정 위치에 있는 데이터를 수정한다. 그리고 기존에 있던 데이터를 리턴한다.

Object set

 

매개변수로 넘긴 컬렉션의 데이터를 추가한다.

boolean addAll

 

매개변수로 넘긴 컬렉션의 데이터를 지정된 위치에 추가한다.

boolean addAll

 

LinkedList가 여러 종류의 인터페이스를 구현했기 때문에 이러한 상황이 발생한 것이다.

 

Last가 붙지 않은 조회용 메소드는 모두 맨 앞의 데이터를 가져온다고 생각하면 된다. 그리고 맨 앞에 데이터를 가져오는 메소드는 모두 내부적으로 getFirst()메소드를 호출하므로 이 메소드를 사용할 것을 권장한다. 추가로 LinkedList에 어떤 객체가 포함되어 있는지를 확인하는 메소드는 다음과 같은 것들ㅇ ㅣ있다.

contains

indexOf

lastINdexOf

 

데이터를 삭제하는 메소드의 목록은 다음과 같다. 대부분의 삭제 관련 메소드는 LinkedList 객체에서 삭제하고, 데이터를 리턴해주기 때문에 앞에서 삷펴본 조회용 메소드 보다는 이 메소드를 많이 사용한다.

 

 

삭제 관련 메소드들은 특정 데이터를 삭제하는 메소드를 제외하고는 모두 삭제된 데이터를 리턴한다. 맨 앞에 있는 데이터를 삭제하는 많은 메소드들은 모두 removeFirst() 메소드를 내부적으로 호출한다. 반대로 맨 뒤에 있는 데이터를 삭제하는 메소드들ㅇ느 removeList() 메소드를 내부적으로 호출한다. 혼동을 피하려면 remove가 붙은 메소드를 사용할 것을 권장ㅎ나다.

 

 

LinstIterator는 Iterator 인터페이스가 다음 데이터만을 검색할 수 있다는 단점을 보완하여 이전 데이터도 검색할 수 있는 이터레이터다

previous 메소드를 사용하면 이전 데이터를 확인할 수 있다.

 

 

 

 

 

정리 + 

 

Set은 중복되는 데이터를 처리하기 위해 사용되며, Queue는 먼저 들어온 데이터를 먼저 처리해주는 FIFO 기능을 처리하기 위해서 사용된다 .특히 Queue는 여러 쓰레드에서 들어오는 작업을 순차적으로 처리할 때 많이 사용된다.

 

Set은 데이터로 검색할 수 있다. List에서 데이터를 검색하려면 0번 째부터 하나씩 찾아봐야 하지만, Set은 보다 빠르게 데이터를 검색할 수 있다.

Queue는 가장 앞에 요청 뿐만 아니라 가장 마지막에 온 요청도 빨리 찾을 수 있다. 그래서 웹서버나 WAS등과 같이 사용자의 요청을 처리하는 서버에서 이러한 큐로 처리한다고 보면 된다.