'NIO'에 해당되는 글 2건

  1. 2020.02.01 :: Netty - 네티 개념과 아키텍쳐
  2. 2019.02.11 :: Java - JDK1.7(JAVA 7) 특징 try-with-resource 등
Web/Netty 2020. 2. 1. 14:12

 

오늘 다루어볼 포스팅 내용은 Netty의 개념과 아키텍쳐에 대한 대략적인 설명이다. Netty에 대해 알아보기 전에 AS-IS 자바의 네트워킹 동작 방식에 대해 먼저 다루어본다.

 

자바의 네트워킹

순수 자바로 네트워크 통신을 하기위해서 생긴 최초의 라이브러리는 java.net 패키지이다. 해당 소켓 라이브러리가 제공하는 방식은 블로킹 함수만 지원했다. 해당 라이브러리를 이용한 서버코드를 간단히 보면 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public void blockCall() throws IOException {
        ServerSocket serverSocket = new ServerSocket(8080);
        Socket clientSocket = serverSocket.accept();
        BufferedReader in = new BufferedReader(
                new InputStreamReader(clientSocket.getInputStream())
        );
 
        PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
 
        String request, response;
        while ((request = in.readLine()) != null) {
            if ("OK".equals(request)) {
                break;
            }
            response = processRequest(request);
            System.out.println(response);
        }
    }
 
    public String processRequest(String request) {
        return request + "Done";
    }
cs

 

해당 코드는 한 번에 한 연결만 처리한다. 다수의 동시 클라이언트를 관리하려면 새로운 클라이언트 Socket마다 새로운 Thread를 할당해야한다. 이런식의 블로킹 처리는 어떠한 결과를 초래하게 될 것인가? 여러 스레드가 입력이나 출력 데이터가 들어오기를 기다리며 무한정 대기 상태로 유지될 수 있고, 이것은 고로 리소스의 낭비로 이어진다. 그리고 하나의 연결당 하나의 스레드가 생성되므로, 많은 수의 클라이언트를 관리하기 위해서는 많은 수의 스레드를 생성해야 하고, 이것은 리소스 낭비는 물론 잦은 컨텍스트 스위칭에 의한 오버헤드가 발생하게 된다. 

 

이러한 문제때문에 그 다음 나온 자바의 네트워킹 통신은 자바 NIO방식이다.

 

자바 NIO

해당 방식은 네트워크 리소스 사용률을 세부적으로 제어할 수 있는 논블로킹 호출이 포함되어 있다. 논블로킹은 내부적으로 시스템의 이벤트 통지 API를 이용해 논블로킹 소켓의 집합을 등록하면 읽거나 기록할 데이터가 준비됐는지 여부를 알 수 있다. 즉, 계속해서 기다릴 필요가 없어지는 것이다.

 

 

OIO와는 달리 NIO는 채널과 소켓이 1:1 매칭되지 않는다. java.nio.channels.Selector가 논블로킹 Socket의 집합에서 입출력이 가능한 항목을 지정하기 위해 이벤트 통지 API를 이용하기 때문에, 언제든지 읽기나 쓰기 작업의 완료상태를 확인가능하다. 그렇기 때문에 채널당 하나의 쓰레드가 분배되지 않고(블록킹하면 기다리지 않아도) 필요할때마다 쓰레드에게 통지를 하므로써 필요할때만 일을 할 수 있고, 필요하지 않을때는 다른 일을 할 수 있게 한다. 이런식의 처리흐름을 도입함으로써

 

  • 적은 수의 스레드로 더 많은 연결을 처리할 수 있어 메모리 관리와 컨텍스트 스위치에 대한 오버헤드가 준다.
  • 입출력을 처리하지 않을 때는 스레드를 다른 작업에 활용할 수 있다.

 

라는 장점이 생긴다. 하지만 그에 따른 단점이 존재한다면, 순수 자바 라이브러리를 직접 사용해 애플리케이션을 제작하기에는 아주 어렵다. 특히 부하가 높은 상황에서 입출력을 안정적이고 효율적으로 처리하고 호출하는 것과 같이 까다롭고 문제 발생이 높은 일은 어려운 일이기 때문이다.

 

이러한 어렵고 까다로운 일을 대신해주기 위해 네티와 같은 네트워크 프레임워크가 존재하는 것이다.

 

Netty(네티)

네티는 위와 같이 어려운 자바의 고급 API를 내부에 숨겨 놓고, 사용자에게 비즈니스 로직에만 집중할 수 있도록 추상화한 API를 제공한다. 또한 적은양의 리소스를 소모하면서 더 많은 요청을 처리할 수 있게 설계되었으므로 확장하기에도 부담이 없다. 아래는 네티의 특징을 요약한 표이다.

 

category feature
설계 단일 API로 블로킹과 논블로킹 방식의 여러 전송 유형을 지원하며, 단순하지만 강력한 스레딩 모델을 제공한다. 
이용 편의성 JDK 1/6+을 제외한 추가 의존성이 필요 없다.
성능 코어 자바 API보다 높은 처리량과 짧은 지연시간을 갖는다. 풀링과 재사용을 통한 리소스 소비를 감소시켰고, 메모리 복사를 최소화하였다.
견고성 저속, 고속 또는 과부하 연결로 인한 OOM이 잘 발생하지 않는다.(요청을 처리하는 스레드 수가 굉장히 적기 때문) 고속 네트워크 상의 NIO 애플리케이션에서 일반적인 읽기/쓰기 비율 불균형이 발생하지 않는다.
보안 완벽한 SSL/TLS 및 StarTLS 지원. 애플릿이나 OSGi 같은 제한된 환경에서도 이용 가능.

 

비동기식 이벤트 기반 네트워킹

 

  • 네티의 논블로킹 네트워크 연결은 작업 완료를 기다릴 필요가 없다. 완전 비동기 입출력은 이 특징을 바탕으로 한 단계 더 나아간다. 비동기 메서드는 즉시 반환하며 작업이 완료되면 직접 또는 나중에 이를 통지한다. 
  • 셀렉터는 적은 수의 스레드로 여러 연결에서 이벤트를 모니터링할 수 있게 해준다

 

위의 특징들을 종합해보면 블로킹 입출력 방식을 이용할 때보다 더 많은 이벤트를 훨씬 빠르고 경제적으로 처리할 수 있다. 

 

네티의 핵심 컴포넌트들

  • Channel
  • Callback
  • Future
  • 이벤트와 핸들러

Channel은 하나 이상의 입출력 작업을 수행할 수 있는 하드웨어 장치, 파일, 네트워크 소켓, 프로그램 컴포넌트와 같은 엔티티에 대한 열린 연결을 뜻한다. 쉽게 말해 인바운드 데이터와 아웃바운드 데이터를 위한 운송수단이라고 생각하면 좋을 것 같다.

 

Callback은 간단히 말해 다른 메서드로 자신에 대한 참조를 제공할 수 있는 메서드다. 관심 대상에게 작업 완료를 알리는 가장 일반적인 방법 중 하나이다. 네티는 이러한 콜백을 내부적으로 이벤트 처리에 사용한다.

 

Future는 작업이 완료되면 애플리케이션에게 알리는 한 방법이다. 즉, 작업이 완료되는 미래의 어떤 시점에 그 결과에 접근할 수 있게 해준다. 하지만 자바의 Future는 결과를 얻기 위해서는 반드시 블로킹해야만 한다. 그래서 네티는 비동기 작업이 실행됐을 때 이용할 수 있는 자체 구현 ChannelFuture를 제공한다. 더 자세한 내용은 뒤에서 다루어본다.

 

마지막으로 네티는 이벤트가 들어오면 해당 이벤트를 핸들러 클래스의 사용자 구현 메서드로 전달하여 이벤트를 변환하며 이러한 핸들러의 체인으로 이벤트들을 처리한다. 이벤트와 핸들러 또한 뒤에서 더 자세히 다룬다.

posted by 여성게
:

Java - JDK1.7 특징 try-with-resource 등



JAVA 10버전 이상 나온 시점에 자바 7의 특징을 설명하자니 많이 뒤쳐진 느낌이 들지만 저처럼 아직도 자바 5,6 세대에 머무르고 있는 분들이 있어서는 안되지만 혹시라도 계시는 분들이 있을 것이기에 간단하게 정리해보았습니다. 자바7도 안되있는 상태에서 자바8,9,10 등을 공부하는게 뭔가 순리에 어긋나는 것같습니다. 부족하지만 하나하나 천천히 공부해 나가야 할것 같습니다. 간단히 대략 몇 가지정도만 정리하려고 합니다.



Type Interface



Java 7 이전에는 제너릭 타입 파라미터를 선언과 생성시 중복해서 써주어야했습니다. 사실 저는 현재 생성자 쪽은 제네릭타입을 따로 넣지 않고 사용했었는데, 이게 당연히 원래 되는 건줄 알고 있었는데, 알고보니 JAVA 7에서 개선된 점이었내요.


JDK 7 이전

1
2
Map<String, List<String>> employeeRecords = new HashMap<String, List<String>>();
List<Integer> primes = new ArrayList<Integer>();

JDK 7

1
2
Map<String, List<String>> employeeRecords = new HashMap<>();
List<Integer> primes = new ArrayList<>();



물론 기존에도 제너릭 메소드를 제공함으로써 Type Inference가 가능했는데요. 예를 들어 Static Factory Method를 아래처럼 사용하면 별도의 타입 파라미터 없이도 간단히 제너릭 객체를 리턴 받아 사용 가능합니다. Guava 라이브러리 등에서 자주 사용하던 방식입니다.

1
2
3
4
5
public static <K,V> HashMap<K,V> newContacts() {
   return new HashMap<K,V>();
}
 
HashMap<String, Set<Integer>> contacts = newContacts();

하지만 Java7에서는 스펙 자체에 <> 연산자가 추가되어 굳이 불필요한 Static Factory Method를 안만들어도 된다는 장점이 있겠죠?
<>조차 사용 안하면 어때?라는 생각도 들지만 제너릭 지원 이전의 버젼 호환성을 위해서는 새로운 연산자의 등장은 어쩔 수 없는 선택이었나 봅니다.


String value in Switch


Switch문 내에서 문자열 사용이 가능해졌습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch (day) {
    case "NEW":
        System.out.println("Order is in NEW state");
        break;
    case "CANCELED":
        System.out.println("Order is Cancelled");
        break;
    case "REPLACE":
        System.out.println("Order is replaced successfully");
        break;
    case "FILLED":
        System.out.println("Order is filled");
        break;
    default:
        System.out.println("Invalid");
}

try-with-resource


JAVA 7 이전에는 DB Connection,File 등의 자원을 사용한 후에는 항상 try-catch-finally의 finally문에서 close()를 호출하여 닫아줘야했습니다. 항상 같은 패턴이지만 사용한 곳에서는 꼭 넣어줘야만 하는 로직이었습니다. 그래야 자원 반납이 되어 리소스 풀이 나지 않았으니, 그리고 또한 예상치 못한 오류가 발생할 수도 있기에 항상 finally 문 안에서 자원 반납하는 코드가 들어갔습니다.

JDK 7 이전

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void main(String args[]) {
    FileInputStream fin = null;
    BufferedReader br = null;
    try {
        fin = new FileInputStream("info.xml");
        br = new BufferedReader(new InputStreamReader(fin));
        if (br.ready()) {
            String line1 = br.readLine();
            System.out.println(line1);
        }
    } catch (FileNotFoundException ex) {
        System.out.println("Info.xml is not found");
    } catch (IOException ex) {
        System.out.println("Can't read the file");
    } finally {
        try {
            if (fin != null)
                fin.close();
            if (br != null)
                br.close();
        } catch (IOException ie) {
            System.out.println("Failed to close files");
        }
    }
}

하지만 JAVA 7에서는 try-with-resource 구문이 추가 되어 자동으로 자원반납을 해줍니다. 하지만 전제? 필요조건이 있습니다. 그것은 사용한 자원이 AutoClosable 혹은 Closeable 인터페이스를 구현한 경우에만 해당 구문을 사용가능합니다. 기본적으로 우리가 사용하는 자원반납용 객체들은 다행이 구현하고 있습니다.

JDK 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String args[]) {
    try (FileInputStream fin = new FileInputStream("info.xml");
            BufferedReader br = new BufferedReader(new InputStreamReader(
                    fin));) {
        if (br.ready()) {
            String line1 = br.readLine();
            System.out.println(line1);
        }
    } catch (FileNotFoundException ex) {
        System.out.println("Info.xml is not found");
    } catch (IOException ex) {
        System.out.println("Can't read the file");
    }
}



Underscore in Numeric literal


숫자형(정수,실수)에 _(underscore) 문자열을 사용 할 수 있습니다. 금융권 등에서 큰 숫자들을 다룰 경우 가독성 향상에 도움이 되겠죠?

1
2
3
4
5
int billion = 1_000_000_000; // 10^9
long creditCardNumber = 1234_4567_8901_2345L; //16 digit number
long ssn = 777_99_8888L;
double pi = 3.1415_9265;
float pif = 3.14_15_92_65f;

_ 위치가 다음과 같을 경우엔 컴파일 에러가 발생하니 주의해야합니다.

1
2
3
double pi = 3._1415_9265; // 소수점 뒤에 _ 붙일 경우
long creditcardNum = 1234_4567_8901_2345_L; // 숫자 끝에 _ 붙일 경우
long ssn = _777_99_8888L; // 숫자 시작에 _ 붙일 경우


Catching Multiple Exception Type in Single Catch Block


단일 catch 블럭에서 여러개의 Exception 처리가 가능해졌습니다. (Multi-Catch 구문 지원) JDK7 이전에는 2개의 Exception을 처리하기 위해서는 2개의 catch 블럭이 필요했었죠.

JDK 7 이전

1
2
3
4
5
6
7
try {
    //......
} catch(ClassNotFoundException ex) {
    ex.printStackTrace();
} catch(SQLException ex) {
    ex.printStackTrace();
}

JDK 7

1
2
3
4
5
try {
    //......
} catch (ClassNotFoundException|SQLException ex) {
   ex.printStackTrace();
}

단, Multi-Catch 구문 사용시 다음과 같이 하위클래스 관계라면 컴파일 에러가 발생하므로 주의하세요.

1
2
3
4
5
6
7
8
try {
    //...... }
catch (FileNotFoundException | IOException ex) {
    ex.printStackTrace();
}
 
Alternatives in a multi-catch statement cannot be related by sub classing, it will throw error at compile time :
java.io.FileNotFoundException is a subclass of alternative java.io.IOException at Test.main(Test.java:18)

Java NIO 2.0


JDK7에서 java.nio.file 패키지를 선보였는데요. 기본파일시스템에 접근도 가능하고 다양한 파일I/O 기능도 제공합니다. 예를 들면 파일을 옮기거나 복사하거나 삭제하는 등의 유용한 메소드들을 제공하며, 파일속성이 hidden인지 체크도 가능합니다. 또한 기본파일시스템에 따라 심볼릭링크나 하드링크도 생성 가능합니다. 와일드카드를 사용한 파일검색도 가능하며 디렉토리의 변경사항을 감시하는 기능도 제공합니다. 어쨋든 외부 라이브러리로 해결했던 많은 일들이 JDK안으로 녹아들었네요.


More Precise Rethrowing of Exception


다음 예제를 보면 try 블럭안에서 ParseException, IOException의 Checked Exception이 발생 할 수 있습니다. 각각의 Exception을 처리하기 위해 Multi-Catch 또는 다중 Catch 문으로 예외처리를 할 수 있지만 예제에서는 최상위 클래스인 Exception으로 처리하였습니다. catch 구문에서 발생한 예외를 상위 메소드로 전달하기 위해 throw 할 경우 메소드 선언부에 해당 예외를 선언해주어야하는데요.

JDK7 이전 버젼에서는 다음 예처럼 catch 구문내에서 선언한 예외 유형만 던질 수 있었습니다. (Exception 클래스),

1
2
3
4
5
6
7
8
9
public void obscure() throws Exception {
    try {
        new FileInputStream("abc.txt").read();
        new SimpleDateFormat("ddMMyyyy").parse("12-03-2014");
    } catch (Exception ex) {
        System.out.println("Caught exception: " + ex.getMessage());
        throw ex;
    }
}

하지만 JDK7에서는 좀 더 정확하게 발생한 Exception을 전달할 수 있습니다. (ParseException, IOException 클래스) 물론 이전과 같이 throws Exception으로 처리할 수도 있지만 메소드를 호출한 쪽에 좀 더 정확한 예외를 던져주는게 좋겠죠? 

1
2
3
4
5
6
7
8
9
public void precise() throws ParseException, IOException {
    try {
        new FileInputStream("abc.txt").read();
        new SimpleDateFormat("ddMMyyyy").parse("12-03-2014");
    } catch (Exception ex) {
        System.out.println("Caught exception: " + ex.getMessage());
        throw ex;
    }
}



ArrayList와 HashMap의 개선


JAVA 7 초기 버전까지만 해도 초기값을 명시하지 않은 Default 생성자로 ArrayList 생성시 내부적으로 크기가 10인 Object 배열을 생성했습니다. 생성하고 사용하지 않았는데도 해당 리스트객체는 크기가 10인 Object배열을 쥐고 있는 것이죠. HashMap 또한 Capacity가 16인 Map을 디폴트로 생성합니다.

  • JDK 1.6의 ArayList 생성자
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this(10);
}
 
public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    this.elementData = new Object[initialCapacity];
}


  • JDK 1.6의 HashMap 생성자
1
2
3
4
5
6
7
8
9
10
/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_CAPACITY];
    init();
}

JDK 1.7에서 변경된 사항

JDK 1.7 update 40 이후의 ArrayList는 다음과 같이 빈 Object 배열을 가지고 있습니다. static으로 선언되어 있으니 모든 ArrayList 인스턴스에서 공유하겠죠? Default 생성자로 ArrayList 생성시 해당 빈 Object를 초기값으로 설정합니다. 이전 버젼처럼 불필요하게 10인 Object 배열을 생성해 메모리를 낭비하지 않습니다. (Default 생성자의 주석은 아직 update가 안되었나보네요. ‘주석의 나쁜 예’겠죠?)

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * Shared empty array instance used for empty instances.
 */
private static final Object[] EMPTY_ELEMENTDATA = {};
 
/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

HashMap도 별반 다르지 않습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * An empty table instance to share when the table is not inflated.
 */
static final Entry<?, ?>[] EMPTY_TABLE = {};
 
/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry<K, V>[] table = (Entry<K, V>[]) EMPTY_TABLE;
 
public V put(K key, V value) {
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // .....
}


posted by 여성게
: