UI5

public vs private — UI5 모듈 API 설계 패턴 #shorts #SAP #UI5

▶ YouTube에서 보기

UI5 모듈 API 설계: public/private 분리 패턴

SAPUI5 / OpenUI5 애플리케이션이 커질수록 모듈 하나가 외부에 노출하는 함수의 범위를 어떻게 통제하느냐가 유지보수성을 좌우합니다. 자바스크립트에는 private 키워드가 별도로 존재하지 않기 때문에, UI5에서는 sap.ui.define 팩토리 함수의 클로저 스코프를 이용해 공개 API와 비공개 헬퍼를 분리합니다. 이 글에서는 그 설계 원칙과 단계별 코드 패턴을 다룹니다.

📖 이 글에서 다룰 것

UI5 모듈을 작성할 때 어떤 함수는 외부에 노출하고 어떤 함수는 내부에만 두어야 하는지, 그리고 그 경계를 자바스크립트 클로저로 어떻게 강제하는지를 정리합니다. sap.ui.define 반환 객체가 모듈의 public 인터페이스가 되는 이유, 언더스코어 prefix 컨벤션, 그리고 formatter 모듈을 예시로 한 실전 적용까지 살펴봅니다.

  • 모듈 반환 객체 = public API라는 원칙 이해
  • 클로저로 비공개 함수를 은닉하는 메커니즘 파악
  • 언더스코어(_) 컨벤션과 JSDoc @private 활용
  • formatter, util 모듈에 적용 가능한 분리 패턴 학습
  • 테스트와 리팩터링 관점에서의 트레이드오프 검토

📚 이 글을 보기 전에

UI5의 AMD 스타일 모듈 시스템(sap.ui.define, sap.ui.require)을 한 번이라도 작성해 본 경험이 권장됩니다. 자바스크립트 클로저, IIFE(즉시 실행 함수), strict mode에 대한 기초적인 이해가 있으면 흐름을 따라가기 수월합니다. 또한 컨트롤러나 formatter 파일을 분리해 본 경험이 있다면 도움이 됩니다.

🔧 환경 / 버전 / 준비물

이 글의 예제는 다음 환경에서 일반적으로 동일하게 동작합니다.

  • SAPUI5 1.96 이상 또는 OpenUI5 1.96 이상 (LTS 라인 기준)
  • 최신 권장 버전: SAPUI5 1.120.x 이상 (Evergreen / Long-term maintenance)
  • Node.js 18 LTS 이상 + UI5 Tooling(@ui5/cli) 3.x
  • 브라우저: Chrome / Edge 최신 (ES2017 이상 지원)
  • 에디터: VS Code + SAP Fiori tools 확장 (선택)

SAP BTP의 SAP Build Work Zone, Launchpad service, 혹은 ABAP 환경에 배포되는 UI5 앱 모두 동일한 모듈 패턴이 적용됩니다. 본 글의 코드는 별도 백엔드 의존성이 없는 순수 모듈이므로, ui5 serve만으로 즉시 테스트할 수 있습니다.

💡 핵심 개념

1. sap.ui.define 반환 객체는 곧 public 인터페이스

UI5의 모듈 로더는 AMD(Asynchronous Module Definition) 패턴을 기반으로 합니다. sap.ui.define([dependencies], function(...) { ... }) 형태에서 팩토리 함수가 return한 값이 곧 그 모듈의 "외부 얼굴"이 됩니다. 다른 모듈이 sap.ui.require(["my/module"], function(m) { ... }) 또는 의존성 배열로 가져올 때 받게 되는 객체가 바로 이 반환값입니다.

비유하자면 모듈은 카페이고, 반환 객체는 카운터에 진열된 메뉴판입니다. 주방(function 내부)에서 어떤 도구로 음료를 만드는지는 고객(다른 모듈)이 알 필요가 없습니다. 메뉴판에 적힌 것만 주문할 수 있습니다.

2. 클로저가 private을 만든다

팩토리 함수 내부에 선언된 변수와 함수는 그 함수가 종료된 뒤에도 반환된 객체의 메서드들이 참조하는 한 살아남습니다. 이게 자바스크립트 클로저입니다. 외부 코드는 팩토리 함수 스코프에 직접 접근할 수 없으므로, 반환 객체에 포함되지 않은 함수는 사실상 private입니다.

// 도식
sap.ui.define([], function() {
  // ┌─ 클로저 스코프 (외부 접근 불가) ─┐
  function _hidden() { ... }          // private
  var _secret = 42;                    // private
  // └────────────────────────────────┘
  return {
    publicApi: function() {            // public
      return _hidden();                // 내부에서는 사용 가능
    }
  };
});

3. 컨벤션: 언더스코어 prefix

UI5 표준 라이브러리 코드를 보면 비공개 함수에 _ 접두사를 붙이는 관행이 자주 보입니다(_handlePress, _getModel 등). 언어 차원의 강제는 아니지만, 코드 리뷰 시점과 IDE 자동완성에서 "이건 내부용"이라는 신호가 됩니다. JSDoc에 @private를 함께 명시하면 정적 분석 도구(ESLint, TypeScript 체커)에서 추가 검증을 받을 수 있습니다.

4. 분리의 이점

  • 안정성: 외부에서 내부 헬퍼를 호출하지 못하므로, 헬퍼의 시그니처를 자유롭게 바꿔도 호출처 영향이 없습니다.
  • 가독성: 모듈 사용자는 반환 객체만 보고 사용법을 파악할 수 있습니다.
  • 리팩터링 안전성: private 함수는 모듈 내부에서만 검색하면 되므로 영향 범위가 좁습니다.

💻 실전 코드 3단계

1단계: 반환 객체로 public 인터페이스 정의

가장 단순한 형태는 모든 메서드를 반환 객체에 그대로 노출하는 것입니다. 작은 formatter 모듈에서 흔히 보이는 패턴입니다.

sap.ui.define([], function() {
  "use strict";
  return {
    formatDate: function(sDate) {
      return new Date(sDate).toLocaleDateString("ko-KR");
    },
    formatAmount: function(nAmount) {
      return Math.round(nAmount * 100) / 100;
    }
  };
});

이 모듈은 외부에 formatDate, formatAmount 두 메서드를 제공합니다. 다른 컨트롤러에서는 다음과 같이 사용합니다.

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "my/app/model/formatter"
], function(Controller, formatter) {
  "use strict";
  return Controller.extend("my.app.controller.Main", {
    formatter: formatter,
    onInit: function() {
      var sFormatted = formatter.formatDate("2026-06-10");
      // "2026. 6. 10." 형식
    }
  });
});

이 단계에서는 모든 함수가 public이므로 별도의 은닉은 없습니다. 모듈이 단순할 때는 충분합니다.

2단계: 클로저 스코프로 private 함수 은닉 (에러/로깅 포함)

입력 검증, sanitize 같은 보조 로직이 늘어나면 외부에 노출하고 싶지 않은 함수가 생깁니다. 이때 반환 객체 바깥에 함수를 선언해 두면 자연스럽게 private이 됩니다.

sap.ui.define([
  "sap/base/Log"
], function(Log) {
  "use strict";

  var SOURCE = "my.app.model.inputProcessor";

  function _validate(sInput) {
    return !!sInput && sInput.trim() !== "";
  }

  function _sanitize(sInput) {
    return sInput.replace(/<[^>]*>/g, "");
  }

  return {
    process: function(sInput) {
      try {
        if (!_validate(sInput)) {
          Log.warning("빈 입력값", null, SOURCE);
          return null;
        }
        var sClean = _sanitize(sInput);
        Log.debug("sanitize 완료: " + sClean, null, SOURCE);
        return sClean;
      } catch (oErr) {
        Log.error("process 실패: " + oErr.message, null, SOURCE);
        return null;
      }
    }
  };
});

핵심은 _validate_sanitize가 반환 객체 어디에도 포함되지 않았다는 점입니다. 외부에서 inputProcessor._validate를 호출하려 해도 undefined가 반환됩니다. 동시에 process 내부에서는 클로저로 자유롭게 접근할 수 있습니다.

sap/base/Log 모듈을 활용해 각 단계별 로깅을 추가했습니다. 운영 환경에서는 Log.setLevel로 로그 레벨을 조정해 디버그 메시지를 제어할 수 있습니다. try/catch는 예기치 못한 입력(예: null이 아닌 객체)에 대해 모듈이 throw하지 않고 null을 반환하도록 안전장치 역할을 합니다.

3단계: 프로덕션 패턴 — public/private 완전 분리 formatter

실제 프로젝트에서 자주 쓰이는 한국어 날짜 formatter를 예로 들어, 출력 형식 구성을 private 헬퍼로 분리한 형태입니다. 테스트 가능성과 i18n 확장을 염두에 둔 구조입니다.

sap.ui.define([
  "sap/base/Log",
  "sap/ui/core/format/DateFormat"
], function(Log, DateFormat) {
  "use strict";

  var SOURCE = "my.app.model.formatter";

  /**
   * 두 자리 zero-padding
   * @private
   */
  function _pad(n) {
    return String(n).padStart(2, "0");
  }

  /**
   * Date 객체를 "YYYY년 MM월 DD일" 문자열로 변환
   * @private
   */
  function _toKoreanDate(d) {
    return d.getFullYear() + "년 " +
           _pad(d.getMonth() + 1) + "월 " +
           _pad(d.getDate()) + "일";
  }

  /**
   * Date 유효성 검사
   * @private
   */
  function _isValidDate(d) {
    return d instanceof Date && !isNaN(d.getTime());
  }

  return {
    /**
     * ISO 문자열을 한국어 날짜로 변환
     * @public
     * @param {string} sISO - ISO 8601 형식 문자열
     * @returns {string} 변환된 문자열 또는 빈 문자열
     */
    formatDate: function(sISO) {
      if (!sISO) { return ""; }
      var oDate = new Date(sISO);
      if (!_isValidDate(oDate)) {
        Log.warning("유효하지 않은 날짜: " + sISO, null, SOURCE);
        return "";
      }
      return _toKoreanDate(oDate);
    }
  };
});

이 모듈은 외부에 단 하나의 메서드 formatDate만 노출합니다. _pad, _toKoreanDate, _isValidDate는 클로저 안에 머무릅니다. 만약 나중에 출력 형식을 2026.06.10으로 바꾸고 싶다면 _toKoreanDate만 수정하면 됩니다. 외부 API 시그니처는 변하지 않으므로 호출하는 컨트롤러나 XML 뷰의 바인딩 표현식은 그대로 둘 수 있습니다.

XML 뷰에서는 다음과 같이 사용합니다.

<mvc:View
  controllerName="my.app.controller.Main"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc">
  <Text text="{
    path: 'orderDate',
    formatter: '.formatter.formatDate'
  }" />
</mvc:View>

테스트 코드는 public API에 집중합니다. QUnit으로 작성하면 다음과 같습니다.

sap.ui.define([
  "my/app/model/formatter"
], function(formatter) {
  "use strict";
  QUnit.module("formatter");

  QUnit.test("정상 ISO 문자열 처리", function(assert) {
    assert.strictEqual(
      formatter.formatDate("2026-06-10"),
      "2026년 06월 10일"
    );
  });

  QUnit.test("빈 입력은 빈 문자열 반환", function(assert) {
    assert.strictEqual(formatter.formatDate(""), "");
    assert.strictEqual(formatter.formatDate(null), "");
  });

  QUnit.test("잘못된 형식은 빈 문자열 반환", function(assert) {
    assert.strictEqual(formatter.formatDate("not-a-date"), "");
  });
});

private 함수를 직접 테스트하지 않는 이유는, 그것이 구현 세부사항이기 때문입니다. public API의 결과가 명세를 만족하면, 내부 구현은 자유롭게 리팩터링할 수 있는 여지를 남겨두는 것이 권장됩니다.

⚠️ 흔한 실수 / 트러블슈팅

실수 1: 반환 객체에 private 함수를 실수로 노출

아래처럼 객체 리터럴 안에 모든 함수를 정의하면 _helper도 외부에서 호출 가능합니다.

return {
  _helper: function() { ... },   // 사실상 public!
  publicMethod: function() { ... }
};

해결책은 _helper를 반환 객체 바깥의 함수 선언으로 옮기는 것입니다. 언더스코어 prefix는 "private이라는 신호"일 뿐, 객체 키에 포함된 이상 외부 호출을 막지는 못합니다.

실수 2: this 컨텍스트 혼동

private 함수에서 this.someProp를 참조하려다 undefined를 만나는 경우가 흔합니다. private 함수는 일반 함수로 호출되므로 this가 모듈 객체를 가리키지 않습니다. 필요한 값은 인자로 명시적으로 전달하는 편이 권장됩니다.

FAQ 1: ES6 class 문법으로 #private 필드를 쓰면 안 되나요?

UI5 1.96 이후 ES2022 클래스 private 필드(#name) 문법을 지원하는 브라우저에서는 동작합니다. 다만 UI5의 컨트롤은 Control.extend를 사용하는 경우가 많고, 빌드 타깃 브라우저가 구버전을 포함한다면 트랜스파일이 필요합니다. 라이브러리 호환성을 고려해 클로저 패턴을 기본으로 두고, 컨트롤러/컨트롤 정의에서는 UI5의 extend 메서드를 사용하는 편이 일반적입니다.

FAQ 2: private 함수도 테스트해야 하나요?

구현 세부사항이므로 직접 테스트하지 않는 것이 일반적인 권장 사항입니다. public API를 통해 기대 동작이 모두 검증된다면 private은 자유롭게 바뀔 수 있어야 합니다. 만약 private 함수가 너무 복잡해 직접 테스트하고 싶어진다면, 그 함수를 별도 모듈로 분리해 자체적인 public API를 갖게 만드는 신호일 수 있습니다.

FAQ 3: 모듈 간 비공개 함수 공유는 어떻게 하나요?

두 모듈이 같은 헬퍼를 써야 한다면, 그 헬퍼는 더 이상 한 모듈의 private이 아닙니다. 별도의 internal util 모듈(예: my/app/model/_internal/dateHelper)을 만들어 두 모듈이 함께 import하는 구조가 권장됩니다. 폴더명 또는 파일명에 _internal을 두어 "외부 컨슈머가 사용하지 말 것"이라는 신호를 줍니다.

🚀 다음 단계 / 관련 주제

모듈 API 분리를 익혔다면 다음 주제로 확장해볼 수 있습니다.

  • Component 설계: Component.js에서 manifest를 통해 노출되는 라우팅/모델 구성과 컨트롤러 간 인터페이스 설계
  • 커스텀 컨트롤: Control.extendmetadata.properties / events / aggregations이 public API 역할을 하는 메커니즘
  • UI5 TypeScript: @private JSDoc 대신 private 접근 제어자를 사용해 컴파일 타임에 강제하는 방식
  • Library 작성: .library 파일과 library.js를 통한 namespace 단위 public API 선언
  • ESLint 규칙: no-underscore-dangle과 커스텀 규칙으로 private 호출을 정적으로 차단

📚 참고 자료

댓글 0

아직 댓글이 없습니다.