🐾
개발자국
  • 🐶ABOUT
  • 🚲프로그래밍
    • 객체 지향 프로그래밍
    • 오브젝트
      • 1장: 객체, 설계
      • 2장: 객체지향 프로그래밍
      • 3장: 역할, 책임, 협력
      • 4장: 설계 품질과 트레이드오프
      • 5장: 책임 할당하기
      • 6장: 메시지와 인터페이스
      • 7장: 객체 분해
      • 8장: 의존성 관리하기
      • 9장: 유연한 설계
      • 10장: 상속과 코드 재사용
      • 11장: 합성과 유연한 설계
      • 12장: 다형성
      • 13장: 서브클래싱과 서브타이핑
      • 14장: 일관성 있는 협력
      • 15장: 디자인 패턴과 프레임워크
    • 도메인 주도 개발 시작하기
      • 1장: 도메인 모델 시작하기
      • 2장: 아키텍처 개요
      • 3장: 애그리거트
      • 4장: 리포지토리와 모델 구현
      • 5장: 스프링 데이터 JPA를 이용한 조회 기능
      • 6장: 응용 서비스와 표현 영역
      • 7장: 도메인 서비스
      • 8장: 애그리거트 트랜잭션 관리
      • 9장: 도메인 모델과 바운디드 컨텍스트
      • 10장: 이벤트
      • 11장: CQRS
    • 클린 아키텍처
      • 만들면서 배우는 클린 아키텍처
        • 계층형 아키텍처의 문제와 의존성 역전
        • 유스케이스
        • 웹 어댑터
        • 영속성 어댑터
        • 아키텍처 요소 테스트
        • 경계 간 매핑 전략
        • 애플리케이션 조립
        • 아키텍처 경계 강제하기
        • 지름길 사용하기
        • 아키텍처 스타일 결정하기
    • 디자인 패턴
      • 생성(Creational) 패턴
        • 팩토리 패턴
        • 싱글톤 패턴
        • 빌더 패턴
        • 프로토타입 패턴
      • 행동(Behavioral) 패턴
        • 전략 패턴
        • 옵저버 패턴
        • 커맨드 패턴
        • 템플릿 메서드 패턴
        • 반복자 패턴
        • 상태 패턴
        • 책임 연쇄 패턴
        • 인터프리터 패턴
        • 중재자 패턴
        • 메멘토 패턴
        • 비지터 패턴
      • 구조(Structural) 패턴
        • 데코레이터 패턴
        • 어댑터 패턴
        • 퍼사드 패턴
        • 컴포지트 패턴
        • 프록시 패턴
        • 브리지 패턴
        • 플라이웨이트 패턴
      • 복합 패턴
  • 시스템 설계
    • 1. 사용자 수에 따른 규모 확장성
    • 2. 개략적 규모 추정
    • 3. 시스템 설계 접근법
    • 4. 처리율 제한 장치
    • 5. 안정 해시
    • 6. 키-값 저장소
    • 7. 유일한 ID 생성기
    • 8. URL 단축기
    • 9. 웹 크롤러
    • 10. 알림 시스템
    • 11. 뉴스 피드
    • 12. 채팅 시스템
    • 13. 검색어 자동완성
    • 14. 유튜브 스트리밍
    • 15. 구글 드라이브
    • ⭐️. 캐싱 전략
    • ⭐️. 재고 시스템으로 알아보는 동시성이슈 해결방법
    • ⭐️. 실습으로 배우는 선착순 이벤트 시스템
  • 🏝️자바
    • 자바의 내부 속으로
      • Java 언어의 특징
      • JDK
      • JVM
        • 메모리 관리
        • Garbage Collector
          • 기본 동작
          • Heap 영역을 제외한 GC 처리 영역
          • (WIP) GC 알고리즘
        • 클래스 로더
      • 자바 실행 방식
      • 메모리 모델과 관리
      • 바이트 코드 조작
      • 리플렉션
      • 다이나믹 프록시
      • 어노테이션 프로세서
    • 자바의 기본
      • 데이터 타입, 변수, 배열
    • 이펙티브 자바
      • 2장: 객체의 생성과 파괴
        • item 1) 생성자 대신 정적 팩토리 메서드를 고려하라
        • item2) 생성자에 매개변수가 많다면 빌더를 고려하라
        • item3) private 생성자나 열거 타입으로 싱글톤임을 보증하라
        • item4) 인스턴스화를 막으려면 private 생성자를 사용
        • item5) 자원을 직접 명시하는 대신 의존 객체 주입 사용
        • item6) 불필요한 객체 생성 지양
        • item7) 다 쓴 객체는 참조 해제하라
        • item8) finalizer와 cleaner 사용 자제
        • item9) try-with-resources를 사용하자
      • 3장: 모든 객체의 공통 메서드
        • item 10) equals는 일반 규약을 지켜 재정의 하자
        • item 11) equals 재정의 시 hashCode도 재정의하라
        • item 12) 항상 toString을 재정의할 것
        • item 13) clone 재정의는 주의해서 진행하라
        • item 14) Comparable 구현을 고려하라
      • 4장: 클래스와 인터페이스
        • item 15) 클래스와 멤버의 접근 권한을 최소화하라
        • item 16) public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라
        • item 17) 변경 가능성을 최소화하라
        • item 18) 상속보다는 컴포지션을 사용하라
        • item 19) 상속을 고려해 설계하고 문서화하고, 그러지 않았다면 상속을 금지하라
        • item 20) 추상 클래스보다는 인터페이스를 우선하라
        • item 21) 인터페이스는 구현하는 쪽을 생각해 설계하라
        • item 22) 인터페이스는 타입을 정의하는 용도로만 사용하라
        • item 23) 태그 달린 클래스보다는 클래스 계층구조를 활용하라
        • item 24) 멤버 클래스는 되도록 static으로 만들라
        • item 25) 톱레벨 클래스는 한 파일에 하나만 담으라
      • 5장: 제네릭
        • item 26) 로 타입은 사용하지 말 것
        • item 27) unchecked 경고를 제거하라
        • item 28) 배열보다 리스트를 사용하라
        • item 29) 이왕이면 제네릭 타입으로 만들라
        • item 30) 이왕이면 제네릭 메서드로 만들라
        • item 31) 한정적 와일드카드를 사용해 API 유연성을 높이라
        • item 32) 제네릭과 가변 인수를 함께 사용
        • item 33) 타입 안전 이종 컨테이너를 고려하라
      • 6장: 열거 타입과 어노테이션
        • item 34) int 상수 대신 열거 타입을 사용하라
        • item 35) ordinal 메서드 대신 인스턴스 필드를 사용하라
        • item 36) 비트 필드 대신 EnumSet을 사용하라
        • item 37) ordinal 인덱싱 대신 EnumMap을 사용하라
        • item 38) 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
        • item 39) 명명 패턴보다 어노테이션을 사용하라
        • item 40) @Override 어노테이션을 일관되게 사용하라
        • item 41) 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
      • 7장: 람다와 스트림
        • item 42) 익명 클래스보다는 람다를 사용하라
        • item 43) 람다보다는 메서드 참조를 사용하라
        • item 44) 표준 함수형 인터페이스를 사용하라
        • item 45) 스트림은 주의해서 사용하라
        • item 46) 스트림에서는 부작용 없는 함수를 사용하라
        • item 47) 반환 타입으로는 스트림보다 컬렉션이 낫다
        • item 48) 스트림 병렬화는 주의해서 적용하라
      • 8장: 메서드
        • item 49) 매개변수가 유효한지 검사하라
        • item 50) 적시에 방어적 복사본을 만들라
        • item 51) 메서드 시그니처를 신중히 설계하라
        • item 52) 다중정의는 신중히 사용하라
        • item 53) 가변인수는 신중히 사용하라
        • item 54) null이 아닌, 빈 컬렉션이나 배열을 반환하라
        • item 55) 옵셔널 반환은 신중히 하라
        • item 56) 공개된 API 요소에는 항상 문서화 주석을 작성하라
      • 9장: 일반적인 프로그래밍 원칙
        • item 57) 지역 변수의 범위를 최소화하라
        • item 58) 전통적인 for문보다 for-each문을 사용하기
        • item 59) 라이브러리를 익히고 사용하라
        • item 60) 정확한 답이 필요하다면 float, double은 피하라
        • item 61) 박싱된 기본타입보단 기본 타입을 사용하라
        • item 62) 다른 타입이 적절하다면 문자열 사용을 피하라
        • item 63) 문자열 연결은 느리니 주의하라
        • item 64) 객체는 인터페이스를 사용해 참조하라
        • item 65) 리플렉션보단 인터페이스를 사용
        • item 66) 네이티브 메서드는 신중히 사용하라
        • item 67) 최적화는 신중히 하라
        • item 68) 일반적으로 통용되는 명명 규칙을 따르라
      • 10장: 예외
        • item 69) 예외는 진짜 예외 상황에만 사용하라
        • item 70) 복구할 수 있는 상황에서는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
        • item 71) 필요 없는 검사 예외 사용은 피하라
        • item 72) 표준 예외를 사용하라
        • item 73) 추상화 수준에 맞는 예외를 던지라
        • item 74) 메서드가 던지는 모든 예외를 문서화하라
        • item 75) 예외의 상세 메시지에 실패 관련 정보를 담으라
        • item 76) 가능한 한 실패 원자적으로 만들라
        • item 77) 예외를 무시하지 말라
      • 11장: 동시성
        • item 78) 공유 중인 가변 데이터는 동기화해 사용하라
        • item 79) 과도한 동기화는 피하라
        • item 80) 스레드보다는 실행자, 태스크, 스트림을 애용하라
        • item 81) wait와 notify보다는 동시성 유틸리티를 애용하라
        • item 82) 스레드 안전성 수준을 문서화하라
        • item 83) 지연 초기화는 신중히 사용하라
        • item 84) 프로그램의 동작을 스레드 스케줄러에 기대지 말라
      • 12장: 직렬화
        • item 85) 자바 직렬화의 대안을 찾으라
        • item 86) Serializable을 구현할지는 신중히 결정하라
        • item 87) 커스텀 직렬화 형태를 고려해보라
        • item 88) readObject 메서드는 방어적으로 작성하라
        • item 89) 인스턴스 수를 통제해야 한다면 readResolve보다는 열거 타입을 사용하라
        • item 90) 직렬화된 인스턴스 대신 직렬화 프록시 사용을 검토하라
    • 모던 자바 인 액션
      • 1장: 자바의 역사
      • 2장: 동작 파라미터화
      • 3장: 람다
      • 4장: 스트림
      • 5장: 스트림 활용
      • 6장: 스트림으로 데이터 수집
      • 7장: 병렬 데이터 처리와 성능
      • 8장: 컬렉션 API 개선
      • 9장: 람다를 이용한 리팩토링, 테스팅, 디버깅
      • 10장: 람다를 이용한 DSL
      • 11장: null 대신 Optional
      • 12장: 날짜와 시간 API
      • 13장: 디폴트 메서드
      • 14장: 자바 모듈 시스템
      • 15장: CompletableFuture와 Reactive 개요
      • 16장: CompletableFuture
      • 17장: 리액티브 프로그래밍
      • 18장: 함수형 프로그래밍
      • 19장: 함수형 프로그래밍 기법
      • 20장: 스칼라 언어 살펴보기
    • 자바의 이모저모
      • Javax
      • Objects
      • NIO
      • Thread
      • Concurrent
        • Atomic
        • Executor, ExecutorService
        • Interrupt
      • Assertions
    • Netty
      • 네티 맛보기
      • 네티의 주요 특징
      • 채널 파이프라인
      • 이벤트 루프
      • 바이트 버퍼
      • 부트스트랩
      • 네티 테스트
      • 코덱
      • 다양한 ChannelHandler와 코덱
      • 웹소켓
      • UDP 브로드캐스팅
    • 자바 병렬 프로그래밍
      • 2장: 스레드 안전성
      • 15장: 단일 연산 변수와 논블로킹 동기화
  • 🏖️코틀린
    • 코틀린 인 액션
      • 코틀린 언어의 특징
      • 코틀린 기초
      • 함수 정의와 호출
      • 클래스, 객체, 인터페이스
      • 람다
      • 타입 시스템
      • 연산자 오버로딩과 기타 관례
      • 고차 함수
      • 제네릭스
      • 어노테이션과 리플렉션
      • DSL 만들기
  • 🌸스프링
    • Spring Core
      • Cron Expression
      • Bean
        • Lifecycle
        • Aware
    • Spring MVC
    • Spring Security
      • 로그인 처리
      • 로그아웃 처리
      • JWT 인증 방식
      • 메소드별 인가 처리
    • Spring Data
      • Pageable
      • Spring Data Couchbase
      • Spring Data Redis
        • Serializer
    • Spring REST Docs
    • Spring Annotations
    • Spring Cloud
      • Service Discovery
      • API Gateway
      • Spring Cloud Config
      • MicroService Communication
      • Data Synchronization
    • Test
      • 테스트 용어 정리
      • JUnit
      • Spring Boot Test
      • Mockito
    • QueryDSL
      • 프로젝트 환경설정
      • 기본 문법
      • 중급 문법
      • 순수 JPA와 QueryDSL
      • 스프링 데이터 JPA와 QueryDSL
    • Lombok
      • @Data
      • @Builder
      • Log Annotations
  • 🕋DB
    • MySQL
      • CentOS7에서 MySQL 8 버전 설치하기
    • MongoDB
      • 
    • Redis
      • Sentinel
      • Cluster
      • Transaction
      • 자료구조
        • String
        • List
        • Set
        • Hash
        • Bitmaps
        • SortedSet
      • Lettuce 단일 서버, 클러스터 서버, 풀링 사용 방법
  • 📽️인프라
    • 리눅스
      • 주요 명령어 모음
    • Docker
      • Docker
      • Docker Compose
      • Docker Swarm
      • Docker Network
      • Linux에서 root 아닌 유저로 docker 실행하기
    • Kubernetes
      • 기초 개념
      • Pod
      • Configuration
      • ReplicationSet
      • Network
      • ConfigMap & Secret
      • Volume, Mount, Claim
      • Controller
      • Multi Container Pod
      • StatefulSet & Job
      • Rollout & Rollback
      • Helm
      • 개발 워크플로우와 CI/CD
      • Container Probes
      • Resource Limit
      • Logging & Monitoring
      • Ingress
      • Security
      • Multi Node/Architecture Cluster
      • Workload & Pod management
      • CRD & Operator
      • Serverless Function
      • K8S Cheat Sheet
    • Kafka
      • 카프카 개요
      • 카프카 설치 및 실습
      • Kafka Broker
      • Topic, Partition, Record
      • Producer
      • Consumer
      • Kafka Streams
      • Kafka Connect
      • MirrorMaker
  • AWS
    • AWS Console / CLI / SDK
    • IAM
    • EC2
      • EC2 Advanced
    • ELB / ASG
    • RDS / Aurora / ElastiCache
    • DynamoDB
    • DocumentDB / Neptune / Keyspaces / QLDB / Timestream
    • Route 53
    • Beanstalk
    • Solution Architect
    • S3
      • 보안
    • CloudFront
    • Global Accelerator
    • AWS Storage
    • Messaging
    • Container
    • Serverless
    • Data Analysis
    • Machine Learning
    • Monitoring
    • Security
    • VPC
    • Data Migration
    • 기타 서비스
  • 🏔️CS
    • 운영 체제
      • Introduction
      • System Structures
      • Process
      • Synchronization
      • Muitithreaded Programming
      • Process Scheduling
      • Memory Management
      • Virtual Memory
    • 네트워크
      • 네트워크 기초
      • 네트워크 통신 방식
      • OSI 7계층
        • 1계층: 물리계층
        • 2계층: 데이터 링크 계층
        • 3계층: 네트워크 계층
        • 4계층: 전송 계층
        • 5계층: 세션 계층
        • 6계층: 표현 계층
        • 7계층: 응용 계층
      • TCP/IP 스택
      • ARP
      • 데이터 크기 조절
      • WDM
      • NAT
      • DNS
      • DHCP
      • VPN
      • 네이글 알고리즘
      • 서버 네트워크
      • 네트워크 보안
        • 보안의 기본
        • 보안 장비
      • 이중화
    • 데이터베이스
      • 트랜잭션
    • 컴퓨터 구조
      • 개요
      • Instruction Set Architecture
      • Procedure Call & Return
      • Linking
      • Pipeline
      • Memory Hierarchy
      • Virtual Memory
      • Interrupt / Exception, IO
    • 자료 구조
      • Array
      • List
      • Map
      • Set
      • Queue
      • PriorityQueue
      • Stack
    • 웹 기술
      • HTTP
        • 쿠키와 세션
  • 🪂Big Data
    • Apache Hadoop
  • 🕹️ETC
    • Git
      • 내부 구조
      • 내가 자주 사용하는 명령어 모음
      • Commit Convention
    • 이력서 작성하기
    • Embedded
      • 라즈베리파이에서 네오픽셀 적용기
    • 기술블로그 모음집
Powered by GitBook
On this page
  • 이벤트 실행
  • 채널 파이프라인
  • 구조
  • 동작
  • 이벤트 핸들러
  • 인바운드 이벤트
  • 인바운드 이벤트 발생 순서
  • ChannelInboundHandler
  • SimpleChannelInboundHandler
  • 아웃바운드 이벤트
  • ChannelOutboundHandler
  • 다중 이벤트 핸들러 처리
  • ChannelHandlerAdapter
  • 메모리 관리
  • ChannelPipeline
  • ChannelHandlerContext
  • write 호출의 차이
  • 참조 캐싱 방식
  • 공유 가능한 ChannelHandler
  • 예외 처리
  • 인바운드 예외 처리
  • 아웃바운드 예외 처리
  1. 자바
  2. Netty

채널 파이프라인

채널에서 발생한 이벤트가 이동하는 채널 파이프라인에 대해 알아본다.

이벤트 실행

  • 네티 없이 일반 서버 네트워크 프로그램을 작성 시 아래와 같이 동작한다.

  1. 소켓에 데이터가 있는지 확인

  2. 데이터가 존재하면 데이터 읽는 메서드 호출 / 데이터가 존재하지 않으면 대기

  3. 대기 중 네트워크가 끊어지면 에러 처리 메서드 호출

  • 네티를 사용하면 이벤트를 채널 파이프라인과 이벤트 핸들러로 추상화하여 데이터가 수신되었는지 확인하거나 소켓과 연결이 끊겼는지 직접 메서드 호출을 하지 않아도 된다.

  • 대신 이벤트가 발생했을 때 호출할 메서드만 구현해두면 된다.

  • 네티를 사용해 서버 네트워크 프로그램을 작성 시 아래와 같이 동작한다.

  1. 부트스트랩으로 네트워크 애플리케이션에 필요한 설정 지정 및 이벤트 핸들러로 채널 파이프라인 구성

  2. 이벤트 핸들러의 데이터 수신 이벤트 메서드에서 데이터를 읽는다.

  3. 이벤트 핸들러의 네트워크 끊김 이벤트 메서드에서 에러 처리를 한다.

  • 소켓 채널에 데이터가 수신되었을 때 네티는 아래와 같이 수신 이벤트 메서드를 실행한다.

  1. 이벤트 루프는 채널 파이프라인에 등록된 첫 이벤트 핸들러를 가져온다.

  2. 이벤트 핸들러에 데이터 수신 이벤트 메서드가 구현되어 있다면 실행하고, 구현되어 있지 않으면 다음 이벤트 핸들러를 가져온다.

  3. 2번 과정을 마지막 핸들러에 도달할 때까지 반복한다.

채널 파이프라인

구조

  • 네티의 채널, 이벤트, 이벤트 핸들러는 전기의 흐름과 비슷하다.

  • 채널은 일반적인 소켓 프로그래밍에서 말하는 소켓과 같다. (발전소)

  • 이 소켓에서 발생한 이벤트는 채널 파이프라인을 따라 흐른다. (전선 / 멀티탭)

  • 이벤트 핸들러는 채널에서 발생한 이벤트들을 수신하고 처리한다. (가전제품)

  • 하나의 채널 파이프라인에는 여러 이벤트 핸들러를 등록할 수 있다.

동작

  • childHandler를 통해 클라이언트가 사용할 채널 파이프라인을 설정한다.

  • ChannelInitializer 인터페이스의 initChannel 메서드는 클라이언트 소켓 채널이 생성될 때 실행된다.

  • 클라이언트 소켓 채널을 생성할 때에는 빈 채널 파인프라인 객체를 생성해 할당한다.

  • 이 때 채널 파이프라인에는 이벤트 핸들러를 여러 개 등록할 수 있다.

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
        @Override
        public void initChannel(SocketChannel ch) {
            // 채널 파이프라인 설정
            ChannelPipeline p = ch.pipeline();
            p.addLast(new EchoServerHandler());
        }
    });
  • 채널 파이프라인이 초기화되는 순서를 그림으로 살펴보면 아래와 같다.

  • 먼저 클라이언트 연결에 대응되는 소켓 채널 객체를 생성하고, 빈 채널 파이프라인 객체를 생성해 소켓 채널에 할당한다.

  • 소켓 채널에 등록된 ChannelInitializer 구현체의 initChannel 메서드를 호출한다.

  • 소켓 채널 참조로부터 채널 파이프라인 객체를 가져와 이벤트 핸들러를 등록한다.

  • 이렇게 초기화가 완료되면 채널이 등록되었다는 이벤트가 발생하고, 클라이언트와 서버 간 데이터 송수신을 위한 이벤트 처리가 시작된다.

이벤트 핸들러

  • 네티는 비동기 호출을 지원하기 위해 Future 패턴과 리액터 패턴의 구현체인 이벤트 핸들러를 제공한다.

  • 이벤트 핸들러는 네티의 소켓 체널에서 발생한 이벤트를 처리하는 인터페이스이다.

  • 채널 파이프라인으로 입력되는 이벤트를 이벤트 루프가 가로채어 이벤트에 해당하는 메서드를 수행한다.

인바운드 이벤트

  • 상대방이 어떤 동작을 취했을 때 소켓 채널에서 발생하는 이벤트

  • 채널 활성화, 데이터 수신 등의 이벤트가 이에 해당한다.

  • 인바운드 이벤트를 채널 파이프라인에 보내면, 이벤트 핸들러 중 ChannelInboundHandler 인터페이스를 구현한 인바운드 이벤트 핸들러들이 메서드를 수행한다.

인바운드 이벤트 발생 순서

  • 아래와 같이 인바운드 이벤트 발생 순서를 나타낼 수 있으며, 각 과정에 대응되는 메서드가 ChannelInboundHandler 인터페이스에 존재한다.

ChannelInboundHandler

  • 인바운드 이벤트를 처리할 수 있도록 하는 ChannelInboundHandler 인터페이스의 메서드들을 하나씩 살펴본다.

channelRegistered

  • 채널이 처음 생성되어 이벤트 루프에 등록되었을 때 호출되는 메서드이다.

  • 서버에서는 처음 서버 소켓 채널을 생성할 때와 클라이언트가 서버에 접속해 소켓 채널이 생성될 때 이 메서드가 호출된다.

  • 클라이언트에서는 서버 접속을 위한 connect() 메서드를 수행할 때 이 메서드가 호출된다.

channelActive

  • channelRegistered 이벤트 이후에 채널 입출력을 수행할 상태가 되었을 때 호출되는 메서드이다.

  • 서버 또는 클라이언트가 상대방에 연결한 직후 한번 수행할 작업을 처리하기에 적합하다.

    • 서버에 연결된 클라이언트 개수를 세거나, 최초 연결에 대한 메시지를 전송할 때 사용될 수 있다.

channelRead

  • 데이터가 수신되었을 때 읽기 작업을 위해 호출되는 메서드이다.

  • 아래 EchoServerV1Handler 클래스는 데이터가 수신되었을 때 데이터를 출력하고 상대방에게 그대로 돌려주는 이벤트 핸들러이다.

  • 네티 내부에서는 모든 데이터가 ByteBuf로 관리되므로, Object 타입을 ByteBuf로 형변환하여 메시지를 확인할 수 있다.

public class EchoServerV1Handler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf readMessage = (ByteBuf) msg;
        System.out.println("channelRead : " + readMessage.toString(Charset.defaultCharset()));
        ctx.writeAndFlush(msg);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

channelReadComplete

  • 채널의 데이터를 다 읽고 더이상 데이터가 없는 상태가 되어 읽기 작업이 완료되었을 때 호출되는 메서드이다.

  • 아래 EchoServerV2Handler 클래스는 EchoServerV1Handler와 달리 channelRead에서는 write만 해두고 데이터 수신이 모두 완료되었을 때 flush하여 클라이언트에 데이터를 전송한다.

public class EchoServerV2Handler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf readMessage = (ByteBuf) msg;
        System.out.println("channelRead : " + readMessage.toString(Charset.defaultCharset()));
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        System.out.println("channelReadComplete 발생");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

channelInactive

  • 채널이 비활성화되어 로컬 피어에 대한 연결이 해제되었을 때 호출되는 메서드이다.

  • 이 메서드 호출 후에는 채널에 대한 입출력 작업을 수행할 수 없다.

channelUnregistered

  • 채널이 이벤트루프에서 제거되었을 때 발생하는 이벤트이다.

  • 이 메서드 호출 후에는 채널에서 발생한 이벤트를 처리할 수 없다.


channelWritabilityChanged

  • Channel의 기록 가능 상태가 변경되면 호출된다. OutOfMemoryError를 방지하기 위해 너무 빠르게 기록되지 않게 하거나 Channel이 기록 가능한 상태가 되면 기록을 재개할 수 있다.

  • Channel의 isWritable() 메서드를 호출해 해당 채널의 기록 가능 여부를 감지할 수 있다. 기록 가능 여부를 결정하는 임계값은 Channel.config().setWriteHighWaterMark()와 Channel.config().setWriteLowWaterMark()메서드로 설정한다.

userEventTriggered

  • POJO가 ChannelPipeline을 통해 전달되어 ChannelInboundHandler.fireUserEventTriggered()가 트리거되면 호출되는 메서드이다.

SimpleChannelInboundHandler

  • ChannelInboundHandler 구현체가 channelRead() 메서드를 재정의하는 경우 풀링된 ByteBuf 인스턴스의 메모리를 아래와 같이 명시적으로 해제해주어야 한다.

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // ...
        ReferenceCountUtil.release(msg);
    }
  • SimpleChannelInboundHandler를 사용하면 리소스를 자동으로 해제해주어 편리하게 사용 가능하다.

    @Sharable
    public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object>
    {
        @Override
        public void channnelRead0(ChannelHandlerContext ctx, Object msg)
        {
            // 리소스를 명시적으로 해제할 필요가 없음
        }
    }

아웃바운드 이벤트

  • 네티 사용자가 요청한 동작에 해당하는 이벤트

  • 연결 요청, 데이터 전송, 소켓 종료 등이 이에 해당한다.

  • 아웃바운드 작업과 데이터는 ChannelOutboundHandler 인터페이스를 구현한 아웃바운드 핸들러에 의해 처리된다.

  • 원격 피어에 대한 쓰기 작업이 일시 중단된 경우 flush 작업을 지연하여 나중에 재개할 수 있는 등 정교하게 요청을 처리할 수 있다.

ChannelOutboundHandler

bind

  • 서버 소켓 채널을 로컬 주소에 바인딩할 때(클라이언트의 연결을 받아들이는 ip, port 정보 설정) 호출되는 메서드이다.

  • 서버 소켓 채널이 사용하는 IP, 포트 정보를 확인할 수 있다.

connect

  • 클라이언트 소켓 채널이 서버에 연결 요청을 보냈을 때 호출되는 메서드이다.

  • 원격지의 SocketAddress 정보와 로컬 SocketAddress 정보가 인수로 입력되며, 연결 생성 시 로컬 SocketAddress를 입력하지 않았다면 여기서 수신하는 로컬 SocketAddress는 null이다.

disconnect

  • 클라이언트 소켓 채널의 연결을 끊을 때 호출되는 메서드이다.

close

  • 클라이언트 소켓 채널 연결이 닫을 때 호출되는 메서드이다.

read

  • 소켓 채널에서 데이터 읽기 요청이 들어왔을 때 호출되는 메서드이다.

write

  • 소켓 채널에 데이터가 기록되었을 때 호출되는 메서드이다.

  • 기록된 데이터 버퍼가 인수로 들어온다.

  • 이 메서드에서는 작업을 처리하고 메시지를 폐기하고 싶은 경우 아래와 같이 메시지를 해제해주고, ChannelPromise에 성공함을 알려 ChannelFutureListener가 메시지 처리에 대한 알림을 받을 수 있도록 한다.

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ReferenceCountUtil.release(msg);
    promise.setSuccess();
}

flush

  • 소켓 채널에 대한 flush 메서드가 호출되었을 때 발생하는 이벤트이다.

다중 이벤트 핸들러 처리

  • 한 채널 파이프라인에는 여러 이벤트 핸들러를 등록할 수 있다.

  • 여러 이벤트 핸들러에서 각각 다른 이벤트를 다루는 메서드를 구현할 경우, 각각의 이벤트가 발생하면 이벤트에 해당하는 메서드가 실행될 것이다.

  • 하지만 여러 이벤트 핸들러에서 동일한 이벤트를 다루는 메서드를 각자 다르게 구현할 경우, 첫번째 이벤트 핸들러에서 이벤트를 다루면 두번째 이벤트 핸들러에서는 이벤트를 받아볼 수 없다. 왜냐하면 이벤트 메서드를 수행하면 이벤트가 사라지기 때문이다.

  • 하나의 이벤트가 발생했을 때 여러 이벤트 핸들러의 이벤트 메서드를 실행시키고 싶다면, 첫 이벤트 핸들러의 이벤트 메서드에서 해당 이벤트를 재발생시켜주어야 한다.

  • 아래 예제와 같이 FirstHandler에서 이벤트를 재발생 시키게 하지 않으면 SecondHandler의 channelRead 메서드는 영영 불리지 않게 된다.

public class EchoServerV4FirstHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf readMessage = (ByteBuf) msg;
        System.out.println("FirstHandler channelRead : " + readMessage.toString(Charset.defaultCharset()));
        ctx.write(msg);
        ctx.fireChannelRead(msg); // channelRead 이벤트 재발생
    }
}
public class EchoServerV4SecondHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf readMessage = (ByteBuf) msg;
        System.out.println("SecondHandler channelRead : " + readMessage.toString(Charset.defaultCharset()));
    }
    
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        System.out.println("channelReadComplete 발생");
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

ChannelHandlerAdapter

  • ChannelHandlerAdapter의 구현체인 ChannelInboundHandlerAdapter, ChannelOutboundHandlerAdapter에는 기본 구현이 되어 있으므로 이 클래스를 상속받아 원하는 메서드만 오버라이드하여 핸들러를 쉽게 추가할 수 있다.

  • ChannelHandlerAdapter 추상 클래스는 isSharable 메서드를 제공하여 Sharable 어노테이션을 붙인 핸들러는 별도의 채널 파이프라인에서 동작하도록 한다.

메모리 관리

  • 네티는 ResourceLeakDetector 클래스를 통해 잠재적인 문제 진단을 돕기 위해 애플리케이션 버퍼 할당의 약 1%를 샘플링하여 메모리 누출을 검사한다.

  • -Dio.netty.leakDetectionLevel 실행 인자를 사용해 원하는 누수 감지 수준을 설정할 수 있다.

  • 누수 감지 수준은 아래 네 가지가 존재한다.

    • DISABLED

      • 누출 감지를 비활성화한다. 이 설정은 포괄적인 테스트를 거친 후에만 이용한다.

    • SIMPLE

      • 기본 샘플링 비율 1%를 이용해 발견된 누출을 보고한다. 기본 샘플링 비율은 대부분의 경우 적합하다.

    • ADVANCED

      • 발견된 누출과 메시지에 접근한 위치를 보고한다. 기본 샘플링 비율을 이용한다.

    • PARANOID

      • ADVANCED와 비슷하지만 모든 접근을 샘플링한다. 성능에 큰 영향을 미치므로 디버깅 단계에서만 이용해야 한다.

ChannelPipeline

  • 하나의 채널이 생성될 때 채널 파이프라인이 할당된다.

  • 채널 파이프라인은 ChannelHandler 객체들이 체이닝된 집합이다.

  • 채널파이프라인에서는 인바운드 이벤트가 트리거되면 인바운드 핸들러들이 차례로 수행되고, 아웃바운드 이벤트가 트리거되면 아웃바운드 핸들러들이 차례로 수행된다. 전체적인 흐름은 아래와 같다.

                                                  I/O Request
                                             via Channel or
                                         ChannelHandlerContext
                                                       |
   +---------------------------------------------------+---------------+
   |                           ChannelPipeline         |               |
   |                                                  \|/              |
   |    +---------------------+            +-----------+----------+    |
   |    | Inbound Handler  N  |            | Outbound Handler  1  |    |
   |    +----------+----------+            +-----------+----------+    |
   |              /|\                                  |               |
   |               |                                  \|/              |
   |    +----------+----------+            +-----------+----------+    |
   |    | Inbound Handler N-1 |            | Outbound Handler  2  |    |
   |    +----------+----------+            +-----------+----------+    |
   |              /|\                                  .               |
   |               .                                   .               |
   | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
   |        [ method call]                       [method call]         |
   |               .                                   .               |
   |               .                                  \|/              |
   |    +----------+----------+            +-----------+----------+    |
   |    | Inbound Handler  2  |            | Outbound Handler M-1 |    |
   |    +----------+----------+            +-----------+----------+    |
   |              /|\                                  |               |
   |               |                                  \|/              |
   |    +----------+----------+            +-----------+----------+    |
   |    | Inbound Handler  1  |            | Outbound Handler  M  |    |
   |    +----------+----------+            +-----------+----------+    |
   |              /|\                                  |               |
   +---------------+-----------------------------------+---------------+
                   |                                  \|/
   +---------------+-----------------------------------+---------------+
   |               |                                   |               |
   |       [ Socket.read() ]                    [ Socket.write() ]     |
   |                                                                   |
   |  Netty Internal I/O Threads (Transport Implementation)            |
   +-------------------------------------------------------------------+
  • 부트스트랩을 생성할 때 핸들러를 등록하면서 채널 파이프라인의 다양한 메서드를 사용할 수 있다. 아래 예제에서는 addLast메서드를 사용했다.

    • addFirst, addBefore, addAfter, addLast 메서드를 통해 원하는 위치에 핸들러를 추가할 수 있고, remove, replace 메서드를 사용해 핸들러를 제거하거나 교체할 수 있다.

Bootstrap b = new Bootstrap();
b.group(group)
  .channel(NioSocketChannel.class)
  .remoteAddress(new InetSocketAddress(host, port))
  .handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch)
        throws Exception {
      ch.pipeline().addLast(
          new EchoClientHandler());
    }
  });
  • 채널파이프라인의 핸들러는 이벤트 루프를 통해 수행되기 때문에 입출력 처리 전체 프로세스에 영향을 주지 않으려면 블로킹되지 않는 작성하는 것이 중요하다.

  • 만약 블로킹되는 로직이 필요한 상황이라면 addXXX 메서드를 통해 핸들러를 등록할 때 EventExecutorGroup 객체와 함께 등록하여 이벤트 루프가 아닌 EventExecutor에서 별도로 수행되도록 해야 한다.

  • 아래는 DefaultEventExecutorGroup에 스레드 개수를 넣어 새로운 Executor를 만들고 이벤트가 들어왔을 때 해당 Executor 내부에서 핸들러가 동작하도록 하는 코드이다.

ch.pipeline().addLast(new DefaultEventExecutorGroup(1), new EchoClientHandler());
  • 채널 파이프라인에 속한 핸들러에 접근하는 메서드들도 아래와 같이 존재한다.

// 핸들러를 이름 또는 타입으로 조회
ChannelHandler channelHandler = ch.pipeline().get("handler1");
ChannelHandler channelHandler = ch.pipeline().get(EchoClientHandler.class);

// 채널 파이프라인에 존재하는 모든 핸들러의 이름 반환
List<String> names = ch.pipeline().names();

ChannelHandlerContext

  • ChannelHandlerContext는 ChannelHandler와 ChannelPipeline 간의 연결을 나타내며, ChannelHandlerContext를 이용해 핸들러 간 상호작용을 할 수 있다.

  • 다음 ChannelHandler에 알림을 전하거나 채널 파이프라인을 수정할 수도 있다.

  • ChannelHandler와 연결된 ChannelHandlerContext는 절대 변경되지 않으므로 참조를 저장해 캐싱해두어도 된다.

  • ChannelHandlerContext 에서 제공하는 API는 다른 클래스의 메서드에 비해 이벤트 흐름이 짧아 잘 활용하여 성능상 이점을 챙길 수 있다.

write 호출의 차이

  • 소켓에 쓰기 작업을 할 때 ChannelHandlerContext의 channel() 메서드를 통해 Channel에 대한 참조를 얻어 write할 수도 있고, pipelline() 메서드를 통해 ChannelPipeline에 대한 참조를 얻어 write할 수도 있다.

ChannelHandlerContext ctx = ...;
ctx.channel().write(Unpooled.copiedBuffer("Netty In Action", CharsetUtil.UTF-8));

ChannelHandlerContext ctx = ...;
ctx.pipeline().write(Unpooled.copiedBuffer("Netty In Action", CharsetUtil.UTF-8));
  • 위와 같이 호출하게 되면 채널 파이프라인의 맨 처음 핸들러부터 시작해 모든 핸들러가 이벤트를 받게 된다.

  • ChannelHandlerContext에서 write() 메서드를 직접 호출하게 되면 매핑된 ChannelHandler 이후의 핸들러부터 이벤트를 받아보게 된다.

  • 이처럼 ChannelPipeline의 특정 지점에서 이벤트를 전파하면, 관련없는 핸들러를 건너뛰어 오버헤드를 줄일 수 있고, 이벤트와 관련된 특정 핸들러에서 이벤트가 처리되는 것을 방지할 수 있다.

참조 캐싱 방식

  • ChannelHandlerContext의 참조를 변수에 담아두어 사용할 수 있다.

public class WriteHanlder extends ChannelHandlerAdapter{
    private ChannelHandlerContext ctx;
    @Override
    public void handlerAdded(ChannelHanderContext ctx) {
        this.ctx = ctx;
    }
    
    public void send(String msg) {
        ctx.writeAndFlush(msg); // 이전에 저장한 ChannelHandlerContext를 이용해 메시지를 전송
    }
}

공유 가능한 ChannelHandler

  • 둘 이상의 ChannelPipeline에 속하는 ChannelHandler라면 여러 ChannelHandlerContext와 바인딩될 수 있다. 이 경우에는 ChannelHandler에 @Sharable 어노테이션을 지정해야 하고, thread-safe하기 위해 객체 내부에 상태를 저장하면 안된다.

  • 같은 ChannelHandler를 여러 ChannePipeline에 추가하는 가장 일반적인 이유는 여러 Channel에서 통계 정보를 얻기 위해서이다.

  • 아래는 공유 가능한 ChannelHandler의 구현 예제이다.

@Sharable
public class SharableHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        System.out.println("Channel read message : " + msg);
        ctx.fireChannelRead(msg);
    }
}

예외 처리

인바운드 예외 처리

  • 인바운드 이벤트 처리 중 예외가 발생하면 채널 파이프라인의 예외가 인바운드 핸들러들을 계속해서 통과하게 된다.

  • 예외가 파이프라인 끝에 도달하면 예외가 처리되지 않았음을 알리는 로그가 남게 된다.

  • 핸들러에서 예외를 처리하고자 한다면 exceptionCaught() 메서드를 재정의해야 한다.

  • 일반적으로 ChannelPipeline의 끝부분에 예외 처리하는 로직이 담긴 핸들러를 배치하여 어떤 위치에서 예외가 발생하든 처리할 수 있도록 한다.

  • 예외를 처리하는 방법은 다양하며, Channel을 닫거나 복구를 시도할 수 있다.

아웃바운드 예외 처리

  • 모든 아웃바운드 작업은 ChannelFuture를 반환한다. 작업이 완료되면 ChannelFuture에 등록된 ChannelFutureListener에 성공이나 오류에 대한 알림이 제공된다.

  • ChannelOutboundHandler의 거의 모든 메서드에는 ChannelPromise가 전달된다. ChannelFuture의 하위 클래스인 ChannelPromise에도 비동기 알림을 위한 수신기를 할당할 수 있으며, 즉시 알림을 지원하는 쓰기 가능 메서드도 있다.

  • ChannelFutureListener를 추가하려면 ChannelFuture 인스턴스의 addListener 메서드를 사용하면 된다.

  • 아래와 같이 future에 리스너를 직접 등록하거나, 핸들러의 ChannelPromise 인자를 사용해 리스너를 등록할 수 있다.

ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture f) {
        if (!f.isSuccess()) {
            f.cause().printStackTrace();
            f.channel().close();
        }
    }
})
public class OutboundExceptionHandler extends ChnnaelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        promise.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) {
                if (!f.isSuccess()) {
                    f.cause().printStackTrace();
                    f.channel().close();
                }
            }
        })
    }
}
Previous네티의 주요 특징Next이벤트 루프

Last updated 1 year ago

🏝️