- 신규 서버 프로젝트의 구조 및 개발 방향 구상 중, 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 라이브러리를 통해 관리하기 때문에 이렇게 판단했다.
어차피 공부 겸 재미용으로 만든 코드이기 때문에 테스트를 여러가지 케이스에서 막 굴리지는 않았다.
무조건 이름이 같은 경우에만 복사하고 아님 말고의 방식으로 개발되어, 규칙을 지켜주며 객체 생성을 하지 않을거라면 못쓰는 코드에 가깝다.
무엇보다 이거 괜찮은거 맞나? 싶다. 모든 서버 API 반환부에서 저 함수가 실행된다고 생각하면, 매번 method를 순회하고 타입 비교를 해가면서 변환 시켜준다는 것이 cpu 성능을 필요 이상으로 사용하는 건 아닌가 하는 걱정이 든다. 솔직히 개발 중간에 이럴거면 뭐하러 만드나 하는 생각까지 했지만, 그래 어차피 연습용이니까 하고 진행했다.
까놓고 옛날에 xml 설정으로 각 객체간 mapping 등록을 해서 복사해주는 방식도 본적이 있긴한데 걔나 얘나 다 별로같다.
* 귀찮아서 그냥 진행하긴 했는데 여기서 말하는 DTO가 Entity 객체고, VO가 DTO다. 시작할 때 네이밍을 잘못 정했다.
궁극적인 목표를 보고 생각한다면, 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에 설정된 예상된 결과와 비교하는 것이다.
(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
좀 많이 이론/코어로 들어가 설명이 되어있긴 하지만 이해할 시간이 있다면 언어 상관 없이 얻을 것이 많은 글이다.