자바로 WAS를 구현해 보았는데 사실 이번 미션은 HTTP와 WAS에 대해 공부하는 것인데 스프링 MVC 구조에 집중해서 조금 아쉬웠다. 

 

프록시 패턴에 대해 간단 정리

더보기

 처음에는 컨트롤러에서 여러 메서드를 핸들링하려고 하다 보니 컨트롤러에 요청을 받는 public 메서드와 각각의 처리(회원가입, 유저 조회 등)를 하는 private 메서드를 만들어야 했다. 그래서 내 맘대로 프록시라는 클래스를 만들고 Enum으로 매핑을 해주었다.

이렇게 컨트롤러에 메서드를 추가할때마다 프록시와 Enum 클래스를 수정하는 이상한 코드가 완성이 되었다. 내가 원하는 것은 컨트롤러에 메서드만 추가하면 동작이 되는 것이었기 때문에 결국 김영한님의 스프링 고급편 강의로 어설프게 들어본 프록시 패턴과 리플렉션에 대해 공부를 해보았다.

 

프록시 패턴은 접근 제어, 캐싱, 부가 기능(데코레이트) 등의 목적으로 사용이 된다. GOF 디자인 패턴에서는 의도에 따라 프록시 패턴과 데코레이터 패턴으로 나누는데 프록시 패턴은 접근 제어가 목적이고, 데코레이터 패턴은 새로운 기능 추가가 목적이다.

이전에 Redis로 캐싱을 할 때 @Cacheable로 간단하게 캐싱을 했었는데 스프링부트는 애노테이션으로 프록시 AOP 기술을 쉽게 사용할 수 있게 해 준다. 아래 예시는 공식 문서에서 @Transactional을 예시로 들었는데 @Transactional 애노테이션을 사용하면 프록시가 트랜잭션 관련 처리를 해준다. 프록시와 실제 비즈니스 로직이 있는 AccountServiceImpl이 같은 AccountService를 구현하고 있는 것을 볼 수 있다.

 

프록시와 대상은 다형성으로 호출하는 쪽에서는 프록시를 호출한 것인지 실제 대상을 호출한 것인지 몰라야 된다. 

 

인터페이스 대신 부모 상속으로도 가능하지만 결국 프록시 클래스를 계속 직접 만들어야 되는 것은 변하지 않는다. JDK 동적 프록시는 InvocationHandler를 통해 동적으로 프록시를 생성할 수 있지만 인터페이스가 필요하다는 조건이 있고 CGLIB를 사용하면 인터페이스가 없이도 동적 프록시를 생성할 수 있다고 한다.

 

결국 디자인 패턴은 의도가 중요하다고 하는데 아직 디자인 패턴을 깊게 볼 때는 아닌것 같고 현재 나는 요청을 전달하고 싶은 것뿐이라서 @RequestMapping 애노테이션을 만들고 리플렉션으로 요청에 맞는 컨트롤러와 메서드를 찾아서 실행(invoke)해주는 방향으로 전환했다.

 

 

전체적인 흐름은 아래와 같은데 템플릿 엔진은 구현하지 못했다.

 

 

먼저 동시에 여러 요청을 처리하기 위해 ThreadPool을 생성하고 serverSocket에서 accept()를 한다.

accept() 호출 시 메인 스레드는 block 상태가 되고 연결이 되면 생성한 소켓(connect)을 반환해서 스레드풀로 비동기 처리를 한다. 

 

 

정리하면서 알았는데 executorService.shutdown()이 호출되면 새로운 작업은 실행하지 않고 현재 실행중인 작업만 처리한다.

하지만 실행중인 작업이 종료가 될 때까지 blocking 하지 않기 때문에 실행 중인 작업까지 처리하기 위해서는 awaitTermination(), shutdownNow()를 활용해서 종료해야 한다.

 

 

HttpHandler는 Http 요청, 응답을 처리하는 핸들러로 request를 파싱해서 handle()로 보낸 뒤 response가 담긴 결과를 sendResponse()로 보낸다.

 

handle()은 RequestUrl의 확장자를 통해 정적 리소스 요청인지, 동적 리소스 요청인지 체크한다. 동적 요청의 경우 싱글톤으로 생성한 DispatcherServlet을 가져와서 doDispatch()를 실행한다. HttpHandler는 요청당 생성이 되고 DispatcherServlet을 관리하는 별도의 컨테이너가 없어서 싱글톤으로 생성을 했다.

 

doDispatch()는 ControllerAdapter에서 handle()을 통해 컨트롤러에게 요청을 전달해서 처리하고 ModelAndView를 반환받는다.

(예외처리나 리다이렉트 처리 부분은 필터를 적용하면 좋을 것 같은데 구현을 못했다.)

 

ControllerAdapter는 Controller 클래스들의 @RequestMapping 정보를 읽어서 요청을 처리할 수 있는 Controller를 찾는 역할을 한다. findAllMappingUrl()은 컨트롤러의 @RequestMapping 매핑 정보를 초기화하기 위해 사용한다

 

다시 ControllerAdapter의 handle() 메서드를 보면

 

1. findController: 요청을 처리할 수 있는 컨트롤러를 찾고

2. findControllerMethod: 해당 컨트롤러의 메서드를 찾는다.

3. ArgumentResolver를 통해 메서드를 실행하기 위해 필요한 파라미터를 resolve 한다.

4. 메서드 실행에 필요한 arguments를 만들어서 메서드를 실행(invoke)한다.

5. 메서드의 반환값으로 ReturnValueHandler를 통해 ModelAndView를 생성한다. (ViewName, Json Convert 등 처리)

 

MethodArgumentResolver는 인터페이스로, Argument 타입에 따라 나뉘는데 ObjectArgumentResolver는 ObjectBinder를 사용해서 QueryParameter를 객체로 바인딩해 준다.

ObjectBinder는 기본 생성자와 setter를 필요로 한다. setter를 호출하기 위해 setName, setAge와 같은 setter 메서드명을 직접 만들어야 하는데 컴파일 에러가 뜨는 것도 아니라서 위험하다.

MessageBody 처리는 MessageConverter를 사용하는데 원래는 ObjectBinder라는 클래스가 없었지만 여기서도 ObjectBinder 기능이 필요하기 때문에 따로 클래스로 만들게 되었다. (form-data만 파싱만 구현했다.)

 

다시 DispatcherServlet의 doDispatch()로 돌아와서 ViewResolver를 통해 View를 찾고 렌더링을 하면 HttpHandler에서 Response 응답을 스트림으로 보낸다. 맨처음 말한것처럼 HTTP 헤더를 제대로 처리하지 못해서 아쉽다.

 

결과적으로 Controller에서는 어느 정도 의도한 대로 동작을 할 수 있게 되었다. @Valid로 컨트롤러 파라미터의 객체 생성 시 validation을 하는 경우에도 ArgumentResolver가 동작해서 BindingResult에 담아주는데 간단하게나마 구현을 해보니 어떤 식으로 동작이 되는지 이해할 수 있었다. 여전히 새로운 컨트롤러를 생성하는 로직을 추가해야 되는데 이 부분은 DI 컨테이너를 구현해야 될 것 같다.

 

 

 

[참고]

김영한님 스프링 고급편

 

[Spring] @Valid와 @Validated를 이용한 유효성 검증의 동작 원리 및 사용법 예시 - (1/2)

Spring으로 개발을 하다 보면 DTO 또는 객체를 검증해야 하는 경우가 있습니다. 이를 별도의 검증 클래스로 만들어 사용할 수 있지만 간단한 검증의 경우에는 JSR 표준을 이용해 간결하게 처리할 수

mangkyu.tistory.com

 

Transactions, Caching and AOP: understanding proxy usage in Spring

In the Spring framework, many technical features rely on proxy usage. We are going to go in depth on this topic using three examples: Transactions, Caching and Java Configuration. All the code samples shown in this blog entry are available on my github acc

spring.io

 

[Java] ExecutorService.shutdown(Now) & awaitTermination

헷갈리는 ExecutorService 의 종료와 관련된 메소드를 테스트해봤다.

velog.io

 

 

이번에 코드스쿼드 미션으로 웹서버를 구현해 보면서 공부한 내용을 정리해 보았다.

 

[기본으로 주어진 예제 코드]

try (ServerSocket listenSocket = new ServerSocket(port)) {
    Socket connection;
    
    while ((connection = listenSocket.accept()) != null) {
        Thread thread = new Thread(new RequestHandler(connection));
        thread.start();
    }
}

 

1. 소켓(Socket)과 포트(Port)


유튜브 널널한 개발자님

위 그림에서 TCP와 Process 사이 노란 원이 소켓을 표현한 건데 소켓은 유저 모드 애플리케이션에서 커널 영역에 접근하기 위한 통로 역할을 한다.

소켓의 본질은 파일(File)이라고 하는데 여기서 파일은 운영체제 커널 영역에 구현되어 있는 요소들에 대한 추상화된 인터페이스로 이때 프로토콜 요소(여기서는 TCP/IP 프로토콜)에 대해서 추상화를 한 경우 소켓이라고 부른다.

OS에서는 읽고 쓰는 것을 파일로 추상화해서 범용적인 의미로 사용하기 때문에 일반적으로 아는 파일, 디렉토리, 심볼릭 링크(바로가기) 소켓 등 많이 것들이 파일에 포함된다. 처음에는 소켓이 왜 파일인지 의아했는데 결국 소켓도 스트림으로 읽고 쓰는 것이라고 생각하니 조금 이해가 되었다.

 

김영한님 HTTP 강의

소켓의 스트림을 통해 데이터 <-> TCP(세그먼트) <-> IP(패킷) <-> Frame으로 전달이 되는데 여기서 TCP의 주요 특징인 포트가 나온다. 기존에 IP 패킷만 전달할 때는 신뢰성, 순서 보장 등의 문제도 있었지만 한 IP 내에서 여러 애플리케이션이 실행중일 때 어디로 전달이 되어야 하는지를 모르는 문제가 있었다.

 

 

그래서 프로세스를 식별하기 위한 포트 정보를 같이 전달하게 되는데 포트 번호는 16bit(2^16) 0 ~ 65535의 범위를 가지며 일반적으로 1~1023는 용도가 정해진 포트 번호들이 있고, 서버 소켓의 포트로는 1024 ~ 49151 범위로 사용하는 것 같다. 동적 포트는 보통 브라우저에서 클라이언트 쪽에 할당하는 포트 번호이다.

 

- Well Known 포트 : 0부터 1023 (root 권한으로 바인드)

- Registered 포트 : 1024부터 49151 (일반유저 권한으로 바인드)

- Dynamic/Private 포트 : 49152부터 65535 (클라이언트와 서버 간 통신 시 클라이언트가 사용하는 포트)

 

 

 

2. ServerSocket, Socket


 이제 예제 코드에서 사용하는 소켓을 보면 ServerSocket, Socket이 있다.

 

먼저 ServerSocket을 보면 생성자 파라미터로 포트 번호를 전달하고 있는데 해당 포트 번호로 바인딩을 시도하고 정상적으로 바인딩이 되면 서버 소켓은 LISTEN 상태가 된다.

sudo lsof -PiTCP -sTCP:LISTEN

LISTEN 상태에서 서버 소켓은 클라이언트의 연결 요청을 수신하며 TCP/IP 연결을 하고 연결이 되면 연결 정보를 내부적으로 큐에 관리한다. accept()를 호출하면 큐에 있는 연결 정보를 꺼내거나 없을 경우 현재 스레드에서 block 상태로 대기한다. (non-blocking 방식 socket도 있다고 한다.)

 

ServerSocket의 생성자를 보면 backlog로 큐의 사이즈를 설정할 수 있는데 기본 50으로 설정이 된다.

연결 요청을 관리하는 큐는 2개가 있는데 3 Way-Handshake 연결이 된 후 커넥션을 Accept Queue로 이동시키고 서버 소켓에서 accept()로 꺼낼 수 있다. 서버 소켓에서 설정한 backlog가 Accept Queue의 사이즈가 된다. (스프링 부트에서 톰켓의 backlog 값도 설정할 수 있다고 한다..!)

 

다시 예제 코드를 보면 accept()를 통해 Socket 인스턴스가 반환이 되는데 이 소켓은 클라이언트와 연결된 소켓으로 클라이언트쪽 소켓의 포트 정보를 가지고 있으며 Stream을 통해 데이터를 주고받을 수 있다. 즉, 서버쪽 포트를 바인딩하고 클라이언트 연결을 수신하는 ServerSocket클라이언트 연결이 accept() 될 때마다 생성되는 Socket이 있는 것이다.

 

accept()로 생성된 소켓에서 양쪽의 포트 정보를 확인할 수 있다.

 

예제에서는 8080번 포트를 사용했는데 "http://localhost:8080"처럼 뒤에 포트 번호를 붙여주고 전송을 하면 된다. RFC에 기본 포트로 지정이 되어 있는 80(HTTP) 443(HTTPS)로 포트 바인딩을 하면 scheme(HTTP, HTTPS)에 따라 포트 번호 생략이 가능하다.

 

 

3. Thread


예제 코드를 보면 Thread 인스턴스를 생성하고 thread.run() 또는 thread.start()를 호출해서 Runnable 타겟을 실행할 수 있는데 run()의 경우는 스레드가 생성되지 않고 현재 스레드에서 실행이 된다. 반면 start()는 스레드를 생성하고 해당 스레드에서 작업을 실행한다.

무엇을 사용하든 스레드 자체는 new Thread()를 할 때 생성이 되는 줄 알았는데 수업을 듣고 잘못 알고 있는 것 같아서 테스트를 해봤다.

Thread thread = new Thread(Runnable target);

// start()? run()?
thread.start();
thread.run();

 

 

먼저 thread.run()의 경우 blocking이 되고 Runnable의 동작이 시작해서 끝난 뒤에 CALL 메세지가 출력이 된다.

 

thread.start()로 변경한 결과 Runnable의 동작을 기다리지 않고 CALL 메시지가 먼저 출력이 된다.

사실 출력 내용만 봐도 run()은 main 스레드에서 실행을 하고 있고 start()는 Thread-n에서 실행을 하고 있다.

 

main 스레드를 sleep 시키고 thread.start()를 했을 때 스레드의 상태를 확인해 보았다.

반면 run()으로 테스트를 했을 때는 메인 스레드가 sleep이 풀릴 때까지 기다렸다가 실행이 된다.

 

new Thread()를 하면 힙 영역에 인스턴스가 생성되는 것뿐이고(이때 thread 상태는 NEW) 실제 스레드 공간이 할당되는 것은 start()를 호출할 때이다.

 

 

추가로 스레드 덤프에 대해 알게 되었는데 멀티스레드 환경에서 발생할 수 있는 스레드 간 경합, 데드락 등 여러 문제를 분석할 수 있다. 스레드가 제대로 반납이 되지 않을 때 스레드 덤프를 확인해 보면 좋을 것 같다.

현재 내 코드에서는 스레드풀을 사용하기 때문에 WAITING 상태로 대기하는 것을 볼 수 있었다.

 

 

[참고]

 

널널한 개발자님 유튜브, 김영한님 HTTP 강의

 

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Linux man : listen - 소켓의 연결을 위한 대기열을 만든다.

서버측 프로그램은 socket(2)함수를 이용해서 클라이언트(:12)의 연결을 받아들일 듣기소켓을 만들게 된다. 클라이언트의 연결은 듣기소켓을 통해서 이루어지는데 클라이언트는 connect(2)를 호출해

www.joinc.co.kr

 

스프링 부트에서 socket backlog 모니터링과 튜닝법

 

www.manty.co.kr

 

http의 기본 포트가 80, https의 기본 포트가 443인 이유는 무엇일까?

80은 처음부터 지정, 443은 나중에 요청을 받아 빈 공간으로 순서대로 배정

johngrib.github.io

 

리눅스보안기본#1 : : 포트(port)의 개념과 포트 제어 > 강좌 | 클라우드포털

포트(port)의 개념과 포트 제어 클라이언트/서버 환경에서 시스템의 접속은 포트와 포트간의 통신이라고 할 정도로 포트의 의미는 매우 중요하다. 당연히 시스템 내에서 작동하고 있는 서비스를

www.linux.co.kr

 

개발자를 위한 레시피

Recipes for Developer.

recipes4dev.tistory.com

 

Java client socket returned by ServerSocket.accept()

This is more of a general socket question. In Java, if I have a ServerSocket bound to a specific port, say 4444, I understand that it's listening for connection requests. The accept() method blocks

stackoverflow.com

 

Thread Dump 분석

Thread Dump 생성 방법Unix : kill -3 [PID]window : Ctrl + Break공통 : jstack [PID]반드시 생성시 3~5회 연속으로 생성하여 문제 상황에 대한 변화 과정을 확인 Thread Dump 정보tid (Java-Level Thread ID )를 이용하여 정보

ijbgo.tistory.com

 

[JAVA] - Thread.start()와 Thread.run()의 차이

JAVA로 Thread 관련 프로그래밍을 학습하다보면 start() 메서드와 run() 메서드를 보게되는데 두 메서드를 실행하게되면 Thread의 run() 메서드를 실행하게 된다. 다만 이 두 메서드의 동작방식을 제대로

kim-jong-hyun.tistory.com

 

[Java] JVM Thread Dump 분석하기

java-study에서 스터디를 진행하고 있습니다. JVM Thread Dump 분석하기 스레드 덤프가 필요한 이유 웹 서버에서는 많은 수의 동시 사용자를 처리하기 위해 수십 ~ 수백 개정도의 스레드를 사용한다. 두

steady-coding.tistory.com

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

 

 

일반적으로 사용하는 HashMap 의 경우 동시성을 보장해주지 않아서 멀티 스레드 환경에서 의도한대로 동작하지 않는다.

 

 

[Java] HashMap 구조

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

treecode.tistory.com

 

이전에 HashMap의 동작 방식에 대해 학습을 해봤는데 간단하게 정리하면 HashMap은 key 값으로 사용하는 객체의 hashCode()를 활용해서 배열(버킷)의 index를 구한다. 하지만 버킷의 크기는 제한적이고 동적으로 확장 시키면서 사용하기 때문에 다른 key, 같은 index를 가지는 경우가 생기고 이런 경우 자바에서는 링크드 리스트 또는 트리 노드 형태로 연결(Chaining) 시키면서 값을 저장한다.

 

다음은 HashMap을 사용하고 동시에 여러 스레드에서 User를 저장하는 코드이다. save() 메서드는 id 값을 늘리고 user에 담아서 HashMap에 저장하는 간단한 동작이다.

 

테스트 코드는 ThreadPool을 여러개 생성하고 스레드별로 for문을 돌리면서 save()를 호출하도록 한다. CountDownLatch는 스레드가 종료될때마다 값을 countDown()으로 내리면서 0이 될때까지 대기(await)를 하는데 Main 스레드가 동작하는 것을 막기 위해 사용했다.

 

 

리뷰어님한테 동시성 테스트의 경우 확률에 따라 영향을 받는다는 점을 유의하라는 피드백을 받고 @RepeatedTest를 통해 테스트를 반복해봤는데 여러번 돌려보니까 첫번째는 항상 실패를 하고 이후부터는 거의 다 성공을 한다.

뭔가 이상해서 테스트 수행 시간을 보니 첫번째 테스트를 하기 전에 초기화(?) 작업들을 하면서 시간이 오래 걸리고 그 영향으로 계속 실패를 하는 것 같았다. 실제로는 테스트 케이스가 너무 적어서 동시성 문제가 거의 발생하지 않는 상황인 것이다. 그래서 테스트 케이스를 확 늘려보니 의도한 결과가 나왔다. 동시성 테스트를 할 때는 반복 횟수와 테스트 케이스를 높게 주는 것을 신경 써야겠다.

자바는 HashMap의 동시성 문제를 해결하기 위해 ConcurrentHashMap(java.util.concurrent)을 제공하는데 ConcurrentHashMap으로 바꿔봐도 테스트는 여전히 실패한다. ConcurrentHashMap의 key 값으로 사용하는 Long 타입의 값을 증가시키는 과정에서 동시성 문제가 발생하기 때문이다. 자바는 언어 명세상 long, double을 제외한 변수를 읽고 쓰는 것이 원자적으로 수정이 완전히 반영된 값을 읽어오지만 한 스레드가 변경한 값이 다른 스레드에서 보이는가를 보장하지 않는다. 그래서 한 스레드가 변경한 값이 반영되기 전에 다른 스레드가 값을 읽어온 상태면 같은 id 값을 가지는 문제가 생긴다. (이펙티브 자바)

 

 

AtomicLong(java.util.concurrent)을 사용하면 이런 문제를 해결할 수 있다. 스레드는 메인 메모리에서 값을 읽어와서 CPU 캐시 메모리에 저장을 하는데 volatile 키워드를 사용하면 메인 메모리에서 직접 값을 읽어올 수 있다.

AtomicLong은 CAS(Compare-And-Swap) 알고리즘을 활용한 lock free 방식으로 동작하는데 내부적으로 volatile을 사용해서 스레드에 저장된 값과 메인 메모리의 값을 비교하고 같으면 변경하는 식으로 동작한다. 만약 값을 비교해서 다르면 재시도를 하기 때문에 synchronized처럼 락을 걸 필요가 없다. 다시 테스트를 해보니 정상적으로 통과가 되었다.

 

 

 

 

ConcurrentHashMap은 CAS와 부분적으로 lock을 사용해서 동시성을 보장하는데 빈 버킷에 값을 저장할때는 CAS를 사용하고 버킷에 노드가 존재하는 경우 블록 단위로 synchronized를 사용해서 값을 저장한다. read를 할 때는 락을 걸지 않기 때문에 성능도 준수한 편이다.

 

멀티 스레드 환경에서는 공유 자원으로 인해 발생하는 문제를 조심해야 하는데 상황에 따라 ThreadLocal을 활용하는 것도 좋을 것 같다. (ThreadPool의 경우 스레드를 재사용하기 때문에 초기화에 신경써야 한다.)

 

 

[참고]

https://www.geeksforgeeks.org/concurrenthashmap-in-java/

 

HashMap vs HashTable vs ConcurrentHashMap

이미지 출처: Top 35 Data Structure & Algorithms Interview Questions and Answers in 2021 각 자료구조는 필요에 따라 선택되고 활용된다. 인터페이스의 구현체로는 , , 등이 있다. Map 인터페이스를 구현하면, 형태

tecoble.techcourse.co.kr

 

 

자바 ConcurrentHashmap

Thread-Safe 함을 보장하면서도 높은 성능을 보장하는 HashMap 이다. 즉 해쉬맵을 쓰레드 세이프하도록 만든 클래스. 하지만 HashMap과는 다르게 key, value에 null을 허용하지 않는다. ConcurrentHashMap은 concurr

applefarm.tistory.com

 

'Java' 카테고리의 다른 글

[Java] 자바로 간단한 웹서버 구현 2  (0) 2023.06.08
[Java] 자바로 간단한 웹서버 구현 1  (0) 2023.06.03
[Java] HashMap 구조  (0) 2023.03.27
[Java] System.in 테스트 하는 방법  (0) 2023.03.11
[Java] static import 주의점  (0) 2022.10.13

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

 

먼저 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

 

 

+ Recent posts