자라나라 개발머리

[데이터 분산 처리] 맵리듀스(MapReduce) 간단 설명/ Mapper, Reducer 구현해보기 본문

AI&Data

[데이터 분산 처리] 맵리듀스(MapReduce) 간단 설명/ Mapper, Reducer 구현해보기

iammindy 2024. 4. 28. 18:15

 

맵리듀스(MapReduce)는 데이터 분산 처리에 활용되는 프로그래밍 모델로서, 대량의 데이터를 세분화해서 각 머신에서 로직을 처리하고, 다시 합쳐 효율적으로 데이터 처리를 할 수 있도록하는 모델입니다.

 

맵 리듀스는 총 4가지 기능으로 구성되어있습니다.

 

1. Mapper: 입력 데이터를 받아서 세분화, key-value 쌍으로 변환, 필요한 로직을 적용하여 중간 결과를 생성

2. Reducer: 중간 결과를 받아서 동일한 키를 기준으로 그룹화하고 데이터를 합침

3. Partitioners: 맵 단계에서 생성된 중간(key, value) 쌍을 리듀스 태스크로 분배

4. Combiners:  최적화 도구. output을 로컬에서 미리 집계하여 네트워크 상으로 전송되는 데이터 양을 줄이는 역

 

전체적인 흐름은, Mapper가 InputData를 key-value로 세분화(Mapping)한 뒤 특정 로직을 처리합니다. 처리된 데이터를 Partitioner가 각 Reducer로 전달해주고, Reducer를 각 key와 value를 이용해 데이터를 합치고, Combiner를 거쳐 최적화 후 저장 시스템에 저장되거나 반환됩니다.

 

오늘은 javascript를 이용하여 MapReduce의 핵심이 되는 Mapper와 Reducer를 만들고, MapReduce 구조를 구현해보겠습니다.

예시로는, 맵리듀스 예시로 널리 이용되는 wordcount, 단어 숫자세는 기능을 구현해보겠습니다.

 

1. interface

// Mapper 인터페이스
class Mapper {
  constructor() {}
  map(input) {}
}
// Reducer 인터페이스
class Reducer {
  constructor() {}
  reduce(intermediateResults) {}
}

 

2. Mapper 구현

각 word를 key, 횟수를 value로 하는 map을 생성합니다.

class WordCountMapper extends Mapper {
  map(input) {
    const words = input.split(' '); //공백으로 나누기
    const wordCountMap = new Map();
    
    words.forEach(word => {
      if (wordCountMap.has(word)) {
        wordCountMap.set(word, wordCountMap.get(word) + 1); //word가 있으면 해당 value+1
      } else {
        wordCountMap.set(word, 1);
      }
    });
    return Array.from(wordCountMap.entries());
  }
}

 

3. Reducer 구현

mapper가 만든 데이터를 합치는 연산을 합니다.

class WordCountReducer extends Reducer {
  reduce(intermediateResults) {
    const reducerInput = intermediateResults.reduce((acc, current) => {
      current.forEach(([word, count]) => {
        if (acc.has(word)) {
          acc.set(word, acc.get(word) + count);
        } else {
          acc.set(word, count);
        }
      });
      return acc;
    }, new Map());
    return reducerInput;
  }
}

 

4. MapReduce 구조 사용

// 입력 데이터 (문자열 배열)
const inputData = [
  "apple banana apple",
  "banana orange",
  "orange apple banana"
];

const mapper = new WordCountMapper();
const reducer = new WordCountReducer();

// 맵 함수를 이용하여 중간 키-값 쌍 생성
const intermediateResults = inputData.map(input => mapper.map(input));

// 리듀스 함수를 이용하여 중간 결과 처리
const finalResult = reducer.reduce(intermediateResults);

// 결과 출력
finalResult.forEach((count, word) => {
  console.log(`${word}: ${count}`);
});

 

여기서는, mapper와 reducer를 단일로 사용했습니다. 단일로 사용하면 mapReduce 구조가 데이터 처리에 별 도움이 되지 않습니다.

데이터가 많아질수록 mapper와 reducer의 갯수를 늘려서 사용하면 그제서야 mapReduce구조가 빛을 발합니다.

const mapper1 = new WordCountMapper();
const mapper2 = new WordCountMapper();
const mapper3 = new WordCountMapper();

const reducer1 = new WordCountReducer();
const reducer2 = new WordCountReducer();

// 입력 데이터를 각각의 매퍼에게 전달
const intermediateResults1 = inputData.slice(0, 1).map(input => mapper1.map(input));
const intermediateResults2 = inputData.slice(1, 2).map(input => mapper2.map(input));
const intermediateResults3 = inputData.slice(2, 3).map(input => mapper3.map(input));

// 리듀스 함수를 이용하여 중간 결과 처리
const finalResult1 = reducer1.reduce(intermediateResults1);
const finalResult2 = reducer2.reduce(intermediateResults2);

// 중간 결과를 다시 리듀서에게 전달하여 최종 결과 생성
const finalResult3 = reducer2.reduce(intermediateResults3);

// 중간 결과를 하나로 합치기
const mergedIntermediateResults = [...finalResult1, ...finalResult2, ...finalResult3];

// 최종 결과 생성
const finalResult = reducer2.reduce([mergedIntermediateResults]);

// 결과 출력
finalResult.forEach((count, word) => {
  console.log(`${word}: ${count}`);
});

 

객체로서 3개를 만든 예시를 보였지만, 실제로는 서버가 될 것 입니다. 또한 각 처리를 비동기로 구성하여 코드를 짜야 진정한 MapReduce가 될 것 입니다!

 

이렇게 해서 간단하게 MapReduce 구조에 대해 알아보고, 간단 예제를 구현해보았습니다. 대용량 데이터 처리 시스템을 설계할 때, MapReduce 패턴을 이해하고 설계한다면 더욱 효율적인 시스템을 만들 수 있을 것 입니다. 읽어주셔서 감사합니다😀