Network

네트워크 최적화는 백엔드 성능을 향상시키는 핵심 요소 중 하나이다. 사용자 경험에 직접적인 영향을 미치는 지연 시간과 처리량을 개선함으로써 웹 애플리케이션의 전반적인 성능을 크게 향상시킬 수 있다.

Hosting Backend Close to Users to Minimize Network Latency

네트워크 지연 시간은 데이터가 출발지에서 목적지까지 이동하는 데 걸리는 시간을 의미한다. 물리적 거리는 이 지연 시간에 직접적인 영향을 미치는 요소이다.

지연 시간의 영향

지연 시간이 사용자 경험에 미치는 영향은 상당하다:

지역 분산 배포 전략

사용자와 서버 간의 물리적 거리를 줄이기 위한 전략은 다음과 같다:

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의 이점
서버 측 구현 예시
 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의 주요 기능
서버 측 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에 적합한 콘텐츠 유형

CDN 구현 전략

CDN 서비스 설정

주요 클라우드 제공업체는 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
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);
});

Optimising Backend Performance through Prefetching or Preloading Resources

사용자의 다음 행동을 예측하여 필요한 리소스를 미리 로드함으로써 백엔드 애플리케이션의 체감 속도를 향상시킬 수 있다.

프리페칭 vs. 프리로딩

클라이언트 측 구현

 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));
});

네트워크 최적화 측정 및 모니터링

네트워크 성능 최적화는 지속적인 측정과 모니터링을 통해 이루어진다.

핵심 네트워크 성능 지표

모니터링 도구

성능 모니터링 구현

 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

용어 정리

용어설명

참고 및 출처