코드스쿼드 미션으로 게시판을 구현해보는데 이전에 혼자 게시판을 만들어 볼 때는 구현하기 급급했다면 이번에는 왜 이걸 썼는지 좀 생각하면서 구현을 해보려고 한다.

 

먼저 DB 연동 없이 자바 코드로 Repository를 구현하면서 동시성 처리를 위해 ConcurrentHashMap을 사용했는데 대충 알고 쓰는 것 같아서 정리를 하기 위해 HashMap의 구조부터 학습을 했다.

 

해싱(Hashing)이란 해시 함수를 이용해서 데이터를 해시 테이블에 저장하고 검색하는 방법으로 해시 함수는 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 단방향 알고리즘 함수이다.

 

HashMap은 해싱을 구현한 컬렉션 자료구조로 키에 대한 해시 값을 사용하여 값을 저장하는 Associate array(key-value의 형태로 키를 통해 연관되는 값을 얻을 수 있는 자료 구조)이다.

 

HashMap은 배열 + 링크드 리스트의 조합으로 되어 있는데 링크드 리스트는 데이터의 삽입, 삭제와 같은 동작에는 효율적이지만 검색 속도는 비효율적이다. 하지만 HashMap은 Node를 배열(버킷)로 가지고 있는 형태이기 때문에 조회 속도가 O(1)로 빠른 편이다.

 

 

그런데 왜 HashMap은 왜 배열 + 링크드 리스트 구조로 되어 있을까? 자바의 HashMap은 객체의 hashCode()를 해시 함수로 사용하는데 Object 클래스의 hashCode()는 객체의 주소를 이용하는 알고리즘으로 해시 코드를 만들어 내며 int 값을 반환한다. 하지만 HashMap 배열(버킷)의 용량이 값을 모두 수용할 수 없으며 메모리 효율 문제도 있기 때문에 해시 코드 값에 나머지 연산을 활용하여 버킷의 index를 구한다. (균등하게 index를 분포하기 위해 보조 해시 함수도 사용한다고 한다.)  그렇게 되면 중복된 key 값을 가지는 경우가 생기는데 이를 해시 충돌이라 한다.

 

해시 충돌이 발생할 경우를 대비해여 Open Addressing, Separate Chaining 이 있다.

 

HashMap은 Separate Chaining 방식을 사용하는데 같은 Key를 가지는 경우 equals()를 비교해서 같으면 동일 객체로 판단하여 교체를 하고 다르면 Chaining으로 연결을 한다.

 

 

Java8 부터는 데이터가 많아질 경우 개수에 따라 Tree로 변환한다.

 

 

 

HashMap의 버킷 용량이 동적으로 확장될 때마다 모든 key-value를 읽어서 Separate Chaining을 재구성해야 하기 때문에 상황에 따라 생성자로 initialCapacity, loadFactor를 정의해주는 것도 좋을 것 같다.

 

 

[참고]

 

 

[자료구조] 코드로 알아보는 java의 Hashmap

HashMap이란 HashMap은 Key, Value를 저장하는 Map의 구현체 중 하나입니다. 자료구조에 Key를 넣으면 Value를 반환하도록 합니다. 그리고 HashMap은 Key를 Hashing을 하여 저장하여 빠르게 처리 그리하여 HashMap

sabarada.tistory.com

https://d2.naver.com/helloworld/831311

 

 

코드스쿼드 미션을 진행하던 중 사용자로부터 콘솔 입력을 받아서 값을 반환해주는 메서드를 테스트 하는 방법을 찾아보니 콘솔 입력을 값으로 넣어줄 수 있는 방법이 있었다.

 

내가 테스트한 메서드는 inputLadderHeight()로 필드 변수인 scanner를 통해 입력을 받고 있다. (원래는 scanner를 static final로 선언하고 메서드도 static으로 작성했는데 변경된 상태이다.) 

 

메서드를 보면 내부에서 scanner.nextLine()으로 입력을 읽는데 Scanner를 생성할때 java.lang 패키지의 System.in을 파라미터로 전달을 한다. 설명을 보면 이 표준 InputStream은 이미 열려서 데이터를 받을 준비가 되어있고, user가 설정할 수도 있다고 나와 있다.

 

스레드 초기 설정 후에 initPhase1() 메서드에서 System class를 초기화 하는데 처음에 Systen.in이 java.io 패키지의 BufferedInputStream으로 초기화가 되는 것을 확인할 수 있다.

 

 

다시 맨 위 코드를 보면 Screen 클래스가 생성될때 Scanner도 초기화가 되는데 이때 System의 static final 변수인 in이 전달이 된다.

그래서 Screen을 생성하기 전에 System.setIn()으로 테스트용 InputStream을 set 해주면 내가 임의로 만든 테스트 입력 값을 Scanner가 읽도록 할 수 있다.

 

 

테스트 코드를 보면 InputStream을 만들어서 Scanner가 초기화 되기 전에 System.in을 교체해주면 된다.

 

 

처음에 Scanner를 static final로 선언했더니 여러번 반복 테스트를 할 때 계속 의도한 대로 동작하지가 않았다.

생각을 해보니 여러번 테스트를 할 때 InputStream을 새로 생성해서 setIn()을 해줘도 처음 Screen 클래스가 로드되면서 Scanner에 주입된 InputStream이 그대로 있어서 입력이 들어가지 않았던 것이다.

 

역시 static은 되도록 사용하지 않는 것이 좋다고 느꼈다..

 

 

 

[참고]

https://sakjung.tistory.com/33

https://www.geeksforgeeks.org/java-lang-system-class-java/

https://mommoo.tistory.com/71

static import를 사용하면 static 메서드를 사용할 때나 이너 클래스를 사용할 때 클래스명을 생략할 수 있다. 잘 활용하면 테스트 코드를 작성할때 가독성을 올려준다거나 하는 이점도 있지만 패키지 구조를 다 아는 내 눈에만 예뻐 보일 수 있겠다는 생각이 들었다.

 

오히려 가독성을 떨어트리고 헷갈리게 할 수 있기 때문에 전역적으로 사용되고 static import를 해도 이해하기 쉬운 부분에 사용하는 것이 좋을 것 같다.

 

 

언제 사용하면 좋은지 해당 글에서 보았다.

 

Static Import

In order to access static members, it is necessary to qualify references with the class they came from. For example, one must say: double r = Math.cos(Math.PI * theta); In order to get around this, people sometimes put static members into an interface and

docs.oracle.com

 

when should you use static import? Very sparingly! 

use it when you require frequent access to static members from one or two classes.

 

아주 드물게 사용해야 한다고 강조하고 있다!

 

If you overuse the static import feature, it can make your program unreadable and unmaintainable, polluting its namespace with all the static members you import. Readers of your code (including you, a few months after you wrote it) will not know which class a static member comes from. Importing all of the static members from a class can be particularly harmful to readability

대충 남용하면 가독성 떨어지고.. 유지 보수 어렵고.. 네임스페이스 오염시키고.. 코드 읽는 사람이 헷갈리고.. 등등 엄청 안좋다는 말

 

if you need only one or two members, import them individually. Used appropriately, static import can make your program more readable, by removing the boilerplate of repetition of class names.

 

이전에 혼자 코드 짤 때 static import 하는게 깔끔한거 같아서 막 다 static import를 했었는데 다른 분이 짠 코드를 보다보니 static import가 안 되어있는 것이 보기에 이해가 더 잘 됐다. 그래서 이거 사용하는거 맞나 의문이 들었는데 찾아보길 잘한 것 같다.

 

 

 

 

[참고]

 

[Java] Static import에 대한 관찰

JDK5에서 Static import가 추가되었다. 먼저 static import를 적용하지 않은 일반 코드를 보자. 가장 기본적인 용법은 import문 뒤에 static을 붙이고, {패키지.클래스.\*} 혹은 {패키지.클래스.멤버} 를 적으면

velog.io

 

제네릭메서드나 클래스에 컴파일 시 타입 체크를 해주는 기능으로 컴파일 단계에서 타입 체크를 하기 때문에 객체의 타입 안정성을 높이고 형변환의 번거로움을 줄여준다.

 

타입 안정성을 높인다는 것은 의도치 않은 타입의 객체가 저장되는 것을 막고, 형변환의 오류를 줄여주는 것이다.

 

 

class Box <T> {
    T item;
    
    void setItem(T item) {
        this.item = item;
    }
    
    T getItem() {
        return item;
    }
}

T 를 타입 변수(타입 매개변수)라고 하는데 여기서 T는 'Type'의 T를 뜻하며, 요소의 경우 'Element'의 첫 글자인 'E'로 표현할 수도 있다. 좌표를 보통 x,y로 나타내는 것처럼 꼭 T, E, K 이런게 정해져 있는것은 아니지만 코드를 이해하는데 있어 맞추는 것이 중요하다.

기호의 종류만 다를 뿐 임의의 참조형 타입을 의미한다는 것은 같다.

 

제네릭에서 타입 변수를 사용하지 않는 경우에도 문제가 생기진 않는데 이는 호환성으로 인해 가능하지만 주의하는 것이 좋다고 한다.

 

 

[Java] 제네릭에서 Raw Type 선언 주의 (이펙티브 자바)

'Raw use of parameterized class 'List' 라는 경고를 보고 Raw use가 무엇인지 찾아봤는데 이펙티브 자바에 있는 내용이라 아직 초반부를 보고 있지만 해당 부분을 읽어보았다. 제네릭 타입은 일련의 매개변

treecode.tistory.com

 

Box <String>으로 선언을 하면 타입 매개변수에 String이 입력이 돼서 컴파일 후에 원시 타입인 Box로 바뀌게 되고 제네릭 타입은 제거되며 타입 매개변수의 'T'가 String으로 치환이 되고 적절하게 형변환이 된다. 

 

class FrutBox<T extends Fruit> {
    ArrayList<T> list = new ArrayList<T>();
    ...
}

 

 그냥 T 만 사용하면 모든 타입이 들어올 수 있는데 위와 같이 제네릭 타입에 extends를 사용하면 특정 타입의 자손들만 대입할 수 있도록 제한할 수 있다. (인터페이스의 경우에도 extends)

 

 

제네릭과 와일드 카드 <?>

 

제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거하기 때문에 제네릭 타입을 다르게 하는 것으로는 오버로딩이 성립하지 않는다. 이럴 때는 와일드 카드 '?'를 사용하면 되는데 다음과 같이 사용한다.

 

1) <? extends T> 상한 제한, T와 그 자손들만 가능

2) <? super T> 하한 제한, T와 그 조상들만 가능

3) <?> 모든 타입 가능

 

제네릭 타입을 클래스 말고 메서드에 선언한 것을 제네릭 메서드라고 하는데 이번에 글로 정리하게 된 이유이다.

 

 

제네릭 메서드

 

제네릭 메서드는 말 그대로 메서드에 제네릭을 선언하는 것으로 클래스에 정의된 타입 매개변수와 제네릭 메서드에 정의된 타입 매개변수는 서로 별개의 것인데 제네릭 메서드는 제네릭 클래스가 아니더라도 정의할 수 있다.

 

제네릭 메서드에 선언된 타입 매개변수는 해당 메서드 내에서만 지역적으로 사용이 되기 때문에 지역 변수를 선언한 것과 같다고 생각해도 된다. (타입 매개변수는 인스턴스 변수로 간주되기 때문에 원래 static 멤버에는 사용할 수 없지만, 메서드에 제네릭 타입을 선언하고 사용하는 것은 가능하다.)

(-> 그렇구나.. 하긴 했지만 아직 정확히 이해는 안됨)

 

static <T extends Fruit> Juice makeJuice(FruitBox<T> box)

 

제네릭 메서드를 호출할 때는 메서드 앞에 타입을 대입해야 하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략해도 된다.

 

Juicer.<Fruit>makeJuice(fruitBox);

 

책에 밑줄이 그어져 있는 것으로 보아 이전에도 보긴 했었는데 확실히 보기만 하고 실제로 사용을 안하니 금방 까먹고 이해도 잘 안되는 것 같다.  공부하고 있는 프로젝트의 코드를 다시 보았다.

 

 

해당 메서드는 Api 응답을 만드는 메서드로 매개변수에 어떤 클래스가 들어올 지 몰라서 매개변수 타입을 제네릭 타입 변수 'T'로 선언을 해야 한다. 원래 static 메서드에는 타입 변수 'T' 를 선언할 수 없지만 제네릭 메서드로 선언을 하면 매개변수에 타입 변수를 사용할 수 있다.

 

처음에 T가 3개씩 보여서 제네릭 모른다고 당황했었는데 이렇게 다시 보니 이해가 좀 된다.

 

 

문제의 코드인데 ApiResult를 builder로 생성해서 반환하니 incompatible types 에러가 떠서 어리둥절 했었다. 

 

롬복의 builder()도 제네릭 메서드로 선언이 되어 있는데 제네릭 메서드를 호출할 때는 원래 메서드 앞에 타입 변수를 대입해야하지만 대부분 컴파일러가 타입을 추정할 수 있기 때문에 생략을 하고 사용한다. 하지만 아래와 같이 제네릭 클래스에서 builder()를 사용하는 경우 타입 변수를 생략하면 Object로 반환이 되어서 명시적으로 타입 선언을 해줘야 한다.

 

 

 

 

[참고]

자바의 정석

 

자바 제네릭(Generics) 기초

tecoble.techcourse.co.kr

 

+ Recent posts