이번 포스팅에서는 컬렉션 프레임워크의 기본 인터페이스 중 하나인 Map에 속하는 클래스들에 대해 알아보려한다. Map은 Key(K), Value(V)로 구성된 데이터인데 예시로 들 수 있는 것은 json이라는 파일 형식과 Python의 Dictionary 자료형이다. Map은 데이터 접근을 위해 Key를 사용하기 때문에 Key는 중복이 불가능하나, 데이터(Value)는 Key가 다르더라도 동일한 값을 저장하는 것이 가능하다.
{
"username": "bluesky",
"nickname": "bluesky"
}
Map은 Key와 Value 두 데이터를 한 번에 다루기 때문에 다른 컬렉션 프레임워크와 비교했을 때 구조가 독특하다. Key와 Value는 Map 클래스 내부의 Entry라는 내부 클래스에 정의되어 있으며, Map은 Entry 객체의 배열을 가지고 있는 형태다. 이러한 구조를 토대로 Map 역시 여러 파생형을 가지는데, 대표적인 Map 클래스로 HashMap, LinkedHashMap, TreeMap, Properties를 들 수 있다.
HashMap부터 하나씩 살펴보자.
1. HashMap 클래스
* 컬렉션 프레임워크 등장 전 HashTable이라는 클래스가 존재했으나, 컬렉션 프레임워크 등장과 동시에 HashMap이 HashTable을 계승했다. Vector와 마찬가지로 HashTable 역시 아직까지 연관된 코드가 있어 그대로 남아있는 것이다.
HashMap은 이름에서도 알 수 있듯이 Hash를 사용하는 Map이다. 이전의 포스팅에서 다루었던 Comparator.compareTo()에서 두 값을 비교할 때 사용한 Object.hashCode()를 사용하여, Key 값의 중복 여부를 확인함과 동시에 데이터 조회, 저장, 삭제 시 입력된 Key 값을 실제 Key 값과 비교하는데 사용한다.
위의 그림을 보면 Map은 Key 저장 시 Key에 Hash 함수를 적용한 값을 배열에 저장한다. 그리고 Value는 Key의 Hash값이 저장된 배열 메모리와 링크(Link)로 연결된다. 이 구조를 보면 Map의 장점이 확실히 나타나는데, Key를 통한 검색은 배열 구조를 이루고 있어 매우 빠르며, 값(Value)의 저장은 링크를 사용하여 값이 저장된 메모리 주소만 가리키기 때문에 데이터의 삭제, 추가, 수정이 매우 용이하다는 것이다.
HashMap은 Key가 중복을 허용하지 않는 특성으로 인해 Set과 매우 유사하게 동작하는데, 이 때문에 HashMap 역시 Entry(Key와 Value를 동시에 일컫는 용어다)를 출력하는 순서가 입력 순서와는 다르다. 만약 순서까지 고려해야하는 경우라면 HashMap 대신 LinkedHashMap을 사용하면 된다.
HashMap은 Key와 Value, 즉 Entry를 동시에 다루는 컬렉션 프레임워크이기 때문에, Entry를 추가하는 매서드의 이름이 put()으로 - 다른 클래스와 상이하게, add() - 지정되어 있다.
만약 Key 값이 동일하나 Value는 다른 데이터를 put으로 추가한다면, 기존의 Key 값에 저장된 Value가 추가된 Value로 대체된다.
물론, 값을 대체하는 replace()라는 매서드도 존재한다. replace()는 매개변수로 사용된 Key 값이 Map 객체 내에 존재하지 않는다면 false 만 되돌려 아무런 동작을 하지 않지만, put은 Map 객체 내에 Key 값이 없을 경우 추가를 진행하기 때문에 데이터의 무결성(Integrity)이 중요한 상황에서 많이 사용한다.
replace()는 Key 값 외에도 지정되어 있던 Value 값의 일치여부까지 확인하여 값을 변경할 수도 있다. 이 경우 Key 값이 존재하더라도 해당 Key의 Value값이 매개변수에 입력한 값과 일치하지 않는다면 값이 변경되지 않는다.
위의 예시에서도 알 수 있듯이 HashMap에 저장된 Entry의 특정 Key 값에 저장된 Value 값을 조회하는 매서드는 get()이다. get() 매서드 역시 HashMap 내에 키 값이 존재하지 않는다면 단순히 false를 반환한다.
Key와 Value, 그리고 Entry는 별도로 추출이 가능한 매서드를 제공한다. Entry와 Key는 중복 허용이 진행되지 않는다는 특성으로 인해 Set 객체로, Value는 컬렉션 프레임워크의 최상의 인터페이스인 Collection 객체로 반환되도록 한다.
entrySet(): HashMap의 모든 엔트리를 Set 형태로 반환
keySet() : HashMap의 모든 키를 Set 형태로 반환
values(): HashMap의 모든 값(value)를 Collection 형태로 반환
entrySet()의 결과가 Key=Value 형태로 요소를 구성한다는 것만 빼면 크게 다룰만한 것은 없다.
지금까지 회원 정보 등 정형화된 데이터를 다루기 위해 필자는 위와 같이 별도의 클래스를 정의하여 예시로 많이 사용하였다. 클래스 내부 구조를 보면 알겠지만, 클래스 내 인스턴스 변수들은 Key / Value 형태를 띄고 있기 때문에 이들만 별도로 HashMap으로 구현하는 것도 가능하다.
Map의 결과는 Json이라고 불리는 파일 형식의 포맷과 유사한 형태를 띄는데, Json은 서버와의 API 통신 시 전송할 데이터(Request Body라고 한다)를 입력하는 형식으로 사용되는 파일 형식이다.
2. TreeMap 클래스
HashMap과 TreeMap의 관계는 HashSet과 TreeSet의 관계와 동일하다. TreeMap의 경우도 이진 검색(BinarySearch)를 적용하여 정렬과 범위 검색에서 효율적이라는 특성을 가진다. 다만 특정 값의 검색과 추가/삭제는 HashMap의 특성 상 TreeMap보다 크게 나쁘다고 할 수 없기 때문에 TreeMap의 경우 HashMap에서 정렬이 필요한 경우 사용하는 경우가 많다.
위에서 HashMap으로 생성한 객체들은 Key 값이 정렬되어 있지 않은데, 이를 TreeMap으로 변환하여 저장된 Entry를 출력하면 다음과 같이 나타난다.
HashMap에서 Key 값이 사전 정렬이 되지 않은 것에 반해, TreeMap은 Key 값이 사전 정렬된 순서대로 나타나는 것을 확인할 수 있다. 물론 이 순서 역시 앞서 배웠던 Comparator 인터페이스와 Comparator.compare() 매서드를 생성자에 적용하거나, TreeMap에서 제공하는 descendingKeySet()매서드를 사용하면 출력 순서의 튜닝 또는 역순 출력이 가능하다.
Comparator 인터페이스로 compare() 매서드 정의 시, 반환값이 1이면 데이터를 입력한 Key 순서대로 나열되며, -1인 경우 데이터 입력 순서의 역순으로 나열된다. 위의 예시에서 필자는 compare() 매서드 반환값을 -1로 지정했는데, 필자가 memberId -> name -> cpNum -> birthDate -> signUpDate 순으로 데이터를 입력했으나 저장은 그 역순으로 되어 있는 것을 확인할 수 있다. 즉, 위의 예시를 사용하면 TreeMap에서도 데이터 입력 순서를 유지할 수 있다.
단, 생성자에 정렬 순서를 Comparator로 지정할 때 주의해야 할 점은, 인스턴스 초기화 블럭 내에 TreeMap.put() 매서드를 사용하면 추후 아무리 데이터를 추가해도 데이터 추가가 되지 않는다는 점이다. 이유가 뭔지는 필자도 조금 더 확인이 필요하다.
범위 검색 역시 TreeSet과 마찬가지로 head, tail로 시작하는 매서드를 제공한다. headMap()은 매개인자로 입력된 Key 값보다 앞서 존재하는 자료를 NavigableMap 객체로 반환하고, tailMap()은 뒤에 존재하느 자료를 NavigableMap으로 반환한다. subMap()은 매개인자로 입력된 두 키 값 사이의 자료를 NavigableMap으로 반환한다.
3. Properties 클래스
Map 인터페이스의 마지막 클래스인 Properties다. 앞서 보았던 HashMap이나 TreeMap과 크게 다른 점은 없고, 단지 Key와 Value가 모두 String 객체로 입력된다는 것이다. 따라서 Properties는 만들고자 하는 프로그램의 환경설정이나 속성 등을 읽어들이거나 수정하기 위해 많이 사용한다. 그래서인지, 다른 Map 클래스와 비교하면 다른 파일 형식으로 변환하거나 파일로부터 읽어들이는 매서드 혹은 일괄 출력하는 매서드가 추가로 정의되어 있다.
Properties는 기본 생성자로 인스턴스를 생성할 수 있다. 그러나 데이터를 넣는 매서드의 이름은 store()를 사용하는데, HashMap, TreeMap에서 동일한 기능을 하는 매서드 이름인 put()과 다르다.
Properties의 인스턴스 객체 변수명을 print() 함수로 바로 출력하면, Map 형식의 데이터가 화면에 출력된다. 하지만 설정 내용을 저장하는 목적으로 사용되는 Properties의 경우, 이렇게 한 줄로 나열되면 가시성이 떨어지기 때문에, list()라는 매서드를 통해 출력을 진행하는 경우가 대부분이다.
list() 매서드 사용 시, 매개변수로 PrintSream 또는 PrintWriter라는 타입의 객체를 입력해야한다. 자세한 내용은 나중에 포스팅 할 I/O(입출력) 관련 포스팅에서 다루겠지만, 간략히 설명하자면 PrintStream에 저장된 내용, 혹은 문서로부터 읽어들인 파일의 내용을 출력한다고 보면 된다.
Properties의 내용을 읽어들여서 화면에 출력하는 역할을 list()가 담당한다면, 파일에 저장하는 기능은 store()가 담당한다. store()는 매개변수로 저장할 파일을 FileOutputStream 객체로, 그리고 Comment를 String 객체로 받는다.
파일 내용을 열어보면, 주석으로 입력한 String은 "#"으로 시작되는 문장으로 문서 최상단에 위치한다.
동일한 역할을 하는 매서드로 save()가 있으나 지금은 사용하지 않는(deprecated) 매서드이므로 store()를 사용하면 된다.
저장된 Properties 형식의 파일을 읽어들이는 기능도 존재하는데 load()라는 매서드가 그 역할을 한다. 매개변수로는 FileInputStream 객체로 저장한 파일을 지정하면 된다. 바로 조금 전에 저장한 properties.txt 저장 내용을 읽어들이는 방법은 아래와 같다.
Properties는 XML파일로 변환하거나 XML 파일 내용을 Properties로 변환할 수도 있다. store()와 load() 매서드 이름 뒤에 ToXML, FromXML만 추가한 매서드들이 그 기능을 담당한다.
그럼, 설정 파일을 Properties 객체로 읽어들였는데, 각 키에 저장된 값은 어떻게 읽어들이면 될까? 다른 Map 클래스에서 get() 매서드를 사용한 것과 달리 Properties는 getProperty()라는 매서드를 사용한다. getProperty는 매개인자로 키로 지정된 String을 입력하면 된다. 매개변수에 지정된 키가 존재하지 않으면 아무것도 반환하지(null) 않는다. 매개변수로 두 번 째 인자에 String을 추가할 수 있는데, 두 번째 인자는 null이 반환된 경우 반환할 문자열을 명시한다.
지정된 키의 값을 변경하기 위해서는 setProperty() 매서드를 사용하면 된다. 키가 존재하지 않으면 null을 반환한다.
다른 Map 클래스와 달리 Value에 해당하는 항목을 Collection으로 추출하는 기능은 존재하지 않으나, Key 값은 Enumeration과 Set 형태로 추출할 수 있는 기능은 propertyNames()와 stringPropertyNames()라는 매서드로 제공한다.
길고 길었던 컬렉션 프레임워크의 포스팅도 어느새 마무리되어간다. 다음 포스팅에서는 컬렉션 프레임워크를 조금 더 손쉽게 다룰 수 있는 도구인 Collections 클래스에 대해 알아보고 컬렉션 프레임워크를 마무리하려한다.
Fin.
'Java > Java Basic' 카테고리의 다른 글
[Java Basic] 39 - Generic 개요와 사용법 (0) | 2022.09.12 |
---|---|
[Java Basic] 38 - 컬렉션 프레임워크8 (java.util.Collections) (0) | 2022.09.07 |
[Java Basic] 36 - 컬렉션 프레임워크6 (HashSet, TreeSet) (0) | 2022.09.05 |
[Java Basic] 35 - 컬렉션 프레임워크5 (Arrays, Comparable, Comparator) (0) | 2022.09.05 |
[Java Basic] 34 - 컬렉션 프레임워크4 (Iterator, ListIterator, Enumeration) (0) | 2022.09.02 |
댓글