OPC DB server Project First Commit

This commit is contained in:
2026-02-09 02:55:47 +00:00
commit 3181052619
53 changed files with 5719 additions and 0 deletions

137
.Notebook/BRIN 설명.md Normal file
View File

@@ -0,0 +1,137 @@
1⃣ BRIN 인덱스란?
Block Range INdex 의 약자
대용량 테이블에서 연속적인 값(순차적/시간적)에 최적화
일반 B-Tree 인덱스보다 작고 빠르게 생성 가능
하지만 랜덤값 검색에는 적합하지 않음
💡 요약:
특징 설명
장점 작은 크기, 빠른 생성, 디스크 절약
단점 랜덤/불규칙 값 검색 느림
사용 예 날짜, 시퀀스, ID 등 순차적 값 컬럼
2⃣ BRIN 인덱스 생성 기본 문법
CREATE INDEX index_name
ON table_name USING brin (column_name);
예제:
CREATE INDEX brin_test_table_age
ON test_table USING brin (age);
test_table의 age 컬럼에 BRIN 인덱스 생성
대량 데이터에서 범위 검색할 때 효율적
3⃣ BRIN 인덱스 옵션
CREATE INDEX brin_test_table_age
ON test_table USING brin (age)
WITH (pages_per_range = 32);
pages_per_range → 한 인덱스 범위에 몇 페이지를 묶을지 설정 (기본값 128)
범위를 작게 하면 검색 정확도 ↑, 인덱스 크기 ↑
범위를 크게 하면 인덱스 작고 빠름, 정확도 ↓
4⃣ 사용 예제
범위 검색
-- age가 30 이상 40 이하인 데이터 검색
SELECT * FROM test_table
WHERE age BETWEEN 30 AND 40;
BRIN 인덱스를 생성하면 범위 검색이 빨라짐
작은 테이블에서는 체감 안 될 수 있음 → 수십만~수백만 행 이상에서 효과적
복합 컬럼 BRIN 인덱스
CREATE INDEX brin_test_table_name_age
ON test_table USING brin (name, age);
여러 컬럼 순서대로 범위 기반 검색 가능
💡 Tip
BRIN 인덱스는 연속적인 값에서 진가
순차적 시퀀스, 타임스탬프 컬럼에 주로 사용
랜덤/고르게 분포된 값 → B-Tree 인덱스가 더 적합
===========================================================================
1⃣ TimescaleDB란?
PostgreSQL 기반 시계열(Time-Series) 데이터베이스 확장
하이퍼테이블(Hypertable) 구조로 자동 파티셔닝
시계열 데이터 삽입, 조회, 집계 최적화
2⃣ BRIN + TimescaleDB의 조합
TimescaleDB는 내부적으로 하이퍼테이블 파티셔닝
시계열 데이터는 시간 컬럼 기준으로 연속적이기 때문에
BRIN 인덱스와 궁합이 매우 좋음
💡 특징:
특징 설명
인덱스 크기 BRIN → 매우 작음
삽입 속도 TimescaleDB → 초당 수십만~수백만 레코드 처리 가능
조회 효율 시간 범위 쿼리 → BRIN + 하이퍼테이블 최적화
3⃣ 기본 사용법
1) TimescaleDB 설치 후 extension 활성화
-- PostgreSQL에서
CREATE EXTENSION IF NOT EXISTS timescaledb;
2) 테이블을 하이퍼테이블로 변환
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
device_id INT NOT NULL,
temperature DOUBLE PRECISION NOT NULL
);
-- 하이퍼테이블로 변환 (시간 컬럼: time)
SELECT create_hypertable('sensor_data', 'time');
3) BRIN 인덱스 생성
-- 시간 기준 BRIN 인덱스
CREATE INDEX brin_sensor_time
ON sensor_data USING brin (time);
-- 필요시 device_id 추가 가능
CREATE INDEX brin_sensor_device_time
ON sensor_data USING brin (device_id, time);
시간 컬럼 순차적 → BRIN 범위 검색에 최적화
작은 인덱스로 수십억 행도 검색 가능
4⃣ 범위 조회 예제
-- 특정 시간 범위 데이터 조회
SELECT *
FROM sensor_data
WHERE time BETWEEN '2026-02-01 00:00:00' AND '2026-02-02 00:00:00';
BRIN 인덱스 덕분에 빠른 범위 스캔 가능
🔹 정리
TimescaleDB = 시계열 최적화 + 자동 파티셔닝
BRIN 인덱스 = 연속적 시간 컬럼에 최적화된 작은 범위 인덱스
조합 = 대용량 시계열 데이터 + 낮은 디스크 사용 + 빠른 범위 쿼리

View File

@@ -0,0 +1,60 @@
1⃣ 프로젝트 폴더에서 NuGet 설치
터미널에서 프로젝트 디렉토리로 이동:
cd ~/projects/DbTest
패키지 설치:
dotnet add package OPCFoundation.NetStandard.Opc.Ua
이 명령어가 NuGet에서 라이브러리 다운로드 + 프로젝트에 추가까지 해줌
설치 후 출력 메시지에 Successfully added 확인
2⃣ 설치 확인
dotnet list package
출력 예시:
Project 'DbTest' has the following package references
[net8.0]:
Top-level Package Requested Resolved
> OPCFoundation.NetStandard.Opc.Ua 1.4.365 1.4.365
3⃣ 테스트 코드
설치 완료 후 간단한 OPC UA 연결 테스트 작성 가능:
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
class Program
{
static async Task Main()
{
var config = new ApplicationConfiguration
{
ApplicationName = "ExperionClient",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier()
},
ClientConfiguration = new ClientConfiguration()
};
await config.Validate(ApplicationType.Client);
var endpointURL = "opc.tcp://<EXPERION_IP>:4840";
var selectedEndpoint = CoreClientUtils.SelectEndpoint(endpointURL, useSecurity: false);
var session = await Session.Create(config,
new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(config)),
false, "", 60000, null, null);
Console.WriteLine("Experion OPC UA 연결 성공!");
}
}

View File

@@ -0,0 +1,400 @@
Experion 서버 연결 테스트 코드를 작성해드리겠습니다!
🚀 빠른 테스트 프로젝트 생성
bashcd ~/projects
mkdir OpcConnectionTest
cd OpcConnectionTest
# 콘솔 프로젝트 생성
dotnet new console
# .NET 8.0 고정
dotnet new globaljson --sdk-version 8.0.123 --force
# OPC UA 패키지 설치
dotnet add package OPCFoundation.NetStandard.Opc.Ua
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
📝 Program.cs
csharpusing Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace OpcConnectionTest
{
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("=== Experion OPC UA Connection Test ===\n");
// Experion 서버 정보 (VMware 환경)
string primaryEndpoint = "opc.tcp://192.168.xxx.xxx:4840"; // Primary 서버 IP
string secondaryEndpoint = "opc.tcp://192.168.xxx.xxx:4840"; // Secondary 서버 IP (있다면)
Console.WriteLine($"Primary Server: {primaryEndpoint}");
Console.WriteLine($"Secondary Server: {secondaryEndpoint}\n");
try
{
// 1. Application Configuration 생성
var config = new ApplicationConfiguration()
{
ApplicationName = "OPC UA Test Client",
ApplicationType = ApplicationType.Client,
ApplicationUri = "urn:localhost:OpcUaTestClient",
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = "Directory",
StorePath = "OPC Foundation/CertificateStores/MachineDefault"
},
AutoAcceptUntrustedCertificates = true, // 테스트용
RejectSHA1SignedCertificates = false
},
TransportConfigurations = new TransportConfigurationCollection(),
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
TraceConfiguration = new TraceConfiguration
{
OutputFilePath = "OpcUaTest.log",
TraceMasks = 0 // 로그 레벨 (0: 없음, 1: Error, 511: 전부)
}
};
await config.Validate(ApplicationType.Client);
// 2. Primary 서버 검색
Console.WriteLine("Step 1: Discovering Primary Server...");
var endpoints = await DiscoverEndpoints(primaryEndpoint);
if (endpoints == null || endpoints.Count == 0)
{
Console.WriteLine("❌ No endpoints found on Primary server!");
return;
}
Console.WriteLine($"✅ Found {endpoints.Count} endpoint(s)");
foreach (var ep in endpoints)
{
Console.WriteLine($" - {ep.EndpointUrl} ({ep.SecurityMode})");
}
// 3. Primary 서버 연결
Console.WriteLine("\nStep 2: Connecting to Primary Server...");
var session = await CreateSession(config, endpoints[0]);
if (session == null)
{
Console.WriteLine("❌ Failed to create session!");
return;
}
Console.WriteLine($"✅ Connected! Session ID: {session.SessionId}");
// 4. 서버 정보 읽기
Console.WriteLine("\nStep 3: Reading Server Information...");
await ReadServerInfo(session);
// 5. Redundancy 정보 읽기 (Experion Redundant 확인)
Console.WriteLine("\nStep 4: Checking Redundancy Support...");
await CheckRedundancy(session);
// 6. 샘플 태그 읽기 (있다면)
Console.WriteLine("\nStep 5: Reading Sample Tags...");
await ReadSampleTags(session);
// 7. 연결 종료
Console.WriteLine("\nStep 6: Closing connection...");
session.Close();
Console.WriteLine("✅ Connection closed successfully!");
// Secondary 서버도 테스트 (선택)
if (!string.IsNullOrEmpty(secondaryEndpoint) && secondaryEndpoint != primaryEndpoint)
{
Console.WriteLine("\n\n=== Testing Secondary Server ===");
await TestSecondaryServer(config, secondaryEndpoint);
}
Console.WriteLine("\n✅ All tests completed!");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ Error: {ex.Message}");
Console.WriteLine($"Stack: {ex.StackTrace}");
}
Console.WriteLine("\nPress any key to exit...");
Console.ReadKey();
}
// Endpoint 검색
static async Task<EndpointDescriptionCollection> DiscoverEndpoints(string serverUrl)
{
try
{
var endpointConfiguration = EndpointConfiguration.Create();
endpointConfiguration.OperationTimeout = 10000;
using (var client = DiscoveryClient.Create(new Uri(serverUrl), endpointConfiguration))
{
var endpoints = await client.GetEndpointsAsync(null);
return endpoints;
}
}
catch (Exception ex)
{
Console.WriteLine($"Discovery failed: {ex.Message}");
return null;
}
}
// 세션 생성
static async Task<ISession> CreateSession(ApplicationConfiguration config, EndpointDescription endpoint)
{
try
{
var endpointConfiguration = EndpointConfiguration.Create(config);
var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfiguration);
var session = await Session.Create(
config,
configuredEndpoint,
false,
config.ApplicationName,
60000,
new UserIdentity(new AnonymousIdentityToken()), // 익명 인증
null
);
return session;
}
catch (Exception ex)
{
Console.WriteLine($"Session creation failed: {ex.Message}");
return null;
}
}
// 서버 정보 읽기
static async Task ReadServerInfo(ISession session)
{
try
{
// Server Status 노드 읽기
var nodeId = new NodeId(Objects.Server_ServerStatus);
var value = await Task.Run(() => session.ReadValue(nodeId));
Console.WriteLine($"Server Status: {value.Value}");
// Server State 읽기
var stateNodeId = new NodeId(Variables.Server_ServerStatus_State);
var stateValue = await Task.Run(() => session.ReadValue(stateNodeId));
Console.WriteLine($"Server State: {stateValue.Value}");
// Current Time 읽기
var timeNodeId = new NodeId(Variables.Server_ServerStatus_CurrentTime);
var timeValue = await Task.Run(() => session.ReadValue(timeNodeId));
Console.WriteLine($"Server Current Time: {timeValue.Value}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to read server info: {ex.Message}");
}
}
// Redundancy 지원 확인
static async Task CheckRedundancy(ISession session)
{
try
{
// ServiceLevel 읽기 (Primary: 200+, Secondary: <200)
var serviceLevelNode = new NodeId(Variables.Server_ServiceLevel);
var serviceLevel = await Task.Run(() => session.ReadValue(serviceLevelNode));
byte level = Convert.ToByte(serviceLevel.Value);
Console.WriteLine($"Service Level: {level}");
if (level >= 200)
{
Console.WriteLine("✅ This is PRIMARY server");
}
else if (level > 0)
{
Console.WriteLine("⚠️ This is SECONDARY server");
}
else
{
Console.WriteLine(" Standalone server (no redundancy)");
}
// ServerRedundancy 노드 확인
var redundancyNode = new NodeId(Objects.Server_ServerRedundancy);
var redundancyValue = await Task.Run(() => session.ReadValue(redundancyNode));
Console.WriteLine($"Redundancy Support: {redundancyValue.StatusCode}");
}
catch (Exception ex)
{
Console.WriteLine($"Redundancy check failed: {ex.Message}");
Console.WriteLine(" Server may not support redundancy");
}
}
// 샘플 태그 읽기
static async Task ReadSampleTags(ISession session)
{
try
{
// Browse root folder
var browser = new Browser(session)
{
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = (int)NodeClass.Variable | (int)NodeClass.Object,
ResultMask = (uint)BrowseResultMask.All
};
// Objects 폴더 탐색
var references = browser.Browse(ObjectIds.ObjectsFolder);
Console.WriteLine($"Found {references.Count} nodes in Objects folder:");
int count = 0;
foreach (var reference in references)
{
Console.WriteLine($" - {reference.DisplayName} ({reference.NodeClass})");
if (++count >= 10) // 처음 10개만 표시
{
Console.WriteLine(" ... (more)");
break;
}
}
// 특정 태그가 있다면 읽기 (NodeId를 알고 있을 때)
// var tagNodeId = new NodeId("ns=2;s=YourTagName");
// var tagValue = await Task.Run(() => session.ReadValue(tagNodeId));
// Console.WriteLine($"Tag Value: {tagValue.Value}");
}
catch (Exception ex)
{
Console.WriteLine($"Tag reading failed: {ex.Message}");
}
}
// Secondary 서버 테스트
static async Task TestSecondaryServer(ApplicationConfiguration config, string serverUrl)
{
try
{
var endpoints = await DiscoverEndpoints(serverUrl);
if (endpoints == null || endpoints.Count == 0)
{
Console.WriteLine("❌ Secondary server not reachable");
return;
}
Console.WriteLine($"✅ Secondary server found with {endpoints.Count} endpoint(s)");
var session = await CreateSession(config, endpoints[0]);
if (session != null)
{
Console.WriteLine("✅ Secondary server connection successful!");
// ServiceLevel 확인
var serviceLevelNode = new NodeId(Variables.Server_ServiceLevel);
var serviceLevel = await Task.Run(() => session.ReadValue(serviceLevelNode));
Console.WriteLine($"Secondary Service Level: {serviceLevel.Value}");
session.Close();
}
}
catch (Exception ex)
{
Console.WriteLine($"Secondary test failed: {ex.Message}");
}
}
}
}
⚙️ 설정 수정 포인트
Line 15-16: Experion 서버 IP 주소 변경
csharpstring primaryEndpoint = "opc.tcp://192.168.1.10:4840"; // ← 실제 IP
string secondaryEndpoint = "opc.tcp://192.168.1.11:4840"; // ← 실제 IP
🚀 실행
bashcd ~/projects/OpcConnectionTest
# 빌드
dotnet build
# 실행
dotnet run
```
---
## 📊 예상 출력
```
=== Experion OPC UA Connection Test ===
Primary Server: opc.tcp://192.168.1.10:4840
Secondary Server: opc.tcp://192.168.1.11:4840
Step 1: Discovering Primary Server...
✅ Found 2 endpoint(s)
- opc.tcp://192.168.1.10:4840 (SignAndEncrypt)
- opc.tcp://192.168.1.10:4840 (None)
Step 2: Connecting to Primary Server...
✅ Connected! Session ID: ns=1;i=123456
Step 3: Reading Server Information...
Server State: Running
Server Current Time: 2026-02-08 15:30:45
Step 4: Checking Redundancy Support...
Service Level: 255
✅ This is PRIMARY server
Redundancy Support: Good
Step 5: Reading Sample Tags...
Found 15 nodes in Objects folder:
- Server (Object)
- DeviceSet (Object)
- ...
Step 6: Closing connection...
✅ Connection closed successfully!
✅ All tests completed!
🔧 문제 해결
1. "Connection refused" 에러
bash# 방화벽 확인
sudo ufw status
sudo ufw allow 4840/tcp
# Experion 서버 ping 확인
ping 192.168.1.10
2. "Certificate validation failed"
csharp// Line 25에서 이미 설정됨
AutoAcceptUntrustedCertificates = true
3. IP 주소 확인
bash# VMware에서 Experion 서버 IP 확인
# Windows 개발 PC에서
ping experion-server-name
📝 다음 단계
연결 성공하면:
✅ Tag 목록 가져오기
✅ Tag 구독 (Subscription) 테스트
✅ 실시간 데이터 수신 테스트

View File

@@ -0,0 +1 @@
셧다운 : sudo shutdown now

View File

@@ -0,0 +1,174 @@
현재 설치 상태 요약
✅ 이미 설치된 것들
1. 개발 환경
✓ .NET SDK 8.0.123
✓ .NET SDK 10.0 (aspnetcore-runtime-10.0)
✓ dotnet-sdk-8.0
2. 데이터베이스
✓ PostgreSQL 16.11
✓ TimescaleDB 2.25.0
✓ postgresql-client-16
3. 시스템 도구
✓ Git 2.43.0
✓ curl, wget
✓ vim, nano
✓ htop (시스템 모니터링)
✓ tmux (터미널 멀티플렉서)
🔧 추가로 설치해야 할 것들
1. OPC UA 라이브러리 (NuGet)
bashcd ~/projects/OpcUaMinimal
dotnet add package OPCFoundation.NetStandard.Opc.Ua --version 1.5.374.126
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client --version 1.5.374.126
또는 더 간단한 라이브러리:
bashdotnet add package Technosoftware.OpcUaNetStandardLibrary --version 7.0.0
2. Entity Framework Core (PostgreSQL)
bashdotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.23
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.23
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 8.0.23
3. SignalR (실시간 통신)
bashdotnet add package Microsoft.AspNetCore.SignalR.Client --version 8.0.23
4. 기타 필수 라이브러리
bash# Logging
dotnet add package Serilog.AspNetCore --version 8.0.3
dotnet add package Serilog.Sinks.File --version 6.0.0
dotnet add package Serilog.Sinks.PostgreSQL --version 3.2.0
# Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
# Background Service
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Hosting.Systemd
5. 시스템 서비스 관련 (Linux에서 필요)
bash# systemd 개발 헤더 (이미 설치됨)
✓ systemd-dev
# 추가 필요한 것
sudo apt update
sudo apt install -y build-essential
📁 프로젝트 구조 생성
bashcd ~/projects
mkdir -p OpcClientSystem
cd OpcClientSystem
# 1. Core 프로젝트
dotnet new classlib -n Domain -o 01_Core/Domain
dotnet new classlib -n Application -o 01_Core/Application
# 2. Infrastructure 프로젝트
dotnet new classlib -n Infrastructure -o 02_Infrastructure
# 3. OPC Client Service
dotnet new worker -n OpcDataCollector.Service -o 03_Service
# 4. Web API
dotnet new webapi -n IndustrialAutomation.API -o 04_WebAPI
# 5. Blazor Web (선택)
dotnet new blazor -n IndustrialAutomation.Web -o 05_Web
# Solution 파일 생성
dotnet new sln -n OpcClientSystem
# 프로젝트들을 Solution에 추가
dotnet sln add 01_Core/Domain/Domain.csproj
dotnet sln add 01_Core/Application/Application.csproj
dotnet sln add 02_Infrastructure/Infrastructure.csproj
dotnet sln add 03_Service/OpcDataCollector.Service/OpcDataCollector.Service.csproj
dotnet sln add 04_WebAPI/IndustrialAutomation.API/IndustrialAutomation.API.csproj
🗄️ PostgreSQL 설정
DB 생성 (아직 안 했다면)
bashsudo -u postgres psql
# PostgreSQL 프롬프트에서
CREATE DATABASE scada;
CREATE USER opcuser WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE scada TO opcuser;
# TimescaleDB 확장 활성화
\c scada
CREATE EXTENSION IF NOT EXISTS timescaledb;
\q
연결 테스트
bashpsql -h localhost -U opcuser -d scada
# 비밀번호 입력 후 연결 확인
🔐 환경 변수 설정
bash# ~/.bashrc 또는 ~/.profile에 추가
echo 'export ConnectionStrings__DefaultConnection="Host=localhost;Database=scada;Username=opcuser;Password=your_password"' >> ~/.bashrc
source ~/.bashrc
🚀 개발 시작 체크리스트
✅ 완료된 것
Ubuntu Server 24.04 설치
PostgreSQL 16 설치
TimescaleDB 설치
.NET SDK 8.0 설치
Git 설치
VSCode Remote SSH 연결
📝 해야 할 것
OPC UA NuGet 패키지 설치
EF Core PostgreSQL 패키지 설치
프로젝트 구조 생성
Database 스키마 생성
OPC Client 기본 연결 테스트
Experion 서버 연결 확인
🎯 다음 단계 제안
bash# 1. 간단한 테스트 프로젝트부터 시작
cd ~/projects
dotnet new console -n OpcTest
cd OpcTest
# 2. OPC UA 패키지 설치
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
# 3. Experion 연결 테스트 코드 작성
# (다음 메시지에서 샘플 코드 제공 가능)
# 4. 테스트 실행
dotnet run
필요한 것이 더 있으면 말씀해주세요!
OPC UA 연결 테스트 코드?
DB 스키마 생성 스크립트?
전체 프로젝트 템플릿 코드?
nuget 이미 설치했는데?오전 10:01아! 죄송합니다. NuGet은 .NET SDK에 포함되어 있으니 이미 사용 가능합니다.
제가 말한 건 NuGet 패키지 관리자가 아니라 NuGet 패키지(라이브러리) 설치를 의미한 거였습니다.
정리하면:
✅ 이미 있는 것 (설치 완료)
✓ NuGet (패키지 관리자) - .NET SDK에 포함
✓ .NET SDK 8.0
✓ PostgreSQL 16
✓ TimescaleDB
📦 해야 할 것 (프로젝트에 라이브러리 추가)
프로젝트를 만든 후에:
bashcd ~/projects/OpcUaMinimal # 또는 새 프로젝트 경로
# 이런 명령어들로 라이브러리 추가
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
NuGet 자체는 설치할 필요 없고,
NuGet을 통해 라이브러리들을 추가하면 됩니다!
🚀 바로 시작할 수 있는 것
bashcd ~/projects/OpcUaMinimal
# OPC UA 라이브러리 추가
dotnet add package OPCFoundation.NetStandard.Opc.Ua.Client
# PostgreSQL 라이브러리 추가
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# 설치 확인
dotnet list package
이제 Experion 서버 연결 테스트 코드가 필요하신가요?