SOFTWARE FUNDAMENTALS

소프트웨어 엔지니어로서 중요한 두 가지를 뽑으라면 CRITICAL THINKINGABSTRACTION 라고 생각하며, 뛰어난 소프트웨어 엔지니어는 문제를 깊이 이해하는 비판적 사고와 복잡함을 단순화하는 추상화 능력을 갖추고 있다.

CRITICAL THINKING

소프트웨어를 만드는 이유는 문제를 해결해서 가치를 창출하기 위함이다. 소프트웨어 엔지니어는 문제를 정의하고 해결하며 가치를 만드는 사람이다.

소프트웨어 엔지니어에게 가장 중요한 것은 문제 해결 능력인데, 문제 해결 능력에서 가장 중요한 것은 문제 정의 이며, 문제 정의를 잘 하기 위해서 필요한 능력은 CRITICAL THINKING 이다.

비판적 사고(critical thinking)는 정보를 객관적으로 분석하고 평가하여 합리적인(rational) 판단을 내리는 사고 과정을 말한다. 즉, 체계적이고 논리적으로 사고하는 능력을 의미한다.

문제 해결을 위해서는 아래와 같은 단계를 거치는데 최악의 경우는 문제 정의를 잘못해서 구현 및 검증 단계에서 문제 정의를 다시해야함을 깨닫는 것이다. 특히 실타래 처럼 꼬여져있는 코드 및 정책과 복잡한 도메인 다루는 서비스에서 버그가 발생하여 로그를 보면서 트러블슈팅(troubleshooting) 을 하는 경우 문제 정의를 잘못하면 시간을 허비하게 된다.

  • 문제 정의: 정확히 어떤 문제를 해결해야 하는지 명확히 이해하고 문제의 범위와 제약조건을 파악, 문제의 근본 원인을 파악하는 단계
  • 가설 수립: (최적의) 해결책을 구상하는 단계
  • 구현 및 검증: 구현 및 검증하는 단계

문제 정의를 잘하기 위해서는 어떻게 해야할까?

나는 문제를 정확히 하고 논리적인 사고를 구조화하고, 오류 등을 발견하기 위한 도구로서 주석, 노트, 화이트보드 등을 이용하여 자신의 손으로 직접 문제 정의를 하는 것이 정말 많은 도움이된다. 특히 복잡한 문제일 수록 더욱 도움이 된다. 코딩 테스트나 시스템 아키텍처 면접 등에서도 자신이 이해한 바를 손으로 잘 정리하는 습관을 들이면 좋다.

나의 주니어 개발자 시절 경험을 풀어보자면, 코드를 작성하기 전에 해결해야하는 문제가 복잡하거나 어렵다고 느껴지면 항상 주석으로 먼저 문제를 정의하고 코드를 작성했다.

2년차 개발자 시절에는 대전의 공단으로 파견을 나가 '소상공인 임차인 확인서 발급 시스템' 을 혼자서 구축했었다. 환경은 JDK5 에다 Legacy Code 로 가득하였으며 상당히 나에겐 어려운 개발 환경이었다. 배포 이후 운영 과정에서 문제가 없었다면 좋았겠지만, 문제가 발생했고 그 문제를 해결하기 위해서 로그와 각종 컨텍스트들을 수집하면서 A4 용지에 손으로 기록해가며 문제를 해결했던 경험이 있다.

2021.04.26 - Critical Thinking

AI 가 많이 활용되고 있으며, 대부분의 코딩을 Agent 와 같이한다. Agent 에게 작업을 지시하기 전에 TASK.md 를 작성하는 것은 AI 가 문제를 올바르게 이해하고 코드를 작성해줄 가능성이 높아지기 때문에 더욱 중요하며, TASK.md 자체가 일종의 문서화가 될 수 있다.

손으로 문제를 정의하는 습관을 잘 들이지 않는 개발자들이 있지만 앞으로 AI 시대에서는 더욱 필수적으로 변할 것이다. 그리고 이러한 능력을 갖추고 있는지를 면접때 평가하려하지 않을까 생각한다.

ABSTRACTION

코드나 시스템 아키텍처를 설계할때 ABSTRACTION 는 매우 중요하다. 추상화를 디자인하는 과정(Designing Abstractions) 은 근본적인 복잡성을 숨기고 시스템을 사용하는 사람 또는 대상에게 더 간단한 인터페이스를 제공하는 과정을 의미한다.

시스템 설계 면접을 진행하다보면, 대부분의 사람들은 요구 사항을 구체화 하기보다 LB, WAS, DB, KAFKA 형태의 그림을 그려놓는 것을 답으로 생각하는 면접자들을 많이 볼 수 있다. 시스템 설계는 요구 사항으로 부터 발전하는 것인데 문제 정의 를 제대로 하지 않고 진행한다면 감점 요인이지 않을까? 생각된다.

QUEUE, BUFFER, CACHE, LOCK, TRANSACTION, STREAM, FUTURE, SINK 등은 복잡한 내부 동작이나 구현을 숨기고, 사용자가 이해하기 쉬운 모델이나 규칙을 제공하는 추상화된 개념이며, 이러한 추상화된 개념을 이해하고 다양한 상황에서 적절하게 쓸 수 있는지? 등이 중요하다고 생각한다.

예를 들어 BUFFER 는 일반적으로 데이터가 한 곳에서 다른 곳으로 이동하는 동안 일시적으로 저장하는 데 사용되는 메모리 영역을 의미하며, 생산과 소비의 속도 차이를 해결하기 위한 '임시 저장 공간' 개념을 추상화한 것이다.

네트워크에서 NIC(네트워크 인터페이스 카드)에 패킷이 도착하면, 해당 하드웨어는 패킷을 처리하기 전에 내장 버퍼에 일시적으로 저장한다. 이 버퍼는 패킷의 데이터와 헤더 정보를 임시로 보관하는 공간으로, 이후 운영체제(OS)의 TCP/IP 스택에서 패킷의 순서를 재조합하고, 오류를 검사하며, 최종 애플리케이션으로 전달하는 등의 후속 처리를 수행할 수 있도록 돕는다.

  1. 패킷 도착 및 임시 저장: 네트워크 케이블을 통해 패킷이 도착하면, NIC 의 하드웨어는 이 패킷을 받아들여 자체적으로 가지고 있는 내장 버퍼에 저장한다.
  2. 버퍼의 역할: 이 버퍼는 CPU 가 데이터를 처리할 때까지 패킷들을 보관하는 역할을 한다. 다양한 속도로 도착하는 패킷들을 잠시 모아두어 데이터의 손실을 방지하고 CPU 가 처리할 수 있는 양으로 묶어 전달할 수 있게 한다.
  3. 운영체제로 전달: 버퍼에 저장된 패킷들은 운영체제의 네트워크 스택(OS 프로토콜 스택)으로 전달된다.
  4. 프로세스 수행:
    • 운영체제는 이 패킷들에 대해 다음과 같은 처리를 수행한다.
    • 에러 검사: 패킷에 오류가 없는지 확인한다.
    • 순서 재조합: 만약 패킷의 순서가 뒤바뀌어 도착했더라도, 버퍼에 저장된 정보를 이용하여 올바른 순서로 데이터를 재조합한다.
    • 애플리케이션 결정 및 전달: 패킷이 어느 애플리케이션을 위한 것인지 결정하고 해당 애플리케이션의 버퍼로 패킷을 전달한다.

이 전체 과정을 '스마트 물류 창고'에 비유할 수 있다.

  • 하역장 (NIC 버퍼): 수많은 트럭(네트워크)들이 쉴 새 없이 가져오는 택배(패킷)를 일단 내려놓는 공간이다.
  • 자동 분류 시스템 (OS 네트워크 스택):
    • 컨베이어 벨트 위에서 택배의 바코드를 스캔하며 파손 여부를 확인합니다. (에러 검사)
    • 같은 주문 번호로 나뉘어 온 여러 개의 박스를 하나로 모읍니다. (순서 재조합)
    • 최종 주소를 확인하여 지역별 배송 라인으로 보냅니다. (애플리케이션 결정)

이번엔 QUEUE 에 대해서 살펴보자.

핵심 추상화 원칙:

  • FIFO(First In, First Out): 먼저 들어온 것이 먼저 나가는 공정한 처리 순서
  • 비동기 처리: 생산자와 소비자가 서로 다른 속도로 동작해도 안정적 연결
  • 부하 분산: 순간적인 요청 폭증을 완충하여 시스템 안정성 확보

큐가 포화 상태일 때의 제어 메커니즘(Back Pressure):

  • Flow Control: 생산자의 전송 속도 조절
  • Circuit Breaker: 일시적 큐 접근 차단
  • Adaptive Batching: 큐 상태에 따른 배치 크기 동적 조절

메시지 큐는 분산 시스템에서 시간적, 공간적 결합을 분리하는 추상화 계층을 제공한다.

  • 시간적 분리: 생산자와 소비자가 동시에 활성 상태일 필요 없음
  • 공간적 분리: 서비스들이 서로의 위치나 내부 구현을 알 필요 없음
  • 스케일 분리: 각 서비스가 독립적으로 확장 가능
  • 실패 격리: 한 서비스의 장애가 전체 시스템에 전파되지 않음

Kafka 와 같은 메시징 시스템은 QUEUE 의 개념을 핵심으로 사용한다. MSA 환경에서 서비스 간의 결합도를 낮추고 비동기 통신을 구현하기 위해 메시지를 큐에 넣고(Publish), 다른 서비스가 큐에서 메시지를 가져가(Subscribe) 처리하는 방식으로 동작한다.

이렇게 각 추상화된 개념들을 이해하고 요구 사항에 맞는 제품을 적절히 선택하는 능력이 중요하다.