본문 바로가기
코딩과 알고리즘

몽고DB 콘솔에서 맵리듀스(MapReduce) 기술

지난 챕터에서는 몽고DB의 가장 기본 중의 기본, CRUD 에 대해 다루어 보았는데요.

itadventure.tistory.com/383

 

PHP + 몽고DB 크루드! ( CRUD )

'크루드'하면 웬지 "딴-딴-딴딴 따라다~' 배경음악이 등장하는 미션 임파서블'의 '톰 크루즈'가 생각이 납니다 :) 톰 크루즈는 불운한 어린 시절을 보냈지만 이를 극복하고 결국 멋진 연기 인생을

itadventure.tistory.com

이번 시간에는 몽고 DB에서 통계를 산출하는데 매우 유용한 맵리듀스(Map Reduce)에 대해서 다루어보도록 하겠습니다. 약간 난이도가 있으니 정확한 이해를 위해서는 몇번 읽어보셔야 하실 수 있습니다.

맵리듀스란 무엇일까요? + 리듀스입니다 :)
말장난일 수도 있지만 분석해보니 맵(Map)을 산출하는 함수와 그것을 추려서 간추리는(Reduce) 함수를 만들어 놓고 두 함수를 조합해서 사용하는 기술인데요. 구글에서 빅 데이터를 처리하기 위해 만든 개념이라네요.

개념만으로는 이해가 안될테니 실례로 들어가봅시다.
아주 간단히 예를 들면 크레이 대학교에 3개 학과가 있고, 각각 3개씩 학급이 있다고 칩시다.
모든 학급은 정원이 25명이고 현재 수강생은 아래처럼 상이합니다.

컴퓨터사이언스 : 3개 학급 ( 모든학급정원:25명, 1반:23명, 2반:21명, 3반:24명 )
사회복지 : 3개 학급 ( 모든학급정원:25명, 1반:20명, 2반:23명, 3반:25명 )
실용영어 : 3개 학급 ( 모든학급정원:25명, 1반:19명, 2반:12명, 3반:20명 )

이 때 각 학과별로 학급수와 학생수 합을 사람이 산출하기 위해서는 어떻게 해야 할까요?
가장 간단한 방법은 하나씩 세는 것입니다.
컴퓨터 사이언스 학과에서 학급이 하나, 둘, 셋 총 3개이고
어디보자~ 학생이 23 + 21 + 24니까 총 68명이고, 다음 학과는..
한사람이 이렇게 세려면 한 학과당 5초 정도 걸린다고 할때, 총 15초가 걸리겠지요.
아마도 학과가 120개 정도 된다고 한다면 10분이 걸릴 겁니다.

만일 10 사람이 동원되어 일을 나누어서 처리한다고 한다면 어떨까요?
그렇다면 120개 학과를 세는 데에는 1분으로 단축되겠지요.

이 렇게 일을 나누는 것을 분산처리라고 하는데요. ( 분산 서버와는 다른 개념입니다. )
몽고db에서는 맵리듀스 기술로 불립니다.
우리는 이 기술이 어떻게 개발되었는지는 알 필요는 없습니다.
다만 몽고db에서 통계산출을 위해서는 맵 리듀스를 사용하는 것이 가장 효율성이 좋고,
우리는 이 기술을 사용하는 방법만 익히면 되는거니까요.

먼저 위와 같은 샘플 데이터를 만드는 PHP 코드를 만들어 보았습니다.

우선 아래 내용을 share.php 파일명으로 저장해주세요.
참고로 dump() 함수는 php에서 기본제공되는 var_dump 함수를 좀 더 간편하게 볼 수 있도록 응용한 함수인데,
크레이가 직접 개발한 거니 자유롭게 사용하셔도 좋습니다.

<?
// 공유파일

// 개발자 모드일때만 아래 두줄 사용. 사용하지 않을 때는 주석 처리하세요.
error_reporting(E_ALL & ~E_NOTICE); // 노티스는 제외
ini_set("display_errors", 1); // 오류 및 경고를 화면에 표시

// var_dump() 함수를 더 간단히 볼 수 있도록 크레이가 개발한 함수
function dump($var)
{
	echo "<xmp>";
	ob_start(); var_dump($var);
	$result = explode("\n", ob_get_clean());
	for($i=0;$i<count($result);++$i)
	{
		if(strpos($result[$i], "string(") !== false)
		{
			$result[$i]=substr( $result[$i], strpos($result[$i], ")")+1, strlen($result[$i]));
		}
		if(strpos($result[$i], "int(") !== false)
		{
			$pos1 =strpos($result[$i], "int(") + 4;
			$pos2 = strpos($result[$i], ")");
			$val = substr($result[$i], $pos1, $pos2-$pos1);
			$result[$i]=$val;
		}
		if(strpos($result[$i],"array(")!==false)
		{
			$result[$i]=substr($result[$i], 0, strpos($result[$i],"array("))." {";
		}
	}
	for($i=count($result)-1;$i>=0;--$i)
	{
		if(substr($result[$i],-2,2)=="=>")
		{
			$result[$i]=str_replace("[\"", "", $result[$i]);
			$result[$i]=str_replace("\"]", "", $result[$i]);
			$result[$i].=trim($result[$i+1]);
			unset($result[$i+1]);
			$i++;
		}
	}
	echo implode("\n", $result);
	echo "</xmp>";
}

// 몽고DB 커넥션
$mongo_connection="mongodb://uadmin:4321@localhost:27017/crayUniversity";
$manager = new MongoDB\Driver\Manager($mongo_connection);

// 업서트 싱글 옵션
$upsertsingle_option=[
	'multi' => false, // false(처음 1개만 업데이트), true( 모두 업데이트 )
	'upsert' => true    // 없으면 삽입
];


?>

그리고 아래 파일은 share.php 와 같은 폴더에 mongo_sample.php 로 저장해 주세요.
이 샘플코드는 앞의 3개의 학과와 각각의 학급 정보를 입력해 줍니다.
트리 구조로 입력하여 주고 각각의 학급은 배열형태로 입력됩니다.

<?php
// 공유파일.
include_once(dirname(__FILE__)."/share.php");

echo "모든 학과를 삭제하고 새로 입력합니다.<br/>";

// 벌크준비
$bulk = new MongoDB\Driver\BulkWrite;

// 모든 자료를 삭제
$bulk->delete([]);

// 데이터 트리구조로 삽입
$bulk->update(
	['code' => '10010'],
	['$set' => [
		'name' => '컴퓨터사이언스',
		'classInfo' => [
			[ 'classNo'=>1, 'limitstudent'=>25, 'nowstudent'=>23 ],
			[ 'classNo'=>2, 'limitstudent'=>25, 'nowstudent'=>21 ],
			[ 'classNo'=>3, 'limitstudent'=>25, 'nowstudent'=>24 ]
		]
	]],
	[ 'multi' => false, 'upsert' => true ]
);

$bulk->update(
	['code' => '10011'],
	['$set' => [
		'name' => '사회복지',
		'classInfo' => [
			[ 'classNo'=>1, 'limitstudent'=>25, 'nowstudent'=>20 ],
			[ 'classNo'=>2, 'limitstudent'=>25, 'nowstudent'=>23 ],
			[ 'classNo'=>3, 'limitstudent'=>25, 'nowstudent'=>25 ]
		]
	]],
	[ 'multi' => false, 'upsert' => true ]
);

$bulk->update(
	['code' => '10012'],
	['$set' => [
		'name' => '실용영어',
		'classInfo' => [
			[ 'classNo'=>1, 'limitstudent'=>25, 'nowstudent'=>19 ],
			[ 'classNo'=>1, 'limitstudent'=>25, 'nowstudent'=>12 ],
			[ 'classNo'=>1, 'limitstudent'=>25, 'nowstudent'=>20 ]
		]
	]],
	[ 'multi' => false, 'upsert' => true ]
);

// 벌크 실행
$result = $manager->executeBulkWrite('crayUniversity.department', $bulk);
dump($result);
?>

이제 웹브라우저에서 퍼블릭URL/mongo_sample.php 를 실행하면 
아래와 같은 결과가 보이실 겁니다. var_dump 함수보다는 좀 더 보기 쉽지요? :)
여기서 nUpserted=>3 으로 3개의 자료가 업서트된 것을 확인하실 수 있습니다.

모든 학과를 삭제하고 새로 입력합니다.
object(MongoDB\Driver\WriteResult)#3 (9) {
  nInserted=>0
  nMatched=>0
  nModified=>0
  nRemoved=>3
  nUpserted=>3
  upsertedIds=>{
    [0]=>{
      index=>1
      _id=>object(MongoDB\BSON\ObjectId)#4 (1) {
        oid=>"5fccd0d4691330092bf2aaaf"
      }
    }
    [1]=>{
      index=>2
      _id=>object(MongoDB\BSON\ObjectId)#5 (1) {
        oid=>"5fccd0d4691330092bf2aab0"
      }
    }
    [2]=>{
      index=>3
      _id=>object(MongoDB\BSON\ObjectId)#6 (1) {
        oid=>"5fccd0d4691330092bf2aab1"
      }
    }
  }
  writeErrors=>{
  }
  writeConcernError=>NULL
  writeConcern=>object(MongoDB\Driver\WriteConcern)#7 (0) {
  }
}

이제 AWS 콘솔창에서 몽고DB 콘솔에 접속하신 다음에,

mongo -u uadmin crayUniversity

학과 정보를 확인해 봅시다.

db.department.find().pretty()

아래와 같이 3개의 학과 정보가 입력된 것을 확인하실 수 있는데요.

{
	"_id" : ObjectId("5fccd125691330092bf2aab5"),
	"code" : "10010",
	"classInfo" : [
		{
			"classNo" : 1,
			"limitstudent" : 25,
			"nowstudent" : 23
		},
		{
			"classNo" : 2,
			"limitstudent" : 25,
			"nowstudent" : 21
		},
		{
			"classNo" : 3,
			"limitstudent" : 25,
			"nowstudent" : 24
		}
	],
	"name" : "컴퓨터사이언스"
}
{
	"_id" : ObjectId("5fccd125691330092bf2aab6"),
	"code" : "10011",
	"classInfo" : [
		{
			"classNo" : 1,
			"limitstudent" : 25,
			"nowstudent" : 20
		},
		{
			"classNo" : 2,
			"limitstudent" : 25,
			"nowstudent" : 23
		},
		{
			"classNo" : 3,
			"limitstudent" : 25,
			"nowstudent" : 25
		}
	],
	"name" : "사회복지"
}
{
	"_id" : ObjectId("5fccd125691330092bf2aab7"),
	"code" : "10012",
	"classInfo" : [
		{
			"classNo" : 1,
			"limitstudent" : 25,
			"nowstudent" : 19
		},
		{
			"classNo" : 1,
			"limitstudent" : 25,
			"nowstudent" : 12
		},
		{
			"classNo" : 1,
			"limitstudent" : 25,
			"nowstudent" : 20
		}
	],
	"name" : "실용영어"
}

이제 여기서 맵리듀스 기술로 각 학과별 학급수 합과 학생수 합을 산출하도록 합시다.
우선 맵(Map) 함수를 먼저 만들어 봅시다. 명칭은 꼭 map 이 아니어도 되긴 합니다.

var map = function(){
  for(var i=0;i<this.classInfo.length;++i)
  {  
    var key=this.code;
    var value={ 'classCount':1, 'totalStudent': this.classInfo[i].nowstudent };
    emit(key, value);
  }
};

자바스크립트 아닌가요? 네, 맞습니다. 몽고DB는 자바 스크립트를 사용합니다.
이 함수는 각각의 학과에 대해 한번씩 실행될 예정이며,
함수 안에서는 오로지 한개의 학과만을 대상으로 합니다.

먼저 함수 내에 this 라는 객체가 기본으로 제공되는데요.
이는 컬렉션 내의 하나의 도큐먼트, 여기서는 하나의 학과에 대한 정보가 입력됩니다.
그래서 this.code는 학과코드, this.classInfo 는 학급정보를 의미합니다.

학급정보는 배열이기 때문에 아래 코드는 학급정보의 각 배열 요소를 반복하며 순회합니다.

for(var i=0;i<this.classInfo.length;++i)
{
  :
}

그리고 하나의 학급을 셀때마다 해당 학과의 1개의 학급수와 해당 학급의 학생 수를 집계하도록 분출(emit:내뿜다)합니다. 참고로 여기서는 합계를 산출하지 않고 다만 분출만 할 뿐이지요.

    var key=this.code;
    var value={ 'classCount':1, 'totalStudent': this.classInfo[i].nowstudent };
    emit(key, value);

모든 학과에 대해 이 함수가 실행되면, 내부적으로 아래와 같은 자료가 쌓입니다.
하지만 아직 실행된 것은 아니고 이렇게 실행될 예정인 것이지요.

code:10010, [ {classCount:1, totalStudent:23 },
              {classCount:1, totalStudent:21 }
              {classCount:1, totalStudent:24 } ],
              
code:10011, [ {classCount:1, totalStudent:20 }
              {classCount:1, totalStudent:23 }
      :

다음으로 리듀스(Reduce) 함수는 아래와 같은데요.

var reduce=function(key, valueArray){
  var result={
    'classCount':0,
    'totalStudent':0
  };
  for(var i=0;i<valueArray.length;++i){
    result.classCount+=valueArray[i].classCount;
    result.totalStudent+=valueArray[i].totalStudent;
  }
  return result;  
};

이 함수는 맵함수를 통해 수집된 자료가 한 덩어리씩 순차 입력됩니다.
맵 함수에서 키값이 code 로 사용되었기 때문에,
학과코드 10010 에 대해 먼저 하나의 덩어리가 함수내에 입력이 되는데요.
이 때 key 파라미터에는 '10010' 값이,
그리고 valueArray 에는 아래와 같이 3개의 자료가 한꺼번에 배열로 입력이 됩니다.

[ {classCount:1, totalStudent:23 },
  {classCount:1, totalStudent:21 }
  {classCount:1, totalStudent:24 } ]

여기서 리듀스 함수는 하나의 학과에 대해서만 처리해주면 되는데요.
먼저 학급수와 학생수 합계를 산출하기 위해 아래와 같이 2가지 속성을 가진 변수를 정의해주고,

var result={
  'classCount':0,
  'totalStudent':0
};

집계된 자료의 갯수만큼 반복을 돌려줍니다.

for(var i=0;i<valueArray.length;++i){
  :
}

반복문 안에서는 파라미터의 각 배열 요소에 대한 합계를 result 변수의 속성에 누적해주고

result.classCount+=valueArray[i].classCount;
result.totalStudent+=valueArray[i].totalStudent;

끝으로 그 결과를 반환해 줍니다.

return result;

이 것이 리듀스 함수의 역활입니다. 하지만 아직 기능이 실행된 것은 아닙니다.

이제 이 2개의 함수를 조합해서 실행하는 실제적인 명령은 아래와 같습니다.

db.department.mapReduce(
  map,
  reduce,
  { out: 'department_out' }
);

내용인즉, department 컬렉션에 대해 리듀스를 수행하는데,
맵함수로는 map 을 사용하고, 리듀스 함수로는 reduce를 사용하라~ 라는 의미입니다.
그리고 그 최종 결과를 department_out 컬렉션에 저장하라는 의미인데요.
이 명령을 내릴때 실제적으로 맵리듀스가 실행이 됩니다.

어떤 결과가 나왔는지 확인해볼까요?

db.department_out.find().pretty()
{
        "_id" : "10011",
        "value" : {
                "classCount" : 3,
                "totalStudent" : 68
        }
}
{
        "_id" : "10010",
        "value" : {
                "classCount" : 3,
                "totalStudent" : 68
        }
}
{
        "_id" : "10012",
        "value" : {
                "classCount" : 3,
                "totalStudent" : 51
        }
}

각각의 학과코드는 _id 값이 보관되었고,
학급수는 3학급씩,
총 학생수는 68, 68, 51명입니다.

이 것으로 맵 리듀스에 대한 짧은 해석이 되었을 수 있습니다만,
본 예제는 맵과 리듀스가 1:1로 대응된 경우이며,
맵과 리듀스는 N:M으로 갯수가 서로 다를수 있습니다.

이를테면 학생수 분포별로 1~10, 11~20, 21~30명인 분포별 학급수를 산출한다거나,
학과명을 ㄱ~ㅁ, ㅂ~ㅈ, ㅊ~ㅎ 까지 분류해서 각각의 학급수, 학생수를 산출한다고 하면,
서로간의 갯수는 아마도 일치하지 않겠지요.

향후 맵리듀스의 활용 방법에 대해 기회가 되면 추가로 다뤄보도록 하겠습니다.

필요하신 분에게 도움이 되셨을지 모르겠습니다.
오늘도 여기까지 읽어주셔서 감사합니다.


성경 말씀을 믿어 영혼의 복을 누리시기를 소원합니다.

하나님이 능히 모든 은혜를 너희에게 넘치게 하시나니
이는 너희로 모든 일에 항상 모든 것이 넉넉하여
모든 착한 일을 넘치게 하게 하려 하심이라

- 고린도후서 9장 8절 말씀 -

Cray Fall님의
글이 좋았다면 응원을 보내주세요!