Backend/Java

Stream에 Decorator 패턴 써먹기

비비빅B 2023. 4. 3. 02:40

"데이터 가공이 필요해요."

공통 기능(엑셀 다운로드)에서 메모리를 절약하기 위해 대용량 데이터를 조회할 경우 마이바티스 `Cursor` 타입을 매개변수로 받도록 설계했다. 여기서 문제를 명확히 하기 위해 커서 데이터가 전부 소비됐으면, 예외를 던져주기로 했다.

class ExcelFile {
	public void write(Cursor<?> cursor) {
    	if (cursor.isConsumed()) throw new IllegalArgumentException("데이터가 없어요.");
        try (cursor) {
            // cursor 데이터를 엑셀 파일에 작성하는 로직
        }
    }
}

공통 기능을 사용하는 개발자는 커서 데이터 타입을 인자로 넘겨줘야 하는데, 흔히 개발자들이 DB에서 조회된 이 커서 데이터를 인자로 넘겨주기 전에 가공해야 하는 경우가 생긴다. 흔한 컬렉션(List, Set, Map) 타입은 개발자들이 인자로 넘겨주기 전에 가공하면 되지만, 문제는 이 커서 타입 데이터는 스트림이라는 것이다. 커서 데이터를 한번 읽고 넘겨준다면, 공통 기능이 제대로 동작하지 않을 것이다.

Cursor<Object> objects = repository.findAll(); // db 데이터 조회
for (obj : objects) {
    // 객체 수정
}
ExcelFile file = new ExcelFile();
file.write(objects); // **예외 발생** "데이터가 없어요."

 

개발자들이 공통 기능을 사용하기 전에 데이터를 수정할 수 있으면서, (메모리 절약을 위해) 커서 데이터는 유지해야 한다. 이를 해결하기 위해 데코레이터 패턴을 사용한 경험을 작성하고자 한다.


Mybatis Cursor Stream

마이바티스에서 제공하는 데이터 타입인데, 대용량 쿼리 조회 시 발생할 수 있는 메모리 부족 문제를 해결해 준다. 쿼리문 실행 결과를 마치 페이징처리된 것처럼 조금씩 어플리케이션 메모리에 스트림 방식으로 조금씩 로드하는 것이다.

하지만 스트림방식의 특성상, 한 번 소비되면 재사용 될 수 없기 때문에 데이터를 여러 번 읽을 수 없다.

 

Cursor (mybatis 3.5.13 API)

All Superinterfaces: AutoCloseable, Closeable, Iterable All Known Implementing Classes: DefaultCursor Cursor contract to handle fetching items lazily using an Iterator. Cursors are a perfect fit to handle millions of items queries that would not normally f

mybatis.org

 


데코레이터(Decorator) 패턴

패턴에 대한 것은 잘 설명된 글이 많아 간단히만 설명하겠다.

먼저 데코레이터 패턴에 대해 한 단어로 설명하자면 객체꾸미기라고 말할 수 있다. 객체를 같은 타입으로 계속해서 겉에 꾸며나가는 것이다.

흔히 예로 많이 드는 카페에서 주문을 받는 상황에서, 카페라떼에 휘핑크림추가 + 두유변경 + 사이즈업과 같은 여러 개의 옵션을 추가하는 기능을 구현할 때 적합한 패턴이다.

java.io 패키지를 보면 대부분이 데코레이터 패턴을 사용했다. 같은 타입의 데코레이터를 원하는 만큼 감싸서 기능을 추가하도록 설계되었다. 아래 코드는 백준에서 습관적으로 작성하는 읽기∙쓰기 작업 속도를 향상시키기 위해 버퍼 기능을 추가한 것이다.
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
     BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out))) {}

Cursor Decorator

커서 데이터를 읽을 때, 특정 로직을 수행할 수 있도록 하는 추가 기능 데코레이터를 제공했다. 

class T CursorDecorator<T> implements Cursor {
    private final Cursor<T> cursor;
    private final Consumer<T> consumer;
    
    public CursorDecorator(Cursor<T> cursor, Consumer<T> consumer) {
    	this.cursor = cursor;
        this.consumer = consumer;
    }
    
    @Override
    public void forEach(Consumer<? super T> action) {
        for (c : cursor) {
            this.consumer.accept(c);
            action.accept(c);
        }
    }
}

 

이제 데이터 가공이 필요한 개발자들은 아래와 같이 사용할 수 있다.

Cursor<Object> objects = repository.findAll(); // db 데이터 조회
Cursor<Object> modifiedObjects = new CursorDecorator(objects, (obj) -> {
    // 객체 수정
});
ExcelFile file = new ExcelFile();
file.write(modifiedObjects); // 성공

남들이 보기엔 별 문제도 아닌 간단한 상황일 수도 있지만, 개인적으로 예전에 공부했던 디자인패턴을 실제 문제 상황에 적용해 해결했다는 것이 뿌듯하기도 하고 재밌었다.