3 minute read

넘나 바쁜시기,,,, 그렇다고 클린코드 한권을 얼마동안 포스팅 하고 있는거냐?….

빨리 마무리 하자 ㅠ

3,4독 하려면,,, 1년 걸릴듯,,

목차

  1. 경계란?
  2. 우리코드 보호하기
  3. 외부코드 호환하기
  4. 외부 라이브러리 테스트하기

경계

  • 오픈소스 라이브러리를 안쓰는 코드는 없다.
  • 우리가 만든 코드를 외부와 병합해야함
  • 외부코드는 외부에서 만든 코드인데, 외부 시스템과 호출하거나 단순히 외부에서 만들어진 코드일수 있다.
  • 우리코드와 외부코드를 깔금하게 통합시키기 위해 경계를 잘 지어야 한다.

우리코드 보호하기

캡슐화 (Encapsulation)

객체의 실제 구현을 외부로 부터 감추는 방식 (TMI 하지 말자)

캡슐화 example

Sensor를 관리하자. Sensor는 외부에서 사용된다.

SensorId와 Sensor 객체로 사용하기 위해 Map을 사용한다. 하지만 Map을 그대로 사용하면 Map이가진 Clear() 기능이 외부로 노출되게 된다. Sensor의 외부코드 값으로써 Sensor 객체의 값만 가져오고 싶다. (캡술화를 사용한다.)

이해가 안될수 있으므로 Sensor 캡슐화를 예제를 통해 알아보자

Bad Code Example

Map<Sensor> sensors = new HashMap();
Sensor s = sensors.get(sensorId); 
  • Map인터페이스가 제공하는 clear등 불필요한 기능이 노출된다.
  • 외부 코드가 무분별하게 호출하면 sensor 데이터가 손상될수 있고, 의도와 멀어진다.

Good Code Example

public class Sensor {
    private Map<Sensor> sensors = new HashMap();

    public Sensor getById (String sensorId) {
        return sensors.get(sensorId); 
    }
}
  • 캡슐화를 통해 Map을 감춘다.
  • 원하는 기능만 사용한다.
  • 적절한 경계로 우리 코드를 보호한다.

3. 외부코드 사용하기

외부 코드를 사용할때도 우리가 원하는데로 사용하도록 하자. (adapter)

Adapter 패턴

외부 코드를 호출할때, 우리가 정의한 인터페이스대로 호출하기 위한 패턴

Adapter in Elasticsearch

elastic search 에서 이러한 외부 라이브러리를 Adapter pattern 를 통해 사용하였는지 알아보자,

package org.elasticsearch.http.nio;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;

import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.nio.FlushOperation;
import org.elasticsearch.nio.Page;
import org.elasticsearch.nio.WriteOperation;

import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.function.BiConsumer;

class NettyAdaptor {

    private final EmbeddedChannel nettyChannel;
    private final LinkedList<FlushOperation> flushOperations = new LinkedList<>();

    NettyAdaptor(ChannelHandler... handlers) {
        nettyChannel = new EmbeddedChannel();
        nettyChannel.pipeline().addLast("write_captor", new ChannelOutboundHandlerAdapter() {

            @Override
            public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
                // This is a little tricky. The embedded channel will complete the promise once it writes the message
                // to its outbound buffer. We do not want to complete the promise until the message is sent. So we
                // intercept the promise and pass a different promise back to the rest of the pipeline.

                try {
                    ByteBuf message = (ByteBuf) msg;
                    promise.addListener((f) -> message.release());
                    NettyListener listener = NettyListener.fromChannelPromise(promise);
                    flushOperations.add(new FlushOperation(message.nioBuffers(), listener));
                } catch (Exception e) {
                    promise.setFailure(e);
                }
            }
        });
        nettyChannel.pipeline().addLast(handlers);
    }

    public void close() throws Exception {
        assert flushOperations.isEmpty() : "Should close outbound operations before calling close";

        ChannelFuture closeFuture = nettyChannel.close();
        // This should be safe as we are not a real network channel
        closeFuture.await();
        if (closeFuture.isSuccess() == false) {
            Throwable cause = closeFuture.cause();
            ExceptionsHelper.maybeDieOnAnotherThread(cause);
            throw (Exception) cause;
        }
    }

    public void addCloseListener(BiConsumer<Void, Exception> listener) {
        nettyChannel.closeFuture().addListener(f -> {
            if (f.isSuccess()) {
                listener.accept(null, null);
            } else {
                final Throwable cause = f.cause();
                ExceptionsHelper.maybeDieOnAnotherThread(cause);
                assert cause instanceof Exception;
                listener.accept(null, (Exception) cause);
            }
        });
    }

    public int read(ByteBuffer[] buffers) {
        ByteBuf byteBuf = Unpooled.wrappedBuffer(buffers);
        int initialReaderIndex = byteBuf.readerIndex();
        nettyChannel.writeInbound(byteBuf);
        return byteBuf.readerIndex() - initialReaderIndex;
    }

    public int read(Page[] pages) {
        ByteBuf byteBuf = PagedByteBuf.byteBufFromPages(pages);
        int readableBytes = byteBuf.readableBytes();
        nettyChannel.writeInbound(byteBuf);
        return readableBytes;
    }

    public Object pollInboundMessage() {
        return nettyChannel.readInbound();
    }

    public void write(WriteOperation writeOperation) {
        nettyChannel.writeAndFlush(writeOperation.getObject(), NettyListener.fromBiConsumer(writeOperation.getListener(), nettyChannel));
    }

    public FlushOperation pollOutboundOperation() {
        return flushOperations.pollFirst();
    }

    public int getOutboundCount() {
        return flushOperations.size();
    }
}
  • 위 코드에서 처럼 엘라스틱서치 NettyAdapter.class 를 보면 (11~18) 라인에서 Netty라는 외부 코드를 사용하는것을 볼 수 있다.
  • NettyAdapter 은 외부코드인 NettyChannel을 감싸고 있다.
  • 이렇게 되면 NettyChannel 을 사용하기 위해선 NettyAdapter을 통해서 사용을 해야한다.
// HttpReadWriteHandler 
public class HttpReadWriteHandler implements ReadWriteHandler {

    private final NettyAdaptor adaptor;
    
    ...
    
    @Override
    public int consumeReads(InboundChannelBuffer channelBuffer) throws IOException {
        int bytesConsumed = adaptor.read(channelBuffer.sliceAndRetainPagesTo(channelBuffer.getIndex()));
        Object message;
        while ((message = adaptor.pollInboundMessage()) != null) {
            handleRequest(message);
        }

        return bytesConsumed;
    }

이렇게 하면 아래와 같은 장점들이 생기게된다.

  • 우리가 원하는 방식인 read할 때 ByteBuffer[]로 parameter를 보내면, 외부 코드인 nettyChannel에 ByteBuf 타입으로 parameter를 변환하여 전달한다.
  • Page[]타입 parameter로도 전달할 수 있다. Adapter에 메서드를 추가해 우리가 원하는 타입의 파라미터를 전달할 수 있다.
  • 만약 adapter를 통한 변환을 거치지 않았다면 nettyChannel에 데이터를 전달할 때마다 타입을 변환하는 과정이 필요했고, 이는 중복을 발생시켰을 것이다.

4. 외부 라이브러리 테스트하기

Learning Test를 작성해 라이브러리를 테스트한다.

  • 외부코드를 배우고, 안정성을 미리 파악할 수 ㅇ있다.
  • 학습테스트는 이해도를 높인다.
  • 외부 코드의 변경이 일어났을떄, 우리코드와 호환이 되는지 확인할 수 있다.

Categories:

Updated: