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 - 예제
  • 네트워크 프로그램1 - 분석
  • DNS 탐색
  • 클라이언트 코드 분석
  • 서버 코드 분석
  • 네트워크 프로그램2 - 예제
  • 네트워크 프로그램2 - 분석
  • 서버 소켓과 연결 더 자세히
  • ServerV2 의 문제
  • 네트워크 프로그램3
  • 자원 정리1
  • 자원 정리2
  • 자원 정리3
  • 자원 정리4
  1. Language
  2. Java
  3. 강의
  4. 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션

네트워크 - 프로그램1

네트워크 프로그램1 - 예제

이제 본격적으로 자바 네트워크 프로그램을 작성해보자.

여기서는 TCP/IP 로 개발할 예정이다.

프로그램을 작성하기 전에 스레드 정보와 현재 시간을 출력하는 간단한 로깅 유틸리티를 만들자.

  • 앞으로 대부분의 출력은 이 로깅 클래스를 사용하겠다.

package util;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class MyLogger {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

    public static void log(Object obj) {
        String time = LocalTime.now().format(formatter);
        System.out.printf("%s [%9s] %s\n", time, Thread.currentThread().getName(), obj);
    }
}

이번에 만들 프로그램은 아주 간단한 네트워크 클라이언트, 서버 프로그램이다.

클라이언트가 "Hello" 라는 문자를 서버에 전달하면 클라이언트의 요청에 ", World!" 라는 단어를 더해서 "Hello, World!" 라는 단어를 클라이언트에 응답한다.

먼저 코드를 작성하자.

package network.tcp.v1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

import static util.MyLogger.log;

public class ClientV1 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        log("소켓 연결 : " + socket);

        // 서버에 문자 보내기
        String toSend = "Hello";
        output.writeUTF(toSend);
        log("client -> server : " + toSend);

        // 서버로부터 문자 받기
        String received = input.readUTF();
        log("client <- server : " + received);

        // 자원 정리
        log("연결 종료 : " + socket);
        input.close();
        output.close();
        socket.close();
    }
}
package network.tcp.v1;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV1 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트 : " + PORT);

        Socket socket = serverSocket.accept();
        log("소켓 연결 : " + socket);

        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());

        // 클라이언트로부터 문자 받기
        String received = input.readUTF();
        log("client -> server : " + received);

        // 클라이언트에게 문자 보내기
        String toSend = received + ", World";
        output.writeUTF(toSend);
        log("client <- server : " + toSend);

        // 자원 정리
        log("연결 종료 : " + socket);
        output.close();
        input.close();
        socket.close();
    }
}

서버를 먼저 실행하고, 그 다음에 클라이언트를 실행해야 한다.

네트워크 프로그램1 - 분석

TCP/IP 통신에서는 통신할 대상 서버를 찾을 때 호스트 이름이 아니라, IP 주소가 필요하다.

네트워크 프로그램을 분석하기 전에 호스트 이름으로 IP 를 어떻게 찾는지 확인해보자.

DNS 탐색

package network.tcp.v1;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressMain {

    public static void main(String[] args) throws UnknownHostException {
        InetAddress localhost = InetAddress.getByName("localhost");
        System.out.println("localhost = " + localhost);

        InetAddress google = InetAddress.getByName("google.com");
        System.out.println("google = " + google);
    }
}

자바의 InetAddress 클래스를 사용하면 호스트 이름으로 대상 IP 를 찾을 수 있다.

찾는 과정은 다음과 같다.

  1. 자바는 InetAddress.getByName("호스트명") 메서드를 사용해서 해당하는 IP 주소를 조회한다.

  2. 이 과정에서 시스템의 호스트 파일을 먼저 확인한다.

    1. /etc/hosts (리눅스, mac)

    2. C:\Windows\System32\drivers\etc\hosts (윈도우,Windows)

  3. 호스트 파일에 정의되어 있지 않다면 DNS 서버에 요청해서 IP 주소를 얻는다.

클라이언트 코드 분석

클라이언트와 서버의 연결은 Socket 을 사용한다.

Socket socket = new Socket("localhost", PORT)
  • localhost 를 통해 자신의 컴퓨터에 있는 12345 포트에 TCP 접속을 시도한다.

    • localhost 는 IP 가 아니므로 해당하는 IP 를 먼저 찾는다. 내부에서 InetAddress 를 사용한다.

    • localhost 는 127.0.0.1 이라는 IP 에 매핑되어 있다.

    • 127.0.0.1:12345 에 TCP 접속을 시도한다.

  • 연결이 성공적으로 완료되면 Socket 객체를 반환한다.

  • Socket 은 서버와 연결되어 있는 연결점이라고 생각하면 된다.

  • Socket 객체를 통해서 서버와 통신할 수 있다.

클라이언트와 서버간의 데이터 통신은 Socket 이 제공하는 스트림을 사용한다.

DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
  • Socket 은 서버와 데이터를 주고 받기 위한 스트림을 제공한다.

  • InputStream : 서버에서 전달한 데이터를 클라이언트가 받을 때 사용한다.

  • OutputStream : 클라이언트에서 서버에 데이터를 전달할 때 사용한다.

  • InputStream , OutputStream 을 그대로 사용하면 모든 데이터를 byte로 변환해서 전달해야 하기 때문에 번거롭다. 여기서는 DataInputStream , DataOutputStream 이라는 보조 스트림을 사용해서, 자바 타입의 메시지를 편리하게 주고 받을 수 있도록 했다.

// 서버에게 문자 보내기 
String toSend = "Hello"; 
output.writeUTF(toSend);
  • OutputStream 을 통해 서버에 "Hello" 메시지를 전송한다.

// 서버로부터 문자 받기
String received = input.readUTF();
  • InputStream 을 통해 서버가 전달한 메시지를 받을 수 있다.

  • 클라이언트가 "Hello"를 전송하면 서버는 " World!" 라는 문자를 붙여서 반환하므로 "Hello World!"라는 문자를 반환 받는다.

사용한 자원은 반드시 정리해야 한다.

// 자원 정리
log("연결 종료: " + socket); 
input.close(); 
output.close(); 
socket.close();

사용이 끝나면 사용한 자원은 반드시 반납해야 한다. 지금은 간단하고 허술하게 자원 정리를 했지만, 뒤에서 자원 정리를 매우 자세히 다루겠다.

서버 코드 분석

서버 소켓

서버는 특정 포트를 열어두어야 한다. 그래야 클라이언트가 해당 포트를 지정해서 접속할 수 있다.

ServerSocket serverSocket = new ServerSocket(PORT);
  • 서버는 서버 소켓( ServerSocket )이라는 특별한 소켓을 사용한다.

  • 지정한 포트를 사용해서 서버 소켓을 생성하면, 클라이언트는 해당 포트로 서버에 연결할 수 있다.

클라이언트와 서버의 연결 과정을 그림으로 자세히 알아보자.

  • 서버가 12345 포트로 서버 소켓을 열어둔다. 클라이언트는 이제 12345 포트로 서버에 접속할 수 있다.

  • 클라이언트가 12345 포트에 연결을 시도한다.

  • 이때 OS 계층에서 TCP 3 way handshake 가 발생하고, TCP 연결이 완료된다.

  • TCP 연결이 완료되면 서버는 OS backlog queue 라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관한다.

    • 이 연결 정보를 보면 클라이언트의 ip,port, 서버의 ip,port 정보가 모두 들어있다.

클라이언트와 랜덤 포트

TCP 연결시에는 클라이언트 서버 모두 IP, 포트 정보가 필요하다. 예제에서 사용된 IP 포트는 다음과 같다.

  • 클라이언트 : localhost(127.0.0.1), 50000(포트 랜덤 생성)

  • 서버 : localhost(127.0.0.1), 12345

그런데 생각해보면 클라이언트 자신의 포트를 지정한 적이 없다.

서버의 경우 포트가 명확하게 지정되어 있어야 한다. 그래야 클라이언트에서 서버에 어떤 포트에 접속할지 알 수 있다. 반면에 서버에 접속하는 클라이언트의 경우에는 자신의 포트가 명확하게 지정되어 있지 않아도 된다. 클라이언트는 보통 포트를 생략하는데, 생략할 경우 클라이언트 PC에 남아 있는 포트 중 하나가 랜덤으로 할당된다.

참고로 클라이언트 의 포트도 명시적으로 할당할 수는 있지만 잘 사용하지 않는다.

accept()

Socket socket = serverSocket.accept();
  • 서버 소켓은 단지 클라이언트와 서버의 TCP 연결만 지원하는 특별한 소켓이다.

  • 실제 클라이언트와 서버가 정보를 주고 받으려면 Socket 객체가 필요하다. (서버 소켓이 아니라 소켓 객체이다!)

  • serverSocket.accept() 메서드를 호출하면 TCP 연결 정보를 기반으로, Socket 객체를 만들어서 반환한다.

accept() 호출 과정을 그림으로 알아보자.

  • accept() 를 호출하면 backlog queue 에서 TCP 연결 정보를 조회한다.

    • 만약 TCP 연결 정보가 없다면 연결 정보가 생성될 때까지 대기한다. (블로킹)

  • 해당 정보를 기반으로 Socket 객체를 생성한다.

  • 사용한 TCP 연결 정보는 backlog queue 에서 제거된다.

Socket 생성 후 그림

  • 클라이언트와 서버의 Socket 은 TCP 로 연결되어 있고, 스트림을 통해 메시지를 주고 받을 수 있다.

DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
  • Socket 은 클라이언트와 서버가 데이터를 주고 받기 위한 스트림을 제공한다.

  • InputStream : 서버 입장에서 보면 클라이언트가 전달한 데이터를 서버가 받을 때 사용한다.

  • OutputStream : 서버에서 클라이언트에 데이터를 전달할 때 사용한다.

  • 메시지 송수신 후 자원을 꼭! 정리해야만 한다.

문제

이 프로그램은 메시지를 하나만 주고 받으면 클라이언트와 서버가 모두 종료된다. 메시지를 계속 주고 받고, 원할 때 종료할 수 있도록 변경해보자.

네트워크 프로그램2 - 예제

이번에는 클라이언트와 서버가 메시지를 계속 주고 받다가, "exit"라고 입력하면 클라이언트와 서버를 종료해보자.

package network.tcp.v2;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

import static util.MyLogger.log;

public class ClientV2 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        log("소켓 연결 : " + socket);

        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 서버에 문자 보내기
            System.out.print("전송 문자 : ");
            String toSend = scanner.nextLine();
            output.writeUTF(toSend);
            log("client -> server : " + toSend);

            if (toSend.equals("exit")) {
                break;
            }

            // 서버로부터 문자 받기
            String received = input.readUTF();
            log("client <- server : " + received);
        }

        // 자원 정리
        log("연결 종료 : " + socket);
        input.close();
        output.close();
        socket.close();
    }
}
package network.tcp.v2;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV2 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트 : " + PORT);

        Socket socket = serverSocket.accept();
        log("소켓 연결 : " + socket);

        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());

        while (true) {
            // 클라이언트로부터 문자 받기
            String received = input.readUTF();
            log("client -> server : " + received);

            if (received.equals("exit")) {
                break;
            }

            // 클라이언트에게 문자 보내기
            String toSend = received + ", World";
            output.writeUTF(toSend);
            log("client <- server : " + toSend);
        }

        // 자원 정리
        log("연결 종료 : " + socket);
        output.close();
        input.close();
        socket.close();
    }
}

문제

서버는 하나의 클라이언트가 아니라, 여러 클라이언트의 연결을 처리할 수 있어야 한다.

여러 클라이언트가 하나의 서버에 접속하도록 세팅 후 실행하면 다음과 같은 현상이 일어난다.

  • 첫번째 클라이언트와는 정상적으로 송수신이 된다.

  • 두번째 클라이언트의 메시지를 서버가 받지 못한다.

네트워크 프로그램2 - 분석

서버 소켓과 연결 더 자세히

서버 소켓과 연결에 대해서 좀 더 자세히 알아보자. 이번에는 여러 클라이언트가 서버에 접속한다고 가정해보자.

  • 서버는 12345 서버 소켓을 열어둔다.

  • 50000번 랜덤 포트를 사용하는 클라이언트가 먼저 12345 포트의 서버 접속을 시도한다.

  • 이때 OS 계층이 TCP 3 way handshake 가 발생하고, TCP 연결이 완료된다.

  • TCP 연결이 완료되면 서버는 OS backlog queue 라는 곳에 클라이언트와 서버의 TCP 연결 정보를 보관한다.

여기서 중요한 점이 있는 이 시점에 3 way handshake 가 완료되었기 때문에, 클라이언트와 서버의 TCP 연결은 이미 완료되고, 클라이언트 소켓 객체도 정상 생성된다. 참고로 이 시점에 아직 서버의 소켓 객체(서버 소켓 아님) 는 생성되지 않았다.

  • 이번에는 60000번 랜덤 포트를 사용하는 클라이언트가 12345 포트의 서버에 접속을 시도하고 연결을 완료한다.

  • 50000번 클라이언트와 60000번 클라이언트 모두 서버와 연결이 완료되었고, 클라이언트의 소켓도 정상 생성된다.

  • 서버가 클라이언트와 데이터를 주고 받으려면 소켓을 획득해야 한다.

  • ServerSocket.accpet() 메서드를 호출하면 backlog 큐의 정보를 기반으로 소켓 객체를 하나 생성한다.

  • 큐이므로 순서대로 데이터를 꺼낸다. 처음 50000번 클라이언트의 접속 정보를 기반으로 서버에 소켓이 하나 생성된다.

  • 50000번 클라리언트와 서버는 소켓의 스트림을 통해서 서로 데이터를 주고 받을 수 있다.

  • 그림에서 60000번 클라이언트도 이미 서버와 TCP 연결은 되어 있다.

    • OS 계층에서 TCP 3 way handshake 가 발생하고, TCP 연결이 완료되었다.

  • 60000번 클라리언트도 서버와 TCP 연결이 되었기 때문에 서버로 메시지를 보낼 수 있다.

    • 아직 서버에 Socket 객체가 없더라도 메시지는 보낼 수 있다. 이미 TCP 연결은 완료되었다.

그림에서 볼 수 있듯이, 소켓을 통해 스트림으로 메시지를 주고 받는다는 것은 사실은 이러한 과정을 거치는 것이다.

자바 애플리케이션은 소켓 객체의 스트림을 통해 서버와 데이터를 주고 받는다. 데이터를 주고 받는 과정은 다음과 같다.

클라이언트가 "Hello, world!"라는 메시지를 서버에 전송하는 경우

  • 클라이언트

    • 애플리케이션 -> OS TCP 송신버퍼 -> 클라이언트 네트워크 카드

  • 클라이언트가 보낸 메시지가 서버에 도착했을 때, 서버

    • 서버 네트워크 카드 -> OS TCP 수신버퍼 -> 애플리케이션

여기서 60000번 클라이언트가 보낸 메시지는 서버 애플리케이션에서 아직 읽지 않았기 때문에, 서버 OS 의 TCP 수신 버퍼에서 대기하게 된다.

여기서 핵심적인 내용이 있는데, 소켓 객체 없이 서버 소켓만으로도 TCP 연결은 완료된다는 점이다. (서버 소켓은 연결만 담당한다) 하지만 연결 이후에 서로 메시지를 주고 받으려면 소켓 객체가 필요하다.

accept() 는 이미 연결된 TCP 연결 정보를 기반으로 서버 측에 소켓 객체를 생성한다. 그리고 이 소켓 객체가 있어야 스트림을 사용해서 메시지를 주고 받을 수 있다.

  • 이렇게 소켓을 연결하면 소켓의 스트림을 통해 OS TCP 수신 버퍼에 있는 메시지를 읽을 수 있고, 또 전송할 수도 있다.

  • accept() 메서드는 backlog queue 에 새로운 연결 정보가 도착할 때 까지 블로킹 상태로 대기한다. 새로운 연결 정보가 오지 않으면 계속 대기하는 블로킹 메서드이다.

ServerV2 의 문제

ServerV2 에 둘 이상의 클라이언트가 작동하지 않는 이유는 다음과 같다.

  • 새로운 클라이언트가 접속하면?

    • 새로운 클라이언트가 접속했을 때 서버의 main 스레드는 accept() 메서드를 절대로 호출할 수 없다! 왜냐하면 while 문으로 기존 클라이언트와 메시지를 주고 받는 부분만 반복하기 때문이다.

    • accept() 를 호출해야 소켓 객체를 생성하고 클라이언트와 메시지를 주고 받을 수 있다.

  • 2개의 블로킹 작업 - 핵심은 별도의 스레드가 필요하다!

    • accept() : 클라이언트와 서버의 연결을 처리하기 위해 대기

    • readXxx() : 클라이언트의 메시지를 받아서 처리하기 위해 대기

    • 각각의 블로킹 작업은 별도의 스레드에서 처리해야 한다. 그렇지 않으면 다른 블로킹 메서드 때문에 계속 대기할 수 있다.

ServerV2 코드 - 간략

ServerSocket serverSocket = new ServerSocket(PORT); 
Socket socket = serverSocket.accept(); // 블로킹


while(true) {
    String received = input.readUTF(); // 블로킹
    output.writeUTF(toSend);
}

네트워크 프로그램3

이번에는 여러 클라이언트가 동시에 접속할 수 있는 서버 프로그램을 작성해보자.

  • 서버의 main 스레드는 서버 소켓을 생성하고, 서버 소켓의 accept() 를 반복해서 호출해야 한다.

  • 클라이언트가 서버에 접속하면 서버 소켓의 accept() 메서드가 Socket 을 반환한다.

  • main 스레드는 이 정보를 기반으로 Runnable 을 구현한 Session 이라는 별도의 객체를 만들고, 새로운 스

    레드에서 이 객체를 실행한다. 여기서는 Thread-0 이 Session 을 실행한다.

  • Session 객체와 Thread-0 은 연결된 클라이언트와 메시지를 주고 받는다.

  • 새로운 TCP 연결이 발생하면 main 스레드는 새로운 Session 객체를 별도의 스레드에서 실행한다. 그리고 이 과정을 반복한다.

  • Session 객체와 Thread-1 은 연결된 클라이언트와 메시지를 주고 받는다.

역할의 분리

  • main 스레드

    • main 스레드는 새로운 연결이 있을 때 마다 Session 객체와 별도의 스레드를 생성하고, 별도의 스레드가 Session 객체를 실행하도록 한다.

  • Session 담당 스레드

    • Session 을 담당하는 스레드는 자신의 소켓이 연결된 클라이언트와 메시지를 반복해서 주고 받는 역할을 담당한다.

package network.tcp.v3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

import static util.MyLogger.log;

public class ClientV3 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        log("클라이언트 시작");
        Socket socket = new Socket("localhost", PORT);
        DataInputStream input = new DataInputStream(socket.getInputStream());
        DataOutputStream output = new DataOutputStream(socket.getOutputStream());
        log("소켓 연결 : " + socket);

        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 서버에 문자 보내기
            System.out.print("전송 문자 : ");
            String toSend = scanner.nextLine();
            output.writeUTF(toSend);
            log("client -> server : " + toSend);

            if (toSend.equals("exit")) {
                break;
            }

            // 서버로부터 문자 받기
            String received = input.readUTF();
            log("client <- server : " + received);
        }

        // 자원 정리
        log("연결 종료 : " + socket);
        input.close();
        output.close();
        socket.close();
    }
}
package network.tcp.v3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

import static util.MyLogger.log;

public class SessionV3 implements Runnable{

    private final Socket socket;

    public SessionV3(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        DataInputStream input = null;
        try {
            input = new DataInputStream(socket.getInputStream());
            DataOutputStream output = new DataOutputStream(socket.getOutputStream());

            while (true) {
                // 클라이언트로부터 문자 받기
                String received = input.readUTF();
                log("client -> server : " + received);

                if (received.equals("exit")) {
                    break;
                }

                // 클라이언트에게 문자 보내기
                String toSend = received + ", World";
                output.writeUTF(toSend);
                log("client <- server : " + toSend);
            }

            log("연결 종료 : " + socket);
            output.close();
            input.close();
            socket.close();

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • Session의 목적은 소켓이 연결된 클라이언트와 메시지를 반복해서 주고 받는 것이다.

  • 생성자를 통해 Socket객체를 입력 받는다.

  • Runnable 을 구현해서 별도의 스레드에서 실행한다.

package network.tcp.v3;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

import static util.MyLogger.log;

public class ServerV3 {

    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException, InterruptedException {
        log("서버 시작");
        ServerSocket serverSocket = new ServerSocket(PORT);
        log("서버 소켓 시작 - 리스닝 포트 : " + PORT);

        while (true) {
            Socket socket = serverSocket.accept();
            log("소켓 연결 : " + socket);

            SessionV3 session = new SessionV3(socket);
            Thread thread = new Thread(session);
            thread.start();
        }
    }
}
  • 중요한 코드는 main 스레드가 작동하는 부분이다.

  • main 스레드는 서버 소켓을 생성하고, serverSocket.accept() 을 호출해서 연결을 대기한다.

  • 새로운 연결이 추가될 때 마다 Session 객체를 생성하고 별도의 스레드에서 Session 객체를 실행한다.

  • 이 과정을 반복한다.

여러 클라이언트가 접속해도 문제없이 작동하는 것을 확인할 수 있다. 그리고 각각의 연결이 별도의 스레드에서 처리되는 것도 확인할 수 있다.

서버 소켓을 통해 소켓을 연결하는 부분과 각 클라이언트와 메시지를 주고 받는 부분이 별도의 스레드로 나뉘어 있다. 블로킹 되는 부분은 이렇게 별도의 스레드로 나누어 실행해야 한다.

문제

여기서 실행 중인 클라이언트를 IDE stop 버튼을 눌러서 종료해보자.

클라이언트를 직접 종료한 경우 서버 로그

클라이언트의 연결을 직접 종료하면 클라이언트 프로세스가 종료되면서, 클라이언트와 서버의 TCP 연결도 함께 종료 된다. 이때 서버에서 readUTF() 로 클라이언트가 메시지를 읽으려고 하면 EOFException 이 발생한다. 소켓의 TCP 연결이 종료되었기 때문에 더는 읽을 수 있는 메시지가 없다는 뜻이다. EOF(파일의 끝)가 여기서는 전송의 끝이 라는 뜻이다.

그런데 여기서 심각한 문제가 하나 있다. 이렇게 예외가 발생해버리면 서버에서 자원 정리 코드를 호출하지 못한다는 점이다. 서버 로그를 보면 연결 종료 로그가 없는 것을 확인할 수 있다.

// 자원 정리
log("연결 종료: " + socket); 
input.close(); 
output.close(); 
socket.close();

자바 객체는 GC 가 되지만 자바 외부의 자원은 자동으로 GC 가 되는게 아니다. 따라서 꼭! 정리를 해주어야 한다. (TCP 연결의 경우 운영체제가 어느정도 연결을 정리해주지만, 직접 연결을 종료할 때 보다 더 많은 시간이 걸릴 수 있다.)

자원 정리1

이번 내용은 자바 중급 1편에서 다룬 예외 처리의 실전 심화 내용이다.

자원 정리를 이해하기 위해 간단한 예제 코드를 만들어보자.

package network.tcp.autocloseable;

public class CallException extends Exception {
    public CallException(String message) {
        super(message);
    }
}
package network.tcp.autocloseable;

public class CloseException extends Exception{
    public CloseException(String message) {
        super(message);
    }
}
package network.tcp.autocloseable;

public class ResourceV1 {

    private String name;

    public ResourceV1(String name) {
        this.name = name;
    }

    public void call() {
        System.out.println(name + " call");
    }

    public void callEx() throws CallException {
        System.out.println(name + " callEx");
        throw new CallException(name + " ex");
    }

    public void close() {
        System.out.println(name + " close");
    }

    public void closeEx() throws CloseException {
        System.out.println(name + " closeEx");
        throw new CloseException(name + " ex");
    }
}
  • call() : 정상 로직 호출

  • callEx() : 비정상 로직 호출 CallException 을 던진다.

  • close() : 정상 종료

  • closeEx() : 비정상 종료, CloseException 을 던진다.

package network.tcp.autocloseable;

public class ResourceCloseMainV1 {

    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            throw new RuntimeException(e);
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            throw new RuntimeException(e);
        }
    }

    private static void logic() throws CallException, CloseException {
        ResourceV1 resource1 = new ResourceV1("resource1");
        ResourceV1 resource2 = new ResourceV1("resource2");

        resource1.call();
        resource2.callEx(); // CallException

        System.out.println("자원 정리");    // 호출 안됨
        resource2.closeEx();
        resource1.closeEx();
    }
}
  • 서로 관련된 자원은 나중에 생성한 자원을 먼저 정리해야 한다.

  • 예를 들어, resource1 을 생성하고, resource1 의 정보를 활용해서 resource2 를 생성한다면, 닫을 때는 그 반대인 resource2 를 먼저 닫고, 그 다음에 resource1 을 닫아야 한다. 왜냐하면 resource2 의 입장에서 resource1 의 정보를 아직 참고하고 있기 때문이다.

  • 이 예제에서는 두 자원이 서로 관련이 없기 때문에 생성과 종료 순서가 크게 상관이 없지만, resource1 의 정보를 기반으로 resource2 를 생성한다고 가정하겠다.

callEx() 를 호출하면서 예외가 발생했다. 예외 때문에 자원 정리 코드가 정상 호출되지 않았다. 이 코드는 예외가 발생하면 자원이 정리되지 않는다는 문제가 있다.

자원 정리2

이번에는 예외가 발생해도 자원을 정리하도록 하자.

package network.tcp.autocloseable;

public class ResourceCloseMainV2 {

    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            throw new RuntimeException(e);
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            throw new RuntimeException(e);
        }
    }

    private static void logic() throws CallException, CloseException {

        ResourceV1 resource1 = null;
        ResourceV1 resource2 = null;

        try {
            resource1 = new ResourceV1("resource1");    // EX
            resource2 = new ResourceV1("resource2");

            resource1.call();
            resource2.callEx(); // CallException
        } catch (CallException e) {
            System.out.println("ex : " + e);
            throw e;
        } finally {
            if(resource2 != null){
                resource2.closeEx();    // CloseException 발생
            }
            if(resource1 != null){
                resource1.closeEx();    // 이 코드 호출 안됨
            }
        }
    }
}

null 체크

이번에는 finally 코드 블록을 사용해서 자원을 닫는 코드가 항상 호출되도록 했다.

만약 resource2 객체를 생성하기 전에 예외가 발생하면 resource2 는 null 이 된다. 따라서 null 체크를 해야 한다. resouece1 의 경우에도 resource1 을 생성하는 중에 예외가 발생한다면 null 체크가 필요하다.

자원 정리중에 예외가 발생하는 문제

finally 코드 블록은 항상 호출되기 때문에 자원이 잘 정리될 것 같지만, 이번에는 자원을 정리하는 중에 finally 코드 블록 안에 resource2.closeEx() 를 호출하면서 예외가 발생한다. 결과적으로 resource1.closeEx1() 는 호출되지 않는다.

핵심 예외가 바뀌는 문제

이 코드에서 발생한 핵심적인 예외는 CallException 이다. 이 예외 때문에 문제가 된 것이다. 그런데 finally 코드 블록에서 자원을 정리하면서 closeException 예외가 추가로 발생했다. 예외 때문에 자원을 정리하고 있는데, 자원 정리중에 또 예외가 발생한 것이다. 이 경우 logic() 을 호출한 쪽에서는 핵심 예외인 CallException 이 아니라 finally 블록에서 새로 생성된 CloseException 을 받게 된다. 핵심 예외가 사라진 것이다.

개발자가 원하는 예외는 당연히 핵심 예외이다. 이 핵심 예외를 확인해야 제대로 된 문제를 찾을 수 있다. 자원을 닫는 중에 발생한 예외는 부가 예외일 뿐이다.

정리하면 이 코드는 다음과 같은 문제가 있다.

  • close() 시점에 실수로 예외를 던지면, 이후 다른 자원을 닫을 수 없는 문제 발생

  • finally 블럭 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally 에서 발생한 부가 예외로 바뀌어 버린다. 그리고 핵심 예외가 사라진다.

자원 정리3

이번에는 자원 정리의 코드에서 try-catch 를 사용해서 자원 정리 중에 발생하는 예외를 잡아서 처리해보자.

package network.tcp.autocloseable;

public class ResourceCloseMainV3 {

    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            throw new RuntimeException(e);
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            throw new RuntimeException(e);
        }
    }

    private static void logic() throws CallException, CloseException {

        ResourceV1 resource1 = null;
        ResourceV1 resource2 = null;

        try {
            resource1 = new ResourceV1("resource1");    // EX
            resource2 = new ResourceV1("resource2");

            resource1.call();
            resource2.callEx(); // CallException
        } catch (CallException e) {
            System.out.println("ex : " + e);
            throw e;
        } finally {
            if(resource2 != null){
                try {
                    resource2.closeEx();    // CloseException 발생
                } catch (CloseException e) {
                    // close() 에서 발생한 예외는 버린다. 필요하면 로깅 정도
                    System.out.println("close ex : " + e);
                }
            }
            if(resource1 != null){
                try {
                    resource1.closeEx();    // CloseException 발생
                } catch (CloseException e) {
                    // close() 에서 발생한 예외는 버린다. 필요하면 로깅 정도
                    System.out.println("close ex : " + e);
                }
            }
        }
    }
}
  • finally 블럭에서 각각의 자원을 닫을 때도, 예외가 발생하면 예외를 잡아서 처리하도록 했다.

  • 이렇게 하면 자원 정리 시점에 예외가 발생해도, 다음 자원을 닫을 수 있다.

  • 자원 정리 시점에 발생한 예외를 잡아서 처리했기 때문에, 자원 정리 시점에 발생한 부가 예외가 핵심 예외를 가리지 않는다.

  • 자원 정리 시점에 발생한 예외는 당장 더 처리할 수 있는 부분이 없다. 이 경우 로그를 남겨서 개발자가 인지할 수 있게 하는 정도면 충분하다.

이전에 발생헀던 다음 2가지 문제를 해결했다.

  • close() 시점에 실수로 예외를 던지면, 이후 다른 자원을 닫을 수 없는 문제 발생

  • finally 블럭 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally 에서 발생한 부가 예외로 바뀌어 버린다. 그리고 핵심 예외가 사라진다.

핵심적인 문제들은 해결되었지만 코드 부분에서 보면 아쉬운 부분이 많다.

  • resource 변수를 선언하면서 동시에 할당할 수 없음( try , finally 코드 블록과 변수 스코프가 다른 문제)

  • catch 이후에 finally 호출, 자원 정리가 조금 늦어진다.

  • 개발자가 실수로 close() 를 호출하지 않을 가능성

  • 개발자가 close() 호출 순서를 실수, 보통 자원을 생성한 순서와 반대로 닫아야 함

지금까지 수 많은 자바 개발자들이 자원 정리 때문에 고통 받아왔다. 이런 문제를 한번에 해결하는 것이 바로 자바 중급1편에서 학습한 try-with-resources 구문이다.

자원 정리4

자바 중급 1편에서도 try-with-resources 를 학습했지만, 이번에는 try-with-resources 에 대해서 조금 더 깊이있게 들어가보자.

package network.tcp.autocloseable;

public class ResourceV2 implements AutoCloseable{

    private String name;

    public ResourceV2(String name) {
        this.name = name;
    }

    public void call() {
        System.out.println(name + " call");
    }

    public void callEx() throws CallException {
        System.out.println(name + " callEx");
        throw new CallException(name + " ex");
    }

    @Override
    public void close() throws CloseException {
        System.out.println(name + " close");
        throw new CloseException(name + " ex");
    }
}
  • AutoCloseable 을 구현했다.

  • close() 는 항상 CloseException 을 던지도록 했다.

package network.tcp.autocloseable;

public class ResourceCloseMainV4 {

    public static void main(String[] args) {
        try {
            logic();
        } catch (CallException e) {
            System.out.println("CallException 예외 처리");
            throw new RuntimeException(e);
        } catch (CloseException e) {
            System.out.println("CloseException 예외 처리");
            throw new RuntimeException(e);
        }
    }

    private static void logic() throws CallException, CloseException {
        try (ResourceV2 resource1 = new ResourceV2("resource1");
             ResourceV2 resource2 = new ResourceV2("resource2")) {

            resource1.call();
            resource2.callEx(); // CallException

            // 자동으로 호출
            // resource2.close()
            // resource1.close()
        } catch (CallException e) {
            System.out.println("ex : " + e);

            // 핵심 예외 안에 하위 예외를 넣어준다. (suppressed)
            Throwable[] suppressed = e.getSuppressed();
            for (Throwable throwable : suppressed) {
                System.out.println("suppressedEx : " + throwable);
            }

            throw e;
        }
    }
}

try-with-resources 는 단순하게 close() 를 자동 호출해준다는 정도의 기능한 제공하는 것이 아니다. 고민한 6가지 문제를 모두 해결하는 장치이다.

2가지 핵심 문제

  • close() 시점에 실수로 예외를 던지면, 이후 다른 자원을 닫을 수 없는 문제 발생

  • finally 블럭 안에서 자원을 닫을 때 예외가 발생하면, 핵심 예외가 finally 에서 발생한 부가 예외로 바뀌어 버린다. 그리고 핵심 예외가 사라진다.

4가지 부가 문제

  • resource 변수를 선언하면서 동시에 할당할 수 없음( try , finally 코드 블록과 변수 스코프가 다른 문제)

  • catch 이후에 finally 호출, 자원 정리가 조금 늦어진다.

  • 개발자가 실수로 close() 를 호출하지 않을 가능성

  • 개발자가 close() 호출 순서를 실수, 보통 자원을 생성한 순서와 반대로 닫아야 함

Try with resourses 장점

  • 리소스 누수 방지: 모든 리소스가 제대로 닫히도록 보장한다. 실수로 finally 블록을 적지 않거나, finally 블럭 안에서 자원 해제 코드를 누락하는 문제들을 예방할 수 있다.

  • 코드 간결성 및 가독성 향상: 명시적인 close() 호출이 필요 없어 코드가 더 간결하고 읽기 쉬워진다.

  • 스코프 범위 한정: 예를 들어 리소스로 사용되는 resource1,2 변수의 스코프가 try 블럭 안으로 한정된다. 따라서 코드 유지보수가 더 쉬워진다.

  • 조금 더 빠른 자원 해제: 기존에는 try catch finally로 catch 이후에 자원을 반납했다. Try with resources 구분은 try 블럭이 끝나면 즉시 close() 를 호출한다.

  • 자원 정리 순서: 먼저 선언한 자원을 나중에 정리한다.

  • 부가 예외 포함: 다음 내용에서 설명

Try with resources 예외 처리와 부가 예외 포함

try-with-resources 를 사용하는 중에 핵심 로직 예외와 자원을 정리하는 중에 발생하는 부가 예외가 모두 발생하면 어떻게 될까?

  • try-with-resources 는 핵심 예외를 반환한다.

  • 부가 예외는 핵심 예외안에 Suppressed 로 담아서 반환한다.

  • 개발자는 자원 정리 중에 발생한 부가 예외를 e.getSuppressed() 를 통해 활용할 수 있다.

try-with-resources 를 사용하면 핵심 예외를 반환하면서, 동시에 부가 예외도 필요하면 확인할 수 있다.

참고로 자바 예외에는 e.addSuppressed(ex) 라는 메서드가 있어서 예외 안에 참고할 예외를 담아둘 수 있다. 참고로 이 기능도 try-with-resources 와 함께 등장했다.

PreviousFile, FilesNext네트워크 - 프로그램2

Last updated 16 days ago