Network#
네트워크 최적화는 백엔드 성능을 향상시키는 핵심 요소 중 하나이다. 사용자 경험에 직접적인 영향을 미치는 지연 시간과 처리량을 개선함으로써 웹 애플리케이션의 전반적인 성능을 크게 향상시킬 수 있다.
Hosting Backend Close to Users to Minimize Network Latency#
네트워크 지연 시간은 데이터가 출발지에서 목적지까지 이동하는 데 걸리는 시간을 의미한다. 물리적 거리는 이 지연 시간에 직접적인 영향을 미치는 요소이다.
지연 시간의 영향#
지연 시간이 사용자 경험에 미치는 영향은 상당하다:
- 1초 이상의 로딩 시간: 사용자의 사고 흐름 중단
- 3초 이상의 로딩 시간: 이탈률 40% 증가
- 5초 이상의 로딩 시간: 전환율 70% 감소
지역 분산 배포 전략#
사용자와 서버 간의 물리적 거리를 줄이기 위한 전략은 다음과 같다:
1) 멀티 리전 배포#
여러 지역에 애플리케이션을 배포하여 사용자와 가장 가까운 서버에서 서비스를 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // AWS CloudFormation을 사용한 멀티 리전 배포 예시
const multiRegionStack = {
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"PrimaryRegion": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://s3.amazonaws.com/my-templates/app-stack.yaml",
"Parameters": {
"Environment": "Production",
"Region": "us-east-1"
}
}
},
"AsiaRegion": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://s3.amazonaws.com/my-templates/app-stack.yaml",
"Parameters": {
"Environment": "Production",
"Region": "ap-northeast-2" // 서울 리전
}
}
},
"EuropeRegion": {
"Type": "AWS::CloudFormation::Stack",
"Properties": {
"TemplateURL": "https://s3.amazonaws.com/my-templates/app-stack.yaml",
"Parameters": {
"Environment": "Production",
"Region": "eu-central-1" // 프랑크푸르트 리전
}
}
}
}
};
|
2) 지역 기반 DNS 라우팅#
사용자의 지역에 따라 가장 가까운 서버로 라우팅하는 DNS 서비스를 활용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| # AWS Route 53 지역 기반 라우팅 설정 예시
import boto3
route53 = boto3.client('route53')
response = route53.create_health_check(
CallerReference='string',
HealthCheckConfig={
'IPAddress': '192.0.2.44',
'Port': 80,
'Type': 'HTTP',
'ResourcePath': '/health',
'FullyQualifiedDomainName': 'example.com',
'RequestInterval': 30,
'FailureThreshold': 3
}
)
response = route53.change_resource_record_sets(
HostedZoneId='Z3M3LMPEXAMPLE',
ChangeBatch={
'Changes': [
{
'Action': 'CREATE',
'ResourceRecordSet': {
'Name': 'www.example.com',
'Type': 'A',
'SetIdentifier': 'Asia',
'GeoLocation': {
'ContinentCode': 'AS'
},
'TTL': 300,
'ResourceRecords': [
{
'Value': '192.0.2.44' # 아시아 리전 IP
}
]
}
},
{
'Action': 'CREATE',
'ResourceRecordSet': {
'Name': 'www.example.com',
'Type': 'A',
'SetIdentifier': 'Europe',
'GeoLocation': {
'ContinentCode': 'EU'
},
'TTL': 300,
'ResourceRecords': [
{
'Value': '192.0.2.45' # 유럽 리전 IP
}
]
}
}
]
}
)
|
엣지 컴퓨팅#
CDN 엣지 로케이션에서 코드를 실행하여 사용자와 더 가까운 위치에서 로직을 처리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Cloudflare Workers 예시 - 사용자 위치에 따른 응답 맞춤화
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
// 사용자의 지역 정보 가져오기
const userRegion = request.headers.get('CF-IPCountry')
let responseBody = ''
// 지역에 따른 응답 맞춤화
if (userRegion === 'KR') {
responseBody = '안녕하세요! 한국 사용자를 위한 최적화된 컨텐츠입니다.'
} else if (userRegion === 'US') {
responseBody = 'Hello! This is optimized content for US users.'
} else {
responseBody = 'Welcome! This is our global content.'
}
return new Response(responseBody, {
headers: { 'content-type': 'text/plain' }
})
}
|
데이터베이스 지역 전략#
읽기 전용 복제본#
읽기가 많은 애플리케이션에서는 각 지역에 읽기 전용 복제본을 배포하여 지연 시간을 줄일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| -- MySQL 읽기 전용 복제본 설정 예시
-- 마스터 DB에서:
CREATE USER 'replication_user'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'replication_user'@'%';
-- 복제본 DB에서:
CHANGE MASTER TO
MASTER_HOST='master-db-hostname',
MASTER_USER='replication_user',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=107;
START SLAVE;
|
다중 마스터 복제#
쓰기 작업이 많은 애플리케이션에서는 각 지역에 마스터 데이터베이스를 배포하고 복제 메커니즘을 통해 동기화할 수 있다.
지역 샤딩#
사용자 데이터를 지역별로 분할하여 해당 지역에서 주로 접근하는 데이터를 로컬에 저장한다.
1
2
3
4
5
6
7
8
9
10
11
12
| # 지역 기반 데이터 샤딩 로직 예시
def get_database_connection(user_id):
user_region = get_user_region(user_id)
if user_region == 'asia':
return connect_to_asia_db()
elif user_region == 'europe':
return connect_to_europe_db()
elif user_region == 'americas':
return connect_to_americas_db()
else:
return connect_to_default_db()
|
Utilization of HTTP Keep-Alive for Reducing Connection Overhead#
HTTP 프로토콜 최적화는 네트워크 성능 향상의 중요한 요소이다.
HTTP Keep-Alive 활용#
HTTP Keep-Alive는 여러 요청에 단일 TCP 연결을 재사용함으로써 연결 오버헤드를 줄인다.
Keep-Alive의 이점#
- TCP 핸드셰이크 감소: 3-way 핸드셰이크 과정 생략
- TCP 슬로우 스타트 회피: 기존 연결은 이미 최적의 전송 속도 확보
- 서버 리소스 절약: 적은 수의 소켓으로 더 많은 클라이언트 처리
- 지연 시간 감소: 연결 설정에 필요한 왕복 시간(RTT) 절약
서버 측 구현 예시#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Node.js의 Express 서버에서 Keep-Alive 설정
const express = require('express');
const app = express();
const server = require('http').createServer(app);
// Keep-Alive 설정
server.keepAliveTimeout = 60000; // 60초
server.headersTimeout = 65000; // keepAliveTimeout + 5000ms 권장
app.get('/', (req, res) => {
res.send('Hello World!');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Python Flask에서 Keep-Alive 설정
from flask import Flask
from werkzeug.serving import WSGIServer
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == '__main__':
http_server = WSGIServer(('', 5000), app)
http_server.keep_alive_timeout = 60 # 60초
http_server.serve_forever()
|
클라이언트 측 구현 예시#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // JavaScript fetch API에서 Keep-Alive 사용
async function fetchWithKeepAlive() {
const controller = new AbortController();
const options = {
method: 'GET',
headers: {
'Connection': 'keep-alive'
},
signal: controller.signal
};
try {
const response = await fetch('https://api.example.com/data', options);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
|
HTTP/2 활용#
HTTP/2는 여러 최적화 기능을 통해 HTTP/1.1보다 효율적인 통신을 제공한다.
HTTP/2의 주요 기능#
- 멀티플렉싱: 단일 연결에서 여러 요청 및 응답 동시 처리
- 헤더 압축: HPACK 알고리즘을 통한 헤더 데이터 압축
- 서버 푸시: 클라이언트 요청 전 필요한 리소스를 사전에 전송
- 스트림 우선 순위 지정: 중요한 리소스에 더 높은 우선 순위 부여
서버 측 HTTP/2 구현#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // Node.js에서 HTTP/2 서버 구현
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
});
server.on('stream', (stream, headers) => {
// 경로 확인
const path = headers[':path'];
if (path === '/') {
// 메인 HTML 응답
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end('<html><body><h1>Hello World</h1></body></html>');
// 서버 푸시 - CSS 파일을 사전에 푸시
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
if (err) throw err;
pushStream.respond({
'content-type': 'text/css',
':status': 200
});
pushStream.end('body { color: blue; }');
});
}
});
server.listen(8443);
|
HTTP/3 (QUIC) 고려#
최신 HTTP/3는 UDP를 기반으로 하여 연결 설정 시간을 더욱 단축하고 네트워크 변경에 강한 특성을 제공한다.
Utilization of CDNs for Static and Frequently Accessed Assets#
CDN은 전 세계 여러 위치에 분산된 서버 네트워크를 통해 콘텐츠를 사용자와 가까운 곳에서 제공한다.
CDN의 주요 이점#
- 지연 시간 감소: 사용자와 가까운 서버에서 콘텐츠 전송
- 오리진 서버 부하 감소: 요청이 오리진까지 도달하지 않음
- 대역폭 비용 절감: 트래픽 분산으로 인한 비용 효율성
- 가용성 향상: 분산 아키텍처를 통한 장애 내성
CDN에 적합한 콘텐츠 유형#
- 정적 자산: CSS, JavaScript, 이미지, 폰트 파일
- 미디어 파일: 비디오, 오디오 콘텐츠
- API 응답: 자주 접근하고 변경이 적은 API 응답
- 정적 웹페이지: 자주 변경되지 않는 HTML 페이지
CDN 구현 전략#
CDN 서비스 설정#
주요 클라우드 제공업체는 CDN 서비스를 제공한다:
- AWS CloudFront
- Google Cloud CDN
- Azure CDN
- Cloudflare
- Akamai
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| // AWS CDK를 사용한 CloudFront 배포 예시
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
export class CdnStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// 콘텐츠를 저장할 S3 버킷
const bucket = new s3.Bucket(this, 'StaticAssetsBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
// CloudFront 배포 생성
const distribution = new cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: new origins.S3Origin(bucket),
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
// 추가 경로 패턴 설정
additionalBehaviors: {
'/api/*': {
origin: new origins.HttpOrigin('api.example.com'),
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
allowedMethods: cloudfront.AllowedMethods.ALL,
},
'/assets/*': {
origin: new origins.S3Origin(bucket),
cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED,
// 1년 캐시 (불변 자산)
functionAssociations: [{
function: new cloudfront.Function(this, 'AddCacheHeaders', {
code: cloudfront.FunctionCode.fromInline(`
function handler(event) {
var response = event.response;
response.headers['cache-control'] = {value: 'public, max-age=31536000, immutable'};
return response;
}
`)
}),
eventType: cloudfront.FunctionEventType.VIEWER_RESPONSE
}]
}
}
});
}
}
|
백엔드 API와 CDN 통합#
API 응답도 CDN을 통해 캐싱할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Express.js 백엔드에서 캐시 헤더 설정
app.get('/api/products', (req, res) => {
// 제품 데이터 가져오기
const products = getProducts();
// CDN 캐싱을 위한 헤더 설정
res.set('Cache-Control', 'public, max-age=300'); // 5분 캐싱
res.set('Surrogate-Control', 'max-age=3600'); // CDN에서는 1시간 캐싱
res.set('Vary', 'Accept-Language'); // 언어별 캐싱
res.json(products);
});
// 개인화된 콘텐츠는 캐싱 방지
app.get('/api/user-profile', (req, res) => {
const userId = req.user.id;
const profile = getUserProfile(userId);
res.set('Cache-Control', 'private, no-store');
res.json(profile);
});
|
사용자의 다음 행동을 예측하여 필요한 리소스를 미리 로드함으로써 백엔드 애플리케이션의 체감 속도를 향상시킬 수 있다.
프리페칭 vs. 프리로딩#
- 프리페칭(Prefetching): 사용자가 향후 필요할 가능성이 높은 리소스를 미리 다운로드
- 프리로딩(Preloading): 현재 페이지에서 곧 필요하지만 아직 발견되지 않은 리소스를 미리 로드
클라이언트 측 구현#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- HTML에서 프리로드 구현 -->
<head>
<!-- 중요한 CSS 파일 프리로드 -->
<link rel="preload" href="/styles/main.css" as="style">
<!-- 폰트 프리로드 -->
<link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
<!-- 다음 페이지 프리페치 -->
<link rel="prefetch" href="/next-page.html">
<!-- API 데이터 프리페치 -->
<link rel="prefetch" href="/api/common-data">
</head>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // JavaScript에서 동적 프리페칭 구현
document.addEventListener('DOMContentLoaded', () => {
// 메인 콘텐츠 로드 완료 후 추가 리소스 프리페치
setTimeout(() => {
const links = [
'/api/recommended-products',
'/images/banner-image.jpg',
'/next-likely-page.html'
];
links.forEach(url => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
document.head.appendChild(link);
});
}, 1000);
});
|
백엔드 측 구현#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // Express.js에서 서버 푸시 또는 HTTP 헤더를 통한 리소스 힌트
app.get('/', (req, res) => {
// HTML 응답
const html = `
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<h1>Welcome</h1>
</body>
</html>
`;
// 리소스 힌트 헤더 추가
res.set('Link', [
'</styles/main.css>; rel=preload; as=style',
'</scripts/app.js>; rel=preload; as=script',
'</api/initial-data>; rel=prefetch'
].join(', '));
res.send(html);
});
|
GraphQL에서의 데이터 프리페칭#
GraphQL을 사용하면 단일 요청으로 필요한 모든 데이터를 한 번에 가져올 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Apollo Client를 사용한 데이터 프리페칭
import { ApolloClient, InMemoryCache } from '@apollo/client';
import { GET_USER_DATA, GET_PRODUCT_DETAILS } from './queries';
const client = new ApolloClient({
uri: 'https://api.example.com/graphql',
cache: new InMemoryCache()
});
// 현재 필요한 데이터 로드
client.query({ query: GET_USER_DATA, variables: { userId: 'current-user' } })
.then(result => {
// UI 렌더링
renderUserProfile(result.data);
// 곧 필요할 수 있는 데이터 미리 로드
client.query({
query: GET_PRODUCT_DETAILS,
variables: { productId: 'recommended-product' }
});
});
|
효율적인 데이터 전송 기법#
압축 활용#
데이터 압축은 전송 크기를 줄여 네트워크 대역폭을 절약하고 전송 시간을 단축한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Express.js에서 압축 미들웨어 사용
const express = require('express');
const compression = require('compression');
const app = express();
// 압축 미들웨어 설정
app.use(compression({
// 1KB 이상의 응답만 압축
threshold: 1024,
// 압축 레벨 (1: 가장 빠름, 9: 가장 높은 압축률)
level: 6,
// 특정 콘텐츠 타입만 압축
filter: (req, res) => {
const contentType = res.getHeader('Content-Type');
return /text|json|javascript|css|xml/i.test(contentType);
}
}));
app.get('/api/large-data', (req, res) => {
// 대용량 데이터 반환 (자동으로 압축됨)
res.json(getLargeDataSet());
});
|
콘텐츠 인코딩 최적화#
서버는 클라이언트가 지원하는 최적의 압축 알고리즘을 선택해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Node.js에서 다양한 압축 알고리즘 지원
const express = require('express');
const compression = require('compression');
const shrinkRay = require('shrink-ray-current');
const app = express();
// Brotli 또는 Gzip 압축 사용
app.use(shrinkRay({
brotli: { quality: 8 },
zlib: { level: 6 }
}));
app.get('/api/data', (req, res) => {
res.json({ data: 'Large response...' });
});
|
JSON 대신 Protocol Buffers 또는 MessagePack 사용#
바이너리 직렬화 형식은 JSON보다 더 효율적인 데이터 전송을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Node.js에서 Protocol Buffers 사용 예시
const protobuf = require('protobufjs');
const express = require('express');
const app = express();
// 프로토콜 정의 로드
const root = protobuf.loadSync("./protos/messages.proto");
const UserMessage = root.lookupType("userpackage.User");
app.get('/api/user/:id', (req, res) => {
// 사용자 데이터 가져오기
const userData = getUserData(req.params.id);
// 프로토콜 버퍼로 인코딩
const message = UserMessage.create(userData);
const buffer = UserMessage.encode(message).finish();
// 바이너리 데이터 전송
res.set('Content-Type', 'application/x-protobuf');
res.send(Buffer.from(buffer));
});
|
네트워크 최적화 측정 및 모니터링#
네트워크 성능 최적화는 지속적인 측정과 모니터링을 통해 이루어진다.
핵심 네트워크 성능 지표#
- 지연 시간(Latency): 요청과 응답 사이의 시간
- 처리량(Throughput): 단위 시간당 전송되는 데이터량
- 오류율(Error Rate): 실패한 요청의 비율
- 연결 시간(Connection Time): TCP 연결 설정에 걸리는 시간
- 첫 바이트까지의 시간(TTFB): 요청 후 첫 바이트를 받기까지의 시간
모니터링 도구#
- 브라우저 개발자 도구: 네트워크 요청 분석
- Lighthouse: 웹사이트 성능 측정
- WebPageTest: 다양한 위치와 조건에서 웹사이트 테스트
- New Relic, Datadog: 애플리케이션 성능 모니터링
- Pingdom, Uptrends: 가용성 및 응답 시간 모니터링
성능 모니터링 구현#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| // 클라이언트 측 성능 측정
const performanceData = {};
// 네트워크 요청 시작 시간 기록
function trackRequestStart(url) {
performanceData[url] = { startTime: Date.now() };
}
// 네트워크 요청 완료 시간 기록 및 보고
function trackRequestEnd(url, success) {
if (performanceData[url]) {
const endTime = Date.now();
const duration = endTime - performanceData[url].startTime;
// 분석 서버로 성능 데이터 전송
navigator.sendBeacon('/analytics/network', JSON.stringify({
url,
duration,
success,
timestamp: endTime
}));
delete performanceData[url];
}
}
// 사용 예시
async function fetchData(url) {
trackRequestStart(url);
try {
const response = await fetch(url);
const data = await response.json();
trackRequestEnd(url, true);
return data;
} catch (error) {
trackRequestEnd(url, false);
throw error;
}
}
|
고급 네트워크 최적화 기법#
WebSocket을 활용한 실시간 통신#
장시간 연결을 유지하고 양방향 통신이 필요한 경우 WebSocket이 효율적이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // Node.js에서 WebSocket 서버 구현 (Socket.IO 사용)
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = new Server(server);
// WebSocket 연결 처리
io.on('connection', (socket) => {
console.log('Client connected');
// 특정 룸 조인
socket.join('updates-room');
// 클라이언트에서 이벤트 수신
socket.on('client-event', (data) => {
console.log('Received:', data);
// 응답 전송
socket.emit('server-response', { status: 'received' });
});
// 연결 종료 처리
socket.on('disconnect', () => {
console.log('Client disconnected');
});
});
// 주기적인 업데이트 브로드캐스트
setInterval(() => {
io.to('updates-room').emit('update', { time: new Date() });
}, 10000);
server.listen(3000, () => { console.log('Server listening on port 3000'); });
|
Server-Sent Events(SSE)#
서버에서 클라이언트로 단방향 실시간 업데이트를 제공해야 할 때 효율적이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| // Express.js에서 SSE 구현
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
// SSE 헤더 설정
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 클라이언트에 ID 부여
const clientId = Date.now();
// 주기적인 이벤트 전송
const intervalId = setInterval(() => {
res.write(`id: ${Date.now()}\n`);
res.write(`data: ${JSON.stringify({ time: new Date(), value: Math.random() })}\n\n`);
}, 5000);
// 클라이언트 연결 종료 시 정리
req.on('close', () => {
clearInterval(intervalId);
console.log(`Client ${clientId} connection closed`);
});
});
app.listen(3000, () => {
console.log('SSE server started on port 3000');
});
|
Service Worker를 활용한 네트워크 최적화#
Service Worker는 오프라인 경험과 네트워크 요청 가로채기를 통한 성능 향상을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| // Service Worker 등록
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('ServiceWorker registered:', registration);
})
.catch(error => {
console.log('ServiceWorker registration failed:', error);
});
});
}
// sw.js - Service Worker 파일
const CACHE_NAME = 'v1-cache';
const URLS_TO_CACHE = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/api/initial-data'
];
// 설치 단계 - 리소스 캐싱
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Cache opened');
return cache.addAll(URLS_TO_CACHE);
})
);
});
// 활성화 단계 - 이전 캐시 정리
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
// 네트워크 요청 가로채기
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 캐시에서 찾으면 반환
if (response) {
return response;
}
// 캐시에 없으면 네트워크 요청
return fetch(event.request)
.then(response => {
// 유효한 응답인지 확인
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 응답 복제 (스트림은 한 번만 사용 가능)
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
// API 요청만 캐싱 제외
if (!event.request.url.includes('/api/')) {
cache.put(event.request, responseToCache);
}
});
return response;
});
})
);
});
|
모바일 환경에서의 네트워크 최적화#
모바일 환경은 네트워크 연결이 불안정하고 제한된 데이터 요금제를 사용하는 경우가 많다.
모바일 최적화 전략#
적응형 로딩#
네트워크 상태에 따라 콘텐츠 로딩 전략을 조정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 네트워크 상태에 따른 콘텐츠 로딩 전략
function loadContent() {
// 네트워크 연결 정보 확인
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!connection) {
// 연결 정보를 사용할 수 없는 경우 기본 로딩
loadDefaultContent();
return;
}
const { effectiveType, saveData } = connection;
// 데이터 절약 모드 확인
if (saveData) {
loadMinimalContent();
return;
}
// 네트워크 품질에 따른 로딩 전략
switch (effectiveType) {
case 'slow-2g':
case '2g':
loadLowResolutionContent();
break;
case '3g':
loadMediumResolutionContent();
break;
case '4g':
loadHighResolutionContent();
break;
default:
loadDefaultContent();
}
}
|
오프라인 지원#
Service Worker와 IndexedDB를 활용하여 오프라인 기능을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| // IndexedDB를 사용한 오프라인 데이터 저장
function storeDataForOffline(data) {
const request = indexedDB.open('OfflineDB', 1);
request.onupgradeneeded = event => {
const db = event.target.result;
db.createObjectStore('userData', { keyPath: 'id' });
};
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction(['userData'], 'readwrite');
const store = transaction.objectStore('userData');
// 데이터 저장
store.put(data);
transaction.oncomplete = () => {
console.log('Data stored for offline use');
};
};
}
// 오프라인 데이터 로드
function loadOfflineData() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('OfflineDB', 1);
request.onsuccess = event => {
const db = event.target.result;
const transaction = db.transaction(['userData'], 'readonly');
const store = transaction.objectStore('userData');
const allDataRequest = store.getAll();
allDataRequest.onsuccess = () => {
resolve(allDataRequest.result);
};
allDataRequest.onerror = error => {
reject(error);
};
};
request.onerror = error => {
reject(error);
};
});
}
|
마이크로서비스 환경에서의 네트워크 최적화#
마이크로서비스 아키텍처에서는 서비스 간 통신이 많아 네트워크 최적화가 특히 중요하다.
API 게이트웨이 활용#
API 게이트웨이는 클라이언트와 백엔드 서비스 사이의 중간 계층으로 다양한 최적화를 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| // Node.js Express를 사용한 간단한 API 게이트웨이 구현
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const rateLimit = require('express-rate-limit');
const compression = require('compression');
const cache = require('memory-cache');
const app = express();
// 압축 활성화
app.use(compression());
// 레이트 리미팅
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1분
max: 100, // 1분당 최대 100 요청
standardHeaders: true,
legacyHeaders: false,
});
app.use(limiter);
// 캐싱 미들웨어
const cacheMiddleware = (duration) => {
return (req, res, next) => {
const key = '__express__' + req.originalUrl || req.url;
const cachedBody = cache.get(key);
if (cachedBody) {
res.send(cachedBody);
return;
}
const originalSend = res.send;
res.send = function(body) {
cache.put(key, body, duration * 1000);
originalSend.call(this, body);
};
next();
};
};
// 사용자 서비스 라우팅
app.use('/api/users', cacheMiddleware(60), createProxyMiddleware({
target: 'http://user-service:3001',
changeOrigin: true,
pathRewrite: {'^/api/users': '/users'}
}));
// 제품 서비스 라우팅
app.use('/api/products', cacheMiddleware(300), createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true,
pathRewrite: {'^/api/products': '/products'}
}));
// 주문 서비스 라우팅 (캐싱 없음)
app.use('/api/orders', createProxyMiddleware({
target: 'http://order-service:3003',
changeOrigin: true,
pathRewrite: {'^/api/orders': '/orders'}
}));
app.listen(3000, () => {
console.log('API Gateway running on port 3000');
});
|
서비스 메시 도입#
서비스 메시는 마이크로서비스 간 통신을 관리하고 최적화하는 인프라 계층이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # Istio 서비스 메시 설정 예시
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: product-service
spec:
hosts:
- product-service
http:
- route:
- destination:
host: product-service
subset: v1
retries:
attempts: 3
perTryTimeout: 2s
timeout: 5s
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: product-service
spec:
host: product-service
trafficPolicy:
connectionPool:
tcp:
maxConnections: 100
http:
http1MaxPendingRequests: 50
maxRequestsPerConnection: 10
outlierDetection:
consecutiveErrors: 5
interval: 30s
baseEjectionTime: 30s
subsets:
- name: v1
labels:
version: v1
|
용어 정리#
참고 및 출처#