주요글: 도커 시작하기
반응형

* 장표에서 사용한 코드: https://github.com/madvirus/tfstudy


반응형

기존 access 로그를 이용해서 데이터를 추출해야 할 일이 생겨서 ELK를 사용했다. 앞으로도 비슷한 요구가 생기면 유사한 설정, 코드를 사용할 것 같아 기록으로 남긴다.


로그스태시(logstash) 설정 파일


여러 웹 어플리케이션의 access 로그 파일을 로그스태시를 이용해서 일레스틱서치에 밀어 넣었다. 각 웹 어플리케이션마다 미세하게 파싱해야 하는 내용이 달라, 로그스태시 설정 파일을 웹 어플리케이션 별로 하나씩 만들었다. 다음은 그 파일 중 하나이다.


input {

  stdin {

    type => "web"

    add_field => {

      appname => "myappname"

    }

  }

}


filter {

  grok {

    match=> { message => "%{IPORHOST:clientip} %{HTTPDUSER:ident} %{HTTPDUSER:auth} \[%{HTTPDATE:timestamp}\] \"(?:%{WORD:verb} %{URIPATH:uripath}(?<query>(\?\S+)?)(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest})\" %{NUMBER:response} (?:%{NUMBER:bytes}|-)"}

  }

  date {

    match => [ "timestamp", "dd/MMM/yyyy:HH:mm:ss Z" ]

  }

  grok {

    match => {

      uripath => "(/im/(?<context>(ui))/\S*)|(/gcs/(?<context>(\S+))/.*)"

    }

  }

  mutate {

    remove_field => ["query", "uripath"]

    add_field => {

      "methoduri" => "%{verb} %{uripath}"

    }

  }

  metrics {

    meter => "documents"

    add_tag => "metric"

    flush_interval => 60

  }

}


output {

  if "_grokparsefailure" not in [tags] {

    elasticsearch {

      hosts => "일레스틱서버주소"

      flush_size => 20000

    }

  }


  if "metric" in [tags] {

    stdout {

      codec => line {

        format => "1m rate: %{[documents][rate_1m]} ( %{[documents][count]} )"

      }

    }

  }

}


설정에서 특이한 점은 다음과 같다.
  • input의 stdin: 기존 로그 파일을 cat 명령어를 이용해서 logstash에 전달할거라서 stdin을 입력으로 설정했다.
  • filter의 첫 번째 grok: match의 message 패턴으로 로그스태시가 기본 제공하는 HTTPD_COMMONLOG 패턴을 사용하지 않은 이유는 요청 경로에서 쿼리 문자열과 요청 URI를 분리할 필요가 있었기 때문이다. 참고로 HTTPD_COMMONLOG 패턴은 쿼리문자열을 포함한 요청 경로를 request 필드로 추출한다.
  • filter의 date: 로그의 timestamp 필드를 이벤트의 @timestamp 값으로 사용한다. date를 사용하지 않으면 로그스태시가 로그 파일을 넣는 시점을 @timestamp 값으로 사용하기 때문에 기간별 로그 분석을 하기 어려워진다.
  • filter의 mutate: 요청 방식과 요청 URI를 합쳐서 하나로 분석해야 해서 이 둘을 연결한 값을 새로운 필드로 추가한다.
  • filter의 metrics: 로그스태시가 얼마나 처리하는지 보기 위해 메트릭 값을 60초마다 생성한다.
  • output의 elasticsearch: flush_size 값은 일레스틱서치 서버의 상황을 고려해 설정한다. 1000~5000 사이에서 시작해서 테스트하면서 점진적으로 늘려서 처리속도를 높여나간다.
  • output의 stdout: metrics가 생성한 메트릭 값을 콘솔에 출력한다. 초당 몇 개씩 처리하는지 1분 주기로 콘솔에 찍혀서 그나마 덜 심심하다.

로그 밀어넣기 위한 실행 스크립트


기존 로그 파일을 특정 폴더에 모아두고, 그 파일을 로그스태시로 일레스틱서치에 쭈욱 밀어 넣었다. 파일 하나하나 수동으로 하면 힘드니까 다음의 쉘 파일을 만들어서 실행했다.


#!/bin/bash

FILES="./로그폴더/*.log"

for f in $FILES

do

  echo "$f start"

  SECONDS=0

  cat $f | ../logstash/bin/logstash -b 2500 --config app1.logstash.conf

  duration=$SECONDS

  echo "$f done [$(($duration / 60))m $(($duration % 60))s elapsed]"

  echo "---"

  sleep 3

done


logstash의 -b 옵션은 프로세스 파이프라인 배치 크기이다. 파이프라인 워커 개수는 기본이 8이다. 워커개수*배치크기를 elasticsearch의 flush_size와 맞췄다.

테스트하는 동안 심심함도 달래고 성능도 확인할 겸 로그 파일 한 개를 처리하는데 걸리는 시간을 측정해서 값을 출력하도록 했다.


실행 결과


작성한 쉘 스크립트를 돌리면 콘솔에 다음과 같이 시간 값이 찍혀서 덜 심심하다.


$ ./logdump.app1.sh

...

./로그폴더/access_2016_04_06.log start

Settings: Default pipeline workers: 8

Pipeline main started

1m rate: 6336.785481508631 ( 407172 )

1m rate: 6937.548787233175 ( 856873 )

Pipeline main has been shutdown

stopping pipeline {:id=>"main"}

./misweb_in1_rest/access_2016_04_06.log done [2m 17s elapsed]

...

...


일단 1-2개 로그 파일로 작업해보고 퇴근 전에 nohup을 걸어서 나머지를 밀어 넣었다.


$ nohup ./logdump.app1.sh > dump.log 2>&1 &



반응형

마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 3주차 요약.


* 함수형을 잘 모르는 상태에서 요약한 것이므로 내용에 오류가 존재할 수 있음

* 3주차는 주로 클래스와 객체에 대한 내용으로 비교적 익숙했음


클래스 계층


추상 클래스는 구현이 없는 멤버를 포함할 수 있다. 구현이 없으므로 new로 추상 클래스의 인스턴스를 생성할 수 없다.


abstract class IntSet {

  def incl(x: Int): IntSet

  def contains(x: Int): Boolean

}


IntSet 클래스를 확장(extend)한 두 클래스-Empty, NonEmpty-는 다음과 같이 구현 가능하다.


class Empty extends IntSet {

  def contains(x: Int): Boolean = false

  def incl(x: Int): IntSet = new NonEmpty(x, new Empty, new Empty)

}

class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {

  def contains(x: Int): Boolean = 

    if (x < elem) left contains x else if (x > elem) right contains x else true

 

  def incl(x:Int): IntSet = 

    if (x < elem) new NonEmpty(elem, left incl x, right)

    else if (x > elem) new NonEmpty(elem, left, right incl x)

    else this

}


여기서 Empty와 NonEmpty는 IntSet 타입을 따르며(conform), IntSet 타입이 필요한 곳에 이 두 타입의 객체를 사용할 수 있다.


IntSet을 Empty와 NonEmpty의 상위클래스(superclass)라고 하며, Empty와 NonEmpty를 IntSet의 하위클래스(subclass)라고 한다.


상위 클래스를 지정하지 않으면 java.lang.Object 클래스를 상위 클래스로 가정한다. C 클래스의 직/간접 상위클래스를 베이스 클래스(base class)라고 한다. NonEmpty 클래스의 경우 IntSet과 Object가 베이스 클래스가 된다.


오버라이드를 사용하면 상위 클래스에 정의된 비추상 멤버를 재정의할 수 있다. 다음 코드에서 Sub 클래스의 foo 멤버는 Base 클래스의 foo 멤버를 재정의하고 있다.


abstract class Base {

  def foo = 1

  def bar: Int

}

class Sub extends Base {

  override def foo = 2

  def bar = 3

}


오브젝트


스칼라는 한 개의 인스턴스만 갖는 타입인 오브젝트를 지원한다. 예를 들어, NonEmpty는 한 개 인스턴스가 존재하면 되는데 다음과 같이 오브젝트 정의를 사용해서 NonEmpty를 싱글톤으로 정의할 수 있다.


object Empty extends IntSet {

  def contains(x: Int): Boolean = false

  def incl(x: Int): IntSet = new NonEmpty(x, Empty, Empty)

}


동적 바인딩(dynamic binding)


스칼라를 포함한 객체 지향 언어는 동적 바인딩을 구현한다. 동적 바인딩은 메서드를 호출할 때 메서드를 포함한 객체의 런타임 타입에 의존한다는 것을 의미한다.


치환 모델로 동적 바인딩의 계산 과정을 풀면 다음과 같다.


(new NonEmpty(7, Empty, Empty)) contains 7

-> [7/elem][7/x][new NonEmpty(7, Empty, Empty)/this] 

      if (x < elem) this.left contains x 

      else if (x > elem) this.right contains x else true

-> if (7 < elem) new NonEmpty(7, Empty, Empty).left contains 

    else if (7 > 7) new NonEmpty(7, Empty, Empty).right contains 7 else true

-> true


패키지와 임포트


자바처럼 패키지를 이용해서 클래스와 오브젝트를 구성한다. 패키지를 포함한 fully qualified 이름으로 클래스나 오브젝트를 참조할 수 있다.


import를 이용해서 단순 이름으로 사용할 수 있다. scala, java.lang, scala.Predef 오브젝트의 모든 멤버는 자동으로 임포트한다.


트레잇(trait)


스칼라의 클래스는 한 개의 상위클래스만 가질 수 있다. 다중 상속이 필요한 경우 트레잇을 사용할 수 있다. 트레잇은 추상 클래스처럼 추상 멤버와 구현을 가진 멤버를 선언할 수 있다. 


trait Planar {

  def height: Int

  def width: Int

  def surfce = height * width

}


클래스, 오브젝트, 트레잇은 최대 한 개의 클래스를 상속할 수 있지만, 트레잇은 여러 개를 상속할 수 있다.


class Square extends Shape with Planar with Movable ...


트레잇은 필드와 메서드 구현을 가질 수 있기 때문에 자바 인터페이스보다 더 강력하다. 클래스가 파라미터를 가질 수 있는 것과 달리 트레잇은 가질 수 없다.


스칼라 클래스 계층

  • Any: 모든 타입의 베이스 타입이다. ==, !=, equals, hashCode, toString 메서드를 정의한다.
  • AnyRef: 모든 레퍼런스 타입의 기반 타입이다. java.lang.Object에 해당한다. scala.List, scala.Seq 등이 속한다.
  • AnyVal: 모든 원시 타입의 기반 타입니다. (scala.Int, scala.Boolean 등이 속한다.)
  • Null: 레퍼런스 타입은 값으로 null을 가질 수 있다. null은 Null 타입 값으로 Null 타입은 Object를 상속한 모든 클래스의 하위타입이다.
  • Nothing: 모든 타입의 하위 타입으로 스칼라 타입 계층의 최하단에 위치한다.

다형(polymorphism)


다형은 함수 타입이 여러 폼(form)이 된다는 것을 의미한다. 이는 프로그래밍에서 다음을 뜻한다.

  • 함수를 여러 타입의 인자에 적용할 수 있거나 -> 지네릭: 함수나 클래스의 인스턴스를 타입 파라미터로 생성
  • 타입이 많은 타입의 인스턴스를 가질 수 있다 -> 하위타입: 하위클래스의 인스턴스를 베이스 클래스(형태)로 전달
예)
trait List[T]
class Cons[T](val head: T, val tail: List[T]) extends List[T]
class Nil[T] extends List[T]


상속을 통한 다형 : Nil 인스턴스를 List가 필요한 곳에 전달할 수 있다 -> new Cons(10, Nil)

타입파라미터를 통한 다형 : Cons를 여러 타입에 적용할 수 있다. Cons[User](u1, Nil), Cons[Member](m1, Nil)


반응형

마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 2주차 요약.


* 함수형을 잘 모르는 상태에서 요약한 것이므로 내용에 오류가 존재할 수 있음


고차 함수(Higher-Order Function)


함수형 언어는 함수를 일급 값(first-class value)으로 처리한다. 다른 값 처럼 함수를 파라미터로 전달하거나 결과로 리턴할 수 있다. 이는 프로그램을 조합하는 유연한 방법을 제공한다.


파라미터로 다른 함수를 전달받거나 함수를 결과로 리턴하는 함수를 고차 함수(higher order function)라고 부른다.


고차 함수를 사용하면 여러 기능에 출현하는 공통 패턴을 도출할 수 있다. 다음 두 기능은 공통 패턴이 있는데,


def sumInts(a: Int, b: Int): Int = if (a > b) 0 else a + sumInt(a + 1, b)

def sumCubes(a: Int, b: Int): Int = if (a > b) 0 else (a * a * a) + sumCubes(a + 1, b)


고차 함수를 이용해서 구현하면 다음과 같이 공통 패턴을 뽑아낼 수 있다.


def sum(f: Int => Int, a: Int, b: Int): Int = if (a > b) 0 else f(a) + sum(f, a+1, b)


이 고차 함수를 이용해서 sumInt, sumCubes를 구현한 코드는 다음과 같다.


def id(x: Int): Int = x

def cube(x:Int): Int = x * x * x


def sumInts(a: Int, b: Int): Int = sum(id, a, b) // id 함수를 전달

def sumCubes(a: Int, b: Int): Int = sum(cube, a, b) // cube 함수를 전달


함수는 기본적인 추상인데, 그 이유는 이름을 부여한 명시적인 요소를 사용해서 연산을 수행하기 위한 일반적인 수단을 만들 수 있도록 해주기 때문이다.


임의 함수(anonymous function)


작은 함수를 매번 새로 정의하는 것은 성가신 일인데 임의 함수를 사용해서 이를 해소할 수 있다.


def sumInts(a: Int, b: Int): Int = sum( (x: Int) => x, a, b) // id 함수를 전달

def sumCubes(a: Int, b: Int): Int = sum(x => x*x*x, a, b) // cube 함수를 전달


임의 함수의 파라미터 타입은 컴파일러가 추론가능하면 생략가능하다.


커링(currying)


다음 sum 함수는 (Int => Int) 타입의 함수를 인자로 받아 (Int, Int) => Int 타입의 함수를 리턴한다.


def sum(f: Int => Int): (Int, Int) => Int = {

 def sumf(a,: Int, b: Int): Int = if (a > b) 0 else f(a) + sumf(a+1, b)


  sumf // sumf 함수를 리턴

}


다음은 함수를 리턴하는 sum 함수를 사용해서 구현한 sumCubes이다.


def cubes(x: Int):Int = x*x*x

def sumCubes = sum(cubes)


sum 함수가 cubes를 사용하는 sumf를 리턴하므로, sumCubes(1, 3)은 1 + 8 + 27을 리턴한다. sumCubes 없이 다음과같이 표현할 수도 있을 것이다.


sum(cubes)(1, 3)


함수는 왼쪽에서 오른쪽으로 적용하므로 다음이 성립한다.


sum(cubes)(1,3) == (sum (cubes)) (1,3)


스칼라는 함수를 리턴하는 함수를 위한 다중 파라미터 목록 문법을 제공한다. 앞서 함수를 리턴하는 sum을 다음과 같이 구현할 수 있다.


def sum (f: Int => Int) (a: Int, b: Int): Int =

  if (a > b) 0 else f(a) + sum(f)(a+1, b)


위 함수를 사용하면 sum(cubes) (1, 3)과 같이 함수를 실행할 수 있다.


일반적으로 다중 파라미터 목록을 가진 함수 정의가 있을 때,


def f(args1)...(argsn) = E ( n > 1)


이는 다음과 동등하다.


def f = (args1 => (args2 => ... (argsn => E ) )  )


이런 방식으로 함수를 정의하고 적용하는 것을 커링이라고 부른다. (커링은 Haskell Brooks Cury의 이름에서 딴 것이다.)


함수와 데이터


스칼라는 클래스를 이용해서 데이터를 추상화한다. 클래스 타입의 요소를 객체라고 부르며, 클래스 생성자의 적용(application) 앞에 new 연산자를 위치시켜 객체를 생성한다.


예) new Rational(1, 2)


객체의 멤버는 자바처럼 중위 연산자 "."를 사용해서 선택한다.


데이터를 다루는 함수를 클래스 자체에 넣을 수 있는데 이런 함수를 메서드라고 부른다.


데이터 추상화


다음 세 개의 Rational 클래스를 보자. 클라이언트 입장에서 다음의 두  Rational 클래스는 동일한 데이터를 제공한다.


class Rational(x: Int, y: Int) {

    private def gcd(a: Int, b: Int): Int = ...생략

    private val g = gcd(x, y)

    def numer = x  / g

    def denom = y / g

}


class Rational(x: Int, y:Int) {

    private def gcd(a: Int, b:Int): Int = ....

    val numer = x / gcd(x, y)

    val denom = y / gcd(x, y)

}


두 Rational의 numer와 denom은  클라이언트 입장에서 동일하게 동작한다. 이렇게 클라이언트에 영향없이 데이터의 다른 구현을 선택할 수 있는 것을 데이터 추상화라고 한다.


클래스와 치환 모델


함수를 적용할 때 치환에 기반한 계산 모델을 사용한 것처럼, 클래스와 객체에도 이 모델을 적용한다. 예를 들어, 클래스 다음의 클래스 정의가 있다고 하자.


class C(x1, ..., xn) { def f(y1, ..., yn) = b ..}


이 때 new C(v1, ..., vn).f(w1, ..., wn) 식은 다음과 같이 치환한다.


[w1/y1, ...., wn/yn][v1/x1, ... , vn/xn][new C(v1, ..., v1)/this]b


연산자


파라미터가 한 개인 메서드는 다음과 같이 중위 연산자처럼 사용할 수 있다.


r.add(s) ---> r add s


스칼라의 연산자는 실제로 메서드이며 연산자를 식별자로 사용할 수 있다. 식별자는 다음이 될 수 있다.

  • alphanumeric 식별자: 글자로 시작하고 글자나 숫자가 뒤에 온다.
  • symbolic 식별자: 연산자 심볼로 시작할 수 있고, 다른 연산자 심볼이 올 수 있다.
  • 밑줄('_')은 글자로 처리한다.
  • 밑줄로 끝나는 alphanumeric 식별자 뒤에 연산자 심볼이 위치할 수 있다.

다음은 식별자의 예이다.


xn3   *   ?  list_++   <== ==>


반응형


2016-07-16 DDD Start 수다 세미나 발표 영상




반응형

마틴 오더스키 교수님이 코세라에서 진행중인 Functional Programming Principles in Scala 강의(https://www.coursera.org/learn/progfun1)의 1주차 요약.


프로그래밍 패러다임


과학의 경우 패러다임은, 어떤 학문 분야에서 구분되는 개념이나 사고 패턴을 설명한다.


프로그래밍 패러다임: 명령형(Imperative) 프로그래밍, 함수형(Functional) 프로그래밍, 로직(Logic) 프로그래밍

(객체 지향 프로그래밍은 이들 패러다임에 orthogonal하다)


명령형 프로그래밍과 한계


명령형 프로그래밍은 다음에 관한 것이다.
  • 변경가능한 변수를 수정하고 할당을 사용하고 (메모리, 로드/저장 명령)
  • 조건문, 루프, 브레이크, 리턴과 같은 제어 구조를 사용 (점프)

순수 명령형 프로그래밍은 명령어를 순차적으로 실행하는 구조로, 단어별로(word-by-word???) 데이터 구조의 개념을 도출하는 경향이 있다.


다항, 콜렉션, 문자열, 문서와 같은 고수준 추상화를 정의할 수 있는 다른 기법이 필요하다.


이론(Theory)이란?


이론은 다음으로 구성된다.

  • 데이터 타입
  • 타입에 대한 연산(오퍼레이션)
  • 값과 연산 간의 관계를 기술하는 규칙

보통, 이론은 변경(mutation)에 대해 묘사하지 않는다. 


변경 없는 이론 예1) 다항식 이론

(a*x + b) + (c*x + d) = (a+c)*x + (b+d) 계수를 변경하는 연산을 정의하지 않음


변경 없는 이론 예2) 문자열 이론의 문자열 연결 연산자

(a ++ b) ++ c = a ++ (b ++ c) 시퀀스 원소 변경 연산자를 정의하지 않음


프로그래밍을 위한 결론

  • 연산자를 위한 이론을 정의하는데 집중
  • 상태 변경을 최소화
  • 연산자를 함수로 여기고, 종종 단순한 함수의 조합으로 처리

함수형 프로그래밍과 언어


 -

 함수형 프로그래밍

함수형 프로그래밍 언어 

 좁은(restricted) 의미

변경가능한 변수, 할당, 루프, 다른 명령형 제어 구조 없는 프로그래밍

변경 가능 변수, 할당, 명령형 제어 구조가 없는 언어 

 넓은(wider) 의미

함수에 집중하는 것 

함수를 이용해서 elegant한 프로그램 을 구성하는 것 


함수형 프로그래밍 언어에서 함수는 일급(first-class)이다.

  • 모든 곳에 정의 가능함 (다른 함수 내부 포함)
  • 다른 값처럼 함수를 파라미터로 전달 가능하고 결과로 리턴될 수 있음
  • 함수를 조합하는 집합 연산자 존재

요즘 함수형이 뜨는 이유


멅티코어와 클라우드 컴퓨티 환경에서 병렬을 활용하는 좋은 방법을 제공하기 때문이다.



프로그래밍의 요소(Elements of Programming)


언어는 다음을 제공한다.

  • 단순한 요소를 표현하는 프리미티브(원시) 식(primitive expression)
  • 식을 묶는 방법
  • 식을 추상화하는 방법(식에 이름을 붙이고 이를으로 참조)

식의 계산(evaluation)


원시 식이 아닌 연산자를 포함한 식은 다음 과정을 따라 계산한다.

  1. 가장 왼쪽 연산자를 선택
  2. 선택한 연산자의 피연산자의 값을 구함(왼쪽 피연산자의 값을 먼저 구함)
  3. 두 피연산자에 연산자를 적용
식에 포함된 이름은 그 이름 정의의 오른쪽으로 대치한다.(예 def name = body 이면, name을 body로 대치)
최종 결과가 값이 될 때까지 과정을 반복한다.

과정 예) pi = 3.14159 이고 radius = 10
(2 * pi) * radious (가장 왼쪽 연산자 선택)
-> (2 * 3.14159) * radius (연산자의 피연산자 값 구함, pi를 3.14159로 대치 후 연산자 적용)
-> 6.28318 * radius (가장 왼쪽 연산자 선택)
-> 6.28318 * 10 (연산자의 피연산자 값 구함, radius를 10으로 대치 후 연산자 적용)
-> 62.8318 (최종 결과로 값 구함)

함수 적용(function application) 계산

파라미터를 가진 함수의 계산 과정은 다음과 같다.
  1. 모든 함수 인자를 왼쪽부터 오른쪽으로 차례대로 계산
  2. 함수 적용을 함수의 우측(함수의 몸체)으로 대치하고, 동시에
  3. 함수의 formal 파라미터를 실제 인자로 대치함
적용 예) 
def square(x: Double) = x * x
def sumOfSquares(x: Double, y: Double) = square(x) + square(y)

sumOfSquares(3, 2+2)
-> sumOfSquares(3, 4) (인자 값 구함)
-> square(3) + square(4) (sumOfSquares 함수의 우측으로 대치 및 파라미터를 인자로 대치)
-> 3 * 3 + square(4) (square 함수의 우측으로 대치)
-> 9 + square(4) (식의 계산에 의해 첫 번째 연산자 식을 계산)
----> 9 + 16 (비슷한 과정 생략)
-> 25

치환 모델(Substitution model)

앞의 식을 계산하는 방식을 치환 모델이라고 부른다. 이 모델의 아이디어는 모든 계산은 식을 한 개의 값으로 reduce한다. 식에 side effect가 없으면 이 모델을 모든 식에 적용할 수 있다.

계산의 끝(termination)

모든 식의 계산이 끝나는 것은 아니다.

예)
def loop: Int = loop
val i = loop()  // loop 식 계산은 끝나지 않음


값 계산의 두 가지 전략

  • call-by-value(이하 CBV): 식을 값으로 바로 계산해서 전달, 모든 함수 인자를 한 번만 계산하는 장점.
  • call-by-name(이하 CBN): 식의 값이 실제로 필요할 때까지 이름으로 전달, 함수에서 사용하지 않는 인자는 함수 몸체를 적용하는 과정에서 계산하지 않는 장점

두 방식은 다음 조건을 충족하는 한 두 방식은 동일한 값으로 reduce된다.

  • reduce한 식이 순수 함수로 구성
  • 둘 다 계산이 끝남

CBV 식이 끝나면 동일한 CBN 식도 끝나지만, 반대는 성립하지 않는다. 아래 코드에서 CBN의 경우 first(1, loop)는 1로 계산되지만 CBV의 경우 두 번째 인자인 loop의 값 계산이 끝나지 않아 식이 끝나지 않는다.


def first(x: Int, y: Int) = x

first(1, loop)


조건 식


스칼라의 if-else은 식이다. if의 predicate은 Boolean 값이다. &&와 ||는 왼쪽 피연산자의 값에 따라 오른쪽 피연산자를 계산하지 않는다. 예를 들어, true || e의 경우 왼쪽이 true이므로 오른쪽의 e 값을 구할 필요가 없으므로 결과는 true가 된다. 이런 식을 "short-circuit evaluation"을 사용한다고 말한다.


중첩 함수


스칼라는 함수 안에서만 접근할 수 있는 중첩 함수를 정의할 수 있다. 특정 함수 안에서만 사용하는 함수를 중첩함으로써 "name-space pollution"을 피할 수 있다.


블록


스칼라의 블록은 중괄호로 구분하며, 정의나 식을 포함한다. 블록 그 자체는 식이므로 식이 올 수 있는 모든 자리에 블록이 올 수 있다.


블록 안에 위치한 정의는 블록 안에서만 보이며, 블록 밖의 같은 이름을 가진 정의를 감춘다. 블록 밖의 정의는 블록 안에서 접근할 수 있다.


val x = 0

val y = 1

def f(y: Int) y + 1

val result = {

    val x = f(3) // 블록 밖의 x를 감춤

    x * x + y // 블록 안의 x임, 블록 밖의 정의에 접근 가능

} + x // 블록 밖의 x


꼬리 재귀(tail recursion)


함수의 마지막 액션이 자신을 호출하면, 함수의 스택 프레임을 재사용할 수 있다. 이를 꼬리 재귀라 한다. 스칼라는 꼬리 재귀를 최적화한다.


def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) // 마지막 액션이 꼬리 재귀, 최적화함


def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n-1) // 마지막 액션이 꼬리 재귀 아님



반응형

앵귤러2 기반 웹앱에 파일 업로드 기능을 추가해야 할 일이 생겨서 검색을 했더니 다음의 두 라이브러리가 얻어 걸렸다.

  • ng2-file-upload (https://github.com/valor-software/ng2-file-upload)
  • ng2-uploader (https://github.com/jkuri/ng2-uploader)

둘 다 쉽게 파일 업로드를 붙일 수 있다. 두 라이브러리를 실험해보고 나서 ng2-file-upload를 선택했다. 그 이유는 구현하려는 기능에 조금 더 맞았기 때문이다.


ng2-file-upload의 기본 사용법


데모(http://valor-software.com/ng2-file-upload/) 사이트를 보면 ng2-file-upload의 기본 사용법을 볼 수 있는데, 진짜 쉽다. ng2-file-upload가 제공하는 FileUploader 클래스와 ng2FileDrop 디렉티브와 ng2FileSelect 디렉티브를 사용하면 된다.


나 같은 경우 앵귤러2 사이트의 튜토리얼 문서에 있는 과정을 따라서 프로젝트를 생성했다. ng2-file-upload를 사용하기 위해 package.json 파일에 ng2-file-upload에 대한 의존을 추가했다.


{

  ...

  "dependencies": {

    "@angular/common": "2.0.0-rc.5",

    ...

    "bootstrap": "^3.3.6",

    "ng2-file-upload": "1.0.3"

  },

  "devDependencies": {

    "typescript": "^1.8.10",

    "typings":"^1.0.4"

  }

}


추가한 뒤에 npm install 명령어를 사용해서 ng2-file-upload를 다운로드 받았다.


그리고, 모듈 설정을 위해 systemjs를 사용했기 때문에 systemjs.config.js 파일에 ng2-file-upload에 대한 설정을 다음과 같이 추가해서 모듈 이름으로 접근할 수 있도록 했다.


(function (global) {

    // map tells the System loader where to look for things

    var map = {

        'app': '/operation-app/app-testeditor', // 'dist',

        '@angular': '/node_modules/@angular',

        'rxjs': '/node_modules/rxjs',

        'ng2-file-upload': '/node_modules/ng2-file-upload'

    };

    // packages tells the System loader how to load when no filename and/or no extension

    var packages = {

        'app': {main: 'test-editor-main.js', defaultExtension: 'js'},

        'rxjs': {defaultExtension: 'js'},

        'ng2-file-upload': {defaultExtension: 'js'}

    };

    ...

    ...

    System.config(config);

})(this);


파일 업로드 기능이 필요한 앵귤러 컴포넌트는 다음과 같이 ng2-file-upload 모듈을 임포트하면 된다.


import {FILE_UPLOAD_DIRECTIVES, FileUploader} from "ng2-file-upload/ng2-file-upload";


@Component({

    selector: 'test-file-uploader',

    templateUrl: './test-file-uploader.component.html',

    directives: [FILE_UPLOAD_DIRECTIVES]

})

export class TestFileUploader {

    public uploader:FileUploader = new FileUploader({url: '/my/upload/path');


    ...

}


FileUploader 클래스는 업로드할 파일 목록을 관리하고 파일을 전송하는 기능을 제공한다. ng2-file-upload 사이트의 데모 코드를 보면 다음과 같이 템플릿 코드에 디렉티브를 사용해서 업로드할 파일을 추가할 수 있다.


<div ng2FileDrop [uploader]="uploader" ....>

    Base drop zone

</div>


<div ng2FileDrop [uploader]="uploader" ....>

    Another drop zone

</div>


Multiple

<input type="file" ng2FileSelect [uploader]="uploader" multiple  /><br/>


Single

<input type="file" ng2FileSelect [uploader]="uploader" />


ng2FileDrop 디렉티브로 지정한 영역에 파일을 드롭하거나 ng2FileSelect를 이용해서 파일을 선택하면, 해당 파일이 [uploader] 속성으로 지정한 FileUploader 객체에 추가된다.


이렇게 FileUploader에 추가한 파일을 업로드하려면 다음과 같이 FileUplaoder의 uploadAll()을 사용하면 된다. 또는 추가한 개별 파일별로 업로드를 할 수도 있다. 데모 사이트에서 완전한 코드를 확인할 수 있다.


<button type="button" class="btn btn-success btn-s"

        (click)="uploader.uploadAll()" [disabled]="!uploader.getNotUploadedItems().length">

    <span class="glyphicon glyphicon-upload"></span> Upload all

</button>


디렉티브 없이 FileUploader 직접 사용


ng2-file-upload가 그 자체로 좋지만 내가 필요한 건 ng2-file-upload가 제공하는 파일 업로드 기능이었고, 디렉티브를 통한 연동은 필요없었다. 그래서 디렉티브를 사용해서 파일 목록을 FileUploader에 추가하지 않고 직접 추가했다.


FileUploader 생성과 파일 추가

FileUploader 객체를 앵귤러 컴포넌트 생성자에서 직접 생성했다. <input type="file"> 버튼을 눌러 선택한 파일 목록을 받기 위한 메서드는 handleUploadFileChanged()이다.


@Component( ... )

export class TestFileUploader {

    public uploader:FileUploader;


    constructor() {

        let uploadUrl = window.location.protocol + "//" + window.location.host + "/testfile/upload";

        this.uploader = new FileUploader({url: uploadUrl});

        ...

    }


    handleUploadFileChanged(event) {

        this.uploader.clearQueue();

        let files:File[] = event.target.files;

        let filteredFiles:File[] = [];

        for (var f of files) {

            if (f.name.endsWith(".pdf")) {

                filteredFiles.push(f);

            }

        }

        if (filteredFiles.length == 0) {

            this.showGuide = true;

        } else {

            this.showGuide = false;

            let options = null;

            let filters = null;

            this.uploader.addToQueue(filteredFiles, options, filters);

        }

    }

    ...


요구사항은 파일을 하나만 업로드하는 것이었기에 handleUploadFileChanged()는 먼저 현재 uploader.clearQueue())를 이용해서 uplaoder에 추가되어 있는 파일 목록을 지운다. 이를 제거하지 않으면 파일을 선택할 때마다 업로드할 파일이 추가되기 때문에, 실제 업로드를 수행할 때 마지막 선택한 파일을 포함한 이전에 선택한 모든 파일을 업로드한다.


위 코드에서는 확장자가 pdf인 파일을 걸러낸 뒤에 uploader.addToQueue()를 이용해서 업로드할 파일 목록을 직접 uploader에 추가했다.


handleUploadFileChanged()를 실행하기 위한 템플릿 코드는 다음과 같다.


<input type="file" (change)="handleUploadFileChanged($event)">



업로드와 완료 처리


업로드할 파일을 선택했다면 uploader.uploadAll()을 이용해서 선택한 파일을 업로드할 수 있다. 업로드를 완료한 뒤에 성공/실패 유무에 따라 알맞은 후속 작업(안내 문구 출력 등)을 하고 싶다면 uploader에 이벤트를 리스너를 등록하면 된다.


@Component( ... )

export class TestFileUploader {

    public uploader:FileUploader;

    private uploadResult:any = null;


    constructor() {

        let uploadUrl = window.location.protocol + "//" + window.location.host + "/testfile/upload";

        this.uploader = new FileUploader({url: uploadUrl});

        this.uploader.onSuccessItem = (item, response, status, headers) => {

            this.uploadResult = {"success": true, "item": item, "response": 

                response, "status": status, "headers": headers};

        };

        this.uploader.onErrorItem = (item, response, status, headers) => {

            this.uploadResult = {"success": false, "item": item, 

                "response": response, "status": status, "headers": headers};

        };

        this.uploader.onCompleteAll = () => {

            this.handleUploadComplete();

        };

    }


    uploadFile() {

        this.uploader.uploadAll(); // 업로드 시작

    }


    private handleUploadComplete() {

        console.log("upload compete : " + this.uploadResult.response);

        if (this.uploadResult.success) {

            ...성공 메시지 출력

        } else {

            ...실패 메시지 출력

        }

    }


FileUploader에는 onSuccessItem, onErrorItem, onCompleteAll 등의 이벤트 리스너를 등록할 수 있다. 이들 리스너를 등록하면 업로드 성공/실패 여부를 이벤트로 받아 그에 알맞은 처리를 할 수 있다. 위 코드는 한 개 파일만 업로드하므로 onSuccessItem 리스너와 onErrorItem 리스너에서 업로드 결과를 uploadResult 필드에 할당했다. 그리고, 모든 업로드 처리가 끝나면 불리는 onCompleteAll 리스너에서 업로드 성공/실패 결과에 따라 알맞은 처리를 수행하도록 했다.


반응형

앵귤러2로 프로젝트를 진행하는 과정에서 입력한 내용에 따라 textarea의 높이를 자동으로 조절해주는 기능이 필요했다. 검색 결과 다음 디렉티브가 적당해 보여 적용을 했다.

  • https://github.com/stevepapa/angular2-autosize

angular2-autosize 디렉티브 사용법은 간단하다. 위 사이트에서 제공하는 샘플 코드처럼 템플릿에 autosize 디렉티브를 적용해주면 된다.


import {Component} from '@angular/core';

import {Autosize} from 'angular2-autosize';


@Component({

  template: `

    <textarea autosize >Hello, this is an example of Autosize in Angular2.</textarea>

  `,

  directives: [Autosize]

})

class App {


}


예제 사이트(https://stevepapa.com/angular2-autosize/)에서 <textarea>에 입력하면 자동으로 높이가 늘었다 줄었다하는 것을 할 수 있다. 



약간의 수정

angular2-autosize는 내용이 없을 경우 다음 그림처럼 textarea가 표시된다.



두 줄 정도 높이로 표시되는데, 내가 원하는 건 내용이 없거나 한 줄 분량인 경우 높이도 한 줄 높이로 맞춰지는 것이었다. 그래서 angular2-autosize의 코드(https://github.com/stevepapa/angular2-autosize/blob/master/src/autosize.directive.ts)를 다음과 같이 약간 수정했다. 추가한 부분을 굵게 표시했다.


import {ElementRef, HostListener, Directive, Input} from '@angular/core';


@Directive({

    selector: 'textarea[autosize]'

})

export class Autosize {

    @HostListener('input',['$event.target'])

    onInput(textArea: HTMLTextAreaElement): void {

        this.adjust();

    }


    @Input("autosize") initValue:string;


    constructor(public element: ElementRef){

    }


    ngOnInit(): void{

        this.element.nativeElement.rows = 1;

        this.element.nativeElement.value = this.initValue;

        this.adjust();

    }

    adjust(): void{

        this.element.nativeElement.style.overflow = 'hidden';

        this.element.nativeElement.style.height = 'auto';

        this.element.nativeElement.style.height = this.element.nativeElement.scrollHeight + "px";

    }

}


@Input을 이용해서 textarea에 보여줄 값을 초기화하도록 구현했다. 수정한 디렉티브의 사용은 다음과 같다.


<textarea class="form-control" cols="30" rows="1"

          [(ngModel)]="mytext"

          [autosize]="mytext"></textarea>


ngModel로 설정한 값과 연동하는 방법이 있을 것 같은데, angular2 자체를 아직 자세히 몰라 거기까진 진행하지 못했다.


실제 적용한 결과 화면은 다음과 같다.





반응형

다음과 같은 간단한 이벤트 관련 코드를 만들 일이 생겼다.

  1. 도메인 객체가 트랜잭션 범위에서 이벤트를 발생하면 핸들러로 처리
  2. 트랜잭션이 커밋된 이후에 이벤트 핸들러에 이벤트 전달해야 함
  3. 이벤트가 유실되어 처리하지 못해도 됨(실패시 후처리)
  4. 이벤트 핸들러는 비동기로 실행

스프링 4.2나 그 이후 버전을 사용한다면 아주 간단하게 위 조건을 충족하는 코드를 만들 수 있다. 다음 조합을 사용하면 된다.

  • ApplicationEventPublisher.publishEvent(Object event) 사용
  • @TransactionEventListener
  • @Async로 비동기 처리


1. Events 클래스

다음은 도메인 객체에서 이벤트를 발생시킬 때 사용할 Events 클래스이다.


import org.springframework.context.ApplicationEventPublisher;


public class Events {

    private static ThreadLocal<ApplicationEventPublisher> publisherLocal = 

            new ThreadLocal<>();


    public static void raise(DomainEvent event) {

        if (event == null) return;


        if (publisherLocal.get() != null) {

            publisherLocal.get().publishEvent(event);

        }

    }


    static void setPublisher(ApplicationEventPublisher publisher) {

        publisherLocal.set(publisher);

    }


    static void reset() {

        publisherLocal.remove();

    }

}


Events 클래스의 raise() 메서드는 ApplicationEventPublisher를 이용해서 이벤트를 퍼블리싱한다. 참고로 raise() 메서드의 event 파라미터는 Event 타입인데 이 타입은 원하는 타입으로 알맞게 만들면 된다.


도메인 객체에서 Events.raise()로 발생한 이벤트를 ApplicationEventPublisher로 퍼블리싱하려면 도메인 객체를 실행하기 전에 Events.setPublisher()로 ApplicationEventPublisher를 설정해야 한다. 이를 위해 다음의 Aspect를 구현했다..


import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.springframework.context.ApplicationEventPublisher;

import org.springframework.context.ApplicationEventPublisherAware;

import org.springframework.stereotype.Component;


@Aspect

@Component

public class EventPublisherAspect implements ApplicationEventPublisherAware {

    private ApplicationEventPublisher publisher;

    private ThreadLocal<Boolean> appliedLocal = new ThreadLocal<>();


    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")

    public Object handleEvent(ProceedingJoinPoint joinPoint) throws Throwable {

        Boolean appliedValue = appliedLocal.get();

        boolean nested = false;

        if (appliedValue != null && appliedValue) {

            nested = true;

        } else {

            nested = false;

            appliedLocal.set(Boolean.TRUE);

        }

        if (!nested) Events.setPublisher(publisher);

        try {

            return joinPoint.proceed();

        } finally {

            if (!nested) {

                Events.reset();

                appliedLocal.remove();

            }

        }

    }


    @Override

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {

        this.publisher = eventPublisher;

    }

}


트랜잭션 범위에서 실행되는 경우에만 이벤트를 처리하기 위해 @Transactional을 적용한 경우에만 적용하도록 설정했다. 대상 메서드를 실행하기 전에 Events.setPublisher()를 이용해서 스프링의 ApplicationEventPublisher를 설정하고, 대상 메서드를 실행한 후에 Events.reset()으로 초기화하도록 했다.


이제 트랜잭션 범위에서 실행되는 도메인 객체는 다음과 같은 코드를 이용해서 이벤트를 발생시키면 된다.


public class Order {


    public void cancel() {

        ...

        Events.raise(new OrderCanceledEvent(this.id));

    }

}



2. @TransactionEventListener로 이벤트 핸들러 구현하기

스프링 4.2 이전까지는 트랜잭션과 동기화해서 뭘 실행하려면 TransactionSynchronizationManager를 사용해야 했는데, 스프링 4.2에 들어간 @TransactionEventListener를 사용하면 손쉽게 트랜잭션 커밋 이후에 이벤트 핸들러를 실행할 수 있다.


import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }



@TransactionalEventListener의 phase 속성을 사용하면 트랜잭션 커밋 이후뿐만 아니라 커밋 전, 롤백 이후, 커밋이나 롤백 이후에 이벤트를 처리하도록 설정할 수 있다.


트랜잭션 여부에 상관없이 이벤트 발생 시점에 이벤트를 처리하고 싶다면 @EventListener를 사용하면 된다.


3. @EnableAsync와 @Async로 비동기로 핸들러 실행하기

이벤트 핸들러를 비동기로 처리하고 싶다면 @EnableAsync와 @Async를 사용하면 된다. 스프링 설정 클래스에 @EnableAsync를 추가했다면, @TransactionalEventListener와 @Async를 함께 사용해서 이벤트를 트랜잭션 커밋 이후에 비동기로 처리할 수 있다.



import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Component;

import org.springframework.transaction.event.TransactionalEventListener;


@Component

public class EventHandler {


    @Async

    @TransactionalEventListener

    public void handle(OrderCanceledEvent event) {

        // ... 이벤트 처리

    }



간단한 샘플


https://github.com/madvirus/event-sample 에서 간단한 샘플 코드를 다운로드 받을 수 있다. 트랜잭션 완료 후에 비동기로 실행되는 예를 보여주기 위해 SampleService에 다음과 같이 슬립 시간을 주었다.


@Service

public class SampleService {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private JdbcTemplate jdbcTemplate;


    @Transactional

    public void doSome() {

        logger.info("raise event");

        Events.raise(new SampleEvent());

        try {

            Thread.sleep(2000L); // 이벤트 발생후 2초 슬립

        } catch (InterruptedException e) {

        }


        Integer result = jdbcTemplate.query("select 1", new ResultSetExtractor<Integer>() {

            @Override

            public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException {

                resultSet.next();

                return resultSet.getInt(1);

            }

        });

        logger.info("doSome: query result = {}", result);

        try {

            Thread.sleep(2000L); 쿼리 실행후 2초 슬립

        } catch (InterruptedException e) {

        }

    }


예제 프로젝트의 이벤트 핸들러는 다음과 같다.


@Component

public class EventHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());


    @Async

    @TransactionalEventListener

    public void handle(SampleEvent event) {

        logger.info("handle event");

    }


}


예제를 실행하면 다음과 같은 결과가 출력된다. 결과 로그를 보면 이벤트를 발생한 뒤에 EventHandler가 바로 실행되지 않은 것을 알 수 있다. 또한, 이벤트를 발생시킨 쓰레드 이름이 "main"이고 EventHandler를 실행한 쓰레드 이름이 "cTaskExecutor-1"인데 이를 통해 이벤트를 발생시킨 쓰레드가 아닌 다른 쓰레드에서 이벤트를 비동기로 처리했음을 알 수 있다.


2018-02-05 12:47:21.794  INFO 20636 --- [           main] eventsample.app.SampleService : raise event

2018-02-05 12:47:23.830  INFO 20636 --- [           main] eventsample.app.SampleService : doSome: query result = 1

2018-02-05 12:47:25.869  INFO 20636 --- [cTaskExecutor-1] eventsample.app.EventHandler : handle event


  1. coding8282 2017.03.19 09:43

    저는 Domain Event Handler를 따로따로 만들어서 사용했었는데, 이 방법을 적용하니 정말 좋네요. 비동기 처리도 간단하구요~... 응용 범위가 굉장히 넓을 것 같습니다~~~ 감사합니다

  2. tigmi 2018.01.25 22:58

    마지막에 말씀해주신 것처럼 @Async와 @TransactionalEventListener를 동시에 설정했을 경우
    제가 로컬에서 시도해봤을 때는
    1.transactionA에서 handle이 호출 될 경우
    2. @Async가 먼저 실행 되어 새로운 thread에서 새로운 transactionB가 시작되고
    3. transactionB가 commit되었을 때 실제 handle 함수가 실행되는 걸로 알고 있는데,
    의도하신 대로 transactionA가 commit된 이후에 async하게 handle 함수가 실행되나요?

    • madvirus 2018.02.05 12:43 신고

      https://github.com/madvirus/event-sample 에 간단한 예제를 올렸습니다.

      github에서 받으신 뒤에 EventSampleApplication을 실행해보시면 됩니다.

    • tigmi 2018.02.06 18:36

      확인해보니 제가 예전에 @TransactionalEventListener를 잘못된 방법으로 사용하고 있었네요 .공유 감사합니다!!

  3. podo 2020.03.04 19:51

    DDD Start! 읽고 Event를 개인프로젝트 적용해보고 있습니다.

    Spring에서 지원해주는게 있을 것 같아 찾아봤는데,

    너무 정리가 잘되있어서 보니 저자님 블로그였네요 ㅎㅎ(!)

    감사합니다 :) 많이 배우고 갑니다!!!

반응형

최근 프로젝트에서 다음 세 가지 종류의 테스트를 작성하고 있다.

  • 단위 테스트
  • 서버 기능을 확인하기 위한 컨트롤러, 서비스, 리포지토리 등에 대한 통합 테스트
  • 웹 브라우저에서 DB까지 모두를 포함한 E2E(end-to-end) 테스트
통합 테스트나 E2E 테스트를 작성하다보면 테스트 목적에 맞게 @Before나 테스트 대상 기능을 실행하기 전에 DB를 원하는 상태로 변경한다. DB 상태를 맞추기 위해 테이블을 비우거나(truncate), 특정 조건의 데이터를 지우거나 추가한다.

E2E 테스트를 작성하지 않은 기능은 통합 테스트와 단위 테스트를 기반으로 기능을 완성한 뒤에 직접 수동으로 웹 브라우저에서 테스트를 진행한다. 이 과정에서 한 가지 불편한 점이 있다. 수동으로 테스트를 하려는 시점의 데이터는 이전에 마지막으로 수행한 수동 시점의 데이터가 아니라는 점이다. 예를 들어, 수동 테스트에서 로그인할 때 사용한 'admin' 계정을 통합 테스트 과정에서 비활성화했다면 로그인이 안 되는 그런 식이다.


이런 불편을 줄이려고 사용한 방법은 다음의 두 가지 정도다.

  • DB 구분
  • 수동으로 데이터 초기화

자동화된 테스트와 수동 테스트에서 사용하는 DB를 구분하는 방법은 메이븐과 같은 빌드 도구의 프로필이나 스프링 프로필을 사용해서 처리한다. 이 방식을 사용하면 개발 과정에서 실행한 테스트 코드가 DB 상태를 변경해도, 수동 테스트에서 사용할 데이터는 영향을 받지 않는다. 내가 이전에 웹 브라우저에서 확인한 마지막 상태에서 다시 시작할 수 있어, 맥이 끊기지 않는 느낌을 받았다.


반면에 개발 과정에서 자동화 테스트용 DB의 스키마를 변경하면 수동 테스트용 DB 스키마를 함께 변경해 주어야 한다. 이 단점은 Flyway나 Liquibase와 같은 도구를 완화할 수 있다.


자동화된 테스트와 수동 테스트에서 사용하는 DB가 같다면, 수동 테스트를 시작하기 전에 데이터를 수동 테스트에 알맞은 상태로 되돌리는 방법을 사용한다. 초기화를 위한 SQL 파일을 하나 만들고 이 파일을 수동 테스트 전에 실행하는 방식을 주로 사용한다. 로컬 서버를 구동할 때 옵션을 주면 SQL 파일을 실행하는 방법도 사용해봤다. 이 방법은 서버 구동 시점에 데이터를 초기화하니까 편한데, 대신 습관적으로 서버 실행 명령어를 입력하다보면 원치 않게 데이터가 초기화되는 상황이 종종 발생하기도 한다.


하다보면 두 방식을 혼용해서 사용하고 싶어진다. 방식이 늘어나면 뭔가 더 많이 하는 것 같아 거부감이 들기도 하지만, 테스트 코드가 그런 것처럼 결과적으로 개발 시간을 줄여주는 효과를 준다.

반응형

난 개발과 관련해서 문서 작성을 선호하는 편이다. 그렇다고 검수 산출물과 같은 형식적인 문서 작성을 선호하는 것은 아니다. 주로 다음을 위한 문서 작성을 선호한다.

  • 대화 재료
  • 미래에 시스템을 운영할 사람

대화 재료로서의 문서는 회의에서 효과가 크다. 기능 스펙, 설계 초안, 구현 아이디어, 목차 등을 정리한 문서는 대화를 시작하기 위한 좋은 재료가 된다. 이때 문서는 소통이 가능한 수준의 내용만 담으면 된다. 간단한 클래스 다이어그램이나 상태 다이어그램이어도 되고, 자유 형식의 사용자 스토리여도 된다. 또는 종이에 끄적인 내용을 복사해서 함께 봐도 되고, 화이트보드에 그려 놓은 내용을 사진으로 찍어 출력한 문서여도 된다. 이 문서를 바탕으로 대화를 하면서 내용을 발전시켜 나갈 수만 있으면 된다.


대화 재료로 사용할 문서가 격식을 갖출 필요는 없지만, 그렇다고 내용을 대충 만들어도 된다는 건 아니다. 재료가 나쁘면 대화도 함께 나빠진다. 대화 과정에서 발전시켜야 할 내용이 문서에 없으면, 그 문서는 도움이 안 된다. 그래서, 대화 재료에 사용할 문서를 작성하는 사람은 논의할 내용의 핵심이 잘 담기도록 노력해야 한다.


잘 만든 문서 하나는 대화 참석자를 빠르게 핵심 주제로 안내하고 대화 결과물이 좋을 가능성을 높여준다. 몇 년 전에 스프링캠프 발표 주제를 정리하기 위해 운영진과 회의를 한 적이 있다. 그때 발표하고 싶은 주제와 개요 수준의 목차를 담은 마인드맵 문서를 대화 재료로 들고 나갔다. 그 문서를 기반으로 빠르게 컨텍스트를 맞춘 덕에 회의가 수월하게 끝난 기억이 난다.


미래에 시스템을 운영할 사람을 위한 문서는 중요하다. 퇴사 시점에 이런 문서를 작성해서 후임자에게 넘기는 경우가 많은데, 그 시점보다는 중요 마일스톤을 찍는 시점에 운영과 관련된 문서도 함께 정리하는 것이 좋은 것 같다. 한참 시간이 흘러 작성하려면 왜 배포 과정에서 우회 방법을 선택했는지, 특정 IP를 왜 열었는지와 같은 이유를 잊게 된다. 이는 중요한 정보를 사라지게 만든다.


운영을 위한 문서는 코드처럼 정리를 해야 한다. 더 이상 필요 없으면 삭제해서 혼란을 제거하고, 기존 문서의 설명과 다른 절차가 추가되었다면 반영해서 정보가 사라지지 않도록 해야 한다. 운영을 위한 문서가 현재를 반영하지 못하면 그 문서는 죽은 문서가 되고, 중요 정보를 구전으로 전수받아야할 상황이 발생한다.


이런 문서는 정확한 표현이 중요하다. 애매모호한 표현을 사용하면 문서를 읽는 사람을 혼란스럽게 할뿐이다. 이는 말로 하는 것과 차이가 없다. 말로 전달하는 것 이상의 효과를 얻으려면 공유할 내용을 적절한 방법으로 표현하는 연습을 해야 한다. 경우에 따라 문장을 쓰거나 코드를 보여주거나 다이어그램으로 표현할 수 있어야 한다. 


이런 표현력이 쌓이면 대화 중에도 즉석에서 도움이 되는 대화 재료를 만들 수 있다. 아래 그림은 몇 해 전에 회의 중간에 그린 다이어그램이다. 간단한 다이어그램이지만 이 그림이 나오기 전까지 회의에 참여한 사람들은 서로 다른 소리를 하고 있었다. 개발자, PM, 기획자, 또 다른 개발자가 같은 이야기를 듣고 서로 다르게 이해를 했다. 그런 상황이 계속될 기미가 보이기 시작해서, 자리에서 일어나 화이트보드에 대화를 정리하기 위한 다이어그램을 그렸다. 이 그림이 그려진 이후 같은 이해를 바탕으로 회의가 정리되기 시작했다.



개발자가 본능적으로 문서 작성을 싫어하는지는 모르겠지만, 좋은(?) 개발자가 되려면 의사소통 능력이 중요하며 그 능력을 향상시켜주는 것 중 하나가 문서 작성 역량이라고 생각한다. 문서는 말과 함께 의사소통을 위한 중요한 수단이기 때문이다. 코드의 표현력이 중요한 이유가 코드에 담긴 지식을 자신을 포함한 누군가에게 전달하기 위함인것처럼, 동일하게 문서 역시 누군가에게 뭔가를 전달할 때 사용할 수 있는 방법이다.


문서를 작성하는 것은 너무 너무 귀찮은 일이지만, 도움이 되는 문서를 제대로 작성하는 것은  매우 중요한 일이기도 하다. 이 중요한 일을 잘 하기 위한 연습을 게을리하는 개발자는 되지 말자.


반응형

DDD Start 부록 모임 발표자료.




  1. MJC 2016.07.22 02:36

    안녕하세요.

    여기저기서 책 소개를 보고 구매를 고려중입니다.

    책 목차를 보니까 DDD 전체가 아닌 코딩 부분인 tactic 부분만 있는거 같은데요...
    Eric Evans가 자신의 책에서 tactic 부분을 앞부분 챕터에 배치한것이 가장 큰 실수라고 했고..그것을 보완하기 위해 Vaughn Vernon의 속칭 빨간책에서는 tactic 부분은 약간 뒤로 밀려놨습니다. 두 저자 모두가 DDD의 핵심은 strategy라고 계속 강조를 하는데 DDD Start에서는 strategy 부분이 아예 통째로 다 빠진거 같습니다.

    특별한 이유가 있나요? 슬라이드를 보면 또 아닌거 같은데..목차만 보면 그런 느낌이 듭니다.

    한글책이 나왔다는 소식에 읽어보려고 하는데 의아해서 질문 드립니다.

    • madvirus 2016.07.25 17:51 신고

      안녕하세요, MJC님.
      말씀하신 것처럼 이 책은 tactic 위주로 구성이 되어 있고 strategy에 해당하는 내용은 9장에 갼락하게 나와 있습니다.

      이렇게 구성한 이유는 이 책이 DDD에 입문하는데 도움을 주는 징검다리로 사용하기 위함입니다.

      evans나 vernon의 책이 매우 훌륭한 책이지만 그 책을 어려워하는 분들이 많았기에 입문자가 조금 쉽게 DDD에 접근할 수 있는 책이 있었으면 좋겠다고 생각했고, 그 어려워하는 것 중에 구현 관점에서 이 책을 구성하게 되었습니다.

      이를 통해 DDD에 관심을 유발하고, 이후에 더 깊게 빠지고 싶은 분들이 자연스럽게 저 책들로 연결되기를 바라고 있습니다.

      그러다보니 이미 DDD에 익숙하신 분들은 이 책보다는 evans의 책이나 다른 깊이 있는 책을 한번 더 읽는게 좋습니다.

  2. MJC 2016.07.26 04:10

    답변 감사드립니다.

    혹시 strategy쪽으로도 책을 내실 생각이 있으신가요?

    개인적으로 접근하기에는 아무래도 tatic위주면 코드로 빨리 시작할수있고 해서 좋은데..아무래도 DDD를 팀 혹은 회사 전체에 적용하려면 strategy중요한데...이 부분이 사실 쉽지가 않더라구요..경험이 많은 개발자들은 evans나 vernon의 책을 봐도 되겠지만 신입의 경우는 쉽지가 않더군요. 아무래도 한글이 있으면 많은 도움이 될거같습니다.

반응형

신림 프로그래머 공개 모임 2016에서 발표한 MVP 패턴 소개 자료



반응형

내장 톰캣을 사용해서 동작하는 스프링부트 기반 웹어플리케이션을 AJP 프로토콜을 이용해서 아파치 HTTPD 웹 서버와 연동할 일이 생겼다. 연동을 위해 할 내용은 생각보다 간단했다. 다음의 두 가지만 해 주면 된다.

  1. 스프링부트 어플리케이션: 내장 톰캣을 위한 AJP 커넥터 설정
  2. 아파치 웹 서버 : ProxyPass로 ajp 연동 설정

스프링부트 내장 톰캣 설정


내장 톰캣 설정에 AJP 커넥터를 추가한다.


@Configuration

public class ContainerConfig {

    @Bean

    public EmbeddedServletContainerCustomizer containerCustomizer() {

        return container -> {

            TomcatEmbeddedServletContainerFactory tomcat

                    (TomcatEmbeddedServletContainerFactory) container;


            Connector ajpConnector = new Connector("AJP/1.3");

            ajpConnector.setProtocol("AJP/1.3");

            ajpConnector.setPort(9090);

            ajpConnector.setSecure(false);

            ajpConnector.setAllowTrace(false);

            ajpConnector.setScheme("http");

            tomcat.addAdditionalTomcatConnectors(ajpConnector);

        };

    }

}


아파치 설정


아파치에 톰캣 관련 설정을 추가한다.


ProxyPass "/contextPath" "ajp://localhost:9090/contextPath"


그리고 아파치 서버를 재시작하면 끝이다.

  1. 임예준 2016.06.14 10:29

    이걸 몰라서 boot 내장 톰캣을 못쓰고,
    static 과 java 리소스를 httpd 와 외부 톰캣에 각각 배포 해야 되나 싶었는데 말입니다.
    ProxyPass를 사용하면 JK를 안써도 되는거죠?

    • madvirus 2016.06.14 11:42 신고

      준! 아파치 httpd 2.1부터 가능하고
      https://httpd.apache.org/docs/2.4/mod/mod_proxy_ajp.html 문서를 보면 도움이 될 듯.

  2. 김린 2018.12.12 15:18

    현재 스프링부트 2.0.3 버전으로 개발 중입니다.
    내장 톰캣과 아파치 연동 하려는데,
    소스 추가시 EmbeddedServletContainerFactory 가 없습니다.
    버전 업에 따른 업데이트된 다른 방법이 있는지요?

    • madvirus 2018.12.20 09:21 신고

      직접 해 본 건 아닌데요,
      https://docs.spring.io/spring-boot/docs/2.0.7.RELEASE/reference/htmlsingle/#boot-features-customizing-embedded-containers 문서를 보면 TomcatServletWebServerFactory 클래스를 사용하면 될 것 같습니다.

반응형

스프링 부트는 application.properties 파일을 이용해서 설정을 제공한다. 이 파일에는 부트가 제공하는 프로퍼티뿐만 아니라 커스텀 프로퍼티를 추가할 수 있다. 커스텀 프로퍼티를 사용하는 몇 가지 방법이 있는데 그 중에서 설정 프로퍼티 클래스를 사용하면 관련 설정을 한 클래스에서 관리할 수 있어 편리하다.


설정 프로퍼티 클래스를 사용하는 방법은 간단하다. 먼저, 설정 프로퍼티 클래스로 사용할 클래스를 작성한다. 이 클래스는 다음과 같이 작성한다.

  • @ConfigurationProperties 애노테이션을 클래스에 적용한다. application.properties 파일에서 사용할 접두어를 지정한다.
  • application.properties 파일에 설정한 프로퍼티 값을 전달받을 setter를 추가한다.
  • 프로퍼티 값을 참조할 때 사용할 get 메서드를 추가한다.
다음은 설정 프로퍼티 클래스의 작성 예이다.


@ConfigurationProperties(prefix = "eval.security")

public class SecuritySetting {

    private String authcookie;

    private String authcookieSalt;


    public String getAuthcookie() {

        return authcookie;

    }


    public void setAuthcookie(String authcookie) {

        this.authcookie = authcookie;

    }


    public String getAuthcookieSalt() {

        return authcookieSalt;

    }


    public void setAuthcookieSalt(String authcookieSalt) {

        this.authcookieSalt = authcookieSalt;

    }


}


application.properties 파일의 프로퍼티의 설정 프로퍼티 클래스의 프로퍼티는 다음과 같이 매칭된다.


* application.properties 프로퍼티 이름 = prefix + "." + setter 프로퍼티 이름


예를 들어, 위 코드에서 prefix는 "eval.security"이므로 setAuthcookie()에 해당하는 프로퍼티는 "eval.security.authcookie"가 된다. 만약 setter의 프로퍼티 이름 중간에 대문자가 포함되어 있다면 다양한 매핑을 지원한다. 예를 들어, 위 코드에서 authcookieSalt가 중간에 대문자를 포함하고 있는데 이 경우 다음과 같은 프로퍼티로부터 값을 가져올 수 있다.

  • eval.security.authcookieSalt
  • eval.security.authcookie-salt
  • eval.security.authcookie_salt
  • EVAL_SECURITY_AUTHCOOKIE_SALT

@ConfigurationProperties를 적용한 클래스를 만들었다면, 다음 할 일은 빈으로 등록하는 것이다. 스프링 빈으로 등록하는 방법은 간단하다. 다음의 두 가지 방식 중 하나를 사용하면 된다.

  • @EnableConfigurationProperties을 이용해서 지정하기
  • 설정 프로퍼티 클래스에 @Configuration 적용하기 (또는 설정 프로퍼티 클래스를 @Bean으로 등록하기)

먼저 @EnableConfigurationProperties을 사용하는 방법은 다음과 같다. @EnableConfigurationProperties을 설정 클래스에 추가하고 @EnableConfigurationProperties의 값으로 @ConfigurationProperties를 적용한 클래스를 지정하면 된다. 이 경우 @EnableConfigurationProperties는 해당 클래스를 빈으로 등록하고 프로퍼티 값을 할당한다.


@SpringBootApplication

@EnableConfigurationProperties(SecuritySetting.class)

public class Application { ... }


두 번째 방법은 설정 프로퍼티 클래스를 빈으로 등록하는 것이다. 스프링 부트는 컴포넌트 스캔을 하므로 설정 프로퍼티 클래스에 @Configuration을 붙이면 자동으로 빈으로 등록된다. 스프링 부트는 해당 빈이 @ConfigurationProperties를 적용한 경우 프로퍼티 값을 할당한다.


@ConfigurationProperties 적용 클래스를 빈으로 등록했다면 이제 설정 정보가 필요한 곳에서 해당 빈을 주입받아 사용하면 된다. 예를 들면 다음과 같이 자동 주입 받아 필요한 정보를 사용하면 된다.


@Configuration

public class SecurityConfig {

    @Autowired

    private SecuritySetting securitySetting;


    @Bean

    public Encryptor encryptor() {

        Encryptor encryptor = new Encryptor();

        encryptor.setSalt(securitySetting.getAuthcookieSalt());

        ...

    }


@ConfigurationProperties를 사용할 때의 장점은 다음과 같다.

  • 필요한 외부 설정을 접두어(prefix)로 묶을 수 있고 중첩 설정을 지원한다. (예, YAML을 사용하면 계층 구조로 묶을 수 있다.)
  • 기본 값을 쉽게 지정할 수 있다. 설정 프로퍼티 클래스의 필드에 기본 값을 주면 된다.
  • int, double 등 String 이외의 타입을 설정 프로퍼티 클래스의 프로퍼티에 사용할 수 있다. 설정 파일의 문자열을 설정 프로퍼티 클래스의 프로퍼티 타입으로 스프링이 알아서 변환해준다.



+ Recent posts