[SkyStat] 아키텍처 고민: 왜 헥사고날 인가?

1. 암호문 해독을 위한 설계

지난 글에서 항공 기상 통계 서비스 ‘SkyStat’의 탄생 배경이 암호문 같은 기상 전문(METAR)을 엑셀로 수작업하던 고통 때문이었다고 말씀드렸습니다.

SkyStat의 핵심 미션은 명확합니다. 이 난해한 텍스트 덩어리인 METAR를 프로그래밍적으로 정확하게 읽어내는 것입니다.

METAR 예시: RKSI 071330Z 32014KT 9999 FEW030…

컴퓨터가 이 문자열에서 시정(Visibility), 바람(Wind), 구름(Cloud) 등의 정보를 각각 추출해낼 수만 있다면, “월별 저시정 발생 일수”나 “계절별 강풍 빈도”와 같은 구체적인 통계치는 그 정보들을 조합하여 계산해낼 수 있기 때문입니다.

본격적인 개발에 앞서, 저는 이 핵심 기능을 가장 잘 구현하고 유지보수할 수 있는 시스템 아키텍처가 무엇일지 고민에 빠졌습니다.

2. 도메인 분석: 변하는 것과 변하지 않는 것

아키텍처를 결정하기 위해 가장 먼저 한 일은 SkyStat이 다루는 도메인(Domain)의 특성을 분석하는 것이었습니다. 저는 두 가지 핵심적인 특징을 발견했습니다.

첫째, 핵심 도메인은 변하지 않는다. (The Core is Stable)

METAR는 ICAO(국제민간항공기구)와 WMO(세계기상기구)가 정한 엄격한 국제 표준을 따릅니다. 이 표준은 수십 년간 큰 틀을 유지해왔으며, 앞으로도 급격하게 변할 가능성은 매우 낮습니다.

즉, 텍스트에서 시정, 바람, 구름 정보를 파싱(Parsing)하는 핵심 비즈니스 로직은 매우 안정적이며 견고한 영역입니다.

둘째, 요구사항은 언제든 변한다. (Use Cases are Volatile)

반면, 추출된 정보를 바탕으로 산출해야 하는 ‘통계’는 다릅니다. 오늘은 “최근 3년간 인천공항의 안개 발생 일수”가 필요했지만, 내일은 “제주공항의 여름철 측풍(Crosswind) 빈도”가 필요할 수도 있습니다.

즉, 핵심 정보(시정, 바람 등)를 ‘조회’하는 기능이 있다면, 이를 활용한 통계치 산출 기능은 필요에 따라 얼마든지 추가, 변경, 제거될 수 있는 가변적인 유스케이스(Use Case) 영역입니다.

3. 의사 결정: 헥사고날 아키텍처 (Hexagonal Architecture)

분석 결과는 명확했습니다.

  • 변하지 않는 견고한 핵심(도메인 로직)을 중심에 두고 보호해야 한다.
  • 변하기 쉬운 외부 요청(통계 요구사항, 웹/DB 환경)은 핵심 로직에 영향을 주지 않고 플러그인처럼 탈부착 가능해야 한다.

이 논리를 만족하는 가장 적합한 구조가 바로 헥사고날 아키텍처였습니다.

일반적인 계층형(Layered) 아키텍처는 데이터베이스나 웹 프레임워크 의존성이 도메인 로직으로 침투하기 쉽습니다. 하지만 헥사고날 아키텍처는 도메인이 중심이 되고, 외부의 기술적 요소들이 어댑터를 통해 도메인에 의존하는 구조를 가집니다.

4. 구현: 도메인과 기술의 분리

그렇다면 실제 코드 레벨에서 이 아키텍처를 어떻게 구현했을까요? 가장 많은 고민이 있었던 ‘파싱(Parsing)’ 로직의 배치를 예로 들어보겠습니다.

아마 “파싱은 핵심 기능이니 도메인 영역에 있어야 하지 않나?”라고 생각할 수 있습니다. 하지만 저는 파싱을 ‘도메인’이 아닌 ‘기술’의 영역으로 정의하고 다음과 같이 분리했습니다.

(1) 엔티티 중심 설계 (Entity-Centric)

도메인 계층의 Metar 엔티티는 오직 비즈니스 데이터의 구조와 검증에만 집중합니다. 이 객체는 자신이 텍스트에서 파싱되었는지, DB에서 조회되었는지 전혀 모릅니다. 순수한 데이터 그 자체입니다.


// Domain Layer:
package com.skystat.api.weather.domain.entity.metar;

@Builder
public record Metar(
  String rawText,

  // required fields (ICAO Annex 3)
  String stationIcao,
  ReportType reportType,
  ObservationTime observationTime,
  Wind wind,
  Visibility visibility,
  // ... (생략)

) implements MetricAttribute, NonMetricAttribute {
}

(2) 포트를 통한 인터페이스화 (Interface via Ports)

애플리케이션 계층에는 MetarParsingPort를 두었습니다. 이는 “우리 시스템은 파싱 기능이 필요하다”라는 요구사항(What)만 정의할 뿐, 어떻게(How) 파싱하는지는 정의하지 않습니다.


// Application Layer (Port)
package com.skystat.api.weather.application.port.out.parsing;

public interface MetarParsingPort {
  Metar parse(String rawText, ObservationTime obsTime);
}

(3) 기술의 격리 (Technology Isolation)

가장 복잡하고 지저분한 정규표현식(Regex) 파싱 로직은 프레임워크 계층의 MetarParser에 격리했습니다.


// Framework Layer (Adapter)
package com.skystat.api.weather.framework.common.parser.metar;

public class MetarParser {

  private final CompositeRegexParser parser;

  public Metar parse(String rawText) {
    try {
      Map map = parser.parse(rawText);
      return Metar.builder()
              .rawText(rawText)
              .stationIcao(require(map, STATION_ICAO))
              // ... (복잡한 매핑 로직)
              .build();
    }
    // ... (예외 처리)
  }
}

이렇게 설계함으로써 얻는 이점은 명확합니다. 나중에 정규표현식 대신 AI 기반 파서를 도입하거나 파싱 라이브러리를 교체하더라도, 도메인(Metar)과 비즈니스 로직은 단 한 줄도 수정할 필요가 없습니다. 이것이 제가 헥사고날 아키텍처를 선택한 진짜 이유입니다.

5. 복잡함을 감수하고 얻는 가치

물론 1인 프로젝트에 헥사고날 아키텍처를 도입하는 것은 초기 구현의 복잡도를 높이는 일일 수도 있습니다. 단순히 CRUD만 있는 서비스라면 과한 선택일 겁니다.

하지만 SkyStat의 본질은 ‘복잡한 도메인 로직(METAR 파싱)’에 있습니다. 이 소중한 핵심 로직을 외부의 변화로부터 철저히 격리하고 보호하는 것. 그것이 비록 초기 비용이 들더라도, SkyStat을 오랫동안 안정적으로 서비스하기 위한 가장 확실한 투자라고 확신했습니다.

댓글 남기기