안녕하세요! 오늘은 Docker로 Redis 컨테이너를 실행하고, 로컬 터미널에서 redis-cli로 접속했는데 환경은 분리되어 있는데 컨테이너 속 데이터가 그대로 보이는 경우가있어서 궁금해서 찾아본 결과를 작성하였습니다.  

사건의 발단

새로운 프로젝트를 위해 터미널에 익숙한 명령어를 입력합니다.

docker run --name my-redis -p 6379:6379 -d redis

 

my-redis라는 이름의 컨테이너가 백그라운드에서 잘 실행되고 있습니다. 이제 데이터가 잘 들어가는지 확인하기 위해, 새로운 터미널 창을 열고 redis-cli에 접속해봅니다.

# 새로운 터미널 창
redis-cli

127.0.0.1:6379> keys *
1) "RT:user123"
2) "post:likes:42"
... (애플리케이션이 저장한 키 목록)

 

그런데 이상합니다. 저는 분명 Docker 컨테이너 안에 Redis를 띄웠는데, 어떻게 로컬 PC에서 실행한 redis-cli가 컨테이너 속 데이터를 보고 있는 걸까요? 마치 내 컴퓨터에 직접 Redis를 설치한 것처럼 말이죠.

혹시 내가 모르는 사이에 로컬에도 Redis가 설치되어 있었나? 데이터가 복사라도 된 건가? 이런저런 생각에 잠시 혼란에 빠집니다.

범인은 바로 '포트 포워딩'

결론부터 말하면, 이것은 버그나 실수가 아닌 Docker의 매우 유용한 기능 덕분입니다. 범인은 바로 docker run 명령어에 사용했던 -p 6379:6379 옵션, 즉 **포트 포워딩(Port Forwarding)**입니다.

-p 옵션은 [호스트 포트]:[컨테이너 포트] 형식을 가집니다.

-p 6379:6379
"이 PC(호스트)의 6379번 포트로 들어오는 모든 네트워크 요청을, 저 Docker 컨테이너 내부의 6379번 포트로 전달(포워딩)해주세요."

 

마치 전화기의 '착신 전환' 기능과 같습니다. 내 자리(localhost:6379)로 걸려 온 전화를 지정된 다른 번호(Docker 컨테이너)로 자동으로 연결해주는 것이죠.

우리가 터미널에서 아무 옵션 없이 redis-cli를 실행하면, 이 프로그램은 기본적으로 localhost6379번 포트로 접속을 시도합니다. 이때 Docker가 이 요청을 가로채서 my-redis 컨테이너 안으로 투명하게 전달해주기 때문에, 우리는 마치 로컬에 직접 접속하는 것처럼 컨테이너 내부의 Redis와 통신할 수 있었던 것입니다.

직접 증명해보기: 컨테이너 속으로 직접 들어가보자

"정말 그런지 내 눈으로 확인해야겠어!" 라고 생각하신다면, docker exec 명령어로 컨테이너 안에서 직접 redis-cli를 실행해볼 수 있습니다.

# 실행 중인 'my-redis' 컨테이너 안에서 'redis-cli keys *' 명령어를 실행
docker exec my-redis redis-cli keys '*'

결과는 어떨까요? 놀랍게도(혹은 당연하게도) 로컬에서 실행했을 때와 완벽하게 똑같은 키 목록이 출력됩니다.

이것으로 데이터는 오직 Docker 컨테이너 안에만 존재하며, 로컬에서의 접속은 포트 포워딩이라는 가상의 통로를 통한 '원격 접속'이었음이 증명되었습니다.

 

 

마치며

이 간단한 원리 덕분에 개발자는 큰 축복을 받습니다. Spring Boot 애플리케이션의 application.yml 파일에 spring.redis.host=localhost 라고 설정하기만 하면 되고, IntelliJ 같은 IDE의 데이터베이스 툴에서도 localhost로 손쉽게 접속하여 데이터를 확인할 수 있습니다.

이제 Docker로 Redis를 띄우고 로컬에서 데이터가 보일 때 더 이상 당황하지 마세요. 그것은 여러분의 개발 경험을 한결 편하게 만들어주는 Docker의 똑똑한 마법이니까요

안녕하세요! 오늘은 강력한 오픈소스 부하 테스트 도구인 Apache JMeter를 사용해서 제 프로젝트에 대한 성능 테스트를 진행했던 여정을 처음부터 끝까지 공유해 보려고 합니다.

이 글은 단순히 JMeter의 기능을 나열하는 것이 아니라, 제가 실제로 겪었던 수많은 에러와 그 해결 과정을 모두 담고 있습니다. 

 

JMeter를 처음 접했을 때 가장 막막했던 부분은 단순한 API 호출이 아니었습니다. 제 프로젝트는 로그인을 통해 발급된 JWT(JSON Web Token)를 Authrization 헤더에 포함해야만 다른 API를 이용할 수 있는, 매우 일반적인 구조였습니다. 

오늘의 목표 시나리오

  1. 테스트용 로그인 API를 호출하여 JWT 토큰을 발급받는다.
  2. 발급받은 토큰을 HTTP 헤더에 담아, 인증이 필요한 다른 API(예: 내 정보 조회)를 호출한다.
  3. 이 과정을 여러 명의 가상 사용자동시에 반복하도록 설정하여 서버에 부하를 준다.

 


1. JMeter 실행하는 방법 (macOS 기준)

가장 첫 단계인 JMeter 설치 및 실행부터 시작하겠습니다.

1단계: Java 설치 확인

JMeter는 Java로 만들어졌기 때문에 Java(JDK)가 반드시 설치되어 있어야 합니다. 터미널을 열고 아래 명령어로 설치 여부를 확인하세요.

Generated bash

java -version

만약 설치되어 있지 않다면, Homebrew를 사용해서 간단하게 설치할 수 있습니다.

 

# Homebrew로 OpenJDK 17 설치 (다른 버전도 괜찮습니다)
brew install openjdk@17

2단계: JMeter 다운로드

  1. JMeter 공식 다운로드 페이지에 접속합니다.
  2. Binaries 섹션에서 apache-jmeter-5.x.x.tgz 파일을 다운로드합니다. (zip 파일도 괜찮습니다)
  3. 다운로드한 파일의 압축을 풉니다.

3단계: JMeter 실행

JMeter는 GUI 모드와 CLI 모드가 있지만, 테스트 스크립트를 작성할 때는 GUI 모드를 사용합니다. 터미널을 열고 압축을 푼 JMeter 폴더의 bin 디렉토리로 이동한 뒤, 아래 명령어를 입력하세요.

# /Users/000/Downloads/apache-jmeter-5.6.3 경로에 압축을 풀었다고 가정
cd /Users/000/Downloads/apache-jmeter-5.6.3/bin

# jmeter.sh 스크립트 실행
sh jmeter

이 명령어를 실행하면, 잠시 후 익숙한 JMeter GUI 화면이 나타납니다. 이제 모든 준비가 끝났습니다!


2. JMeter 테스트 계획 완벽 가이드 (Step-by-Step)

Step 1: 가상 사용자 그룹 설정 (Thread Group)

가장 먼저 "몇 명의 사용자가, 어떻게 행동할지"를 정의하는 Thread Group을 추가합니다.

  1. 왼쪽 트리의 Test Plan 우클릭 → AddThreads (Users)Thread Group

[설정 Tip]

  • Number of Threads (users): 가상 사용자 수. 처음엔 1로 두고, 나중에 부하를 줄 때 100 등으로 늘립니다.
  • Loop Count: 각 사용자가 시나리오를 반복할 횟수. 10 정도로 설정합니다.

Step 2: 작업 묶어주기 (Simple Controller)

"로그인 → API 호출"은 하나의 묶음으로 반복되어야 합니다. 그렇지 않으면 토큰 만료 에러가 발생합니다. 이 '묶음' 역할을 하는 Simple Controller를 추가합니다.

  1. Thread Group 우클릭 → AddLogic ControllerSimple Controller

이제부터 만드는 모든 요청은 이 Simple Controller 아래에 추가할 것입니다.

Step 3: 테스트 로그인 요청 만들기

만들어둔 테스트용 로그인 API(POST /api/v1/auth/test/login)를 호출하는 단계입니다.

  1. Simple Controller 우클릭 → AddSamplerHTTP Request
  2. 이름을 테스트 로그인으로 변경하고 아래와 같이 설정합니다.
    • Server Name or IP: localhost
    • Port Number: 8080
    • Method: POST
    • Path: /api/v1/auth/test/login
    • Body Data 탭 클릭 후 내용 입력:Generated json
      {
          "email": "test-for-jmeter@example.com"
      }
  3. [첫 번째 에러 해결] Content-Type 헤더 추가하기
    이대로 실행하면 서버에서 Content-Type 'text/plain' is not supported 에러가 발생합니다. Body 데이터가 JSON임을 알려줘야 합니다.
    • 방금 만든 테스트 로그인 요청 우클릭 → AddConfig ElementHTTP Header Manager
    • 생성된 헤더 매니저에서 Add 버튼을 누르고 아래와 같이 입력합니다.
      • Name: Content-Type
      • Value: application/json
    [두 번째 에러 해결] 이때 헤더 매니저에 이름과 값이 비어있는 빈 줄이 추가되면 IllegalArgumentException: The HTTP header line [: ] does not conform to RFC 7230 에러가 발생합니다. 반드시 빈 줄은 Delete 버튼으로 삭제해주세요.

Step 4: 토큰 추출하기 (JSON Extractor)

로그인 성공 후 응답으로 온 JSON 데이터에서 액세스 토큰을 추출하여 변수에 저장합니다.

  1. 테스트 로그인 요청 우클릭 → AddPost ProcessorsJSON Extractor
  2. 아래와 같이 설정합니다.
    • Names of created variables: ACCESS_TOKEN (우리가 사용할 변수 이름)
    • JSONPath expressions: $.accessToken (응답 JSON의 키 경로)
    • Default Values: TOKEN_NOT_FOUND (디버깅용)

Step 5: 인증 필요한 API 호출하기

이제 추출한 토큰을 사용해서 실제 테스트 대상 API(예: GET /api/v1/users/me)를 호출합니다.

  1. Simple Controller 우클릭 → AddSamplerHTTP Request
  2. 이름을 내 정보 조회로 변경하고 아래와 같이 설정합니다.
    • Server Name or IP: localhost
    • Port Number: 8080
    • Method: GET
    • Path: /api/v1/users/me (실제 API 경로)
  3. [가장 중요한 설정] Authorization 헤더 추가하기
    이 요청에만 Authorization 헤더를 붙여줘야 합니다.
    • 방금 만든 내 정보 조회 요청 우클릭 → AddConfig ElementHTTP Header Manager
    • 생성된 헤더 매니저에서 Add 버튼을 누르고 아래와 같이 입력합니다.
      • Name: Authorization
      • Value: Bearer ${ACCESS_TOKEN} (Bearer 뒤 공백 필수!)

Step 6: 결과 확인 및 분석 리스너 추가

이제 모든 테스트 결과를 보고 분석할 창을 추가합니다.

  1. Thread Group 우클릭 → AddListenerView Results Tree
    • 디버깅용 리스너: 개별 요청의 성공/실패, 요청/응답 데이터 등 모든 것을 상세히 볼 수 있습니다. 에러가 발생하면 가장 먼저 확인해야 할 곳입니다.
  2. Thread Group 우클릭 → AddListenerSummary Report
    • 성능 분석용 리스너: 평균 응답 시간, 에러율, 처리량(Throughput) 등 전체적인 성능 지표를 표로 보여줍니다.

3. 최종 테스트 계획 구조 및 실행

모든 과정을 마친 테스트 계획의 최종 모습은 아래와 같습니다. 이 구조를 잘 기억해주세요!

Generated code

Test Plan
└─ Thread Group (100명의 사용자, 10회 반복)
   ├─ View Results Tree
   ├─ Summary Report
   └─ Simple Controller
      ├─ 테스트 로그인 (HTTP Request)
      │   ├─ Content-Type (HTTP Header Manager)
      │   └─ JSON Extractor
      └─ 내 정보 조회 (HTTP Request)
          └─ Authorization (HTTP Header Manager)

이제 Thread Group에서 Number of Threads를 원하는 만큼(예: 100) 늘리고, 상단의 초록색 시작(▶️) 버튼을 눌러 본격적인 부하 테스트를 시작하세요!

그리고 Summary Report를 보며 내 서버의 Average(평균 응답 시간)와 Error %(에러율)가 어떻게 변화하는지 지켜보시면 됩니다.

 

 

 

안녕하세요! 자바와 스프링으로 개발하다 보면 JPA는 이제 선택이 아닌 필수처럼 느껴집니다. 그런데 JPA를 처음 다룰 때, 우리는 하나의 의문과 마주하게 됩니다.

 

"왜 엔티티(Entity) 클래스에는 꼭 비어있는 기본 생성자를 만들어야 할까?"

 

다른 생성자를 만들면 컴파일러가 알아서 기본 생성자를 만들어주지 않는데도, 우리는 굳이 protected Member() {} 같은 코드를 추가하곤 합니다. 그냥 "JPA 규칙이니까"라고 넘어가기엔 너무 궁금합니다.

오늘은 이 오래된 궁금증을 해결하기 위해, JPA 내부의 동작 원리를 살짝 엿보는 시간을 갖겠습니다.

시작: JPA의 고민 "이 데이터를 어디에 담아야 하지?"

우리가 memberRepository.findById(1L) 와 같은 코드를 실행하면, JPA는 우리를 대신해 데이터베이스에 다녀옵니다.

  1. 개발자: "JPA야, ID가 1번인 회원 정보 좀 찾아줘."
  2. JPA: "알겠습니다! (DB에 SELECT * FROM member WHERE id = 1 쿼리 실행)"
  3. DB: "여기 있습니다. id=1, name='홍길동', age=25"
  4. JPA: "좋아, 데이터를 가져왔다. 이제 이 데이터를 개발자에게 돌려줘야 하는데... 어? 이 id, name, age어디에 담아서 주지?"

바로 이 순간, JPA는 DB에서 가져온 데이터를 담을 '그릇', 즉 Member 클래스의 **객체(인스턴스)**를 새로 만들어야 할 필요성을 느낍니다.

JPA의 선택: 가장 간단하고 확실한 약속, "기본 생성자"

자, Member 객체를 만들어야 합니다. 그런데 Member 클래스에 다음과 같이 다양한 생성자가 있다면 어떨까요?

 

@Entity
public class Member {

    @Id
    private Long id;
    private String name;
    private int age;
    private String email;

    // 생성자 1
    public Member(String name, int age) {
        // ...
    }

    // 생성자 2
    public Member(String name, String email) {
        // ...
    }
    // ... 또 다른 생성자가 있을지도?
}

JPA는 어떤 생성자를 호출해야 할까요? nameage를 받는 생성자? 아니면 nameemail을 받는 생성자? JPA는 개발자의 의도를 전혀 알 수 없습니다.

이런 혼란을 피하기 위해 JPA 명세(Specification)는 아주 간단하고 명료한 규칙을 만들었습니다.

 

"엔티티 객체를 생성할 때는, 어떤 상황에서도 예측 가능하도록 '인자가 없는 기본 생성자'를 사용하기로 약속한다!"

 

이것이 바로 JPA와 개발자 사이의 사회적 계약입니다.

마법의 기술 1: 애너테이션(Annotation)

JPA는 이 약속을 어떻게 실행할까요? 먼저, 어떤 클래스가 DB와 연결되는 특별한 클래스(엔티티)인지 알아야 합니다. 이때 사용되는 것이 바로 애너테이션(Annotation) 입니다.

@Entity // "JPA야, 이 Member 클래스는 너가 관리해야 할 특별한 엔티티야!"
public class Member {
    
    @Id // "이 필드는 테이블의 기본 키(PK)야."
    private Long id;

    // ...
}

애너테이션은 코드에 붙이는 '꼬리표' 입니다. JPA는 애플리케이션이 실행될 때 @Entity라는 꼬리표가 붙은 클래스들을 모두 찾아내어 자신의 관리 대상으로 등록합니다.

마법의 기술 2: 리플렉션(Reflection)

자, 이제 JPA는 Member 클래스가 엔티티라는 것을 알게 되었고, DB에서 데이터를 가져왔습니다. 약속대로 기본 생성자를 호출해서 객체를 만들 차례입니다.

이때 사용하는 기술이 바로 리플렉션(Reflection) 입니다. 리플렉션은 프로그램 실행 중에(런타임에) 클래스의 설계도(필드, 메서드, 생성자 정보)를 거울처럼 들여다보고, 심지어 직접 조작까지 할 수 있는 강력한 기술입니다.

1단계: 기본 생성자로 '빈 집' 짓기

JPA는 리플렉션을 사용해 Member 클래스의 기본 생성자를 찾아 호출합니다.

// JPA 내부에서 일어나는 일을 상상해봅시다 (의사 코드)

// 1. Member 클래스의 설계도를 가져온다.
Class<Member> memberClass = Member.class;

// 2. 설계도에서 인자가 없는 생성자를 찾는다.
Constructor<Member> constructor = memberClass.getDeclaredConstructor(); // 기본 생성자 찾기!

// 3. 찾은 생성자를 호출해 '텅 빈' 객체를 만든다.
Member memberInstance = constructor.newInstance();

만약 이때 Member 클래스에 기본 생성자가 없다면? getDeclaredConstructor() 에서 NoSuchMethodException 예외가 발생하며 프로그램은 즉시 중단됩니다. "약속된 기본 생성자가 없어서 일을 진행할 수 없습니다!" 라고 외치는 셈이죠.

이것이 바로 우리가 기본 생성자를 반드시 만들어야 하는 첫 번째 이유입니다.

2단계: '자물쇠'를 열고 값 채워넣기

이제 텅 빈 memberInstance 객체가 만들어졌습니다. 하지만 필드는 모두 private으로 잠겨있죠. JPA는 어떻게 이 private 필드에 DB에서 가져온 값을 채워 넣을까요?

여기서 리플렉션의 두 번째 마법이 등장합니다.

 

// JPA 내부 상상도 2 (의사 코드)

// 1. DB에서 가져온 이름 '홍길동'을 'name' 필드에 넣어야 한다.
Field nameField = memberClass.getDeclaredField("name");

// 2. 'private'이라는 자물쇠를 강제로 연다!
nameField.setAccessible(true); 

// 3. 필드에 직접 값을 '꽂아' 넣는다. (setter를 사용하지 않음!)
nameField.set(memberInstance, "홍길동");

// age, email 등 다른 모든 필드에 대해서도 이 작업을 반복...

setAccessible(true)private 접근 제어자를 런타임에 무시하게 만드는 강력한 기능입니다. 이 덕분에 JPA는 setter 메서드가 없어도 필드에 직접 값을 주입할 수 있습니다.

그렇다면 왜 protected를 쓸까?

기본 생성자를 만들 때 public 대신 protected를 권장하는 이유도 명확해집니다.

  • public: 누구나 new Member()로 의미 없는 빈 객체를 만들 수 있게 허용합니다. 이는 객체의 상태를 불안정하게 만들 수 있습니다.
  • protected: 개발자에게는 "이 생성자는 직접 사용하지 마세요!"라는 메시지를 주면서, JPA(같은 패키지 또는 자식 클래스-프록시)가 접근하는 것은 허용하는 가장 이상적인 타협점입니다.

결론

Q: JPA 엔티티에 기본 생성자는 왜 필수인가요?

A: JPA가 DB에서 데이터를 조회한 후, 그 데이터를 담을 객체를 '런타임'에 동적으로 생성해야 하기 때문입니다. 이때 JPA는 리플렉션 기술을 사용하여 어떤 상황에서든 예측 가능한 '기본 생성자'를 호출하기로 약속(JPA 명세 규칙)했습니다. 따라서 개발자는 이 약속을 지키기 위해 반드시 기본 생성자를 제공해야 합니다.

 

긴 글 읽어주셔서 감사합니다!


그리디 알고리즘이란?

한 문장으로 정리하면 이렇다.

 

"매 순간 최선이라고 생각되는 선택을 하는 것."

 

전체를 보고 최종적인 최적해를 찾는 게 아니라, 지금 이 순간의 선택이 최선이라면 일단 고르고 본다. 예를 들어, 등산할 때 전체 경로를 보는 게 아니라, 지금 눈앞에 보이는 오르막 중 가장 가파른 쪽으로 계속 가는 느낌.

이게 단순해 보여도, 조건만 맞으면 아주 효율적으로 동작한다.


아무 데서나 써도 되는 건 아니다

그리디 알고리즘은 모든 문제에 적용할 수 있는 건 아니다. 다음 두 가지 조건이 반드시 만족돼야 정답을 보장할 수 있다.

  1. 탐욕적 선택 속성
    → 매 순간의 최선의 선택이 결국 전체 문제의 최선의 해로 이어져야 한다.
  2. 최적 부분 구조
    → 문제를 작은 문제로 나눴을 때, 그 부분 문제도 같은 방식으로 해결할 수 있어야 한다.

이 두 조건이 만족되지 않으면, 그리디는 그냥 오답 생성기일 뿐이다.


대표 예제로 이해해보기 (Java 기준)

1. 거스름돈 문제

동전 단위: 500, 100, 50, 10원
목표: 1260원을 최소 개수의 동전으로 거슬러주기

여기선 단순히 큰 단위 동전부터 가능한 만큼 내면 된다. 이유는 동전 단위가 서로 배수 관계이기 때문.

int change = 1260;
int[] coins = {500, 100, 50, 10};
int count = 0;

for (int coin : coins) {
    count += change / coin;
    change %= coin;
}

System.out.println("최소 동전 개수: " + count); // 출력: 6

2. 회의실 배정 문제

여러 회의의 시작/끝 시간이 주어졌을 때, 한 회의실에서 최대한 많은 회의를 열 수 있도록 선택

이 문제에서 핵심은 회의가 빨리 끝나는 순서대로 선택하는 것이다. 빨리 끝나야 다음 회의를 많이 받을 수 있으니까.

int[][] meetings = {
    {1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 8},
    {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 13}, {12, 14}
};

Arrays.sort(meetings, (a, b) -> {
    if (a[1] == b[1]) return a[0] - b[0]; // 종료 시간이 같으면 시작 시간 기준
    return a[1] - b[1]; // 종료 시간 기준 정렬
});

int count = 0;
int end = 0;

for (int[] meeting : meetings) {
    if (meeting[0] >= end) {
        count++;
        end = meeting[1];
    }
}

System.out.println("최대 회의 수: " + count); // 출력: 4

 


그리디 알고리즘의 한계

항상 정답을 보장하진 않는다. 예를 들어, 동전 단위가 {1, 3, 4}일 때, 6원을 만들려고 하면?

  • 그리디: 4 + 1 + 1 → 동전 3개
  • 최적해: 3 + 3 → 동전 2개

이런 식으로, 탐욕적인 선택이 전체 최적해로 이어지지 않는 구조에서는 그리디는 잘못된 해를 줄 수 있다.


정리

  • 그리디 알고리즘은 간단하고 빠르지만, 무조건 믿을 수는 없다.
  • 그리디를 쓰기 전에 "이 선택이 진짜 전체 최적해로 이어질 수 있는가?" 를 먼저 확인해야 한다.
  • 제대로 쓰면 DP보다 빠르게 정답을 낼 수 있지만, 잘못 쓰면 DP보다 위험할 수 있다.

이렇게 정리해보니 그리디 알고리즘은 단순한 만큼 강력하고, 또 조심스러운 도구라는 걸 다시 느낀다.
문제 유형을 많이 접해보면서 어떤 문제에 그리디가 통하는지 감을 익히는 게 제일 중요한 것 같다.

'자료구조(알고리즘) > 개념' 카테고리의 다른 글

Java의 Queue 정리  (0) 2025.07.04
BFS (Breadth-First Search) - 너비 우선 탐색  (0) 2025.05.30

게임 맵 최단거리, BFS로 어떻게 풀었는지 (Java)

문제 상황

프로그래머스에서 "게임 맵 최단거리" 문제를 마주했는데, 조건이 간단했다.

  • (0,0)에서 시작해서 (n-1,m-1)까지 가야 함
  • 맵은 1(길)과 0(벽)로 구성
  • 상하좌우로만 움직일 수 있음
  • 도달할 수 없으면 -1을 반환

문제에서 눈에 띄는 건 단연 **"최단거리"**라는 키워드.
이 단어 하나 때문에 어떤 알고리즘을 써야 할지가 딱 떠오름.

어떻게 풀지? — '최단거리'에 집중

솔직히 '도달만 하면 된다'는 문제라면 DFS든 뭐든 써도 되겠지만,
"최단"이라고 명시된 순간 BFS가 제일 먼저 떠오름.

왜냐하면 BFS는 가까운 곳부터 차례대로 탐색하니까,
처음 도달한 경로가 무조건 최단거리임이 보장됨.
물감 퍼지듯이 번져나가는 그 느낌으로, 거리를 레벨별로 확장해 나가는 거라서.

내가 쓴 핵심 도구 3개

1. Queue<int[]>

BFS니까 큐는 필수.
여기서 int[]에 [x, y, 현재까지 거리]를 같이 넣었음.
거리를 따로 세는 게 편해서 이렇게 구조 짰음.

2. boolean[][] visited

방문 여부 기록용.
한 번 간 칸을 또 가는 거 막으려고 썼고, 중복 탐색도 줄어들어서 성능도 좋아짐.

3. dx, dy 방향 배열

상하좌우로 탐색할 때 코드 길어지는 거 방지하려고
dx = {0, 0, 1, -1}, dy = {1, -1, 0, 0} 식으로 방향 배열을 만들어서
반복문 하나로 방향 처리함.

코드 흐름 요약

전체 코드는 아래에 있지만, 대략 이런 흐름으로 작성했음.

  1. 맵 크기 저장
  2. 큐와 방문 배열 초기화
  3. (0,0)부터 시작해서 큐에 넣음
  4. 큐가 빌 때까지 BFS 반복
  5. 다음 좌표로 갈 수 있는지 체크 → 갈 수 있으면 큐에 추가
  6. 도착 지점에 도달하면 거리 반환
  7. 다 돌았는데 도착 못 했으면 -1

내가 작성한 코드

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

class Solution {
    public int solution(int[][] maps) {
        int n = maps.length;
        int m = maps[0].length;

        boolean[][] visited = new boolean[n][m];
        Queue<int[]> queue = new LinkedList<>();

        visited[0][0] = true;
        queue.add(new int[]{0, 0, 1}); // {x, y, 거리}

        int[] dx = {0, 0, 1, -1};
        int[] dy = {1, -1, 0, 0};

        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int x = current[0];
            int y = current[1];
            int dist = current[2];

            if (x == n - 1 && y == m - 1) {
                return dist;
            }

            for (int i = 0; i < 4; i++) {
                int nx = x + dx[i];
                int ny = y + dy[i];

                if (nx >= 0 && nx < n && ny >= 0 && ny < m &&
                    maps[nx][ny] == 1 && !visited[nx][ny]) {
                    visited[nx][ny] = true;
                    queue.add(new int[]{nx, ny, dist + 1});
                }
            }
        }

        return -1;
    }
}

중간에 헷갈렸던 점

처음에 queue.poll() 해서 좌표만 비교하고 거리를 따로 관리 안 하니까,
계속 잘못된 거리만 나옴
그래서 거리까지 같이 큐에 넣는 방식으로 바꾸니까 바로 해결됨.
그리고 visited 체크 순서를 잘못 넣으면 중복 방문 생기니까,
큐에 넣기 전에 무조건 방문 체크하는 것도 중요.

'자료구조(알고리즘) > 프로그래머스(java)' 카테고리의 다른 글

올바른 괄호 문제  (2) 2025.07.05
같은 숫자는 싫어 (12906)  (1) 2025.07.04
전화번호 목록(hash)  (1) 2025.06.22
포켓몬(hash)  (0) 2025.06.22
완주하지 못한 선수(hash)  (1) 2025.06.22

올바른 괄호 문제 풀이 & 시행착오 정리

문제 링크 : 프로그래머스 - 올바른 괄호 (Lv.2)

문제: 문자열 s가 주어질 때, 괄호의 짝이 올바르면 true, 아니면 false를 리턴하는 문제다.
예를 들어 "(())()"는 올바른 괄호고, ")()(", "(()(" 같은 건 잘못된 괄호다.


✅ 처음 접근 방식

처음엔 단순하게 괄호 짝을 맞추는 문제니까 스택을 쓰면 되겠다고 생각했다. 여는 괄호 '('는 스택에 넣고, 닫는 괄호 ')'는 스택에서 빼는 식으로.

public static boolean solution(String s) {
    boolean answer = true;
    Stack<Character> stack = new Stack<>();

    char[] charArray = s.toCharArray();

    for (char c : charArray) {
        if (charArray[0] == ')') {
            stack.push(c);
            break;
        }

        if (c == '(') {
            stack.push(c);
        } else if (c == ')') {
            stack.pop();
        }
    }

    if (!stack.isEmpty()) {
        answer = false;
    }

    return answer;
}

❌ 실패 원인 1: 첫 글자 ')'일 때만 검사

위 코드에서 if (charArray[0] == ')')로 첫 번째 문자가 닫는 괄호일 때만 예외 처리를 해줬는데, 사실 이런 식으로 처리하면 다른 위치에서 잘못된 괄호가 나올 경우를 놓치게 된다.

예를 들어 "())(" 같은 문자열은 처음엔 괜찮다가 중간에 짝이 안 맞는 괄호가 생기는데, 위 로직은 그걸 제대로 처리하지 못한다.


❌ 실패 원인 2: 스택 비었을 때 pop() 하면 예외 발생

이 부분이 **런타임 에러(EmptyStackException)**의 진짜 원인이었다.

else if (c == ')') {
    stack.pop(); // ← 스택이 비어있을 수도 있음
}

만약 여는 괄호 없이 닫는 괄호만 먼저 나왔다면, 스택이 비어있을 텐데 여기서 pop()을 시도하면 예외 터짐. 즉, 닫는 괄호가 나왔을 때는 반드시 스택이 비어있지 않은지 확인하고 pop() 해야 한다.


✅ 고친 코드

문제를 정확히 해결하려면 아래처럼 조건을 조심해서 체크해야 함.

public static boolean solution(String s) {
    Stack<Character> stack = new Stack<>();

    for (char c : s.toCharArray()) {
        if (c == '(') {
            stack.push(c);
        } else if (c == ')') {
            if (stack.isEmpty()) {
                return false;
            }
            stack.pop();
        }
    }

    return stack.isEmpty();
}
  • (가 나오면 무조건 push
  • )가 나오면 stack이 비어있으면 false
  • 끝났는데 stack이 비어있지 않으면 짝이 안 맞는 거니까 false

 

'자료구조(알고리즘) > 프로그래머스(java)' 카테고리의 다른 글

게임 맵 최단거리  (0) 2025.07.06
같은 숫자는 싫어 (12906)  (1) 2025.07.04
전화번호 목록(hash)  (1) 2025.06.22
포켓몬(hash)  (0) 2025.06.22
완주하지 못한 선수(hash)  (1) 2025.06.22

1. 네트워크, 왜 필요한 걸까?

우리가 친구한테 메시지 보내거나 유튜브 영상 보는 것도 다 네트워크 덕분이다. 네트워크는 여러 기기들이 서로 연결돼서 정보를 주고받고, 자원도 공유할 수 있게 해주는 통신 시스템이다. 웹페이지를 띄우거나, 파일을 주고받고, 데이터를 정확하게 전달하는 것도 전부 네트워크가 도와주는 일이다. 데이터가 잘못된 길로 가지 않게 길잡이 역할도 하고, 여러 장치를 거칠 때는 그 사이사이 연결도 챙겨준다. 요즘 세상에서 네트워크 없이 살긴 힘들다.


2. 프로토콜이란?

 

기기들끼리 데이터를 주고받으려면 서로 말이 통해야 한다. 한국어로 질문했는데 영어로 답하면 대화가 안 되듯이, 네트워크에서도 서로 지켜야 할 규칙이 있다. 그걸 ‘프로토콜’이라고 부른다. 통신을 할 때 필요한 절차, 형식 같은 걸 정해놓은 약속이라고 보면 된다. 모든 기기는 이 약속에 따라야만 통신이 가능하다.


3. 모든 기능을 하나에? 그건 너무 비효율적이다

네트워크 기능을 하나로 뭉뚱그리면 편할 것 같지만, 실제로는 완전 비효율적이다. 마치 앱을 하나 짜는데 코드 전부를 한 파일에 몰아넣는 것처럼. 고치기도 어렵고, 새 기능 추가하기도 힘들어진다. 그래서 네트워크도 기능을 나눠서 관리한다. 이걸 ‘모듈화’라고 하고, 각 모듈(계층)이 자기 역할만 잘하면 시스템 전체도 깔끔하게 돌아간다.


4. 계단처럼 나눠서 관리한다 – OSI 모델

네트워크 기능을 나누는 데 가장 많이 쓰는 기준이 바로 ‘OSI 7계층’이다. 기능별로 층층이 쌓여 있는 구조인데, 총 7개 층으로 구성되어 있다. 위에서부터 아래로 내려가면서 각 계층이 다른 역할을 담당하고, 아래 계층의 도움을 받아서 자기 일을 처리하는 구조다. 개발자나 네트워크 공부할 때 자주 보게 되는 모델이다.


5. OSI 7계층, 계층별 역할은?

 

계층 번호 주요 역할 주요 프로토콜

응용 계층 L7 앱이 사용하는 통신 기능 HTTP, DNS, SMTP, FTP
표현 계층 L6 인코딩, 암호화, 압축 등 JPEG, MPEG, SSL, TLS
세션 계층 L5 앱 간 연결 관리 RPC, NetBIOS
전송 계층 L4 데이터 전송 방식 (신뢰성) TCP, UDP
네트워크 계층 L3 목적지까지 데이터 전송 IP, ICMP, ARP
데이터 링크 계층 L2 인접 장치와 통신 Ethernet, Wi-Fi
물리 계층 L1 실제 데이터 전송 (0과 1) 전압, 주파수, 케이블 등

6. L5~L7: 앱과 가까운 계층들

  • 응용 계층 (L7)
    웹사이트 요청, 이메일, 도메인 변환 등 실제 앱이 쓰는 통신 방식들을 다룬다. HTTP, SMTP, DNS 같은 게 여기 들어간다.
  • 표현 계층 (L6)
    데이터를 보내기 전에 어떤 형식으로 보낼지 정하는 곳. 인코딩, 암호화, 압축 같은 걸 담당한다.
  • 세션 계층 (L5)
    통신을 시작하고 끝내는 연결 관리. 마치 전화 걸고 끊는 것처럼, 앱끼리 통신할 때 필요한 연결을 유지한다.

이 세 계층은 앱과 밀접하게 연결되어 있어서, 통틀어 ‘어플리케이션 레이어’라고도 부른다.


7. L3~L4: 데이터가 움직이는 핵심

  • 전송 계층 (L4)
    데이터가 빠르게 갈지, 신뢰성 있게 갈지를 정한다. TCP는 안정적이고, UDP는 빠르지만 조금은 위험부담이 있다.
  • 네트워크 계층 (L3)
    데이터를 어느 경로로 보낼지 정한다. IP 주소 보고 목적지를 찾고, 최적 경로로 데이터가 가도록 한다.

8. L1~L2: 실제로 데이터가 오가는 곳

  • 데이터 링크 계층 (L2)
    같은 네트워크 안에 있는 장치들끼리 통신할 때 사용된다. MAC 주소 기반으로 동작하고, 오류가 있는지도 검사한다.
  • 물리 계층 (L1)
    데이터를 진짜 전기 신호나 무선 신호로 바꿔서 케이블이나 와이파이를 통해 전송한다. 완전 하드웨어적인 계층.

9. 데이터는 ‘포장’돼서 전송된다 – 캡슐화

메시지를 보내면, 위에서 아래로 내려오면서 각 계층이 자기 정보를 하나씩 덧붙인다. 이게 마치 선물을 포장하는 것처럼 여러 겹으로 싸여지는 건데, 이 과정을 ‘캡슐화’라고 한다.

도착한 메시지는 반대로 아래에서 위로 올라가면서 포장이 하나씩 벗겨진다. 각 계층이 자기 역할만 수행하고 위로 넘기는 식이다. 이건 ‘역캡슐화’라고 한다.


10. OSI 모델만 있는 건 아니다 – TCP/IP 모델

실제 인터넷에서 쓰이는 건 OSI보다 TCP/IP 모델이 더 많다. 이건 4계층으로 구성돼 있다.

  • 애플리케이션 계층: OSI의 L5~L7 통합
  • 전송 계층: TCP/UDP 담당
  • 인터넷 계층: IP 주소를 통한 경로 설정
  • 네트워크 접근 계층: 물리적 전송

OSI 모델이 개념 정리에 좋다면, TCP/IP는 실제 구현에 더 가까운 구조라고 보면 된다.


※ 이 내용은 아래 유튜브 영상을 참고해 정리했습니다.
출처: OSI 7계층 설명 영상 - 유튜브

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

네트워크와 인터넷, 그리고 그 동작 방식  (0) 2025.07.03

프로그래머스 문제 12906 - 같은 숫자는 싫어

배열에서 연속으로 나타나는 숫자는 하나만 남기고 제거하는 문제


문제 설명

같은 숫자가 연속해서 2번 이상 등장하면,
하나만 남기고 제거한 결과를 반환하는 문제입니다.

입력: [1, 1, 3, 3, 0, 1, 1]
출력: [1, 3, 0, 1]

처음 시도: queue.peek() 사용의 문제점

처음에는 queue.peek()로 현재 값과 비교하려 했지만,
이 방식은 "가장 앞(front)에 있는 값만 확인" 하기 때문에
항상 맨 처음 값만 비교하게 됩니다.

Queue<Integer> queue = new LinkedList<>();
queue.add(arr[0]);

for (int i = 1; i < arr.length; i++) {
    if (queue.peek() != arr[i]) {
        queue.add(arr[i]);
    }
}

왜 틀렸을까?

  • queue.peek()은 큐의 가장 앞에 있는 값만 반환합니다.
  • 하지만 이 문제에서 비교해야 할 값은 "직전에 추가한 값", 즉 큐의 가장 끝 값입니다.
  • 결국 중복 판단이 항상 맨 앞 값과만 비교되므로,
    이후 값들이 모두 잘못된 기준으로 처리됩니다.

수정방법

→ queue.peek() 대신 별도의 변수 recentNumber를 사용해
직전에 큐에 추가한 값을 기억하게 만들었고,
그 값과 arr[i]를 비교하여 연속 중복 여부를 판단했습니다.

int recentNumber = arr[0];
for (int i = 1; i < arr.length; i++) {
    if (arr[i] != recentNumber) {
        queue.add(arr[i]);
        recentNumber = arr[i];
    }
}

 


코드 구현 (Java)

public static int[] solution(int[] arr) {
    Queue<Integer> queue = new LinkedList<>();

    // 첫 번째 값은 무조건 포함
    queue.add(arr[0]);
    int recentNumber = arr[0];

    // 두 번째 값부터 비교 시작
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] != recentNumber) {
            queue.add(arr[i]);
            recentNumber = arr[i];
        }
    }

    // Queue → 배열 변환
    int[] answer = new int[queue.size()];
    int index = 0;
    while (!queue.isEmpty()) {
        answer[index++] = queue.poll();
    }

    return answer;
}

시간복잡도

  • 시간복잡도: O(N)
    → 배열 전체를 한 번 순회하며 처리
  • 공간복잡도: O(N)
    → 결과를 저장할 큐와 배열 공간 필요

핵심 포인트 정리

포인트 설명

연속된 중복만 제거 서로 떨어진 중복은 제거하지 않음
직전 값과만 비교 마지막으로 추가한 값만 기억하면 됨
Queue를 선택한 이유 삽입 순서를 유지하면서 유연하게 처리 가능
List로도 대체 가능 배열로 바로 변환하고 싶을 경우 List<Integer> → stream 방식도 가능

 

'자료구조(알고리즘) > 프로그래머스(java)' 카테고리의 다른 글

게임 맵 최단거리  (0) 2025.07.06
올바른 괄호 문제  (2) 2025.07.05
전화번호 목록(hash)  (1) 2025.06.22
포켓몬(hash)  (0) 2025.06.22
완주하지 못한 선수(hash)  (1) 2025.06.22

1. Queue란?

  • FIFO(First In First Out) 자료구조
  • 가장 먼저 들어온 데이터가 가장 먼저 나간다
  • java.util.Queue는 인터페이스이며, Collection의 하위 인터페이스

2. 주요 구현 클래스

구현체 특징

LinkedList 양방향 연결 리스트로 구현된 큐. 가장 범용적으로 사용됨
ArrayDeque 배열 기반의 큐로, null 허용 안 됨. 성능 좋고 스택처럼도 사용 가능
PriorityQueue 우선순위 큐. FIFO가 아니라 우선순위에 따라 요소가 나감

3. 주요 메서드 정리

메서드 설명 예외 발생 여부

offer(E e) 큐의 끝에 요소 추가 실패 시 false 반환
add(E e) 큐의 끝에 요소 추가 실패 시 IllegalStateException 발생
poll() 큐의 앞에서 요소 제거 및 반환 비어있으면 null 반환
remove() 큐의 앞에서 요소 제거 및 반환 비어있으면 NoSuchElementException 발생
peek() 큐의 앞 요소를 제거하지 않고 반환 비어있으면 null 반환
element() 큐의 앞 요소를 제거하지 않고 반환 비어있으면 NoSuchElementException 발생

offer / poll / peek → 예외 안 남, 안정적
add / remove / element → 예외 발생 가능, 좀 더 엄격함


4. Queue vs Stack

구분 Queue Stack

구조 FIFO LIFO
주요 연산 offer, poll, peek push, pop, peek
용도 순서대로 처리 (ex. 작업 큐) 후입선출 처리 (ex. 재귀 호출, 되돌리기 등)

5. 예제 코드 (LinkedList 기반)

import java.util.*;

public class QueueExample {
    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();

        queue.offer(1); // 또는 queue.add(1);
        queue.offer(2);
        queue.offer(3);

        System.out.println(queue.peek()); // 1
        System.out.println(queue.poll()); // 1
        System.out.println(queue.poll()); // 2
        System.out.println(queue.poll()); // 3
        System.out.println(queue.poll()); // null (큐가 비었음)
    }
}

6. 사용 예시

  • 프린터 작업 큐
  • BFS (너비 우선 탐색) 알고리즘
  • 이벤트 처리 순서 관리
  • 멀티스레드 작업 분배

7. 참고: Queue vs Deque

특징 Queue Deque (ArrayDeque)

삽입/삭제 방향 한 쪽 (뒤/앞) 양방향 (앞/뒤 모두)
예시 메서드 offer, poll, peek addFirst, addLast, removeFirst, removeLast

Deque는 Stack처럼도 쓸 수 있어서 유연함
Stack 클래스는 구식 → Deque 사용 권장


8. 요약

  • Queue는 순서가 중요한 작업에 적합한 자료구조
  • 예외를 피하고 안정적으로 사용하려면 offer, poll, peek 사용 권장
  • LinkedList와 ArrayDeque는 범용 큐 용도로 많이 사용
  • 우선순위 큐가 필요하면 PriorityQueue 사용

1. 들어가며: 네트워크와 인터넷, 왜 알아야 할까요?

이 글을 통해 우리는 네트워크가 무엇인지, 인터넷이 무엇인지, 그리고 인터넷이 어떻게 동작하는지를 완벽하게 이해할 수 있습니다. 매일 사용하는 스마트폰, 컴퓨터, 스마트 TV 등 다양한 디지털 기기들이 어떻게 서로 연결되고 데이터를 주고받는지 궁금하셨다면, 이 글이 그 궁금증을 해결해 줄 것입니다. 우리가 살아가는 디지털 세상의 근간을 이루는 네트워크와 인터넷의 기본 개념부터 실제 동작 방식까지, 차근차근 알아보겠습니다.

2. 우리 집 네트워크는 어떻게 만들어질까요?

가정에서 인터넷에 연결하기 위해서는 몇 가지 필수적인 장치와 과정이 필요합니다. 통신사에서 모든 설치를 해주지만, 그 원리를 알면 더 유용합니다.

  • IP 주소: 인터넷상의 우리 집 주소 인터넷에 연결된 모든 기기는 고유한 주소를 가집니다. 이것이 바로 IP(Internet Protocol) 주소입니다. 우리가 인터넷에 가입하면 통신사가 자동으로 이 주소를 할당해 줍니다. IP 주소는 인터넷상에서 우리 집의 위치를 알려주는 중요한 역할을 합니다.
  • 모뎀: 신호 변환의 시작 인터넷 케이블이 집으로 들어오면 가장 먼저 연결되는 장치가 바로 모뎀(Modem)입니다. 모뎀은 인터넷 신호를 우리가 사용하는 디지털 기기들이 이해할 수 있는 신호로 변환하거나, 반대로 디지털 기기의 신호를 인터넷으로 보낼 수 있는 형태로 변환하는 역할을 합니다. 즉, 신호 변환 장치라고 생각하면 됩니다.
  • 공유기: 여러 기기를 하나로 묶는 허브 모뎀 다음에는 공유기(Router 또는 Home Router)를 연결합니다. 공유기는 한 개의 IP 주소를 여러 기기들이 동시에 사용할 수 있도록 해주는 핵심 장치입니다. 스마트 TV, 컴퓨터, 스마트폰 등 다양한 기기들을 공유기에 연결하면, 이 모든 기기가 같은 네트워크 소속이 되어 서로 통신할 수 있으며, 동시에 인터넷에도 연결될 수 있습니다. 유선(랜선)과 무선(Wi-Fi) 방식으로 기기를 공유기에 연결할 수 있습니다.
  • 스위치: 더 많은 기기 연결을 위한 확장 장치 만약 공유기의 랜 포트가 부족하여 더 많은 유선 기기를 연결해야 할 때, 스위치(Switch) 또는 스위칭 허브를 사용합니다. 스위치는 같은 네트워크 내에 있는 기기들이 서로 통신할 수 있도록 하며, 공유기의 포트 수를 확장하는 역할을 합니다. 스위치를 공유기에 연결하고, 다시 스위치에 컴퓨터 등 기기들을 연결하면 모든 기기가 같은 네트워크에 소속되어 인터넷을 사용할 수 있게 됩니다.

3. 네트워크란 무엇인가요?

위에서 살펴본 것처럼, 우리 집에서 여러 기기가 서로 데이터를 주고받고 인터넷에 연결되는 모습이 바로 '네트워크'의 한 예시입니다. 네트워크란 컴퓨터나 기타 기기들이 리소스를 공유하거나 데이터를 주고받기 위해 유선 또는 무선으로 연결된 통신 체계를 의미합니다. 쉽게 말해, 기기들이 서로 소통할 수 있도록 연결된 망이라고 생각할 수 있습니다.

4. LAN과 WAN: 네트워크의 종류

네트워크는 그 범위에 따라 크게 두 가지 종류로 나눌 수 있습니다.

  • LAN (Local Area Network): 우리 주변의 작은 네트워크 LAN은 집, 학교, 회사, 건물처럼 제한된 범위 내에서 컴퓨터나 기타 기기들을 연결하여 데이터나 리소스를 공유할 수 있도록 하는 네트워크입니다. 우리가 가정에서 구축하는 네트워크가 바로 LAN의 대표적인 예시입니다.이더넷과 와이파이: LAN을 구성하는 핵심 기술 LAN을 구성하는 두 가지 중요한 기술은 유선 통신을 담당하는 이더넷(Ethernet)과 무선 통신을 담당하는 와이어리스 LAN(Wireless LAN)입니다. 특히 와이어리스 LAN은 우리가 흔히 사용하는 와이파이(Wi-Fi)로 불립니다.
  • WAN (Wide Area Network): 넓은 세상을 잇는 네트워크 WAN은 여러 LAN이나 다른 종류의 네트워크들을 하나로 묶어 멀리 떨어진 기기들도 통신이 가능하도록 하는 네트워크입니다. LAN보다 훨씬 넓은 범위를 커버하며, 지리적으로 떨어진 지역을 연결합니다.4G, 5G: 무선 WAN의 대표적인 예시 우리가 스마트폰으로 사용하는 4G, 5G 이동통신 기술도 바로 무선 WAN의 한 종류입니다. 전국에 퍼져있는 은행 ATM이 중앙 전산망에 연결되어 작동하는 것도 WAN의 예시입니다.

5. 인터넷이란 무엇인가요?

그렇다면 인터넷은 무엇일까요? 인터넷은 단순히 우리가 웹사이트에 접속하는 통로 그 이상입니다. 인터넷은 '네트워크들의 네트워크'라고 불리며, 세계에서 가장 큰 WAN이라고 할 수 있습니다. 전 세계의 수많은 LAN과 WAN들이 서로 연결되어 하나의 거대한 통신망을 이룬 것이 바로 인터넷입니다. 따라서 인터넷은 전 세계를 커버하는 글로벌 네트워크입니다.

6. ISP: 인터넷 연결의 문지기

우리가 인터넷을 사용하기 위해서는 누군가가 인터넷 연결 서비스를 제공해야 합니다. 그 역할을 하는 존재가 바로 ISP(Internet Service Provider), 즉 인터넷 서비스 제공업체입니다. 우리나라의 KT, SK브로드밴드, LG유플러스와 같은 통신사들이 대표적인 ISP에 해당합니다. ISP는 일반 사용자나 회사, 기관 등이 인터넷을 사용할 수 있도록 인터넷 연결 서비스를 제공합니다.

7. 인터넷은 어떻게 연결될까요? ISP 계층 구조와 라우터의 역할

우리가 가입한 ISP를 통해 전 세계 어디에 있는 서버와도 통신할 수 있는 이유는 무엇일까요? 이는 인터넷이 여러 ISP 네트워크의 복잡한 연결로 이루어져 있기 때문입니다.

  • ISP 티어(Tier) 구조: 인터넷의 계급도 ISP는 그 역할과 규모에 따라 계층(Tier)으로 나뉩니다.티어 1: 인터넷의 중추, 백본 네트워크 티어 1 ISP는 국제적인 범위의 네트워크를 가지고 있으며, 인터넷에 있는 모든 네트워크에 접근할 수 있습니다. 이들은 '인터넷 백본'이라고 불리며 인터넷의 중추 역할을 합니다. 티어 1 ISP는 일반 사용자에게 직접 서비스를 제공하기보다는, 다른 하위 티어 ISP와 연결되어 인터넷 전체가 유기적으로 작동하도록 합니다. 이들 간에는 트래픽 전송 비용을 주고받지 않는 '피어링' 협약을 맺는 것이 일반적입니다.티어 2: 국가 및 광역 서비스 제공자 티어 2 ISP는 국가 또는 넓은 지방의 범위를 커버하는 네트워크를 보유합니다. 우리나라의 주요 통신사(KT, SK브로드밴드, LG유플러스)가 여기에 해당합니다. 이들은 일반 사용자나 기업에 인터넷 연결 서비스를 직접 제공하며, 티어 1 ISP와 연결되어 인터넷의 모든 영역에 접속합니다. 티어 2 ISP 간에도 서로 비용 정산 없이 데이터를 주고받는 경우가 많지만, 티어 1 ISP를 이용할 때는 비용을 지불합니다.티어 3: 지역 기반 서비스 제공자 티어 3 ISP는 작은 지역 범위를 커버하며, 해당 지역 내에서 인터넷 연결 서비스를 제공합니다. 자체 네트워크가 없는 경우도 있으며, 상위 ISP(티어 1 또는 티어 2)에게 비용을 지불하고 인터넷 트래픽을 구매하여 사용자에게 서비스를 제공합니다.
  • 라우터: 데이터 길잡이 이처럼 다양한 ISP 네트워크들이 서로 연결되어 데이터를 주고받을 수 있게 하는 핵심 장치가 바로 라우터(Router)입니다. 라우터는 데이터를 목적하는 네트워크로 보내는 역할을 합니다. 우리가 데이터를 보낼 때, 데이터는 여러 라우터를 거쳐 목적지 IP 주소를 찾아 이동합니다. 라우터는 서로 다른 ISP가 관리하는 네트워크를 연결하는 다리 역할도 수행합니다.
  • 데이터는 어떻게 이동할까요? 우리가 컴퓨터에서 웹사이트에 접속하거나 파일을 보낼 때, 데이터는 먼저 우리가 가입한 ISP의 라우터로 전달됩니다. 이 라우터는 목적지까지 데이터를 보내기 위해 다른 ISP 네트워크의 라우터로 데이터를 넘기고, 이 과정이 반복되어 최종적으로 목적지에 도달합니다. 즉, 데이터는 여러 라우터와 여러 ISP 네트워크를 거쳐 이동하게 됩니다.

8. 네트워크를 이루는 장치들: 노드, 엔드 시스템, 클라이언트-서버

네트워크는 다양한 장치들로 구성되어 있으며, 이들을 통칭하는 용어들이 있습니다.

  • 노드: 네트워크에 연결된 모든 장치 노드(Node)는 네트워크를 구성하는 컴퓨터, 공유기, 스위치, 모뎀, 라우터 등 네트워크에 연결된 모든 장치를 통틀어 부르는 말입니다.
  • 엔드 시스템 (호스트): 네트워크의 끝에서 서비스를 사용하는 장치 노드 중에서 네트워크의 끝에 있는 장치를 엔드 시스템(End System) 또는 호스트(Host)라고 부릅니다. 이들은 주로 일반 사용자가 애플리케이션을 사용하기 위해 연결된 장치들입니다. (예: 스마트폰, 컴퓨터, 스마트 TV)
  • 클라이언트와 서버: 요청하고 응답하는 관계 엔드 시스템은 주로 클라이언트(Client)와 서버(Server)로 나뉩니다.클라이언트: 다른 호스트의 데이터나 리소스를 요청하는 호스트를 의미합니다. 예를 들어, 우리가 컴퓨터로 유튜브 영상을 볼 때, 우리의 컴퓨터는 유튜브 서버에 영상을 요청하는 클라이언트가 됩니다.서버: 다른 호스트에게 서비스를 제공하는 호스트입니다. 클라이언트의 요청에 따라 데이터나 리소스를 제공합니다. 유튜브 영상이 저장되어 있고, 우리의 요청에 응답하여 영상을 전송해 주는 유튜브의 컴퓨터가 바로 서버입니다.클라이언트와 서버는 서로 다른 호스트이지만, 매우 긴밀하게 협력하여 다양한 기능을 수행하며, 이러한 방식으로 동작하는 애플리케이션을 '클라이언트-서버 모델'로 동작한다고 말합니다.

 

 

※ 이 내용은 아래 유튜브 영상을 참고해 정리했습니다.

출처 - 네트워크와 인터넷 개념 설명!(쉬운 코드) - 유튜브 

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

OSI 7 Layer  (0) 2025.07.05

+ Recent posts