Finalize란?

Class의 resource를 자동으로 관리해주기 위한 장치이다. 

비유하자면, c++의 destructor(소멸자)의 역할을 한다. 

ex) file open을 한 후 작업이 끝났을 때, finalize에서 file close 작업을 처리하도록 등록을 한다. 그러면 객체 소멸 시 finalize가 실행 되면서 file close 작업을 진행하게 된다. 

* Finalize의 의의는 resource 소유권을 객체의 수명에 묶어서, 개발자가 아닌 platform/software에 의해 관리되도록 개발하는 것이다. 

 

하지만?

* Java 9에서 deprecated 되었다. 

resource 관리의 안전성을 보장해주는 method가 어째서 deprecated되었을까?

 

Finalize의 단점

Finalize를 override한 객체는 GC(garbage collector)에서 특별하게 처리된다. 

 

할당이 해제된 후 바로 GC에 수집되지 않는다. 해당 객체는 queue에 등록된 후 별도의 종료 thread가 queue에서 pop을 시켜 순차적으로 종료 시킨다. 때문에 finalize가 요청된 시점과는 다른 주기에 소멸 작업이 진행된다.

 

위 과정에서 발생하는 thread의 생성과 실행은 오버헤드를 발생시킨다. 

JVM는 작업의 안전성 보장을 위해 finalize 처리용 thread를 생성하는데, 해당 thread가 finalize 작업을 진행하는 과정 중에 오류가 발생하거나 blocking이 발생 할 수 있다. 

 

resource의 회수 및 객체 소멸에 대한 처리를 하는데 위와 같은 오버헤드의 위험도를 매번 감수할 수는 없다.

때문에 공식적으로도 finalize를 사용하지 않는 방향으로 개발할 것을 추천해왔고, Java 9버전에 와서 deprecated 처리가 된 것이다. 

 

필요하다면?

resource의 회수와 소멸 기능의 구현이 필요할 때 추천되는 구현 방식은 try-catch-finally를 사용한 logic block을 통한 처리 방식이다. 

 

List interface 하위 Class

  1. Vector (deprecated)
    1. Stack
  2. ArrayList
  3. LinkedList

 

주의사항

Stack은 Vector를 상속받은 불필요한 로직만 추가된 클래스이고(책피셜)

Vector는 deprecated 된 삭제 예정 클래스이다.

실제 사용할 수 있는 클래스는 LinkedList와 ArrayList 두 개라고 생각하면 된다.

 

ArrayList

고정 크기의 Array를 기반으로 구현되는 List Class이다.

 

LinkedList

Doubly Linked List(양방향 연결 리스트)로 구현되는 동적 증가가 가능한 List Class이다.

 

ArrayList vs. LinkedList

데이터 접근/수정 패턴에 따라 사용해야 할 Class를 정해야 한다.

  • element add 비용
    • list end node에서의 add
      • ArrayList는 element의 수가 공간의 크기보다 커지면, 새로운 Array 생성을 위한 공간 할당 비용이 추가로 발생한다.
      • LinkedList는 end node에 new node의 reference를 추가하기 때문에, O(1)의 고정 비용만 발생한다.
    • 중간 index에서의 add
      • ArrayList는 해당 index 기준 우측의 element들을 모두 한 칸씩 이동시켜야 한다.
      • LinkedList는 삽입 지점으로의 node 탐색 비용 O(n)이 발생한다. 대신 양쪽 node의 reference를 해제한 후 new element에 재등록하는 O(2)의 비용이 발생한다.

* element 삭제에도 동일한 비용이 발생한다.

 

결론

List의 Random index 조회가 빈번할 경우, 조회 비용 O(1)을 보장하는 ArrayList가 유리하다.

순차 조회 혹은 List element 삭제/추가가 빈번할 경우 LinkedList가 유리하다.

 

확인 필요 사항

LinkedList의 순환을 하면서 element 조회를 할 때

LinkedList의 각 element 조회 비용 O(n)이 필요한지, 아니면 순환 동작을 통한 O(1)로 가능한지는 확인이 필요하다.

 

 

 

 

1. 문제 발생

- 신규 서버 프로젝트의 구조 및 개발 방향 구상 중, DB model object(이하 DTO)와 Service model object(VO)을 따로 구현 및 관리하기로 결정이 되었음

- 기존 운영 프로젝트에선 DTO의 구조가 그대로 Client까지 전달이 되었지만, 신규 구성에선 그런 동작을 방지하기로 결정 

- 하지만 클라이언트로 전달되지 말아야 하는 데이터 이외의 대부분의 값들은 Client에게 전달이 되어야 하는 상황

 

2. 방안 1

- DTO로 받은 각 값들을 일일이 소스 코드에서 변환 처리 해준다

- 개인적으로는 이게 취향

- 성능적으로도 솔직히 이것만큼 예측이 쉽고 문제 원인이 될 것도 없음

- 하지만 Human error의 위험 및 엄청난 귀차니즘, 장기간의 소스 코드 관리의 문제가 발생할 수 있음 

- 나중에 DB table 구조에 수정 및 추가가 생기면 다 수정해야 하는 시간 비용도 두려움

 

3. 방안 2 (진행)

- DTO와 VO간의 자동 변환 기능을 만들자

- 솔직히 아마 검색해보면 분명히 있을거지만, 요즘 개인 개발이 좀 느슨해진김에 해볼까 함

- 개념상으로만 알고 있던 Reflection 코딩 적용하면, 약간의 규칙만 정해주면 가능할 것 같았음

- 결과가 괜찮으면 검토 건의 해보지 뭐 ㅋ

 

4. 구성 및 기초 설계

4-1. 규칙 1

DTO와 VO간의 동일한 타입 및 이름이어야 한다.

"DB에서 int로 쓰고있지만 클라이언트는 String으로 받아서 쓸 것이다"라는 규칙으로 개발을 해봤지만, 좋은 꼴을 본 적이 없다.

4-2. 규칙 2

규칙 1을 기반으로, 각 객체의 field는 Java에서 제안하는 getter/setter 규격을 따라야 한다. 이 네이밍 규칙을 기준으로 데이터 복사 작업을 진행할 것이다. 또한 최근 자바 프로젝트는 다 이 규격을 사용하는 lombok 라이브러리를 통해 관리하기 때문에 이렇게 판단했다. 

 

5. 구현 

더보기
package reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

public class ConverterUtil{

    public static <T_DTO, T_VO> void convertObject(T_DTO objectDTO, T_VO objectVO) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 1. 공통 method 조회(Object Class method 제외 처리에 사용)
        HashSet<String> objectMethodSet = new HashSet<>();
        {
            Object testObj = new Object();
            Method[] objectMethods = testObj.getClass().getMethods();
            for(int i = 0 ; i < objectMethods.length ; i ++){
                objectMethodSet.add(objectMethods[i].getName());
            }
        }

        // 2. DTO getter method 정리
        HashSet<String> DTO_getterSet = new HashSet<>();
        {
            Method[] methods = objectDTO.getClass().getMethods();
            for(int i = 0 ; i < methods.length ; i ++){
                if(!objectMethodSet.contains(methods[i].getName())){
                    if(methods[i].getName().substring(0,3).equals("get")){
                        DTO_getterSet.add(methods[i].getName());
                    }
                }
            }
        }


        {
            // 3. VO setter method 
            HashMap<String, String> VO_getter_Map = new HashMap<>();
            HashMap<String, String> VO_setter_Map = new HashMap<>();

            Method[] methods = objectVO.getClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                if (!objectMethodSet.contains(methods[i].getName())) {
                    if (methods[i].getName().substring(0, 3).equals("get")) {
                        VO_getter_Map.put(methods[i].getName().substring(3, methods[i].getName().length()), methods[i].getName());
                    }
                    if (methods[i].getName().substring(0, 3).equals("set")) {
                        VO_setter_Map.put(methods[i].getName().substring(3, methods[i].getName().length()), methods[i].getName());
                    }
                }
            }

            // 4. 복사 작업 진행
            Iterator<String> getterKeyItr = DTO_getterSet.iterator();
            while (getterKeyItr.hasNext()) {

                String getKey = getterKeyItr.next();
                String fieldName = getKey.substring(3, getKey.length());
                if (VO_getter_Map.containsKey(fieldName)) {

                    Method testVOGetMethod = objectVO.getClass().getMethod(getKey);

                    Class dtoClass = objectDTO.getClass().getMethod(getKey).invoke(objectDTO).getClass();
                    Class objToPriClass = null;
                    if (dtoClass == Integer.class) {
                        objToPriClass = int.class;
                    } else if (dtoClass == Double.class) {
                        objToPriClass = double.class;
                    } else if (dtoClass == Float.class) {
                        objToPriClass = float.class;
                    } else if (dtoClass == Character.class) {
                        objToPriClass = char.class;
                    } else if (dtoClass == Long.class) {
                        objToPriClass = long.class;
                    } else if (dtoClass == Byte.class) {
                        objToPriClass = byte.class;
                    } else if (dtoClass == Boolean.class) {
                        objToPriClass = boolean.class;
                    } else if (dtoClass == Short.class) {
                        objToPriClass = short.class;
                    } else {
                        objToPriClass = objectDTO.getClass().getMethod(getKey).invoke(objectDTO).getClass();
                    }

                    Method testVOSetMethod = objectVO.getClass().getMethod(VO_setter_Map.get(fieldName), objToPriClass);

                    testVOSetMethod.setAccessible(true);

                    testVOSetMethod.invoke(
                            objectVO,
                            objectDTO.getClass().getMethod(getKey).invoke(objectDTO)
                    );

                }
            }
        }
    }
}

6. 테스트 진행

test.zip
0.01MB

프로젝트의 testMain.java 실행하면 된다. 

jdk 1.8 기준으로 작성했다.

그냥 객체 두 개 두고 복사되는지만 봤다. 안보는게 낫다 

 

7. 결론 및 후기 

어차피 공부 겸 재미용으로 만든 코드이기 때문에 테스트를 여러가지 케이스에서 막 굴리지는 않았다. 

무조건 이름이 같은 경우에만 복사하고 아님 말고의 방식으로 개발되어, 규칙을 지켜주며 객체 생성을 하지 않을거라면 못쓰는 코드에 가깝다. 

무엇보다 이거 괜찮은거 맞나? 싶다. 모든 서버 API 반환부에서 저 함수가 실행된다고 생각하면, 매번 method를 순회하고 타입 비교를 해가면서 변환 시켜준다는 것이 cpu 성능을 필요 이상으로 사용하는 건 아닌가 하는 걱정이 든다. 솔직히 개발 중간에 이럴거면 뭐하러 만드나 하는 생각까지 했지만, 그래 어차피 연습용이니까 하고 진행했다. 

까놓고 옛날에 xml 설정으로 각 객체간 mapping 등록을 해서 복사해주는 방식도 본적이 있긴한데 걔나 얘나 다 별로같다. 

 

* 귀찮아서 그냥 진행하긴 했는데 여기서 말하는 DTO가 Entity 객체고, VO가 DTO다. 시작할 때 네이밍을 잘못 정했다. 

* 빡치게 진짜 있네 

https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application

근데 이 라이브러리도 내부를 까본건 아니지만 parameter가 객체와 class type을 받아가는 것 보면, 결국 방향성은 내 코드와 비슷해보인다. 물론 예외처리나 최적화는 더 잘했겠지만!

HashMapIteration.java
0.00MB

필요한 기능 

1. Iterator : Iterator는 Java Collection Framework에 속하는 컬렉션 객체들(Map과 List interface를 상속받은 객체들)에 저장되어있는 value를 읽어오는 방법을 표준화하기위한 interface이다. 

2. entrySet & keySet : HashMap의 Loop를 실행하고 데이터를 사용하기 위해서는 HashMap이 가지는 Key를 알 필요가 있다. Key를 알아내기 위한 수단으로 사용할 method들이다.

 

비교할 대상

실행 시간을 비교할 조건은 Iterator의 사용 유무와 HashMap의 Key 조회 방법, 그리고 HashMap과 LinkedHashMap간의 차이를 비교할 것이다. 

그리고 하는 김에 ArrayList의 loop도 비교해볼 것이다. 

 

테스트 환경

jdk 1.8

 

테스트 방법 

간단하게 HashMap에 숫자를 0~1000000까지 넣고 반복문을 돌릴 것이다.

 

사용할 변수 선언 및 초기화 

1
2
3
4
5
6
7
8
9
10
11
12
13
    static HashMap<String,Integer> testMap = new HashMap<String,Integer>();
    static LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<String, Integer>();
    static ArrayList<Integer> arrayList = new ArrayList<Integer>();
 
    static
    {
        for(int i=0; i< 1000000; i++)
        {
            testMap.put("key_" + i, i);
            linkedHashMap.put("key_" + i, i);
            arrayList.add(i);
        }
    }
 
 

 

HashMap

[HashMap, entrySet]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
for (Map.Entry<String,Integer> entry : testMap.entrySet()) {
    entry.getKey();
    entry.getValue();
}
System.out.println("    1 Using entrySet() in for-each loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, keySet]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for (String key : testMap.keySet()) {
    testMap.get(key);
}
System.out.println("    2 Using keySet() in for-each loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, entrySet, Iterator]

1
2
3
4
5
6
7
8
9
10
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Map.Entry<String,Integer>> itr1 = testMap.entrySet().iterator();
while(itr1.hasNext())
{
    Map.Entry<String,Integer> entry = itr1.next();
    entry.getKey();
    entry.getValue();
}
System.out.println("HashMap iterator loop");
System.out.println("    3 Using entrySet() and iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, keySet, Iterator]

1
2
3
4
5
6
7
8
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<String> itr2 = testMap.keySet().iterator();
while(itr2.hasNext())
{
    String key = itr2.next();
    testMap.get(key);
}
System.out.println("    4 Using keySet() and iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

LinkedHashMap

[LinkedHashMap, entrySet]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
for (Map.Entry<String,Integer> entry : linkedHashMap.entrySet()) {
    entry.getKey();
    entry.getValue();
}
System.out.println("    1 LinkedHashMap entrySet() loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, keySet]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for (String key : linkedHashMap.keySet()) {
    linkedHashMap.get(key);
}
System.out.println("    2 LinkedHashMap keySet() loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, entrySet, Iterator]

1
2
3
4
5
6
7
8
9
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Map.Entry <String,Integer>> itr4 = linkedHashMap.entrySet().iterator();
while(itr4.hasNext())
{
    Map.Entry<String,Integer> entry = itr4.next();
    entry.getKey();
    entry.getValue();
}
System.out.println("    3 LinkedHashMap entrySet() iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, keySet, Iterator]

1
2
3
4
5
6
7
8
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<String> itr3 = linkedHashMap.keySet().iterator();
while(itr3.hasNext())
{
    String key = itr3.next();
    linkedHashMap.get(key);
}
System.out.println("    4 LinkedHashMap keySet() iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

ArrayList

[ArrayList]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for(int i = 0 ; i < arrayList.size() ; i ++) {
    arrayList.get(i);
}
System.out.println("    1 ArrayList loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[ArrayList, Iterator]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Integer> itr5 = arrayList.iterator();
while(itr5.hasNext()) {
    int val = itr5.next();
}
System.out.println("    2 ArrayList iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

실행 시간 결과 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HashMap just loop
    1 Using entrySet() in for-each loop : 65
    2 Using keySet() in for-each loop : 81
==================================
HashMap iterator loop
    3 Using entrySet() and iterator : 52
    4 Using keySet() and iterator : 60
==================================
LinkedHashMap just loop
    1 LinkedHashMap entrySet() loop : 45
    2 LinkedHashMap keySet() loop : 64
==================================
LinkedHashMap iterator loop
    3 LinkedHashMap entrySet() iterator : 43
    4 LinkedHashMap keySet() iterator : 56
==================================
    1 ArrayList loop : 22
    2 ArrayList iterator : 28
 

 

정리 

1. HashMap의 경우, entrySet이 keySet으로 순환하는 것 보다 더 빠르다. 그리고 Iterator로 순환하는 것이 아닌 것 보다 더 빠르다.

가장 빠른 것 : Iterator entrySet loop

 

2. LinkedHashMap의 경우, HashMap에 Link가 추가되어 있어 전체적으로 HashMap보다 실행 속도가 더 빠르다. 그리고 나머지 조건은 HashMap과 동일하게 entrySet이 keySet보다 더 빠른 것을 확인할 수 있다. 

가장 빠른 것 : Iterator entrySet loop

 

3. ArrayList의 경우, 이미 indexing이 되어 있기 때문에 별도의 Iterator 객체를 만들어 순환하는 것보다 그냥 size를 가지고 순환하는 것이 더 빠르다. 

가장 빠른 것 : non-Iterator loop

1. JUnit이란?

Java에서 사용하는 기능 단위별 테스트를 위한 라이브러리다.

* Java를 배우는 학부생들이라면 한번즘 접했을거라 생각된다.

 

2. 중요한가?

궁극적인 목표를 보고 생각한다면, JUnit과 유사한 기능들을 가진 툴/라이브러리들이 중요한 것은 아니다. 

중요한 것은 소프트웨어 개발 과정 중에 정리된 케이스로 테스트를 진행한다는 단계가 있다는 것이다.

문서로 잘 정리할 귀찮음을 감수할 자신이 있거나 혹은 자기 입맛에 맞는 테스팅 툴을 개발할 수 있다면 그걸로 상관없다.

* 자바로 개발하다가 파이썬으로 개발할 일이 생긴다면 어차피 JUnit은 쓸 수도 없다. 

 

3. 사용 방식 

테스트할 기능(method)를 대상으로 "예상되는 결과""method 호출 시 실제 반환 결과"를 비교하는 것이다. 

* 단위 테스팅 툴을 사용하지 않는 기업에서는 기능을 통째로 실행하거나 하위 기능을 복붙해서 실행하는 방식으로 많이 개발한다. 당장 내가 다녀본 회사나 주변 지인들이 있는 곳 대부분이 그렇게 하고 있다. (규모가 있거나 여유가 있는 곳이 아니면 대부분 이런 식으로 진행할 것이다.)

 

4. 환경 및 dependency

Spring boot + gradle

org.springframework.boot:spring-boot-starter-test

내가 주로 개발하는 환경이 Spring이어서 이렇게 설정한 것이지 JUnit만 사용할 거라면 JUnit라이브러리만 import하면 된다.

 

5. Spring boot 구성도

Spring boot 구성

Spring 환경에서 개발을 하다보면 src하위에 자동으로 생성되는 test 폴더를 본 적이 있을 것이다. 

개발된 기능의 test용 코드를 관리하라고 주는 별도의 경로이니, Spring에서 진행중이라면 활용하자.

 

테스트의 대상은 servicelogic/tdd/Service/impl에 위치한 TddServiceImpl이고, 

테스트 실행은 test폴더 하위에 있는 servicelogic/tdd/controller에 위치한 TddControllerTest이다. 

실행 방법은 TddControllerTest에서 TddServiceImpl의 method를 호출하여 TddControllerTest에 설정된 예상된 결과와 비교하는 것이다. 

 

6. 소스 코드 

A. TddServiceImpl : 서비스 로직이 위치할 코드

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class TddServiceImpl implements TddService {
 
    @Override
    public List selectTest() throws Exception {
 
        System.out.println("TddServiceImpl > selectTest");
 
        List<Integer> temp = new ArrayList<Integer>();
        temp.add(1);
        temp.add(2);
        temp.add(3);
        temp.add(4);
        temp.add(5);
 
        return temp;
    }
}
 
 
 

테스트 용 결과값으로 [1,2,3,4,5] 가 저장된 List를 반환해준다. 

 

B. TddControllerTest : 서비스 로직을 테스트할 코드 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class TddControllerTest {
 
    @Autowired
    TddService tddService;
 
    @Test
    public void test_Call() throws Exception {
        List testResult = new ArrayList();
        testResult.add(1);
        testResult.add(2);
        testResult.add(3);
        testResult.add(4);
        testResult.add(5);
        assertEquals(testResult, tddService.selectTest());
    }
    
}
 
 

TddServiceImpl의 반환값과 비교할 값을 설정한 후, 대상 method를 호출하여 비교한다. 

 

17번째 줄의 assertEquals 함수가 JUnit에서 제공되는 비교 함수이다. 

만약 반환값의 특정 키값만 확인하고 싶을 때와 같이 값 이외의 다른 것을 비교하고 싶다면, 테스팅 코드 내에 원하는 조건을 추가해 확인할 수 있다.

또한 같은 코드에 여러 테스팅 호출 함수를 작성하더라도, 원하는 method만을 별도로 실행할 수 도 있다. (이게 단위 테스트 강점이 아닐까)

 

7. 정리

서버 API를 개발할 때, 하나의 API를 대상으로 개발할수록 점점 기능이 쌓여간다.

API내에 백엔드에서 사용할 데이터가 발생하고 저장되며 별도의 처리를 하게 된다. 

이렇게 쌓여갈 때 마다 테스트하기 점점 힘들어지고 소스코드 몇 줄 추가하여 거기만 테스트 하려고 하면 그 전에 구현된 전체 로직을 계속 반복 실행하며 쌓여가는 로그를 쳐다보고 있어야 한다. 

학부생 때 처음이자 마지막으로 써봤던 JUnit을 이제와서 찾게 된 것은 사실 실무에서 이러고 있는게 너무 귀찮고 짜증나서이다. 

가끔 API 호출 조건 자체가 까다로울 때가 있는데, 그럴 땐 진짜 환장한다. (호출 시 파라미터가 암호화가 되어있다던가)

 

Agile 개발론 중에는 TDD(Test Driven Development - 테스트 주도적 개발)라는 것이 있다. (사실 아는게 이거밖에 없다.)

TDD를 지향하기 위해서 테스트 케이스를 정형화하고 테스트 실행에 쉽게 접근할 수 있어야 하는 법인데, 그 대표적인 방식이 바로 단위 테스트이다. 

 

학생땐 이걸 왜 배우나 했지... 어허허 

학생 때 배우는 거 쓸모 없다는 사람 많지만 절대 그렇지 않으니까 공부 열심히 하자. 

Map을 상속받는 객체는


(1) HashTable :: (class)
(2) HashMap :: (class)
(3) LinkedHashMap :: (class)
(4) SortedMap :: (interface)
과 SortedMap을 상속받는
(5) TreeMap :: (class)
까지 포함하여 4개의 class와 1개의 interface가 있다.

모두 Map 인터페이스를 따라 
(1 Key, 1 Value) 구조로 입력/조회를 하게 된다.

각각의 객체를 순서대로 보자.

 

HashTable


- 구버전이다! (다른 애들과 달리 Java1부터 있었다.)
- HashMap과 동일한 Hash구조를 가지고 있다.
- Key와 Value에 null 값을 저장할 수 없다.
- 멀티 스레딩 개발을 할 때, Thread-safe 기능을 지원한다.

 

HashMap

 

- HashTable과 동일한 Hash구조를 가지고 있다.(ㅋ)
- Key와 Value에 null 값을 저장할 수 있다.
- 자체적으로 Thread-safe 기능을 지원하지는 않는다. (Collection 객체의 Sync 기능을 사용하여 Thread-safe하게 개발할 수 있다.)
- 저장 순서가 보장되지 않는다.(Key를 Set으로 저장하기 때문이다.)

 

LinkedHashMap

 

- HashMap과 거의 동일하다. (HashMap의 기능을 추가/변경한 확장된 객체이기 때문에 이렇게 되었다.)
- HashMap에서 Key를 Doubled Linked List(쌍방향 연결 리스트)방식으로 Key Value가 입력되는 순으로 저장하기 때문이다. (이름앞에 Linked가 붙은 이유다.)

* HashTable/HashMap/LinkedHashMap 은 몇 가지를 제외하곤 동일 인터페이스와 동일 구조를 갖기 때문에, 특별한 상황이 아니라면(Thread-safe 개발) 최신 객체인 HashMap을 추천하는 편이다.
* Hash 구조로 '선형구조'의 특징을 따라간다.(조회 시 발생하는 BigO 비용은 O(n)이다.

SortedMap

 

- 얘는 객체가 아니다. 인터페이스다(!)
- 입력받는 Key를 오름차순으로 저장하도록 형상화 되어있다.
- 왜 있을까?

 

TreeMap -> 얘 때문이다!

 

- Map이 아닌, SortedMap을 상속받는다.
- 위에서 설명한 다른 객체들과 다르게, Tree의 구조로 데이터를 관리한다.
- Tree 구조를 만들기 위해 정렬된 데이터를 필요로 하게 되었기 때문에, 데이터 정렬을 고려하지 않은 Map을 사용할 수 없기에 SortedMap 인터페이스가 만들어지고 상속받게 된 것 같다.(뇌피셜이다. 거기까지 자세하게 알고 싶지 않다.)
- Tree 구조 특성 상, 자료 조회 시의 비용은 O(n*logn)이다. 같은 이유로 데이터 입력 속도는 선형 구조인 HashMap보다 느리다.

*실무에선 사실 상 HashMap만 쓴다고 생각해도 된다.(물론 성능을 따지는 개발 또는 빡빡하게 개발하는 곳은 아닐 수 있다. 근데 그럴거면 JAVA가 아닌 성능을 우선시하는 언어를 사용할 확률이 더 높다.)


*Map객체로 데이터를 받는 경우는 대부분 정렬을 필요로 하지 않고, 대용량 데이터를 넣는 경우가 없기 때문에 자연스럽게 익숙한 HashMap을 찾게 된다.


*하지만 DB저장을 하지 않는 상황에서 대량의 데이터를 갱신 없이(주의) 유지할 일이 있다면, TreeMap을 고려해볼만은 하다.


*보통 On Memory DB를 사용하고 만다. (ex:Memcached, Redis)
-> On Memory DB는 자바 객체를 그대로 저장할 수 있기 때문에, 갱신 주기가 적은 데이터라면 TreeMap객체로 저장하면 효과를 볼 수 있을지도 모른다.



 

 

 

 

실무에서 가장 많이 사용하는 객체

단언컨데 HashMap일 것이다.
- 서비스 개발을 할 때 HashMap만 알아도 큰 걱정이 없는 것은 사실이다. 하지만 개발을 하면서 구글링을 하거나 레거시 코드(Legacy code)를 보다보면 HashTable로 대체되어 사용되는 경우를 많이 볼 수 있다. 정작 비교해보면 기능의 차이도 크게 나지 않는데 굳이 대중적인 HashMap이 아닌 HashTable을 사용해야 했을까 의문이 든다. 왜 이렇게 된 것일까?

HashMap은 언제부터?

HashMap이 포함된 Map 인터페이스는 1998년 JAVA2에서부터 지원되었고, JAVA5 이후부터 인터페이스의 변경 없이 내부 성능 향상만 있었다(JAVA8에서부터 지원된 RXJava와 Lamda 문법은 제외하고). 실무 환경도 대부분 JAVA7 이상의 버전을 사용하는 지금 시점에서는 신경 쓸 필요가 없는 사항이다.

JAVA2의 얘기는 왜 꺼내는가?

HashTable과 HashMap의 기능의 차이점Thread Safe 코딩(멀티 스레드 개발 시 데이터 일관성을 유지하는 개발) 정도인데 굳이 따로 구현된 것인지 의문을 생긴다.

 

동일한 Hash구조와 Map 인터페이스를 가지고 있는 두 객체는 같이 구현되는 것이 옳았을 것 같다. 그렇다면 '객체 단위 개발을 통한 중복 코드 제거'를 무시하고 두 번 개발된 것은 왜일까?

 

HashTable은 Map Interface가 포함된 'Java Collection Framework'가 확립되기 전, JAVA1 때 구현된 객체이기 때문이다. Java2에서 Java Collection Framework가 개발되면서 HashMap이 생겼고, Java1을 기반으로 개발된 소프트웨어들의 호환성을 위해 삭제하지 않고 남은 것으로 보인다. 

* 덕분에 HashTable은 Map 인터페이스를 상속받으면서 자바 객체 네이밍 방식인 '[자료구조][인터페이스]' (ex : Hash/Map, Tree/Map, Hash/Set) 따라 Map , Table .

*약간의 기능 차이는 있지만 HashTable과 HashMap의 모든 기능이 동일하다고 봐도 무방하다(차이나는 기능들도 다른 방식으로 동일하게 구현이 가능하다).

* HashTable을 추천하지 않거나, 실무에서 쓰지않는 것이 좋다라는 글들이 있는 것은 이러한 이유 때문이다. (추후 지원이 안되고 대체 가능한 최신 기능이 있는데 쓰기 싫은 것)

> 알고 있는 정보 및 자료를 찾아서 이해를 한 선에서 간단하게 추리긴 했지만, 더 자세한 정보가 많이 공개되어 있다.
네이버 기술 블로그 >> https://d2.naver.com/helloworld/831311

좀 많이 이론/코어로 들어가 설명이 되어있긴 하지만 이해할 시간이 있다면 언어 상관 없이 얻을 것이 많은 글이다.



 



 



 

+ Recent posts