Code Optimization

백엔드 성능 최적화는 현대 웹 애플리케이션 개발에서 필수적인 요소이다. 사용자 경험을 향상시키고, 서버 자원을 효율적으로 활용하며, 확장성을 보장하기 위해서는 다양한 코드 최적화 기법을 적절히 적용해야 한다.

Streaming of Large Requests/Responses

스트리밍의 중요성

대용량 데이터를 처리할 때 전통적인 방식은 전체 데이터를 메모리에 로드한 후 처리하는 것이다.

이 방식은 다음과 같은 문제점을 갖고 있다:

스트리밍 방식은 데이터를 작은 청크(chunk)로 나누어 순차적으로 처리함으로써 이러한 문제를 해결한다.

스트리밍 구현 예시

Node.js에서의 파일 스트리밍:

 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
const fs = require('fs');
const http = require('http');

// 스트리밍 없이 파일 전송 (메모리에 전체 파일 로드)
http.createServer((req, res) => {
  fs.readFile('./large-file.mp4', (err, data) => {
    if (err) throw err;
    
    res.writeHead(200, {'Content-Type': 'video/mp4'});
    res.end(data);
  });
}).listen(3000);

// 스트리밍 방식으로 파일 전송
http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'video/mp4'});
  
  const fileStream = fs.createReadStream('./large-file.mp4');
  fileStream.on('error', (error) => {
    console.error('Stream error:', error);
    res.end();
  });
  
  // 파이프를 통해 파일 스트림을 응답 스트림으로 연결
  fileStream.pipe(res);
}).listen(3001);

Spring Boot에서의 응답 스트리밍:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class StreamingController {
    
    @GetMapping(value = "/download-large-file", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<StreamingResponseBody> downloadLargeFile() {
        return ResponseEntity
            .ok()
            .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=large-file.csv")
            .body(outputStream -> {
                // 청크 단위로 데이터 생성 및 쓰기
                try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream))) {
                    for (int i = 0; i < 1_000_000; i++) {
                        writer.write("Line " + i + " with some data\n");
                        // 일정 간격으로 버퍼 플러시
                        if (i % 10_000 == 0) {
                            writer.flush();
                        }
                    }
                }
            });
    }
}

스트리밍의 활용 사례

  1. 대용량 파일 다운로드: 사용자가 다운로드를 시작하자마자 첫 번째 청크부터 전송 시작
  2. 실시간 로그 모니터링: 로그 데이터를 실시간으로 스트리밍하여 모니터링 시스템에 전달
  3. 비디오/오디오 스트리밍: 미디어 파일을 청크 단위로 전송하여 빠른 재생 시작
  4. 대용량 데이터베이스 쿼리 결과: 결과를 청크 단위로 클라이언트에 전송
  5. 대용량 CSV/JSON 파일 처리: 파일을 한 줄씩 처리하여 메모리 사용량 최소화

Identifying Performance Bottlenecks through Code Profiling

프로파일링의 중요성

코드 프로파일링은 애플리케이션의 실행 흐름을 분석하여 성능 병목점을 찾아내는 과정이다.

다음과 같은 정보를 수집한다:

프로파일링 도구

  1. Java 애플리케이션 프로파일링 도구

    • JProfiler: 메모리, CPU 사용량, 스레드 상태 등 상세 분석
    • VisualVM: JDK에 포함된 무료 프로파일링 도구
    • YourKit: 메모리 누수, 스레드 분석에 탁월
    • Spring Boot Actuator: 애플리케이션 메트릭 제공
  2. Node.js 프로파일링 도구

    • Node.js 내장 프로파일러: --prof 플래그 사용
    • Clinic.js: CPU, 메모리, 이벤트 루프 지연 등 분석
    • New Relic: 실시간 성능 모니터링
    • flamegraphs: 호출 스택 시각화

프로파일링 과정 예시

Node.js 애플리케이션 프로파일링:

 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
// 프로파일링할 간단한 Express 애플리케이션
const express = require('express');
const app = express();

// 비효율적인 함수 (의도적인 예시)
function inefficientCalculation(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      result += i * j;
    }
  }
  return result;
}

app.get('/efficient', (req, res) => {
  const result = req.query.num * 2;
  res.json({ result });
});

app.get('/inefficient', (req, res) => {
  const num = parseInt(req.query.num) || 1000;
  const result = inefficientCalculation(num);
  res.json({ result });
});

app.listen(3000);

프로파일링 실행:

1
2
3
4
5
6
7
8
# CPU 프로파일링 수행
node --prof server.js

# 부하 테스트 실행
ab -n 100 -c 10 "http://localhost:3000/inefficient?num=1000"

# 프로파일 결과 해석
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt

프로파일링 결과 분석 및 최적화

프로파일링 결과를 분석하여 다음과 같은 최적화를 수행할 수 있다:

  1. 핫스팟 식별: 실행 시간이 오래 걸리는 함수 찾기
  2. 불필요한 함수 호출 제거: 중복 호출, 조건부 실행 등
  3. 알고리즘 개선: 시간 복잡도가 높은 알고리즘 교체
  4. 메모리 누수 식별 및 해결: 참조가 해제되지 않는 객체 찾기
  5. 비동기 처리 최적화: 블로킹 코드를 비동기로 전환

최적화 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 최적화 전: O(n²) 시간 복잡도
function inefficientCalculation(n) {
  let result = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      result += i * j;
    }
  }
  return result;
}

// 최적화 후: O(1) 시간 복잡도
// 수학적 공식 사용: sum(i*j) for i,j from 0 to n-1 = (n-1)*n*(n-1)*n/4
function efficientCalculation(n) {
  return (n - 1) * n * (n - 1) * n / 4;
}

Optimization of Algorithms and Data Structures Used

시간 복잡도와 공간 복잡도의 중요성

알고리즘의 효율성은 시간 복잡도(실행 시간)와 공간 복잡도(메모리 사용량)로 측정된다.
백엔드 성능을 최적화하기 위해서는 적절한 알고리즘과 데이터 구조를 선택하는 것이 중요하다.

알고리즘/데이터 구조최적 사용 사례피해야 할 사례
해시 테이블빠른 검색, 삽입, 삭제 (O(1))순서가 중요한 경우
배열/리스트순차 접근, 인덱스 기반 접근빈번한 검색 작업
트리 (이진 검색 트리)정렬된 데이터 검색 (O(log n))불균형 데이터
우선순위 큐, 최대/최소값 빠른 접근일반적인 검색 작업
그래프네트워크 관계, 경로 탐색단순 데이터 저장

알고리즘 최적화 사례

정렬 알고리즘 최적화:

 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
// 최적화 전: 버블 정렬 (O(n²))
public void bubbleSort(int[] arr) {
    int n = arr.length;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                // 교환
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

// 최적화 후: 퀵 정렬 (평균 O(n log n))
public void quickSort(int[] arr, int low, int high) {
    if (low < high) {
        int partitionIndex = partition(arr, low, high);
        quickSort(arr, low, partitionIndex - 1);
        quickSort(arr, partitionIndex + 1, high);
    }
}

private int partition(int[] arr, int low, int high) {
    int pivot = arr[high];
    int i = low - 1;
    
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            i++;
            // 교환
            int temp = arr[i];
            arr[i] = arr[j];
            arr[j] = temp;
        }
    }
    
    // 피벗 위치 교환
    int temp = arr[i + 1];
    arr[i + 1] = arr[high];
    arr[high] = temp;
    
    return i + 1;
}

검색 알고리즘 최적화:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 최적화 전: 선형 검색 (O(n))
def linear_search(arr, target):
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

# 최적화 후: 이진 검색 (O(log n)) - 정렬된 배열에서만 사용 가능
def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    
    while left <= right:
        mid = (left + right) // 2
        
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
            
    return -1

데이터 구조 최적화 사례

리스트 대신 해시맵 사용:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 최적화 전: 배열에서 검색 (O(n))
function findUserByIdUsingArray(users, userId) {
  for (let i = 0; i < users.length; i++) {
    if (users[i].id === userId) {
      return users[i];
    }
  }
  return null;
}

// 최적화 후: 해시맵 사용 (O(1))
function findUserByIdUsingMap(userMap, userId) {
  return userMap.get(userId) || null;
}

// 초기화
const userMap = new Map();
users.forEach(user => userMap.set(user.id, user));

Optimizing Critical Paths and Frequently Accessed Endpoints

핵심 경로 식별

웹 애플리케이션에서 일반적으로 핵심 경로(critical path)는 다음과 같다:

  1. 인증/로그인 처리: 사용자 세션 생성 및 관리
  2. 검색 기능: 사용자가 자주 사용하는 데이터 검색
  3. 결제 프로세스: 전자상거래 사이트의 결제 흐름
  4. 데이터 대시보드: 실시간 데이터 표시
  5. 파일 업로드/다운로드: 대용량 데이터 전송

엔드포인트 최적화 전략

캐싱 적용
 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
@RestController
@RequestMapping("/products")
public class ProductController {
    
    private final ProductService productService;
    private final CacheManager cacheManager;
    
    // 생성자...
    
    @GetMapping("/{id}")
    @Cacheable(value = "products", key = "#id")
    public Product getProduct(@PathVariable Long id) {
        // 이 결과는 캐싱됨 (동일한 ID로 재요청 시 DB 접근 없음)
        return productService.findById(id);
    }
    
    @GetMapping("/popular")
    @Cacheable(value = "popularProducts", sync = true)
    public List<Product> getPopularProducts() {
        // 계산 비용이 높은 인기 제품 목록 조회
        return productService.findPopularProducts();
    }
    
    @PutMapping("/{id}")
    @CachePut(value = "products", key = "#id")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        // 제품 업데이트 후 캐시도 갱신
        return productService.update(id, product);
    }
    
    @DeleteMapping("/{id}")
    @CacheEvict(value = "products", key = "#id")
    public void deleteProduct(@PathVariable Long id) {
        // 제품 삭제 후 캐시에서도 제거
        productService.delete(id);
    }
}
데이터베이스 쿼리 최적화
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 최적화 전: N+1 문제 발생
public List<Order> getOrdersWithItems() {
    List<Order> orders = orderRepository.findAll();
    
    // 각 주문마다 별도의 쿼리 실행 (N+1 문제)
    for (Order order : orders) {
        order.getItems().size(); // 지연 로딩 강제 실행
    }
    
    return orders;
}

// 최적화 후: 조인 쿼리 사용
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
public List<Order> findOrdersWithItemsByStatus(@Param("status") OrderStatus status);
비동기 처리
 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
@RestController
public class AsyncController {
    
    private final TaskService taskService;
    
    @GetMapping("/process")
    public CompletableFuture<ResponseEntity<String>> processAsync() {
        return CompletableFuture.supplyAsync(() -> {
            // 시간이 오래 걸리는 작업
            taskService.performLongRunningTask();
            return ResponseEntity.ok("Processing completed");
        });
    }
    
    @GetMapping("/parallel-tasks")
    public CompletableFuture<List<Result>> executeParallelTasks() {
        CompletableFuture<Result> task1 = taskService.executeTask1();
        CompletableFuture<Result> task2 = taskService.executeTask2();
        CompletableFuture<Result> task3 = taskService.executeTask3();
        
        // 모든 작업이 완료될 때까지 기다림
        return CompletableFuture.allOf(task1, task2, task3)
            .thenApply(v -> Stream.of(task1, task2, task3)
                .map(CompletableFuture::join)
                .collect(Collectors.toList()));
    }
}
응답 압축
1
2
3
4
// Spring Boot application.properties
server.compression.enabled=true
server.compression.min-response-size=1024
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript

Node.js Express:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const compression = require('compression');
const express = require('express');
const app = express();

// 응답 압축 미들웨어 적용
app.use(compression({
  level: 6, // 압축 레벨 (0-9)
  threshold: 1024, // 압축 적용을 위한 최소 크기 (바이트)
}));

app.get('/large-data', (req, res) => {
  // 대용량 JSON 응답
  const largeData = generateLargeData();
  res.json(largeData);
});

Utilizing Compiled Languages like Go or Rust

컴파일 언어의 장점

컴파일 언어는 코드를 기계어로 미리 변환하여 실행 속도가 빠르고, 메모리 관리가 효율적이다. Go와 Rust는 백엔드 개발에 적합한 현대적인 컴파일 언어이다.

특성GoRustNode.js (JavaScript)Python
실행 속도매우 빠름매우 빠름중간느림
메모리 효율성높음매우 높음중간낮음
동시성 처리고루틴 (경량 스레드)스레드 안전성 보장이벤트 루프GIL 제한
컴파일 검사정적 타입강력한 타입 시스템동적 타입동적 타입
학습 곡선중간가파름완만함완만함

Go 언어 예시: 웹 서버

 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
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"
)

// 제품 정보 구조체
type Product struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Price float64 `json:"price"`
}

// 제품 데이터베이스 (예시용 인메모리 저장소)
var (
    products = []Product{
        {ID: 1, Name: "노트북", Price: 1299.99},
        {ID: 2, Name: "스마트폰", Price: 799.99},
        {ID: 3, Name: "헤드폰", Price: 199.99},
    }
    mu = sync.RWMutex{} // 동시성 제어를 위한 뮤텍스
)

// 모든 제품 조회 핸들러
func getProductsHandler(w http.ResponseWriter, r *http.Request) {
    mu.RLock()
    defer mu.RUnlock()
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(products)
}

// 병렬 작업 처리 핸들러
func processDataHandler(w http.ResponseWriter, r *http.Request) {
    // 동시에 여러 데이터 소스에서 정보 수집
    resultCh := make(chan map[string]interface{}, 3)
    
    // 병렬 처리 시작
    go fetchDataSource1(resultCh)
    go fetchDataSource2(resultCh)
    go fetchDataSource3(resultCh)
    
    // 결과 수집
    results := make([]map[string]interface{}, 0, 3)
    for i := 0; i < 3; i++ {
        result := <-resultCh
        results = append(results, result)
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(results)
}

func fetchDataSource1(ch chan<- map[string]interface{}) {
    // 첫 번째 데이터 소스에서 정보 가져오기 (시뮬레이션)
    time.Sleep(100 * time.Millisecond)
    ch <- map[string]interface{}{
        "source": "db1",
        "data": []string{"item1", "item2", "item3"},
    }
}

// fetchDataSource2, fetchDataSource3 함수도 유사하게 구현...

func main() {
    // 라우트 설정
    http.HandleFunc("/products", getProductsHandler)
    http.HandleFunc("/process", processDataHandler)
    
    // 서버 시작
    log.Println("서버 시작: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Rust 언어 예시: 고성능 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
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, RwLock};

// 제품 정보 구조체
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Product {
    id: u32,
    name: String,
    price: f64,
}

// 애플리케이션 상태
struct AppState {
    products: RwLock<Vec<Product>>,
}

// 모든 제품 조회 핸들러
async fn get_products(data: web::Data<Arc<AppState>>) -> impl Responder {
    let products = data.products.read().unwrap();
    HttpResponse::Ok().json(&*products)
}

// 제품 추가 핸들러
async fn add_product(
    data: web::Data<Arc<AppState>>,
    product: web::Json<Product>,
) -> impl Responder {
    let mut products = data.products.write().unwrap();
    products.push(product.into_inner());
    HttpResponse::Created().finish()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 초기 제품 데이터
    let products = vec![
        Product { id: 1, name: "노트북".to_string(), price: 1299.99 },
        Product { id: 2, name: "스마트폰".to_string(), price: 799.99 },
        Product { id: 3, name: "헤드폰".to_string(), price: 199.99 },
    ];
    
    // 공유 상태 생성
    let app_state = Arc::new(AppState {
        products: RwLock::new(products),
    });
    
    // 서버 시작
    println!("서버 시작: http://localhost:8080");
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(app_state.clone()))
            .route("/products", web::get().to(get_products))
            .route("/products", web::post().to(add_product))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

컴파일 언어의 실제 적용 사례

  1. 성능 중심 마이크로서비스: 특정 고성능이 필요한 서비스만 Go/Rust로 구현
  2. API 게이트웨이: 대량의 요청을 처리하는 엔트리 포인트
  3. 실시간 데이터 처리: 대용량 로그 분석, 실시간 통계 등
  4. 시스템 유틸리티: 백업, 모니터링 도구 등
  5. 레거시 시스템 대체: 성능 병목이 있는 기존 시스템 점진적 교체

Architectural Styles and Service Decomposition

주요 백엔드 아키텍처 스타일

모놀리식 아키텍처

모든 구성요소가 단일 애플리케이션에 통합된 형태이다.

장점:

단점:

마이크로서비스 아키텍처

작고 독립적인 서비스들로 시스템을 분해하는 접근 방식이다.

장점:

단점:

서비스 분해 전략

도메인 기반 분해

비즈니스 도메인별로 서비스를 분리하는 방식이다.

예시 (이커머스 플랫폼):

기능적 분해

기술적 기능별로 서비스를 분리하는 방식이다.

예시:

서비스 간 통신 최적화

효율적인 서비스 간 통신은 마이크로서비스 아키텍처의 성능을 좌우하는 핵심 요소이다.

동기식 vs. 비동기식 통신

동기식 통신(REST, gRPC):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// REST API 호출 예시 (Spring WebClient 사용)
@Service
public class OrderService {
    private final WebClient webClient;
    
    public OrderService(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("http://payment-service").build();
    }
    
    public Mono<OrderResponse> processOrder(Order order) {
        return webClient.post()
            .uri("/payments")
            .body(Mono.just(new PaymentRequest(order.getId(), order.getAmount())), PaymentRequest.class)
            .retrieve()
            .bodyToMono(PaymentResponse.class)
            .map(paymentResponse -> {
                // 결제 응답 처리
                return new OrderResponse(order.getId(), paymentResponse.getStatus());
            });
    }
}

비동기식 통신(메시지 큐):

 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
// Kafka 메시지 생산자 예시
@Service
public class OrderService {
    private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
    
    public OrderService(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
        this.kafkaTemplate = kafkaTemplate;
    }
    
    public void processOrder(Order order) {
        // 주문 처리 로직
        OrderEvent orderEvent = new OrderEvent(order.getId(), OrderStatus.CREATED, order.getItems());
        
        // 이벤트 발행 (비동기)
        kafkaTemplate.send("order-events", order.getId().toString(), orderEvent)
            .addCallback(
                result -> log.info("Order event published: {}", order.getId()),
                ex -> log.error("Failed to publish order event", ex)
            );
    }
}

// Kafka 메시지 소비자 예시 (다른 서비스에 구현)
@Service
public class PaymentService {
    @KafkaListener(topics = "order-events")
    public void processOrderEvent(OrderEvent orderEvent) {
        // 주문 이벤트 처리
        if (orderEvent.getStatus() == OrderStatus.CREATED) {
            // 결제 처리 로직
            processPayment(orderEvent);
            
            // 결제 완료 이벤트 발행
            PaymentEvent paymentEvent = new PaymentEvent(
                orderEvent.getOrderId(), 
                PaymentStatus.COMPLETED
            );
            kafkaTemplate.send("payment-events", paymentEvent);
        }
    }
}
통신 최적화 전략
  1. 서비스 메시(Service Mesh) 활용: Istio, Linkerd 등을 사용하여 통신 인프라 추상화
  2. 회로 차단기(Circuit Breaker) 패턴: 장애 확산 방지
  3. API 게이트웨이: 클라이언트와 서비스 사이의 중간 계층
  4. 로컬 캐싱: 반복적인 서비스 호출 방지
  5. 배치 요청: 여러 작은 요청을 하나의 큰 요청으로 통합

Managing Network Issues: Setting Appropriate Connection Timeouts and Implementing Efficient Retry Mechanisms

연결 타임아웃 설정

외부 시스템과의 통신에서 적절한 타임아웃 설정은 매우 중요하다. 타임아웃이 너무 짧으면 불필요한 재시도가 발생하고, 너무 길면 리소스가 낭비된다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// RestTemplate 타임아웃 설정 예시
@Bean
public RestTemplate restTemplate() {
    // 연결 타임아웃, 읽기 타임아웃 설정
    SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
    factory.setConnectTimeout(3000); // 3초
    factory.setReadTimeout(5000);    // 5초
    
    return new RestTemplate(factory);
}

// WebClient 타임아웃 설정 예시
@Bean
public WebClient webClient() {
    HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000) // 연결 타임아웃
        .responseTimeout(Duration.ofSeconds(5));            // 응답 타임아웃
        
    return WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
}

효율적인 재시도 전략

네트워크 통신은 일시적인 장애가 발생할 수 있으므로, 적절한 재시도 메커니즘을 구현하는 것이 중요하다.

Spring Retry 활용 예시
 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
@Configuration
@EnableRetry
public class RetryConfig {
    @Bean
    public RetryTemplate retryTemplate() {
        RetryTemplate retryTemplate = new RetryTemplate();
        
        // 지수 백오프 전략 설정 (재시도 간격이 점점 늘어남)
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(1000);    // 초기 재시도 간격 (1초)
        backOffPolicy.setMultiplier(2);            // 간격 증가 승수
        backOffPolicy.setMaxInterval(10000);       // 최대 재시도 간격 (10초)
        retryTemplate.setBackOffPolicy(backOffPolicy);
        
        // 재시도 정책 설정
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(3);             // 최대 재시도 횟수 (초기 시도 포함)
        retryTemplate.setRetryPolicy(retryPolicy);
        
        return retryTemplate;
    }
}

@Service
public class ProductService {
    private final RestTemplate restTemplate;
    private final RetryTemplate retryTemplate;
    
    // 생성자 주입...
    
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
    public Product getProductById(Long id) {
        return restTemplate.getForObject("/products/" + id, Product.class);
    }
    
    // 수동 재시도 로직
    public Order processOrder(OrderRequest request) {
        return retryTemplate.execute(context -> {
            try {
                // 외부 결제 서비스 호출
                PaymentResponse response = paymentService.processPayment(request.getPaymentInfo());
                
                // 주문 완료 처리
                return orderRepository.save(new Order(request, response.getTransactionId()));
            } catch (HttpServerErrorException | SocketTimeoutException e) {
                // 일시적 오류로 간주하고 재시도
                log.warn("결제 처리 실패, 재시도 중...", e);
                throw e;
            } catch (Exception e) {
                // 영구적 오류로 간주하고 재시도하지 않음
                log.error("결제 처리 심각한 오류", e);
                throw new NonRetryableException("결제 처리 불가", e);
            }
        });
    }
}
재시도 시 주의사항
  1. 멱등성(Idempotency) 보장: 동일한 요청을 여러 번 실행해도 결과가 동일해야 함
  2. 회로 차단기(Circuit Breaker) 결합: 지속적인 실패 시 재시도 중단
  3. 로깅 및 모니터링: 재시도 패턴 분석을 위한 로깅
  4. 백오프 전략: 지수 백오프로 서버 부하 감소

Minimizing Overhead Through Batch Processing

배치 처리는 여러 작업을 그룹화하여 한 번에 처리함으로써 네트워크 및 처리 오버헤드를 줄이는 기법이다.

데이터베이스 배치 처리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 배치 삽입 예시 (JPA/Hibernate)
@Service
@Transactional
public class ProductImportService {
    private final EntityManager entityManager;
    private static final int BATCH_SIZE = 500;
    
    // 생성자 주입...
    
    public void importProducts(List<Product> products) {
        for (int i = 0; i < products.size(); i++) {
            entityManager.persist(products.get(i));
            
            // BATCH_SIZE마다 플러시 및 클리어
            if (i % BATCH_SIZE == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        // 남은 항목 처리
        entityManager.flush();
        entityManager.clear();
    }
}

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
// 프론트엔드에서 여러 API 요청을 하나로 묶는 예시
async function batchGetUserData(userIds) {
  // 개별 요청 대신 배치 요청
  const response = await fetch('/api/users/batch', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ userIds })
  });
  
  return response.json();
}

// 백엔드 배치 처리 API (Node.js/Express)
app.post('/api/users/batch', async (req, res) => {
  const { userIds } = req.body;
  
  // 단일 쿼리로 여러 사용자 조회
  const users = await User.find({ _id: { $in: userIds } });
  
  // ID를 키로 하는 맵 생성
  const userMap = {};
  users.forEach(user => {
    userMap[user._id] = user;
  });
  
  res.json(userMap);
});

메시지 배치 처리

 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
// Kafka 배치 생산자 설정 예시
@Bean
public ProducerFactory<String, EventData> producerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
    props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
    
    // 배치 설정
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);          // 배치 크기 (바이트)
    props.put(ProducerConfig.LINGER_MS_CONFIG, 50);              // 배치 지연 시간 (ms)
    props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy"); // 압축 타입
    
    return new DefaultKafkaProducerFactory<>(props);
}

// Kafka 배치 소비자 설정 예시
@Bean
public ConsumerFactory<String, EventData> consumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
    
    // 배치 설정
    props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);      // 한 번에 가져올 최대 레코드 수
    props.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024 * 1024); // 최소 데이터 크기 (1MB)
    props.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 500);     // 최대 대기 시간 (ms)
    
    return new DefaultKafkaConsumerFactory<>(props);
}

배치 처리의 장단점

장점:

단점:

종합적인 백엔드 최적화 사례 연구

실제 상황에서는 여러 최적화 기법을 종합적으로 적용해야 한다.

다음은 고트래픽 전자상거래 플랫폼의 백엔드 최적화 사례이다.

최적화 결과:

  1. 제품 목록 페이지 로딩 속도 개선: 2초 → 300ms (85% 감소)
  2. 결제 성공률 향상: 95% → 99.8%
  3. 서버 리소스 사용량 감소: CPU 사용률 70% → 30%
  4. 처리량 증가: 초당 트랜잭션 수 500 → 2000

용어 정리

용어설명

참고 및 출처