관리 메뉴

HAMA 블로그

[이더리움에서 배우는 Go언어] 강력하게 밀착된 컴포지션 본문

Go

[이더리움에서 배우는 Go언어] 강력하게 밀착된 컴포지션

[하마] 이승현 (wowlsh93@gmail.com) 2019. 1. 10. 10:47

컴포지트(composite) 디자인 패턴과 이름이 헷갈린 컴포지션은 UML 측면에서는 연관(Aggregation) 하고도 헷갈리기도 하는데 이 글에서는 컴포지션과 연관을 구분하지 않겠다. 컴포지션이란 간단히 말해 내가(객체)가 가지고 있어야 하는 특성을 외부에서 가져오는 것을 말하는데, 가져오는 방식이 상속을 통하는 방법과 다르게 외부에서 주입되는 방식이다. 상속의 경우는 폴리모피즘이 반드시 필요하면 제한적으로 사용하되, 컴포지션을 통해서 객체를 구축하는 방식을 추천한다. 

이 글에서는 Golang에서 컴포지션을 어떻게 지원하는지 살펴 볼 것이다. 자바/C++ 보다는 훨씬 깊숙히 임베디드 되는 모습을 보게 될 것이란 것을 미리 귀뜸해 둔다.

먼저 자바의 컴포지션을 살펴보자. 

abstract class Form{
public void run(){};
}
class WolfForm extends Form{
public void run (){
System.out.println("늑대처럼 달린다!!!");
}
}
class Player{
public String name;
public Form form;
public Player(String name, Form form){
this.name = name;
this.form = form;
}
}
public class Main {
public static void main(String[] args) {
Player player = new Player("hama", new WolfForm());
player.form.run();
}
}

Player 객체는 Wolf 폼 객체를 포함(컴포지션)하고 있으며, Player객체에서 Wolf폼 객체의 기능을 사용하려면 당연히player.wolf.run() 식으로 내부에서 wolf 로 접근한 후에 사용한다. 근데  Golang에서는??
그냥 player.run()으로 접근가능하다.

Golang에서의 컴포지션 

*  객체/값의 구분을 하지 않았음을 알려드린다. 이 글에서는 포인팅되는지 값 자체인지의 구분이 중요치 않다.

type Form interface{
Run()
}
type WolfForm struct{
}
func (wolf *WolfForm) Run() {
fmt.Print("늑대처럼 달린다!!!");
}
type Player struct{
Form
}
func main(){
player := &Player{Form : &WolfForm{}}
player.Run()
}

Player 구조체는 Form을 혼연일체 (임베디드) 되어 가지고 있게 된다. 즉 자신의 메소드인 양 그냥 사용 해 버릴수 있다. 어찌보면 강력해 보이기도 하는데, 이게 코드읽기/관리가 될 때는 좀 피곤 할 수 있을 것이다. 
자바의 경우 player.wolf.run()으로 관계가 명시적으로 코드에 보이는 반면에, Golang식의 player.run()은 도대체 run은 어디에 구현되어있는지 알수 없으면, 찾아보는 수고를 해야한다. player 내부의 Form 인터페이스를 확인해야하며, 이 Form인터페이스를 덕타이핑으로 구현상속한 녀석을 또 찾아봐야한다. player 내부에 Form인터페이스 말고 다른것들도 있다면?? 두배로 힘들어지게 된다. 아래 이더리움 코드를 통해 피부로 느껴보자.

이더리움코드에서의  컴포지션 
[이더리움 코어] DevP2P 소스코드 분석 (feat. golang) 를 같이 읽으면  좋을 거 같다.

이더리움에서 P2P 부분을 보면 리모트서버와 접속이 이루어짐과 동시에 conn 객체가 만들어 지는데,
conn 구조체는 아래와 같다. 위에 공부해 보았듯이 transport 가 컴포지션 되었다는 것을 알 수 있다.

type conn struct {
fd net.Conn
transport
flags connFlag
cont chan error
id discover.NodeID
caps []Cap
name string
}

conn 객체에는 net.Conn이라는 소켓파일디스크립터과 그 fd 를 가지고transporting 하는 방식을 책임지는 transport 인터페이스를 구현상속한 녀석을 가지고 있다. 그럼 transport는 무엇을 하는 것일까? transport 인터페이스를 살펴보자.

type transport interface {
doEncHandshake(prv *ecdsa.PrivateKey, dialDest *discover.Node)(discover.NodeID, error)
doProtoHandshake(our *protoHandshake) (*protoHandshake, error)
MsgReadWriter
close(err error)
}

리모트노드와 접속이 이루어지면 Encrypt와 Protocol 정보를 교환해야하는 책임을 가지고 있으며, 
MsgReadWriter 인터페이스를 구현상속한 또 어떤 녀석을 통해서 리모트노드와 읽고,쓰기를 하는거 같다.
이더리움에서는 메세지를 시리얼라이즈 하는데  RLP 라는 인코딩/디코딩 알고리즘을 사용하는데, MsgReadWriter 인터페이스를 구현한 녀석은 RLP의 능력도 가지고 있다. 

이제 conn 객체의 생성 코드를 살펴보자. 확인해 볼것은 2가지인데 하나는 transport 인터페이스는 어떤 객체로 만들어지나이고,  transport 인터페이스가 가지고 있는 MsgReadWriter 인터페이스 또한 어떤 객체로 만들어 지나이다.

func (srv *Server) SetupConn(fd net.Conn, flags connFlag, dialDest *discover.Node) error {
...
c := &conn{fd: fd, transport: srv.newTransport(fd), flags: flags, cont: make(chan error)}
...
}

transport 에 srv.newTransport(fd) 가 컴포지션 되고 있다.

srv.newTransport = newRLPX

func newRLPX(fd net.Conn) transport {
return &rlpx{fd: fd}
}

rlpx 객체를 만드는 것 같다. (내부에 fd 를 가지고 있으며, rlp 엔코딩/디코딩으로 시리얼라이즈를 한 후에 fd로 소켓read/write호출)  

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpx 구조체는 위와 같으며, 이 구조체는 transport 인터페이스가 가지고 있는 메소드를 모두 덕타이핑(자바처럼 extends로 명시적으로 상속한다고 말하지 않아도 동일한 함수시그니처를 구현하고 있으면 폴리모피즘적으로 같다라고 봄) 으로 포함 할 것이다. 그래야만 rlpx 가 transport가 될 수 있으니~ 아래와 같이~

func (t *rlpx) doProtoHandshake(our *protoHandshake) (their *protoHandshake, err error) {

....서로가 가진 프로토콜 정보 확인 ...난 eth63을 가지고있는데 넌 무엇을?
}

func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) {
....서로가 가진 암호화 정보 확인 ECC기반...auth정보 교환..
t.rw = newRLPXFrameRW(t.fd, sec)
...
}

마지막으로 transport 인터페이스에서 선언한 MsgReadWriter 또한 transport를 구현상속한 rlpx 가 가지고 있어야 하는데 어디 있을까? 다시 rlpx 구조체를 보자.

type rlpx struct {
fd net.Conn

rmu, wmu sync.Mutex
rw *rlpxFrameRW
}

rlpxFrameRW가 뭔가 읽고,쓰기와 관련되어 있는 감이 오지 않나? (자바라면 명시적으로 보였을텐데..) 
그럼rlpxFrameRW가 생성되는 곳을 살펴봐야한다. 이것은 위의 doEncHandshake 메소드 안에 있다.

t.rw = newRLPXFrameRW(t.fd, sec)

newRLPXFrameRW 는 이렇다.

func newRLPXFrameRW(conn io.ReadWriter, s secrets) *rlpxFrameRW {
...
return &rlpxFrameRW{
conn: conn,
enc: cipher.NewCTR(encc, iv),
dec: cipher.NewCTR(encc, iv),
macCipher: macc,
egressMAC: s.EgressMAC,
ingressMAC: s.IngressMAC,
}
}

rlpxFrameRW 라는 구조체를 만든다. 이쯤되면 감 잡힐 것이다. 이 구조체는 MsgReadWriter 인터페이스를 구현 상속 했다는 것을..(RW라는 이름을 통해서 유추해야한다. 따라서 Golang에서 이름짓기는 더 중요할 것이다) 

type MsgReadWriter interface {
MsgReader
MsgWriter
}}

type MsgReader interface {
ReadMsg() (Msg, error)
}
type MsgWriter interface {
WriteMsg(Msg) error
}

MsgReadWriter 인터페이스는 MsgReader/MsgWriter라는 인터페이스를 또  가지고 있으며, rlpx 에서는 ReadMsg/WriteMsg만 덕타이핑해 놓으면 될 것이다. 아래 코드에서 확인하자.

func (t *rlpx) ReadMsg() (Msg, error) {
t.rmu.Lock()
defer t.rmu.Unlock()
t.fd.SetReadDeadline(time.Now().Add(frameReadTimeout))
return t.rw.ReadMsg()
}

func (t *rlpx) WriteMsg(msg Msg) error {
t.wmu.Lock()
defer t.wmu.Unlock()
t.fd.SetWriteDeadline(time.Now().Add(frameWriteTimeout))
return t.rw.WriteMsg(msg)
}

이렇게 이더리움에서 컴포지션이 이루어지는 모습을 살펴보았다. 어떻게 느껴졌는가?? 강력하다? 헷갈린다?? 모든 기술은 장,단이 있으며 사람의 취향에 따라서 달라질 것이다. 자바스러움이 좋은 사람, Golang스러움이 좋은 사람~개인적으로는 자바스러움이 좋긴한데, Golang의 동시성  철학과 빠른컴파일속도에 점수를 주어서 Golang에 집중하고 있다. 언어 선택에 더 중요한것은 내가 지금 하는 일에 어떤 언어가 더 가깝냐는것이 겠지만~



Comments