그래프(Graph)란 무엇일까?

그래프는 정점(Vertex)와 간선(Edge)으로 구성된 자료구조이다.

 

  • 정점(Vertex) : 그래프의 구성요소로, 사람이나 도시처럼 개별 요소를 나타낸다.
  • 간선(Edge) : 정점들을 연결하는 선이다. 두 정점 사이의 관계(예:도로, 친구 관계)를 나타낸다.
  • 예시 : 학교에서 여러 학생(정점)들이 친구 관계(관선)를 맺고 있는 것을 그래프로 생각할 수 있다.

 

그래프의 표현 방법

그래프를 컴퓨터에서 표현하는 방법에는 여러 가지가 있다. 가장 일반적인 두 가지 방법은 인접 리스트인접 행렬이다.

  • 인접 리스트(Adjacency List) : 각 정점에 연결된 정접들의 리스트를 저장한다.

       예시

0: [1, 2]
1: [0, 3]
2: [0, 3]
3: [1, 2]

 

  • 인접 행렬(Adjacency Matrix) : 정점 간의 연결 여부를 2차원 배열 형태로 저장한다.

        예시

matrix[0][1] = 1

 

 

현실 세계에서의 그래프 예시로는 

  • 학교 네트워크 : 학생들을 정점으로 하고, 친구 관계를 간선으로 표현
  • 지도와 도로 : 도시를 정점, 도로를 간선으로 표현하여 경로 탐색 문제 해결
  • 인터넷 : 웹 페이지를 정점으로 하고, 하이퍼링크를 간선으로 표현하여 검색 알고리즘 등에 응용

등이 있다.

 

 

 

깊이 우선 탐색(DFS, Depth First Search)

1. DFS 의 기본 원리

DFS는 시작 정점에서 출발하여 한 방향으로 끝까지 탐색한 후, 더 이상 진행할 수 없으면 마지막 분기점으로 돌아가 다른 경로를 탐색하는 방식이다. 이러한 과정은 재귀 호출을 통해 자연스럽게 구현된다.

 

2. DFS의 동작 방식과 재귀 호출

동작 방식 :

  1. 시작 정점을 방문하고, 방문 목록에 추가한다.
  2. 현재 정점에서 인접한 정점 중 아직 방문하지 않은 정점을 선택한다.
  3. 선택한 정점으로 이동하여 같은 과정을 반복한다.
  4. 더 이상 방문할 정점이 없으면, 이전 정점으로 되돌아가 (Backtracking) 다른 경로를 탐색한다.

재귀 호출 : DFS는 재귀적으로 자신을 호출하면서, 깊은 경로를 먼저 탐색한다.

 

 

3. DFS의 장단점

 

장점 :

  • 구현이 간단하고 직관적이다.
  • 경로 탐색, 사이클 검출 등 다양한 문제에 응용할 수 있다.

단점 : 

  • 재귀 호출로 인해 스택 오버플로우가 발생할 수 있다.
  • 최악의 경우 모든 정점을 방문하므로, 시간 복잡도가 높을 수 있다.

 

4. DFS 구현 예제(Kotlin)

// DFS를 구현하기 위해, 방문한 정점을 기록할 집합과 재귀 함수를 사용합니다.
fun dfs(node: Int, visited: MutableSet<Int>, graph: Map<Int, List<Int>>) {
    // 현재 정점을 방문 목록에 추가하고, 출력합니다.
    visited.add(node)
    println("DFS 방문: $node")

    // 현재 정점과 연결된 모든 정점을 순회합니다.
    for (neighbor in graph[node] ?: emptyList()) {
        // 아직 방문하지 않은 정점이면 재귀 호출합니다.
        if (!visited.contains(neighbor)) {
            dfs(neighbor, visited, graph)
        }
    }
}

fun mainDFS() {
    // 간단한 무방향 그래프를 인접 리스트로 표현합니다.
    val graph: Map<Int, List<Int>> = mapOf(
        0 to listOf(1, 2), // 0번 정점은 1번,2번 정점에 간선을 갖는다.
        1 to listOf(0, 3),
        2 to listOf(0, 3),
        3 to listOf(1, 2)
    )
    
    val visited = mutableSetOf<Int>()
    println("=== DFS 탐색 결과 ===")
    dfs(0, visited, graph)
}

 

위에 코드에서 인접 리스트로 표현된 그래프를 실제로 살펴보면

인접리스트 예시

위 그림에서 방향성을 나타내는 화살표를 빼고 안쪽에 있는 간선 3개를 빼면 위의 코드의 그래프이다.

 

해당 코드를 실행해보면 시작 정점 0에서 시작하여 0 > 1 > 3> 2 순으로 방문을 진행하고, 각 단계에서 재귀 호출이 이루어지고, 되돌아가며 다른 경로를 탐색하는 모습은 미로에서 한 길로 쭉 들어갔따가 다시 돌아가는 것과 비슷하다.

 

 

 

 

너비 우선 탐색(BFS, Breadth First Search)

1. BFS의 기본 원리

BFS는 시작 정점에서부터 인접한 모든 정점을 먼저 방문하고, 그 다음 단계의 정점을 방문하는 방식이다. 즉, "가까운 것부터 넓게" 탐색하는 방법이다.

 

 

2. BFS의 동작 방식과 큐의 역할

동작 방식 :

  1. 시작 정점을 방문하고, 큐에 추가한다.
  2. 큐에서 정점을 꺼내 그 정점과 인접한 모든 정점을 방문한다.
  3. 방문한 정점을 큐에 추가하면서, 순차적으로 탐색한다.

큐의 역할 :  BFS에서는 먼저 들어온 정점을 먼저 처리하는 FIFO(선입선출) 자료구조인 큐를 사용한다.

 

 

3. BFS의 장단점

장점 : 

  • 최단 경로를 찾는 데 효과적이다.
  • 큐를 사용하므로 DFS보다 스택 오버플로우 위험이 적다.

단점 : 

  • 모든 인접 정점을 저장해야 하므로 메모리 사용량이 많아질 수 있다.
  • 그래프의 모든 정점을 한 번씩 방문하므로, 큰 그래프에서는 시간이 오래 걸릴 수 있다.

 

4. BFS 구현 예제(Kotlin)

import java.util.LinkedList
import java.util.Queue

fun bfs(start: Int, graph: Map<Int, List<Int>>) {
    val visited = mutableSetOf<Int>()
    val queue: Queue<Int> = LinkedList()

    // 시작 정점을 방문 처리하고 큐에 추가합니다.
    visited.add(start)
    queue.offer(start)

    while (queue.isNotEmpty()) {
        val node = queue.poll()
        println("BFS 방문: $node")
        
        // 현재 정점과 연결된 모든 정점을 순회하며 방문하지 않은 정점을 큐에 추가합니다.
        for (neighbor in graph[node] ?: emptyList()) {
            if (!visited.contains(neighbor)) {
                visited.add(neighbor)
                queue.offer(neighbor)
            }
        }
    }
}

fun mainBFS() {
    // 간단한 무방향 그래프를 인접 리스트로 표현합니다.
    val graph: Map<Int, List<Int>> = mapOf(
        0 to listOf(1, 2),
        1 to listOf(0, 3),
        2 to listOf(0, 3),
        3 to listOf(1, 2)
    )

    println("=== BFS 탐색 결과 ===")
    bfs(0, graph)
}

 

코드의 실행에 대해서 생각해보면 시작 정점 0에서 시작하여, 먼저 0에 인접한 1과 2를 방문하고, 그 후 1과 2의 인접 정점(3 등)을 방문하는 순서로 진행된다.

 

 

 

DFS와 BFS의 비교

1. 탐색 방식의 차이

  • DFS : 한 방향으로 깊게 들어간 후, 더 이상 진행할 수 없으면 되돌아가는 방식 -> "깊이 우선"으로 탐색한다.
  • BFS : 시작 정점에서 가까운 정점을 모두 방문한 후, 그 다음 레벨로 넘어가는 방식 -> "너비 우선"으로 탐색한다.

 

2. 시간 복잡도와 메모리 사용 비교

  • 시간 복잡도 : 두 알고리즘 모두 그래프의 정점 수(V)와 간선 수 (E)에 따라 O(V+E)의 시간 복잡도를 가진다.
  • 메모리 사용 : DFS는 재귀 호출(또는 스택)을 사용하므로 깊이에 따라 메모리 사용량이 결정된다.                                                                         / BFS는 큐에 모든 인접 정점을 저장하므로, 그래프가 넓을 경우 메모리 사용량이 증가할 수 있다.

 

언제 각각 사용해야 할까?

  • DFS 사용 경우 : 경로 깊이가 중요한 경우 / 모든 경로를 탐색해야 하는 문제(예: 순열, 조합, 백트래킹 문제)
  • BFS 사용 경우 : 최단 경로 문제 / 너비 우선 탐색을 통해 빠르게 탐색할 수 있는 경우

 

위의 DFS와 BFS는 문제를 더 풀어보면서 Kotlin 을 연습해봐야겠다.

 

브라우저 캐시는 단순히 프론트엔드의 영역이 아니라, 실제 서비스의 성능과 트래픽 비용, 심지어 버그 발생 가능성까지 영향을 미치는 아주 중요한 요소이다.

 

브라우저 캐시란?

브라우저 캐시는 클라이언트(사용자의 웹 브라우저)가 자주 사용하는 데이터를 저장해두고, 다음에 다시 요청할 때 빠르게 보여주기 위한 기술이다. 

 

예 : 어떤 쇼핑몰 사이트에 방문시, 그 사이트의 로고 이미지가 매번 서버에서 오면 느릴것이다. 그래서 브라우저는 이걸 한 번 다운로드한 뒤, 다음부터는 자기 컴퓨터에 저장된걸 보여준다.

 

 

브라우저 캐시의 핵심 메커니즘 

1. Cache-Control

서버가 클라이언트에게 이 리소스를 어떻게 캐시해야 할지 알려주는 HTTP 헤더이다.

  • max-age=3600 -> 1시간 동안 캐시해라
  • public -> 누구나 캐시해도 된다

2. ETag / Last-Modified

  • 캐시된 파일이 변경됐는지 확인하는 데 사용한다.
  • 변경이 없으면 304 Not Modified로 응답해서 데이터를 아예 보내지 않는다.

 

 

3. 실무에서 사용 가능한 전략

리소스 타입 캐시 전략
이미지, JS, CSS long-term 캐시 + 파일명에 해시
HTML 페이지 no-cache 또는 must-revalidate
API 응답(GET) short max-age 또는 no-store

 

 

4. CDN과의 캐시 연계

클라우드플레어, AWS CloudFront 같은 CDN은 브라우저 뿐 아니라 서버 앞단에서 전 세계에 캐시를 제공한다. 이를 통해 TTFB(Time to First Byte)를 극적으로 낮출 수 있다. 

 

서버 -> CDN -> 브라우저 순으로 캐시 계층을 구성하면 성능 최적화에 큰 효과가 있다.

 

 

5. 디버깅 방법

브라우저 개발 도구에서 Network 탭을 열고, 리소스를 확인하면 아래 정보들을 볼 수 있다.

  • Status : 200 OK vs 304 Not Modified
  • Cache-Control 헤더
  • ETag / Last-Modified 여부

실제로 문제를 겪을 때 이 도구를 통해 빠르게 원인을 추적할 수 있다.

요즘 인스타 광고글이나 여러 블로그 포스팅등등 여러 곳에서 쿠버네티스라는 용어를 자주 접했다. 저게 뭘까...? 처음엔 네카라쿠배인줄.. 

 

쿠버네티스 그게 도대체 뭐야?

 

쿠버네티스란?

쿠버네티스는 컨테이너화된 애플리케이션을 여러 대의 컴퓨터(서버)에 효율적으로 배포하고 관리할 수 있도록 돕는 도구이다.

 

컨테이너?는 뭘까?

컨테이너는 애플리케이션과 그에 필요한 라이브러리, 설정 파일 등을 한데 묶어 어디서든 똑같이 실행할 수 있도록 만든 작은 가상화 기술이다.

 

쿠버네티스에서는 하나 이상의 컨테이너를 묶어서 Pod라고 부른다. 쉽게 말해, Pod는 애플리케이션의 한 부분으로, 내 앱이 실제로 살아있는 단위라고 생각하면 된다.

 

네트워크적 관점에서 쿠버네티스는 Pod가 서로 통신할 수 있는 가상 네트워크 환경을 제공하며, 외부 사용자도 이 네트워크를 통해 내 애플리케이션에 접근할 수 있도록 해준다.

 

 

쿠버네티스 네트워크의 기본 구조

쿠버네티스 클러스터(여러 서버가 모여 만든 하나의 큰 시스템) 안에는 여러 가지 네트워크 구성 요소가 있다. 크게 세 가지로 나눠볼 수 있는데

  • Pod 네트워크 : 모든 Pod는 서로 IP 주소를 부여받아 같은 네트워크 상에서 직접 연결된다. 마치 같은 동네에 사는 사람들이 서로 쉽게 연락할 수 있는 것과 비슷하다.
  • 서비스(Service) : 여러 Pod가 모여 있는 애플리케이션에는 '서비스'라는 추상화 계층을 두어, 외부 사용자나 다른 Pod들이 하나의 고정된 주소(예: 클러스터IP)를 통해 애플리케이션에 접근할 수 있도록 한다.
  • 인그레스 / 이그레스 : 외부 네트워크(인터넷)와 내부 네트워크(클러스터)의 경계 역할을 담당한다. 인그레스는 외부에서 들어오는 요청을, 이그레스는 내부에서 외부로 나가는 요청을 다룬다.

또 쿠버네티스는 CNI(Container Network Interface)라는 표준을 사용해서 네트워크를 구성한다. 대표적인 CNI 플러그인으로 Calio. Flannel, Cilium 등이 있는데, 이들은 각각 특색 있는 기능을 제공해 네트워크 성능이나 보안을 강화해 준다.

 

 

쿠버네티스 구조

 

위 다이어그램을 살펴보면

  1. 쿠버네티스 클러스터 : 여러 대의 서버(노드)로 구성된 전체 시스템을 의미한다.
  2. 마스터 노드 : 클러스터를 관리하는 두뇌 역할을 한다. 여기에서는 API 서버, 컨트롤러, 스케줄러 등이 포함되어 있어서, 워커 노드에 어떤 작업을 언제 할당할지 결정한다.
  3. 워커 노드들 : 실제 애플리케이션이 실행되는 서버들이다. 워커 노드에는 하나 이상의 Pod가 실행된다.
  4. Pod : 하나 이상의 컨테이너가 묶인 가장 작은 배포 단위이다. 하나의 Pod 내에 있는 컨테이너들은 같은 IP를 공유하며 함께 동작한다.

 

결론적으로 쿠버네티스는 컨테이너화된 애플리케이션이 제대로 실행되고, 필요한 경우 자동으로 확장 및 축소되도록 도와주는 도구이다.

 

'네트워크' 카테고리의 다른 글

서버-클라이언트 간 데이터 전송 성능 극대화를 위한 프로토콜 분석 (TCP vs UDP vs QUIC)  (0) 2025.06.02
기본 인증  (0) 2024.08.15
클라이언트 식별과 쿠키  (0) 2024.08.09
캐시  (1) 2024.08.07
웹 서버  (1) 2024.08.06

+ Recent posts