0. 목차

1. 개요

2. 문제 상황

3. 가장 쉬운 접근

4. 캐싱

5. 결론

1. 개요

본 글에서는 로그 파일을 파싱하고 구조화 할때 사용한 메모리 캐싱에 대해 다룹니다. 파일을 순차적으로 탐색하며 진행할 때 이미 지나친 파일에 대한 정보가 필요했습니다. 이를 해결하는 방식으로 한 차례 더 순차 탐색을 진행할 수도 있었지만 내부적으로 캐싱 방식을 구현하여 해결한 케이스입니다. 스프링 캐시나 별도의 캐시 라이브러리를 사용하지 않고 직접 구현한 경험을 소개합니다.

2. 문제 상황

아래와 같은 SimpleLogEventReader는 LogData를 String으로 받아 이를 읽고 LogEvent로 변환하는 작업을 수행합니다. 이때 LogEvent는 내부에 Message를 비롯한 복잡한 메시지 타입의 객체를 갖고 있어 이들은 LogEvent를 Root로 하는 LogAggregate로 분류됩니다.

@Component
@RequiredArgsConstructor
public class SimpleLogEventReader implements LogEventReader {

    private static final String LOG_EVENT_PATTERN = "정규식 패턴";

    private final EventActionFactory eventActionFactory;
    private final MessageFactory messageFactory;

    @Override
    public List<LogEvent> read(String logData) {
        return Arrays.stream(logData.split("\\n"))
           		// ...
                .collect(Collectors.toList());
    }

    public LogEvent createLogEvent(Matcher matcher) {
        EventAction eventAction = eventActionFactory.fromString(matcher.group(5));
        Message<?> Message = messageFactory.createMessage(matcher.group(7), recentLogEvents.get(extractUUIDFromMessage(matcher.group(7))));

        LogEvent logEvent = LogEvent.builder()
                .파라미터1(LocalTime.parse(matcher.group(1)))
                .파라미터2(matcher.group(2))
                .파라미터3(matcher.group(3))
                .파라미터4(matcher.group(4))
                .파라미터5(eventAction)
                //...
                .build();

        return logEvent;
    }
}

LogEvent는 그 속성에 따라 Request와 Response로 분류될 수 있습니다. 즉, 어떤 LogEvent는 Request 타입이며, 어떤 LogEvent는 Response 타입입니다. 즉, LogEvent 각자는 고유한 개별 객체이지만 도메인 백그라운드가 적용된 논리적인 존재로서 pair 형태로 존재함을 의미합니다.

여기서 Request와 Response는 UUID를 공유하며 이를 통해 연결됩니다. 따라서 Request 속성의 LogEvent는 스스로의 pair로서 response를 인지할 필요가 있으며, 반대도 마찬가지입니다.

이때, Request와 Response 타입은 Message를 통해 판단되어지기 때문에 LogEventReader는 Message 생성을 다음과 같은 MessageFactory에 위임합니다.

@Component
@RequiredArgsConstructor
public class MessageFactoryImpl implements MessageFactory {

    private final PayloadActionFactory payloadActionFactory;

    @Override
    public <P> Message<P> createChannelMessage(String rawData, LogEvent pairLogEvent) {
        JsonArray jsonArray = new JsonParser().parse(rawData).getAsJsonArray();
        String uniqueId = jsonArray.get(1).getAsString();

        return Arrays.stream(MessageFormat.values())
                .filter(format -> format.getMessageCode() == jsonArray.get(0).getAsInt())
                .findFirst()
                .map(format -> (Message<P>) format.createCMessage(uniqueId, payloadActionFactory, jsonArray, pairLogEvent))
                .orElse(Message.nullObject());
    }
}

MessageFactory는 MessageFormat의 타입을 판단해주고, 각각의 타입 포맷에게 다시 스스로의 생성 책임을 맡깁니다. 이때 과거의 LogEvent에 대한 정보를 알아야 하는 필요성이 생기는데 앞서 살펴보았듯 LogEvent는 UUID를 통해 Request와 Response가 연결되어 있는 상황입니다. Response 타입은 스스로 Payload를 인지하지 못하며 Request에 대응되는 Payload여야 하기 때문에 Request를 알아야 자기 자신의 Payload를 규정할 수 있기 때문입니다.

다음과 같이 Response Format은 UUID가 일치하여 pair로 판명난 LogEvent를 받아와 해당 LogEvent의 Request Payload가 무엇인지를 이해해야 해당 Payload에 맞는 Response로 스스로를 생성할 수 있습니다.

public class SomethingResultFormat implements MessageFormatter {
    @Override
    public <P> Message<P> create(int messageTypeId, String uniqueId, PayloadActionFactory payloadActionFactory, JsonArray jsonArray, LogEvent pairLogEvent) {

        if (pairLogEvent == null) {
// 처리 후 리턴 로직
        }

// 처리 후 리턴 로직
    }
}

이러한 플로우가 진행된다고 할 때, Response 타입에 해당하는 LogEvent는 pair로서 Request 타입의 LogEvent를 인지해야 합니다.