gugbab2's GitBook
  • Language
    • C++
      • 강의
        • C++ 언매니지드 프로그래밍
          • C++ 프로그래밍
          • 출력(Output)
          • 입력(Input)
          • bool 타입, Reference
          • 상수(const)
          • 문자열(string)
          • 파일 입출력
          • 개체지향 프로그래밍1
          • 개체지향 프로그래밍2
          • 개체지향 프로그래밍3
          • 캐스팅(형변환, casting)
          • 인라인 함수
          • static 키워드
          • 예외(Exception)
          • STL(Standard Template Library) 컨테이너(Container) - Vector
          • STL 컨테이너 - Map
          • STL 컨테이너 - Queue, Stack, Set, List
          • 템플릿(Template) 프로그래밍
          • 새로운 키워드(C++11 ~) 1
          • 새로운 키워드(C++11 ~) 2
          • 새로운 자료형
          • 새로운 STL 컨테이너
          • 스마트(smart) 포인터
          • 이동생성자 및 이동대입연산자
          • constexpr
          • Lamda Expression
      • 책
        • The C++ Programming Lanuaage
          • 2부 : 기본 기능
            • 6. 타입과 선언
            • 7. 포인터, 배열, 참조
            • 8. 구조체(struct), 공용체(union), 열거형(enum)
            • 10. 표현식
            • 11. 선택 연산
            • 12. 함수
            • 13. 예외 처리
            • 15. 소스 파일과 프로그램
          • 3부 : 추상화 메커니즘
            • 16. 클래스
            • 17. 생성, 소멸, 복사와 이동
            • 18. 연산자 오버로딩
            • 19. 특수 연산자
            • 20. 파생클래스
        • 씹어먹는 C++
          • 2. C++ 참조자(reference) 의 도입
          • 5.1 연산자 오버로딩(비교, 대입 연산자)
          • 5-2. 연산자 오버로딩(이항, 입출력, 타입변환, 증감 연산자)
          • 6-2. 가상(virtual) 함수와 다형성
          • 6-3. 가상 함수에 대한 지식들
          • 9-1. 코드를 찍어내는 틀 - C++ 템플릿(template)
          • 9-2. 가변 길이 템플릿(Variadic template)
          • 9-3. 템플릿 메타 프로그래밍 (Template Meta Programming)
          • 9-4. 템플릿 메타 프로그래밍2
          • 16.1 유니폼 초기화(Uniform Initialization)
          • 토막글 2. 람다(lambda)
    • Java
      • 강의
        • 김영한의 실전 자바 - 기본편
          • 절차 지향 vs 객체 지향
            • 절차 지향 프로그래밍
            • 객체 지향 프로그래밍
          • 변수
            • 클래스 변수 / 인스턴스 변수, 멤버 변수 / 지역 변수
            • 기본형 vs 참조형
          • 패키지
            • 패키지
            • CLI 환경에서 .java 파일 컴파일 && 실행
          • 접근 제어자
            • 접근 제어자 - 기본
            • 캡슐화
          • static
            • 자바 메모리 구조
            • static 기본
            • 스택 영역, 힙 영역
              • 스택 영역, 힙 영역 - 기본
              • 메소드가 실행될 때 어떤일이 일어나는가?
          • 상속
            • 상속 기본
          • 다형성(Pilymorphism)
            • 다형성 기본
            • 다형성의 활용
              • 다형성의 활용 - 기본
              • 다형성의 활용 - 추상클래스
              • 다형성의 활용 - 인터페이스
            • 다형성과 설계
              • 좋은 객체 지향 프로그래밍
        • 김영한의 실전 자바 - 중급1편
          • 1. Object 클래스
          • 2. 불변 객체
          • 3. String 클래스
          • 4. 래퍼, Class 클래스
          • 5. 열거형 - ENUM
          • 6. 날짜와 시간
          • 7. 중첩 클래스, 내부 클래스1
          • 8. 중첩 클래스, 내부 클래스2
          • 9. 예외 처리1 - 이론
          • 10. 예외 처리 - 실습
        • 김영한의 실전 자바 - 중급2편
          • 1. 제네릭 - Generic1
          • 2. 제네릭 - Generic2
          • 3. 컬렉션 프레임워크 - ArrayList
          • 4. 컬렉션 프레임워크 - LinkedList
          • 5. 컬렉션 프레임워크 - List
          • 6. 컬렉션 프레임워크 - 해시(Hash)
          • 7. 컬렉션 프레임워크 - HashSet
          • 8. 컬렉션 프레임워크 - Set
            • 레드 블랙 트리
          • 9. 컬렉션 프레임워크 - Map, Stack, Queue
            • 왜(?) Set 은 내부에서 Map 을 사용할까?
          • 10. 컬렉션 프레임워크 - 순회, 정렬, 전체 정리
        • 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성
          • 프로세스와 스레드 소개
          • 스레드 생성과 실행
          • 스레드 제어와 생명 주기1
          • 스레드 제어와 생명 주기2
          • 메모리 가시성
          • 동기화 - synchronized
            • synchronized 키워드 이해도 체크
          • 고급 동기화 - concurrent.Lock
          • 생산자 소비자 문제1
          • 생산자 소비자 문제2
          • CAS - 동기화와 원자적 연산
          • 동시성 컬렉션
          • 스레드 풀과 Executor 프레임워크1
          • 스레드 풀과 Executor 프레임워크2
        • 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
          • 문자 인코딩
          • I/O 기본1
          • I/O 기본2
          • I/O 활용
          • File, Files
          • 네트워크 - 프로그램1
          • 네트워크 - 프로그램2
          • 채팅 프로그램
          • HTTP 서버 만들기
          • 리플렉션
          • 애노테이션
          • HTTP 서버 활용
        • 김영한의 실전 자바 - 고급3편, 람다, 스트림, 함형 프로그래밍
          • 람다가 필요한 이유
          • 람다
          • 함수형 인터페이스
          • 람다 활용
          • 람다 vs 익명 클래스
          • 메서드 참조
          • 스트림API1 - 기본
          • 스트림 API2 - 기능
          • 스트림 API3 - 컬렉터
          • Optional
          • 디폴트 메서드
          • 병렬 스트림
          • 함수형 프로그래밍
        • 기초 탄탄! 독하게 시작하는 Java - Part2: OOP 와 JVM
          • 2. 클래스 - 첫 번째
          • 3. 클래스 - 두번째
          • 4. 상속과 관계
          • 6. JVM(Java Virtual machine) 기본 이론
          • 7. JVM 과 GC 그리고 객체
          • 8. 불변 객체와 String 클래스
      • 책
        • 자바의 신
          • 변수
            • 클래스 변수(static) 사용 주의 케이스
            • Java volatile 과 Atomic 변수(+CAS)
          • 연산자
            • 비트 연산자 활용 예제
          • 배열
          • 참조 자료형
          • 상속
          • Object 클래스
          • interface, abstract class, enum
          • 예외
          • String 클래스
            • String 구조
            • String 문자열을 byte 로 변환하기
            • String 클래스에서 자주 사용되는 메서드
            • String 클래스로 살펴보는 불변(Immutable)객체
            • StringBuilder, StringBuffer
          • Nested 클래스
          • 어노테이션
            • 어노테이션 기본
            • 어노테이션의 사용
          • JVM 이해하기
            • 왜 JVM 을 사용해?
            • JVM, JRE, JDK
            • JVM 구조 이해하기
            • 클래스 로더 시스템
            • JIT(Just-In-Time) 컴파일러
            • GC(Garbage Collector)
              • GC Part.1
              • GC Part.2
              • GC 튜닝
          • java.lang
            • Wrapper 클래스
            • System 클래스
          • Generic
            • 제네릭 기본
            • 와일드카드
            • 와일드카드 GET / SET 경계
            • 와일드카드 extends / super 사용시기
            • 혼동할 수 있는 와일드카드 표현
          • Collection
            • 자료구조
              • 이진 탐색 트리 vs 레드 블랙 트리
            • Collection
            • List
              • ArrayList
              • Vector
              • Stack
              • LinkedList
            • Set, Queue
              • HashSet
              • LinkedHashSet
              • TreeSet
              • Priority Queue
              • ArrayDeque
            • Map
              • HashMap
              • Hashtable
              • LinkedHashMap
              • TreeMap
          • Thread
            • Thread 기본
            • Thread 와 관련이 많은, Synchronized
            • Thread 를 통제하는 메서드
            • ThreadGroup
          • I/O
            • InputStream, OutputStream
            • Reader, Writer
          • Serializable, NIO
            • Serializable
            • NIO (New IO)
          • 네트워크 프로그래밍
            • 네트워크 기본 & TCP 통신
            • UDP 통신
          • 람다
            • 함수형 인터페이스
            • 람다란?
        • 벨둥(Bealdung)
          • Java Concurrency
            • Java Concurrency Basics
              • Overview of the java.util.concurrent
              • Guide to the Synchronized Keyword in Java
              • Guide to the Volatile Keyword in Java
              • Guide to the java.util.concurrent.Future
              • ThreadLocal in Java
      • 그 외
        • 시스템 콜과 자바에서의 시스템 콜 사용례
        • 자바 NIO 의 동작원리 및 IO 모델
        • 함수형 인터페이스(FunctionInterface) - 자바8
  • Spring
    • 강의
      • 스프링 핵심 원리 - 기본편
        • 큰 흐름 잡기
        • 스프링 핵심 원리 이해1 - 예제 만들기
        • 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
        • 스프링 컨테이너와 스프링 빈
        • 싱글톤 컨테이너
        • 컴포넌트 스캔
        • 의존관계 자동 주입
        • 빈 생명주기 콜백
        • 빈 스코프
      • 토비의 스프링6 - 이해와 원리
        • 3. 오브젝트와 의존관계1
        • 3. 오브젝트와 의존관계2
        • 4. 테스트
        • 5. 템플릿
        • 6.예외
        • 7. 서비스 추상화
    • 책
      • JSP 2.3 웹 프로그래밍
        • Servlet
        • JSP
        • 쿠키 / 세션
        • MVC 패턴
        • 실무 때 고민할 만한 부분
      • 스프링 입문을 위한 자바 객체지향의 원리와 이해
        • 자바와 절차적/구조적 프로그래밍
        • 객체지향의 4대 특성
        • 객체지향 설계의 5원칙
        • 스프링이 사랑한 디자인 패턴
        • IoC / DI
        • AOP(Aspect Oriented Programming), 관점 지향 프로그래밍
      • 토비의 스프링 3.1
        • Spring vs Spring Boot
        • 1. 오브젝트와 의존관계
          • 1.4 제어의 역전(IoC)
          • 1.5 스프링의 IoC
          • 1.6 싱글톤 레지스트리와 오브젝트 스코프
    • 그 외
      • 스프링 부트(SpringBoot) 탄생 배경
  • CS
    • DATA STRUCTURES
      • 선택 정렬(Selection Sort)
      • 버블 정렬(Bubble Sort)
      • 삽입 정렬(Insertion Sort)
    • OS
      • 강의
      • 책
        • 혼자 공부하는 컴퓨터구조 + 운영체제
          • 1. 컴퓨터 구조 시작하기
          • 2. 데이터
          • 3. 명령어
          • 4. CPU 의 작동원리
          • 5. CPU 성능 향상 기법
          • 6. 메모리와 캐시메모리
          • 7. 보조기억장치
          • 8. 입출력장치
          • 9. 운영체제 시작하기
          • 10. 프로세스와 스레드
    • NETWORK
      • 그 외
        • REST API
          • REST API
          • URI & MIME type
          • Collection Pattern
          • Collection Pattern 적용
          • Spring Web MVC 구현
        • SSL 인증 동작
        • DTO & JSON & CROS
          • DTO
          • 직렬화(Serialization)
          • Jackson ObjectMapper
          • CROS
        • Connection Timeout / Read Timeout
      • 강의
        • 외워서 끝내는 네트워크 핵심이론 - 기초
          • Internet 기반 네트워크 입문
            • Host 는 이렇게 외우자
            • 스위치가 하는 일과 비용
          • L2 수준에서 외울 것들
            • NIC, L2 Frame, LAN 카드 그리고 MAC 주소
            • L2 스위치에 대해서
            • LAN 과 WAN 의 경계 그리고 Broadcast
          • L3 수준에서 외울 것들
            • IPv4 주소의 기본 구조
            • L3 IP Packet 으로 외워라
            • 패킷의 생성과 전달 및 계층별 데이터 단위
            • 이해하면 인생이 바뀌는 TCP/IP 송, 수신 구조
            • IP 헤더 형식
            • 서브넷 마스크와 CIDR
            • Broadcast IP 주소와 Localhost
            • TTL 과 단편화
            • 인터넷 설정 자동화를 위한 DHCP
            • ARP 과 Ping(RTT : Round Trip Time)
          • L4 수준 대표주자 TCP 와 UDP
            • TCP 와 UDP 개요
            • TCP 연결 및 상태 변화
            • TCP 연결 종료 및 상태 변화
            • TCP, UDP 헤더 형식과 게임서버 특징
            • TCP 가 연결이라는 착각
            • TCP 연결과 게임버그
          • 웹을 이루는 핵심기술
            • DNS
            • URL, URI
        • 외워서 끝내는 네트워크 핵심 이론 - 응용
          • 네트워크 장치의 구조
            • 세 가지 네트워크 장치 구조
            • Inline 구조
            • Out of path 구조와 DPI 그리고 망중립
            • Proxy(클라이언트 입장) - 우회
            • Proxy(클라이언트 입장) - 보호와 감시
            • Reverse Proxy(서버 입장)
          • 인터넷 공유기의 작동 원리
            • 공유기 개요
            • Symmetric NAT
            • Full Cone 방식
            • Restricted Cone, Port Restricted Cone
            • 포트 포워딩
            • UPnP 와 NAT
          • 부하분산 시스템 작동 원리
            • L4 부하분산 무정지 시스템
            • 대규모 부하분산을 위한 GSLB
          • VPN과 네트워크 보안 솔루션
            • PN 과 VPN
            • IPSec VPN 과 터널링 개념
            • VPN 과 재택근무
        • 외워서 끝내는 SSL 과 최소한의 암호기술
          • 기초이론
            • Checksum (검사합)
            • Hash
          • 암호기술에 대한 이해
            • 대칭키
            • 비대칭키
          • PKI 시스템과 인터넷
            • 인터넷을 위한 비대칭키 체계
            • 공개키 신뢰를 위한 검증체계
            • 웹서비스와 공인인증서
      • 책
        • 그림으로 배우는 네트워크 원리
          • 1. 네트워크 기본
          • 2. 네트워크를 만드는 것
          • 3. 네트워크의 공통 언어 TCP/IP
    • SECURITY
      • 그 외
        • Basic Auth
        • HMAC 기반 인증
    • 그 외
      • 동기/비동기 & 블로킹/논블록킹
  • DB
    • 그 외
      • 인덱스(Index)
      • 트랜잭션(TRANSACTION)
      • 실무에서 외래키를 사용하지 않는 이유
      • ORM vs SQL Mapper
      • 문자열 vs DATE
      • EXPLAIN 명령어
    • 강의
      • Real MySQL 시즌 1
        • Part.1
          • 1강. CHAR vs VARCHAR
          • 2강. VARCHAR vs TEXT
          • 3강. COUNT(*) & COUNT(DISTINCT) 튜닝
          • 4강. 페이징 쿼리 작성
          • 5강. Stored Function
      • 토크온 41차. JPA 프로그래밍 기본 다지기
        • 1. JPA 소개
        • 2. JPA 기초와 매핑
        • 3. 필드와 컬럼 매핑
        • 4. 연관관계 매핑
        • 5. 양방향 매핑
        • 6. JPA 내부구조
        • 7. JPA 객체지향쿼리
        • 8. Spring Data JPA 와 QueryDSL 이해
    • 책
  • Software Development Methodology
    • TDD
      • 강의
        • Spring Boot TDD - 입문부터 실전까지 정확하게
          • 세션2. TDD 소개
          • 세션5. API 설계
          • 세션6. TDD 주기 첫 번째 경험
          • 세션7. TDD 주기 반복
      • 그 외
        • 단위 테스트(Unit Test) 작성의 필요성
        • JUnit5
          • A Guide to JUnit 5
          • Guide to JUnit 5 Parameterized Tests
          • AssertJ Exception Assertions
          • Testing in Spring Boot
          • Junit 과 Mockito 기반의 Spring 단위 테스트 코드 작성법
        • Code Coverage
          • Code Coverage?
    • DDD
      • 책
        • 도메인 주도 설계(Domain-Driven Design)
          • 04 - 도메인의 격리
          • 05 - 소프트웨어에서 표현되는 모델
          • 06 - 도메인 객체의 생명주기
          • 07 - 언어의 사용(확장 예제) (1)
          • 07 - 언어의 사용(확장 예제) (2)
        • 도메인 주도 개발 시작하기
          • 1. 도메인 모델 시작하기
          • 2. 아키텍처 개요
          • 3. 애그리거트
          • 4. 리포지터리와 모델 구현
            • DAO vs Repository
      • 강의
        • DDD 세레나데(NEXTSTEP)
          • 1주차
            • 도메인 주도 설계 등장 배경
            • 레거시 코드
            • 유연한 설계 - ASSERTION
          • 2주차
            • 전략적 설계 - UBIQUITOUS LANGUAGE
            • 전략적 설계 - BOUNDED CONTEXT
          • 3주차
            • 전술적 설계 - VALUE OBJECT 와 ENTITY
            • 전술적 설계 - AGGREGATE 와 REPOSITORY
            • 전술적 설계 - SERVICE
    • REFACTORING
      • 일급 컬렉션(First Class Collection) 소개와 사용해야하는 이유
  • ARCHITECTURE
    • Event Driven Architecture
  • 멘토링
    • F-Lab
      • 10회차(2024.12.29)
Powered by GitBook
On this page
  • 1. 바이트 코드(ByteBuffer)
  • 일반 버퍼의 동작 과정
  • Direct Buffer 의 동작 과정
  • 어떻게 네이티브 메모리를 자바에서 사용이 가능한가?
  • 결론
  • 2. 채널 (Channel)
  • ScatteringByteChannel, GatheringByteChannel
  • 파일 채널 (FileChannel)
  • 소켓 채널(SocketChannel)
  • 3. 셀렉터(Selector)
  • 3-1. 기존의 네트워크 프로그래밍(ThreadPool) 모델의 단점
  • 3-2. 논 블로킹 모델과 셀렉터 동작 원리
  • 4. I/O 모델 및 간단한 블로킹IO & NIO 예제
  • 4-1. I/O 모델
  • 4-2. 간단한 블로킹IO & NIO 예제
  1. Language
  2. Java
  3. 그 외

자바 NIO 의 동작원리 및 IO 모델

Previous시스템 콜과 자바에서의 시스템 콜 사용례Next함수형 인터페이스(FunctionInterface) - 자바8

Last updated 6 months ago

참고 링크

1. 바이트 코드(ByteBuffer)

  • Buffer 에 대한 사용법은 많은 블로그나 정보가 인터넷에 깔려있다. 그 중 볼만하다고 여겨지는 것은 이다.

  • 여기서는 왜 바이트버퍼(ByteBuffer) 에 대해서 알아보자.

    • 왜 바이트버퍼만 다루려 하는 것일까? -> 바이트 버퍼가 시스템 메모리를 직접 사용하는 다이렉트 버퍼를 만들 수 있는 버퍼 클래스이기 때문이다.

    • 그렇다면 왜? 바이트 버퍼만 다이렉트 버퍼를 만들 수 있게 되었을까? -> 운영체제가 사용하는 가장 기본적인 단위가 바이트이고, 시스템 메모리 또한 순차적인 바이트들의 집합이기 때문이다.

  • 우리가 해당 메서드를 통해 기존 allocate() 통해서 버퍼를 생성하는 것과 같이 다이렉트 버퍼를 만들 수 있다.

    • 여기서 리턴 할 때 DirecByteByffer 를 생성해주는데, 이 녀석을 잘 보면 MappedByteBuffer 를 상속받는 객체임을 알 수 있다.

일반 버퍼의 동작 과정

  • 일반 버퍼의 파일 I/O 에서는 JVM 힙 메모리 -> 네이티브 메모리로의 중간 복사가 발생하여, 데이터가 JVM 에서 운영체제 메모리로 복사 되는 추가 오버헤드가 생겨난다.

  • 대용량 파일을 다루거나 빈번한 I/O 가 있는 경우, 이 중간 복사 단계가 병목을 초래할 수 있다.

일반 버퍼의 파일 읽기

  • 애플리케이션이 파일을 읽으려 하면 JVM 은 시스템 콜을 하고 운영체제는 디스크에서 해당 데이터를 읽어 네이티브 메모리(임시 버퍼) 에 임시 저장 한다.

  • 운영체제는 이 데이터를 JVM 힙 메모리의 버퍼로 복사한다.

  • 애플리케이션이 JVM 힙 메모리에 저장된 데이터를 읽는다.

일반 버퍼의 파일 쓰기

  • JVM 은 애플리케이션이 보낸 데이터를 JVM 힙 메모리에 버퍼로 저장한다.

  • JVM 은 이 데이터를 네이티브 메모리로 복사하여 운영체제에 전달한다. (시스템 콜)

  • 운영체제는 이 데이터를 디스크에 기록한다.

Direct Buffer 의 동작 과정

Direct Buffer 생성

  • ByteBuffer.allocateDirect() 메서드를 통해 Direct Buffer 를 생성하면 JVM 은 네이티브 메모리에 직접 버퍼를 할당한다.

    • 네이티브 메모리에 할당된 Direct Buffer 는 GC 대상이 아니기 때문에, 메모리 해제를 코드 내에서 직접 해야하는데, 이로 인해 메모리 누수 위험이 증가한다.

    • 시스템 콜을 통해서 네이티브 메모리에 객체를 생성해야 하기 때문에, 초기화 비용이 비교적 크다!

파일 읽기

  • 애플리케이션이 Direct Buffer 를 통해 파일 읽기를 요청하면, 운영체제는 디스크에서 데이터를 읽어 바로 네이티브 메모리(Direct Buffer) 로 로드한다.

  • 애플리케이션은 Direct Buffer 에 매핑된 데이터를 바로 사용 가능하며, JVM 힙 메모리로의 추가 복사 과정이 필요하지 않다.

파일 쓰기

  • 애플리케이션이 Direct Buffer 에 데이터를 쓰면, 해당 데이터는 네이티브 메모리 영역에 직접 기록된다.

  • 운영체제는 이 네이티브 메모리에 있는 데이터를 디스크에 직접 쓸 수 있어, 중간 복사 없이 효율적으로 데이터가 기록된다.

어떻게 네이티브 메모리를 자바에서 사용이 가능한가?

  • 그렇다면 빨라져서 좋은 건 알겠는데, 어떻게 네이티브 메모리를 자바에서 사용이 가능할까?

  • 자바는 보통 JVM 힙 메모리만을 사용하는 언어로 알려져 있지만, 자바 NIO 의 Direct Buffer 와 같은 기능을 통해 네이티브 메모리도 사용이 가능하다. 이를 가능하게 하는 주요 요인은 JNI(Java Native Interface) 와 Unsafe 클래스의 사용이다.

  • 자바는 이러한 방법을 통해 운영체제의 네이티브 메모리에 접근할 수 있는 기능을 제한적으로 제공하며, Direct Buffer 는 이 메커니즘을 이용해 네이티브 메모리에 접근한다.

Java Native Interface(JNI)

  • JNI 는 자바 애플리케이션이 네이티브 라이브러리(C, C++ 로 작성된 라이브러리) 와 상호작용 할 수 있도록 해주는 인터페이스이다. 자바는 보통 플랫폼 독립적인 언어로 설계되었지만, JNI 는 네이티브 코드와 통신을 가능하게 하여 네이티브 메모리에 직접 접근하는 기능을 제공한다.

    • 자바 NIO에서 Direct Buffer는 JNI를 통해 네이티브 메모리에 접근하여, 메모리를 할당하고 관리할 수 있습니다.

    • Direct Buffer가 생성되면, JVM은 JNI 호출을 통해 운영체제에서 메모리를 할당받아 Direct Buffer에 할당합니다.

    • 이런 방식으로 JVM 힙 메모리와 독립적인 네이티브 메모리 영역을 사용하여 데이터를 처리할 수 있게 됩니다.

sun.misc.Unsafe 클래스(비공개 API)

  • sun.misc.Unsafe 클래스는 자바의 비공개 API로, 일반적으로는 사용이 권장되지 않지만, 직접 메모리 할당과 조작을 지원합니다. JVM 내부에서는 이 클래스를 사용해 네이티브 메모리 접근과 같은 저수준 작업을 수행합니다.

    • Direct Buffer도 내부적으로 Unsafe 클래스를 사용하여, 네이티브 메모리에 데이터를 직접 읽고 쓰는 기능을 구현합니다.

    • Unsafe를 통해 네이티브 메모리를 할당하거나 해제할 수 있으며, JVM의 가비지 컬렉터의 관리 밖에서 메모리를 관리하게 됩니다.

    • 이를 통해 네이티브 메모리에 접근하여, 파일 I/O와 같은 데이터 전송을 중간 복사 없이 수행할 수 있게 됩니다.

Direct Buffer의 구현 방식

  • 자바 NIO의 ByteBuffer.allocateDirect() 메서드를 사용하면, JVM은 운영체제의 네이티브 메모리에 직접 할당된 버퍼를 생성합니다. Direct Buffer는 JVM 힙 메모리와 별도로 관리되며, 가비지 컬렉터가 아닌 운영체제에서 메모리를 해제합니다.

  • Direct Buffer가 생성되고 해제되는 과정을 요약하면 다음과 같습니다:

    1. Direct Buffer 요청: ByteBuffer.allocateDirect()가 호출되면, JVM은 Direct Buffer 생성 요청을 받습니다.

    2. 네이티브 메모리 할당: JVM은 JNI를 사용하여 운영체제에 네이티브 메모리 할당을 요청하고, 이를 통해 메모리를 직접 할당받습니다.

    3. 메모리 접근 및 관리: Direct Buffer는 Unsafe 클래스 또는 JNI를 통해 네이티브 메모리의 주소를 관리하며, 데이터를 직접 읽고 쓸 수 있는 기능을 제공합니다.

    4. 메모리 해제 (가비지 컬렉션과의 독립성) : Direct Buffer는 JVM 힙 메모리가 아니기 때문에, 가비지 컬렉션의 대상이 아니다.

      • 그렇다면 어떻게 메모리를 해제할 수 있을까? (직접적인 방법을 권장!)

        • 간접적인 방법

          • Direct Buffer 는 GC 의 관리 대상은 아니지만, 참조하는 객체가 더 이상 필요하지 않을 경우, 해당 객체에 대한 참조를 제거하면 GC 가 메모리를 해제하기 위한 신호를 받을 수 있다.

          • 그러나 이는 완전한 해제를 보장하지 않으며, GC 가 직접적으로 네이티브 메모리를 해제하지는 않는다

        • 직접적인 방법

          • Direct Buffer 를 생성하면 내부적으로 Cleaner 객체가 생성되며, Cleaner 객체를 사용해 직접적으로 네이티브 메모리를 해제할 수 있다.

결론

  • 그렇다면 논 다이렉트 버퍼를 사용하지 않아야 할까?

  • 답은, 상황에 따라 다르다이다!

  • 우리는 위에서 다이렉트 버퍼와, 논 다이렉트 버퍼를 비교하면서 힌트를 얻었다.

  1. 다이렉트 버퍼(Direct Buffer)

    • 장점 : 읽고 쓰기가 네이티브 메모리를 사용하므로 매우 빠르다.

    • 단점 : 네이티브 메모리를 사용하기 때문에 할당 / 해제 비용이 다소 비싸다.

  2. 논 다이렉트 버퍼(Non-Direct Buffer)

    • 장점 : 네이티브 메모리가 아닌 Heap 영역에 생성되기 때문에 할당 / 해제 비용이 보다 저렴하다.

    • 단점 : 두 번의 버퍼를 거치기 때문에 읽고 쓰기가 느리다.

  • 따라서 일반적으로 성능에 민감하고 버퍼를 오랫동안 유지해서 사용할 필요가 있을 경우(대용량 파일)에는 다이렉트 버퍼를 유지하고, 그 외에는 논 다이렉트 버퍼를 사용하자!

2. 채널 (Channel)

  • 채널과 스트림은 상당히 유사하지만, 채널이 스트림의 확장이나 발전된 형태는 아니다. 일종의 게이트웨이라 볼 수 있는데, 단기 기존의 파일이나 소켓 등에서 사용하던 스트림을 NIO 기능을 이용할 수 있도록 도와주는 메서드를 제공한다.

  • 스트림과 차이점을 위주로 설명하면 아래와 같다.

    • 데이터를 받기 위한 타겟으로 바이트버퍼(ByteBuffer) 를 사용

      • 다양한 래핑 클래스들(CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer)이 존재한다.

    • 채널을 이용하면 운영체제 수준의 네이티브 IO 서비스들을 직간접적으로 사용 가능하다.

      • MMIO / 파일 락킹 등

    • 스트림과 달리 단방향 뿐 아니라, 양방향 통신도 가능하다.

      • 항상 양방향 통신이 가능한 것은 아니다 ;; (소켓 채널은 양방향 통신을 지원하지만, 파일 채널은 지원하지 않는다)

  • 우리가 볼 채널은 파일채널과 소켓채널이다. 그 전에 ScatteringByteChannel, GatheringByteChannel 을 보고자 한다.

ScatteringByteChannel, GatheringByteChannel

  • 시스템 콜과 자바에서의 시스템 콜 사용례 글에서 운영체제에서 지원하는 MMIO(Memory-mapped I/O) 에 대해 알아보았다.

  • NIO 채널에서는 효율적인 입출력을 위해 운영체제가 지원하는 네이티브 IO 서비스인 Scatter/Gather 를 사용할 수 있도록 위의 인터페이스를 지원해주고 있다.

  • 이 인터페이스를 사용하므로, 시스템 콜과 커널 영역에서 프로세스 영역으로 버퍼 복사를 줄여주거나 또는 완전히 없애줄 수 있다.

파일 채널 (FileChannel)

  • 파일 채널은 파일의 관련된 작업들을 지원하는 채널들로 아래의 특징을 가지고 있다.

    • ByteChannel 인터페이스를 구현한다.

      • 이 인터페이스를 구현하므로 양방향성을 가질 수 있으나, 항상 그런 것은 아니다.

    • AbstactInterruptibleChannel 추상 클래스를 구현하고 있다.

      • 따라서, 비동기적인 방식으로 채널을 닫을 수 있게 되어 스레드와 채널 간의 상태 불일치가 발생하지 않도록 도와준다.

    • ScatteringByteChannel, GatheringByteChannel 을 구현한다.

      • 따라서 보다, 빠른 I/O 수행이 가능하다.

  • 위는 구현에 관련된 특징이라면, 실제로 파일 채널이 같는 특징은 다음과 같다.

    • 파일 채널은 항상 블로킹 모드이며, 논 블로킹 모드로 설정할 수 없다.

    • 파일 채널 객체를 직접 만들 수 없다.

    • 대부분의 채널처럼 파일 채널도 가능하면 네이티브 I/O 서비스를 이용하려 한다.

    • 파일 채널 객체는 Thread-safe 하다.

파일 채널의 특징

  • 파일 채널은 항상 블로킹 모드이며, 논 블로킹 모드로 설정할 수 없다.

    • 이 이유는 운영체제의 기능과 연관이 있는데, 현대의 운영체제들은 강력한 캐싱과 프리패치 알고리즘으로 디스크의 I/O 를 사용하지만, 논 블로킹 모드로 사용할 경우 처리 루틴이 달라져 이러한 기능들을 사용하는데 제한이 되기 때문이다.

    • 그렇다면, 파일 채널은 항상 블로킹 I/O 만 사용해야 할까?

      • 비동기 I/O 모델은 포스팅 결론부에서 다룰 예정이기에, 이런 방식이 있다고만 알아두자.

  • 파일 채널 객체를 직접 만들 수 없다.

    • 파일 채널 객체는 이미 열려있는 파일 객체의 팩토리 메서드(getChannel()) 를 호출해서 생성된다. 따라서, FileIntputStream 으로 생성된 채널은 읽기만, FileOutputStream 으로 생성된 채널은 쓰기만 가능하다.

  • 대부분의 채널처럼 파일 채널도 가능하면 네이티브 I/O 서비스를 이용하려 한다.

  • 파일 채널 객체는 Thread-safe 하다.

    • 같은 파일채널 인스턴스에 대해 여러 쓰레드들이 동시에 메서드를 호출해도 동기화 문제가 발생하지 않는다.

    • 이게 가능한 이유는 여러 쓰레드가 접근했을 때 만약 한 쓰레드가 파일 크기 또는 파일 채널의 포지션을 변경하는 부분을 수행하는 메서드를 호출하면 다른 쓰레드들은 해당 작업을 마무리할 때까지 기다렸다가 수행하기 때문이다.

파일 채널의 속성1 - 파일 락킹(File Locking)

  • 파일 락킹의 주요한 특징은 다음과 같다.

    • 파일 락킹은 채널이 아닌 파일을 대상으로 하는 것이다.

    • 동일한 JVM 내부의 여러 스레드 사이가 아닌 외부 프로세스 사이에서 파일의 접근을 제어하기 위함이다.

  • 파일 락킹의 경우에는 채널의 락이 걸렸을 경우, FileLock 객체가 리턴되며, 이 객체 내부의 메서드들을 통해서 공유 락인지 아닌지 (isShared()), 락을 해체할 것인지(release()) 등을 처리할 수 있다.

  • 파일 락킹 예시

    • 아래 코드를 보면 try-resource 문으로 처리하여, release() 를 명시적으로 선언안해줘도 알아서 release() 가 된다.

    • 코드 자체는 채널을 가져오고 락을 걸어서 공유락이지 판단하고 릴리즈까지 하는 일련의 예시 코드라고 볼 수 있다.

      public class FileLocking {
      
          public static void main(String[] args) {
              File file = new File("/Users/liquid.bear/Downloads/test.txt");
      
              try (FileChannel channel = new RandomAccessFile(file, "rw").getChannel()){
                  try (FileLock lock = channel.lock(0, Long.MAX_VALUE, true)) {
                      boolean isShared = lock.isShared();
                      System.out.println("Is Shared Lock? : " + isShared);
                  }
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }

파일 채널의 속성2 - MMIO(Memory-mapped I/O)

  • 파일 채널은 MMIO 를 지원한다. 추상 메서드인 map() 을 통해서 처리가 된다.

  • 이때 인자를 보면 MapMode 객체를 받는 것을 확인한 수 있는데, 이 객체는 3개의 상수값을 갖는다.

    1. READ_ONLY : 버퍼에서 읽기만 가능한 모드

    2. READ_WRITE : 버퍼에서 읽기와 쓰기 모두 가능한 모드

    3. PRIVATE : 읽기와 쓰기 둘다 가능하지만 쓰기를 할 경우 복사본을 만들어 변경 내역을 별도로 보관하여 원본 파일에는 적용되지 않는다.

  • 이렇게 map() 메서드를 통해서 MMIO 를 구현할 수 있다. 하지만, 주의할 점은 위에서 파일 락킹 같은 경우 release() 를 통해서, 해체되지만 MMIO 는 해제할 수 없고, 한번 생성되면 GC 가 발생할 때까지 남아있게 된다.

    • 이렇게 설계 된 이유는 보안문제와 성능문제 때문이라고 한다.

  • MMIO 예시 코드는 다음과 같다.

    • 어떤 파일의 버퍼를 만드는데 이때 빠른 I/O 를 처리하기 위해서 사용할 수 있다.

      ...
      private void initFileBuffer(int size, File file) throws FileNotFoundException {
          int bufferCount = size / FILE_BLOCK_SIZE;
          size = bufferCount * FILE_BLOCK_SIZE;
      
          try (RandomAccessFile fileData = new RandomAccessFile(file, "rw")) {
              fileData.setLength(size);
              ByteBuffer fileBuffer = fileData.getChannel()
                  .map(MapMode.READ_WRITE, 0L, size);
      
              divideBuffer(fileBuffer, FILE_BLOCK_SIZE, fileQueue);
      
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
      ...

파일 채널의 속성3 - 채널 간 직접 전송

  • 채널은 JVM 버퍼를 거쳐서 처리할 수도 있지만, 채널 사이에서 다이렉트로 데이터를 전송 할 수도 있다!

  • 이런 기능은 transTo(), transForm() 메서드를 통해서 가능하다.

  • 예를 들어, 파일을 네트워크 전송을 해야 하는 상황이라고 할 때, 파일 채널과 소켓 채널을 사용해 JVM 버퍼를 거치지 않고 사용할 수 있다. -> 중간에 JVM 버퍼를 거치지 않기 때문에, 속도 향상을 기대할 수 있다.

  • 예제 코드

package nio_copy;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class DirectTransterChannelIO extends MyTimer {
    private static final String DEST_PATH = "/Users/liquid.bear/Downloads/io_test_out.txt";
    public static void main(String[] args) throws IOException {
        start();
        copy();
        end("Channel TransferTo I/O");
    }

    public static void copy() throws IOException {
        try (FileInputStream fileInputStream = new FileInputStream(MyTimer.PATH);
            FileOutputStream fileOutputStream = new FileOutputStream(DEST_PATH);
            FileChannel fileInputChannel = fileInputStream.getChannel();
            FileChannel fileOutputChannel = fileOutputStream.getChannel()) {

            fileInputChannel.transferTo(0, fileInputChannel.size(), fileOutputChannel);
        }
    }
}

소켓 채널(SocketChannel)

  • 이제는 파일 채널과 양대산맥인 소켓 채널에 대해서 알아보자.

  • 소켓 채널은 파일 채널과 다르게 비교하여 몇 가지 다른 특징이 존재한다.

    • 논 블로킹 모드 지원

    • SelectableChannel 을 상속해서 Selector 와 함께 멀티플레스 I/O 가 가능하다.

  • 기존 소켓, I/O 를 통한 네트워크 프로그래밍의 문제점이 있다.

    • 블로킹 모드만 지원된다..

    • 이로 인해, 각 클라이언트 요청에 대해 하나의 스레드를 생성해야 하는 멀티스레드 모델을 사용하는 경우, 클라이언트가 많아질수록 생성해야 할 스레드 수가 기하급수적으로 증가합니다. 이는 자원(메모리, CPU 등)의 비효율적인 사용을 초래합니다.

    • 스레드가 많아질수록 운영체제는 스레드 간의 컨텍스트 스위칭을 수행해야 하며, 이는 CPU의 작업 효율성을 떨어뜨립니다. 컨텍스트 스위칭은 스레드의 상태를 저장하고 복원하는 과정으로, 이 과정에서 CPU 시간이 소모됩니다.

  • 하지만 논 블로킹 소켓 채널이 도입됨에 따라 멀리플렉스 I/O 를 지원하는 셀렉터가 도입되어 기존의 문제가 해결되었다.

  • 참고로, 소켓 채널은 별 다른 설정을 하지 않으면 기본적으로 블로킹 모드로 설정된다. 따라서 아래와 같이 논 블로킹 모드로 바꿔주어야 한다.

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("host ip", port));
socketChannel.configureBlocking(false);
  • 소케 채널을 사용한 간단한 논 블로킹 채팅 프로그램은 결론 부분에서 확인하자.

3. 셀렉터(Selector)

  • 하지만 간단하게 살펴보면, 이벤트 주도 아키텍처에서 리액터 패턴이라는 것이 존재하고, 셀렉터는 바로 이 리액터 패턴을 구성하는 요소 중에 리액터를 담당하는 놈이라고 이해하면 된다.

    • 즉, 여러 채널의 셀렉션 키를 자신에게 등록하게 하고 등록된 채널의 이벤트 요청들을 나누어서 적절한 서비스 제공자에게 보내 처리하는 것이다 .

    • 이를 통해 I/O 멀티플렉싱을 가능하게 한다.

3-1. 기존의 네트워크 프로그래밍(ThreadPool) 모델의 단점

  • 스레드 풀과 같은 기존 모델은 단순하고 널리 사용되었지만, 대규모 네트워크 서버나 실시간 처리가 필요한 애플리에키션 환경에서 몇가지 한계를 드러냈다.

  • 특히 클라이언트가 늘어나면서 스레드 풀 방식의 문제점이 더 두드러졌고, 이를 보완하기 위해서 Selector 와 같은 비동기 논블로킹 기술이 등장하게 되었다.

스레드 풀 모델의 단점

  1. 클라이언트 수 증가에 따른 스레드 수 증가

    1. 스레드 풀은 요청마다 스레드를 할당하여 처리하는데, 클라이언트가 많아지면 스레드 수가 그에 비례해 증가한다.

    2. 스레드가 많아질수록 메모리 사용량은 늘고, 스레드 간 컨텍스트 스위칭이 빈번해지면서 CPU 오버헤드가 증가한다.

    3. 수천, 수만 개의 동시 연결이 필요한 대규모 서버에서는 이 방식으로는 효율적으로 확장하기가 어렵다 ..

  2. 스레드가 비효율적으로 대기하는 구조

    1. 블로킹 I/O 사용할 때 스레드는 I/O 작업이 완료될 때까지 대기 상태로 전환된다. 응답이 느리거나, 지연이 발생하면 해당 스레드는 응답 대기 동안 다른 작업을 할 수 없게 되며, 이는 다중 클라이언트 환경에서 비효율 적이다.

    2. 대기 상태일 때는 CPU 자원을 반납하고, 대기가 풀리면 다시 CPU 자원을 할당 받아 사용한다.

  • 이러한 단점을 해결하기 위해서 I/O 멀티플렉싱 모델이 탄생했다!

이를 해결하게 위해 I/O 멀티플렉싱 모델이 탄생하게 되었다!

3-2. 논 블로킹 모델과 셀렉터 동작 원리

  • I/O 멀티플렉싱 모델의 핵심적인 기능은 크게 세가지로 볼 수 있다.

  1. 셀렉터(Selector) : 리액터 패턴에서 리액터 역할을 해주는 객체

  2. 셀렉터블채널(SelectableChannel) : 셀렉터에 등록할 수 있는 채널들은 이 클래스를 상속받는다. 우리가 볼 예제는 소켓 채널 클래스이므로, 셀렉터에 등록할 수 있다.

  3. 셀렉션키(SelectionKey) : 특정 채널과 셀렉터 사이에서 해당 이벤트에 대한 내용에 정보를 들고 있는데, 이 값을 토대로 이벤트 요청을 처리한다.

  • 위 내용을 기반으로 전체적인 흐름을 보자면 다음과 같다.

  1. 채널을 셀렉터에 등록하면 이 등록에 관련된 채널과 셀렉터와 연관 정보를 갖는 셀렉션키가 셀렉터에 저장되고 리턴된다.

  2. 위의 셀렉션키를 토대로 어떤 채널이 자신이 등록한 모드에 대해 동작할 준비가 되면 셀렉션키는 그 준비상태를 내부적으로 저장한다.

  3. 소켓 서버의 예시를 들자면

    1. 클라이언트를 accept 할 준비가 되면 셀렉션키는 준비상태가 된 것이고,

    2. 이 때 셀렉터가 select() 메서드를 호출해서 자신에게 등록된 모든 셀렉션키의 상태를 체크하여

    3. 상태가 준비상태라면 하나씩 순서대로 꺼내서 요청한 이벤트에 대해서 적절하게 처리한다.

  • 이제 이 동작을 기반으로 하나씩 살펴보자.

3-2-1. SelectobleChannel

  • 위에서 이 클래스를 상속받은 클래스만이 셀렉터에 등록될 수 있다고 하였다.

  • 우리가 살펴볼 SelectableChannel 의 기능은 크기 2가지이다.

    • 첫번째, 소켓채널에서 본 논블로킹 모드 활성화 기능 (해당 기능은 소켓 채널에서 다루었다)

    • 두번째, 어떻게 셀렉터에 등록하는가?

  • 아래 register() 메서드를 통해서 채널을 셀렉터에등록할 수 있다. (세번째 인자인 Object att 는 셀렉션키에서 설명하겠다)

  • 여기서 ops 는 이벤트의 모드라고 볼 수 있다. 셀렉터에 등록할 수 있는 이벤트 모드들은 4가지가 있다. (해당 이벤트들은 상수로 등록되어 있다)

    1. OP_READ : 서버가 클라이언트의 요청을 read 할 수 있을 때 발생하는 이벤트

    2. OP_WRITE : 서버가 클라이언트의 응답을 write 할 수 있을 때 발생하는 이벤트

    3. OP_CONNECT : 서버가 클라이언트의 접속을 허락했을 때 발생하는 이벤트

    4. OP_ACCEPT : 클라이언트가 서버에 접속했을 때 발생하는 이벤트

  • 아래와 같은 코드로 채널을 셀렉터에 등록할 수 있다 .

// 셀렉터 생성
Selector selector = Selector.open();
// 서버소켓 채널 생성
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false); // 논블록킹 모드 활성화

ServerSocket socket = server.socket();
SocketAddress addr = new InetSocketAddress(port);
socket.bind(addr); // 소켓 생성 후 해당 주소에 바인드

// 셀렉터에 생성된 ServerSocketChannel과 ACCEPT 이벤트 등록
server.register(selector, SelectionKey.OP_ACCEPT); 
  • 각 채널 구현체마다 등록될 수 있는 이벤트는 다른데 아래와 같다.

채널 구현체
등록할 수 있는 이벤트

ServerSocketChannel

OP_ACCEPT

SocketChannel

OP_CONNECT, OP_READ, OP_WRITE

DatagramChannel

OP_READ, OP_WRITE

Pipe.SourceChannel

OP_READ

Pipe.SinkChannel

OP_WRITE

  • 여러 개의 이벤트를 등록할 수 있는 채널은 아래와 같이 여러개의 이벤트도 등록할 수 있으며, 하나의 셀렉터에 여러개의 채널도 등록할 수 있다.

Selector selector = Selector.open();

SocketChannel channel1 = SocketChannel.open();
channel1.configureBlocking(false);
SocketChannel channel2 = SocketChannel.open();
channel2.configureBlocking(false);
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);

server.register(selector, SelectionKey.OP_ACCEPT);
channel1.register(selector, SelectionKey.OP_READ);
channel2.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

Set<SelectionKey> keys = selector.keys();

for (SelectionKey key : keys) {
    System.out.println(key.channel().getClass() + " " + key.interestOps());
}
  • 그림으로 보면 다음과 같을 것이다.

    • 셀렉터는 이렇게 이벤트가 발생한 채널들만 선택해서 각 이벤트에 맞는 동작을 하도록 모든 이벤트들에 대한 컨트롤러 역할을 한다.

3-2-2. SelectionKey

  • 어떤 채널이 어떤 셀렉터에, 어떤 이벤트 모드로 등록되었는지, 그 등록한 이벤트를 수행할 준비가 되었는지에 대한 정보들을 담고 있는 객체이다.

    • 즉, 이벤트 처리에 대해서 셀렉터와 채널 사이에서 도와주는 역할을 하는 객체이다.

  • 셀렉션 키에는 크게 두가지 집합이 존재한다.

    • interest set

      • 위에서 여러 채널과 이벤트를 등록하는 예시 코드 내 key.interestOps() 라는 메서드가 존재한다. 이 정보들은 register() 할 때 등록했던 상수들 값이다.

      • 따라서 interest set 은 셀렉터에 등록한 이벤트 정보를 담고있는 집합이다.

    • ready set

      • ready set 은 채널에서 이벤트가 발생하면 그 이벤트들을 저장하는 집합이다.

  • 즉, 셀렉션키는 interest set, ready set 을 활용하여 이벤트 헨들링을 도와주는 역할을 한다.

  • register() 메서드의 세번째 인자인 att 는 셀렉션키에 참조할 객체를 추가하는 메서드이고, 해당 키에 참조할 객체가 있다면 그 객체를 리턴하고, 없다면 null 을 리턴한다.

  • 셀렉션키에 참조된 객체는 attachement() 메서드로 가져올 수 있으며, register() 로 등록이 가능하지만, attach() 메서드로도 등록할 수 있다.

    • attach() 메서드로 등록된 객체는 SelectionKey 가 참조하고 있기 때문에, GC 대상이 되지 않는다. 때문에, SelectionKey 가 삭제되기 전에 명시적으로 SelectionKey.attach(null) 또는 SelectionKey.cancel() 을 호출하여 참조를 해제해야 한다.

    • 그렇게 해야만 메모리 누수가 발생하지 않는다.

3-2-3. Selector

  • 셀렉터는 위에서 언급한 바와 같이 등로된 채널들이 발생시킨 이벤트에 대해 적절한 처리 핸들러로 요청을 분기해주는 컨트롤러 역할을 한다.

  • 위에서 셀렉터에 대한 내용은 많이 언급했으니 중요한 특징만 가지고 설명하고자 한다.

  • 셀렉터 또한 등록된 이벤트를 처리하기 위해서는 자신에게 등록된 채널과 연관된 셀렉션키에 대해서 알고 있어야 한다.

  • 그러므로 셀렉터 내부에는 셀렉션키에 대한 집합을 가지고 있다. 이 집합은 크게 3가지이며, 셀렉터 내부에는 아래의 집합들을 관리한다.

    • 등록된 키 집합(Registered Key Set)

      • 셀렉터에 등록된 모든 셀렉션키의 집합이다. 하지만 이 집합에 있는 모든 키가 유효하지는 않다.

      • 메서드 : Selector.keys()

    • 선택된 키 집합(Selected Key Set)

      • 등록된 키 집합 내 포함되어 있다.

      • 셀렉션키가 수행 준비상태가 되어서 ready set(이벤트가 발생) 이 비어있지 않은 키들이 Selector.select() 메서드에 호출되어서 선택되었을 때 이 집합에 추가된다.

    • 취소된 키 집합(Cancelled Key Set)

      • 등록된 키 집합 내 포함되어 있다.

      • 등록을 해제하고 싶을 때 SelectionKey.cancel() 메서드로 등록을 취소할 수 있는데, 이 키는 바로 유효하지 않은 키로 설정되고 취소된 키 집합에 추가된다.

  • 주의 사항으로 셀렉터는 스레드 세이프하지만, 세 가지 키 집합은 스레드 세이프 하지 않으므로, 멀티스레드 환경에서는 반드시 동기화처리를 해주어야 한다.

  • 이제 셀렉터의 동작 원리에 대해서 살펴보자. 셀렉터는 select(), poll() 과 같은 시스템 콜을 래핑한 것이다.

    • 실제 사용 방식은 select() 메서드를 호출하면서 사용되는데 내부적으로 아래와 같은 방식으로 동작한다.

    • 아래 1~3 동작과정을 반복하면서 진행하는데, 그 실행 시점과 블로킹 여부만 차이가 있다.

  1. 취소된 키 집합을 체크한다.

    • 만약 집합이 비어 있지 않다면

      • 이 집합에 저장된 각각의 키들은 셀렉터가 관리하는 세가지 집합에서 모두 삭제되어 각 키와 연관된 채널이 셀렉터에서 등록이 해제된다.

  2. 등록된 키 집합을 체크한다.

    • 만약 ready set이 비어있지 않은 셀렉션키가 존재한다면

      • 등록된 키 집합에 넣는다. (이미 존재한다면 그 키를 업데이트 처리만 한다.)

  3. 셀렉터가 selectedKeys() 메서드를 호출한다.

    • 저장된 선택된 키 집합을 가져오고, 그 안에 저장된 셀렉션키의 이벤트 형식에 따라 적절한 핸들러에게 처리를 넘긴다.

  • 셀렉터가 제공하는 select 함수는 총 세가지이다.

  1. select()

    • 블록킹되는 메서드이며, 선택된 키 집합이 비어있다면 키가 추가될 때까지 블록킹 된다. 그러다가 사용할 수 있는 키가 추가되면 ① ~ ③을 실행한다.

  2. select(long timeout)

    • 밀리세컨드마다 select() 함수와 동일하게 처리된다. 따라서, 해당 시간마다 블록킹이 된다.

  3. selectNow()

    • 논블록킹 메서드이다. 따라서 이용 가능한 채널이 없으면 0을 리턴하고, 아니면 마찬가지로 등록된 키 집합안에 들어있는 셀렉션키의 개수를 리턴한다.

  • 여기서 추가로, wakeup() 메서드는 스레드가 블로킹 되어 있는 경우 이 블로킹 된 스레드를 깨우는데 사용한다.

4. I/O 모델 및 간단한 블로킹IO & NIO 예제

4-1. I/O 모델

  • IO / NIO 를 다루면서 자주 했던 말이 블로킹과 논 블로킹이다.

  • 이것들은 I/O 모델이라는 개념에 속해있다. 이번 포스팅에서는 4가지 I/O 모델을 다루어보자.

    • 블로킹(Blocking) I/O && 동기(Synchronous) I/O 모델

    • 논 블로킹(Non-Blocking) I/O 모델

    • 비동기(Asynchronous) I/O 모델

    • I/O 다중화(Multiplexing) 모델

4-1-1. 블로킹(Blocking) I/O && 동기(Synchronous) I/O 모델

  • 위 그림을 보면 어플리케이션은 커널에서 응답이 올 때까지 블로킹된다. (다른 작업은 하지 못하고 waiting 상태가 된다)

  • 당연히 우리의 똑똑한 선배님들은 이러한 응답을 대기하는 대기시간이 발생하기 때문에, 이 시간을 줄일 수 없을까? 고민을 하게 되었고, 그렇게 나온 I/O 모델이 논 블로킹 I/O 모델이다.

4-1-2. 논 블로킹(Non-Blocking) I/O 모델

  • 논 블로킹 모델은 그림과 같이 시스템 콜이 발생한 뒤에 응답이 끝날때까지 기다리는 것이 아니라, 제어권을 어플리케이션이 가지고 있다.

  • 그렇다면 비동기 통신이랑은 무엇이 다른 것일까?

4-1-3. 비동기(Asynchronous) I/O 모델

  • 가장 큰 차이점은 논 블로킹 I/O 모델처럼 주기적으로 처리 여부를 응답하는 것이 아니라, 커널에 시스템 콜을 한 뒤 어플리케이션은 다른 일을 하다가 커널이 콜백으로 완료 여부를 알려준다.

    • 즉, I/O 처리가 완료된 타이밍에 결과를 회신하는 모델이다.

  • 차이점을 정리하자면 비동기 I/O 모델은 완료 했을 때 통지를 하지만, 논 블로킹 I/O 모델은 처리가 가능한 상태를 판단하면서 처리한다.

  • 여기서 논 블로킹 I/O 의 단점을 생각해 볼 수 있는데, 시스템 콜이 계속해서 발생할 수 있다는 단점이 있다..

  • 이러한 단점을 해결하기 위해서, 이벤트 등록(필요한 시점에만 물어보게끔 하는 것이다) 을 통해 이를 처리하는 방법을 고안하였고, 이 방법이 I/O 다중화 모델이다!

4-1-4. I/O 다중화(Multiplexing) 모델

  • select 시스템 콜은 Selector.select() 라고 볼 수 있을 것이며, data ready 부분은 셀렉션키의 ready set 이 존재하는 경우이다.

  • 즉, 우리가 공부한 NIO 는 I/O 다중화 모델을 구현할 수 있는 객체들이다. 이러한 개념들을 출발하여 오늘날 I/O 모델의 중심이라 볼 수 있는 이벤트 주도 아키텍처 등이 탄생했다고 볼 수 있다.

4-2. 간단한 블로킹IO & NIO 예제

4-2-1. 블로킹 IO (전통적인 방식)

  • 블로킹 IO 에서 입출력 작업이 완료될 때까지 스레드를 기다린다. 이 방식은 동기적으로, 각 요청에 대해 스레드가 하나씩 할당되어 작업을 완료할 때가지 해당 스레드가 블로킹 상태로 유지된다.

  • 요청이 많아질 경우 많은 스레드를 생성해야 하며, 컨텍스트 스위칭 비용이 증가하여 성능에 영향을 끼칠 수 있다.

import java.io.*;
import java.net.*;

public class BlockingIOServer {
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("Blocking I/O Server started on port 8080");
            while (true) {
                Socket clientSocket = serverSocket.accept(); // 클라이언트가 연결될 때까지 대기
                handleRequest(clientSocket); // 클라이언트 요청 처리
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleRequest(Socket clientSocket) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
             PrintWriter writer = new PrintWriter(clientSocket.getOutputStream(), true)) {

            String request = reader.readLine(); // 요청이 들어올 때까지 블로킹
            System.out.println("Received: " + request);
            writer.println("Echo: " + request); // 응답 전송
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 블로킹 IO 동작

    • serverSocket.accept() 가 호출되면 클라이언트의 연결 요청이 들어올 때까지 스레드가 블로킹된다.

    • clientSocket.getInputStream().readLine() 도 클라이언트가 데이터를 보낼 때까지 블로킹된다.

    • 즉, 모든 입출력 작업은 완료될 때까지 스레드가 대기 상태에 있어야 한다.

4-2-2. NIO

  • NIO 에서는 하나의 스레드로 여러 채널을 관리할 수 있다.

  • 각 채널은 논 블로킹 모드로 동작하여 준비된 작업만 처리한다.

  • 이를 위해서 Selector 를 이용해 이벤트가 발생한 채널을 감지하고 작업을 수행한다.

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NonBlockingIOServer {
    public static void main(String[] args) {
        try (Selector selector = Selector.open();
             ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
            
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false); // 논블로킹 모드로 설정
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("Non-blocking I/O Server started on port 8080");
            
            while (true) {
                selector.select(); // 이벤트 발생할 때까지 대기
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
                
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();
                    
                    if (key.isAcceptable()) {
                        // 새로운 연결 수락
                        ServerSocketChannel server = (ServerSocketChannel) key.channel();
                        SocketChannel client = server.accept();
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_READ);
                        System.out.println("Accepted connection from client");
                    } else if (key.isReadable()) {
                        // 데이터 읽기
                        SocketChannel client = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(256);
                        int bytesRead = client.read(buffer);
                        
                        if (bytesRead == -1) {
                            client.close();
                        } else {
                            buffer.flip();
                            client.write(buffer); // Echo back to client
                            buffer.clear();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 논 블로킹 IO 동작

    • selector.select() 메서드를 하나 이상의 채널에 이벤트가 발생할 때까지 대기한다. 이 호출은 블로킹 상태이지만, 이벤트가 발생한 채널의 작업만 처리하므로, CPU 와 메모리를 효율적으로 사용할 수 있다.

    • SelectionKey.OP_ACCEPT 를 통해 클라이언트 연결이 수락되었을 때 이벤트를 받고,

    • SelectionKey.OP_READ 로 읽기 가능 상태인 채널에서 데이터를 읽는다.

    • SocketChannel.configureBlocking(false) 로 설정했기 때문에, IO 작업을 수행할 때도 스레드가 블로킹 되지 않으며 다른 작업을 처리할 수 있다.

  • I/O 멀티플렉싱을 위한 핵심적인 기능들을 크게 세가지로 볼 수 있다.

  1. 셀렉터(Selector) : 리액터 패턴에서 리액터 역할을 해주는 객체

  2. 셀렉터블채널(SelectableChannel) : 셀러터에 등록할 수 있는 채널들은 이 클래스를 상속받는다. 우리가 볼 예제는 소켓 채널 클리스이므로, 셀렉터에 등록할 수 있다.

  3. 셀렉션키(SelectionKey) : 특정 채널과 셀렉터 사이에서 해당 이벤트에 대한 내용에 대한 정보를 들고 있는다. 이 값을 토대로 이벤트 요청을 처리한다.

  • 위 내용을 토대로 전체적인 흐름을 보자면

  1. 채널을 셀렉터에 등록하면 이 등록에 관련된 채널과 셀렉터와 연관 정보를 갖고 있는 셀렉션키가 셀렉터에 저장되고, 리턴된다.

  2. 위의 셀렉션키를 토대로 어떤 채널이 자신이 등록한 모드에 대해 동작할 준비가 되면 셀렉션키는 그 준비상태를 내부적으로 저장한다.

  3. 소켓 서버의 예시를 들자면 클라이언트 accept 할 준비가 되면 셀렉션키는 준비 상태가 된 것이고, 이 때 셀렉터가 select() 메서드를 호출해서 자신에게 등록된 모든 셀렉션키를 검사하여 준비상태면, 하나씩 순서대로 꺼내서 요청한 이벤트에 대해 적절하게 처리한다.

우리가 셀렉터를 보기 전에 와 을 한번 쯤 볼 필요가 있다.

이벤트 주도 아키텍처
리엑터 패턴
https://brewagebear.github.io/fundamental-nio-and-io-models
https://jenkov.com/tutorials/java-nio/buffers.html
https://medium.com/dtevangelist/event-driven-microservice-%EB%9E%80-54b4eaf7cc4a
Java NIO Buffer