반응형

Git 커밋 메시지 템플릿 만들어서 사용하는 방법 정리한 글입니다.

1. 템플릿 파일 생성

템플릿으로 사용할 파일을 작성합니다. (예: .gitmessage)

# {타입}: {항목} - {작업}, ex) fix: 미수관리 - 결제 유형 오류 수정
# 타입: feat | fix | docs | style | refactor | test | chore


# 커밋 본문 (선택) : 작업 상세 설명 또는 사유


# 푸터 (선택) : 주요 변경 사항 또는 Jira 이슈 ID 기록
# {키워드(해결|참조)}: {#Issue ID}, ex) 해결: #12345

2. 템플릿 파일 등록

git config에 템플릿 파일을 등록합니다.

git config --global commit.template {파일 경로 및 파일명}

.gitconfig 파일에서 템플릿 등록을 확인할 수 있습니다.

cat {사용자 Root}/.gitconfig

3. 커밋 메시지 작성

터미널이나 IDE에서 커밋 메시지를 입력합니다.

커밋 메시지 작성 후 저장하면 주석이 제거되고 커밋 메시지만 저장됩니다.

 

반응형
반응형

Node.js의 Nest 프레임워크를 기존의 물리적 서버 환경에 런칭하는 대신에
Serverless 환경(AWS Lambda)에 배포하는 간단한 예시를 정리하였습니다.

✍️ 여기서는 예시 앱을 신규로 만들고, Serverless 플러그인을 이용하여 AWS Lambda function으로 등록합니다.

1. Nest App 생성

Nest Framework로 신규 앱을 생성합니다.

nest new [app_name]

2. Serverless-cli 설치

CLI를 통해 Serverless 환경에 앱을 배포할 수 있는 Serverless framework을 설치합니다. (전역 설치)

npm install -g serverless

3. AWS credentials 생성

프로젝트 root 디렉토리에 .aws 디렉토리를 생성하고, credentials 파일을 생성합니다.

mkdir .aws
cd .aws
touch credentials

credentials 파일에는 aws credentials 정보를 입력합니다.

[default]
aws_access_key_id = #YOUR_ACCESS_KEY#
aws_secret_access_key = #YOUR_SECRET_ACCESS_KEY#

✍️ Git 리포지토리에 AWS credentials이 노출되는 것을 방지하기 위해 .gitignore.aws를 추가합니다.

4. AWS Lambda 패키지 설치

AWS Lambda 관련 패키지를 설치합니다.

npm install aws-serverless-express
npm install aws-lambda

5. Lambda Entry point 생성 

lambda.ts을 src 디렉토리에 생성합니다.

cd src
touch lambda.ts

Serverless 환경에서 Lambda를 빌드하기 위해 NestFactory를 이용하여 Entry point를 만듭니다.

/* lambda.ts */
import { Handler, Context } from 'aws-lambda';
import { Server } from 'http';
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';

import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';

const express = require('express');

const binaryMimeTypes: string[] = [];

let cachedServer: Server;

async function bootstrapServer(): Promise<Server> {
  if (!cachedServer) {
    const expressApp = express();
    const nestApp = await NestFactory.create(AppModule, new ExpressAdapter(expressApp));
    nestApp.use(eventContext());
    await nestApp.init();
    cachedServer = createServer(expressApp, undefined, binaryMimeTypes);
  }

  return cachedServer;
}

// The entry point of the Lambda function
export const handler: Handler = async (event: any, context: Context) => {
  cachedServer = await bootstrapServer();
  return proxy(cachedServer, event, context, 'PROMISE').promise;
}

6. Serverless 플러그인 설치

Serverless에서 TS를 실행하기 위한 플러그인,
Serverless 환경에서 실행 속도 향상을 위한 optimize 플러그인,
Serverless 에뮬레이터 플러그인을 설치합니다.

개발 모드로 설치 진행합니다.

npm install --save-dev serverless-plugin-typescript
npm install --save-dev serverless-plugin-optimize
npm install --save-dev serverless-offline

7. Serverless 설정

Serverless 환경에서 앱이 실행될 수 있도록 플러그인과 핸들러 정보 등을 설정한 serverless.yaml 파일을 프로젝트 root에 생성합니다.

service: com-sacle-api-prototype

plugins:
  - serverless-plugin-typescript
  - serverless-plugin-optimize
  - serverless-offline

provider:
  name: aws
  region: ap-northeast-2
  runtime: nodejs16.x

functions:
  main:
    handler: src/lambda.handler
    events:
      - http:
          method: any
          path: /{any+}

✍️ 여기서 service는 package.json 파일에서 name 속성 값과 동일해야 합니다. 점(.) 허용되지 않음

8. 로컬 테스트

로컬에서 앱이 정상적으로 실행되는지 확인해 봅니다.

sls offline start

9. Deploy

실제 Serverless 환경에 배포합니다.

sls deploy

AWS CloudFormation에 새로운 stack이 생성되었습니다.

S3에 새로운 Bucket이 생기고, zip 파일 등이 업로드 되었습니다.

Lambda function에도 새로운 함수가 생성되었습니다.

deploy 과정에서 console에 표시된 endpoint에 접속하면 응답을 확인할 수 있습니다.

🔗 관련 링크

https://blog.theodo.com/2019/06/deploy-a-nestjs-app-in-5-minutes-with-serverless-framework/

 

Deploy a NestJS App with Serverless Framework

Here in Theodo we are very enthusiastic about NestJS framework. It is quite young but we consider it currently one of the best NodeJS…

blog.theodo.com

https://www.serverless.com/framework/docs/providers/aws/guide/serverless.yml/#provider

 

Serverless Framework - AWS Lambda Guide - Serverless.yml Reference

The Serverless Framework documentation for AWS Lambda, API Gateway, EventBridge, DynamoDB and much more.

www.serverless.com

 

반응형
반응형

정적 리소스 배포 시 브라우저의 캐시로 인해 변경된 내용이 반영되지 않는 이슈를 해결하기 위해
정적 리소스의 Versioning 방법을 정리하였습니다.

🤔 기존의 문제점

js, css 등 정적 리소스에 오늘 일자를 쿼리 스트링으로 추가하여 매일 새로운 파일을 다운로드 받도록 하고 있었습니다.

<script src="/r/v2/js/assets/common/common.js?ver=${useDate}"></script>

이 방식의 문제점은 일별로 쿼리 스트링이 변경 되므로 정적 리소스가 변경되지 않아도 매일 새로운 파일을 다운로드 받게 되며,
새로운 정적 리소스를 배포할 경우 이미 동일 일자에 접속한 이력이 있으면 변경된 정적 리소스를 다운로드 받지 못 하게 된다는 점입니다.

🔎 해결 방법

1) 파일명에 해시 값 추가

정적 리소스 파일명의 파일의 해시 값을 추가하여, 파일이 변경 되었을 때 새로운 해시 값이 부여되어 새로 다운로드 받을 수 있습니다.

예) <link href="/css/spring.css"/> ➡️ <link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

spring.resources.chain.strategy.content.enabled=true
spring.resources.chain.strategy.content.paths=/**

2) 버전 고정 값 사용

파일명의 교체 대신에 고정된 버전 값을 설정하고 이 값으로 가상의 디렉토리를 생성하여 위치하도록 resolve 하고
버전 값을 변경하면 디렉토리 명이 바뀌어 새로 다운로드 받을 수 있도록 합니다.

예) "/js/lib/mymodule.js" ➡️ "/v12/js/lib/mymodule.js"

spring.resources.chain.strategy.fixed.enabled=true
spring.resources.chain.strategy.fixed.paths=/js/lib/
spring.resources.chain.strategy.fixed.version=v12

위의 방법은 Thymeleaf와 FreeMarker에서는 Spring Boot의 기본 ResourceUrlEncodingFilter가 자동으로 처리해 주지만,
일반적인 JSP 사용 시에는 별도의 ResourceUrlProvider를 사용해야 합니다.

🛠️ 개선된 해결책

JSP를 기본 템플릿으로 사용하는 환경에서는 별도의 ResourceUrlProvider 의 선언 및 추가 설정이 필요합니다.

properties 설정은 하지 않아도 됩니다.

1. ResourceHandler 추가

WebMvcConfigurer에 Version 전략 Resource handler를 추가합니다.

1) Hash 값

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/r/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}

2) 고정 버전 관리

sacle.resources.chain.strategy.fixed.version=202212_01
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${sacle.resources.chain.strategy.fixed.version}")
    String resourcesChainStrategyFixedVersion;
    
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/r/**")
                .addResourceLocations("classpath:/static/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addFixedVersionStrategy(resourcesChainStrategyFixedVersion, "/**"));
    }
}

 

2. JSP 접근 허용

JSP에 정적 리소스에 접근할 수 있는 @ModelAttribute 를 추가합니다.

@ControllerAdvice
public class ResourceUrlAdvice {

    private final ResourceUrlProvider resourceUrlProvider;

    public ResourceUrlAdvice(ResourceUrlProvider resourceUrlProvider) {
        this.resourceUrlProvider = resourceUrlProvider;
    }

    @ModelAttribute("urls")
    public ResourceUrlProvider urls() {
        return this.resourceUrlProvider;
    }
}

3. JSP 적용

실제 적용할 정적 리소스의 URL 부분을 위에서 설정한 @ModelAttribute 를 이용하여 수정합니다.

기존

<script src="/r/v2/js/assets/common/common.js?ver=${useDate}"></script>
<script src="/r/v2/js/assets/common/commonApi.js?ver=${useDate}"></script>

변경

<script src="${urls.getForLookupPath('/r/v2/js/assets/common/common.js')}"></script>
<script src="${urls.getForLookupPath('/r/v2/js/assets/common/commonApi.js')}"></script>

※ 바꾸기 정규표현식
원본: "([A-Za-z0-9/.-]+)?ver=${useDate}"
변환: "${urls.getForLookupPath('$1')}"

🔬 반영 결과

개발자 도구로 정적 리소스 버저닝 확인

🔗  참고 링크

반응형
반응형

※ 본 게시물은 네이버 테크 블로그와 2015년에 출간된 〖네이버를 만든 기술, 읽으면서 배운다 - 자바 편〗 중 05. JVM 이해하기 챕터를 정리한 내용입니다.

 

JVM의 특징

  • 스택 기반의 가상 머신 : x86이나 ARM 아키텍처가 레지스터 기반인데 비해 JVM은 스택 기반으로 작동한다.
  • 심벌릭 레퍼런스 : Primitive data type을 제외한 모든 타입(클래스와 인터페이스)을 메모리 주소 기반이 아닌, 심벌릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션 : 인스턴스는 코드에 의해 명시적으로 생성되고 GC에 의해 자동 파괴된다.
  • 기본 자료형을 명확하게 정의해 플랫폼 독립성 보장 : 전통적인 언어(C/C++)는 플랫폼에 따라 기본 자료형의 크기가 달라지나, JVM은 기본 자료형을 명확하게 정의해 호환성을 유지하고 플랫폼 독립성을 보장한다.
  • 네트워크 바이트 순서(network byte order) : x86의 리틀 엔디안이나 RISC 아키텍처의 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 순서를 유지해야 하므로 네트워크로 데이터를 전송할 때 사용하는 네트워크 바이트 순서를 사용한다. 네트워크 바이트 순서는 빅 엔디안이다.

 

자바 바이트코드

  • 자바 컴파일러는 고수준 언어를 JVM이 이해하는 자바 바이트코드로 번역한다.
  • 자바 바이트코드는 플랫폼 의존적인 코드가 없기 때문에 JVM이 설치된 장비라면 CPU나 운영체제에 상관 없이 실행할 수 있다.
  • 컴파일 결과물의 크기가 소스 코드와 크게 다르지 않기 때문에 네트워크로 전송하여 실행하기 쉽다.
  • 클래스 파일 자체는 바이너리 파일이어서, javap라는 역어셈블러(diassembler)를 제공한다. (결과물을 자바 어셈블리라고 부른다.) 
  • 어셈블리 형식 
    • index : 각 메서드 기준으로 바이트 오프셋
    • opcode : 명령어 OpCode. 1바이트의 바이트 번호로 표현된다. (aload_0=0x2a, getfield=0xb4, invokevirtual=0xb6, ...) 최대 256개
    • operandN : 피연산자 (0개 이상). getfield, invokevirtual 등은 2바이트의 피연산자가 필요하다.
    • comment : 주석 (라인 마지막에 위치)
<index> <opcode> [ <operand1> [ <operand2>... ]] [<comment>]

 

  • 자바 바이트코드의 명령어는 1바이트의 OpCode(최대 256개)와 2바이트 피연산자(Operand)로 분리할 수 있다.
  • 메서드를 호출하는 명령어 OpCode는 다음의 4가지가 있다.
    • invokeinterface : 인터페이스 메서드 호출
    • invokespecial : 생성자, private 메서드, 슈퍼 클래스의 메서드 호출
    • invokestatic : static 메서드 호출
    • invokevirtual : 인스턴스 메서드 호출

 

클래스 파일

ClassFile {
 u4 magic;
 u2 minor_version;
 u2 major_version;
 u2 constant_pool_count;
 cp_info constant_pool[constant_pool_count-1];
 u2 access_flags;
 u2 this_class;
 u2 super_class;
 u2 interfaces_count;
 u2 interfaces[interfaces_count];
 u2 fields_count;
 field_info fields[fields_count];
 u2 methods_count;
 method_info methods[methods_count];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

 

  • 클래스 파일 포맷
    • magic : 자바 클래스 파일 구별을 위한 매직 넘버 (0xCAFEBABE)
    • minor_version, major_version : 클래스 파일의 버전 지정
    • constant_pool_count : constant_pool의 개수 + 1
    • constant_pool[] : 런타임 상수 풀 영역에 들어갈 정보(문자열 상수, 클래스명, 인터페이스명, 필드명, 기타 상수)
    • access_flags : 클래스의 변경자 정보(public, final, abstract, enum 등) 또는 인터페이스 여부를 나타내는 플래그
    • this_class, super_class : 각각 this, super 클래스에 대한 constant_pool 내 인덱스
    • interfaces_count, interfaces[] : 클래스가 구현한 인터페이스의 개수와 인터페이스에 대한 constant_pool 내 인덱스
    • fields_count, fields[] : 클래스의 필드 개수와 필드 정보(필드 이름, 타입 정보, modifier, constant_pool 내 인덱스 등)
    • methods_count, methods[] : 클래스의 메서드 개수와 메서드 정보(메서드 이름, 파라미터 타입과 개수, 반환 타입, modifier, constant_pool 내 인덱스, 메서드 자체 실행 코드, 예외 정보 등)
    • attributes_count, attributes[] : attribute의 개수와 attribute_info 구조체
  • javap -verbose 명령으로 클래스 파일 포맷을 사용자가 읽을 수 있는 형태로 간략하게 보여준다.

 

JVM 구조

  • 자바로 작성한 코드는 클래스 로더가 컴파일된 자바 바이트코드를 런타임 데이터 영역에 로드하고, 실행엔진이 자바 바이트코드를 실행한다.

클래스 로더

  • 자바는 동적 로드, 즉 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크한다. 이 동적 로드를 담당하는 부분이 클래스 로더이다.
  • 클래스 로더의 특징
    • 계층 구조 : 클래스 로더끼리 부모-자식 관계를 이룬다. 최상위 클래스 로더는 부트스트랩 클래스 로더이다.
    • 위임 모델 : 클래스 로더끼리 로드를 위임하는 구조로 작동한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인해 상위에 있으면 해당 클래스를 사용하고 없다면 요청 받은 클래스 로더가 클래스를 로드한다.
    • 가시성 제한 : 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만 상위는 하위의 클래스를 찾을 수 없다.
    • 언로드 불가 : 클래스 로더는 클래스를 로드할 수는 있지만 언로드 할 수는 없다. 대신 현재 클래스 로더를 삭제하고 새로운 클래스 로더를 생성할 수 있다.
  • 클래스 로더가 클래스 로드를 요청받으면, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고 없으면 상위로 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더까지 확인해도 없으면 요청 받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.
    • 부트스트랩 클래스 로더(bootstrap class loader) : JVM 가동할 때 생성되며 Object 클래스를 비롯해 자바 API를 로드한다. 자바가 아닌 네이티브 코드로 구현돼 있다.
    • 확장 클래스 로더(extension class loader) : 기본 자바 API를 제외한 확장 클래스를 로드한다. (다양한 보안 확장 기능)
    • 시스템 클래스 로더(system class loader) : 애플리케이션의 클래스를 로드한다. 사용자가 지정한 CLASSPATH 내 클래스를 로드한다.
    • 사용자 정의 클래스 로더(user-defined class loader) : 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.
  • WAS와 같은 컨테이너는 웹 애플리케이션과 엔터프라이즈 애플리케이션이 서로 독립적으로 작동하도록 사용자 정의 클래스 로더를 사용하여, 애플리케이션의 독립성을 보장한다.
  • 클래스 로드 단계
    • 로드(loading) : 클래스 파일을 JVM 메모리에 로드한다.
    • 검증(verifying) : 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성됐는지 검사한다.
    • 준비(preparing) : 클래스가 필요로 하는 메모리를 할당하고 클래스에 정의된 필드, 메서드, 인터페이스를 나타내는 데이터 구조를 준비한다.
    • 분석(resolving) : 클래스의 상수 풀 내 모든 심벌릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
    • 초기화(initializing) : 클래스 변수를 적절한 값으로 초기화한다. static 필드를 설정된 값으로 초기화 한다.

 

런타임 데이터 영역

  • 런타임 데이터 영역은 JVM이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다.

  • PC(Program Counter) 레지스터 : 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 현재 실행 중인 JVM 명령의 주소가 저장된다.
  • JVM 스택 : 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임이라는 구조체를 저장하는 스택으로, JVM은 JVM에 스택 프레임을 추가하고 꺼내는 작업만 실행한다. 예외 발생 시 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.
    • 스택 프레임 : 메서드가 실행될 때마다 하나의 스택 프레임이 생성돼 해당 스레드의 JVM 스택에 push되고 메서드가 종료되면 스택 프레임이 pop 된다. 각 스택 프레임은 지역 변수 배열, 피연산자 스택, 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다.
    • 지역 변수 배열 : 인덱스 0은 메소드가 속한 클래스 인스턴스의 this 레퍼런스이고, 인덱스 1부터는 메서드에 전달된 파라미터, 메서드 파라미터 이후에는 메서드의 지역 변수가 저장된다.
    • 피연산자 스택 : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 push 하거나 pop 한다.
  • 네이티브 메서드 스택 : JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 실행하기 위한 스택이다.
  • 메서드 영역 : 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. 각 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 제조사마다 다양한 형태로 구현할 수 있으며, 오라클 핫스폿 VM에서는 Permanent Generation(Perm Gen)이라고 한다.
  • 런타임 상수 풀 : 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. JVM 작동에서 가장 핵심적인 역할을 실행하는 곳이다. 각 클래스와 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메로리 상의 주소를 찾아서 참조한다.
  • : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 이슈에서 가장 많이 언급되는 공간이다.

 

실행 엔진

  • JVM 내 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 바이트코드를 명령어 단위로 읽어 실행한다.
  • 실행 엔진은 자바 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경한다.
    • 인터프리터 : 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 해석은 빠른 대신 결과의 실행은 느리다. 바이트코드는 기본적으로 인터프리터 방식으로 작동한다.
    • JIT (Just-In-Time) 컴파일러 : 인터프리터 방식으로 실행하다 적절한 시점에 바이트코드를 컴파일해 네이티브 코드로 변경하고 이후 네이티브 코드로 직접 실행하는 방식이다. 인터프리팅 방식보다 빠르고 네이티브 코드를 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 실행된다.

 

JIT 컴파일러

  • JIT 컴파일러는 바이트코드를 중간 단계의 표현인 IR (Intermediate representation)로 변환해 최적화하고, 그 다음에 네이티브 코드를 생성한다.

  • 오라클 핫스폿 VM은 HotSpot 컴파일러라 불리는 JIT 컴파일러를 사용한다. 내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분 HotSpot을 찾아낸 다음, 이 HotSpot을 네이티브 컴파일한다. HotSpot VM은 컴파일된 바이트코드라도 자주 호출되지 않으면 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터 모드로 작동한다.
  • IBM JVM은 JIT 컴파일러 뿐만 아니라 AOT(Ahead-On-Time) 컴파일러를 도입했다. 이미 컴파일된 네이티브 코드를 여러 JVM이 공유 캐시를 통해 공유한다. AOT 컴파일러를 통해 컴파일된 코드는 다른 JVM에서 컴파일하지 않고 사용할 수 있다. 또한 AOT 컴파일러를 통해 JXE(Java Executable) 파일 포맷으로 프리컴파일된 코드를 작성해 빠르게 실행하는 방법도 제공한다.
  • 자바 성능 개선의 많은 부분은 이 실행 엔진을 개선해 이뤄지고 있다.

 

출처

  • 강경태, 강운덕, 구태진, 김민수, 김택수, 박세훈, 송기선, 이상민, 정상혁, 최동순, 〖네이버를 만든 기술, 읽으면서 배운다 - 자바 편〗, 위키북스(2015) p.123 - 150
  • 네이버 D2 테크 블로그, JVM Internal https://d2.naver.com/helloworld/1230

 

 

반응형

+ Recent posts