프로세스란, 실행중인 프로그램을 의미한다.
프로그램을 실행하기 위해서는 주소공간,파일,메모리 등이 필요한데 운영체제로부터 이런 것을 할당받은 프로그램을 프로세스라 한다.

프로그램은 어떤 작업을 수행하기 위한 파일로써 정적인 상태이고, 프로세스는 그 작업을 수행하는 동적인 상태다.

프로세스의 메모리 구조

프로세스는 아래 그림과 같은 메모리 구조를 띄고 있다.


프로세스는 각자 본인이 사용하는 메모리 영역과 레지스터 값을 가진다.

프로세스의 메모리 영역은 코드,데이터,힙,스택 영역으로 구성된다.

  • 코드 : 사용자가 작성한 프로그램 함수들의 코드가 기계어 명령 형태로 변경되어 저장되는 공간
  • 데이터 : 전역 변수 또는 static 변수 등 프로그램이 사용하는 데이터를 저장하는 공간
  • 스택 : 함수의 복귀주소와 지역변수,매개변수,반환값을 저장하는 공간. 재귀함수가 반복되거나 지역변수가 너무 많으면 stack overflow 발생. 가변적이다.
  • : 프로세스 실행 중에 런타임에 할당되는 영역. 이곳에 메모리를 할당하는 것을 동적 할당이라고 한다. 가변적이다.

    이처럼 프로세스는 각자의 메모리 영역을 가지기에 프로세스간의 메모리는 서로 침범 할 수 없다.

프로세스 제어 블록 (Process Control Block, PCB)

PCB(Process Control Block)는 특정 프로세스에 대한 중요한 정보를 저장하고 있는 운영체제의 자료구조이다. 그렇기에 PCB는 일반적으로 보호된 메모리 영역,예를 들면 커널 스택등에 위치한다.

OS는 프로세스를 관리하기 위해 프로세스를 생성함과 동시에 고유한 PCB를 생성한다.

프로세스는 CPU를 할당받아 작업하다가도 context-switching이 일어나면, 진행하던 작업을 저장하고 CPU를 반환해야하는데, 이때의 작업 진행상황이 PCB에 저장된다.
그 후 다시 CPU를 할당받게 되면 PCB 안에 있던 정보들을 불러와서 작업이 멈추었던 시점에서부터 다시 시작한다.

  • 프로세스 식별자 : PID
  • 프로세스 상태 : new,ready,running,waiting,terminated

  • 프로그램 카운터,PC : 프로세스가 다음에 실행할 명령어 주소
  • CPU 레지스터
  • CPU 스케쥴링 정보 : 우선순위, 스케쥴 큐에 대한 포인터 ← 이것을 바탕으로 MLFQ나 RR이 동작한다.
  • 메모리 관리 정보 : 페이지 테이블 또는 세그먼트 테이블에 대한 정보 포함
  • 입출력 상태 정보 : 프로세스에 할당된 입출력 장치들과 열린 파일 목록
  • 어카운팅 정보 : 사용된 CPU 시간, 시간제한, 계정번호

이 PCB들은 커널 스택과 같은 보호된 메모리 영역 내에서 Linked List 형태로 관리된다. PCB List Head에 PCB들이 생성될때 붙는 방식으로 관리가 된다.

스레드(Thread)

스레드는 프로세스가 할당받은 자원을 이용하는 실행 단위다.

한 프로세스 내에서 동작되는 여러 실행 흐름으로 프로세스 내의 주소 공간이나 자원을 공유할 수 있다.

스레드는 스레드 ID,PC,레지스터 집합, 그리고 각자의 스택으로 구성된다(독립적인 메모리는 아니고, 스택 포인터로 표시한다).

이를 통해 멀티 쓰레딩을 달성할 수 있는데 멀티쓰레딩은 하나의 프로세스를 여러개의 실행단위(스레드)로 나누어서 자원을 공유하며 자원의 생성과 관리의 중복을 줄여 수행능력을 높이는 것이다.

  • 공유
    • 같은 프로세스에 속하는 스레드들은 힙,데이터,코드 영역을 공유한다. 추가적으로 open된 파일등도 공유한다.
  • 공유하지 않는 것
    • 각자의 스택과 레지스터(PC 포함)은 공유하지 않는다.

이렇게 하나의 프로세스 내에서 다수의 실행 단위인 스레드로 구분하여 공유할 자원은 공유하고, 독립적인것은 따로 두어 수행 능력을 향상시키는 것을 멀티스레딩이라 한다.

스레드에서의 메모리 구조

스레드 역시 프로세스와 단짝인 개념인데, 짧게 말하면 프로세스가 할당받은 자원을 이용하는 실행의 단위 이다. 위의 그림처럼, 여러개의 스레드는 각자의 레지스터와 스택을 가지지만, 나머지 영역은 가지지 않는다. 대신에 코드,힙,데이터 영역을 공유해서 병렬적인 수행이 가능하다.

  • TMI : 왜 스레드는 각자의 스택을 가지고 있을까?

    스레드가 하나의 실행의 context라는 것을 생각하면 자명하다. 그 context내에서 아주 간단하게 여러개의 지역변수,파라미터,반환값,복귀주소등을 가지는데, 그 스레드들이 만약 서로의 스택을 공유하게 된다면, 그 context가 서로 섞이게 된다. 사실 각자의 스택을 가지기 때문에 스레드가 종종 Lightweight Process라고 불리는 이유다. 그리고 각자의 스택을 가지고 있다고 얘기하지만 사실은 하나의 메모리공간을 stack pointer를 이용해서 분리하는 것이다.

  • TMI2 : 그럼 스레드는 왜 각자의 PC와 register도 있을까?

    위의 답과 비슷한 이유인데, PC(Program Counter)나 레지스터의 역할은 현재 명령어가 어디까지 수행되었는지,수행될때 쓰던 데이터는 무엇이었는지 알려주는 것이다. 스레드는 CPU를 할당받을때 프로세스처럼 스케쥴러에 의해 결정되는데, 그렇기에 명령어가 연속적으로 실행됨을 보장하지 못하기에 어디까지 실행되었는지 기록할 필요가 있는데 그래서 스레드가 각자의 PC를 가지는 것이다.

스레드의 장점과 단점

  • 장점
    • 반응성
      • 특정 스레드가 I/O 작업을 처리중이거나(blocked) 긴 작업을 수행중이여도 다른 스레드는 본인의 일을 계속 할 수 있다.
    • 자원 공유
      • 프로세스는 shared memory나 message passing과 같은 기법을 이용해서 자원을 공유할 수 있지만, 스레드끼리는 프로세스의 자원을 서로 공유한다.
    • 경제적
      • 프로세스의 자원을 공유하기 때문에 새로운 메모리 주소공간을 할당받을 필요가 없어 생성에 자원이 적게 들어간다.
      • 스레드 간에 컨텍스트 스위칭을 할 때 캐시 메모리 안비워도 된다. 어차피 같은 메모리 주소공간을 사용하니까 오버헤드가 적다.
      • 근데 이것도 극복하려고 thread pool이라는 기술도 있다.
    • 확장가능성
  • 단점
    • 공유자원을 사용하기 때문에 동기화 문제를 고려해야 한다.
      • lock을 함으로써 공유자원에 대한 동기화를 해결할 수 있는데 이게 병목현상이 될 수 있다.
      • 그러므로 적절한 부분만 lock을 하는 것이 필요하다. → Critical Section Only

멀티 프로세싱 vs 멀티 스레드

  • 멀티 프로세싱
    • 두 개 이상의 다수의 프로세스가 협력적으로 하나의 작업을 동시에 병렬적으로 처리하는 것
    • 프로세스간 메모리 공유는안되기 때문에, IPC를 이용해 소통
    • 하나의 프로세스에서 문제가 생겨도 다른 프로세스는 계속 진행할 수 있음.
    • 컨텍스트 스위칭 비용이 높음
  • 멀티 스레딩
    • 단일 프로세스 내에서 여러 쓰레드로 나누어 동시에 실행
    • 컨텍스트 스위칭 비용이 거의 없음
    • 스택을 제외한 데이터,코드,힙 영역을 공유하기 때문에 공유하는데 편리
    • 임계영역의 문제가 있음

GC를 수행하는 Garabage Collector는 아래와 같은 일을 한다.

  • 메모리 할당
  • 사용 중인 메모리 인식
  • 미사용 메모리 인식

Stop-the-World

  • 자바 애플리케이션은 GC 실행시 GC 실행 스레드를 제외한 모든 스레들르 멈추고, GC 완료 후 다시 스레드들을 실행 상태로 변경
  • Stop the World는 모든 애플리케이션 스레드들의 작업이 멈추는 상태
  • 어떤 GC 알고리즘을 사용해도 Stop-the-World는 불가피하며 대개의 GC 튜닝이란 이 Stop-the-World 시간을 줄이는 것이다.

전제

가비지 컬렉터는 두가지 전제 조건 하에서 만들어졌다.

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.

이것을 'weak generational hypothesis'라고 하는데 이것을 살리기 위해서 Young 영역Old 영역으로 나누었다.

  • Young 영역 (Young Generation 영역) : 새롭게 생성한 객체 대부분이 여기에 위치하고, 대부분이 금방 접근불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말함.
  • Old 영역 (Old Generation 영역) : 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다는 GC가 적게 발생한다.(쉽게 가득차지 않으니) 이 영역에서 객체가 사라질 때 Major GC가 발생한다고 말한다.

객체의 데이터 흐름은 아래와 같다. PermG는 Java 8에서 Metaspace로 교체되었다.

PermG에서 GC가 발생해도 MajorGC의 횟수로 친다.

전제의 두번째가 "오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다"인데, 만약 실제로 Old 영역의 객체가 Young 영역의 객체를 참조하는 경우가 생긴다면 Old 영역에 512Byte의 chunck로 되어 있는 card table을 따로 두어 해결한다.

카드 테이블에는 Old 영역에 있는 객체가 Young 영역의 객체를 참조할 때마다 정보가 표시된다. Young 영역의 GC를 실행할 때에는 Old 영역에 있는 모든 객체의 참조를 확인하지 않고, 이 카드테이블만 확인해 GC 대상인지 식별한다.

카드 테이블은 write barrier를 사용하여 관리한다. write barrier는 Minor GC를 빠르게 할 수 있도록 하는 장치인데, 이것 때문에 약간의 오버헤드는 있지만 (Old가 Young을 참조하는지 Old 영역 전체를 일일이 확인하지 않아도 되기에) 전반적인 GC시간은 줄어든다.

Young 영역의 구성

객체가 제일 먼저 생성되는 Young 영역은 크게 3가지로 나뉜다.

  • Eden 영역
  • Survivor 영역(2개,From과 To)

각 영역의 처리 절차를 순서에 따라 기술하면 다음과 같다.

  • 새롭게 생성한 대부분의 객체는 Eden 영역에 위치한다.
  • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동한다.
  • Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
  • 하나의 Survivor 영역이 가득차게 되면, 그 중에서 살아남은 객체를 다른 Surivor 영역으로 이동한다. 그리고 가득찬 Survivor 영역은 이제 아무 데이터가 없는 상태가 된다.
  • 이 과정을 반복하다 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.

이 절차에 따라서 Survivor 영역 중 하나는 반드시 비어있는 상태로 남아 있어야 한다.

💡 오래되었다고 하는 기준은 Young Generation 영역에서 Minor GC 가 발생하는 동안 얼마나 오래 살아남았는지로 판단한다. 각 객체는 Minor GC에서 살아남은 횟수를 기록하는 age bit 를 가지고 있으며, Minor GC가 발생할 때마다 age bit 값은 1씩 증가 하게되며, age bit 값이 MaxTenuringThreshold 라는 설정값을 초과하게 되는 경우 Old Generation 영역을 객체가 이동 되는 것이다. 또는 Age bit가 MaxTenuringThreshold 초과하기 전이라도 Survivor 영역의 메모리가 부족할 경우에는 미리 Old Generation 으로 객체가 옮겨질 수도 있다. JVM 옵션 : -XX:MaxTenuringThreshold

그럼 왜 Survivor 영역이 두개인가?

퍼포먼스와 연관있는데, fragmentation(단편화)를 줄이기 위함이다.
예를 들어 새로운 객체는 Eden에 생성된다. Eden이 가득차면 GC가 수행되고 살아있는 객체는 Survivor로 옮겨진다. 근데 그다음에 Eden이 가득차면, Eden과 Survivor 영역의 메모리를 정리하지만 이 영역은 연속적이지 않게 된다. 이런 현상을 방지하기 위해 두가지 Survivor 영역을 두어서 위의 예시에서 두번째 GC시에 Eden과 Survivor 안에 있는 reachable한 객체들은 비어 있는 새로운 Survivor로 옮겨지거나 특정 객체(Old enough한)는 Old로 Promotion된다. 그리고 두 Survivor space는 역할을 바꾼다. 하나는 텅텅 비어있고 하나는 Eden에서 올라오는 것을 수용하는 공간. 이 과정을 통해 Heap에서의 연속적인 메모리 사용을 가능하게 한다.

다시 말하면 Eden에서도 빈 공간 생기고, Survivor에서도 드문드문 빈 공간이 생기게 되는것. Memory internal Fragmentation과 비슷한일이 일어나는것)

Mark and Copy

SerialGC에서 Young Generation에게 쓰는 GC 방식이다.

  • Fragmentation(단편화) 방지에는 효과적이다.
  • Heap의 절반 밖에 사용하지 못하니 공간 활용의 비효율성
  • Suspend 현상(Copy할때), Copy에 대한 Overhaed 존재

Mark and Copy algorithms are very similar to the Mark and Compact as they too relocate all live objects. The important difference is that the target of relocation is a different memory region as a new home for survivors. Mark and Copy approach has some advantages as copying can occur simultaneously with marking during the same phase. The disadvantage is the need for one more memory region, which should be large enough to accommodate survived objects.

Old 영역에 대한 GC

Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식에 따라 처리 절차가 달라지므로 GC 방식에 따라 접근하고 이해해야 한다. JDK 7 기준 5가지 방식

  • Serial GC (싱글코어를 상정하고 만든 방식이라 운영서버 사용금지)
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC(CMS)
  • G1(Garbage First) GC (도입은 JDK7, JDK9부터 기본 GC)

Serial GC

Young 영역에서의 GC는 앞에서 설명한 방식을 사용하고,(Mark and Copy) Old 영역의 GC는 mark-sweep-compact 알고리즘을 사용한다. 디스크 조각모음과 비슷하다. 두 GC 모두 Stop-the-World를 트리거한다.

  1. Old 영역에 살아 있는 객체를 식별(Mark)한다.
  2. Heap의 앞 부분부터 확인하여 살아있는 것만 남긴다.(Sweep)
  3. 각 객체들이 연속되게 쌓이도록 Heap의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 존재하지 않는 부분으로 남긴다.(Compaction)

적은 메모리와 CPU 코어 개수가 적을때 적합한 방식이다.

Parallel GC

기본적인 알고리즘은 Serial GC와 같다. 그러나 SerialGC가 GC를 처리하는 스레드가 하나인 것에 비해 Parallel GC는 GC를 처리하는 스레드가 여러개로 SerialGC보다 빠르게 수행된다. 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Throughput GC라고도 부른다.


더 빠르게 동작하니 Stop-the-World의 시간도 줄여주는 효과를 얻을 수 있고 Java 애플리케이션 전체가 매끄럽게 동작한다.

Parallel Old GC

JDK5u6부터 제공한 GC 방식이고, Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다르다. 이 방식은 Mark-Summary-Compaction 단계를 거친다. Summary 단계는 앞서 GC를 수행한 영역에 대해 별도로 살아 있는 객체를 식별한다는 점에서 Mark-Sweep-Compcation 알고리즘의 sweep 단계와는 다르며, 약간 더 복잡하다.

  • Sweep단일 스레드가 Old 영역 전체를 훑어 살아있는 객체만 찾는다.
  • Summary여러 스레드가 Old 영역을 분리하여 훑는다. 또 효율성을 위해 Compaction된 영역도 별도로 훑는다.

CMS GC (Concurrent Mark-Sweep Garbage Collector)

GC 과정에서 발생하는 Stop-the-World의 시간을 최소화하는데 초점을 맞춘 GC 방식으로 GC의 과정이 복잡하다.

GC 대상을 최대한 자세히 파악한후, 정리하는 시간(STW가 발생하는 시간)을 짧게 가져가는 컨셉으로, 과정이 복잡한 만큼 다른 GC 대비 CPU 사용량이 높다.

아래의 그림은 Serial GC와 CMS GC를 비교한 그림이다. 엄청 복잡.


Young 영역에서는 Mark and copy방식을 그대로 사용하고 Old 영역은 Concurrent Mark-Sweep 알고리즘을 사용한다.

CMS GC는 Initial Mark → Concurrent Mark → Remark → Concurrent Sweep 과정이다.

  • Initial Mark
    • GC 과정에서 살아남은 객체를 탐색하는 시작 객체(GC Root)에서 참조 Tree상 가장 가까운 객체만 1차적으로 찾아가며 객체가 GC대상(참조가 끊긴)인지를 판단한다. 이때는 STW 현상이 발생하지만, 탐색 깊이가 얕아 STW 발생 기간이 매우 짧다.
  • Concurrent Mark
    • STW 현상없이 진행되며, Initial Mark 단계에서 GC 대상으로 판별된 객체들이 참조하는 다른 객체들을 따라가며 GC 대상인지를 추가적으로 확인한다.
    • 이 단계의 특징은 다른 스레드가 실행중인 상태에서 동시에 진행된다는 것.
  • Remark
    • Concurrent Mark 단계의 결과를 검증하며, 이전 단계에서 GC 대상으로 추가 확인되거나 참조가 제거되었는지 등을 확인한다. 이 과정은 STW를 유발하기 때문에 STW 지속시간을 최대한 줄이기 위해 멀티스레드로 검증 작업을 수행한다.
  • Concurrent Sweep
    • STW 없이 Remark 단계에서 검증 완료된 GC 객체들을 메모리에서 제거한다.

*Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep ,* CMS의 상황
Initial Mark -> Concurrent Mark -> Remark -> Concurrent Sweep , CMS의 상황

CMS GC는 Compaction 작업을 필요한 경우에만 수행한다. 즉, 연속적인 메모리 할당이 어려울 정도로 메모리 단편화과 심한 경우에만 Compaction 과정을 수행하는 것이다.

또 이러한 단계로 진행되는 것이기에 STW 시간이 매우 짧고, 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, Low Latency GC라고도 부른다.

G1 GC (Garbage First)

G1 GC는 기존의 Young 영역과 Old 영역을 구분하던 방식과는 다른 접근을 한다.

아래 그림과 같이 G1 GC는 바둑판처럼 영역을 구분하고 그 영역에 객체를 할당하고 GC를 실행한다. 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다.

기존의 Young의 Eden/Survivor 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC방식이다. 또 G1 GC에선 각각의 바둑판 모양의 영역이 Eden,Survivor,Old 영역의 역할을 동적으로 바꿔가며 GC가 일어난다.

G1 GC는 지금껏 얘기한 어떤 GC 방식보다 빠른 성능을 장점으로 가진다. 다시 말해 짧은 STW를 지향한다는 것이다.

G1 GC는 굉장히 크기가 큰 Heap에서도 짧은 STW 시간은 보이는데 왜 그런것일까?

G1 GC의 비밀

Heap의 용량이 커지면 커질수록, 객체의 갯수가 많아지고, 자연스럽게 GC 수행시간이 길어지며 STW 시간도 늘어난다. 하지만 G1 GC는 다르다.

  1. GC시에 전체 Heap에 대해서 GC를 수행할 필요가 없다. GC 해야하는 영역만 GC를 수행하면 되기 때문이다.
  2. Old 영역에 대한 Compaction을 할때, 전체 Old 영역에 대해서 Compaction을 할 필요가 없다. 특정한 영역에 대해서만 Compaction을 하면 된다.
  3. Garbage를 먼저 수집해간다. G1 GC는 살아있는 객체를 마킹한후에 영역 별로 얼만큼 살려줘야 하는지를 알 수 있다. 그 후 영역 중에 모든 객체가 죽은 리전(유효한 객체가 없는,Garbage만 있는 영역)부터 먼저 회수를 한다. 메모리 회수를 먼저하기에 빈 공간 확보를 더 빨리 할 수 있다. 그러면 GC가 낮은 빈도로 일어난다.

왜 G1GC가 JDK9부터 default GC로 선정되었을까?

G1 GC의 목표는 일시정지 시간 (STW)을 최소화하는데 있다. 영역별로 나누어서 GC를 수행하기 때문에 전체 Old 영역에 대한 GC를 수행하는 일이 생기지 않아 긴 시간의 STW를 가지는 Major GC의 빈도를 낮출수 있어서 선택되었다고 생각한다.

출처 :

JVM GC

네이버 D2 블로그

전체적인 GC에 대한 통계 제공

'Java' 카테고리의 다른 글

JVM의 Java 8에서의 변화  (0) 2022.09.18

MappingRegistry

MappingRegistry는 아까 살펴본 AbstractHandlerMethodMapping의 내부 클래스다. MappingRegistryhandler method에 대한 모든 mapping을 유지 관리하고 lookup을 수행하는 method를 가지고 있고 동시성을 가진 접근을 가능하게 해주는 레지스트리다.

A registry that maintains all mappings to handler methods, exposing methods to perform lookups and providing concurrent access.
Package-private for testing purposes.

가장 중요한 부분이 handler method에 대한 모든 mapping을 유지 관리하고 lookup을 수행하는 method를 가지고 있다는 것이다.

class MappingRegistry {
  private final Map<T, MappingRegistration<T>> registry = new HashMap<>();

  private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();

  private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();

  private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();

  private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  ...
  ...

  // methods
}

MappingRegistry 안에서 Map 자료구조를 가진 멤버 변수들이 있다. 그 중에서LinkedMultiValueMap이라는 자료구조를 사용한다. 이건 한개의 key에 여러 value들을 저장하는 MultiValueMapLinkedHashMap으로 감싼 자료구조로 Spring이 만든 자료구조다.

urlLookupLinkedMultiValueMap의 자료구조인데, key는 url을 가지고, value는 RequestMappingInfo를 가진다. LinkedMultiValueMap을 쓰는 이유는 하나의 url에 여러 handlerMethod들에 대한 정보가 담기기 때문이다.

예를 들어 "/app/user"라는 url 아래 user에 대한 정보를 조회하는 GET,user를 추가하는 POST가 매핑될때, 아래처럼 RequestMappingInfo가 들어가는 것이다.

key : "/app/user/ 
value : [GET /app/user,POST /app/user]

위와 같은 구조를 통해 MappingRegistryurl에 해당하는 handlerMethod를 구별할 수 있게 된다. 코드로 보자.

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
  List<Match> matches = new ArrayList<>();
  List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
  if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
  }
  ...
  ...
  if (!matches.isEmpty()) {
    ...
    ...
    matches.sort(comparator);
    Match bestMatch = matches.get(0);
    ...
    ...
    request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
    handleMatch(bestMatch.mapping, lookupPath, request);
    return bestMatch.handlerMethod;
  }

위는 아까 잠깐 언급한 lookupHandlerMethod이다. 적절한 handlerMethod를 가져온 후 return 한다 고 했는데 그 과정이 담겨있다.
길다고 겁먹지 말고 한줄씩 보자. (match되는 것이 없거나, 2개 이상인 경우는 제외함)

List<Match> matches = new ArrayList<>();

match를 담는 matches라는 리스트가 있다.

List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);

현재 url에 mapping되는 handler method들의 RequestMappingInfo들을 getMappingsByUrl로 가져온 후 directPathMatches에 저장한다. 예를 들어 url/app/user이면 directPathMatches에는 [GET /app/user, POST /app/user] 와 같은 정보가 들어오는 것이다.

if (directPathMatches != null) {
    addMatchingMappings(directPathMatches, matches, request);
}

그 후 [GET /app/user, POST /app/user] 중에서 request 정보와 일치하는 것들을 addMatchingMappings을 통해서 matches에 추가한다.

matches.sort(comparator);
Match bestMatch = matches.get(0);

matches들을 우선순위에 맞게끔 정렬하고, request와 가장 일치하는 0번째 matchbestMatch에 저장한다.

request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;

bestMatch의 멤버인 handlerMethod를 return해서 최종적으로 적합한 handler method를 찾게 된다.

private class Match {

  private final T mapping;

  private final HandlerMethod handlerMethod;

}

Reflection

이제 마지막 궁금증만이 남았다.

출처

MappingReigstry javadoc
LinkedMultiValueMap javadoc

+ Recent posts