728x90

HTTP

웹 서버로부터 대량의 정보를 빠르고, 간편하고, 정확하게 웹 브라우저로 옮길 수 있게 해주는 프로토콜

 

신뢰성 있는 데이터 전송 프로토콜을 사용해 전송 중 손상되거나 꼬이지 않음을 보장하기 때문에

사용자는 정보를 얻는 것에 있어서 걱정할 필요가 없고

개발자는 전송 중 발생할 결함이나 약점에 대한 걱정 없이 개발에만 집중할 수 있다.

 

 

웹 클라이언트와 서버

웹 서버는 웹 콘텐츠가 존재하는 곳으로 클라이언트와 HTTP 프로토콜로 의사소통한다.

 

클라이언트가 서버에게 HTTP 요청을 보내면

서버는 요청된 데이터를 HTTP 응답으로 돌려준다.

https://www.naver.com/index.html

 

우리가 크롬이나 엣지 같은 브라우저(클라이언트)를 통해 위의 주소로 요청을 보내는 경우

https://www.naver.com

 

위의 서버에 index.html를 찾아달라는 HTTP 요청을 보낸 것이고

서버는 클라이언트가 요청한 데이터와 타입, 길이 등의 정보를 담아 HTTP 응답을 돌려준다.

 

 

리소스

리소스는 웹 서버가 관리하고 제공하는 웹 콘텐츠의 원천이다.

 

대표적으로 텍스트, HTML, 이미지, 동영상 등의 웹 서버 파일 시스템의 정적 파일이 있고

요청에 따라 콘텐츠를 생산하는 프로그램 같은 동적 콘텐츠 리소스 등이 있다.

 

또한, 웹 게이트웨이, 검색엔진, 데이터베이스 검색 등 모두 리소스가 될 수 있다.

즉, 종류에 상관 없이 모두 리소스가 될 수 있다.

 

 

미디어 타입

MIME(Multipurpose Internet Mail Extensions, 다목적 인터넷 메일 확장)

 

HTTP는 웹에서 전송되는 수천 가지 데이터 타입을 다루기 위해

전송되는 객체 각각에 MIME 타입이라는 데이터 포맷 라벨을 붙인다.

 

웹 서버는 모든 HTTP 객체 데이터에 MIME 타입을 붙여 클라이언트에게 객체를 전달하고

클라이언트는 MIME 타입을 보고 다룰 수 있는 객체인지 확인할 수 있다.

Content-type : image/jpeg

text/html
image/gif

 

MIME 타입은 위와 같이 주 타입/부 타입 형태로 이루어진 라벨이다.

 

 

URI (Uniform Resource Identifier, 통합 자원 식별자)

리소스를 고유하게 식별하고 위치를 지정할 수 있는 식별자로

이를 통해 클라이언트는 관심 있는 리소스를 지목할 수 있다.

http://www.shop.com/items/book.jpg

 

위의 URI를 해석하면

HTTP 프로토콜을 사용해 www.shop.com으로 이동해

items/book.jpg라는 리소스를 가져오라는 것이다.

 

URI는 URL과 URN 두 가지가 있다.

 

URL (Uniform resource locator, 통합 자원 지시자)

http://www.shop.com/items/book.jpg
http://www.shop.com/index.html
http://www.shop.com/get?item=1234

 

가장 흔한 형태의 리소스 식별자로 특정 서버의 한 리소스에 대한 구체적 위치를 서술한다.

 

대부분 scheme, 서버의 주소, 웹 서버의 리소스 세 부분으로 이루어진 표준 포맷을 따른다.

scheme : 리소스에 접근하기 위해 사용되는 프로토콜로 HTTP, FTP 등이 있다.

 

URN (Uniform resource Name)

리소스에 대한 위치에 영향받지 않는 고유한 이름으로

리소스의 위치가 옮겨지더라도 URN이 변하지 않는 한 접근할 수 있다.

 

 

트랜잭션

HTTP 트랜잭션은 클라이언트가 서버로 보내는 요청 명령과 서버가 클라이언트에 돌려주는 응답 결과로 구성되어 있다.

 

HTTP 요청 메시지는 다음과 같이 명령과 URI를 포함하고

GET /items/book.jpg HTTP/1.0
Host: www.shop.com

 

HTTP 응답 메시지는 다음과 같이 트랜잭션의 결과를 포함하며

HTTP/1.0 200 OK
Content-type: image/jpeg
Content-length: 4321

 

이러한 두 메시지를 하나의 HTTP 트랜잭션이라고 한다.

 

메서드

서버에게 특정 동작을 취하게 할 수 있게 지원하는 여러 가지 종류의 요청 명령이다.

 

GET 지정한 리소스를 보내라
PUT 클라이언트가 보낸 데이터를
지정한 이름의 리소스로 저장하라
(리소스의 대체)
PATCH 지정한 리소스의 일부분을 변경하라
DELETE 지정한 리소스를 삭제하라
POST 클라이언트의 데이터를
서버 게이트웨이 애플리케이션으로 보내라
(리소스의 추가)
HEAD 지정한 리소스에 대한 응답에서
HTTP 헤더 부분만 보내라

 

상태 코드

서버가 클라이언트에게 요청이 성공했는지 실패했는지 혹은 추가 조치가 필요한지 알려주는 세 자리 숫자

 

 

메시지

단순한 일반 텍스트로 이루어진 줄 단위의 문자열로 요청 메시지와 응답 메시지를 말한다.

시작줄  GET /items/book.jpg HTTP/1.0

헤더  Accept: text/*
시작줄  HTTP/1.0 200 OK

헤더  Content-type: image/jpeg
헤더  Content-length: 4321

본문 This is Content!

 

위와 같이 시작줄, 헤더, 본문으로 이루어져 있다.

 

시작줄 : 무엇을 해야 하는지 혹은 무슨 일이 일어났는지 나타낸다.

헤더 : 0개 이상 존재할 수 있으며 각 헤더 필드는 쌍점(:)으로 구분되어 하나의 이름과 값으로 구성된다.

본문 : 어떤 종류의 데이터든 들어갈 수 있는 메시지 본문으로 웹 서버로 데이터를 보내거나 클라이언트에 데이터를 반환할 때 사용한다.

 

 

TCP 커넥션

TCP/IP

TCP와 IP가 층을 이루는 패킷 교환 네트워크 프로토콜로

HTTP는 애플리케이션 계층의 프로토콜로 네트워크 통신의 세부사항에는 신경을 쓰지 않기 때문에

대중적이고 신뢰성 있는 인터넷 전송 프로토콜인 TCP/IP를 사용한다.

 

TCP는 다음과 같은 기능을 제공한다.

  • 오류 없는 데이터 전송
  • 데이터를 언제나 보낸 순서대로 도착하게 보장
  • 언제든 어떤 크기로든 보낼 수 있는 조각나지 않는 데이터 스트림

TCP/IP는 각 네트워크와 하드웨어의 특성을 숨기고,

어떤 종류의 컴퓨터와 네트워크라도 서로 신뢰성 있는 의사소통을 하게 해 준다.

HTTP (애플리케이션 계층)
TCP (전송 계층)
IP (네트워크 계층)
네트워크를 위한 링크 인터페이스 (데이터 링크 계층)
물리적인 네트워크 하드웨어 (물리 계층)

 

개념상 위와 같이 HTTP 프로토콜은 TCP 위의 계층이고

HTTP는 자신의 메시지 데이터를 전송하기 위해 TCP를 사용하는 것이다.

 

접속 / IP 주소 / 포트 번호

HTTP가 서버에 메시지를 전송하기 위해서는 IP 주소와 포트 번호를 사용해

클라이언트와 서버 사이에 TCP/IP 커넥션을 맺어야 한다.

 

IP 주소가 친구네 집 전화번호라면

친구의 집(IP 주소)에 전화해 친구(포트 번호)를 바꿔 달라는 것과 비슷하다.

 

즉, 서버의 컴퓨터에 접근해 웹 서버가 사용 중인 포트까지 접근해야 실제 웹 서버와 통신이 가능한 것이다.

  1. 브라우저는 서버의 URL에서 호스트 명을 추출
  2. 서버의 호스트 명을 IP로 변환
  3. URL에 포트번호가 있다면 추출, 없으면 80이 기본값
  4. 웹 서버와 TCP 커넥션 생성
  5. 서버에 HTTP 요청
  6. 서버가 브라우저에 HTTP 응답 반환
  7. 커넥션이 닫히면 브라우저가 문서를 보여준다.

위와 같은 과정을 통해 우리가 브라우저의 주소창에 URI를 입력하면 페이지가 보이게 된다.

 

 

웹의 구성요소

프록시

보안, 애플리케이션 통합, 성능 최적화 등을 위한 중요한 구성요소 중 하나다.

 

클라이언트와 서버 사이에 위치해

클라이언트의 모든 HTTP 요청을 받아 대부분 요청을 수정해 서버에 전달하는

사용자를 대신해 서버에 접근하는 역할을 수행한다.

 

모든 웹 트래픽의 흐름 속에서 신뢰할 만한 중개자 역할을 하기 때문에

주로 보안을 위해 사용하거나 요청과 응답 필터링 등에 사용된다.

 

캐시

자신을 거쳐 가는 문서들 중 자주 찾는 문서의 사본을 저장하는 곳으로

클라이언트의 다음 요청에 같은 문서를 요청하면 캐시가 갖고 있는 사본을 받을 수 있어

멀리 떨어진 웹 서버보다 가까운 캐시에서 빠르게 문서를 받을 수 있다.

 

게이트웨이

다른 서버들의 중개자로 동작하는 서버로, 주로 HTTP 트래픽을 다른 프로토콜로 변환할 때 사용한다.

 

항상 자신이 실제 리소스를 갖고 있는 진짜 서버인 것처럼 요청을 다루기 때문에

클라이언트는 자신이 실제 서버와 통신하는 것이 아니라 게이트웨이와 통신하는 것을 알 수 없다.

 

예를 들면, 클라이언트가 HTTP 요청을 HTTP/FTP 게이트웨이에 보내면

해당 요청을 FTP 프로토콜로 처리해 다시 클라이언트에 HTTP 응답을 반환한다.

 

터널

두 커넥션 사이에 데이터를 열어보지 않고 그대로 전달해주는 HTTP 애플리케이션으로

주로 HTTP 데이터가 아닌 것을 HTTP 연결을 통해 전달하고자 할 때 사용된다.

 

에이전트

사용자를 위해 HTTP 요청을 만들어주는 클라이언트 프로그램으로

사람의 통제대로 동작하거나 혹은 통제 없이 동작하는 자동화된 에이전트도 있다.

 

예를 들어, 스파이더라는 에이전트는 스스로 웹을 돌아다니며

검색엔진의 DB 같은 유용한 웹 콘텐츠 보관소를 만든다.

'Book' 카테고리의 다른 글

[친절한 SQL 튜닝] SQL 처리 과정과 I/O  (0) 2025.03.02
[HTTP 완벽 가이드] URL과 리소스  (0) 2024.03.08
728x90

Mapped Diagnostic Context

멀티 쓰레드 환경에서 로그가 뒤섞이는 문제를 해결하기 위해 사용할 수 있는 객체로

현재 실행 중인 쓰레드에 메타 정보를 넣고 관리하는 공간이다.

 

  • 내부적으로 쓰레드 로컬을 사용한다.
  • Map 형태로 구성되어 (키, 값) 형태로 값을 저장하고 사용할 수 있다.
  • 다양한 데이터를 저장해 구체적이고 유용한 로그 메시지를 제공할 수 있다.

 

주로 각각의 요청에 대한 로깅을 구분하기 위해 고유한 요청 ID를 설정하거나

사용자 정보, 세션, 트랜잭션, 로깅 수준 등을 설정한다.

 

 

MDC 사용

implementation 'org.springframework.boot:spring-boot-starter-logging'

 

위의 디펜던시를 추가하면 org.slf4j 패키지에 있는 MDC를 사용할 수 있다.

 

위에서 언급한 것처럼 Map 형태로 이루어져 있기 때문에 put, get, remove, clear 처럼

자바의 유틸 클래스에 있는 Map의 메서드와 다를게 없어서 사용에 어려움은 없다.

 

이제 MDC를 어디에서 설정해주는게 효율적인지를 생각해 봐야 하는데

요청에 대한 모든 로그를 남기려면 가장 처음에 접근하는 필터단에서 처리해주는게 효율적이다.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class MDCLoggingFilter implements Filter {

	@Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final UUID uuid = UUID.randomUUID();
        MDC.put("request_id", uuid.toString());
        chain.doFilter(request, response);
        MDC.clear();
    }
}

 

위와 같이 @Order 어노테이션을 사용해 filter 중에서도 가장 빠른 순서로 만들어준다.

 

랜덤으로 고유한 UUID를 만들어 MDC에 저장해주면 요청별로 고유 ID를 기록할 수 있게 되고

쓰레드 로컬 변수는 초기화하지 않으면 쓰레드풀에서 다시 꺼낼 때

이전 데이터가 남아있게 되어 doFilter가 끝난 후에는 MDC를 clear 해줘야 한다.

 

 

Logback 적용

<configuration>
    <appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>utf8</charset>
            <pattern>%date{HH:mm:ss.SSS, Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE_APPENDER" />
    </root>
</configuration>

 

위와 같이 설정하면 콘솔창에 로그가 출력되는 것을 확인할 수 있다.

 

AOP를 적용하여 번거로운 로깅 처리를 간단하게 할 수도 있고

Spring Cloud Sleuth와 Zipkin을 사용해 분산된 마이크로서비스 간의 로깅도 가능하다 하니

이 부분들도 학습하면서 언젠가 정리해보도록 하겠다.

728x90

문제

 

 

접근 방식

가장 낮은 값과 (다음으로 낮은 값 * 2)을 더한 값을 다시 포함하여

모든 수가 K 이상이 되게 만들어야 한다.

 

즉, 가장 낮은 값 두 개를 뺀 후에 섞고 다시 섞은 것을 추가해야 하고

이 때, 항상 정렬된 상태를 유지해야 하는게 중요하다.

 

섞은 것을 추가할 때마다 정렬을 하기에는 시간 복잡도가 비효율적이기 때문에

값이 추가되어도 항상 정렬된 상태를 유지할 수 있는 우선순위 큐를 사용하면 쉽다.

 

처음에는 우선순위 큐에 모든 값들을 넣어주고

큐에서 가장 작은 값을 두 개씩 빼가며 섞어주고 다시 넣어주다가

꺼낸 값이 K 이상이라면 이후의 값들도 당연히 K 이상이기 때문에

현재까지의 카운트를 리턴해주면 되고

값을 하나 꺼냈는데 더 이상 남은 것이 없다면 더 이상 섞을 것이 없기 때문에

모든 값을 K 이상으로 만들 수 없다는 것이 되어 -1을 출력해준다.

 

 

풀이

class Solution {
    public int solution(int[] scoville, int K) {
        PriorityQueue<Integer> q = new PriorityQueue<>();
        
        for (int s : scoville) q.offer(s);
        
        int cnt = 0;
        
        while(!q.isEmpty()) {
            int first = q.poll();
            
            if(first >= K) break;
            if(q.isEmpty()) return -1;
            
            cnt++;
            
            q.offer(first + (q.poll() * 2));
        }
        
        return cnt;
    }
}

 

'Java > Algorithms' 카테고리의 다른 글

[백준] 2156번 : 포도주 시식  (0) 2024.03.08
[백준] 1904번 : 01타일  (0) 2024.03.07
[백준] 10994번 : 별 찍기 - 19  (0) 2024.02.27
[백준] 9184번 : 신나는 함수 실행  (0) 2024.02.23
[백준] 4779번 : 칸토어 집합  (0) 2024.02.22
728x90

자바의 하위 호환성

자바는 이전 버전이 상위 버전의 JVM에서 동작이 보장되지만

상위 버전의 기능을 하위 버전의 JVM에서 컴파일 할 수는 없다.

 

즉, 8버전의 기능을 17버전에서 구동할 수는 있어도

17버전에만 있는 기능을 8버전에서 구동할 수는 없다.

 

JDK 1.1

  • Inner Class
  • JavaBeans (자바로 작성된 소프트웨어 컴포넌트)
  • Remote Method Invocation (분산 애플리케이션 구축 시 사용되며 java.rmi 패키지에 제공된다)
  • Reflection

 

J2SE 1.2

  • Swing GUI
  • JIT
  • Collection Framework

 

J2SE 1.3

  • Hotspot JVM
  • Java Naming and Directory Interface (데이터 및 객체를 발견하고 참고하기 위해 사용)
  • Java Platform Debugger Architecture
  • JavaSound (오디오 재생 지원)

 

J2SE 1.4

  • assert (단언문)
  • 정규표현식
  • IPv6
  • Non-Blocking IO (nio)
  • XML API
  • JCE
  • JSSE
  • JAAS
  • Java Web Start

 

J2SE 5

  • Generics
  • Annotation
  • Concurrency API
  • Enumeration
  • Auto Boxing / Unboxing
  • 가변 길이 파라미터
  • Static import
  • java.util.Scanner

 

Java SE 6

  • 가비지 컬렉터 G1 GC를 테스트용으로만 사용하도록 추가
  • Scripting Language 지원
  • JDBC 4.0

 

Java SE 7

  • Diamond Oeprator <>
  • try 문에 선언된 자원들을 자동으로 관리
  • Switch 문에서 String 사용 가능해짐
  • Dynamic Language 지원
  • 이진수 리터럴, 숫자 리터럴에 _ 지원

 

Java SE 8 (LTS)

  • 람다 표현식
  • 메서드 참조
  • 인터페이스에 디폴트 메서드 구현 가능
  • Optional
  • Clock, ZoneId, LocalDate 등과 같은 날짜 API 추가
  • 스트림 API
  • Heap 영역의 PermGen 영역 제거

 

Java SE 9

  • Jigsaw (런타임의 모듈화 지원, 애플리케이션의 경량화, 성능 향상, 유지보수 용이)
  • java.net.http 패키지 추가
  • JShell (테스트 프로젝트나 main 메서드 없이 신속한 테스트를 가능하게 대화식 REPL 도구 제공)
  • <> 연산자를 익명 클래스에서도 사용 가능해짐
  • try-with-resources 개선
  • 인터페이스에서 Private 메서드 지원
  • Optional의 스트림 지원

 

Java SE 10

  • var 키워드로 지역 변수 타입 추론 가능
  • 병렬 처리 GC
  • 개별 쓰레드로 분리된 Stop-The-World

 

Java SE 11

  • HTTP 클라이언트 API를 정식으로 포함
  • String 클래스에 신규 메서드 추가
  • 람다의 파라미터로 타입 추론 사용 가능

 

Java SE 12

  • Switch 문의 문법 확장
  • GC 개선
  • 성능 개선

 

Java SE 13

  • yield 예약어 추가
  • Text-Block

 

Java SE 14

  • instanceof의 패턴 매칭
  • record 추가

 

Java SE 15

  • Sealed 클래스 추가 (상속 가능한 클래스를 지정할 수 있는 봉인 클래스)

 

Java SE 16

  • Unix 도메인 소켓에 연결 가능해짐

 

Java SE 17

  • Random Generator (예측하기 어려운 난수 생성 API)

'Java > Notion' 카테고리의 다른 글

JVM Warm Up  (0) 2024.03.04
Garbage Collection 파헤치기  (0) 2024.03.03
JVM 뜯어보기 (컴파일, 인터프리팅, JIT, GC, 메모리)  (0) 2024.03.02
자바의 컴파일 과정  (0) 2023.10.25
BigInteger  (0) 2023.05.08
728x90

Warm Up을 알아보기 전에 Warm Up이 필요한 이유에 대해 알아보겠다.

 

 

서버 배포 직후의 Latency 발생

서버 배포 직후 사용자가 요청을 보냈을 때 유독 응답이 느린 문제가 발생한다.

 

이러한 문제가 발생하는 원인은 크게 두 가지가 있는데

바로 클래스 로더의 지연 로딩과 JIT 컴파일러의 동작 방식 때문이다.

 

 

클래스 로더의 지연 로딩

클래스 로더는 클래스 파일을 찾아 메모리에 적재해 실행 가능하게 만들어주는데

이러한 동작을 자바 애플리케이션이 시작될 때 모두 메모리에 적재하는 것이 아니라

각 클래스들이 직접적으로 필요한 시점에 로딩을 하는 지연 로딩 방식을 사용한다.

 

즉, 서버 배포 직후에 사용자가 요청을 보내면 해당 요청을 처리하기 위해 필요한

클래스들은 당연히 아직 메모리에 적재가 되지 않은 상태일테고

이때부터 클래스 로더가  클래스들을 로딩하게 되기 때문에 배포 초기 Latency가 발생한다.

 

 

JVM의 컴파일 방식

JVM은 인터프리팅 방식과 컴파일 방식을 모두 사용하고 기본적으로 인터프리팅 방식을 사용한다.

 

그렇다면 컴파일 방식은 대체 언제 사용할까?

 

컴파일 방식은 모든 소스 코드를 한 번만 컴파일과 최적화를 해두면

이후에는 별도의 과정 없이 기계어로 읽을 수 있기 때문에

매번 코드를 하나하나 컴파일 하는 인터프리팅 방식보다 빠를 수 밖에 없다.

 

하지만 애플리케이션을 실행할 때 모든 class 파일을 컴파일 후에 실행하면

속도는 당연히 빠르겠지만 애플리케이션의 실행 시간이 많이 소요된다.

 

그래서 JIT 컴파일러는 실행 시 모든 코드를 컴파일 하지 않고 실행 중에 동적으로 컴파일을 진행한다.

 

 

JIT 컴파일러

JIT 컴파일러는 비유하자면 즐겨찾기 방식으로 컴파일을 진행하는데

애플리케이션 실행 중에 자주 실행되는 부분인 핫스팟을 컴파일 한다.

 

GC가 age를 기록하는 것과 비슷하게 JIT 컴파일러도 프로파일링이라는 과정을 통해

실행 중인 애플리케이션의 동작을 분석하고 기록하여 이를 토대로 핫스팟을 식별한다.

 

JIT 컴파일러는 이렇게 식별된 핫스팟을 컴파일해 코드 캐시라는 곳에 저장하여

인터프리팅 방식처럼 매번 하나하나 컴파일 할 필요 없이 저장된 것을 꺼내어 사용하기만 하면 된다.

 

 

JIT 컴파일러의 동작 방식

JIT 컴파일러는 Tiered Compliation이라는 여러 단계로 나뉜 컴파일 과정으로 동작하며

인터프리터와 C1, C2 두 개의 컴파일러로 이루어져 있다.

 

먼저, C1과 C2를 간단하게 살펴보겠다.

 

C1

가능한 빠른 실행 속도를 목적으로 하지만 최적화와 컴파일도 가능한 빠르게 하기 위해

제한된 수준으로만 최적화를 진행하는 컴파일러다.

 

특정 메서드가 일정치 이상 호출되면 C1 컴파일러에 의해 최적화 및 컴파일이 이루어 진다.

 

C2

C1 메서드로 최적화 및 컴파일 된 특정 메서드가 일정치 이상 호출되면 C2 컴파일러에 의해

C1보다 높은 수준의 최적화를 거쳐 컴파일이 진행된다.

 

C1과 C2 컴파일러로 컴파일된 코드는 모두 동일하게 코드 캐시에 저장된다.

 

Tiered Compliation

Tiered Compliation 과정은 0 ~ 4 Level로 구분된다.

 

  • Level 0 : Interpreted Code
    • 애플리케이션 실행 초기에는 모든 코드를 인터프리터를 통해 실행한다.
    • 당연히 컴파일 방식보다 성능이 낮다.
  • Level 1 : Simple C1 Compiled Code
    • 복잡도가 낮은 메서드를 대상으로 컴파일한다.
    • C2 컴파일러로 최적화 및 컴파일을 해도 유의미한 성능 향상이 기대되지 않는다.
    • 따라서 프로파일링도 진행하지 않는다.
  • Level 2 : Limited C1 Compiled Code
    • C2 컴파일러의 큐가 가득찬 경우 제한된 수준의 프로파일링과 최적화를 한다.
  • Level 3 : Full C1 Compiled Code
    • 일반적인 상황에서 수행되는 단계이다.
    • 최대 수준의 프로파일링과 최적화를 한다.
  • Level 4 : C2 Compiled Code
    • 장기적 성능 향상을 목적으로 C2 컴파일러가 최적화를 수행한다.
    • 이 단계에서 최적회된 코드는 완전한 최적화가 이루어져 프로파일링을 하지 않는다.

 

 

JVM Warm Up

Latency가 발생하는 두 가지 원인을 확인했으니 어떻게 해결할지는 단순하다.

 

  • 클래스 로더의 지연 로딩이 발생하지 않게 필요한 클래스들을 미리 사용해둔다.
  • C1, C2 컴파일러를 사용하기 위해 메서드를 미리 일정치 이상 호출해둔다.

 

결국 Warm Up은 후라이팬을 사용하기 이전에 예열을 하는 것처럼 JVM을 예열 시키는 방법이라고 볼 수 있다.

 

코드를 모두 실행해서 모든 클래스를 적재하고 메서드들도 예열시키기는 무리가 있기 때문에

자주 사용될 것으로 예상되는 부분들을 위주로 Warm Up을 진행해주면 된다.

 

각 컴파일러의 컴파일 임계치(Compile Threshold)는 C1은 1,500회, C2는 10,000회이다.

'Java > Notion' 카테고리의 다른 글

자바의 버전별 특징  (0) 2024.03.04
Garbage Collection 파헤치기  (0) 2024.03.03
JVM 뜯어보기 (컴파일, 인터프리팅, JIT, GC, 메모리)  (0) 2024.03.02
자바의 컴파일 과정  (0) 2023.10.25
BigInteger  (0) 2023.05.08
728x90

Garbage Collection

JVM의 Heap 영역 내에서 동적으로 할당했던 메모리 중 더 이상 사용하지 않는 메모리 객체들을 모아 주기적으로 자동으로 제거해주는 하나의 프로세스

 

개발자가 메모리를 직접 할당하고 해제하는 일에 신경을 쓸 필요가 없어져 개발에만 집중할 수 있게 해준다.

 

 

GC는 만능일까?

세상에 완벽한 것은 없기 때문에 당연히 단점이 존재한다.

 

우선 GC는 개발자가 제어할 수 있는 영역이 아니기 때문에 GC가 메모리를 언제 해제하는지 알 수 없으며,

GC가 동작하는 동안 관련 쓰레드를 제외한 모든 쓰레드가 멈추기(Stop The World) 때문에 오버헤드가 발생한다.

 

이런 STW 현상은 GC가 과하게 실행된다면 성능 하락으로 이어질 수도 있기 때문에

효율적으로 GC를 실행할 수 있게 최적화 하는 GC 튜닝 작업이 필요하다.

 

 

GC는 어떤 것들을 정리할까?

GC는 객체가 참조되어 접근할 수 있는지에 대한 도달성이라는 개념을 적용하여

객체의 레퍼런스가 존재하면 Reachable, 그렇지 않다면 Unreachable로 구분해 수거한다.

 

사실 당연한 이야기지만 참조가 되고 있지 않다면 앞으로 사용할 수 없는 객체이기 때문에

더 이상 필요 없는게 맞고 삭제하는게 당연하다.

 

 

GC의 청소 원리

청소는 Mark And Sweep 알고리즘으로 이루어진다.

 

청소할 객체를 식별한 후에 제거하면 제거된 공간이 빌 것이고

빈 공간을 앞에서부터 남아있는 객체들로 다시 채워나가는 방식으로 진행된다.

(Mark > Sweep > Compact)

 

우선 Mark 과정은 그래프 순회로 동작하기 때문에 그래프 탐색을 알고 있으면 이해하기 쉬운데

RootSpace부터 연결되어 갈 수 있는 모든 객체들까지 탐색하면서 마크를 해두면

연결되지 않은, 즉 참조되고 있지 않은 객체들은 마크가 되지 않기 때문에 삭제 대상이 될 것이다.

 

다음으로 Sweep 과정은 위에서 마크되지 않은 Unreachable 객체들을 Heap에서 제거한 후에

Compact 과정을 통해 삭제되지 않고 분산되어 떨어져 있는 객체들을 Heap의 시작주소로 모아 압축한다.

 

여기서 RootSpace는 Heap 영역을 참조하는 메서드 영역, 스태틱 변수, 스택, 네이티브 메서드 스택가 포함된다.

 

 

Heap 영역

GC이 대상인 Heap 영역은 Weak Generational Hypothesis를 이용해 설계되었는데

이는 대부분의 객체는 금방 Unreachable 상태가 되고, 오래된 객체에서 새로운 객체로의 참조는 적다는 점이다.

 

객체는 한 번 사용하기 위해 만들어지는 것이 대부분이고, 메모리에 오래 남아있지 않다는 것인데

Heap 영역은 이러한 관점에서 설계되 Young과 Old 영역으로 나누어 구성되었다.

 

 

Young Generation(Minor GC) / Old Generation(Major(Full) GC)

Young 영역은 새로 생성된 객체들이 할당 되고 대부분의 객체가 금방 Unreachable 상태가 되는 것처럼

대부분의 객체가 해당 영역에서 생성되었다가 사라지며, 이 영역에 대한 GC를 Minor GC라고 부른다.

 

Old 영역은 Young 영역에서 Reachable 상태를 오래 유지한 객체가 복사되는 영역으로

Young 영역보다 큰 영역이 할당된만큼 Garbage는 적게 발생하며, 해당 영역에 대한 GC를 Major GC라고 부른다.

 

대체로 수명이 짧은 객체들은 큰 공간을 사용하지 않기 때문에 수명이 긴 객체들이 저장되는 Old 영역이 더 크다.

 

 

Eden / Survivor0, 1

Young 영역은 Eden, Survivor0, Survivor1 세 가지 영역으로 다시 나누어진다.

 

Eden 영역은 new를 통해 새로 생성된 객체가 존재하며 이곳에서 정기적인 GC를 거친 후에

살아남은 객체들은 Survivor 영역으로 보내지게 된다.

 

Survivor 영역은 최소 1번 이상의 GC를 거친 후에 살아남은 객체가 존재하는 영역으로

0과 1 영역 중 하나는 무조건 비어 있어야 한다는 규칙이 있다.

 

힙 영역을 세분화하여 객체를 생존 기간별로 섬세하게 제어할 수 있어 GC를 정확하게 실행할 수 있게 된다.

 

 

Minor GC

Minor GC는 Old 영역에 비해 작은 Young 영역에서 객체를 찾고 제거하기 때문에 상대적으로 적은 시간이 소요된다.

 

우선 모든 객체들은 처음 생성될 때 Young 영역의 Eden 영역에 위치하게 되고, 해당 영역이 가득차면

Minor GC가 실행되어 Mark 과정을 통해 Reachable 객체를 Survivor 영역으로 이동시키고

Eden 영역의 Unreachable 객체의 메모리를 Sweep 하고 이러한 과정들은 반복한다.

 

이 때 살아남은 모든 객체들의 age를 1씩 증가시켜 GC에서 살아남은 횟수를 기록하게 되고

이 age 값이 임계값에 다다르면 Old 영역으로 이동하게 된다.

 

 

Major GC

Minor GC가 Eden 영역의 메모리가 부족할 때 실행되는 것처럼 Major GC도 Old 영역의 메모리가 부족하면 이루어진다.

 

객체의 age가 임계값에 이르러 Old 영역으로 이동하는 Promotion이 반복되서 진행되다

Old 영역의 메모리가 부족한 상황이 발생하면 Major GC가 실행되게 되고 참조되지 않는 객체들을 제거한다.

 

하지만 Old 영역에서의 GC는 큰 공간을 탐색하며 Unreachable 객체를 제거하기 때문에 오랜 시간이 소요되어

Stop The World 문제가 발생해 관련 없는 모든 쓰레드가 멈춘 후에 Mark and Sweep 작업이 수행된다.

 

 

'Java > Notion' 카테고리의 다른 글

자바의 버전별 특징  (0) 2024.03.04
JVM Warm Up  (0) 2024.03.04
JVM 뜯어보기 (컴파일, 인터프리팅, JIT, GC, 메모리)  (0) 2024.03.02
자바의 컴파일 과정  (0) 2023.10.25
BigInteger  (0) 2023.05.08
728x90

개요

우선 JVM 자체가 OS 위에서 자바 프로그램을 실행시켜 주는 역할을 수행해

운영체제에 독립적으로 실행할 수 있게 된다는 것은 당연히 알지만

JVM의 구성, 동작원리, 장단점 등에 대해 깊게 정리해보고자 한다.

 

컴파일 타입 환경에서의 자바 컴파일러

우리가 작성한 원시코드로 이루어진 java 파일은 JVM이 인식할 수 없기 때문에

자바 컴파일러가 java 파일을 자바 바이트코드로 이루어진 class 파일로 변환한다.

 

(java파일 > 어휘 분석 > 구문 분석 > 의미 분석 > 중간 코드 생성 후 최적화 = class 파일)

 

 

실제로 프로그램을 실행한 후에 java 파일들과 이름은 같지만 확장자가 다른

class 파일이 생성된 것을 확인할 수 있다.

 

cmd 창에서 직접 컴파일을 해봤으면 알 수도 있는데 그때 사용하는 javac 명령어는

JDK에 포함된 자바 컴파일러인 javac.exe를 의미한다.

 

이후 컴파일된 자바 바이트 코드를 JVM의 클래스 로더에 전달하게 된다.

 

런타임 환경에서의 JVM

클래스 로더는 동적로딩으로 필요한 클래스들을 로딩하고 링크하여

JVM의 메모리인 런타임 데이터 영역에 올리게 된다.

 

그 후 실행엔진이 메모리에 올라온 바이트 코드들을 명령어 단위로 가져와 실행하는데

이때, 인터프리터 방식과 JIT 컴파일러 방식을 사용한다.

 

클래스 로더의 세부 동작

클래스 로더는 로드 - 링크(검증 - 준비 - 분석) - 초기화 단계로 진행된다.

 

  1. 클래스 파일을 가져와 메모리에 로드
  2. 자바 언어 명세와 JVM 명세대로 구성되었는지 검증
  3. 필드, 메서드, 인터페이스 같이 클래스가 필요로 하는 메모리를 준비(할당)
  4. 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경(분석)
  5. 클래스 변수들을 적절한 값으로 초기화

 

자바 인터프리터

컴파일러는 클래스 파일 전체를 컴퓨터가 인식할 수 있는 이진 코드로 변환한다면

인터프리터는 런타임 시에 한 줄씩 읽으면서 이진 코드로 번역해서 실행한다.

 

추가적인 메모리를 사용하지 않고, 시스템의 이식성이 뛰어나며

코드가 수정되도 전체 코드를 다시 컴파일할 필요가 없다.

 

하지만 매번 인터프리팅을 거쳐야 하기 때문에 전체 실행 속도가 컴파일러에 비해 느리고

중간 코드로 변환되어 프로그램의 코드가 유출될 수 있다.

 

해당 방식을 기본으로 실행하며, 일정 기준이 넘어가면 JIT 컴파일 방식을 사용한다.

 

JIT 컴파일러

위의 인터프리터의 단점을 보완하기 위해 도입된 방식으로

바이트 코드 전체를 컴파일해 바이너리 코드로 변환해

매번 인터프리팅 하는 것이 아닌 바이너리 코드를 직접 실행한다.

 

한 번 컴파일만 완료되면 컴퓨터에서 빠르게 실행이 가능하고

기계어로 번역되어 코드 유출 위험이 적다.

 

하지만 코드가 수정된다면 컴파일을 다시 해야하고, 소스 파일 전체를 컴파일 해

용량이 크고, 모든 소스 파일을 번역하여 컴파일 시간이 비교적 느리며

목적 파일 생성을 위해 추가적인 메모리를 사용한다.

 

JVM의 구성

클래스 로더
메모리 영역 = [메서드 영역, 힙 영역, JVM 스택 영역, PC 레지스터, 네이티브 메서드 스택 영역]
실행 엔진 = [인터프리터, JIT 컴파일러, GC]
JNI
네이티브 메서드 라이브러리

 

JVM은 크게 클래스 로더와 메모리, 실행 엔진으로 구성되어 있으며

클래스 로더와 인터프리터, JIT 컴파일러는 위에서 살펴봤으니 나머지만 알아보겠다.

 

GC(Garbage Collection)

Heap 메모리 영역에서 더이상 사용하지 않는 메모리를 자동으로 회수해

C언어처럼 개발자가 메모리를 직접 관리할 필요가 없다.

 

일반적으로는 자동으로 실행되지만 실행되는 시간이 따로 정해져 있지는 않고

System.gc() 메서드를 사용해 수동으로도 가능하지만 실행이 보장되지도 않는다.

 

GC는 해당 게시글에 정리하기에는 내용이 적지 않은 편이라

추후에 따로 정리하겠다.

 

메모리 영역(런타임 데이터 영역)

자바 애플리케이션을 실행할 때 사용되는 데이터들이 적재되는 영역이다.

 

  • 메서드 영역
    • 클래스와 인터페이스의 런타임 상수 풀, 메서드와 필드, 클래스 변수, 메서드 바이트 코드 등이 적재
    • JVM 시작시 생성되어 프로그램 종료 시까지 존재
    • 런타임 상수 풀은 클래스와 인터페이스의 상수, 메서드와 필드에 대한 모든 레퍼런스를 저장하는 곳
    • JVM은 런타임 상수 풀에서 주소를 찾아 참조
    • GC 대상이 아니지만 명시적으로 null 선언시 GC 대상이 될 수 있음
    • 모든 쓰레드가 공유
  • 힙 영역
    • 프로그램 상에서 런타임 시 동적으로 할당해 데이터를 저장하는 영역
    • New 연산자로 생성된 인스턴스, 배열 같은 참조형 타입 저장
    • JVM이 직접 관리
    • 객체가 더 이상 사용되지 않거나, 명시적으로 null 선언시 GC 대상
    • 객체와 배열이 저장된 곳일뿐 참조 주소는 스택 영역에 있다.
    • 모든 쓰레드가 공유
  • 스택 영역
    • 선입후출 구조
    • 메서드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame 저장
    • 메서드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장
    • 스택 프레임(중괄호 블록)이나 메서드가 끝날 때 저장된 데이터들이 사라진다.
    • 각 쓰레드 마다 생성되는 개별 영역
  • PC 레지스터
    • 현재 실행 중인 JVM 명령어 주소를 가지고 있다.
    • CPU 명령어(instruction)를 수행한다
    • JVM의 리소스를 이용해 연산을 수행해야 하기 때문에
    • CPU가 직접 연산을 수행하는 것이 아닌 현재 작업 내용을 CPU에게 연산으로 제공
    • 이를 위한 버퍼 공간이라고 볼 수 있다.
    • 각 쓰레드 마다 생성되는 개별 영역
  • 네이티브 메서드 스택 영역
    • 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 (C/C++ 등)
    • 네이티브 메서드의 매개변수, 지역변수 등을 바이트 코드로 저장
    • 네이티브 인터페이스 호출 및 종료 시 생성
    • 각 쓰레드 마다 생성되는 개별 영역

 

JNI (Java Native Interface)

다른 언어로 만들어진 애플리케이션과 상호 작용할 수 있는 인터페이스를 제공한다

 

네이티브 메서드 라이브러리

다른 언어로 작성된 라이브러리를 칭하며 필요한 경우 JNI가 해당 라이브러리를 로딩한다.

'Java > Notion' 카테고리의 다른 글

JVM Warm Up  (0) 2024.03.04
Garbage Collection 파헤치기  (0) 2024.03.03
자바의 컴파일 과정  (0) 2023.10.25
BigInteger  (0) 2023.05.08
Optional<T>  (0) 2023.05.03
728x90

정의

N + 1 보다는 1 + N이라고 볼 수 있는데

한 개의 쿼리에 예상과는 다른 N개의 추가 쿼리가 발생하는 문제다.

 

원인

즉시로딩, 지연로딩, 연관 관계와 상관 없이 언제든 N + 1 문제가 발생할 수 있다

 

즉시로딩의 경우

N:M 관계에서 N을 조회하는 쿼리를 날렸을 때

M을 즉시로딩으로 가져오게 되어있다면

N을 조회할 때 M을 한 번의 쿼리만 날려서 조회할거라 예상할 수 있지만

JPQL은 N을 먼저 조회하는 쿼리를 날린 후에

M을 즉시로딩으로 가져오게 설정된걸 확인하고

M을 가져오는 추가 쿼리를 날리게 된다.

 

일대일 관계라면 쿼리가 한 번이 아닌 2번 발생할 것이고

일대다 관계라면 1 + M개만큼의 쿼리가 발생한다.

 

지연로딩의 경우

N:M 관계에서 N을 조회한 후에

지연로딩으로 가져온 M의 객체들을 사용하려는 경우에는

실제 객체가 아닌 프록시 객체가 존재하는 상태이기 때문에

실제 객체를 조회하기 위해 쿼리문이 M번 더 발생하게 된다.

 

 

해결 방법

확실한 정답은 없지만 가장 간단한 해결 방법은 패치 조인을 사용하는 것이다.

 

기본 패치 타입이 즉시로딩인 경우에는 모두 패치 타입을 지연로딩으로 바꾼 후에

연관된 엔티티를 사용해야하는 경우에는 애초에 패치 조인으로 한 번에 가져오는 것이다.

 

물론 이 방법도 문제점이 존재하는데

대표적인 경우가 페이징 처리시 발생하는 문제다.

 

예를 들어 1페이지 당 10개씩 끊어서 페이징을 처리한다 했을 때

100만개의 데이터가 있다면 이 데이터를 모두 메모리에 가져온 후에

페이징 처리를 하게 되기 때문에 데이터의 양이 적다면 메모리 초과가 발생하진 않겠지만

데이터가 많다면 무조건 메모리 초과가 발생할 것이다.

 

일대일, 다대일 관계라면 몇 개의 데이터를 조회할지 측정이 가능하지만

일대다, 다대다 관계라면 정확한 측정이 어렵다.

 

이런 문제를 해결하기 위해서는 배치 사이즈를 조절하는 방법이 있는데

배치는 지연 로딩을 생각하면 이해하기 쉽다.

 

배치 사이즈가 적용되면 같은 쿼리

 

예를 들어, 배치 사이즈가 100이라면 100만 개의 데이터 중에서 100개의 베치를 가져온 후에

이후의 배치가 필요한 경우에는 IN 쿼리를 한 번 더 날리게 된다.

 

사실 함정이 한 가지 있는데 하이버네이트가 최적화를 해두었기 때문에

배치 사이즈를 100으로 설정했다고 100개씩 끊어서 쿼리를 날리진 않는다.

100
100/2 = 50
50/2 = 25
25/2 = 12
1 ~ 10

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 25, 50, 100]

 

배치 사이즈가 100인 경우에는 위와 같이 미리 여러 개의 prepared statement를 만들어 두는데

기본값인 legacy 전략은 데이터보다 적은 수를, padded 전략은 많은 수를 쿼리로 가져온다.

 

예를 들어, 조회하려는 데이터가 총 32개라면 in(?*25)와 in(?*7) 두 개의 쿼리가 날라가고

padded 전략이라면 in(?*50) 한 개의 쿼리가 날라간다.

 

이렇게 최적화를 해주는 이유는 배치 사이즈가 100이라고 in절을 in(?*1) 부터 in(?*100)까지

다 만들어두는 것은 성능적으로 비효율적이기 때문이다.  

 

'Back-End > Spring' 카테고리의 다른 글

스프링 면접 질문 정리 : 오브젝트와 의존관계  (0) 2024.04.03
[로깅] MDC를 이용한 식별된 로깅 처리  (1) 2024.03.05
슬라이스 테스트  (0) 2023.07.05
예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14

+ Recent posts