1. 신입이 '대용량 트래픽 경험'을 대체 어떻게 해요

백엔드 개발자를 꿈꾸며 채용공고를 스크롤해 본 적이 있다면, 유독 눈에 밟히는 문구를 본 기억이 있을 겁니다.

"대용량 트래픽 처리 경험 우대"

신입이나 주니어 개발자에게 이보다 더 막막한 말이 또 있을까요? 이제 막 서버를 만들고 API를 개발하는 법을 배웠는데, 경험해보지도 못한 '대용량 트래픽'이라니요. 경험을 쌓으러 왔는데, 경험이 없어서 안 된다니!

 

"신입은 대체 어디서 대용량 트래픽을 경험하나요?"

이 질문은 많은 주니어 개발자들의 솔직한 심정일 겁니다. 하지만 잠시 관점을 바꿔 생각해봅시다. 기업은 왜 이 경험을 그토록 중요하게 생각할까요? 단순히 대단한 서비스를 개발한 중고신입을 원하는걸까요?

아닙니다. 기업이 '대용량 트래픽 경험'을 통해 진짜 확인하고 싶은 것은 지원자의 숨은 '역량' 입니다.

  1. 시스템의 성능 한계를 인지하고,
  2. 문제가 발생했을 때 병목을 찾아 분석하며,
  3. 이를 개선해 본 경험

그렇다면 이야기는 달라집니다. 실제 수만, 수백만 사용자가 있는 서비스는 경험하지 못했더라도, 그 핵심 '역량'을 기르고 증명할 방법이 있다면 어떨까요?

여기에 대한 가장 확실하고 전문적인 해답이 바로 부하 테스트(Load Testing) 입니다.

2. 부하 테스트, 제대로 알아보기

부하 테스트란 무엇인가? (What)

부하 테스트란, 시스템이 예상되는 최대 부하(Peak Load) 조건에서 안정적으로 성능 목표를 달성하는지 검증하는 활동을 말합니다.

쉬운 비유를 들어볼까요? 여러분이 인기가 많아질 레스토랑의 셰프라고 상상해봅시다. '부하 테스트'는 평일 점심시간처럼 가장 바쁠 것으로 예상되는 시간에 손님(트래픽)들이 몰려와도, 주문이 밀리지 않고(성능 저하 없이) 음식을 제시간에 제공할 수 있는지(성능 목표 달성) 미리 시험해보는 것과 같습니다.

왜 부하 테스트를 해야 하는가? (Why)

부하 테스트를 통해 우리는 다음과 같은 가치를 얻을 수 있습니다.

  • 성능 병목 사전 식별: 서비스 오픈 후 사용자들이 불편을 겪기 전에, 우리 시스템의 어떤 부분이 가장 먼저 느려질지 미리 찾아낼 수 있습니다.
  • 시스템 안정성 확보: "이 정도 사용자까지는 문제없어!"라는 데이터 기반의 확신을 가질 수 있습니다.
  • 용량 계획의 근거 마련: 서버를 몇 대나 증설해야 할지 감에 의존하는 것이 아니라, 실제 테스트 결과를 바탕으로 합리적인 결정을 내릴 수 있습니다.

이것이 바로 기업이 '대용량 트래픽 경험'을 통해 지원자에게 기대하는 능력, 즉 문제를 예측하고, 데이터에 기반해 판단하는 능력과 정확히 일치합니다.

3. 성공적인 부하 테스트의 3가지 핵심 요소

전문적인 부하 테스트는 단순히 '툴을 돌려보는 것'이 아닙니다. 성공적인 테스트를 위해서는 다음 3가지 핵심 요소를 반드시 고려해야 합니다.

핵심 요소 1: 명확한 목표(KPI) 설정

"얼마나 버텨야 성공인가?"를 정의하는 과정입니다. 목표 없는 테스트는 의미 없는 숫자의 나열일 뿐입니다. 우리는 비즈니스 요구사항을 기술적인 목표, 즉 **핵심 성과 지표(KPI, Key Performance Indicator)**로 변환해야 합니다.

주니어 개발자가 부하 테스트 시 반드시 확인해야 할 3대 KPI는 다음과 같습니다.

  1. 처리량 (Throughput / RPS): '초당 요청 처리 수(Requests Per Second)'를 의미합니다. 시스템이 시간당 얼마나 많은 작업을 처리할 수 있는지 나타내는 순수한 성능 지표입니다. (목표 예: 초당 100개의 요청을 처리해야 한다.)
  2. 응답 시간 (Response Time): 요청을 보낸 후 응답을 받기까지 걸리는 시간입니다. 특히 p95, p99 응답 시간을 주목해야 합니다. 이는 '전체 요청 중 95%, 99%가 이 시간 안에 응답했다'는 의미로, 일부 사용자가 겪는 극단적인 느림 현상(Tail Latency)을 잡아내는 데 중요합니다. (목표 예: 모든 요청의 95%는 200ms 안에 응답해야 한다.)
  3. 에러율 (Error Rate): 전체 요청 중 서버 에러(HTTP 5xx 등)가 발생한 비율입니다. 이 수치는 당연히 0%에 가까워야 합니다.

핵심 요소 2: 현실적인 시나리오 설계

"어떻게 실제 사용자를 흉내 낼 것인가?"를 고민하는 단계입니다. 실제 사용자는 한 가지 행동만 반복하지 않습니다.

예를 들어 쇼핑몰이라면, 단순히 상품 조회 API만 호출하는 것은 현실적이지 않습니다. '로그인 → 상품 검색 → 상세 페이지 조회 → 장바구니 담기'와 같은 **실제 사용자 여정(User Journey)**을 반영해야 신뢰도 높은 결과를 얻을 수 있습니다. 처음에는 간단한 시나리오부터 시작하더라도, 최종적으로는 실제 사용자와 유사한 패턴을 만들어가는 것이 중요합니다.

핵심 요소 3: 적절한 도구 선택과 결과 분석

이제 목표와 시나리오를 실행할 도구를 선택할 차례입니다. 세상에는 여러 좋은 부하 테스트 도구가 있으며, 각 도구는 저마다의 철학과 장단점을 가지고 있습니다.

도구 스크립트 언어 주요 특징 추천 대상

JMeter GUI (XML 기반) 강력한 GUI와 폭넓은 프로토콜 지원, 방대한 커뮤니티 QA 전문가 또는 GUI 환경을 선호하는 팀
nGrinder Groovy, Jython 네이버에서 만든 올인원 플랫폼, 웹 UI 기반의 편리한 테스트 관리 테스트 관리를 위한 통합 플랫폼이 필요한 팀
k6 JavaScript (ES6) 개발자 친화적, 'Test as Code' 철학, 매우 적은 리소스로 높은 부하 생성 개발자 중심, CI/CD 파이프라인 연동이 중요한 팀

k6는 현대적인 JavaScript로 테스트 코드를 작성할 수 있어 프론트엔드/백엔드 개발자 모두에게 친숙하고, 'Test as Code' 철학을 통해 테스트 스크립트를 애플리케이션 코드처럼 버전 관리하고 리뷰할 수 있다는 큰 장점이 있습니다.

k6를 활용한 부하 테스트란 이런 거구나를 간단하게 보여드리도록 하겠습니다.

아래는 제가 실제로 사용한 k6 test script입니다. 이전에 서울시내 시위 현황을 한 눈에 보여주고, 유저와 상호작용할 수 있는 서비스인 ‘주변시위 Now’를 개발했을 때 사용했던 코드입니다.

import http from 'k6/http';
import {check, sleep} from 'k6';
import {Counter, Rate, Trend} from 'k6/metrics';

// 테스트 설정 변수
const CONFIG = {
    BASE_URL: `https://${__ENV.API_HOST}`,
    PROTEST_IDS: [1, 2, 3, 4],
    POLLING_INTERVAL: 3000,
    CHEER_REQUEST_PER_SECOND: 3, // 1초에 3번 응원 요청

    // 테스트 단계 설정
    STAGES: [
        {duration: '1m', target: 100},
        {duration: '2m', target: 600},
        {duration: '1m', target: 0},
    ]
}

// 커스텀 메트릭 정의
const cheerCallCounter = new Counter('cheer_calls');
const cheerResponseTime = new Trend('cheer_response_time');
...

// 성공/실패 카운트를 위한 Rate 메트릭 추가
const cheerSuccessRate = new Rate('cheer_success_rate');
const cheerFailRate = new Rate('cheer_fail_rate');
...

export const options = {
    scenarios: {
        rest_api_test: {
            executor: 'ramping-vus',
            startVUs: 1,
            stages: CONFIG.STAGES,
            gracefulRampDown: '30s',
        }
    }
};

let lastPollTime = 0;

export default function () {
    // 1초에 3번의 POST 요청을 수행하기 위한 반복문
    for (let i = 0; i < CONFIG.CHEER_REQUEST_PER_SECOND; i++) {
        const protestId = CONFIG.PROTEST_IDS[Math.floor(Math.random() * CONFIG.PROTEST_IDS.length)];

        // 응원 요청 파라미터 설정
        const params = {
            timeout: '5s', // 요청 타임아웃 설정
            tags: {name: 'cheer-api'} // 요청 태깅
        };

        // 1. POST 요청으로 응원하기
        try {
            let cheerRes = http.post(`${CONFIG.BASE_URL}/api/cheer/protest/${protestId}`, null, params);

            // POST 요청 메트릭 기록
            cheerCallCounter.add(1);
            cheerResponseTime.add(cheerRes.timings.duration);

            // 응답 검증
            const isSuccess = check(cheerRes, {
                'POST 응원 요청 성공': (r) => r.status === 200,
                '응원 응답에 cheerCount 포함': (r) => {
                    try {
                        const body = JSON.parse(r.body);
                        return body.data && body.data.cheerCount !== undefined;
                    } catch (e) {
                        return false;
                    }
                },
                '응답 시간 1000ms 이내': (r) => r.timings.duration < 1000
            });

            // 성공/실패 카운트 업데이트
            cheerSuccessRate.add(isSuccess);
            cheerFailRate.add(!isSuccess);
        } catch (e) {
            cheerFailRate.add(1);
        }

        // 1초를 3등분하여 각 요청 사이에 간격을 둠 (마지막 요청 후에는 쉬지 않음)
        if (i < CONFIG.CHEER_REQUEST_PER_SECOND - 1) {
            sleep(1 / CONFIG.CHEER_REQUEST_PER_SECOND);
        }
    }

    ...
    }
}

아주 간단한 테스트 시나리오로서, sns의 ‘좋아요’ 같은 버튼을 수많은 유저가 난타하는 상황을 가정했습니다.

터미널에서 k6 run test.js 명령을 실행합니다. 그러면 아래와 같은 결과를 확인할 수 있습니다.

  • 처리량 (RPS): http_reqs 라인의 11.17./s를 통해 초당 약 11개의 요청을 처리했음을 알 수 있습니다.
  • 응답 시간 (p95): http_req_duration 라인의 p(95)=2.46s를 통해 요청의 95%가 2.46s 안에 응답했음을 확인할 수 있습니다.
  • 에러율: http_req_failed 라인의 0.00%를 통해 실패한 요청이 없었음을 알 수 있습니다.

이 세 가지 지표만 제대로 읽을 수 있어도, 내 시스템의 현재 성능 상태를 파악하는 첫걸음을 성공적으로 뗀 것입니다.

(위 3가지 필수 지표 외에도, 하드웨어 지표 확인 또한 매우 중요합니다!)

4. 결과 분석, CS 기본기가 빛을 발하는 순간

테스트를 실행하고 결과를 얻었다면, 이제부터가 진짜입니다. 결과 데이터를 보고 "왜?"라는 질문을 던지며 원인을 파고드는 과정, 즉 이때부터가 바로 여러분이 갈고닦은 CS 기본기가 단순한 '지식'을 넘어, 문제를 해결하는 '무기'가 되는 순간입니다.

부하 테스트는 단순히 툴을 사용하는 기술이 아니라, 시스템의 동작 원리를 이해하는 종합 예술에 가깝습니다.

🕵🏻‍♂️ 느린 응답의 진짜 범인(병목) 추적하기

가장 흔한 시나리오를 가정해봅시다.

"테스트 결과, 응답 시간(p95)은 목표치보다 훨씬 느린데, 서버의 CPU 사용률은 20%로 매우 낮습니다. 범인은 누구일까요?"

이때 CS 지식이 있는 개발자는 다음과 같이 생각할 수 있습니다.

"CPU가 놀고 있다는 건, 애플리케이션 자체가 복잡한 계산을 하느라 바쁜 게 아니라는 뜻이다. 그렇다면 애플리케이션이 무언가를 하염없이 '기다리는' 상태, 즉 I/O Bound 상황일 확률이 높다."

부하 테스트와 CS 지식의 연결고리

이처럼 병목을 추론하는 과정은 여러분이 공부해 온 CS 지식과 직접적으로 연결됩니다.

  • 원인 1: 데이터베이스 (I/O Bound)
    • 의심: 비효율적인 쿼리로 인해 DB가 응답을 늦게 주고 있을 가능성이 가장 높습니다.
    • 관련 CS 지식: 데이터베이스 (인덱스, 실행 계획, N+1 문제), 네트워크 (애플리케이션-DB 간 네트워크 지연)
    • 다음 행동: APM(Application Performance Monitoring) 툴로 느린 쿼리를 특정하거나, 슬로우 쿼리 로그를 확인해 볼 수 있습니다.
  • 원인 2: 외부 API 호출 (I/O Bound)
    • 의심: 우리 서비스가 의존하는 외부 서비스(결제 API, 소셜 로그인 API 등)가 느리게 응답하고 있을 수 있습니다.
    • 관련 CS 지식: 네트워크 (HTTP 통신, DNS 조회, TCP Handshake)
    • 다음 행동: 외부 API 호출 부분의 타임아웃을 측정하고, 해당 서비스의 상태 페이지를 확인해 볼 수 있습니다.
  • 반대의 경우: CPU 사용률이 100%에 달할 때 (CPU Bound)
    • 의심: 애플리케이션 내부 로직이 매우 복잡하거나, 비효율적인 연산을 반복하고 있을 수 있습니다.
    • 관련 CS 지식: 자료구조/알고리즘 (비효율적인 알고리즘 사용), 운영체제/JVM (과도한 스레드 경쟁, 잦은 GC 발생)
    • 다음 행동: 코드 프로파일러를 사용하여 어떤 함수가 CPU를 많이 사용하는지 분석해 볼 수 있습니다.

이처럼 부하 테스트 결과 분석은 여러분의 CS 기본기를 실제 문제 해결에 적용하고 증명할 수 있는 좋은 경험입니다.

5. 실전 대비, 부하 테스트 면접 질문 리스트

여러분이 이 글을 통해 학습한 내용을 바탕으로, 면접관이 부하 테스트와 관련하여 어떤 질문을 할 수 있는지 알아봅시다. 이 질문들에 스스로 답해보며 배운 내용을 정리해보세요.

문제 해결/CS 연계 심층 질문

  1. "부하 테스트를 진행했는데, 서버의 CPU 사용률은 매우 낮은데 응답 시간은 길게 나왔습니다. 이 현상의 가장 유력한 원인은 무엇이라고 생각하며, 그 이유는 무엇인가요?"
  2. 💡 답변 Tip: 이 현상이 대표적인 'I/O Bound' 상황임을 설명하세요. 애플리케이션 스레드가 CPU를 사용해 연산하는 대신, 데이터베이스나 외부 API 같은 외부 자원의 응답을 기다리느라 '대기(WAITING/BLOCKED)' 상태에 빠져있기 때문에 CPU 사용률은 낮게 나타난다고 설명하며 OS 지식을 어필할 수 있습니다.
  3. "지난 문제의 원인을 데이터베이스에서의 병목이라고 가정해봅시다. DB 병목 현상을 좀 더 구체적으로 진단하기 위해 어떤 지표들을, 어떤 순서로 확인하시겠어요?"
  4. 💡 답변 Tip: 체계적인 접근법을 보여주는 것이 중요합니다. 예를 들어, (1) APM 툴을 통해 가장 느린 쿼리가 무엇인지 특정합니다. (2) 해당 쿼리의 실행 계획(Execution Plan)을 분석하여 인덱스 누락이나 풀 테이블 스캔(Full Table Scan) 여부를 확인합니다. (3) DB 서버의 CPU, 디스크 I/O, 활성 커넥션 수 등 리소스 상태를 확인합니다. (4) 마지막으로 애플리케이션의 커넥션 풀 상태를 점검합니다. 와 같이 논리적인 단계를 제시하세요.
  5. "분석 결과, 특정 API에서 N+1 쿼리 문제가 발생하고 있음을 발견했습니다. 이 문제를 어떻게 해결할 것이며, 해결되었는지 여부는 어떤 방법으로 '정량적'으로 검증하시겠습니까?"
  6. 💡 답변 Tip: 문제 해결 능력과 검증 능력 모두를 보여줘야 합니다. (1) 해결: JPA 환경이라면 Fetch Join을, 아니라면 쿼리를 수정하여 연관된 데이터를 한 번에 가져오도록 개선하겠다고 답변합니다. (2) 검증: "동일한 부하 테스트 시나리오를 다시 실행하여, ① 목표 API의 p95 응답 시간이 목표치 이내로 개선되었는지, ② 전체 처리량(RPS)이 유의미하게 증가했는지, ③ APM에서 해당 로직의 쿼리 실행 횟수가 실제로 1회로 줄었는지를 수치로 확인하겠습니다." 와 같이 데이터 기반의 검증 계획을 구체적으로 제시하는 것이 핵심입니다.

6. 에필로그: 경험을 넘어, '역량'을 갖춘 개발자로

프롤로그에서 던졌던 질문을 다시 떠올려봅시다. "신입은 어디서 대용량 트래픽을 경험하나요?"

오늘 우리는 그 질문에 대한 답을 찾았습니다. 경험은 주어지는 것이 아니라, 스스로 만드는 것입니다. 여러분은 오늘 이 글을 통해 직접 트래픽을 만들고, 시스템의 성능을 측정하고, 그 결과를 분석하는 방법을 배웠습니다. 단순히 '부하 테스트를 해봤다'는 경험을 넘어, 시스템의 성능을 책임지고 개선할 수 있는 '역량' 의 첫 단추를 꿴 것입니다.

'대용량 트래픽 경험'이라는 단어에 더 이상 주눅 들지 마세요. 대신 부하 테스트를 통해 여러분의 서버를 더 깊이 이해하고, 성능 병목의 근본 원리를 파고들며, 데이터에 기반해 문제를 해결하는 진짜 엔지니어로 성장해나가길 응원합니다.

이러한 고민을 담은 기술면접 문제집을 만들었습니다

단순한 암기형 부하 테스트 질문은 인터넷에서 쉽게 찾을 수 있습니다. 하지만 실제 성능 문제 상황에서 체계적으로 원인을 분석하고 해결하는 능력을 기르기 위해서는 더 깊이 있는 연습이 필요합니다.

이 글에서 다룬 **"부하 테스트 결과 분석 → 병목 진단 → 개선 → 검증"**의 전체 사이클을 실제 면접 상황에서 논리적으로 설명할 수 있도록, AI 기반 평가 시스템과 함께하는 문제집을 준비했습니다.

CPU Bound vs I/O Bound 진단, 데이터베이스 병목 분석, N+1 문제 해결과 정량적 검증 등 실무에서 자주 마주하는 성능 문제들을 직접 분석해보며, 여러분이 얼마나 준비됐는지 확인해보세요!

 

문제집: 

https://cs-master.vercel.app/problem-sets/20

 

성능 분석과 병목 해결: 부하 테스트를 통한 시스템 최적화

부하 테스트를 실행하는 것은 시작일 뿐입니다. 진짜 실력은 결과를 분석해서 병목을 찾고, 원인을 규명하며, 개선까지 완료하는 데서 나타납니다. 단순한 부하 테스트 도구 사용법을 넘어, 실

cs-master.vercel.app

 

 

기술면접 단골 질문


"TCP/IP 4계층에 대해서 설명해주세요."

이 질문은 웹 백엔드 개발자 기술면접에서 빠지지 않는 단골 질문입니다. 대부분의 개발자들은 이렇게 답변합니다.

 

"TCP/IP 4계층 모델은 현대 인터넷의 동작을 논리적으로 나누어 설명하는 데 사용됩니다. 각 계층에는 고유한 역할이 있습니다.
응용 계층은 HTTP나 DNS 같은 애플리케이션 프로토콜을 다루고 사용자의 요청/응답을 처리합니다.
전송 계층은 TCP/UDP를 통해 신뢰성 있는 데이터 전달과 순서 제어를 담당하며, 흐름 제어와 오류 검출/복구를 수행합니다.
인터넷 계층은 IP 프로토콜을 통해 주소 지정과 라우팅을 담당하여 패킷이 여러 네트워크를 거쳐 목적지까지 전달되도록 합니다.
네트워크 계층은 이더넷 같은 로컬 네트워크 기술과 물리 매체 인터페이스를 포함하며, 상위 계층의 데이터를 실제 네트워크 망을 통해 전달합니다."

 

TCP/IP 모델은 왜 계층으로 나누어져 있나요?

이러한 고민을 해보신 적이 있으신가요? 사용자가 웹사이트에 접속할 때, 단순히 "데이터를 보내고 받는 것"인데 굳이 이렇게 복잡하게 여러 단계로 나누어야 할 이유가 있을까요?

브라우저 → [복잡한 4계층 과정] → 서버

기술적으로는 모든 네트워크 기능을 하나의 거대한 시스템으로 통합해서 만들 수도 있을 텐데 말이죠.

이 글에서는 웹 환경에서 데이터 전송 과정을 계층으로 나누어야 하는 실질적인 이유와 실무에서의 활용을 깊이 있게 살펴보겠습니다.

소프트웨어 공학 관점에서 본 계층화

크래프톤 정글 과정 중, PintOS 프로젝트 직전에 들었던 카이스트 교수님의 운영체제 특강에서 인상 깊었던 말이 있습니다.


"개발이란, 복잡한 문제를 작은 단위로 나누고(problem decomposition), 각 단위를 추상화된 계층으로 구성(set layers of abstraction)하는 과정이다"

이는 소프트웨어 공학의 핵심 원칙인 모듈화(Modularity)와 추상화(Abstraction)를 설명하는 말입니다.

모듈화는 복잡한 시스템을 독립적인 기능 단위로 나누는 것이고, 추상화는 각 모듈의 내부 구현은 숨기고 외부에는 단순한 인터페이스만 제공하는 것입니다. 이 두 개념이 소프트웨어 개발에서 가져다주는 장점은 명확합니다:

  • 개발 복잡도 감소: 전체 시스템을 한 번에 이해할 필요 없이, 각 모듈에 집중할 수 있음
  • 재사용성 향상: 잘 정의된 인터페이스를 가진 모듈은 다른 시스템에서도 활용 가능
  • 유지보수성 개선: 특정 모듈의 변경이 다른 모듈에 영향을 주지 않음
  • 병렬 개발 가능: 각 모듈을 독립적으로 개발할 수 있어 개발 효율성 증대

데이터 전송, 생각보다 복잡한 문제

그런데 네트워크를 통한 데이터 전송이 얼마나 복잡한 문제인지 생각해본 적이 있나요?

단순히 "A에서 B로 데이터를 보낸다"고 하지만, 실제로는 이런 복잡한 고려사항들이 있습니다:

  • 어떤 형식으로 데이터를 주고받을지 (HTTP, FTP, DNS 등)
  • 신뢰성 있게 전달하려면 어떻게 할지 (순서 보장, 에러 검출, 재전송)
  • 어떤 경로로 데이터를 라우팅할지 (수많은 네트워크를 거쳐서)
  • 물리적으로 어떤 매체를 통해 전달할지 (이더넷, WiFi, 광케이블)

이 모든 문제를 하나의 거대한 시스템으로 해결하려고 한다면, 복잡도가 기하급수적으로 증가할 것입니다. TCP/IP 4계층은 바로 이 문제를 모듈화와 추상화 원칙을 적용해 해결한 결과입니다:

  • Application Layer: 응용 프로그램 간 통신 방식
  • Transport Layer: 신뢰성 있는 데이터 전달
  • Internet Layer: 네트워크 간 라우팅
  • Network Access Layer: 물리적 데이터 전송

다음으로는 네트워크를 통한 데이터 전송을 여러 계층으로 나눔으로써 얻을 수 있는 장점에 대해 살펴보겠습니다.

1. 재사용성과 호환성

각 계층이 명확한 인터페이스로 분리되어 있어, 특정 계층의 구현을 바꾸어도 다른 계층에는 영향을 주지 않습니다.

같은 웹 애플리케이션이 다양한 환경에서 동작 가능:

HTTP 요청 (Application Layer)
├── TCP (Transport Layer) + 이더넷 (Network Access)
├── TCP (Transport Layer) + WiFi (Network Access)
├── QUIC (Transport Layer) + 이더넷 (Network Access)
└── QUIC (Transport Layer) + 5G (Network Access)

웹 개발자는 HTTP 요청을 보낼 때 하위 계층이 TCP인지 QUIC인지, 이더넷인지 WiFi인지 전혀 신경 쓸 필요가 없습니다. Application Layer의 관점에서는 단순히 "데이터를 보내줘"라고 요청하기만 하면, 하위 계층들이 각자의 역할에 맞게 알아서 처리해줍니다.

2. 문제 격리와 디버깅 용이성

각 계층이 독립적인 책임을 가지고 있어, 문제가 발생했을 때 어느 계층에서 발생한 문제인지 체계적으로 추적할 수 있습니다.

네트워크 문제는 본질적으로 복잡합니다. 하지만 계층별로 나누어져 있기 때문에, 우리는 다음과 같이 순차적으로 접근할 수 있습니다:

웹사이트가 열리지 않을 때 문제 파악 방법 예시:

4층 (Application): HTTP 응답 코드는 정상인가? (200, 404, 500 등)
3층 (Transport): TCP 연결은 성공했는가?
2층 (Internet): IP 라우팅 경로에 문제는 없는가?
1층 (Network Access): 물리적 네트워크 연결은 되어 있는가?

만약 계층이 나누어져 있지 않았다면, 이 모든 가능성을 동시에 고려해야 해서 문제의 원인을 찾는 것이 훨씬 어려웠을 것입니다. 독립된 모듈화 덕분에 거대한 "네트워크 문제"를 구체적인 "특정 계층의 특정 문제"로 좁혀나갈 수 있습니다.

 

실무에서의 계층별 디버깅 접근법

TCP/IP 4 Layer 각각에 대해 잘 이해해하고 있으면 실무에서 어떤 점이 좋을까요?

특정한 에러 코드 없이, 서비스 접속이 안되는 장애상황이 발생했다고 가정해봅시다. 서비스 장애의 원인이 한번에 특정 대상으로 좁혀지면 참 좋겠지만, 실제로는 원인을 찾기도 어려운 ‘블랙박스’ 상황도 충분히 일어날 수 있습니다. 이러한 상황은 특정 계층을 먼저 의심할 단서가 없으므로, 모든 가능성을 열어두고 논리적인 순서에 따라 하나씩 배제해 나가는 Top-Down 방식이 적절할 수 있습니다. TCP/IP 4계층을 위에서부터 한 단계씩 내려가보면서 디버깅하는 것이죠. 계층별 디버깅 방법에는 수많은 방식이 있지만, 그 중 대표적인 예시만 짚고 넘어가겠습니다.

계층별 디버깅 방법론

Layer 4. 애플리케이션 계층 (Application Layer) - 서비스 동작 확인

사용자에게 서비스를 제공하는 애플리케이션과 관련 프로토콜(HTTP, DNS 등)의 정상 동작 여부를 확인합니다. 여기서 문제가 발견되면 하위 계층은 정상일 가능성이 높습니다.

  • 1. DNS 조회 확인
    • Domain name과 IP 주소가 잘 매핑되어 조회가능한지 점검합니다. 이 과정에 문제가 있다면, 통신 시작 자체가 불가능합니다.
    • 대표 명령어: nslookup, dig
    • 체크포인트:
      • DNS 해석 실패 여부, 의도하지 않은 IP로 해석되는지 확인
      • AWS 환경: Route 53 설정 확인
  • 2. HTTP 응답 상태 확인
    • DNS에 문제가 없다면, 해당 IP의 웹 애플리케이션이 정상적인 HTTP 응답을 주는지 확인합니다.
    • 대표 명령어:
    • 주요 응답 코드
      • 2xx OK: 정상. 클라이언트 측 문제일 가능성.
      • 4xx Client Error: 클라이언트 요청 오류 (e.g., 404 Not Found).
      • 5xx Server Error: 서버 측 애플리케이션 로직, DB 연결 등에 문제 발생.
    • 체크포인트:
      • 서버 애플리케이션 로그, 웹 서버(Nginx, Apache) 설정 및 에러 로그.
      • AWS 환경: CloudWatch Logs, EC2 인스턴스 내 애플리케이션 로그

Layer 3. 전송 계층 (Transport Layer) - 포트 연결 확인

애플리케이션이 사용하는 특정 포트(TCP/UDP)까지의 경로가 열려 있는지 확인합니다. 방화벽 문제의 대부분이 여기서 발견됩니다.

  • TCP 포트 연결 테스트
    • 서버의 특정 포트가 연결 요청을 수락하는지 직접 확인합니다.
    • 대표 명령어: telnet example.com 443
    • 결과 분석
      • Connected: 성공. 포트가 열려 있고 서비스가 대기 중.
      • Connection refused: 거부. 포트는 열려 있으나 해당 포트를 사용하는 프로그램이 없거나, 방화벽이 거부함.
      • Timeout: 타임아웃. 중간 경로의 방화벽이 패킷을 버리거나(drop), 서버까지의 경로에 문제가 있음.
  • 체크포인트
    • 서버 방화벽: OS 방화벽(iptables, ufw) 규칙 확인.
    • 클라우드 방화벽: Security Group의 인바운드 규칙에 해당 포트(e.g., 80, 443)가 허용되어 있는지 확인.
    • 로드밸런서: ALB/NLB의 리스너 및 대상 그룹(Target Group) 헬스체크 상태 확인.

Layer 2. 인터넷 계층 (Internet Layer) - IP 도달 가능성 확인

IP 주소를 기반으로 목적지 서버까지 패킷이 정상적으로 전달되는지, 즉 라우팅 경로의 유효성을 확인합니다.

  • 1. 기본 연결성 확인
    • ICMP 프로토콜을 이용해 목적지 서버의 네트워크 스택이 살아있는지 확인합니다.
    • 대표 명령어: ping
    • 체크포인트: ping 실패 시 서버 자체의 문제이거나, ICMP를 차단하는 방화벽(네트워크 ACL 등)이 있을 수 있음.
  • 2. 네트워크 경로 추적
    • 목적지까지 어떤 라우터(hop)들을 거쳐 가는지 추적하여 어느 구간에서 문제가 발생하는지 확인합니다.
    • 대표 명령어: traceroute example.com (Linux/macOS) 또는 tracert example.com (Windows)
    • 체크포인트:
      • 특정 hop에서 응답이 멈추거나 지연이 급증하면 해당 라우터 또는 구간에 문제 발생.
      • AWS 환경: VPC 라우팅 테이블, 네트워크 ACL(NACL), 인터넷 게이트웨이(IGW) 설정 확인

Layer 1. 네트워크 액세스 계층 (Network Access Layer) - 물리/논리적 연결 확인

PC나 서버의 네트워크 카드(NIC)부터 스위치까지의 가장 기본적인 물리적, 논리적 연결 상태를 확인합니다.

  • 1. 네트워크 인터페이스 상태 확인
    • 로컬 장비의 네트워크 인터페이스가 활성화되어 있고 IP 주소를 할당받았는지 확인합니다.
    • 대표 명령어: ip addr (Linux) 또는 ifconfig (macOS), ipconfig (Windows)
    • 체크포인트:
      • 인터페이스가 UP 상태인지, 유효한 IP 주소를 가지고 있는지 확인.
  • 2. ARP 테이블 확인
    • 설명: 동일 네트워크 대역 내에서 IP 주소에 맞는 정확한 MAC 주소를 알고 있는지 확인합니다. (게이트웨이 등)
    • 대표 명령어: arp
    • 체크포인트:
      • 게이트웨이 IP에 대한 MAC 주소가 없거나 잘못되었다면 로컬 네트워크(LAN) 통신에 문제 발생.
      • AWS 환경: EC2 인스턴스의 running 상태, VPC 및 서브넷의 기본 설정 확인
  • 물리적 점검
    • 체크포인트: (물리 서버 환경) 랜 케이블 연결 상태, 스위치의 포트 상태 LED 등.

면접 질문으로 다시 돌아가서

글의 처음에는 아래와 같은 단순한 암기식 질문에서 시작했습니다.

"TCP/IP 4계층에 대해서 설명해주세요."

단순한 질문에서 시작해서, 소프트웨어 공학의 핵심 주제인 ‘모듈화’, ‘추상화’와, TCP/IP 4계층 모델의 실무에서의 적용 예시까지 살펴보았습니다. 그렇다면 아래의 질문에 대한 답변을 고민해보세요.

질문: "TCP/IP 모델은 왜 계층으로 나누어져 있나요? 그 이유는 무엇이라고 생각하세요?"

"TCP/IP 4계층으로 나눈 이유는 소프트웨어 공학의 모듈화와 추상화 원칙을 네트워크라는 복잡한 시스템에 적용하기 위함입니다.

네트워크 통신은 데이터 형식 정의, 신뢰성 보장, 라우팅, 물리적 전송이라는 서로 다른 성격의 복잡한 문제들을 동시에 해결해야 합니다. 이를 하나의 거대한 시스템으로 구현한다면 복잡도가 기하급수적으로 증가할 것입니다.

하지만 각각을 독립적인 계층으로 분리함으로써 두 가지 핵심 이점을 얻을 수 있습니다.

첫째, 재사용성과 호환성입니다. 특정 계층만 교체해도 다른 계층에 영향을 주지 않아 기술 발전에 유연하게 대응할 수 있습니다. 예를 들어, HTTP/3에서 Transport Layer만 TCP에서 QUIC으로 변경해도 Application Layer의 웹 애플리케이션은 수정 없이 그대로 동작합니다.

둘째, 문제 격리와 디버깅 용이성입니다. 네트워크 문제를 계층별로 체계적으로 분석하여 복잡한 문제의 원인을 빠르게 특정할 수 있습니다.

결국 TCP/IP 4계층은 단순한 이론적 모델이 아니라, 복잡한 네트워크 시스템을 효율적으로 설계하고 운영하기 위한 실용적인 아키텍처입니다."


질문: "AWS에 새로운 웹 서비스를 처음으로 배포했습니다. 아키텍처는 Route 53, ALB, EC2로 구성되어 있으며, 배포 후 할당된 도메인으로 접속을 시도했으나 페이지가 전혀 열리지 않습니다. 최초 배포 상황이라 모든 설정이 의심되는 상황입니다. 이 문제의 원인을 찾기 위해 어떤 순서로, 무엇을 점검하시겠습니까?"

웹페이지 접속 불가와 같은 장애가 발생했을 때, 저는 사용자와 가까운 애플리케이션 계층에서부터 하위 계층으로 내려가는 'Top-Down' 방식으로 체계적인 원인 분석을 진행하겠습니다. 이 접근 방식은 문제의 범위를 빠르고 효과적으로 좁히는 데 매우 유용합니다.

먼저 애플리케이션 계층에서는 DNS 조회가 정상적으로 이루어지는지 AWS Route 53 설정을 확인하고, nslookup 명령으로 올바른 IP 주소를 받아오는지 점검하겠습니다. 이후 curl 명령을 통해 HTTP 응답 코드를 확인하여, 만약 5xx 에러가 발생한다면 EC2 인스턴스에 접속하여 웹 서버(Nginx 등)의 로그나 CloudWatch Logs를 분석해 애플리케이션 자체의 오류를 찾아낼 것입니다.

애플리케이션에 이상이 없다면 전송 계층으로 내려와 서버의 서비스 포트(443 등)가 열려 있는지 확인합니다. telnet과 같은 명령어로 직접 접속을 시도해보고, 만약 연결이 거부된다면 AWS Security Group의 인바운드 규칙이 올바르게 설정되었는지 검토할 것입니다. 만약 로드밸런서를 사용 중이라면, ALB나 NLB의 헬스체크 상태와 대상 그룹(Target Group)의 포트 설정을 점검하여 트래픽이 EC2 인스턴스로 제대로 전달되는지 확인할 것입니다.

그 다음 인터넷 계층에서는 ping으로 서버 IP까지의 도달 가능성을 확인하고, traceroute를 실행하여 중간 경로의 문제를 추적하겠습니다. AWS 환경에서는 특히 VPC의 라우팅 테이블이 인터넷 게이트웨이(IGW)로 올바르게 설정되었는지, 그리고 서브넷에 연결된 네트워크 ACL(NACL)이 트래픽을 차단하고 있지는 않은지 면밀히 살펴보겠습니다.

마지막으로 네트워크 액세스 계층에서는 EC2 인스턴스 자체가 AWS 콘솔에서 'running' 상태인지 확인하고, 시스템 내에서 네트워크 인터페이스가 정상적으로 활성화되어 있는지 점검할 것입니다.

이처럼 상위 계층부터 하위 계층으로 체계적으로 점검하면, 문제의 원인이 애플리케이션 로직에 있는지, 방화벽 설정에 있는지, 아니면 네트워크 라우팅에 있는지를 논리적으로 좁혀나가며 신속하고 효율적으로 장애에 대응할 수 있습니다.

 

이러한 고민을 담은 기술면접 문제집을 만들었습니다

인터넷에선 단순한 암기형 예상 면접 질문은 쉽게 찾을 수 있습니다. 하지만 그에 대한 답변을 작성해도, 내 답변이 얼마나 정확한지 알기 어렵죠. 암기형을 넘어 깊이 있는 질문은 찾는 것조차 쉽지 않구요. 이러한 문제를 해결하기 위해 아래와 같은 AI를 사용한 기술면접 평가 서비스를 만들었습니다. 아래 링크에서 위 글에서 다룬 문제를 본인만의 답변으로 풀어보고, 자신의 답변이 얼마나 정확한지, 어떤 점을 개선해야 하는지 파악해보세요!

TCP/IP 4계층 대해 단순한 암기가 아닌, 소프트웨어 공학 원리와 실무 경험을 바탕으로 한 깊이 있는 답변을 준비할 수 있을 것입니다.

 

문제집: 소프트웨어 공학 개념과 AWS로 보는 TCP/IP 4계층

 

기술면접 대비 AI 서비스를 최근 만들다보니, 이번 주부터는 실제 기술면접 질문 답변 암기를 넘어, 실질적으로 이해하기 위한 글을 작성해보기로 했습니다.

이번 주 글의 세부 주제는 ‘HTTP Method’입니다. 지금부터 글 시작하겠습니다!

기술면접 단골 질문

"HTTP Method와 각각이 사용되는 경우에 대해서 설명해주세요."

이 질문은 웹 백엔드 개발자 기술면접에서 빠지지 않는 단골 질문입니다. 대부분의 개발자들은 이렇게 답변합니다.

💡
GET: 데이터를 조회할 때 사용합니다
POST: 새로운 데이터를 생성할 때 사용합니다
PUT: 기존 데이터를 수정할 때 사용합니다
DELETE: 데이터를 삭제할 때 사용합니다 

 

틀린 답변은 아닙니다. 하지만 이 답변을 듣고 나면 자연스럽게 다음 질문이 따라옵니다.

그런데 정말 "왜" 구분해서 써야 할까요?

실제로 개발을 하다 보면 이런 생각이 들 때가 있습니다. 사용자 정보를 조회하든, 게시글을 작성하든, 댓글을 삭제하든 - 결국 모든 기능을 POST 메소드 하나로도 충분히 구현할 수 있지 않을까요?

// 이렇게 POST 하나로 모든 걸 처리할 수 있는데...
POST /api/user/get     // 사용자 조회
POST /api/user/create  // 사용자 생성
POST /api/user/update  // 사용자 수정
POST /api/user/delete  // 사용자 삭제

그렇다면 "POST 메소드 하나로도 모든 CRUD 기능을 구현할 수 있는데, 왜 굳이 GET, PUT, DELETE 등의 메소드를 구분해서 사용해야 하나요?"

이 질문에 대한 명확한 기술적 근거를 아시나요? 단순히 "REST API 설계 원칙이니까"라는 답변을 넘어서, 실제로 어떤 기술적 차이가 발생하는지 알고 계신가요?

이 글에서는 HTTP 메소드를 구분해서 사용해야 하는 이론적 근거와 실제 동작 원리를 깊이 있게 살펴보겠습니다.

Semantic HTTP methods의 중요성

일상 대화에서 의미 구분이 얼마나 중요한지 생각해보겠습니다. 친구에게 "책 좀 줘"라고 말할 때와 "책 좀 빌려줘"라고 말할 때, 단어 몇 개만 다를 뿐이지만 의미는 완전히 다릅니다. 전자는 소유권이 이전되지만, 후자는 임시로 빌리는 것이죠.

HTTP 메소드도 마찬가지입니다. 서버에게 "이 데이터를 달라(GET)"고 말하는 것과 "이 데이터를 만들어줘(POST)"라고 말하는 것은 문법적으로는 비슷하지만 의미적으로는 완전히 다른 요청입니다.

GET /api/users/123     // "사용자 123번 정보를 보여줘"
POST /api/users        // "새로운 사용자를 만들어줘"
PUT /api/users/123     // "사용자 123번 정보를 이것으로 바꿔줘"
DELETE /api/users/123  // "사용자 123번을 삭제해줘"

왜 의미적 구분이 중요할까?

웹은 단순히 두 컴퓨터가 대화하는 공간이 아닙니다. 수많은 중간 시스템들(프록시 서버, CDN, 캐시 서버, 로드밸런서 등)이 이 대화를 듣고 있으며, 각각 다른 방식으로 반응합니다.

마치 우체국 직원이 편지봉투에 적힌 "긴급", "일반우편", "등기우편" 같은 표시를 보고 서로 다른 처리 방식을 적용하는 것과 같습니다. 만약 모든 편지봉투에 "그냥 편지"라고만 적혀 있다면, 우체국은 어떤 편지를 우선 처리해야 할지, 어떤 편지를 안전하게 보관해야 할지 알 수 없을 것입니다.

HTTP 메소드는 바로 이런 "처리 방식을 알려주는 표시" 역할을 합니다. 그렇다면 각 메소드가 가진 구체적인 특성들이 실제로 어떤 기술적 차이를 만들어내는지 살펴보겠습니다.

표준 메소드 사용의 기술적 및 아키텍처적 이점

표준 HTTP 메소드를 의미에 맞게 사용하는 것은 단순히 멋있어 보이기 위함이 아닙니다. 실제 시스템 개발, 운영, 유지보수 전반에 걸쳐 엄청난 이점을 제공합니다.

안전성과 멱등성: 단순한 개념이 아닌 기술적 약속

HTTP 메소드를 구분하는 가장 중요한 기준은 **안전성(Safe)**과 **멱등성(Idempotent)**입니다. 이 개념들이 단순한 이론이 아닌 실제 시스템 동작에 어떤 영향을 미치는지 살펴보겠습니다.

안전한(Safe) 메소드: 서버의 상태를 변경하지 않는 메소드

멱등한(Idempotent) 메소드: 동일한 요청을 여러 번 보내도 결과가 같은 메소드

HTTP 메소드 안전성 (Safe) 멱등성 (Idempotent) 설명

GET ✅ 안전함 ✅ 멱등함 데이터 조회만 수행, 상태 변경 없음
POST ❌ 안전하지 않음 ❌ 멱등하지 않음 새 리소스 생성, 매번 다른 결과
PUT ❌ 안전하지 않음 ✅ 멱등함 전체 리소스 교체, 같은 값으로 반복 시 동일 결과
PATCH ❌ 안전하지 않음 ❓ 경우에 따라 다름 부분 수정, 구현 방식에 따라 멱등성 결정
DELETE ❌ 안전하지 않음 ✅ 멱등함 리소스 삭제, 이미 없는 것 삭제해도 동일 결과

하지만 여기서 중요한 점은, HTTP 메소드 자체가 이런 특성을 강제하는 것이 아니라는 것입니다. 개발자는 GET으로도 데이터를 삭제하는 API를 만들 수 있고, POST로도 단순 조회 API를 만들 수 있습니다.

// 기술적으로는 가능하지만 잘못된 사용
app.get('/api/users/123/delete', (req, res) => {
    // GET인데 데이터를 삭제하는 로직
    database.deleteUser(123);
    res.json({success: true});
});

핵심은 웹의 중간 계층들(브라우저, 캐시 서버, 프록시 등)이 HTTP 명세를 믿고 동작한다는 것입니다.

명확성 및 가독성

API Endpoint만 봐도 이 요청이 어떤 작업을 하려는 건지 명확히 알 수 있습니다.

GET /api/users/123      # "123번 사용자 정보를 읽어오겠구나"
DELETE /api/users/123   # "123번 사용자를 삭제하겠구나"

반면 POST 요청 하나로 모든 작업을 처리한다면, 요청 본문이나 별도의 파라미터를 열어보기 전에는 이게 회원 가입인지, 로그인인지, 정보 수정인지, 탈퇴인지 알 수가 없습니다. 이는 개발자 간 협업과 코드 이해도를 크게 저해합니다.

캐싱: 성능 최적화의 핵심

HTTP 메소드 구분이 가장 극명하게 드러나는 영역은 캐싱입니다.

우체국에서 자주 배달되는 주소는 따로 기억해두듯이, 웹에서도 자주 요청되는 데이터는 중간 지점에 저장해둡니다. 하지만 모든 요청을 캐시할 수는 없습니다.

# 캐시 가능한 요청들
GET /api/users/123           # 브라우저, CDN에서 자동 캐시
GET /api/products?page=1     # 자주 조회되는 상품 목록 캐시

# 캐시하면 안 되는 요청들
POST /api/users              # 매번 새로운 사용자가 생성됨
POST /api/orders             # 매번 새로운 주문이 발생함

GET 요청만 기본적으로 캐시 대상이 됩니다. 이는 HTTP 명세에서 정의한 동작이며, 모든 브라우저, CDN, 프록시 서버가 따르는 규칙입니다. 이러한 HTTP 캐싱 메커니즘을 활용하면 서버 부하를 획기적으로 줄이고 응답 속도를 높여 사용자 경험을 개선할 수 있습니다.

보안(Security)

각 메소드의 특성에 맞는 보안 정책을 적용하기 용이합니다. 예를 들어, 방화벽이나 API 게이트웨이에서 특정 자원에 대해 GET 요청(조회)만 허용하고 PUT이나 DELETE 요청은 차단하는 정책을 쉽게 설정할 수 있습니다.

# API 게이트웨이에서 메소드별 보안 정책
location /api/public/ {
    # 공개 API는 GET만 허용
    limit_except GET {
        deny all;
    }
    proxy_pass <http://backend>;
}

location /api/admin/ {
    # 관리자 API는 모든 메소드 허용 (인증 후)
    auth_basic "Admin Area";
    proxy_pass <http://backend>;
}

메소드를 구분함으로써 이러한 보안 고려사항을 체계적으로 적용할 수 있습니다.

네트워크 인프라 활용

프록시 서버, 로드밸런서, API 게이트웨이 등 다양한 네트워크 구성 요소들은 HTTP 메소드를 인지하고 그에 맞춰 동작을 최적화하거나 보안 정책을 적용합니다.

실무에서 여러 서버를 운영할 때 사용하는 로드밸런서도 HTTP 메소드를 다르게 처리합니다.

# Nginx 로드밸런서 설정 예시
upstream backend {
    server app1.example.com;
    server app2.example.com;
}

# GET 요청은 모든 서버로 분산 가능
location ~ ^/api/.*/$ {
    if ($request_method = GET) {
        proxy_pass <http://backend>;
    }
}

# POST 요청은 세션 어피니티 고려
location /api/orders {
    if ($request_method = POST) {
        proxy_pass <http://backend>;
        # 같은 사용자는 같은 서버로 라우팅
    }
}

GET 요청은 어느 서버에서 처리하든 결과가 동일하므로 자유롭게 분산시킬 수 있지만, POST 요청은 서버 상태를 변경하므로 더 신중한 라우팅이 필요합니다.

상호운용성 및 확장성

표준을 준수하는 API는 다른 시스템과의 통합이 매우 용이합니다. 잘 정의된 인터페이스는 계약과 같아서, 클라이언트와 서버가 서로의 구현을 몰라도 약속된 규칙에 따라 통신할 수 있습니다.

이는 마이크로서비스 아키텍처와 같이 분산된 시스템 환경에서 특히 중요하며, 미래에 기능이 확장되거나 시스템 일부가 교체될 때 유연하게 대처할 수 있게 합니다.

이처럼 HTTP 메소드 구분은 단순한 개발자 컨벤션이 아닌, 웹 인프라 전반에 걸친 기술적 표준입니다.

면접 질문으로 다시 돌아가서

자, 이제 처음에 던진 그 질문으로 돌아가 보겠습니다.

"POST 메소드 하나로도 모든 CRUD 기능을 구현할 수 있는데, 왜 굳이 GET, PUT, DELETE 등의 메소드를 구분해서 사용해야 하나요?"

이 질문에 대한 답변을 지금까지 살펴본 내용을 바탕으로 정리해보겠습니다.

기술면접 모범 답변

💡
"네, 기술적으로는 POST 하나로도 모든 기능 구현이 가능합니다. 하지만 HTTP 메소드를 구분해서 사용해야 하는 이유는 크게 다섯 가지입니다.
첫째, 웹 인프라의 최적화 활용입니다. GET 요청은 브라우저, CDN, 프록시 서버에서 자동으로 캐싱되어 성능 향상과 서버 부하 감소 효과를 얻을 수 있습니다. 또한 로드밸런서에서 메소드별로 다른 라우팅 전략을 적용할 수 있어 시스템 안정성이 높아집니다.
둘째, 보안 정책의 체계적 적용입니다. 메소드별로 서로 다른 보안 규칙을 설정할 수 있습니다. 예를 들어 API 게이트웨이에서 공개 엔드포인트는 GET만 허용하고, 관리자 기능은 인증 후 모든 메소드를 허용하는 식으로 세밀한 접근 제어가 가능합니다.
셋째, 개발 생산성과 유지보수성입니다. API 엔드포인트만 봐도 해당 요청의 의도를 즉시 파악할 수 있어 개발자 간 협업 효율성이 크게 향상됩니다. POST 하나로 모든 기능을 처리하면 요청 본문을 열어봐야만 무슨 작업인지 알 수 있어 코드 가독성이 떨어집니다.
넷째, 안전성과 멱등성을 통한 신뢰성 확보입니다. 각 메소드의 특성을 지키면 예상치 못한 부작용을 방지할 수 있습니다. 특히 GET은 서버 상태를 변경하지 않고, PUT과 DELETE는 동일한 요청을 여러 번 보내도 결과가 같아서 시스템의 예측 가능성이 높아집니다.
다섯째, 상호운용성과 확장성입니다. 표준을 준수하는 API는 다른 시스템과의 연동이 용이하고, 미래에 시스템이 확장되거나 일부가 교체될 때도 유연하게 대응할 수 있습니다.
결론적으로, HTTP 메소드 구분은 현재의 기능 구현뿐만 아니라 장기적인 시스템 운영과 확장을 고려한 아키텍처 설계의 핵심 요소입니다."

 

이러한 고민을 담은 기술면접 문제집을 만들었습니다

"왜 HTTP 메소드를 구분해야 하는가?" 중요한 질문이지만, 혼자 공부하는 취준생은 이러한 질문을 떠올리기도, 답변을 준비하기도 어렵습니다. 인터넷에 떠도는 질문들은 다 비슷비슷하고, 그 비슷한 질문들에 대한 답변을 그저 따라 치는 것만으로 준비가 충분한지 알기 어렵습니다.

따라서 저는 떠도는 기술면접 질문을 더욱 실용적이게 발전시키고, AI가 사용자 답변을 평가함으로써 보다 실질적인 기술면접 대비를 할 수 있는 서비스를 만들었습니다. 아래 링크를 통해 직접 확인해보세요.

https://cs-master.vercel.app/problem-sets/17

Intro

이번 Jungle CS 스터디의 주제는 “System Call”입니다. 저는 이번 개인 주제로서, JVM에서 System Call이 어떻게 동작하는지에 대해 소개해보겠습니다. 글의 개요는 아래와 같습니다.

개요

  • System Call의 배경과 필요성
  • System Call의 동작 원리
  • JVM과 System Call
  • JVM 기반 언어 vs. Native 언어(C)의 System Call 접근 방식 비교

 

System Call의 배경과 필요성

우리는 컴퓨터 하나에서 동시에 많은 프로그램을 사용할 수 있습니다. 이는 운영체제가 각 프로세스 별로 공정하게 하드웨어 자원을 분배하는 역할을 하기 때문이죠. 수많은 프로세스는 결국 유한한 하드웨어를 공유하며 동작하고 있는 것입니다. 그렇다면 만약 하나의 프로세스가 다른 프로세스의 허락 없이 데이터를 읽고 수정할 수 있다면 어떻게 될까요? 또는 어떤 프로세스가 모든 프로세스가 공유하는 하드웨어 자원의 사용을 못하게 망가트린다면? 심각한 데이터 유출이나 발생하거나 컴퓨터 사용에 큰 지장이 생길 것입니다.

따라서 하드웨어 자원을 추상화하고 효율적인 운영에 책임이 있는 OS는, 프로세스의 하드웨어 자원 사용을 엄격하게 제한할 필요가 있습니다. 이를 위해 OS는 최소 두가지 실행 모드를 제공합니다.

  • 사용자 모드(User Mode): 제한된 명령어만 실행할 수 있으며, 하드웨어 자원에 직접 접근할 수 없습니다. 일반 애플리케이션은 이 모드에서 실행됩니다.
  • 커널 모드(Kernel Mode): 모든 하드웨어 자원에 직접 접근이 가능하며, 모든 CPU 명령어 실행 권한을 가집니다. 운영체제의 핵심 부분인 커널이 이 모드에서 실행됩니다.

프로세스(User Mode)는 하드웨어 자원에 대한 접근이 필요하다면, 반드시 OS에게 하드웨어 사용을 요청해 OS가 Kernel Mode에서 하드웨어를 사용할 수 있게해야 합니다. System Call이란, 위 과정에서 프로세스가 OS에게 하드웨어 접근 사용을 허가 받기 위한 요청을 의미합니다. 조금더 자세히 System Call의 역할에 대해 알아봅시다.

System Call의 역할

System Call은 사용자 모드 프로그램과 커널 모드 운영체제 사이의 인터페이스를 제공합니다. 이는 다음과 같은 역할을 수행합니다:

  1. 보호 경계(Protection Boundary) 유지: 사용자 프로그램이 운영체제나 다른 프로그램의 중요한 부분에 직접 접근하는 것을 방지합니다.
  2. 제어된 인터페이스 제공: 애플리케이션이 하드웨어와 시스템 자원에 접근할 수 있는 안전하고 일관된 방법을 제공합니다.
  3. 권한 검증(Privilege Checking): 요청된 작업을 수행할 권한이 있는지 확인하여 보안을 강화합니다.
  4. 추상화 계층(Abstraction Layer) 제공: 하드웨어의 복잡성을 감추고 일관된 API를 제공하여 애플리케이션 개발을 단순화합니다.

System Call을 어렵게 생각할건 없습니다. 우리는 이미 밥먹듯이 System Call을 쓰고 있거든요. 파일을 읽고 실행하고 종료하는 것도 System Call 없이는 불가능합니다. print() 문을 사용하거나, scanner로 사용자 입력을 받는 것도 System Call이 필요하지요. 이렇게 핵심적인 역할을 하는 System Call은 그렇다면 어떻게 동작하는 걸까요?

System Call의 동작 원리

System Call의 핵심은 사용자 모드(User Mode)에서 커널 모드(Kernel Mode)로의 안전한 전환입니다. 이 전환 과정은 일종의 '문지기'가 있는 출입문을 통과하는 것과 비슷합니다. 프로그램은 마음대로 커널 영역에 들어갈 수 없고, 반드시 정해진 절차와 방법을 통해서만 접근이 가능합니다.

트랩(Trap) 메커니즘과 인터럽트 처리

System Call이 작동하는 핵심 메커니즘은 '트랩(Trap)'이라고 불리는 특별한 형태의 인터럽트입니다. 인터럽트란 CPU에게 "잠깐만, 지금 하던 일을 멈추고 이것을 처리해야 해!"라고 알리는 신호입니다.

Interrupt에는 크게 두 종류가 있습니다:

(정확히는, interrupt, trap, exception, fault, and abort 등의 단어는 명확한 정의는 없다고 합니다)

  1. Hardware Interrupt
    1. 디스크, 키보드, 네트워크 카드, DMA 등의 장치가 발생
  2. Software Interrupt
    1. Exception(예외)
      • 0으로 나누기, 접근 권한 위반 등 프로그램 실행 중 오류가 발생할 때 생성
    2. Trap(트랩)
      • 프로그램이 의도적으로 발생시키는 인터럽트로, System Call이 여기에 해당합니다.

트랩은 특별한 명령어(int, syscall, svc 등 아키텍처마다 다름)를 통해 의도적으로 발생시키는 인터럽트입니다. 이 명령어가 실행되면 다음과 같은 일이 발생합니다:

  1. CPU는 현재 실행 중인 프로그램의 상태(레지스터 값, 프로그램 카운터 등)를 저장합니다.
  2. CPU 모드가 사용자 모드에서 커널 모드로 전환됩니다.
  3. 제어권이 커널의 인터럽트 핸들러로 넘어갑니다.

이것은 마치 비행기를 타기 위해 보안 검색대를 통과하는 것과 비슷합니다. 여러분의 신원(프로그램 상태)이 확인되고, 제한 구역(커널 모드)으로 들어갈 수 있는 허가를 받는 과정입니다.

유저 모드에서 커널 모드로의 전환 과정

System Call의 전체 과정을 단계별로 살펴보겠습니다:

  1. 애플리케이션 요청: 프로그램이 System Call 라이브러리 함수를 호출합니다. 예를 들어 C 프로그램에서 printf()를 호출하면, 이는 내부적으로 write() System Call을 사용합니다.
  2. 라이브러리 래퍼(Wrapper): 라이브러리 함수는 필요한 매개변수를 준비하고, CPU 레지스터에 System Call 번호와 매개변수를 설정합니다.
  3. 트랩 명령어 실행: syscall(x86-64), svc(ARM) 등의 특별한 명령어를 실행하여 트랩을 발생시킵니다.
  4. 모드 전환: CPU가 사용자 모드에서 커널 모드로 전환됩니다. 이때 중요한 점은 프로그램의 실행 흐름이 바뀐다는 것입니다. 프로그램의 다음 명령어가 아닌, 커널의 System Call 핸들러로 점프합니다.
  5. System Call 핸들러: 커널은 요청된 System Call 번호를 확인하고, 해당하는 커널 함수를 호출합니다.
  6. 권한 검사: 커널은 프로세스가 요청한 작업을 수행할 권한이 있는지 확인합니다. 예를 들어, 파일을 열려면 해당 파일에 대한 접근 권한이 있어야 합니다.
  7. 요청 처리: 권한 검사를 통과하면, 커널은 요청된 작업(파일 읽기, 네트워크 패킷 전송 등)을 수행합니다.
  8. 결과 준비: 작업이 완료되면 결과 값(성공/실패 코드, 읽은 데이터 등)을 사용자 프로그램에 반환할 준비를 합니다.
  9. 사용자 모드 복귀: 커널은 다시 사용자 모드로 전환하고, 제어권을 System Call을 호출한 프로그램의 다음 명령어로 돌려줍니다. 결과 값은 보통 레지스터를 통해 전달됩니다.

쉽게 얘기해서 음식점에서 손님이 칵테일을 주문하는 것과 비슷합니다. 손님(process)가 직원(OS)를 불러서, 먹고 싶은 칵테일의 번호(Trap No.)를 불러주며 주문합니다(System Call). 그럼 직원은 손님이 성인인지 확인하고(권한검사), 바 테이블로 가서(커널모드 전환) 칵테일을 제조하죠(커널이 작업 수행). 그리고 칵테일이 완성되면 바를 나가(사용자 모드 복귀), 손님에게 주문한 칵테일을 대접하는거죠!

컨텍스트 스위칭의 비용

우리는 칵테일을 한 잔씩 주문할 때마다 직원에게 돈을 내야해죠. 그것처럼 System Call을 통해 User Mode → Kernel Mode로 전환(Context Swiching) 하는데에도 비용이 필요합니다. 이러한 비용을 Context Swiching Cost라고 하죠.

  1. CPU 상태 저장: 현재 실행 중인 프로그램의 상태(레지스터 값, 프로그램 카운터 등)를 저장해야 합니다.
  2. CPU 캐시 영향: 모드가 전환되면 CPU 캐시의 효율성이 떨어질 수 있습니다. 커널 코드와 데이터가 캐시를 차지하게 되어, 사용자 프로그램의 캐시 데이터가 밀려날 수 있습니다.
  3. TLB(Translation Lookaside Buffer) 플러시: 일부 아키텍처에서는 모드 전환 시 TLB를 플러시(비우기)해야 하며, 이는 메모리 접근 성능을 일시적으로 저하시킵니다.
  4. 파이프라인 비우기: 현대 CPU의 명령어 파이프라인이 비워져야 할 수도 있습니다.

일반적인 함수 호출은 몇 나노초 정도 소요되는 반면, System Call은 수백 나노초에서 마이크로초 단위의 시간이 걸릴 수 있습니다. 이는 100~1000배 더 느릴 수 있다는 의미입니다!

이런 비용 때문에, 성능이 중요한 애플리케이션에서는 System Call의 사용을 최소화하는 기법을 사용합니다.

  1. 버퍼링(Buffering): 작은 데이터를 여러 번 쓰는 대신, 큰 버퍼에 모았다가 한 번에 쓰는 방식을 사용합니다. 이는 write() System Call의 호출 횟수를 줄여줍니다.
  2. 메모리 매핑(Memory Mapping): mmap() System Call을 사용하여 파일을 메모리에 매핑하면, 이후 파일 접근이 일반 메모리 접근으로 처리되어 추가적인 System Call이 필요 없게 됩니다.
  3. 배치 처리(Batching): 최신 Linux 커널의 io_uring과 같은 인터페이스는 여러 I/O 작업을 한 번의 System Call로 처리할 수 있게 해줍니다.

휴 System Call 개념을 설명하는 것만 해도 힘이 드네요. 드디어 본론인 JVM에서 System Call은 어떤 과정으로 동작하는지에 대해 알아봅시다.

JVM과 System Call

저번 게시물에도 언급했듯이, 자바의 철학은 “Write Once, Run Anywhere”입니다. 이러한 철학에 대한 구현이 바로 java와 운영체제 사이에 존재하는 JVM(Java Virtual Machine)이구요. 저번에는 JVM의 메모리 구조에 집중했습니다. 이번 주제인 System Call이 JVM에서 사용되는 과정을 이해하기 위해선 JVM의 전체 구조 중, Java Native Method Interface(JNI)와 Native Method Libraries를 집중해서 보겠습니다.

Native Method

먼저, Native Method란 무엇일까요? Java에서 네이티브 메소드란, Java가 아닌 다른 언어(주로 C/C++)로 작성된 메소드로써, native keyword로 선언된 메소드를 의미합니다. Java 자체만으로는 접근하기 어려운 하드웨어 자원 제어, 운영체제 특정 기능 활용, 또는 성능상의 이유로 다른 언어로 구현된 기능 등이 있죠. 이 메소드들은 native 키워드로 선언되며, 실제 구현은 Native Method Libraries에 있습니다.

public class FileExample {
    // 네이티브 메소드 선언
    private native int open(String path, int flags);
    private native int write(int fd, byte[] buffer, int size);
    private native int close(int fd);
    
    // 네이티브 라이브러리 로드
    static {
        System.loadLibrary("fileio");
    }
    
    // Java 메소드에서 네이티브 메소드 호출
    public void writeToFile(String path, String content) {
        int fd = open(path, 1); // 1은 쓰기 모드를 의미
        byte[] data = content.getBytes();
        write(fd, data, data.length);
        close(fd);
    }
}

이 예제에서 open(), write(), close()는 Native Method로, 실제 구현은 'fileio'라는 네이티브 라이브러리에 있습니다. 그리고 Native Method에 대한 구현이 바로 Native Method Libraries에 있는 Native Code인 것이죠. 즉 요약하자면,

  • Native Method: Java 코드 내에 선언된 인터페이스이며, 실제 동작은 Native Code에 위임합니다.
  • Native Code: JVM 외부에서 실행되는 실제 구현체이며, 운영체제와 직접 상호작용하여 Native Method가 요청한 작업을 수행합니다. System Call이 실행되죠(System Call이 필요할 때만)

즉 Java에서 System Call은 Native Method를 통해 Native Code가 실행되며 실행되는 것입니다. 그리고 이를 가능케 하는 중간자로 JNI(Java Native Method Interface)가 있습니다.

Java Native Method Interface(JNI)의 역할

JNI(Java Native Method Interface)는 Java 코드와 네이티브 코드(C, C++ 등) 간의 다리 역할을 하는 인터페이스입니다. JNI를 통해 Java는:

  1. Native Method를 호출할 수 있습니다.
  2. Java 객체를 네이티브 코드에 전달할 수 있습니다.
  3. Native Code에서 Java 객체의 필드를 읽고 쓸 수 있습니다.
  4. Native Code에서 Java 메소드를 호출할 수 있습니다.

JNI는 Java의 플랫폼 독립성을 해치지 않으면서도 운영체제의 네이티브 기능을 활용할 수 있게 해주는 중요한 메커니즘입니다. JNI는 통역사와 비슷한 역할을 하는거라 할 수 있죠. 서로 다른 언어를 쓰는 외국인들끼리 만나도, 통역사를 통해 의사소통이 가능한 거죠.

JNI를 사용하는 네이티브 코드의 예를 살펴보겠습니다:

#include <jni.h>
#include <fcntl.h>
#include <unistd.h>

// Java_클래스명_메소드명 형식으로 함수 이름 지정
JNIEXPORT jint JNICALL Java_FileExample_open
  (JNIEnv *env, jobject obj, jstring path, jint flags) {

// Java 문자열을 C 문자열로 변환
    const char *nativePath = (*env)->GetStringUTFChars(env, path, NULL);

// open() 시스템 콜 호출
    int fd = open(nativePath, flags);

// 자원 해제
    (*env)->ReleaseStringUTFChars(env, path, nativePath);

    return fd;
}

// write(), close() 메소드도 비슷한 방식으로 구현

이 C 코드는 Java의 네이티브 메소드에 대응하는 네이티브 구현입니다. 여기서 open() 함수는 운영체제의 System Call을 직접 호출합니다.

자 이제 JVM 내부에서 System Call이 호출되는 과정을 이해하기 위한 개념 정리는 다 했습니다. 전체 과정을 살펴봅시다.

JVM 내부의 System Call 호출 과정

Java 프로그램에서 System Call이 호출되는 전체 과정을 살펴보겠습니다:

  1. Java 코드 실행: Java 애플리케이션이 표준 라이브러리 메소드(예: FileOutputStream.write())를 호출합니다.
  2. Java API 처리: 표준 라이브러리는 이 호출을 처리하고, 필요한 경우 JVM의 네이티브 메소드를 호출합니다.
  3. 네이티브 메소드 호출: JVM은 JNI를 통해 네이티브 라이브러리의 함수를 호출합니다.
  4. System Call 호출: 네이티브 함수는 운영체제의 System Call을 호출합니다.(By native code, …)
  5. 커널 처리: 커널은 System Call을 처리하고 결과를 반환합니다.
  6. 결과 전달: 결과가 네이티브 함수 → JVM → Java API → Java 애플리케이션으로 전달됩니다.

개념들을 알고나니 실행 과정을 이해하는게 어렵지 않죠? 그렇다면 System Call을 사용하는 Java Standard Libraries의 기능 예시를 몇가지만 살펴봅시다.

Java 표준 라이브러리와 System Call 매핑

Java 표준 라이브러리의 많은 기능들은 내부적으로 System Call에 의존합니다. 주요 예를 살펴보겠습니다:

파일 입출력

FileOutputStream fos = new FileOutputStream("test.txt");
fos.write("Hello, World!".getBytes());
fos.close();

사용 System Calls:

  • open(): 파일 생성/열기
  • write(): 데이터 쓰기
  • close(): 파일 닫기

네트워크 통신

Socket socket = new Socket("example.com", 80);
OutputStream out = socket.getOutputStream();
out.write("GET / HTTP/1.1\\r\\n\\r\\n".getBytes());

사용 System Calls:

  • socket(): 소켓 생성
  • connect(): 서버에 연결
  • write(): 데이터 전송

스레드 관리

Thread thread = new Thread(() -> {
    System.out.println("Hello from thread!");
});
thread.start();

사용 System Calls:

  • clone() 또는 pthread_create(): 새 스레드 생성
  • futex(): 스레드 동기화 (Linux의 경우)

이 외에도, 하드웨어 자원 사용이나 실행 흐름과 관련된 기능 등 매우 많은 기능들이 System Call을 사용합니다. System Call에 대한 이해 없이 언어의 동작 과정을 이해하는 건 불가능에 가깝죠. 하지만 Java는 System Call을 몰라도 편하게 사용할 수 있게 잘 추상화시켜놨습니다.

Java의 System Call 추상화 계층

Java는 운영체제의 System Call을 직접 노출하는 대신, 여러 계층의 추상화를 통해 이를 감춥니다:

  1. 고수준 API: Files, Paths, Channels 등의 현대적인 API
  2. 중간 수준 API: InputStream, OutputStream 등의 전통적인 API
  3. 저수준 JVM 네이티브 메소드: 내부 구현에 사용되는 메소드
  4. 운영체제별 네이티브 라이브러리: 각 OS에 맞는 구현
  5. 운영체제 System Call: 실제 커널 기능

이러한 계층 구조 덕분에 Java 개발자는 운영체제의 차이점을 신경 쓰지 않고도 코드를 작성할 수 있습니다. 같은 Java 코드가 Windows, Linux, macOS에서 동일하게 동작하는 것은 JVM이 각 운영체제에 맞는 네이티브 코드와 System Call을 알아서 처리해주기 때문입니다

JVM 기반 언어 vs. Native 언어(C)의 System Call 접근 방식 비교

JVM 기반 언어(Java, Kotlin, Scala …)는 패키지 여행과 같아 현지 언어와 문화(OS)를 몰라도 가이드가 모든 것을 처리해주죠. 반면 Native 언어는 가이드 없이 가는 자유여행이라 더 자유롭고 내 마음대로 여행할 수 있지만, 현지 언어와 문화(OS)에 맞게 개인이 일일이 준비를 해야합니다. 두 진영의 System Call에 대한 접근 방식도 이러한 차이에서 비롯합니다.

크로스 플랫폼 지원

플랫폼 이식성은 두 접근 방식의 가장 큰 차이점 중 하나입니다:

  1. 네이티브 언어:
    • 각 운영체제별로 소스 코드를 수정하거나 조건부 컴파일이 필요합니다.
    • 각 플랫폼별로 다시 컴파일해야 합니다.
    • System Call의 차이를 개발자가 직접 처리해야 합니다.
    #ifdef _WIN32
    // Windows 시스템 콜 사용
        HANDLE file = CreateFile("test.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        WriteFile(file, "Hello, World!", 13, &bytesWritten, NULL);
        CloseHandle(file);
    #else
    // UNIX/Linux 시스템 콜 사용
        int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
        write(fd, "Hello, World!", 13);
        close(fd);
    #end
    
  2. JVM 기반 언어:
    • "Write Once, Run Anywhere" - 한 번 작성하면 어디서나 실행 가능합니다.
    • JVM이 각 플랫폼의 차이를 추상화합니다.
    • 개발자는 운영체제 차이를 거의 신경 쓰지 않아도 됩니다.
    // 어떤 운영체제에서도 동일하게 작동
    FileOutputStream fos = new FileOutputStream("test.txt");
    fos.write("Hello, World!".getBytes());
    fos.close();
    

이는 마치 네이티브 언어는 각 나라의 현지 언어로 대화해야 하는 것과 같고, JVM 언어는 어디서나 영어로 대화할 수 있는 것과 비슷합니다.

플랫폼 특화 기능 접근성

반면, 각 플랫폼의 고유 기능을 활용하는 측면에서는:

  1. 네이티브 언어:
    • 운영체제의 모든 특화 기능에 쉽게 접근할 수 있습니다.
    • 최신 System Call이나 API를 즉시 활용할 수 있습니다.
  2. JVM 기반 언어:
    • 플랫폼 특화 기능은 JNI를 통해 접근해야 합니다.
    • JVM이 새로운 System Call을 지원하기까지 시간이 걸릴 수 있습니다.
    • 일부 저수준 기능은 표준 API로 제공되지 않을 수 있습니다.

따라서 특정 운영체제의 고유 기능이 필요한 애플리케이션(예: 시스템 드라이버, 저수준 도구)은 네이티브 언어가 유리하고, 다양한 플랫폼에서 일관되게 동작해야 하는 애플리케이션(예: 엔터프라이즈 소프트웨어, 웹 애플리케이션)은 JVM 기반 언어가 유리합니다.

오버헤드의 차이

두 접근 방식의 가장 큰 차이점 중 하나는 성능 오버헤드입니다:

  1. 네이티브 언어: System Call로의 전환만 오버헤드로 발생합니다. 즉, 사용자 모드 → 커널 모드 전환 비용만 지불합니다.
  2. JVM 기반 언어: JVM 추상화, JNI 변환, 그리고 사용자 모드 → 커널 모드 전환 등 여러 단계의 오버헤드가 발생합니다.

실제 벤치마크에 따르면, 단순한 System Call(예: 파일 열기/닫기)의 경우 JVM 언어는 네이티브 언어보다 2~10배 더 느릴 수 있습니다. 그러나 이 차이는 아래와 같은 상황에서 줄어듭니다:

  • 실제 작업(I/O, 연산 등)이 오래 걸리는 경우
  • JIT 컴파일러가 코드를 최적화한 경우
  • 반복적인 호출로 JVM이 최적화 기회를 얻은 경우

이는 마치 택시와 버스의 차이와 비슷합니다. 짧은 거리에서는 택시(네이티브)가 훨씬 빠르지만, 장거리에서는 고속버스(JVM)도 비슷한 시간이 걸릴 수 있습니다.

메모리 사용과 가비지 컬렉션

System Call 성능에 영향을 미치는 또 다른 중요한 요소는 메모리 관리 방식입니다:

  1. 네이티브 언어:
    • 명시적인 메모리 관리(malloc/free)로 인해 개발자가 완전히 제어할 수 있습니다.
    • 가비지 컬렉션으로 인한 지연이 없습니다.
    • 그러나 메모리 누수나 버퍼 오버플로우 위험이 있습니다.
  2. JVM 기반 언어:
    • 가비지 컬렉션이 자동으로 메모리를 관리합니다.
    • 가비지 컬렉션 중 일시적인 지연(Stop-the-World)이 발생할 수 있습니다.
    • 메모리 안전성이 더 높지만, 세밀한 제어는 어렵습니다.

System Call이 많은 애플리케이션에서는 가비지 컬렉션으로 인한 지연이 실시간 성능에 영향을 줄 수 있습니다. 이는 마치 고속도로에서 주행 중에 갑자기 연료(메모리)를 채우기 위해 정차하는 것과 비슷합니다.

결론: 각자의 장단점

JVM 기반 언어와 네이티브 언어의 System Call 접근 방식은 각각 고유한 장단점을 가지고 있습니다:

네이티브 언어의 강점:

  • System Call에 대한 직접적이고 효율적인 접근
  • 더 적은 런타임 오버헤드와 메모리 사용량
  • 운영체제와 하드웨어의 모든 기능에 완전한 접근

JVM 기반 언어의 강점:

  • 뛰어난 플랫폼 이식성과 "Write Once, Run Anywhere" 특성
  • 자동화된 메모리 관리와 향상된 안전성
  • 높은 수준의 추상화와 개발자 생산성

어떤 접근 방식이 "더 좋다"고 단정할 수는 없으며, 기술적으로 해결해야 하는 비즈니스 문제에 따라 적절한 진영을 선택하면 되겠습니다.

후기

후… 글 정리하는데 정말 힘들었습니다. 특히 적절한 비유를 생각해내는게 쉽지 않았는데 CS 개념을 나만의 비유를 만들어 설명하는 방식이 개념을 이해하는데 크게 도움이 되는 것 같았습니다.

또한 java는 언어와 운영체제 사이의 JVM 위에서 동작하지만, 결국 밑으로 들어가면 OS에서 배운 것과 똑같이 Native하게 동작한다는 것을 다시한번 느꼈습니다. 결국 OS 지식 없이는 언어의 동작 원리 무엇하나 이해할 수 없는 것이지요… Krafton Jungle에서 CS 중심의 학습을 하지 않았다면 이런 글을 도저히 적지 못했을 겁니다. 그리고 이번 Jungle CS Study를 통해 복습할 수 있어서 너무 좋네요. 추가로 공부해보고 싶은 내용도 마구마구 생각나구요! 다음 주제는 다른 스터디원 분들이 원하는 것으로 하기로 해 무엇이 될지 모르겠지만, 어떻게라도 자바와 엮어 준비해보겠습니다! 기다려주세용

출처

https://product.kyobobook.co.kr/detail/S000001868716

https://m.yes24.com/Goods/Detail/93738334

https://product.kyobobook.co.kr/detail/S000001550352

https://brewagebear.github.io/java-syscall-and-io/

https://inpa.tistory.com/entry/JAVA-☕-JVM-내부-구조-메모리-영역-심화편?category=976278

https://hongong.hanbit.co.kr/운영체제란-커널의-개념-응용-프로그램-실행을-위한/

 

 

'JAVA' 카테고리의 다른 글

Process Memory 구조와 JVM의 Memory 구조  (6) 2025.04.22

Intro

Jungle CS Study의 첫 주제는 “Process 개념과 관련지어, 자신이 사용 중인 기술 스택의 동작 원리에 대해 설명하자”이다.

상세 주제를 정하기 전에, Process란 무엇인가 복습해보자.

 

Process는,
OS가 Disk에 있던 Program Code를 읽어 RAM(Main memory)에 Load하여, 실행시킨 프로그램이다 (e.g., class와 instance의 관계)

 

프로세스가 thread, memory, system call 등을 모두 포함하는 워낙 포괄적인 개념이다 보니 구체적으로 어떤 주제를 잡을지 고민이 됐다… 그래도 요즘 포트폴리오로 만든 주변시위 Now라는 Spring App의 성능 테스트를 진행하면서 Prometheus와 Grafana로 JVM Memory metric을 접했기 때문에, “Memory”를 주제로 정했다! JVM도 하나의 Process니까, JVM의 메모리 구조를 까보는 것도 괜찮겠지?

Prometheus + Grafana를 통해 확인할 수 있는 JVM Memory 전체 구조
JVM Heap/Non-Heap Memory 구성요소. 각 요소들은 무엇을 뜻할까?

 

글의 목표

  • 일반적인 Virtual Memory 구조와 비교하여, JVM Memory 구조(Runtime Data Area)를 이해한다
  • JVM Memory 구조(Runtime Data Area)에서, 세부 구성 요소를 이해한다
    • Heap Area
      • Eden, Survivor Space
    • Non-heap Area
      • Meta Space

일반적인 Virtual Memory Segments

먼저, 흔히 배우는 가상 메모리 구조는 아래와 같다.

Code(Text) Segment

  • 실행 가능한 프로그램 코드(이진수로 된 기계어)가 저장되는 영역
  • 읽기 전용으로 설정되어 있어 프로그램이 자신의 코드를 수정할 수 없음
  • CPU의 Program Counter Register가 해당 영역의 주소를 저장한다
  • 프로그램이 메모리에 로드될 때 영역의 크기가 정해짐

Data Segment

  • Global 변수나, Static 변수 등 프로그램 전체에서 사용되는 데이터가 저장되는 공간
  • Code 영역과 마찬가지로, 프로그램 시작 시 할당되고 종료 시 해제됨
  • 크게 2가지의 Data 영역이 있다
    1. Initialized Data Segment
      1. Read-Only Data Segment
        • 상수와 문자열 리터럴이 저장됨
        • 실행 중에 수정이 불가능한 영역
        • 메모리 보호 기능으로 쓰기 시도 시 세그먼테이션 오류 발생
        • 예: const int MAX_VALUE = 100;, 문자열 상수 "Hello, World"
      2. GVAR(Global Variables) Segment
        • 초기값이 있는 전역 변수와 정적 변수 저장
        • Read/Write 모두 가능
        • 예: int globalCounter = 0;, static int count = 10;
    2. UnInitialized Data Segmnet
      1. BSS(Block Started by Symbol) Segment
        • 초기화되지 않은 전역 변수와 정적 변수가 저장
        • 프로그램 로드 시 0으로 초기화됨
        • Read/Write 모두 가능

힙(Heap) 영역

  • 동적으로 할당된 메모리를 관리하는 영역
  • 프로그램 실행 중 크기가 변할 수 있음
  • 메모리 할당(malloc/new)과 해제(free/delete)가 명시적으로 이루어짐
  • 낮은 주소에서 높은 주소 방향으로 증가

스택(Stack) 영역

  • 함수 호출 정보, 지역 변수, 매개변수 등이 저장
  • 함수가 호출될 때 스택 프레임이 생성되고 반환될 때 제거됨
  • 높은 주소에서 낮은 주소 방향으로 증가
  • 각 스레드는 독립적인 스택을 가짐

JVM Memory Segments(Runtime Data Area)

이제 JVM의 메모리 구조를 살펴보자.

JVM의 메모리 구조는 크게 모든 JVM 쓰레드가 공유하는 영역(Shared Memory)와 쓰레드마다 개별적으로 가지는 영역으로 구분된다. 그리고 일반적인 memory 구조에는 없는 영역들도 있는데, 하나하나 뭐 하는 녀석들인지 살펴보자.

Heap Memory

  • 모든 JVM 스레드가 공유하는 메모리 영역
  • new() 연산자로 생성한 객체와 배열이 저장되는 곳
  • 전체 크기는 한정되어 있지만, 사용 크기는 앱 실행 중에 동적으로 증가하거나 감소함
  • Garbage Collector(GC)에 의해 자동 관리됨
  • 종류
    • Young Generation
      • Eden Space
        • 새로 생성된 대부분의 객체가 처음 위치하는 영역
      • Survivor Spaces - S0, S1
        • 가비지 컬렉션 후 살아남은 객체가 이동하는 두 개의 영역
        • ‘From’, ‘To’ 영역이라 부르기도 함
    • Old Generation
      • Young Generation에서 오랫동안 살아남은 객체들이 이동하는 영역
      • 보통 더 큰 메모리 공간을 차지하며, Full GC의 대상이 됨

Metaspace

  • JVM 내에서 실행될 수 있는 byte code인 class file format(.class)의 정보를 저장(class에 대한 meta data)
    • constant pool, static variables, member variables/methods에 대한 정보 등
    • Java 8 이전과 이후로 포함 영역이 다름!

  • Java 8 이전: Permanent Generation로 구현
  • Java 8 이후: 네이티브 메모리에 있는 메타스페이스로 대체

 

코드 캐시(Code Cache)

  • JIT(Just-In-Time) 컴파일러가 네이티브 코드를 저장하는 영역
  • 자주 실행되는 메서드의 컴파일된 코드가 저장됨

JVM 스택(JVM Stack)

  • 각 스레드마다 독립적인 스택 생성. Stack 안에 “Frame”이란 것이 함께 만들어진다
  • Frame
    • frame은 메소드가 호출될 때마다 만들어지먀, 메소드의 상태 정보를 저장함
    • frame의 3요소: 1) Local Variables 2) Operand Stack 3) Frame Data
    • Stack Frame의 크기는 Compile time에 정의됨

Program Counter Register

  • 각 스레드마다 생성되는 레지스터
  • 현재 실행 중인 JVM 명령의 주소를 가리킴. Native 명령어의 주소값은 저장하지 않음
  • PC Registers는 Register-base로 구동되는 방식이 아니라 Stack-base로 작동

Native Method Stack

  • java code와 상호작용하는 native methods(C/C++ …)의 실행을 관리하기 위한 스택
  • C Stack이라고도 불림
  • 고정 크기 또는 동적 확장/축소 가능

일반적인 Memory 구조와 JVM의 메모리 구조 비교

이제 JVM의 메모리 구조에 대해서도 살펴보았다. 위 구조를 한 눈에 비교해서 보자

일반적인 Memory 구조
JVM 메모리 구조

주요한 차이 요약은 아래와 같다.

특성  일반 프로세스 JVM
코드 저장 Code Segment(기계어) Metaspace(바이트코드)
전역 데이터 Data Segment/BSS Metaspace, Heap
동적 메모리 Heap(명시적 관리) Heap(자동 GC 관리, 세대별 구조)
지역 변수 Stack JVM Stack(with Frame)
메모리 관리 프로그래머 책임 자동(GC)
메모리 안전성 낮음(버퍼 오버플로우 등 가능) 높음(범위 검사, 타입 안전성)
메모리 모델 운영체제 관리 JVM 관리
추가 구조 단순 PC Register, C Stack 등
플랫폼 의존성 높음 낮음(WORA - Write Once Run Anywhere)
성능 특성 직접 접근으로 빠름, 오버헤드 적음 GC 일시 정지, 추가 추상화 레이어로 오버헤드 있음

이제 상세한 비교 내용을 보자.

Code Segment 비교

  • 실행될 프로세스의 명령어 집합이 저장되는 곳에 대한 비교를 해보자

일반: Code Segment

  • 기계어로 컴파일된 실행 파일의 코드가 직접 저장됨
  • 읽기 전용으로 설정되어 수정 불가능(보안 강화)
  • 예: C 프로그램의 함수 코드는 컴파일되어 기계어로 변환된 후 이 영역에 로드됨

JVM: Metaspace, Code Cache

Metaspace

  • 클래스 파일의 바이트코드가 저장됨
  • 클래스 구조, 메서드, 필드, 상수 풀 정보 포함
  • 예: Java 클래스의 정적 메서드, 상수, 클래스 메타데이터가 이 영역에 저장됨
  • 네이티브 메모리(OS가 관리하는 메모리)에 할당됨

Code Cache

  • 자주 실행되는 메서드의 JIT 컴파일된 코드가 저장됨

주요 차이점:

  • 일반 프로세스는 기계어 코드를 직접 실행하지만, JVM은 바이트코드를 해석하거나 JIT 컴파일을 통해 실행
  • 일반 프로세스의 코드 세그먼트는 일반적으로 프로세스 수명 동안 고정되지만, JVM의 Metaspace 영역은 런타임에 클래스 로딩/언로딩에 따라 크기 변화가 가능

데이터 영역 비교

  • 전역/정적 변수들이 저장/관리 되는 방식은 어떻게 다를까?

일반: Data Segment

  • 초기화된 데이터 세그먼트: 명시적으로 초기화된 전역/정적 변수 저장
  • BSS: 초기화되지 않은 전역/정적 변수 저장(0으로 초기화됨)
  • 프로그램의 전체 실행 기간 동안 존재
  • 예: int global_var = 100; (초기화된 데이터), static int uninitialized_var; (BSS)

JVM: Metaspace, Heap

  • 클래스의 정적 변수는 Metaspace에 저장
  • 참조 타입 정적 변수의 실제 객체는 JVM Heap에 저장됨
  • 예: static int counter = 0;는 metaspace에 저장, static Student student = new Student();에서 참조는 metaspace에, 실제 Student 객체는 힙에 저장

주요 차이점:

  • 일반 프로세스는 데이터 타입에 따라 직접 값을 저장하지만, JVM은 참조 타입의 경우 참조만 저장하고 실제 객체는 항상 힙에 저장
  • JVM에서는 클래스 로딩 시 정적 필드가 초기화되며, 클래스 언로딩 시 제거될 수 있음

Heap Segment 비교

  • Reference type이 주로 저장되는 Heap 영역에는 어떠한 차이가 있는지 알아보자.

일반: Heap

  • malloc(), calloc(), realloc() 등의 함수를 통해 명시적으로 메모리 할당
  • free() 함수를 통해 명시적으로 메모리 해제 필요(메모리 관리는 프로그래머 책임)
  • 할당과 해제가 반복되면서 메모리 단편화 발생 가능
  • 범위를 벗어난 메모리 접근, 해제된 메모리 접근, 메모리 누수 등의 문제 발생 가능
  • 예:
  • int* array = (int*)malloc(10 * sizeof(int)); // 사용 후 free(array);

JVM: Heap

  • 모든 객체 인스턴스와 배열이 저장됨
  • 자동 메모리 관리(가비지 컬렉션)을 통해 더 이상 참조되지 않는 객체 자동 제거
  • 세대별 가비지 컬렉션을 위해 Young/Old 영역으로 구분
  • Young 영역은 다시 Eden과 두 개의 Survivor 공간으로 나뉨
  • 예:
  • ArrayList<String> list = new ArrayList<>();// 힙에 할당 list = null;// 참조 제거, GC 대상이 됨

주요 차이점:

  • JVM은 자동 메모리 관리(GC)를 제공하여 메모리 누수, 댕글링 포인터 문제 감소
  • JVM 힙은 세대별 구조로 최적화되어 단기 객체와 장기 객체를 효율적으로 관리
  • 일반 프로세스는 메모리 할당 크기를 직접 지정하지만, JVM은 객체 크기를 자동으로 계산
  • 일반 프로세스는 메모리 해제 시점을 프로그래머가 결정하지만, JVM은 가비지 컬렉터가 결정

Stack Segment 비교

일반: Stack

  • 함수 호출 정보(활성화 레코드/스택 프레임)와 지역 변수 저장
  • LIFO(Last In First Out) 구조로 작동
  • 컴파일 시간에 크기가 결정되는 지역 변수는 스택에 할당
  • 함수 호출 시마다 새로운 스택 프레임 생성
  • 재귀 호출이 너무 깊거나 지역 변수가 너무 많으면 스택 오버플로우 발생 가능
  • 예:
  • void function() { int local_var = 10;// 스택에 할당// ... }// 함수 종료 시 스택 프레임 제거

JVM: Stack(with Frame)

  • 각 스레드마다 별도의 JVM 스택 생성
  • 메서드 호출마다 Stack Frame 생성
  • 스택 프레임은 로컬 변수 배열, 피연산자 스택, 프레임 데이터 포함
  • 원시 타입 변수와 객체 참조값은 스택에 저장(객체 자체는 힙에 저장)
  • 예:
  • void method() { int x = 10;// 스택에 저장 Object obj = new Object();// 참조는 스택에, 객체는 힙에 저장 }// 메서드 종료 시 스택 프레임 제거

주요 차이점:

  • 일반 프로세스 스택은 주로 함수 실행 상태와 지역 변수 저장에만 집중한다
  • 반면, JVM Stack은 기본적은 stack의 기능과 더불어 method 호출 단위의 Frame이란 개념이 추가되며, byte code 실행과 관련된 기능을 수행하기도 한다
    • JVM 스택은 스레드마다 독립적으로 생성됨
    • JVM 스택 프레임은 피연산자 스택을 포함하여 바이트코드 실행을 지원
    • JVM 스택은 참조 타입의 경우 참조만 저장하고 실제 객체는 항상 힙에 저장됨
    • JVM 스택은 현재 실행 중인 바이트코드에 대한 정보를 포함

추가 JVM 메모리 구조

  • 일반적인 memory 구조에는 없는, JVM에만 있는 memory 영역 또한 존재한다

PC 레지스터

  • 각 스레드마다 별도의 PC 레지스터 존재
  • 현재 실행 중인 JVM 명령어 주소(바이트코드 오프셋) 저장
  • 네이티브 메서드 실행 시 undefined 값 가짐

네이티브 메서드 스택

  • JNI(Java Native Interface)를 통해 호출된 네이티브 메서드를 위한 스택
  • C 스택이라고도 불림
  • 일반 프로세스의 스택과 유사하게 작동

주요 차이점:

  • JVM은 ‘Write Once Run Anywhere’라는 철학으로 만들어졌다. 따라서 특정 OS나 Hardware에 종속되지 않고자 위와 같은 요소를 메모리 구조에 추가하였다

후기

오랜만에 CS 공부를 하니 시간 가는 줄 모르고 정말 재미있게 했다. 특히 요즘 prometheus로 JVM Metric을 수집하는데, JVM 관련 지식에 목말랐던 터라 이번 JVM의 메모리 구조에 대해 공부하는게 더욱 흥미로웠다. 이런 식으로 일반적인 CS 개념이 내가 요즘 궁금해하고 사용하는 기술과 비교하여 공부하니 학습 효과도 더욱 뛰어난 것 같다. 다만 아쉬운 점도 있다. JVM의 Memory 구조에는 ‘Write Once Run Anywhere’라는 java의 철학을 반영하기 위해 존재하는 영역이나 방식이 많아, Java가 이러한 철학을 지키기 위한 전체적인 흐름(JRE, Java Runtime Environment)에 대해 알고 있었다면 더욱 빠른 학습과 깊이 있는 이해가 가능했을 것 같다. 그래도 이번 주제를 열심히 준비한 덕분에 java의 실행 방식에 대해 전체적인 그림을 익힐 수 있었다.

또 이번 글을 준비하며 다음과 같은 주제에 흥미가 생겼다

  • java의 전체적인 동작 흐름(compile부터 종료까지)
  • java의 system call 동작 과정
  • JVM Memory 최적화
  • JIT Compile

앞으로 스터디 할 날이 많으니, 차차 다뤄봐야겠다.

 

출처

https://hongsii.github.io/2018/12/20/jvm-memory-structure/

다이어그램

https://jaemunbro.medium.com/java-metaspace에-대해-알아보자-ac363816d35e

https://johngrib.github.io/wiki/java8-why-permgen-removed/

https://johngrib.github.io/wiki/jvm-stack/

https://loosie.tistory.com/846

https://help.sap.com

https://kotlinworld.com/308

https://velog.io/@lucius__k/JIT-컴파일러와-코드-캐시

'JAVA' 카테고리의 다른 글

System Call과 JVM  (3) 2025.04.28
정글에서 살아남기

 

 

지나온 과거에 대한 성찰

  입대 전까지는 개발의 근간에 대해 고민하지 않고 그저 앞에 닥친 문제를 해결하기 위해 애쓰는 하루의 연속이었다. 나름 애쓰다 보니 눈 앞의 문제들은 어느정도 해결할 수 있었다. 하지만 일을 하면 할 수록 내가 만든 것들의 동작에 대한 이해 없이는 더 레벨업하기 힘들겠다는 것이 느껴졌다. 그러던 와중 갑작스런 입대를 하게 됐다.

 

  군복무 하며 이런 나의 부족한 cs 지식을 채우고 싶었다. 하지만 달마다 1~2주의 훈련이나 휴가를 다녀오면 공부의 맥락이 끊기기 일수였다. 아무리 책을 붙들어도 연속성이 떨어져 머리에 남는게 없었다. 공부 내용과 방법에 대해 시행착오를 하느라 많은 시간이 소비되었다. 한창 사회활동 할 시기에 군복무 중인 것도 너무 시간 아까운데 공부마저 내 마음대로 안되자 배움에 대한 아쉬움은 더욱 커져만 갔다.

 

5개월 동안 얻어가고 싶은 것

  그러던 중 정말 감사하게도 크래프톤 정글 6기 과정을 시작하게 됐다. 이번 과정을 통해 얻고 싶은 것은 다음과 같다.

- Algorithm, Data Structure, OS, Network, DB와 같은 핵심 CS 지식

- 하루 12시간 이상 주 6일 꾸준히 공부하는 습관

- 앞으로의 개발 인생에서 서로 믿고 의지할 수 있는 동료

 

임하고 싶은 자세

  목표로 하는 것을 얻기 위해선 정말 많은 노력이 필요하다. 배울 것은 산더미고 아무리 공부를 많이 한다고 한들 주어진 시간은 충분하지 않다. 그렇다고 포기할 수는 없다. 목표를 크게 가져야 그 일부라도 달성한다. 또 아무리 거대한 목표라도 그것을 최대한 잘게 sub goals로 쪼개고, 그것을 달성하는데 하루하루 집중하다 보면 어느새 수많은 계단을 올라와 있을거라 믿는다.

 

  공부에는 끝이 없다. 마라톤을 스프린트로 뛰면 결코 완주할 수 없다. 5개월의 정글 과정 중에도, 끝난 후에도 꾸준히 하기 위한 시간 관리와 체력 안배 또한 매우 중요하다. 공부만큼 휴식과 운동 또한 잘 챙겨보는 것이 목표다. 강렬한 빛을 내지만 짧게 끝나고 마는 초신성보다, 밝기는 상대적으로 약할 수 있어도 끊임없이 빛나는 항성이 되고 싶다.

 

정글이 끝난 후 나의 모습

  본 과정이 끝난 후 원하는 모습은 간단하다. 위에서 언급한 5개월 동안 얻어가고 싶은 것을 모두 가진 모습이다. 본 과정이 내 모든 문제를 해결해줄거라 기대 하지 않는다. 적어도 이 곳에서의 목표는 모두 이루고, 그것을 양분 삼아 더 빠르게, 더 오래 달릴 수 있는 기초체력이 완성되기를 바란다. 과정 이후로도 만들고 싶은 프로덕트를 만들며, 이를 바탕으로 나를 다음 단계로 성장시킬 수 있는 조직에서 내 몫의 역할을 하며 사회에 기여하겠다.

 

 

 

유튜브 영상 controller는 약 3초 동안 사용자의 움직임이 없으면 영상 controller를 숨김.

이를 react hook으로 간단히 만들어 봄

 

import { useEffect, useRef, useState } from 'react';

export default function useVideoControllers(playing: boolean) {
  const [showControllers, setShowControllers] = useState(true);

  const timeout = useRef<number | null>();

  useEffect(() => {
    const showControllers = () => {
      setShowControllers(true);
      if (timeout.current) window.clearTimeout(timeout.current);
      timeout.current = window.setTimeout(() => {
        setShowControllers(false);
      }, 3500);
    };
    window.addEventListener('mousemove', showControllers);
    return () => window.removeEventListener('mousemove', showControllers);
  }, []);

  return { showControllers };
}

 

알게된 것

  • state가 바뀌면 그냥 local 변수는 값의 재할당이 일어난다
    • re-render와 상관없이 data를 유지하고 싶으면 ref를 꼭 쓰자!!! useRef 쓸 일이 생각보다 정말 많네
  • 등록한 setTimeout은 clearTimeout으로 없앨 수 있다
  • setTimeout의 return 값은 number 형식의 id 이다

문제 상황

회사 일로 react-sortable-tree를 쓰는데, 'Cannot have two HTML5 backends at the same time.'라는 에러가 떴음

문제는 말 내가 한 페이지 내에서 두개 이상의 SortableTree를 사용하기 때문이었음

https://github.com/frontend-collective/react-sortable-tree/issues/177

해결 시도

찾아보니, SortableTreeWithoutDndContext를 쓰고, dnd를 내 컴포넌트에서 주입해주면 된다고 함. 

https://github.com/frontend-collective/react-sortable-tree/issues/511

그렇게 했지만, 렌더는 잘됐던게 아무것도 안뜸. 서치를 더 해보니 밖에서 주입해야하는 dnd가 적용이 안되서 그럼. 분명 안될리가 없는데 안됐음

 

해결

이슈를 계속 뒤적여보니, react-dnd가 내 package와 sortableTree 두 군데서 따로 load되어 사용되기 때문이라는 코멘트가 보였음

나를 살린 한 문장

https://github.com/frontend-collective/react-sortable-tree/issues/665

그래서 npm dedupe --legacy-peer-deps를 적용하니 문제가 해결됨!!

 

npm dedupe

여러 패키지에서 공통으로 사용하는 패키지를 정리해줌

https://outofbedlam.gitbooks.io/npm-handbook/content/cli/npm-dedupe.html

Next.js에서 window 등 client 객체 사용하기

  • Next.js는 기본적으로 SSR임
  • 따라서 평소처럼 window, document 등 client에서 쓰는 객체를 평범하게 사용하려고 하면 반드시 error가 남
  • 위와 같은 객체들은 꼭 useEffect를 통해 mount 되고 사용하는 것을 추천함

Next.js에서 window 객체를 사용해 화면 크기를 구하는 hook 예제

https://stackoverflow.com/questions/63406435/how-to-detect-window-size-in-next-js-ssr-using-react-hook

 

How to detect window size in Next.js SSR using react hook?

I am building an app using Next.js and react-dates. I have two component DateRangePicker component and DayPickerRangeController component. I want to render DateRangePicker when the window's width is

stackoverflow.com

 

tailwind css의 mobile first

  • tailwind는 mobile first임
  • 따라서 반응형으로 스타일링을 할 때, mobile을 default로 하고, pc를 옵션으로 설정해야 함(mobile을 타겟으로 하는 것들 sm:...등은 사용하지 않아야 함

https://tailwindcss.com/docs/responsive-design#mobile-first

 

Responsive Design - Tailwind CSS

Using responsive utility variants to build adaptive user interfaces.

tailwindcss.com

 

1. 좋은 개발자, 좋은 개발 팀이 되기 위한 조언

1) 남을 가르치는 기회를 가져보라 - 내부 컨퍼런스 등

  • 남을 가르치려면 내가 알고 있는 정보 정리를 잘 해야 함
  • 교육을 준비할 때 내가 무엇을 알고 모르는지 정확히 알 수 있음

 

2) 코드리뷰를 하라

  • 하나의 기능을 구현하는 방법은 수백가지임
  • 반면 한 명의 개발자가 생각해낼 수 있는 방법은 소수에 불과함
  • 코드리뷰를 통해 다른 사람의 코드를 참고하고 서로 대화하면, 최적의 구현법을 찾는데 매우 큰 도움이 됨
  • 코드리뷰 프로세스는 개발자 개인의 성장을 위해 매우 필요함
  • 하지만, 코드리뷰가 자리 잡기 위해선 조직적인 합의가 필수임
  • 리뷰 초반에는, 아무리 바쁘더라도 퇴근 전 하루 한 번은 꼭 코드리뷰 하는 것을 추천함

 

3) 테스트 코드를 작성하라

  • 테스트 코드는 경제적임
  • 서비스 / 비즈니스에 따라 적절한 테스트 커버리지가 다름
  • 테스트 코드는 새로운 개발자 landing하는 데에도 좋음

 

4) Software Architecture 문서를 작성하라

  • 비즈니스 / 팀의 상황과 특성에 따라 중요한 특성(Quality Attribute)이 다름
  • Software Architecture 문서는 Quality Attribute 등 “개발 철학”을 정의한 문서임

(Software architecture in practice 책 추천)

 

 

5) 개발 스터디를 하라

  • 공부에는 강제성과 관성이 필요함
  • 혼자하는 공부는 위 두 요소를 충족시키기 어려움 → 지속하기 힘듦
  • 29살 때는 2주 간격의 스터디를 6개 정도 진행함
  • 지금까지도 개발 스터디 하는 중. 아래의 책으로 진행하고 있음

 

6) 문서 작성 또한 중요하다

  • 최상의 엔지니어는 스펙을 정의하고 정리하는 사람임
  • Use Case Specification 문서 정도는 만들어 보는 것을 추천함

 

7) 해외 개발 컨퍼런스에 참가하라

  • 해외 컨퍼런스에는, 유명한 언어 / 프레임 워크의 창시자들을 직접 만나고 질문할 수 있음
  • 그러기 위해 영어공부는 매우 필수임!

 

8) 이슈 트래킹 툴 도입 추천

  • 숫자로 이슈와 관련된 내용들을 트래킹 하는 것이 중요함
  • 개발 조직이 크기 위해 체계와 체계를 실천하기 위한 이슈 트래킹 툴과 같은 도구가 필요함

 

2. Q&A

Q. CS 지식은 언제, 어떻게 공부하는 것이 좋은가?

A. 필요할 때 공부하는게 제일 좋다. 공부한 내용을 체화하려면 피부에 와닿는 현실에서의 이슈가 필요하다

 

Q. 좋은 코드, 좋은 개발자란?

A. 아래와 같이 생각한다

  • 좋은 코드 - simple & solid한 코드. 오랜 세월 계속 사용할 수 있는, 높은 커버리지, 유연함 등
  • 좋은 개발자 - 편견이 없는 유연한 개발자. 다양한 언어 / 소프트웨어의 철학을 이해하고 적절히 조합하여 사용할 수 있는 개발자

 

3. 기타

  • 추천하는 책 목록
    • Design Pattern
    • Refactoring
    • Optimal Java
    • Clean Architecture
  • 성장하지 못하는 개발자들은 난관에 부딪혔을 때 고민 없이 바로 물어봄
    • 훌륭한 개발자가 되기 위해선, 충분히 혼자 고민을 해야 함
    • 고민을 바탕으로 자신만의 질문을 정교화하고, 그 질문에 대한 자신만의 가설 or 결론을 세워야 함
    • 그 뒤에 질문을 해야 함
  • 2030 개발자들에게 꼭 추천하는 것!
    • 연애를 하세요… (40대 이상의 개발 독거노인들 많다..)
    • 영어공부 열심히 하세요
    • 계속 꾸준히 공부하세요. 혼자 힘들면 스터디를 하세요

+ Recent posts