Appearance
TopologyChart
MSA (Microservices Architecture) 분석을 위한 Force 기반 노드-링크 다이어그램 컴포넌트입니다.
개요
TopologyChart는 마이크로서비스 간의 관계와 호출 패턴을 자유로운 형태의 네트워크 그래프로 시각화합니다. Force 시뮬레이션을 통해 자연스러운 노드 배치가 가능하며, 사용자가 직접 노드를 드래그하여 레이아웃을 조정할 수 있습니다.
주요 특징
- 인터랙티브: 노드 드래그, 확대/축소, 팬 기능 지원
- Force 시뮬레이션: 물리 기반 시뮬레이션으로 자연스러운 노드 배치
- 자동 스케일: 데이터에 맞는 자동 스케일 조정
- 노드 필터링: 고립된 노드 숨기기 기능
- 시각적 피드백: 호버/클릭 시 연결된 노드/엣지 하이라이팅
- 실시간 편집: 드래그로 노드 위치 조정 가능
Props Interface
typescript
interface TopologyChartProps {
/** 토폴로지 데이터 */
data: TopologyData;
/** 차트 너비 (기본값: 800) */
width?: number;
/** 차트 높이 (기본값: 600) */
height?: number;
/** 연결점이 없는 고립된 노드 숨김 여부 (기본값: false) */
hideIsolatedNodes?: boolean;
/** Force 시뮬레이션 사용 여부 (기본값: false) */
enableForceSimulation?: boolean;
/** 엣지 애니메이션 활성화 여부 (기본값: true) */
enableEdgeAnimation?: boolean;
/** 노드 스케일 컨트롤 표시 여부 (기본값: true) */
showScaleControls?: boolean;
/** 자동 스케일 조정 활성화 여부 (기본값: true) */
enableAutoScale?: boolean;
/** 노드 라벨 숨김 여부 (기본값: false) */
hideNodeLabels?: boolean;
}
Events
typescript
interface TopologyChartEmits {
/** 노드 호버 시 발생 */
'node-hover': [node: TopologyNode];
/** 노드 클릭 시 발생 */
'node-click': [node: TopologyNode];
/** 엣지 호버 시 발생 */
'edge-hover': [edge: TopologyEdge];
/** 엣지 클릭 시 발생 */
'edge-click': [edge: TopologyEdge];
/** 호버 해제 시 발생 */
'hover-out': [];
/** 차트 렌더링 완료 시 발생 */
'chart-ready': [];
}
기본 사용법
vue
<template>
<TopologyChart
:data="topologyData"
:width="800"
:height="600"
:hide-isolated-nodes="false"
:enable-force-simulation="false"
:enable-auto-scale="true"
@node-click="handleNodeClick"
@edge-click="handleEdgeClick"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { TopologyChart } from '@jennifersoft/apm-components';
import type {
TopologyData,
TopologyNode,
TopologyEdge,
} from '@jennifersoft/apm-apis';
const topologyData = ref<TopologyData>(sampleTopologyData);
const handleNodeClick = (node: TopologyNode) => {
console.log('Node clicked:', node);
};
const handleEdgeClick = (edge: TopologyEdge) => {
console.log('Edge clicked:', edge);
};
</script>
인터랙티브 데모
토폴로지 통계
총 노드:8개
연결된 노드:6개
고립된 노드:2개
총 엣지:8개
정상 엣지:5개
실패 엣지:3개
TopologyChart 컴포넌트 로딩 중...
💡 사용법: 노드 드래그/클릭, 마우스 휠 확대/축소, 배경 드래그 이동, Force 시뮬레이션을 통한 물리 기반 자동 배치 등의 기능을 사용할 수 있습니다.
동적 크기 조절
vue
<template>
<div class="chart-container">
<!-- 크기 선택 컨트롤 -->
<div class="size-controls">
<label>차트 크기:</label>
<select v-model="selectedSize" @change="updateChartSize">
<option value="small">Small (600×400)</option>
<option value="medium">Medium (800×600)</option>
<option value="large">Large (1200×800)</option>
<option value="custom">Custom</option>
</select>
<div v-if="selectedSize === 'custom'" class="custom-size">
<input type="number" v-model="customWidth" placeholder="너비" />
<span>×</span>
<input
type="number"
v-model="customHeight"
placeholder="높이"
/>
</div>
</div>
<TopologyChart
:data="topologyData"
:width="chartWidth"
:height="chartHeight"
v-bind="chartOptions"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
const selectedSize = ref('medium');
const customWidth = ref(800);
const customHeight = ref(600);
const chartWidth = computed(() => {
switch (selectedSize.value) {
case 'small':
return 600;
case 'large':
return 1200;
case 'custom':
return customWidth.value;
default:
return 800; // medium
}
});
const chartHeight = computed(() => {
switch (selectedSize.value) {
case 'small':
return 400;
case 'large':
return 800;
case 'custom':
return customHeight.value;
default:
return 600; // medium
}
});
</script>
이벤트 로깅 시스템
사용자 인터랙션을 추적하고 디버깅에 활용할 수 있는 이벤트 로깅 시스템:
vue
<template>
<div class="topology-with-logs">
<TopologyChart
:data="topologyData"
@node-click="logNodeClick"
@edge-click="logEdgeClick"
@node-hover="logNodeHover"
@edge-hover="logEdgeHover"
@chart-ready="logChartReady"
/>
<!-- 이벤트 로그 패널 -->
<div v-if="events.length > 0" class="event-logs">
<h4>이벤트 로그</h4>
<div class="event-list">
<div
v-for="event in recentEvents"
:key="event.id"
class="event-item"
>
<span class="event-time">{{ event.time }}</span>
<span class="event-type" :class="event.type">{{
event.type
}}</span>
<span class="event-detail">{{ event.detail }}</span>
</div>
</div>
<button @click="clearEvents" class="clear-button">
로그 지우기
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface EventLog {
id: number;
time: string;
type: string;
detail: string;
}
const events = ref<EventLog[]>([]);
let eventIdCounter = 0;
const recentEvents = computed(() => events.value.slice(-10).reverse());
const addEvent = (type: string, detail: string) => {
events.value.push({
id: ++eventIdCounter,
time: new Date().toLocaleTimeString(),
type,
detail,
});
};
const logNodeClick = (node: TopologyNode) => {
addEvent('node-click', `${getNodeDisplayName(node)} 클릭`);
};
const logEdgeClick = (edge: TopologyEdge) => {
const source = getNodeDisplayName(edge.relation.source);
const target = getNodeDisplayName(edge.relation.target);
addEvent('edge-click', `${source} → ${target} 엣지 클릭`);
};
const logNodeHover = (node: TopologyNode) => {
addEvent('node-hover', `${getNodeDisplayName(node)} 호버`);
};
const logEdgeHover = (edge: TopologyEdge) => {
const source = getNodeDisplayName(edge.relation.source);
const target = getNodeDisplayName(edge.relation.target);
addEvent('edge-hover', `${source} → ${target} 엣지 호버`);
};
const logChartReady = () => {
addEvent('chart-ready', '차트 렌더링 완료');
};
const clearEvents = () => {
events.value = [];
};
const getNodeDisplayName = (node: TopologyNode): string => {
return node.props.shortName || node.props.longName || 'Unknown';
};
</script>
통계 정보 표시
토폴로지 데이터의 통계 정보를 실시간으로 계산하고 표시:
vue
<template>
<div class="topology-with-stats">
<TopologyChart :data="topologyData" />
<!-- 통계 정보 패널 -->
<div class="stats-panel">
<h4>토폴로지 통계</h4>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">총 노드 수:</span>
<span class="stat-value">{{ nodeStats.total }}개</span>
</div>
<div class="stat-item">
<span class="stat-label">연결된 노드:</span>
<span class="stat-value">{{ nodeStats.connected }}개</span>
</div>
<div class="stat-item">
<span class="stat-label">고립된 노드:</span>
<span class="stat-value">{{ nodeStats.isolated }}개</span>
</div>
<div class="stat-item">
<span class="stat-label">총 엣지 수:</span>
<span class="stat-value">{{ edgeStats.total }}개</span>
</div>
<div class="stat-item">
<span class="stat-label">정상 엣지:</span>
<span class="stat-value">{{ edgeStats.normal }}개</span>
</div>
<div class="stat-item">
<span class="stat-label">실패 엣지:</span>
<span class="stat-value error"
>{{ edgeStats.failed }}개</span
>
</div>
<div class="stat-item">
<span class="stat-label">총 실패 건수:</span>
<span class="stat-value error"
>{{ edgeStats.totalFailures }}건</span
>
</div>
<div class="stat-item">
<span class="stat-label">최고 실패 엣지:</span>
<span class="stat-value">{{
edgeStats.topFailureEdge
}}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const nodeStats = computed(() => {
const total = topologyData.value.nodes.length;
const connectedNodeIds = new Set<string>();
topologyData.value.edges.forEach((edge) => {
connectedNodeIds.add(getNodeId(edge.relation.source));
connectedNodeIds.add(getNodeId(edge.relation.target));
});
const connected = connectedNodeIds.size;
const isolated = total - connected;
return { total, connected, isolated };
});
const edgeStats = computed(() => {
const total = topologyData.value.edges.length;
const failed = topologyData.value.edges.filter(
(edge) => (edge.statistic.failureCount || 0) > 0
).length;
const normal = total - failed;
const totalFailures = topologyData.value.edges.reduce(
(sum, edge) => sum + (edge.statistic.failureCount || 0),
0
);
// 최고 실패 엣지 찾기
const topFailureEdge = topologyData.value.edges
.filter((edge) => (edge.statistic.failureCount || 0) > 0)
.sort(
(a, b) =>
(b.statistic.failureCount || 0) -
(a.statistic.failureCount || 0)
)[0];
const topFailureEdgeName = topFailureEdge
? `${getNodeDisplayName(
topFailureEdge.relation.source
)} → ${getNodeDisplayName(topFailureEdge.relation.target)} (${
topFailureEdge.statistic.failureCount
}건)`
: '없음';
return {
total,
normal,
failed,
totalFailures,
topFailureEdge: topFailureEdgeName,
};
});
</script>
<style scoped>
.stats-panel {
margin-top: 20px;
padding: 16px;
border: 1px solid #e1e5e9;
border-radius: 8px;
background: #fafbfc;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-top: 12px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: white;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
.stat-label {
font-size: 14px;
color: #6b7280;
}
.stat-value {
font-weight: 600;
color: #1f2937;
}
.stat-value.error {
color: #dc2626;
}
</style>
## 데이터 구조 ### TopologyData ```typescript interface TopologyData { nodes:
TopologyNode[]; edges: TopologyEdge[]; }
TopologyNode
노드는 세 가지 타입으로 구분됩니다:
typescript
// 인스턴스 노드
interface TopologyInstanceNode {
type: 'INSTANCE';
props: {
domainId: number;
instId: number;
oid: number;
shortName: string;
longName: string;
};
}
// 도메인 노드
interface TopologyDomainNode {
type: 'DOMAIN';
props: {
domainId: number;
shortName: string;
longName: string;
};
}
// 원격 호출 노드
interface TopologyRemoteCallNode {
type: 'REMOTE_CALL';
props: {
remoteCallType: RemoteCallTypeDef;
languageTypeOrZero: number;
customMethodDescHashOrZero: number;
customMethodDescOrEmpty: string;
ipAddressOrEmpty: string;
portOrZero: number;
};
}
type TopologyNode =
| TopologyInstanceNode
| TopologyDomainNode
| TopologyRemoteCallNode;
TopologyEdge
typescript
interface TopologyEdge {
relation: {
source: TopologyNode;
target: TopologyNode;
};
statistic: {
count: number; // 호출 횟수
timeSum: number; // 총 응답 시간 (ms)
failureCount: number; // 실패 횟수
};
}
샘플 데이터
자세한 샘플 데이터는 별도 파일에서 관리됩니다:
typescript
// j5-components/data/sample-topology-data.ts 에서 import
import {
getCompleteTopologyData,
sampleTopologyData,
} from './data/sample-topology-data';
// 완전한 샘플 데이터 사용
const topologyData = getCompleteTopologyData();
// 또는 기본 노드만 사용하고 엣지는 직접 생성
const basicData = sampleTopologyData;
샘플 데이터에는 다음 요소들이 포함되어 있습니다:
- 8개 노드: 웹서버, API게이트웨이, 마이크로서비스들, 데이터베이스, 캐시, 고립된 서비스
- 8개 엣지: 다양한 호출량과 에러 발생 시나리오
- 실제적인 메트릭: count, timeSum, failureCount 값들
- Force 시뮬레이션 테스트: 고립된 노드를 포함한 다양한 네트워크 구조
고급 기능
Force 시뮬레이션
enableForceSimulation
을 활성화하면 D3.js의 force 시뮬레이션을 사용하여 노드들이 물리 법칙에 따라 자동으로 배치됩니다:
- Link Force: 연결된 노드들을 적절한 거리에 배치
- Charge Force: 노드들 간의 반발력으로 겹치지 않게 배치
- Center Force: 노드들을 중앙으로 끌어당김
- Collision Force: 노드들이 겹치지 않도록 충돌 검사
시각적 상태
- 정상 노드: 초록색 배경, 연결된 상태
- 에러 노드: 빨간색 배경, 실패한 엣지가 있는 경우
- 고립 노드: 회색 배경, 연결이 없는 경우
- 선택/호버: 관련된 노드와 엣지가 하이라이팅됨
스케일 컨트롤
- 확대/축소: 마우스 휠 또는 버튼으로 차트 확대/축소
- 자동 스케일: 데이터에 맞는 최적의 스케일 자동 계산
- 뷰포트 이동: 배경 드래그로 차트 이동 가능
활용 사례
- 마이크로서비스 아키텍처 시각화: 서비스 간 의존성 파악
- 성능 병목 지점 탐지: 높은 응답 시간을 가진 연결 식별
- 장애 전파 경로 추적: 실패 카운트 기반 장애 영향 범위 파악
- 시스템 구조 분석: 복잡한 서비스 관계를 직관적으로 이해