풀이 과정을 대충 설명하자면,

시간을 int 로 변환하여 저장하였고,

ArrayList에 넣어서,,,

방의 갯수를 관리하였다.

하지만 더 똑똑한 방법이 있는데

그건 다음주에..

 

import java.util.*;

class Solution {
    static class Time {
        int startTime, endTime;
        
        Time(int startTime, int endTime) {
            this.startTime = startTime;
            this.endTime = endTime;
        }
    }
    public int solution(String[][] book_time) {
        int answer = 0;
        
        PriorityQueue<Time> pq = new PriorityQueue<>((a, b) -> (a.startTime - b.startTime));
        
        for(int i=0;i<book_time.length;i++) {
            String[] in = book_time[i][0].split(":");            
            int startTime = Integer.parseInt(in[0])*100 + Integer.parseInt(in[1]);
            
            in = book_time[i][1].split(":");
            int endTime = Integer.parseInt(in[0])*100 + Integer.parseInt(in[1])+10;
            
            if(endTime % 100 >= 60) {
                endTime += 40;
            }
            pq.offer(new Time(startTime, endTime));
        }
        
        List<Integer> list = new ArrayList<>();
        
        w: while(!pq.isEmpty()) {
            Time t = pq.poll();
            
            for(int i=0;i<list.size();i++) {
                if(list.get(i)<= t.startTime) {

                    list.remove(i);
                    list.add(t.endTime);
                    continue w;
                }
            }
            
            list.add(t.endTime);

        }
        
        return list.size();
    }
}

// 14:00 ~ 16:00
// 14:00 ~ 15:50

 

'Algorithm' 카테고리의 다른 글

[boj] 로봇 조종하기_2169 java  (0) 2025.03.19
[boj] 은하철도_17250 java  (1) 2025.02.24
[boj] ACM Craft_1005 java  (1) 2025.01.26
[boj] 호반우 상인의 이상한 품질 계산법_20117 cpp  (1) 2024.12.29
Boj_ 회의실 배정 4 java  (1) 2024.11.11

 

문제를 보자마자 dp인 건 알았지만

dp 초보인 나는 제대로 된 점화식을 작성할 수 없었습니다.

 

import java.io.*;
import java.util.*;

class Main {

	static int[] dx = {0, -1, 0}, dy = {1, 0, -1}; // bfs를 위한 델타 배열 생성(위로는 못 올라감)
	static int[][] mm, arr;
	static int N, M;

	public static void main(String[] args) throws Exception {
		BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(bf.readLine());
		N = Integer.parseInt(st.nextToken());
		M = Integer.parseInt(st.nextToken());

		arr = new int[N][M]; // 유적 금화를 저장하는 배열
		mm = new int[N][M]; // 메모이제이션(위치, 방향 저장)

		// 유적 내 금화 저장
		for(int i=0;i<N;i++) {
			st = new StringTokenizer(bf.readLine());
			for(int j=0;j<M;j++) {
				arr[i][j] = Integer.parseInt(st.nextToken());
			}
		}

		boolean[][] visited = new boolean[N][M];
		
		visited[N-1][M-1] = true;
		mm[0][0] = arr[0][0];
		System.out.println((1000000000+dp(N-1, M-1, visited)));


	}

	public static int dp(int x, int y, boolean[][] visited) {
		// if(mm[x][y] != 0) return mm[x][y];

		int returnVal = arr[x][y];
		int maxRoute = 0;

		// 현 위치 기준 좌, 우, 상 에서 내려오는 경우 중 가장 큰 값 체크
		for(int i=0;i<3;i++) {
			int nX = x+dx[i];
			int nY = y+dy[i];
			if(isInRange(nX, nY) && !visited[nX][nY]) {
				visited[nX][nY] = true;
				maxRoute = Math.max(maxRoute, dp(nX, nY, visited));
				visited[nX][nY] = false;
			}
		}

		// if(x == N-1 && y == M-1) {
		// 	for(int i=0;i<N;i++) {
		// 		for(int j=0;j<M;j++) {
		// 			System.out.print(visited[i][j]+" ");
		// 		}
		// 		System.out.println();
		// 	}
		// }
		
		return mm[x][y] = returnVal + maxRoute;
	}

	public static boolean isInRange(int x, int y) {
		return 0 <= x && x < N && 0 <= y && y < M;
	}

}

 

일단 저는 메모이제이션을 통해 탑다운 방식으로 풀어보려 했어요.

(N-1, M-1) 에서의 최댓값을 구하려면, (N-2, M-1) 과 (N-1, M-2)에서의 최댓값 중 더 큰 것을 고르면 된다고 생각했습니다.

 

하지만 여기서 논리적인 오류가 발생하는데..

 

  • (0, 0)에서 시작한다.
  • (N-1, M-1)에서의 최댓값을 가지는 경로가 (a, b)를 지난다고 가정했을 때, (a, b)에서의 최댓값을 가지진 않을 수 있다.

이 두 가지를 고려하지 않았습니다...

 

예를 들어봅시다.

3 5

1 1 1 3 4

0 0 5 2 -1

0 0 1 1 1

의 input이 있다고 가정합니다.

 

제 코드로 돌려본다면, 12라는 output이 나오게 됩니다.

하지만 정답은 19 입니다.

 

그냥 설계부터 잘못된 코드입니다!

 

아무튼 그래서 다른 방법을 찾게되었고,

  • 가장 첫 행에서는 왼쪽에서 오른쪽으로밖에 이동할 수 없다.

라는 점을 깨달았습니다.

 

        // 가장 첫 행 계산하기 -> 우측으로밖에 가지 못함
        mm[0][0] = arr[0][0];
        for(int i=1;i<M;i++) {
            mm[0][i] = mm[0][i-1] + arr[0][i];
        }

 

그래서 mm 배열 중 가장 첫 행은, 직전 값에 해당 칸의 value를 더한 값으로 업데이트 해줬습니다.

 

다음 행부터는 가짓수가 증가합니다.

  • 왼쪽에서 오른쪽으로 이동하거나
  • 오른쪽에서 왼쪽으로 이동하거나
  • 위에서 아래로 이동한다.

이 세 가지입니다.

 

위에서 아래로 순차적으로 내려올 수 있기 때문에, 

두 번째 행부터 차근차근 계산을 해줄겁니다.

 

각 행마다 fromLeft (왼쪽에서 오른쪽), fromRight (오른쪽에서 왼쪽) 배열을 만들어줍니다.

 

fromLeft의 가장 왼쪽 값과, fromRight에서 가장 오른쪽 값은, 위에서밖에 내려와 업데이트 되지 못합니다.

            // 왼쪽의 시작과 오른쪽의 끝은, 위에서밖에 내려오지 못함.
            fromLeft[0] = mm[i-1][0] + arr[i][0];
            fromRight[M-1] = mm[i-1][M-1] + arr[i][M-1];

 

 

 

fromLeft 에서 (a, b) 좌표의 최댓값은, 이전에 저장해놓은 (a-1, b) 값과 (a, b-1) 중 큰 값에서 오게 하면 됩니다.

이것을 왼쪽에서 오른쪽으로 가면서 계속 갱신합니다.

for(int j=1;j<M;j++) fromLeft[j] = Math.max(fromLeft[j-1], mm[i-1][j]) + arr[i][j];

 

 

fromRight 에서 (a, b) 좌표의 최댓값은, 이전에 저장해놓은 (a-1, b) 값과 (a, b+1) 중 큰 값에서 오게하면 됩니다.

이것을 오른쪽에서 왼쪽으로 가면서 계속 갱신합니다.

for(int j=1;j<M;j++) fromRight[M-1-j] = Math.max(fromRight[M-j], mm[i-1][M-1-j]) + arr[i][M-1-j];

 

 

모두 완료가 됐다면,

같은 위치의 fromLeft와 fromRight를 비교하며, 더 큰 값으로 mm 배열에 저장합니다.

            // mm에 값 넣어주기
            for(int j=0;j<M;j++) {
                mm[i][j] = Math.max(fromLeft[j], fromRight[j]);
            }

 

 

전체 코드 보기

package baekjoon;

import java.io.*;
import java.util.*;

public class 로봇조종하기_2169 {

	static int[] dx = {0, -1, 0}, dy = {1, 0, -1}; // bfs를 위한 델타 배열 생성(위로는 못 올라감)
	static int[][] mm, arr;
	static int N, M;

	public static void main(String[] args) throws Exception {
		BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
		StringTokenizer st = new StringTokenizer(bf.readLine());
		N = Integer.parseInt(st.nextToken());
		M = Integer.parseInt(st.nextToken());

		arr = new int[N][M]; // 유적 금화를 저장하는 배열
		mm = new int[N][M]; // 메모이제이션(위치, 방향 저장)

		// 유적 내 금화 저장
		for(int i=0;i<N;i++) {
			st = new StringTokenizer(bf.readLine());
			for(int j=0;j<M;j++) {
				arr[i][j] = Integer.parseInt(st.nextToken());
			}
		}

        // 가장 첫 행 계산하기 -> 우측으로밖에 가지 못함
        mm[0][0] = arr[0][0];
        for(int i=1;i<M;i++) {
            mm[0][i] = mm[0][i-1] + arr[0][i];
        }

        // 다음 행부터 위쪽에서 내려오거나, 왼쪽에서 오거나, 오른쪽에서 오거나..
        for(int i=1;i<N;i++) {
            int[] fromLeft = new int[M];
            int[] fromRight = new int[M];

            // 왼쪽의 시작과 오른쪽의 끝은, 위에서밖에 내려오지 못함.
            fromLeft[0] = mm[i-1][0] + arr[i][0];
            fromRight[M-1] = mm[i-1][M-1] + arr[i][M-1];

            for(int j=1;j<M;j++) {
                fromLeft[j] = Math.max(fromLeft[j-1], mm[i-1][j]) + arr[i][j];
                fromRight[M-1-j] = Math.max(fromRight[M-j], mm[i-1][M-1-j]) + arr[i][M-1-j];
            }

            // mm에 값 넣어주기
            for(int j=0;j<M;j++) {
                mm[i][j] = Math.max(fromLeft[j], fromRight[j]);
            }
        }

        System.out.println(mm[N-1][M-1]);
	}

}

250219
250226
250304

 

약 3주동안 항상 해왔던 소리

엘라스틱 서치와 친해지고 오겠다. . .

를 드디어 실행하는,,

그래도 나름 백엔드 지망생으로서

코드를 쳐보고 싶었으니깐요

기대된다 ! ! ! ! 

 


요구사항

기존 Rest API를 사용하였던 각종 검색 기능을 Elastic Search로 변환하기.

 

예상 적용 범위

  이름 URI MEMO
1 회원 목록 조회 API /api/v1/members/search?keyword={keyword}
  • ‘나의 앨범’ 태그 검색 or 앨범 생성시 사용자 검색 시에 사용
  • 닉네임 검색시, 포함하는 닉네임 목록 반환
2 앨범 검색 API /api/v1/albums/search?keyword={keyword}
  • 사용자가 포함된 앨범을 키워드 검색하여, 최신순 조회
  • 앨범명, 포함된 사용자 검색
3 태그 내 앨범 검색 API /api/v1/albums/tag?keyword={keyword}
  • 태그에서 검색 시 앨범 조회 기능입니다.
  • keyword로 검색이 가능합니다.

 

  • 추가로 담당자와 논의 필요
  • 아마 3번만 바꾸면 될 것 같음

 

예상 Flow

  • Spring Data Elasticsearch 의존성을 주입
  • @ElasticsearchDocument class 생성
    • 앨범 field값: id, 앨범 명, 구성 인원
    • 멤버 field값: id, 이름
  • EntityListeners를 통한 MySQL 업데이트 - 엘라스틱 서치 동기화

 

고려 사항

ElasticsearchDocument 내 저장할 field 값들

후 유지보수를 위해, 최소한의 구별 값들만 저장 후 일반 서비스단에서 returnDto로 변환하는 과정 적용

 

MySQL과 Elasticsearch 동기화 작업: 이중 작성, 이벤트 리스너, 메시지 큐

  • 이중 작성
    • MySQL 및 Elasticsearch 내 crud 작업 코드 추가
    • 일관성 문제 발생 가능
    • 불편함
  • 이벤트 리스너
    • 비교적 간단하게 구현 가능
    • 비동기 처리
  • 메시지 큐(RabbitMQ, Kafka)
    • 어떻게 보면 결합도를 낮추고 응집도를 높이는,, 가장 이상적인,,
    • 후에 플젝 확장성을 본다면 가장 이상적인 선택
    • 서버 남은 공간이 많지 않음
    • 대공사(살짝 과할수도)
  • 나머지(배치, CDC)
    • 프로젝트나 팀에 상황과 맞지 않음

그래서 JPA 이벤트 리스너를 사용하고자 합니다..

Introduction to SQL


1. Overview of the SQL Query Language


  • 역사와 발전
    • IBM의 System R 프로젝트에서 Sequel로 시작
    • ANSI/ISO 표준: SQL-86, SQL-89, SQL-92, SQL:1999, SQL:2003, SQL:2006, SQL:2008 등

  • SQL의 구성 요소
    • `DDL: 테이블, 스키마, 무결성 제약, 인덱스, 보안 및 물리적 저장구조 정의
    • DML: 데이터 검색, 삽입, 삭제, 갱신
    • 기타: 트랜잭션 제어, 뷰 정의, 임베디드/동적 SQL, 권한 부여

2. SQL Data Definition


  • 테이블 생성 및 스키마 정의

    • CREATE TABLE 명령으로 테이블 생성
    • 각 속성은 자료형과 선택적 제약조건을 가짐
    • 예시:
      create table department (
        dept_name varchar(20),
        building varchar(15),
        budget numeric(12,2),
        primary key (dept_name)
      );
  • 기본 자료형

    • 문자열: char(n) (고정 길이), varchar(n) (가변 길이), nvarchar (유니코드 지원)
    • 숫자형: int, smallint, numeric(p,d), real, double precision, float(n)
    • NULL 값: 존재하지만 값이 없음을 의미하며, 각 자료형은 NULL을 가질 수 있음
  • 무결성 제약

    • Primary Key: 고유하며 NULL 불가
    • Foreign Key: 다른 테이블의 기본키와 연계하여 참조 무결성 유지
    • Not Null: 속성에 NULL 값이 들어가지 않도록 강제

3. Basic Structure of SQL Queries


단일 테이블


  • 모든 부서의 이름을 출력한다면
    select dept_name
    from instructor;
  • 중복 제거는 DISTINCT 사용

    select distinct dept_name
    from instructor;
    

다중 테이블


  • SELECT : 출력할 속성 또는 표현식 지정
  • FROM : 검색할 테이블(들)을 지정하며, 기본적으로 Cartesian product(데카르트 곱)를 생성
  • WHERE : 튜플을 선택하기 위한 조건을 지정

  • 여러 테이블의 데카르트 곱을 생성하므로, 반드시 WHERE 조건으로 원하는 튜플만 선택해야 함
    select name, course_id
    from instructor, teaches
    where instructor.ID = teaches.ID;
    

<br>

### Natural Join

<br>

- 두 테이블 간 동일한 이름의 속성에 대해 자동으로 조인
- 아래 두 SQL문은 같은 결과를 출력한다

```sql
select name, course_id
from instructor natural join teaches;
select name, course_id
from instructor, teaches
where instructor.ID = teaches.ID;

Join ... USING


  • 특정 속성만으로 조인하도록 지정
  • 아래 두 SQL문은 같은 결과를 출력한다
select name, title
from (instructor natural join teaches) join course using (course_id);
select name, title
from instructor natural join teaches, course
where teaches.course_id = course.course_id;

4. Additional Basic Operations


Rename 또는 별칭 사용


  • 속성 및 테이블 별칭: AS 를 사용하여 이름을 재정의
select T.name as instructor_name, S.course_id
from instructor as T, teaches as S
where T.ID = S.ID;

  • SELF JOIN: 같은 테이블을 두 번 사용 시 별칭 필수
select distinct T.name
from instructor as T, instructor as S
where T.salary > S.salary and S.dept_name = 'Biology';

문자열 연산 및 패턴 매칭


  • 문자열 표기: '로 감싸서 표현, 내부의 작은 따옴표라면 두 번 사용('')
  • 문자열 함수: upper(), lower(), trim()
  • 패턴 매칭: LIKE 연산자와 % , _
select dept_name
from department
where building like '%Watson%';

  • 이스케이프 문자: 특수문자는 일반 문자로 인식하기 위해 escape 키워드 사용

작성중입니다 .. . 쓸 게 굉장히 많네요 미친건가 .. . . .

import java.io.*;
import java.util.*;

public class Main {
    static int arr[], parent[];
    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        StringTokenizer st = new StringTokenizer(bf.readLine());
        int N = Integer.parseInt(st.nextToken()), M = Integer.parseInt(st.nextToken());
        arr = new int[N]; parent = new int[N];
        for(int i=0;i<N;i++) {
            arr[i] = Integer.parseInt(bf.readLine());
            parent[i] = i;
        }
        for(int i=0;i<M;i++) {
            st = new StringTokenizer(bf.readLine());
            int a = Integer.parseInt(st.nextToken())-1, b = Integer.parseInt(st.nextToken())-1;
            int fa = find(a), fb = find(b);
            
            if(fa==fb) {
                bw.write(Integer.toString(arr[fa])+"\n");
            } else if(fa < fb) {
                parent[fb] = fa;
                arr[fa] += arr[fb];
                arr[fb] = 0;
                bw.write(Integer.toString(arr[fa])+"\n");
            } else {
                parent[fa] = fb;
                arr[fb] += arr[fa];
                arr[fa] = 0;
                bw.write(Integer.toString(arr[fb])+"\n");
            }
        }
        bw.flush();
    }
    static int find(int a) {
        if(a == parent[a]) {
            return a;
        }
        return parent[a] = find(parent[a]);
    }
}

https://photorize.co.kr

 

Photorize

 

photorize.co.kr

 

현재 뽀또라이즈를 Gitlab -> Github, Mattermost -> Discord로 옮기는 작업을 하고 있어요.

원래 프로젝트CI/CD를 Jenkins에서 관리했단 말이죠

이번에 Github로 옮기면서 Github Action을 사용해봤어요~~

 

디스코드 연동때문에 여러 번 시행착오가 있었고

 

이런 추태를,,,

앞으로는 브랜치 파서 고치고 squash merge 해야겠다고 다짐한 초보 개발자였습니다. . .

 

이젠 잘 되는 모습!

 

그리고 서버 부분도 손봤는데요

역시나 클라이언트 부분보단 까다로웠습니다~~

 

이게 github action 짓거리를 브랜치 하나 파서 몰작(몰래 작업하다) 하려 했단 말이에요

근데 default 브랜치에서만 되더라고요?

 

그래서 추태를 또 부렸음

 

하... 새벽의 트러블슈팅 내용

이제 firebase를 사용하기때문에 해당 .json 파일을 따로 관리한단말임.

그래서 파이프라인 돌아가는 과정에서 넣어줘야되는데

당연히 secrets에 넣어서 파이프라인에서

 

 
시전했단 말이죠

근데 안되는거!!!!!!!!!!!!!!!!!!! 외않돼는데

매우매우 열받띠예라

json 파일을 출력..ㅋㅋ해봤음(님들은 지양하세요)

 

그랬더니 글쎄

 

이 멍청한 자식이 내 json파일을 yml로 인식하는거!!!!!!!!
그러니깐 오류가뜨지!!!!!!!!!!!!아오

 

근데 멍청한 지피티녀석은 이걸 몰랐던거임

이제 또 자기반성하는데

 

 
구글에 이렇게 치기만 하면 바로 나오는 문제를

GPT한테만 물어봐서,,, 이걸 몇 십 분동안 찾고있었다.

아니 심지어 github에서 secrets에 json 저장하는거 지양하라고 써있는데

이걸 왜 gpt는 안알려준건지

내 프롬프트 능력이 부족한건지.. ..

 

암튼

그래서

구글 내 다른 선생님들의 해결책으로는

json 그대로 적용해주는 라이브러리를 사용해라 << 였고,,

 

https://github.com/marketplace/actions/create-json

 

 

create-json - GitHub Marketplace

Create an JSON file from secret or a string of a json

github.com

해당 라이브러리를 만든 사람의 깃허브에 아주~~~~ 잘 설명이 돼있어요.

 

하니깐 바로 됐음핑

에휴! 내 시간!

 

📡 ICMP: 인터넷 제어 메시지 프로토콜 (Internet Control Message Protocol)

 

인터넷에서 호스트와 라우터가 서로 네트워크 계층 정보를 교환할 때 ICMP(Internet Control Message Protocol) 가 사용됩니다.
ICMP는 네트워크 오류 보고, 진단 및 제어 메시지를 전송하는 역할을 하며, RFC 792에 정의되어 있습니다.

이번 포스팅에서는 ICMP의 개념, 동작 원리, 주요 메시지 유형 및 Traceroute와의 관계에 대해 자세히 알아보겠습니다.

 


 

🧐 ICMP란?

ICMP(Internet Control Message Protocol) 는 네트워크 계층에서 네트워크 문제를 감지하고 보고하는 프로토콜입니다.
일반적으로 라우터와 호스트가 네트워크 오류를 알리거나, 진단 메시지를 주고받을 때 사용됩니다.

 

ex) 웹사이트에 접속할 때의 "Destination network unreachable" 오류

→ 네트워크에서 목적지를 찾을 수 없을 때, 라우터가 ICMP 메시지를 생성하여 오류를 보고

 

🛠 ICMP의 주요 특징

  • IP 프로토콜과 밀접한 관계
    • ICMP 메시지는 IP 패킷 내에서 전송되며, IP 프로토콜 상위 계층에서 동작합니다.
    • TCP/UDP와 마찬가지로 IP 페이로드로 전송됩니다.
  • 비연결형 프로토콜
    • ICMP는 상태를 유지하지 않는 비연결형 프로토콜(connectionless protocol) 입니다.
  • 오류 감지 및 보고
    • 네트워크 오류가 발생하면, 해당 오류를 원래 송신자에게 알리는 역할을 합니다.

 

📜 ICMP 메시지 구조 및 유형

ICMP 메시지는 특정 타입과 코드로 분류됩니다.
각 메시지는 IP 헤더와 문제를 일으킨 원본 IP 패킷의 처음 8바이트를 포함합니다.
이 정보를 바탕으로 송신자는 오류 원인을 분석할 수 있습니다.

 

🔹 주요 ICMP 메시지 유형

메시지 유형(Type) 코드(Code) 설명
0 0 Echo Reply (ping 응답)
3 다양한 코드 Destination Unreachable (목적지 도달 불가)
5 0~1 Redirect (경로 변경 요청)
8 0 Echo Request (ping 요청)
11 0~1 Time Exceeded (TTL 초과)

 

📡 Ping 명령어와 ICMP Echo 메시지

 

📌 Ping이란?

ping 명령어는 네트워크 연결 상태를 테스트하는 데 사용됩니다.
ping을 실행하면 ICMP Echo Request(Type 8, Code 0) 메시지를 전송하고, 상대 호스트가 응답하면 ICMP Echo Reply(Type 0, Code 0) 메시지를 반환합니다.

 

🏓 Ping 동작 방식

  1. 송신자는 대상 IP 주소로 ICMP Echo Request(Type 8, Code 0) 메시지를 전송
  2. 대상 호스트는 수신 후 ICMP Echo Reply(Type 0, Code 0) 메시지를 응답
  3. RTT(Round Trip Time) 측정하여 네트워크 상태 분석 가능

 

🔍 Traceroute와 ICMP Time Exceeded 메시지

 

📌 Traceroute란?

Traceroute는 네트워크 경로를 추적하는 도구입니다.
네트워크 패킷이 목적지까지 가는 모든 라우터를 확인하고, 각 구간의 지연 시간(RTT)을 측정하는 역할을 합니다.

 

🛠 Traceroute의 동작 원리

  1. Traceroute는 UDP 패킷을 전송하지만, TTL(Time To Live) 값을 1부터 증가시키며 전송합니다.
  2. 각 라우터는 TTL이 0이 되면, ICMP Time Exceeded(Type 11, Code 0) 메시지를 반환합니다.
  3. 이를 통해 Traceroute는 각 경로의 라우터 IP 주소와 응답 시간을 확인합니다.
  4. 마지막 패킷이 목적지에 도착하면, ICMP Destination Unreachable(Type 3, Code 3) 메시지를 반환하여 탐색을 종료합니다.


 

🚨 ICMP와 혼잡 제어: Source Quench 메시지

과거에는 혼잡 제어를 위해 ICMP Source Quench 메시지(더 이상 사용되지 않음)를 활용했습니다.

  • 혼잡 발생 시, 라우터가 송신자에게 ICMP Source Quench 메시지를 보내 전송 속도를 줄이도록 유도
  • 하지만, TCP 자체적인 혼잡 제어 메커니즘(예: 슬로우 스타트, AIMD)이 발전하면서 ICMP Source Quench 메시지는 더 이상 사용되지 않음

 

🌍 ICMPv6: IPv6에서의 확장

IPv6에서는 기존 ICMP 메시지를 정리하고, 새로운 기능을 추가한 ICMPv6 (RFC 4443) 가 정의되었습니다.

 

🆕 ICMPv6에서 추가된 주요 기능

  • Packet Too Big: IPv6는 패킷 분할(fragmentation)을 지원하지 않기 때문에, 패킷 크기가 너무 클 경우 "Packet Too Big" 메시지를 전송
  • Neighbor Discovery: ARP(Address Resolution Protocol)를 대체하여 IPv6 주소를 MAC 주소로 변환
  • Router Solicitation & Advertisement: 동적 라우터 설정을 지원

 

TCP 혼잡 제어(TCP Congestion Control)

인터넷 환경에서 데이터 전송 시 네트워크의 혼잡을 효과적으로 관리하는 것은 매우 중요합니다. 특히 여러 장치가 동시에 데이터를 주고받는 상황에서는 네트워크 혼잡이 발생하기 쉽습니다. 이때 TCP 혼잡 제어(TCP Congestion Control) 메커니즘이 중요한 역할을 합니다.

이번 포스팅에서는 TCP 혼잡 제어의 개념, 동작 원리, 주요 알고리즘에 대해 자세히 알아보겠습니다.


📦 TCP 혼잡 제어란?

TCP 혼잡 제어(Congestion Control) 는 네트워크의 혼잡 상태를 감지하고, 이에 따라 데이터 전송 속도를 조절하여 네트워크의 안정성을 유지하는 메커니즘입니다.

✅ TCP 혼잡 제어의 필요성

  • 네트워크 과부하 방지: 과도한 트래픽으로 인한 패킷 손실 방지
  • 효율적인 대역폭 사용: 가용한 대역폭을 최대한 활용하면서도 안정적인 전송 유지
  • 신뢰성 보장: 데이터가 안정적으로 전송되도록 손실과 재전송을 최소화

⚙️ TCP 혼잡 제어의 핵심 요소

TCP는 네트워크 혼잡을 감지하고 전송 속도를 조절하기 위해 다음과 같은 요소를 활용합니다.

1️⃣ 혼잡 윈도우 (Congestion Window, cwnd)

  • 송신자가 네트워크로 보낼 수 있는 최대 데이터 양을 결정하는 변수입니다.
  • cwnd 값이 커지면 전송 속도가 증가하고, 작아지면 전송 속도가 감소합니다.

2️⃣ 혼잡 신호 감지 방법

  • 패킷 손실: 타임아웃 발생 또는 3중 중복 ACK(Duplicate ACK) 수신 시 혼잡 발생으로 간주
  • ACK 수신 속도: ACK가 느리게 도착하면 혼잡 가능성으로 해석

3️⃣ 혼잡 제어 단계

TCP 혼잡 제어는 Slow Start → Congestion Avoidance → Fast Recovery 단계를 반복하며 혼잡 상태에 대응합니다.


🚀 TCP 혼잡 제어 알고리즘

TCP 혼잡 제어는 다음과 같은 3가지 주요 알고리즘으로 구성됩니다.

📈 1. 슬로우 스타트 (Slow Start)

  • 초기 연결 시 또는 혼잡 발생 후 전송 속도를 빠르게 증가시키는 단계입니다.
  • cwnd1 MSS(Maximum Segment Size)로 시작하고, ACK를 받을 때마다 1 MSS씩 증가합니다.
  • 매 RTT마다 전송 속도가 2배로 증가지수적 성장 (Exponential Growth)

동작 예시

  1. cwnd = 1 MSS로 시작 → 첫 번째 패킷 전송
  2. ACK 수신 → cwnd = 2 MSS로 증가
  3. 2개의 패킷 전송 → 각각 ACK 수신 시 cwnd = 4 MSS로 증가
  4. 이러한 과정이 계속되어 빠르게 전송 속도가 증가합니다.

슬로우 스타트 종료 조건:

  • cwndssthresh(slow start threshold) 에 도달하면 혼잡 회피(Congestion Avoidance)로 전환
  • 패킷 손실이 발생하면 다시 cwnd = 1 MSS로 초기화

📉 2. 혼잡 회피 (Congestion Avoidance)

  • 슬로우 스타트 단계 이후 안정적인 전송 속도 유지를 목표로 합니다.
  • cwnd선형적으로 증가(Additive Increase) 합니다:
    • 매 RTT마다 cwnd1 MSS씩 증가시켜 대역폭을 탐색
  • 혼잡이 감지되면 cwnd절반으로 감소(Multiplicative Decrease) 합니다.

동작 예시

  1. cwnd = 16 MSS일 때 패킷 손실 발생 → cwnd = 8 MSS로 감소
  2. 이후 매 RTT마다 cwnd를 1 MSS씩 증가시켜 혼잡 여부를 탐색
  3. 다시 혼잡 발생 시 cwnd를 절반으로 감소

이 방식은 AIMD(Additive Increase, Multiplicative Decrease) 원칙을 기반으로 합니다.


⚡ 3. 빠른 회복 (Fast Recovery)

  • 3중 중복 ACK(Triple Duplicate ACK) 를 수신하면, 패킷 손실을 감지하고 빠르게 복구하는 단계입니다.
  • 타임아웃 없이 즉시 손실된 패킷을 재전송하여 성능 저하를 최소화합니다.

동작 방식

  1. 패킷 손실 감지(3중 중복 ACK 수신) → cwnd절반으로 감소
  2. 손실된 패킷을 빠르게 재전송(Fast Retransmit)
  3. 새로운 ACK가 도착하면 혼잡 회피(Congestion Avoidance) 단계로 전환

🔄 TCP 혼잡 제어의 전체 흐름

1️⃣ 연결 초기화
      ↓
2️⃣ Slow Start (지수적 증가)
      ↓ (cwnd ≥ ssthresh 또는 패킷 손실 발생)
3️⃣ Congestion Avoidance (선형적 증가)
      ↓ (패킷 손실 감지 시)
4️⃣ Fast Recovery (빠른 재전송 및 복구)
      ↓ (복구 완료)
5️⃣ 다시 Congestion Avoidance로 복귀

📊 AIMD: TCP 혼잡 제어의 핵심 원칙

TCP 혼잡 제어는 AIMD (Additive Increase, Multiplicative Decrease) 원칙에 기반합니다.

⚡ AIMD의 구성 요소

  • Additive Increase (선형 증가):
    혼잡이 발생하지 않으면 cwnd를 매 RTT마다 1 MSS씩 선형적으로 증가합니다.
    → 네트워크의 여유 대역폭을 탐색하며 서서히 전송 속도를 높이는 방식입니다.
  • Multiplicative Decrease (배수 감소):
    혼잡이 발생(예: 패킷 손실)하면 cwnd즉시 절반으로 감소하여 전송 속도를 급격히 줄입니다.
    → 네트워크 혼잡을 빠르게 완화하기 위한 대응입니다.

🚀 AIMD의 효과

  • 혼잡 방지:
    혼잡 발생 시 즉각적인 속도 감소로 네트워크 과부하 방지
  • 대역폭 최적화:
    여유 대역폭을 최대한 활용하기 위해 선형적으로 전송 속도 증가
  • 공정성(Fairness):
    여러 TCP 연결이 병목 구간을 공유할 때 공평한 대역폭 분배 보장
  • 안정성(Stability):
    급격한 혼잡 발생에도 빠르게 복구하고, 네트워크 안정성 유지

+ Recent posts