java Generics && Netty Bootstrap

<이미지 출처 : http://stackoverflow.com/questions/11377248/return-type-of-java-generic-methods>


1
public abstract class AbstractBootstrap<extends AbstractBootstrap<B, C>, C extends Channel> 
implements Cloneable
cs

이번에 Netty의 소스코드를 분석 하는중 만난 한줄의 class 선언부 입니다. 오늘은 이 한줄의 의미를 파악하기 위해 자바의 Generics 에 대해서 이야기 해보고자 합니다.


1. Generics

Generics는 자바 1.5부터 지원되기 시작한 개념입니다. Generic을 적용하는 가장 큰 이유는 타입을 클래스, 인터페이스, 메소드를 정의할 때 파라미터화 할 수 있다는 것입니다. 메서드 선언에 사용되는 파라미터 형식처럼, 타입 파라미터는 다른 입력값에 대한 동일한 코드 재사용을 할 수 있는 방법을 제시합니다.  또한 Generic을 적용하면 다음과 같은 장점을 가질 수 있습니다.

1. compile 시점에 강력한 타입체크를 합니다.

<무인자 자료형>

1
2
3
List list = new List();
list.add("1");
list.add(2);    //오류 없이 compile완료  
cs

<generic 적용>

1
2
3
List<String> list = new List<String>();
list.add("1");
list.add(2);    //compile시점에 오류 발생
cs

Generic이 도입되기 이전에는 Map, List 와 같은 Collection에는 어떤 타입의 객체가 들어가고 처리 되는지 상위 코드를 통해 확인해야 했습니다. 그러나 개발하는 과정에서 개발자가 실수로 다른 객체를 집어넣어도 compile시점에는 오류가 발생하지 않기 때문에 runtime시 코드가 수행(어디선가 get을 이용하여 꺼내는 시점)되는 도중 실제 문제의 원인(3번째 줄과 같이 int값을 add한 지점)에서 멀리 떨어진 지점에서 문제가 발생 할 수도 있습니다. 이런경우 디버깅 하는 작업은 쉽지 않습니다. 하지만 Generic을 적용하면 컴파일 시점에 오류가 발생하므로 다른 객체가 입력될 가능성을 사전에 차단합니다.


2. 형변환을 명시해줄 필요가 없습니다.

 <무인자 자료형>

1
2
3
List list = new List();
.....
String a = (String)list.get(0);//String 형변환
cs

 <generic 적용>

1
2
3
List<String> list = new List<String>();
.....
String a = list.get(0);  //형변환이 필요 없다.
cs

Generic을 적용하게 되면 compiler가 알아서 형변환을 적용하므로 따로 (String)과 같은 형변환 을 해줄 필요가 없습니다. 왼쪽의 경우 list에 어떠한 값이 들어가 있는지 알지 못하며 만약 사용자가 (String)이 아닌 (Integer)와 같이 형변환을 시도 했다면 오류가 발생합니다. 하지만 오른쪽과 같이 generic을 적용했다면 따로 형변환을 적어주지 않으며 자동으로 100% 안전한 형변환이 이루어 집니다.


위와 같이 generic을 사용하게 되면 프로그램은 더 안전하고 명확해지지만 제일 위의 선언된 한줄과 같이 프로그램이 복잡해 진다는 단점이 생기게 됩니다. 자바 Generic에 대한 문서는 많이 찾을 수 있으므로 기본적인 설명은 여기까지 하고 위의 한줄을 이해하기 위해 필요한 각각의 용어와 의미에 대해서만 간단히 설명후 분석해 보도록 하겠습니다.



2. Generics의 용어 및 관습

일단 Generic을 이해하기 위해서는 사용되는 용어의 의미를 명확하게 알아둘 필요가 있습니다. 

용어 

 예

 실 형인자 
 (actual type parameter)

 String

 형식 형인자 
 (formal type parameter)

 E 

 한정적 형인자 
 (bounded type parameter)

 <E extends Number>

 무인자 자료형 
 (raw type)

 List 

 형인자 자료형 
 (parameterized type)

 List<String> 

 제네릭 자료형 
 (generic type)

 List<E> 

 비한정적 와일드카드 자료형 
 (unbounded wildcard type)

 List<?> 

 한정적 와일드카드 자료형
 (bounded type parameter)

 List<? extends Number> 

 재귀적 형 한정 
 (recursive type bound)

 <T extends Comparable<T>>

 제네릭 메서드
 (generic method)

 static <E> list<E> asList(E[] a) 

 자료형 토큰 (type token)

 String.class


Type 파라미터에는 관습적으로 단일 대문자를 따릅니다. Java SE에 구현된 각 API에서는 다음과 같은 규칙으로 많이 사용되어있는 모습을 확인 할 수 있습니다.. 물론 위의 Netty처럼 B, C 처럼 커스텀한 Type Parameter를 사용할 수도 있습니다.

  • E - Element 
  • K - Key
  • V - Value
  • N - Number
  • T - Type
    * 복수개의 T를 사용하는 경우는 T1, T2, T3,.... Tn 과 같은 형식으로 시퀀스를 붙여서 처리하기도 합니다.


3. Netty Bootstrap

Netty의 Bootstrap의 구조는 다음과 같이 추상클래스의 AbstractBootstrap과 그를 extends하는 Bootstrap과 ServerBootstrap 2개의 class가 있습니다. 그 구조를 생각하며 본격적으로 아래의 선언을 한 조각 조각 분리하여 의미를 이해해 보도록 하겠습니다.

1
public abstract class AbstractBootstrap<extends AbstractBootstrap<B, C>, C extends Channel>  
implements Cloneable
cs


1. extends Channel

C 는 한정적 형인자 용법이 적용되어 형인자 C는 반드시 Channel의 하위 자료형이여야 한다고 제한을 두고 있습니다. 모든 자료형은 자기 자신의 하위 자료형이기도 하므로 C는 io.netty.channel.Channel (interface) 또는 Channel을 extends한 ServerChannel (interface)로 한정될 수 있습니다. 


2. AbstractBootstrap<B, C>

추상클래스인 AbstractBootstrap은 형인자 B와 C를 가집니다. 이를 통해 AbstractBootstrap내부에는 형인자 B와 C를 이용하는 메서드들이 있다는 것을 파악할 수 있습니다.
  ex) public B group (EventLoopGroup group){ ... } , public B channel(Class<? extends C> channelClass){ ... } , 등등


3. extends AbstractBootstrap<B, C>

형인자 B는 형인자가 포함된 표현식으로 형인자를 한정하고 있습니다.  이를 재귀적 자료형 한정 용법이라 합니다. 쉽게 풀어 쓰면 형인자 B는 반드시 형인자 B와 C를 가지는 AbstractBootstrap의 하위형이여야 한다고 정의하고 있습니다. Netty에서는 AbstractBootstrap를 extends한 Bootstrap( client )과 ServerBootstrap ( server )이 바로 이 B로 한정됩니다.  


4. AbstractBootstrap<extends AbstractBootstrap<B, C>, C extends Channel>

이제 모든 Generic선언을 조합한 문장입니다. 이를 종합하면 추상클래스 AbstractBootstrap은 형인자 B와 C를 가지는 자기자신을 extends 한 B와 Channel을 extends한 C를 형인자로 가지는 generic class입니다. 



-_-;;  아직도 명확하지 않은 감이 있지만 아래 AbstractBootstrap을 extends한 2개의 class를 보면 확실하게 이해가 갈 것입니다. 

1
2
3
4
5
6
7
8
9
public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {
    ......
}
 
 
 
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {
    ......
}
cs

위 선언을 보면 Bootstrap이나 ServerBootstrap는 각기 AbstractBootstrap extends하면서 자신을 형인자로 넣어주고 있습니다. 그럼 AbstractBootstrap의 B를 반환하는 generic메서드들은 어떤 타입으로 반환을 할까요??  바로 형인자 B자리에 넣어준 타입(자기자신)으로 자동으로 casting되어 반환이 되는것입니다. 이게 왜 중요하냐면 Bootstrap과 ServerBootstrap은 method-chaining패턴이 적용되어 다음과 같은 형태의 코드로 초기화가 가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Bootstrap b = new Bootstrap();
b.group(workerGroup)
 .channel(NioSocketChannel.class)
 .handler(new ChannelInitializer<SocketChannel>(){ 
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeClientHandler());
    }
});
 
 
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
 .channel(NioServerSocketChannel.class)
 .handler(new ChannelInitializer<SocketChannel>(){
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeServerHandler());
    }
});
cs

위의 각 Bootstrap이 설정되는 과정을 보면 channel, handler등은 AbstractBootstrap에 정의되어있는 메서드로 generic이 적용됨으로 인해 각 메서드가 반환하는 반환타입 B는 자동으로 해당class의 값으로 자동으로 casting됩니다. 그렇게 함으로서 코드의 재사용성도 높이고 있습니다.   

만약 14라인의 channel에 자료형 토큰인 NioServerSocketChannel.class 대신에 NioSocketChannel.class를 넣으려고 했다면 어떻게 될까요? 컴파일 시점에 오류로 표현되어 잘못 넣고 싶어도 그럴수가 없습니다.  public B channel(Class<? extends C> channelClass)인 channel메서드의 C는 ServerBootstrap인경우에는 ServerChannel을 extends한 C로 한정적 형인자 용법에 의해 제한되어 있기 때문입니다. 

하지만 3라인의 channel메서드에 NioServerSocketChannel.class 를 자료형 토큰으로 전달하는것은 가능합니다. 왜냐하면 Channel C는  Channel의 모든 하위자료형으로 한정되어 있으며 ServerChannel은 Channel을 extends한 하위자료형이기 때문입니다. 이 부분에 대한것도 미리 차단할 수 있었으면 좋았을것 같습니다만 구조적으로 어쩔수 없었던것 같습니다. 혹시 이부분을 자세히 알고 계신분은 알려주시면 감사하겠습니다.








New Multi-Channel Dynamic CMS