Docker 에 대한 생각

최근의 S/W환경을 보면 서비스는 점점 더 복잡해 지고 있고, 기술도 다양하고, 외부 환경도 급격하게 변화하고 있다.전통적인 Monolithic Architecture 환경에서는 이런 변화에 발빠르게 대응하는 것이 사실상 불가능 하다고 볼 수 있다. 이런 복잡하고도 빠르게 변하는 환경에 대응하기 위해 많은 시도들이 있어 왔으며, 최근에는 MSA(Micro Service Architecture)라는 사상이 주목을 받고 있다.

MSA라는 것은 독립적이고 단순한 서비스로 전체 서비스를 구성하고, 그에 따라 독립적인 팀이 각 서비스의 주인(개발과 운영에 대한 책임,권한을 모두 가짐)이 되는 것을 말한다.(마틴 파울러 아저씨의 블로그http://martinfowler.com/articles/microservices.html 에 보면 MSA의 개념에 대해 잘 정리가 되어 있다.) 이와 같이 서비스를 micro화 하게 되면 , 분산환경을 통한 scale-out 확장에도 용이하고 monolithic 방식에 비해 배포에 대한 부담이 줄어든다(전체 서비스에 영향을 주지 않고 배포를 할 수 있음). 장애가 발생해도 그 영향의 범위를 국소적으로 제한시킬수 있게 된다.

이렇게 MSA 사상을 도입하면 복잡성과 빠른 변화에 대응하는 것은 가능해 지지만 그에 비해 변경 및 추가된 어플리케이션을 build하고 테스트하고, 배포(ship)하고, 실행시키는 것은 쉬운 문제는 아니다.

분산 환경에 배포되어 있는 많은 machine들이 모두 동일한 환경을 가지고 있으리라는 보장이 없기 때문에 어플리케이션을 분산환경에 배포를 하기 위해서는 환경정보를 잘 확인하고 배포를 해야 한다. 자칫하다가는 장애를 만들어 내기 일쑤다. 개발환경과 테스트 환경, 운영환경이 모두 다를 것이기 때문에 어플리케이션이 동일하게 동작하리라는 보장을 하기가 힘들어 진다.

이런 배경에서 “어디에서나 동일하게 동작하는 소스코드”를 제공하기 위해서 등장한 기술이 Docker라고 볼 수 있다. Docker는 개발자가 개발한 어떤 어플리케이션이든(언어에 관계없이) 그 어플리케이션과  그 어플리케이션이 돌아가게 하는 최소한의 디펜던시만 Container에 담아 shipping할 수 있게 한다. 이런 작업을 “Dockerized”라고 부르게 되는데, 이렇게 Dockerized가 되면 어떤 환경에서도 – OSX, Windows, Redhat, Ubunto, 노트북, 서버, 클라우드 등- 동일하게 동작하는 것을 보장할 수 있다. 구글에서 돌아가는 모든 어플리케이션은 모두 container에 담겨져 있으면 이 Container갯수만 약 20억개 정도 된다고 한다.

이런 개념은 얼핏 보면 VMs과 별로 다르지 않은 것처럼 보이기도 하는데 아래 그림을 보면 그 차이를 이해할 수 있다.

개인 PC에서 개발하면서 개발환경에 웹서버, was, DB등을 설치해 본 경험이 있을 것이다. 당연하지만 이 개발환경에서의 path라든지 각종 시스템 파라미터 등은 테스트 환경이나 운영환경에서와 다를 수 밖에 없으므로 이런 환경의 차이로 인해 “내 환경에서만 동작하는 어플리케이션”이라는 비아냥을 받게 될때도 있을 것이다.

Docker를 이용하여 개발에 필요한 환경들을 모두 Container에 담아, 그 이미지를 QA에게 전달해서 테스트를 부탁할 수 있다. 테스트가 완료가 되면 그 이미지를 그대로 운영환경에 배포해서 바로 서비스를 시작 할수 있게 된다. Docker 이미지는 Git 처럼 버전관리가 가능하기 때문에 장애가 발생했을 경우 이전 버전으로 쉽게 롤백도 가능하다.

Docker Hub(hub.docker.com)에 가보면 여러가지 Docker 이미지가 공개되어 있는 것을 확인해 볼 수 있다. MySql 를 비롯해서 Redis, NginX 등 많은 제품들이 Docker Container로 제공되고 있으며 이 이미지들을 사용한다면 여러분의 노트북에서나 클라우드 환경의 서버에서나 동일하게 동작하는 것을 보장받을 수 있을 것이다.

Docker를 사용하기 위해서는 JVM처럼 각 플랫폼마다 별도로 제공되는 Docker플랫폼을 받아서 설치를 해야 한다. https://docs.docker.com/installation/#installation 에 보면 지원하고 있는 플랫폼을 확인해 볼 수 있는데, Amazon EC2와 Google Cloud Platform 이 눈에 띈다.

Docker 이미지를 Docker Hub에서 내려받고(pull), 수정내용을 반영(commit)하고 다시 Docker Hub로 올리는(Push) 과정은 Git과 거의 동일하다고 볼 수 있다.

Docker 에 패키징되어 있는 어플리케이션은 docker라는 명령을 통해 접근할 수 있으며 가이드 https://docs.docker.com/userguide/ 를 통해 사용방법은 크게 어렵지 않게 익힐수 있을거라 보여진다.

Docker로 인해 앞으로 S/W개발의 많은 모습들이 달라지게 되지 않을까 하는 생각이 든다.

logBack을 사용해 보자

얼마전에 Spring의 기능을 참조할 일이 있어 Spring의 레퍼런스 프로젝트인 spring-petclinic 를 내려받게 되었다.
프로젝트 구조를 살펴보는 중 생소한 파일이 발견되었다.
스크린샷 2015-01-18 20.28.36

바로 ‘logback.xml’ 파일이었는데, 직감적으로 “아! 어느새 log4j를 대체하는 기술이 나온 모양이구나”하는 생각이 들었다. 한동안 기술 트랜드에서 너무 벗어나 있었다는 생각이 다시금 들면서 이놈이 뭔지 좀 살펴보기로 했다.

logback 홈페이지에 들어가 보면 logback을 이렇게 소개한다.

“log4j 프로젝트의 후계자로 만들어졌다”

log4j개발자가 거의 지난 10년동안 log4j가 사용되면서 불편(?)했던 내용들을 모두 개선해서 logback이라는 제품을 만들어 낸 듯한 느낌이다.

개발자가 소개하는 log4j 에서 logback으로 옮겨야 하는 여러 이유들 중 특히 내가 관심이 가는 몇가지 항목들은 다음과 같다.

  • Automatic Reloading Configuration file
    • 필자에게는 logback을 도입해야 하는 제일 중요한 이유라고 생가되는 기능이다.
    • log4j를 사용하던 시절(?)에는 로깅 레벨을 WARN -> INFO 등으로 변경하게 되면 Application(WAS)를 다시 시작했어야 했지만 logback 에서는 그럴 필요가 없다. 설정 파일을 변경하면 지정된 시간이 지나게 되면 파일의 변경을 감지하고 다시 읽어들인다.
  • Graceful Recovery from I/O Failures
    • 기존 log4j에서 FileAppender 를 사용하여 로그를 파일서버에 저장하는 경우, 파일서버에 문제가 있어 I/O fail이 나면 Application(was)를 재시작 했어야 했지만 logback에서는 파일 서버가 정상으로 돌아오면 에러 상황에서 빠르게 자동으로 복구가 된다.
  • Automatic Compress
    • 아마도 기존에 log4j를 사용해서 파일로 로그를 떨어뜨려 놓는 경우 별도의 배치 프로그램을 이용해서 로그파일을 압축하고 다른 곳으로 옮겨놓는 작업을 많이 해 보았을 것이라 생각된다. logback에서는 로그 파일의 자동 압축을 지원하고, 시간(파일의 갯수)이 지난 파일을 자동으로 삭제하는 기능도 제공을 한다.
    • 로그 파일을 거대해서 압축하는데 시간이 오래 걸린다 해도 비동기 방식으로 동작하므로 application의 성능에는 영향을 주지 않는다.
  • Prudnect Mode
    • 다수의 JVM 인스턴스에서 같은 로그 파일을 사용하여 로그를 “안전하게” 기록할 수 있는 기능을 제공한다고 한다. 어떤 제한이 있는것 같다고 하는데 내부적으로 비동기 큐를 사용하고 있는 것이 아닌가 추측이 된다.
    • 정확하게 확인해 보지는 않았지만 이런 모델들이 보통 문제를 많이 일으키는 관계로 꼭 필요하지 않다면 되도록 사용하지 않는 것이 좋을 것 같다는 생각이다.
  • Conditional Processing Configurations
    • 기존에는 개발환경과 운영환경에 별도의 설정파일을 적용-개발환경에서는 로깅레벨을 DEBUG, 운영환경에서는 WARN-시키기 위해 로깅 설정파일을 분리해 놓고 사용하던 경험들이 있을 것이라 생각된다.
    • logback에서는 설정 파일에 if-then-else 의 조건문을 사용할 수 있어 설정 파일 하나로만 개발-운영 환경을 모두 커버할 수 있도록 만드는 것이 가능한다.
  • Stack Traces with Packaging Data
    • Logback을 사용해서 Stack Trace를 남기게 되면 각 패키지의 아티팩트 정보와 버전정보를 출력해 준다. 배포가 문제가 있다면 바로 확인이 가능하게 해주는 유용한 기능이라 생각된다.
    • 스크린샷 2015-01-18 21.02.29

프로젝트에 적용해 보기

1. 디펜던시 설정

logbook 사용하기 위해서는 logback-core, logback-classic, slf4j-api jar들이 필요한다. 디펜던시는 아래와 같이 설정한다.

메이븐을 사용하고 있다면 – [ pom.xml ]

<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-core</artifactId>
	<version>1.1.2</version>
</dependency>
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-classic</artifactId>
	<version>1.1.2</version>
</dependency>
<dependency>
	<groupId>org.slf4j</groupId>
	<artifactId>slf4j-api</artifactId>
	<version>1.7.10</version>
</Dependency>
<!— 조건부 설정을 사용하려면 아래의 디펜던시를 추가해 주어야 한다. —>
<dependency>
	<groupId>org.codehaus.janino</groupId>
	<artifactId>janino</artifactId>
	<version>2.7.7</version>
</dependency>

그래들을 사용하고 있다면 – [build.gradle]

compile 'ch.qos.logback:logback-classic:1.1.2'

compile 'ch.qos.logback:logback-core:1.1.2'

compile 'org.slf4j:slf4j-api:1.7.10'
// 조건부 설정을 사용하려면 아래의 디팬던시를 추가해 주어야 한다
compile 'org.codehaus.janino:janino:2.7.7'

2. logback.xml 파일의 설정은 아래와 같이 한다. 주석으로 처리한 부분을 잘 읽어보자.

<?xml version="1.0" encoding="UTF-8"?>
<!-- 30초마다 설정 파일의 변경을 확인한다. 파일이 변경되면 다시 로딩한다 -->
<configuration scan="true" scanPeriod="30 seconds">

    <!-- 외부 설정파일을 사용할 수 있다. -->
    <property resource="resource.properties"/>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern> %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${APP_HOME}/sujemall-webapp.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 파일이 하루에 한개씩 생성된다 -->
            <fileNamePattern>sujemall-webapp.%d{yyyy-MM-dd}.log</fileNamePattern>

            <!-- maxHIstory 설정은 위 부분에 롤링 정책에 따라 적용되 된다고 보면된다.
             위 설정데로 라면 30일이 지난 파일은 삭제가 된다.-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>

        <encoder>
            <pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="org.springframework" level="info"/>
    <logger name="org.hibernate" level="debug"/>
    <logger name="com.sujemall.webapp" level="debug"/>
    <if condition='property("RUN_MODE").equals("SERVICE")'>
        <then>
            <!-- 설정파일에 RUN_MODE 가 SERVICE로 정의되어 있으면 로깅 레벨을 INFO로 지정 -->
            <root level="info">
                <appender-ref ref="console"/>
                <appender-ref ref="FILE"/>
            </root>
        </then>
        <!-- 설정파일에 RUN_MODE 가 SERVICE로 정의되어 있지 않으면  로깅 레벨을 DEBUG 지정 -->
        <else>
            <root level="debug">
                <appender-ref ref="console"/>
                <appender-ref ref="FILE"/>
            </root>
        </else>
    </if>
</configuration>

3. 로그 찍기는 slf4j api를 사용하는 기존의 방식과 크게 다르지 않다. 마커를 이용해서 로그를 찍어봤다.

package com.sujemall.webapp.service;

import com.sujemall.webapp.model.User;
import com.sujemall.webapp.repository.UserRepository;
import com.sujemall.webapp.utils.CommonUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Created by yhlee on 15. 1. 12..
 */

@Service
public class UserServiceImpl implements UserService {

    static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);

    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public User enrollUser(User user) {

        user.setPassword(CommonUtils.hashString(user.getPassword()));
        user = userRepository.save(user);
        LOGGER.info("#### Success Save User : UserName is  {}, Email is {} " ,user.getUserName(), user.getMainEmail());
        return user;
    }
}

위와같이 설정하고 application을 돌려보면 아래와 같이 로그가 잘 찍히는 것을 확인할 수 있다.

스크린샷 2015-01-18 22.08.02

Java8의 람다 표현식 ( Lambda Expression)

Java8 에 Lambda식이 새로 추가되었다. 람다 표현식에 대해서 잘 모른다면 그 표현식을 봤을 때 문맥을 이해하기가 쉽지 않다.

Java에서의 lambda expression을 어떻게 사용하고 왜 사용하는지 간단하게 정리를 해 보고자 한다.

람다식은 함수형 언어에서 선호하는 “정의”연산자 “->” 를 사용한다. 최근에 함수형 언어가 주목을 받고 있고, Java에서도 이를 지원하려는 움직임으로 보인다.

Scala, Erlang를 비롯한 함수형 언어라는 것의 의미를 먼저 살펴보자.

“함수형 언어”는 “명령형 언어”와 대조적인 관계에 있다. 명령형 언어에서는 상태를 바꾸는 것을 강조하는 것과 달리, 함수형 언어에서는 함수를 정의하는 것에 중점을 둔다.

명령형 언어에서 “a = 1” 이라고 선언을 하는 것은 “a라는 변수에 1을 담아라” 라는 의미이지만, 함수형 언어에서는 a 를 1로 정의한다는 의미로 사용한다. ( 그런 의미에서 R같은 언어에서는 대입 연산자를 A <- 1 이라고 표기하기도 한다)

대입과 정의가 비슷한 것 같아 보이지만 큰 차이가 있다. 대입은 프로그램의 수행중에 언제든지 값이 변경될 수 있고, 정의는 값이 변경될 수 없는 것을 의미한다.

즉 수학에서  f(x) = x + 1 이라고 정의된 함수가 있다면 입력값 x 에 대해 항상 x + 1 이라는 동일한 결과값을 기대할 수 있게 된다. ( 명령형 언어에서라면 변수의 값이 변경될 수 있으므로 같은 입력에 대해 결과값이 달라질 수 있음)

입력에 대해 결과값이 동일하다는 특징은 멀티 스레딩 환경에서 큰 장점이 있고(Thread safe), 이러한 특징은 손쉽게 병렬 프로그래밍을 할 수 있게 해 준다.

이런 특징이 멀티코어 프로세싱이 요구되고 있는 근래의 프로그래밍 환경에서 함수형 언어가 다시 주목을 받게 된 이유라고 볼 수 있다.

Java에서 Lambda 표현식을 사용하는 목적은, 예전처럼 변수를 직접 전달하여 그 값을 변경함으로서 흐름을 처리하지 않고, 행위 자체(Behavor Parameter)를 전달함으로서 함수형 프로그램이 지향하는 바를 얻기 위함이라고 생각된다.사실 이러한 목적으로는 기존에 이미 익명 클래스(Anonymous Class)를 통해 해결할수도 있었던 문제이지만, Lambda식을 사용함으로 해서 코드를 좀 더 깔끔하고 가독성 좋게 만들수 있게 된 것이다.

그리고 공식적으로 “자바는 함수형 프로그래밍을 지원한다!”라고 얘기하려고 한것이 아닐까 하는 생각도..

Lambda표션식이 생소한 이유중 하나가 바로 전에는 클래스가 없는 함수를 사용할 수 없었지만, 이 Lambda라는 놈은 클래스 없이 함수 구현체가 존재하기 때문에 자바에 친숙한 사용자일수록 처음에는 어색하게 느껴질 수 있을 것 같다.

“Java 에서 Lambda 표현식은 추상 메소드가 하나만 있는 인터페이스(Funtional Interface)를 익명 클래스 대신 구현할 수 있게 해 주는 방법” 이라고 할 수 있다.

추상 메소드가 하나만 있는 인터페이스들 중에 우리가 잘 알고 있는 것들이 있다. Java.util.Comparator , java.lang,Runnable 등이 그것인데 Comparator를 이용해서  lambda 표현식을 사용할때와 그렇지 않을 때 어떻게 코드가 달라지는지 살펴보자.

아래와 같이 User 클래스가 있다.


public class User implements Comparable{

     private Long userId;
     private String userName;
     private String emailAddress;
     private Date joinDate;

     // getter &amp;amp; setter 코드 생략

     @Override
     public int compareTo(Object o) {

         User otherUser = (User) o;
         if (this.userId &gt; otherUser.getUserId()) {
             return 1;
         } else if (this.userId &lt; otherUser.getUserId()) {
             return -1;
         } else {
             return 0;
         }
     }
 }

이 User 의 목록이 있을 때 userId를 기준으로 sorting을 하는 코드를 익명 클래스를 이용해서 만들면 아래와 같다.

userList.sort(new Comparator&lt;User&gt;() {
   
@Override
  
 public int compare(User user1, User user2) {
      
       return user1.compareTo(user2);
   
  }

});

익명 클래스를 사용하지 않고 “Lambda”표현식을 이용하여 아래와 같이 코딩할 수 있다.

userList.sort((User o1, User o2) -&gt; o1.compareTo(o2));

이렇게 Lambda 표현식은 함수의 argument로 전달이 될수도 있고, 아래와 같이 변수에 할당하는 것도 가능하다,

Comparator &lt;User&gt; userComparator = (User u1, User u2) -&gt; u1.compareTo(u2);


userList.sort(userComparator);

아래 그림을 통해 Lambda 표현식을 어떻게 작성하는지 살펴보자.
lamgda_exp

Lambda 표현식은 “->” 를 기준으로 왼쪽과 오른쪽으로 나누어 지는데,

왼쪽의 (User 01, User 02) 부분은 functional interface – 추상 메소드가 하나만 있는 인터페이스- 에 선언된 추상메소드의 argument 를 나타낸다.
오른쪽 부분은 추상메소드의 구현내용 – implementation-을 적어주는데 return type은 interface 의 추상메소드 signature를 따라 자동으로 결정된다. ( Runnable의 경우 void 임 )

Spam Mail Classifying

우리가 메일 서비스를 이용할 때 스팸 메일들이 자동으로 스팸함으로 이동되는 것을 볼 수 있다. 메일 서비스들은 도착한 메일이 스팸인지 아닌지 여부를 어떻게 판단해서 스팸으로 분류할 수 있는 것일까?

기본적으로는 “베이지안 추론“이라는 통계적 추론 방법을 통해 메일을 분류하게 되는데, 베이지안 추론이란, 사전확률을 통해 사후확률을 추론하는 것이다.

사용자들이 메일을 수신하고 스팸메일함으로 이동시킨 메일을 대상으로 “스팸으로 분류된 메일에 특정 단어들이 나타날 확률“을 미리 계산해 놓으면(사전확율), 어떤 특정 단어들이 나타났을 때 이 메일이 스팸일 확율(사후 확률)을 구할 수 있게 된다. 즉, 기존의 데이터를 이용해서 스팸메일로 분류할 방법을 학습(learning)시키는 것이라고 볼 수 있다.

1. Data 구하기

Model Training & Test  용 데이터는 UCI – Machine Learning Repository (https://archive.ics.uci.edu/ml/datasets/Spambase) 에서 가져와서 사용한다.

사용할 데이터는 https://archive.ics.uci.edu/ml/machine-learning-databases/spambase/ 에서 확인할 수 있으며, spambase.zip 파일을 다운로드 받도록 한다.

spambase.zip 파일을 다운로드 받아서 압출을 풀면 3개의 파일을 확인해 볼 수 있다.

  • spambase.DOCUMENTATION
    • 데이터파일(spambase.data)에 대한 description 이다. 데이터의 구조와 각 컬럼에 대한 설명을 가지고 있다.
  • spambase.data
    • 우리가 사용할 데이터 파일이다.
    • 총 4,601건의 데이터(row)가 있으며, 1,813개가 스팸으로 분류되어 있다.
    • 총 58개의 컬럼을 가지고 있으며, 1개의 컬럼은 spam/non-spam 분류를 나타내고 나머지 57개 컬럼은 각 컬럽의 타입 속성이 나타난 percentager값(0~100)이다.
  • spambase.names
    • Spambase.data 의 헤더 정보가 들어있다.

2. Tidy Dataset 만들기

Spambase dataset 에 헤더정보가 없으므로 헤더를 구성한다.


load data file
spam.base.df&lt;- read.csv(&quot;./data/spambase.data&quot;,header=FALSE)

# load header file
# MakeHeader.py 로 spambase.names 에 있는 헤더 정보를 추출하여 headers 파일로 기록해 두었음.
spam.base.header &lt;- read.csv(&quot;./data/headers&quot;,header=FALSE,stringsAsFactors = F)
# column 명에 특수문자가 포함되어 있으므로 make.names를 하지 않으면 as.formula에서 오류가 난다.
headers &lt;- make.names(spam.base.header[,1],unique=T)
headers &lt;- c(headers,&quot;spam&quot;)
names(spam.base.df) &lt;- headers
spam.base.df$spam &lt;- as.factor(ifelse(spam.base.df$spam &gt; 0.5, &quot;spam&quot;,&quot;non-spam&quot;))

spambase.names 파일에서 헤더정보를 추출하는 파이썬 코드

__author__ = 'yhlee'
 
import re
 
regex = re.compile('^(.+):\s+continuous\.')
 
f = open(&quot;data/spambase.names&quot;)
out = open(&quot;data/headers&quot;, &quot;w&quot;)
 
for line in f.readlines():
    # print(line)
    m = regex.match(line)
    if m:
        # print line
        header =  m.group(1) + &quot;\n&quot;
        out.writelines(header)

3. Training Data

Machine Learning 모델을 선택해야 한다. 우리가 원하는 것은 특정 관측값(Observation)-수신한 메일-이 스팸 메일인지 아닌지에 대한 “분류”를 추정하는 것이다.

즉, 정량적 (Quantitive) 예측이 아니고 정성적(Qualitative) 예측을 해야 한다. 예측하고자 하는 값(spam/non-spam) 이 데이터에 이미 포함되어 있고, 그 값을 바탕으로 training을 할 것이므로 우리가 하고자 하는 분석은 Supervised Classification이라고 할 수 있다. Classfication Method에는 Naive Bayes, Decision Tree, Logistic Regression, LDA,  KNN 등 여러가지 방법이 있으며,지금은 특정 관측값이 특정 분류에 속하게 될 확률을 추정해야 하고, 분류(Class)결과는 스팸/Non-Spam 두 가지의 (binomial) 결과가 있으므로 “Logistic Regression”을 선택해서 모델을 만들어 보겠다.


#fitting model
## Data Split to Train/Test
require(caret)
set.seed(1234)
trainIdx &lt;- createDataPartition(y=spam.base.df$spam, p=0.75,list=F)
trainData &lt;- spam.base.df[trainIdx,]
testData&lt;- spam.base.df[-trainIdx,]

spamCols &lt;- setdiff(colnames(trainData),list(&quot;spam&quot;))
spam.fomula &lt;- as.formula(paste('spam==&quot;spam&quot;',paste(spamCols,collapse=' + '),sep=' ~ '))

#모델 만들기
fit &lt;- glm(spam.fomula,family = binomial(link='logit'),data=trainData)

4. Calculate Accuracy On TrainData

스크린샷 2015-01-14 16.19.31

 Training Data 에 대한 Confusion Matrix 를 확인해 보면 accuracy가 92% 임을 확인 할 수 있다. Training Data로는 모델이 잘 만들어 진 것 같은데, testData로 내용을 검증해 보도록 하자.

5. Calculate Accuracy On TestData

스크린샷 2015-01-14 16.22.50

TestData 에 대해서도 ConfusionMatrix를 확인해 본 결과 92%를 보여주고 있다. ROC Curve를 체크하면서 Threshold값을 조정한다면 더 정교한 모델을 만들수도 있다.

* GitHub Location

https://github.com/Yonghee/spam-mail-classfication.git

람다 아키텍처

블로그를 한번 써 보기로 마음을 먹고 첫번째 포스팅이 될 것 같다.
페이스북에서 소개된 InfoQ의 Lambda Architecture 에 관한 내용을 우연히 보게 되었다.(http://www.infoq.com/articles/lambda-architecture-scalable-big-data-solutions).
사실 요즈음 함수형 언어와 Java 8의 람다 표현식에 관심을 갖고 이것저것 찾아보고 있던 중이라,
Lambda Architecture 라는 명칭을 보고 혹시 관계가 있는 것인가? 하는 마음에 들쳐보게 되었지만 실상을 그닥 관계가 없는 듯 하다.
왜 Lambda 라는 이름이 붙었을까?
Lambda(참다)는 그리스 문자의 열한번째 문자로 (uppercase Λ, lowercase λ) 로 표기한다.
수학에서는 함수 추상화를 위해 사용되고, 리스프같은 함수형언어는 람다 대수로 부터 직접 영향을 받아 탄생했다고 하니 Java에서는 람다 표현식이라는 말은 이런 의미로 가져다 이름을 지었다고 이해할 수 있겠지만.. 람다 아키텍처라니.. 좀 궁금해 졌다.
Storm Project로 잘 알려진 Nathan Marz 가 자신의 경험을 Lambda Architecture로 소개한 적이 있다고 한다.
요약하자면, 데이터를 batch처리와 실시간 처리로 나누어서 처리하여 대용량의 데이터를 빠르고 안전하게 처리하고자 하는 목적이라고 생각해 볼 수 있을 것 같다.

사실, 수억건이 넘는 데이터를 RDBMS에 넣어놓고 실시간으로 통계나 기타 분석 데이터 ( Data Processing Result )를 보고자 한다면 좀 난감한 상황이 될 수 있다. (사실 거의 불가능 하다고 할 수 있다)
필자도 과거 이동통신사의 NMS를 구축하면서 비슷한 고민을 한 적이 있다.
가입자의 통화데이터(Call Detail Log)에 포함된 여러가지 신호데이터를 이용해서 망(네트워크)의 품질을 계산하는 프로젝트인데, 초당 7,000건 이상씩 쏟아져 들어오는 데이터를 이용해서 최근 1분간의 통계데이터를 처리해서 제공해야 하고, 네트워크의 각 노드별 품질 분석 데이터를 hourly/daily/weekly/monthly 기준으로 제공을 해야 했었다.
지금에야 하둡에 MR에 Hbase, Storm, Spark등을 이용하면 그리 어렵지 않게 아키텍처를 구성할 수 있겠지만 2005년도에만 해도 이런 볼륨의 데이터를 처리하기란 그렇게 쉽지 않았다.
여튼 최근 1분간의 통계(near-real-time)와 주기적인(시간별,일별,주별,월별,분기별)  통계 데이터를 동시에 제공해야 했고, 초당 7,000건(하루에 약 6억건, 한달에 백억건)의 데이터를 가지고 실시간 처리도 문제지만 RDBMS에 넣어놓고 Query로 해결하기에는 당연하게도 불가능 했었다. 그래서 그 당시 여러가지로 고민을 하고 최종 적용했던 방식이 지금 얘기하고 있는 람다 아키텍처와 상당히 유사한 모습을 가지고 있었다.

위 그림에서 처럼 CDR이 발생되면  파일을 전처리(Splitting) 하고,  M/R을 돌린 결과를 메모리에 올려두고 실시간 뷰를 제공하였다. 보통 Speed Layer에서 사용되는 데이터는 최근1시간 데이터면 충분하고, 1시간이 지난 데이터는 메모리에서 디스크로 옮겨놓는다. 디스크로 옮겨진 데이터를 시간별/일별/월별/분기별로 통계를 생성해서 RDBMS에 저장해 둔다. 이때 RDBMS는 데이터에 억세스 하는 것을 빠르게 하고, 보관주기가 지난 데이터를 빠르게 삭제하기 위해 파티셔닝을 해 둔다.  Client 에서 쿼리를 하면 시간 조건을 보고 CEF에서 real time view에서 데이터를 가지고 올지, batch view에서 데이터를 가지고 올지 판단해서 쿼리를 실행하고 결과를 돌려준다. client에서는 조회하고자 하는 데이터가 real time view 에 있는지 batch view에 있는지 고려할 필요가 없다.
과거(2005년)에는 real time view를 구현하기 위해 직접 in-memory DB를 만들어 사용햇었지만(사실 버클리DB를 램디스크에 올려놓고 사용해 보기도 하는 등 여러가지 방법들을 적용해 보았지만…) , 요즘에는 다양한 NoSQL제품들이 많이 있으니 다양한 시도를 해 볼 수 있을 것 같다.
아래는 람다 아키텍처 그림

(이미지 출처 : http://lambda-architecture.net )

람다 아키텍처는 레퍼런스 아키텍처다. 각 레이어에 사용될 적당한 솔루션들이 많이 있으며 어느것도 정답은 될 수 없다.
중요한 점은 대용량 데이터를 가지고 실시간/누적 통계를 모두 제공해야 할 speed layer 와 batch layer를 잘 정의해서 적용 전략을 세우는 것이 아닐까 한다.
참고 자료