홈으로 돌아가기

Java/Vue 기반 YAML 서비스: 빠른 데이터 가져오기

복잡한 API와 SQL을 우회하여 엔터프라이즈 시스템에 과거 데이터를 효율적이고 빠르게 로드하기 위한 간단한 Java 및 Vue.js YAML 서비스를 만드는 방법을 배워보세요.

엔터프라이즈 시스템의 빠른 데이터 가져오기: Java와 Vue.js를 사용한 YAML 접근법
Advertisement 728x90

엔터프라이즈 데이터 가져오기 가속화: Java와 Vue를 활용한 YAML 기반 서비스

현대 엔터프라이즈 시스템에서 대량의 이력 데이터나 초기 데이터를 빠르고 효율적으로 가져오는 작업은 종종 상당한 난관에 부딪힙니다. 번거로운 코드나 복잡한 설정을 요구하는 기존 방식은 이 과정을 며칠로 늘릴 수 있습니다. 이 글에서는 Java 및 Vue.js 스택을 사용하여 가볍고 YAML 지향적인 서비스를 구축하는 혁신적인 방법을 탐구합니다. 이 서비스는 데이터 로딩 시간을 단 몇 분으로 단축하여 기술 전문가가 플랫폼 및 콘텐츠와 상호 작용하는 방식을 크게 간소화하도록 설계되었습니다.

"모든 것을 코드로"라는 원칙에 따라 구축된 새로운 JMatrixPlatform의 개발은 중요한 문제점을 부각시켰습니다. 바로 일회성 데이터 업로드를 위한 편리한 도구의 부재였습니다. 데이터 가져오기가 사소한 작업이었던 구형 시스템과 달리, 새로운 아키텍처는 모든 작업에 대해 Java 코드를 작성해야 했고, 이는 과정을 힘들고 유연하지 못하게 만들었습니다. 분석가들은 데이터 독립적으로 관리할 능력을 잃고 전적으로 개발자에게 의존하게 되었습니다.

몇 가지 표준적인 접근 방식이 고려되었지만, 각각 상당한 단점을 가지고 있었습니다.

Google AdInline article slot
  • 배치 서비스로 REST API 확장: 이는 Excel 데이터를 JSON 배열로 복잡하게 변환해야 하며, Excel 수식 내의 따옴표를 이스케이프하는 작업 자체가 결코 쉽지 않습니다. Postman과 같은 외부 도구를 사용하는 것은 과정을 더욱 복잡하게 만들 것입니다.
  • 각 객체에 curl + json 사용: 이는 토큰 검색을 위한 추가 스크립팅이 필요하며, 오류 복원력을 제공하지 않아 첫 번째 잘못된 레코드에서 가져오기가 중단될 수 있습니다.
  • SQL을 통한 직접 가져오기: 모든 데이터 상호 작용 로직이 애플리케이션 수준에서 구현되므로 이 방식은 배제되었습니다. 플랫폼의 비즈니스 로직을 우회하는 직접적인 데이터베이스 개입은 데이터 불일치를 초래하고 시스템 무결성을 손상시킬 수 있습니다.
  • 각 엔티티에 대한 네이티브 Excel 가져오기 개발: 이는 일회성 가져오기에는 경제적으로 비실용적이고 시간이 많이 소요되는 것으로 판단되었습니다.

기술 전문가가 독립적으로 데이터 업로드를 관리할 수 있도록 하는 빠르고 유연한 도구의 필요성이 분명해졌습니다.

해결책: 배치 업로드를 위한 YAML 지향 서비스

기존 접근 방식의 한계를 인식한 저자는 다양한 작업(생성, 수정, 삭제)을 수행하기 위해 구조화된 데이터를 받아들일 수 있는 단일 진입점(엔드포인트)을 특징으로 하는 최소한의 서비스가 필요하다고 결론 내렸습니다. JSON 대신 YAML이 간결하고 수동으로 생성하거나 간단한 Excel 수식 또는 LLM을 사용하여 자동으로 생성하기 쉽다는 장점 때문에 선택되었습니다.

YAML 요청 예시는 그 간결성과 가독성을 보여줍니다.

Google AdInline article slot
#request
- createObject:
    type: ru.commons.matrix.schema.type.ATPPerson
    policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
    type: ru.commons.matrix.schema.type.ATPPerson1
    policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
    type: ru.commons.matrix.schema.type.ATPPerson
    policy: ru.commons.matrix.schema.policy.ALCPerson

그리고 실행 상태와 객체 ID 또는 오류 메시지를 포함하는 해당 응답은 다음과 같습니다.

#response
---
- createObject:
    status: 200
    message: null
    oid: "f4ba679e-9253-4a83-a390-44daf7ac7756"
- createObject:
    status: 500
    message: "Admin type ru.commons.matrix.schema.type.ATPPerson1 not found. Enter a correct name or contact the administrator."
- createObject:
    status: 200
    message: null
    oid: "d9c98e74-bd17-4b3c-ae07-d9c307151c74"

이 형식은 예를 들어 Excel에서 간단한 수식을 사용하여 명령을 쉽게 생성할 수 있게 합니다.

="- createObject:
    id: "&K2&"
    type: "&I2&"
    policy: "&J2&"
    code: "&B2&"
    title: '"&C2&"'"

이 접근 방식은 완전한 DSL(Domain Specific Language)은 아니지만, 기존 REST API DTO(Data Transfer Objects)를 액션 명령으로 감싸 유연성과 재사용성을 보장합니다.

Google AdInline article slot

Java 및 Spring Framework를 사용한 기술 구현

서비스의 핵심은 들어오는 YAML 요청을 처리하는 JQLController입니다.

@RestController
@RequestMapping("jql")
@RequiredArgsConstructor
public class JQLController {

  /**
   * Executes a batch of commands.
   *
   * LinkedHashMap is used to preserve the order of input and output commands
   * - input commands are processed in the order they appear in YAML
   * - responses are returned in the same order as requests
   * - this is crucial for scenarios where execution order matters (create → connect)
   * Jackson uses LinkedHashMap by default, but explicit specification
   * protects against accidental implementation changes in the future.
   */
  @PostMapping(consumes = "application/yaml", produces = "application/yaml")
  public ResponseEntity<List<LinkedHashMap<String, JQLResponseData>>> promote(@JPathContextVariable JContext ctx,
      @RequestBody List<LinkedHashMap<String, Object>> commands) {

    List<LinkedHashMap<String, JQLResponseData>> results = new ArrayList<>(commands.size());
    for (Map<String, Object> command : commands) {
      Map<JQLEnum, IJQLDTO> parsed = fromYaml(command);
      //an error in a command does not cause the entire batch to fail
      //run accounts for this, so a try-catch here is not needed
      results.add(run(ctx, parsed));

    }

    return ResponseEntity.ok(results);
  }

  private static Map<JQLEnum, IJQLDTO> fromYaml(Map<String, Object> commands) {
    Map<JQLEnum, IJQLDTO> command = new LinkedHashMap<>();

    for (Map.Entry<String, Object> entry : commands.entrySet()) {
      String key = entry.getKey();
      Object value = entry.getValue();

      JQLEnum jqlEnum = JQLEnum.valueOf(key);
      Class<? extends IJQLDTO> dtoClass = jqlEnum.getDTOClass();

      IJQLDTO dto = JObjectJSON.MAPPER.convertValue(value, dtoClass);
      command.put(jqlEnum, dto);
    }

    return command;
  }

  /**
   * Executes commands and returns the result in the same format.
   *
   * Input: { "createObject": { "type": "...", "policy": "..." } }
   * Output: { "createObject": { "status": 200, "id": "..." } }
   *
   * or
   *
   * Output: { "createObject": { "status": 500, "message": "..." } }
   */
  private static LinkedHashMap<String, JQLResponseData> run(JContext ctx, Map<JQLEnum, IJQLDTO> commands) {
    LinkedHashMap<String, JQLResponseData> response = new LinkedHashMap<>();

    if (commands.isEmpty()) {
      return response;
    }

    try {
      ctx.getTxUpdate().executeWithoutResult(tx -> {
        for (Map.Entry<JQLEnum, IJQLDTO> entry : commands.entrySet()) {
          response.put(entry.getKey().name(), entry.getKey().execute(ctx, entry.getValue()));
        }
      });

    } catch (JMatrixLocalizedError ex) {
      commands.keySet().forEach(el -> {
        response.put(el.name(), new JQLResponseData(500, ex.getLocalizedMessage(ctx.getLocale())));
      });
    } catch (Exception ex) {
      commands.keySet().forEach(el -> {
        response.put(el.name(), new JQLResponseData(500, ex.getMessage()));
      });
    }

    return response;
  }
}

JQLController는 Spring 어노테이션을 사용하여 application/yaml 콘텐츠 타입의 POST 요청을 처리합니다. 핵심 기능은 명령 순서 유지를 위해 LinkedHashMap을 사용하는 것인데, 이는 작업 순서가 중요한 시나리오(예: 객체를 연결하기 전에 생성)에서 매우 중요합니다. fromYaml 메서드는 JQLEnum을 사용하여 명령 유형과 DTO 클래스를 결정하고 YAML 객체를 해당 DTO로 역직렬화하는 역할을 합니다. run 메서드는 트랜잭션 내에서 명령을 실행하고 오류를 처리하며 구조화된 YAML 응답을 생성합니다.

YAML 지원을 위한 Spring 설정

Spring Framework는 application/yaml을 콘텐츠 타입으로 기본 지원하지 않으므로, 추가적인 HttpMessageConverter 설정이 필요합니다.

@Configuration
public class YamlConfig {

  @Bean
  public YamlHttpMessageConverter yamlHttpMessageConverter() {
    YAMLFactory factory = new YAMLFactory();
        //.disable(YAMLGenerator.Feature.SPLIT_LINES)
        //.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);

    YAMLMapper mapper = new YAMLMapper(factory);

    return new YamlHttpMessageConverter(mapper);
  }

  public static class YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    public YamlHttpMessageConverter(ObjectMapper objectMapper) {
      super(objectMapper,
          MediaType.parseMediaType("application/yaml"),
          MediaType.parseMediaType("application/x-yaml"),
          MediaType.parseMediaType("text/yaml"));
    }
  }
}

YamlConfigYamlHttpMessageConverter 빈을 정의하며, 이는 AbstractJackson2HttpMessageConverter를 확장하고 YAMLMapper를 등록하여 YAML 형식(application/yaml, application/x-yaml, text/yaml)을 처리합니다. 이를 통해 Spring은 컨트롤러 객체 내에서 YAML 데이터를 자동으로 마샬링하고 언마샬링할 수 있습니다.

DTO 구조 및 명령 Enum

응답을 표준화하기 위해 기본 DTO인 JQLResponseData가 개발되었습니다.

@Getter
public class JQLResponseData {
  private int status = 200;
  private String message = null;

  public JQLResponseData() {

  }

  public JQLResponseData(int status, String message) {
    this.status = status;
    this.message = message;
  }

}

이 클래스는 실행 상태 및 오류 메시지에 대한 일관된 형식을 제공합니다.

초기 MVP(Minimum Viable Product)에서는 enum JQLEnum을 통해 명령 세트가 구현되어 개발을 간소화했습니다. 그러나 더 성숙한 시스템에서는 이를 명령 클래스를 등록하기 위한 더 유연한 메커니즘으로 대체할 수 있습니다.

public enum JQLEnum {
  createObject {
    @Override
    JQLResponseData execute(JContext ctx, IJQLDTO value) {
      JDTODomainObject dto = (JDTODomainObject) value;

      JDomainObject object;
      if (dto.getId() == null) {
        object = new JDomainObject();
      } else {
        object = new JDomainObject(dto.getId());
      }
      dto.unmap(object);
      object.create(ctx, JModel.getRequiredAdminByName(dto.getType()), JModel.getRequiredAdminByName(dto.getPolicy()));

      return new CreateDomainRS(object.getId());
    }

    @Override
    public Class<? extends IJQLDTO> getDTOClass() {
      return JDTODomainObject.class;
    }

  },
  deleteObject {
    @Override
    JQLResponseData execute(JContext ctx, IJQLDTO value) {
      DeleteDomainRQ dto = (DeleteDomainRQ) value;
      new JDomainObject(dto.getId()).delete(ctx);

      return new JQLResponseData();
    }

    @Override
    public Class<? extends IJQLDTO> getDTOClass() {
      return DeleteDomainRQ.class;
    }

  },
  //etc.

  abstract JQLResponseData execute(JContext ctx, IJQLDTO value);

  public abstract Class<? extends IJQLDTO> getDTOClass();
}

JQLEnum 요소는 특정 명령을 실행하기 위한 로직(execute)을 캡슐화하고 해당 DTO 클래스를 검색하는 메서드(getDTOClass)를 제공합니다. 이를 통해 사용 가능한 작업 및 해당 처리를 중앙에서 관리할 수 있습니다.

Vue.js를 사용한 사용자 인터페이스

YAML 서비스와 상호 작용하기 위해 Vue.js를 사용하여 간단한 웹 인터페이스가 개발되었습니다. 이 인터페이스는 YAML 요청을 입력하는 영역과 결과를 표시하는 영역 두 가지로 구성됩니다. ace-builds 라이브러리는 구문 강조 기능이 있는 편리한 YAML 코드 편집 기능을 제공합니다.

<template>
  <div class="jql-base-div">
    <div ref="refRequests" class="jql-requests-div" @keyup.alt.enter="handleAltEnter"></div>
    <div ref="refResults" class="jql-response-div"></div>
  </div>
</template>

<script setup>
import { useJServices } from '@/composables/useJServices';

const { serviceFetch } = useJServices()

import ace from 'ace-builds';

import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/theme-chrome';

import { onMounted, ref } from 'vue';

ace.config.set('basePath', '/ace')
ace.config.set('workerPath', '/ace')
ace.config.set('themePath', '/ace')

const props = defineProps({
  routeParams: Object,
  routeQuery: Object,
  requestBody: Object,
  metaComponent: Object
})

const refRequests = ref(null)
const refResults = ref(null)

let aceEditorRequests = null
let aceEditorResponse = null

onMounted(() => {
  document.title = 'JMatrix: JQL'

  aceEditorRequests = ace.edit(refRequests.value)
  aceEditorRequests.setTheme("ace/theme/chrome")
  aceEditorRequests.session.setMode("ace/mode/yaml")
  aceEditorRequests.setOptions({
    fontSize: "13px",
    showPrintMargin: false,
    r

이 인터페이스는 YAML 명령을 테스트하고 실행하기 위한 대화형 환경을 제공하여 개발자와 분석가가 작업 결과를 신속하게 확인하고 요청을 조정할 수 있도록 합니다. ace-builds의 사용은 YAML 구조 작업의 편의성을 크게 향상시킵니다.

장점 및 향후 개발

YAML 서비스의 구현은 이력 데이터 로딩에 필요한 시간을 수일에서 수분으로 획기적으로 단축했습니다. 이 솔루션은 사전 판매 맞춤화의 효율성을 향상시켰을 뿐만 아니라, 분석가들에게 개발자의 직접적인 개입 없이 독립적으로 데이터를 작업할 수 있는 도구를 제공함으로써 그들의 역량을 크게 확장시켰습니다.

이 접근 방식의 주요 장점:

  • 속도 및 효율성: 간소화된 형식과 배치 처리 덕분에 빠른 데이터 로딩.
  • 유연성: 복잡한 코딩 없이 다양한 소스(Excel, LLM)에서 요청을 쉽게 생성.
  • 분석가 자율성: 일상적인 데이터 가져오기 작업에 대한 개발자 의존도 감소.
  • 아키텍처의 깔끔함: 기존 DTO 활용 및 애플리케이션 수준에서 비즈니스 로직 유지.

향후 개발 고려 사항:

  • 새로운 작업을 추가하는 것을 간소화하기 위해 리플렉션 또는 구성을 사용하여 JQLEnum을 보다 동적인 명령 등록 메커니즘으로 리팩토링.
  • YAML 스키마 유효성 검사 기능 및 더욱 정교한 응답 처리 기능을 갖춘 UI 확장.
  • 가져온 데이터의 변경 사항을 추적하기 위한 버전 제어 시스템과의 통합.

이 접근 방식은 사려 깊은 아키텍처와 적절한 기술 선택이 복잡한 데이터 가져오기 문제를 어떻게 해결하고 팀 생산성 및 시스템 유연성을 크게 향상시킬 수 있는지를 보여줍니다.

핵심 요약

  • 문제: "코드 우선" 엔터프라이즈 시스템에서 일회성 이력 데이터 로딩을 위한 기존 데이터 가져오기 방법(REST API, SQL, 네이티브 유틸리티)은 비효율적이며 종종 며칠이 걸립니다.
  • 해결책: 배치 명령 처리를 위해 Java(Spring Framework) 및 Vue.js를 사용하는 가볍고 YAML 지향적인 서비스 생성.
  • YAML 선택 이유: YAML은 JSON보다 간결하고 생성하기 쉬우며(Excel, LLM에서), 기술 전문가에게 가독성이 뛰어나 선호되었습니다.
  • 아키텍처: 서비스는 JQLController를 사용하여 YAML 요청을 처리하고, Spring 통합을 위해 YamlHttpMessageConverter를 사용하며, 표준화된 DTO(JQLResponseData)와 명령 실행을 위한 enum을 사용합니다.
  • 결과: 데이터 가져오기 시간이 수일에서 수분으로 단축되었고, 분석가 자율성이 증가했으며, 비즈니스 로직 무결성을 유지하면서 시스템 유연성이 향상되었습니다.

— Editorial Team

Advertisement 728x90

다음 읽기