좋은 질문입니다! 하나씩 정리해드리겠습니다.

1. TCP 버퍼의 위치

두 곳에 모두 존재합니다!

Client ←→ [TCP버퍼 30MB] ←→ Apache ←→ [TCP버퍼 30MB] ←→ Tomcat

각 연결마다 독립적인 TCP 버퍼가 있습니다.

2. 버퍼가 차면 보내는가? ❌

아니요! 버퍼가 가득 찰 때까지 기다리지 않습니다.

실제 동작 (스트리밍 방식)

Tomcat → Apache → Client
  |         |
 8KB 읽음 → 즉시 전달 시작
 8KB 읽음 → 즉시 전달
 8KB 읽음 → 즉시 전달
(계속 흐름...)

TCP 버퍼는:

  • "창고"가 아닌 "파이프"처럼 동작
  • 데이터가 들어오면 즉시 꺼내서 전달
  • 가득 차는 건 네트워크가 느릴 때 임시로 쌓이는 것

3. 300MB를 11번에 나눠서? ❌

아니요! 지속적으로 스트리밍됩니다.

300MB 파일 다운로드:

❌ 잘못된 이해:
30MB 읽고 → 전송 완료 기다림 → 30MB 읽고 → ... (11번 반복)

✅ 실제 동작:
8KB → 8KB → 8KB → 8KB → ... (계속 스트리밍)
파이프처럼 끊임없이 흐름

실제 흐름 예시

Tomcat 파일 읽기:
  ├─ 8KB chunk 읽음
  └─ Tomcat→Apache TCP 버퍼에 쓰기

Apache:
  ├─ TCP 버퍼에서 데이터 읽음 (즉시)
  └─ Apache→Client TCP 버퍼에 쓰기 (즉시)

Client:
  └─ TCP 버퍼에서 데이터 읽음 (즉시)

이 과정이 파일이 끝날 때까지 계속 반복

TCP 버퍼 30MB의 의미

  • "한 번에 30MB씩 전송"이 아님
  • "네트워크가 느릴 때 최대 30MB까지 임시 저장 가능"
  • 정상 상황에서는 훨씬 적게 사용하며 계속 흐름

정리: 300MB 파일은 11번이 아닌, 작은 청크들이 끊임없이 흐르는 스트림으로 전송됩니다!

정확합니다! 완벽하게 이해하셨습니다! 👍

TCP 버퍼의 본질

TCP 버퍼 = 네트워크 속도 차이를 흡수하는 임시 대기 공간

실제 동작 예시

상황 1: 네트워크가 빠를 때

Tomcat → [버퍼: 거의 비어있음] → Apache → [버퍼: 거의 비어있음] → Client

데이터가 들어오자마자 바로 나감
버퍼는 몇 KB만 사용

상황 2: 클라이언트 네트워크가 느릴 때

Tomcat (빠름) → Apache → [버퍼: 점점 쌓임 ↑] → Client (느림)

Tomcat이 계속 데이터를 보내는데
Client가 천천히 받으니까
Apache→Client 버퍼에 데이터가 쌓임
최대 30MB까지 쌓일 수 있음

상황 3: 버퍼가 가득 차면?

Apache→Client 버퍼: [████████████] 30MB 가득!

→ Apache가 Tomcat에게: "잠깐 멈춰!" (TCP Flow Control)
→ Tomcat 전송 일시 중지
→ Client가 데이터 받아서 버퍼 공간 생기면
→ Tomcat 다시 전송 재개

비유

수도꼭지(Tomcat) → 물탱크(버퍼 30MB) → 컵(Client)

❌ 물탱크를 가득 채운 후 한번에 붓는다
✅ 물이 계속 흐르고, 탱크는 넘치지 않게 조절하는 역할

정리: 30MB는 "한 번에 보내는 양"이 아니라 "혼잡할 때 대기할 수 있는 최대 공간"입니다!


네, 맞습니다! 각 TCP 연결마다 양쪽 끝에 버퍼가 존재합니다.

정확한 버퍼 위치

다운로드 시나리오 (Tomcat → Apache → Client)

Tomcat                Apache                 Client
  |                     |                      |
[송신버퍼] ---------> [수신버퍼]              |
  30MB                  30MB                  |
                        |                      |
                   [송신버퍼] -----------> [수신버퍼]
                      30MB                   30MB

데이터 흐름

1. Tomcat → Apache 구간:
   Tomcat [송신버퍼 30MB] ⟹ Apache [수신버퍼 30MB]

2. Apache → Client 구간:
   Apache [송신버퍼 30MB] ⟹ Client [수신버퍼 30MB]

실제로는 4개 버퍼

다운로드할 때:

Tomcat 쪽:
 └─ 송신 버퍼 (최대 30MB) - Tomcat이 Apache로 보내는 데이터 대기

Apache 쪽:
 ├─ 수신 버퍼 (최대 30MB) - Tomcat으로부터 받는 데이터 대기
 └─ 송신 버퍼 (최대 30MB) - Client로 보내는 데이터 대기

Client 쪽:
 └─ 수신 버퍼 (최대 30MB) - Apache로부터 받는 데이터 대기

혼잡 시나리오

Client가 매우 느릴 때:

Tomcat → Apache [수신: 거의 비어있음]
         Apache [송신: ████ 28MB 쌓임!] → Client (느림)

Apache→Client 구간이 병목

정리: 각 연결(Client↔Apache, Apache↔Tomcat)의 양쪽 끝에 각각 송신/수신 버퍼가 존재합니다!

좋은 질문입니다! 각 구간별로 정리해드리겠습니다.

1. OS 레벨 TCP 버퍼 (기본 설정)

Linux 시스템 전역 설정:

# 설정 파일 위치
/etc/sysctl.conf

# TCP 수신 버퍼 (최소/기본/최대)
net.ipv4.tcp_rmem = 4096    87380    6291456
                    4KB     85KB     6MB (기본)

# TCP 송신 버퍼 (최소/기본/최대)
net.ipv4.tcp_wmem = 4096    16384    4194304
                    4KB     16KB     4MB (기본)

확인 방법:

sysctl net.ipv4.tcp_rmem
sysctl net.ipv4.tcp_wmem

적용 방법:

# 즉시 적용
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 16384 16777216"

# 영구 적용
echo "net.ipv4.tcp_rmem = 4096 87380 16777216" >> /etc/sysctl.conf
echo "net.ipv4.tcp_wmem = 4096 16384 16777216" >> /etc/sysctl.conf
sysctl -p

2. Apache 설정

설정 파일 위치:

# httpd.conf 또는 apache2.conf

# Tomcat으로 프록시할 때 수신 버퍼 크기
ProxyReceiveBufferSize 0    # 0 = OS 기본값 사용 (권장)
ProxyReceiveBufferSize 131072  # 128KB로 명시 지정

# 송신은 OS 설정 따름

기본값: 0 (OS 설정 사용)

3. Tomcat 설정

설정 파일 위치:

<!-- server.xml -->

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           socket.rxBufSize="0"   <!-- 수신 버퍼, 0 = OS 기본값 -->
           socket.txBufSize="0"   <!-- 송신 버퍼, 0 = OS 기본값 -->
           />

기본값: 설정하지 않으면 OS 기본값 사용

4. Client (브라우저/다운로드 매니저)

Client의 TCP 버퍼도 OS 설정 따름

  • Windows: 레지스트리 설정
  • Linux: 위의 sysctl 설정
  • Mac: sysctl 설정

각 구간별 정리

┌─────────────────────────────────────────────────┐
│ Client 머신                                      │
│ TCP 버퍼: OS 설정 (tcp_rmem/tcp_wmem)           │
└─────────────────────────────────────────────────┘
                    ↕
┌─────────────────────────────────────────────────┐
│ Apache 서버                                      │
│ 수신: ProxyReceiveBufferSize (기본: 0=OS설정)    │
│ 송신: OS 설정 (tcp_wmem)                        │
└─────────────────────────────────────────────────┘
                    ↕
┌─────────────────────────────────────────────────┐
│ Tomcat 서버                                      │
│ TCP 버퍼: socket.rxBufSize/txBufSize (기본: OS) │
│ 또는 OS 설정 (tcp_rmem/tcp_wmem)                │
└─────────────────────────────────────────────────┘

일반적인 권장 설정

대부분의 경우:

  • Apache: ProxyReceiveBufferSize 0 (기본값 사용)
  • Tomcat: 별도 설정 안함 (기본값 사용)
  • OS: 기본값으로도 충분

고속 대용량 전송이 필요한 경우:

# OS 레벨에서만 조정
net.ipv4.tcp_rmem = 4096 87380 16777216  # 최대 16MB
net.ipv4.tcp_wmem = 4096 16384 16777216  # 최대 16MB

핵심: 대부분은 OS 레벨 설정을 따르며, 애플리케이션에서 명시적으로 지정하지 않는 한 OS 기본값을 사용합니다!


물리다운로드가 10초이상 걸리는경우에 scout 같은 apm 에서는 훨씬 빨리 끝난것처럼 보이는 착시?? 현상은 뭐야?

아주 좋은 질문입니다! 이건 APM이 "애플리케이션 처리 시간"만 측정하기 때문입니다.

APM이 측정하는 시점

@GetMapping("/download")
public void download(HttpServletResponse response) throws IOException {
    File file = new File("/path/to/300mb.zip");

    response.setContentType("application/zip");
    response.setHeader("Content-Length", String.valueOf(file.length()));

    // ← APM 측정 시작

    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {

        byte[] buffer = new byte[8192];
        int bytesRead;

        while ((bytesRead = in.read(buffer)) != -1) {
            out.write(buffer, 0, bytesRead);
            out.flush();
        }
    }

    // ← APM 측정 종료 (여기서 끝난 것으로 기록!)
    // 하지만 실제로는...
}

// 실제 전송은 계속 진행 중:
// Tomcat → Apache → Client (10초 이상 소요)

실제 타임라인

시간 →

0초: [요청 도착]
     └─ APM 측정 시작

0.1초: [컨트롤러 로직 실행]
       └─ 파일 찾기, 헤더 설정

0.2초: [첫 데이터 청크를 response.getOutputStream()에 쓰기 시작]
       └─ APM "처리 완료!" (0.2초로 기록)
       └─ Scout: ✅ 200ms

       ⚠️ 하지만 실제로는...

0.3초: 데이터 계속 전송 중...
1초:   데이터 계속 전송 중...
5초:   데이터 계속 전송 중...
10초:  데이터 계속 전송 중...
12초: [전송 완료] ← 실제 다운로드 완료
      └─ 스레드 해제

Scout APM이 보는 것 vs 실제

Scout APM:

GET /download/file.zip
응답 시간: 200ms ✅
상태: 200 OK

실제 서버:

GET /download/file.zip
- 애플리케이션 처리: 200ms
- 네트워크 전송: 12초  ← APM이 측정 안 함!
- 스레드 점유: 12초    ← 진짜 문제는 여기!

왜 이런 차이가 발생하나?

APM은 애플리케이션 코드의 성능을 측정:

APM 측정 범위:
┌─────────────────────────────┐
│ 컨트롤러 진입                │
│ 비즈니스 로직 실행           │
│ response.write() 첫 호출     │ ← 여기서 끝
└─────────────────────────────┘

APM이 측정 안 하는 범위:
└─ 네트워크 전송 (12초)
└─ 클라이언트 수신 (느린 네트워크)

스레드 점유 시간을 제대로 보려면?

1. 서버 로그에 직접 기록

@GetMapping("/download")
public void download(HttpServletResponse response) {
    long startTime = System.currentTimeMillis();

    try {
        // ... 파일 전송
    } finally {
        long duration = System.currentTimeMillis() - startTime;
        log.info("실제 전송 시간: {}ms", duration);  // 12000ms
    }
}

2. Tomcat Access Log 확인

<!-- server.xml -->
<Valve className="org.apache.catalina.valves.AccessLogValve"
       pattern="%h %l %u %t &quot;%r&quot; %s %b %D"
       />
       <!-- %D = 응답 시간 (밀리초) -->
192.168.1.100 - - [14/Nov/2025:10:00:00 +0900] "GET /download/file.zip HTTP/1.1" 200 314572800 12543
                                                                                                ^^^^^ 
                                                                                            실제 12.5초

3. Apache Access Log

LogFormat "%h %l %u %t \"%r\" %>s %b %D" combined
# %D = 마이크로초 단위 응답 시간

정리

Scout APM: "컨트롤러가 200ms 만에 응답했어요!" ✅
실제 서버: "스레드를 12초간 점유했어요..." ⚠️

→ 착시 현상의 원인: APM은 애플리케이션 로직만 측정
→ 실제 문제: 네트워크 전송 시간 + 스레드 점유

대용량 파일 다운로드는 APM이 빠르다고 해도, 실제로는 스레드를 오래 점유하므로 Apache/Nginx 같은 웹 서버가 직접 서빙하는 게 훨씬 효율적입니다!

젠킨스 버전업이 이루어지면서 내부적으로 jdk 11 을 이용하게 되었다.

이때 젠킨스를 이용한 빌드시에 사용하는 remote 패키지가 11로 컴파일된거라 구버전의 젠킨스에서 빌드하던 jdk 8버전의 프로젝트들은 빌드시 remote.jar(jdk 11) 실행에 실패하면서 빌드가 실패나버린다.(내용은 버전 맞지 않음)

 

그럼 어떻게 해야할까?

 

우선 젠킨스자체의 메이븐 빌드 로 아이템을 만들지 말고, 프리스타일로 만든뒤

 

쉘로 직접 빌드하게 해주면되긴한다. 

 

쉘환경이 jdk 8 이면된다.

좀더 설명하면

 

1. jenkins build job 생성시 메이븐이아니라 프리스타일로 생성

2. 생성시 만들어진 이름으로, 빌드실행하면 젠킨스의 빌드노드 홈의, workspace 디렉토리 이하에 그 빌드아이템의 방이 생김

3. 해서... 우선 스크립트(execute shell) 는 비워두고 git 등의 정보를 입력하여 item 을 만들고, 그 item 을 빌드하면 생성한 item 이름으로 디렉토리가 생김. 

4. 그 디렉토리안에 보면 git 의 내용을 clone 해온걸 볼 수 있음

5. 여기서 단순히 jar 빌드이면 mvn 명령으로 빌드시킬수 있도록 직접 테스트 해보고 빌드 잘 되는 쉘 명령어를 item 의 execute shell 에 넣어주면됨.

6. 여기서 만약, 빌드 노드에 settings.xml 이 없다면 원래는 젠킨스의 아이템 생성시 메이븐 설정에 provided settings.xml 항목에 임의의 xml 을 정의해서 그걸 빌드에 사용하게 하는데, 우리는 쉘에서 하는거라 직접해줘야한다.

7. 해서 간단히, 해당 git 안에 그 프로젝트에 맞는 settingsl.xml 을 만들고 거기에 설정 넣어둔뒤 execute shell 실행시 실행될 mvn 옵션으로 -s settings.xml   처럼 맞는 xml 을 넣어주면된다.

 

 

전에는 maven 으로 kotlin , java 를 섞어서 프로젝트를 만들기도 했다. (최초에만 그랬고 그뒤부턴 코틀린만으로 된 프로젝트를 만들어왔긴했다)

 

다만 virtual thread 가 나온이후 java 21 에 대한 관심이 올라가면서... 그리고 테스트 목적으로 하나의 프로젝트만 만들어서 테스트해볼때 문법확인상  java, kotlin 둘다되는 프로젝트를 설정해보려고 찾아봤다  gradle , kotlin dsl  환경으로 했을때, ... 매우 간단했다;;

 

기본적은 springboot는 springboot init (kotlin, gradle kotlin dsl , jar )을 사용해서 만들었다. 거기에 한줄만 추가한 내용이다.

 

plugins 에 한줄만 추가하고  kotlin 과 동등한 패키지 레벨로 java 디렉토리(패키지)만 만들어주면되는거였다.

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "3.2.3"
    id("io.spring.dependency-management") version "1.1.4"
    java
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.spring") version "1.9.22"
    kotlin("plugin.jpa") version "1.9.22"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-actuator")
    implementation("org.springframework.boot:spring-boot-starter-batch")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
//    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
//    implementation("org.springframework.boot:spring-boot-starter-data-redis")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.apache.kafka:kafka-streams")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3")
    implementation("org.springframework.kafka:spring-kafka")
    runtimeOnly("com.h2database:h2")
    runtimeOnly("com.mysql:mysql-connector-j")
    runtimeOnly("com.oracle.database.jdbc:ojdbc11")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3")
    testImplementation("org.springframework.batch:spring-batch-test")
    testImplementation("org.springframework.kafka:spring-kafka-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

 

그래들이 지원되지 않아서 하이라이팅이 되진 않겠지만 최초 spring init 으로 생성후 한줄만 더해줬다. 위에서 언급한것 같이 plugins 에 java 한줄만 넣었다. 이게 다였다. 만약 java 의 버전 관련 수정이 필요하다면 java { ... 이하 부분에서 수정하면된다. 아무튼 매우.. 간단했다. 

 

tree 구조는 아래와 같다

➜  springbootdemo git:(main) tree -I build
.
├── HELP.md
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── springbootdemo
    │   │               └── controller
    │   │                   └── TestControllerJava.java
    │   ├── kotlin
    │   │   └── com
    │   │       └── example
    │   │           └── springbootdemo
    │   │               ├── SpringbootdemoApplication.kt
    │   │               └── controller
    │   │                   └── TestControllerKotlin.kt
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    │           └── hello.html
    └── test
        ├── java
        │   └── com
        │       └── example
        │           └── springbootdemo
        │               └── TestJava01.java
        └── kotlin
            └── com
                └── example
                    └── springbootdemo
                        ├── SpringbootdemoApplicationTests.kt
                        └── Test01.kt

아.. jenkins 의 파이프라인에서 환경변수 변경해서 ... 실행하는 방법이 있었는데;;;

 

바로 이럴때! chatgpt 에게 물어봤음  2024. 2. 27. 매우 간단하게 리마인드 가능했다.

 

 

Jenkins 파이프라인 스크립트에서 실행하는 콘솔의 환경 변수를 변경하려면, withEnv 스텝을 사용할 수 있습니다. withEnv 스텝은 특정 블록의 실행 동안 환경 변수를 임시로 설정하거나 수정할 수 있게 해줍니다. 이는 주로 steps 블록 내부에서 사용됩니다.

예를 들어, JAVA_HOME 환경 변수의 값을 변경하고 싶다면, 다음과 같이 withEnv 블록을 사용하여 해당 환경 변수에 새 값을 할당할 수 있습니다:

pipeline {
    agent any
    stages {
        stage('Set JAVA_HOME') {
            steps {
                // withEnv를 사용하여 JAVA_HOME 환경 변수 설정
                withEnv(["JAVA_HOME=/new/path/to/java"]) {
                    // 쉘 스크립트 또는 배치 스크립트를 실행하여 새 JAVA_HOME을 사용
                    sh 'echo $JAVA_HOME'
                    // 또는 Windows 배치 스크립트를 사용하는 경우
                    // bat 'echo %JAVA_HOME%'
                }
            }
        }
    }
}

 

이 예제에서, withEnv 블록 내부에서 JAVA_HOME 환경 변수는 /new/path/to/java로 설정됩니다. 이 변경은 withEnv 블록 내부에서만 유효하며, 블록을 벗어나면 원래 값(또는 설정되지 않은 경우)으로 복원됩니다.

sh 스텝은 Unix 기반 시스템(예: Linux, macOS)에서 쉘 스크립트를 실행할 때 사용되며, bat 스텝은 Windows 기반 시스템에서 배치 스크립트를 실행할 때 사용됩니다. 사용하는 Jenkins 노드의 운영 체제에 따라 적절한 스텝을 선택하세요.

withEnv를 사용하면 파이프라인의 특정 부분에서만 환경 변수를 변경할 수 있어, 파이프라인의 다른 부분에는 영향을 주지 않으면서 유연하게 환경을 제어할 수 있습니다.

docker 의 이미지를 실행할때 ... 몇가지 옵션을 줘야했다. 결과적으로만 말하자면

 

docker run -it -p 3301:3306 -e MYSQL_ROOT_PASSWORD=root mysql

-p 

아웃:인 포트값이다. 즉 mysql 이 3306을 열고 있고 host 머신은 3301로 접근하는거다. 해서 3301로 접근해서 도커의 3306으로 접속한다. 

 

-e 

환경변수를 주는거다. 그냥 mysql (여기서는 8.1) 을 실행하면 필수옵션이 없어서 실행 실패가 뜬다.

    You need to specify one of the following:
    - MYSQL_ROOT_PASSWORD
    - MYSQL_ALLOW_EMPTY_PASSWORD
    - MYSQL_RANDOM_ROOT_PASSWORD

이경우는 위와같이 암호를 그냥 설정했다. 아니면 두번째 암호를 비워둠을 허용하는걸로 퉁쳐도 될듯한데... 아무튼

 

-it

이건 컨테이너의 output 을 그대로 찍어주는거다. 전면에 뜨는데, 이걸 백그라운드로 (데몬) 처리하세 화면에 아무것도 안나오게 하려면 d 옵션도 함께 주면된다. 즉 -dit

 

 

여기까지는 도커의 실행이고 dbeaver 에서 접근시 에러가 난다. 내용은 without ssl 인경우 allow 해줘야 하는 옵션이 있는데 그걸 커넥션 설정에서 안해서 그렇다는거다.

 

이 화면에서 allowPublicKeyRetrieval 을 true 로 해주면된다. 

git 사용중에 갑자기 editor 연결시 저런 에러가 난다면???

editor 를 다시 설정하는데 정확히는

 

git config --global core.editor vim -f

 

-f 옵션으로 vim 을 사용해라; 

 

https://stackoverflow.com/questions/22699614/git-commit-messages-lost-by-vi

 

Git commit messages lost by vi

I'm a clumsy typist, and I don't use vi/vim very often, but I do use it for commit messages. However, if you type a wrong command while editing a commit message (:Wq, say, instead of :wq), when you

stackoverflow.com

 

.. 내 블로그인데 글쓰기가 없다고?

솔직히 나도 이래서 글쓰기 있는 스킨만 썼는데.. 바보 같은짓이었다;;; 검색한번만 해볼껄;

 

'q' .. Q 키를 누르면 관리화면으로 간다; 거기서 글쓰기하면된다 orz

맞다.. 사라졌다.

 

https://dev.to/abhinavd26/openlens-deprecated-logs-shell-k91

 

OpenLens Deprecated Logs & Shell

OpenLens is the open-source project behind one of the most adopted Kubernetes IDE, Lens. In simple...

dev.to

 

대안은? 

1. extension 기능으로 추가 가능하다. (글의 아래에 보면 @alebcay/openlens-node-pod-menu 가 있고 이 텍스트 그대로 openslens 메뉴의 extension 을 선택후 설치 url? 란에 넣으면 된다)

2. 해당글에서 소개하는 devtron 을 이용한다.

 

우선은 1번으로 하긴하는데... 다른 대안을 찾아야 할지도...

+ Recent posts