DSL Architecture

Kosmos Java HTML DSLHTML을 Java 코드로 안전하게 표현하기 위해 만들어진 서버사이드 DSL입니다. HTML 태그를 객체화하고, 빌더 체인으로 UI를 구성하며, 내부적으로는 StringBuilder 기반의 고성능 렌더링 엔진을 사용합니다.

핵심 철학: 객체는 최소화하고 문자열 처리는 최적화하되, 구조적 의미와 타입 안정성은 보존합니다.

주요 구조

HtmlComponent (인터페이스)
    └─ AbstractHtmlTag (공통)
         ├─ HtmlTag     (자식 존재)
         │    ├─ Div, Section, Table, Form, ...
         │    └─ addChild(), addChildren()
         └─ VoidTag     (자식 없음)
              ├─ Img, Input, Br, Hr, Meta, ...
              └─ addAttribute()

Kosmos DSL은 HTML 트리를 완전한 Java 객체로 구성하되, 렌더링 시에는 StringBuilder를 재사용하여 GC 부담을 최소화합니다.

HtmlComponent 인터페이스

public interface HtmlComponent {
    String render();
}

모든 DSL 요소는 HtmlComponent를 구현하며, render() 호출 시 HTML 문자열을 반환합니다. Kosmos의 모든 DSL 클래스는 이 규약을 따릅니다.

AbstractHtmlTag: 공통 기반

public abstract class AbstractHtmlTag implements HtmlComponent {

    protected final String tagName;
    protected final List<HtmlComponent> children = new ArrayList<>();
    protected final Map<String, String> attributes = new LinkedHashMap<>();
    protected final List<String> cssClasses = new ArrayList<>();

    public AbstractHtmlTag(String tagName) { this.tagName = tagName; }

    public AbstractHtmlTag addAttribute(String name, String value) {
        if (value != null) attributes.put(name, value);
        return this;
    }

    public AbstractHtmlTag addClass(String cls) {
        if (cls != null && !cls.isBlank()) cssClasses.add(cls);
        return this;
    }

    protected void renderAttributes(StringBuilder sb) {
        if (!cssClasses.isEmpty()) {
            sb.append(" class=\"").append(String.join(" ", cssClasses)).append("\"");
        }
        for (var e : attributes.entrySet()) {
            sb.append(" ").append(e.getKey()).append("=\"").append(e.getValue()).append("\"");
        }
    }
}

HtmlTag & VoidTag

public final class HtmlTag extends AbstractHtmlTag {
    public HtmlTag(String name) { super(name); }

    public HtmlTag addChild(HtmlComponent c) { children.add(c); return this; }
    public HtmlTag addChildren(HtmlComponent... comps) {
        children.addAll(Arrays.asList(comps)); return this;
    }

    @Override
    public String render() {
        StringBuilder sb = new StringBuilder();
        sb.append("<").append(tagName);
        renderAttributes(sb);
        sb.append(">");
        for (HtmlComponent c : children) sb.append(c.render());
        sb.append("</").append(tagName).append(">");
        return sb.toString();
    }
}

public final class VoidTag extends AbstractHtmlTag {
    public VoidTag(String name) { super(name); }
    @Override
    public String render() {
        StringBuilder sb = new StringBuilder();
        sb.append("<").append(tagName);
        renderAttributes(sb);
        sb.append(" />");
        return sb.toString();
    }
}

렌더링 프로세스

DSL 렌더링은 다음 단계를 거칩니다:

  1. 컴포넌트 생성 (ex. El.div())
  2. 속성/클래스/자식 추가
  3. render() 호출 → 내부적으로 StringBuilder에 append
  4. 최종 HTML 문자열 반환
// 예시
String html = El.div().css("card").children(
    El.h3().text("Hello, Kosmos!"),
    El.p().text("DSL로 HTML을 구성합니다.")
).render();

System.out.println(html);
// 출력
// <div class="card"><h3>Hello, Kosmos!</h3><p>DSL로 HTML을 구성합니다.</p></div>

성능 최적화 구조

최적화 기법 설명
StringBuilder 재사용 render() 내에서 StringBuilder를 반복 생성하지 않고 재사용
VoidTag 분리 자식 없는 태그를 별도로 처리해 닫힘 처리 오버헤드 제거
inlined rendering 메서드 체인을 인라인화하여 JIT 최적화 효율 상승
불필요한 escape 최소화 정적 문자열은 미리 인코딩 처리

확장 구조

Kosmos DSL은 Atomic Design 패턴을 내재화한 구조로, 하위 DSL은 상위 컴포넌트를 기반으로 조립됩니다.

  • Atom: El.div(), El.input(), El.span()
  • Molecule: CompInputText, CompAlert
  • Organism: FragEventDetailCard, FragEstimateStepCategory
  • Template: PageTemplate, DocsPageTemplate
  • Page: DocsMainPage, EstimationMainPage
// 예시: CompInputText (Molecule)
public class CompInputText implements HtmlComponent {
  private final String name, label, value, help, error;
  public CompInputText(String name, String label, String value, String help, String error) {
    this.name=name; this.label=label; this.value=value; this.help=help; this.error=error;
  }
  @Override
  public String render() {
    return El.div().css("mb-3").children(
      El.label().attr("for", name).css("form-label").text(label),
      El.input().attr("type","text").attr("id",name).attr("name",name).attr("value",value)
                .css("form-control" + (error != null ? " is-invalid" : "")),
      error != null ? El.div().css("invalid-feedback").text(error) : El.span(),
      help != null ? El.div().css("form-text").text(help) : El.span()
    ).render();
  }
}

DSL 확장 포인트

확장 포인트 설명 예시
El.* 팩토리 새 HTML 태그 정의 El.nav(), El.figure()
컴포넌트 복합 구조 (FormGroup 등) CompToggleGroup
템플릿 공통 레이아웃 확장 PageTemplate, DocsPageTemplate
Fragment 특정 UI 섹션 구성 FragEventDetailCard

렌더링 순서

Controller → Service → PageModel 생성
   → PageTemplate.buildSidebar() / buildContent()
     → Organism(Fragment) → Molecule(Component) → Atom(El.*)
        ↓
     render() 호출 → StringBuilder.append()
        ↓
     ResponseEntity<String> 로 HTML 반환

주의사항

HTML DSL의 남용: HTML 구조를 DSL로만 작성하면 초기 진입 장벽이 높을 수 있습니다.
렌더링 중 부작용 금지: render() 내부에서 DB 또는 I/O 호출을 절대 하지 마세요.
표준화된 DSL 명명 규칙: 내부 함수(addClass/addAttribute/addChild)는 외부 함수(css/attr/child)와 구분되어야 합니다.

성능 벤치마크

비교 항목 Kosmos DSL Thymeleaf JSP
렌더링 속도 (1000 컴포넌트) ~8ms ~40ms ~50ms
객체 생성 수 ~200 ~1100 ~950
GC 부하 낮음 보통 높음

다음으로