deview 2016 발표자가 되었다면 풍부하고 실제적인 자료를 전달할 수 있을 터인데, 아깝게 채택되지 못했다.


새로운 것만이 좋은 주제가 아니라, 우리 개발자들이 잘 하고 있는 일을 더욱 개선하는 작업도 좋은 사례로 남기고 싶었다.



이 내용은 나 혼자한 것은 아니고 동료들과 함께 진행한 내용이며,  해당 내용을 간단하게 글로 정리한다. 


(Docker, Jenkins, Rspec에 대한 팁은 내 블로그에서 검색할 수 있을 것이다.)




Spec by Example이라 했지만, (자세한 내용은 https://en.wikipedia.org/wiki/Specification_by_example를 참고한다)

Acceptance Test(인수 테스트)라고 좋고, Integration Test, Functional Test도 괜찮다. 

격리된 환경에서 서비스를 예제로 테스트하는 테스트 환경(Spec by Example)을 Docker로 빠르게 처리하는 방법을 진행했고, 이를 간단히 소개한다.





나는 그동안 직장 생활을 하면서 애플리케이션(또는 플랫폼)을 Spec by Example를 진행한 서비스/플랫폼을 거의 본적이 없다. (간단한 test unit이나 ui 속도 체크, js 속도 체크 정도는 있었지만, 그 이상의 통합 테스트가 없었다)


그리고, 나는 테스트의 의미를 깊이 생각하지 못했다. 개발하기도 바쁜데, 어떻게 테스트 코드까지 짜야하나 하는 불편함이 가지고 있었다. SKP에서 SI업체에서 만든 코드(정리 안된 코드, 테스트 없는 코드)를 받으면서, 정말 어이가 없는 환경에 대한 깊은 고민이 있었다. 테스트 코드에 대한 중요성을 생각하게 되었고, ansible을 공부하게 되었다.(deview 2014 발표)






<거대한 공룡과 싸우기 위해 

필요한 다양한 방패와 무기가 있어야 한다는 그림인데,

여전히 그 때나 지금이나 현실은 조금도 변함이 없는 것 같다. 나에게는 언제나 개발을 대할 때는 이런 것 같다. 
특히 대용량 서비스는 더 장난 아니다.>

이미지 참조 : https://www.amazon.com/Compilers-Principles-Techniques-Alfred-Aho/dp/0201100886







반면, 상용 서비스에서 통합 테스트 환경은 네이버 뉴스에서의 javadoc을 이용한 Functional Test 정도인 듯 하다. (소문에 따르면 지금은 사라졌다고 들었다).

http://d2.naver.com/helloworld/87523



지금 회사에 와서 서비스 개발하면서 가장 큰 수확은 바로 Spec By Example( 통합 테스트 환경)이었다. 수십 개의 컴포넌트가 유기적으로 동작하기 위한 테스트 환경이 Jenkins + Rspec을 기반으로 이미 구축되어 있었다. 

(혹시,,

Rspec을 잘 모른다면, https://semaphoreci.com/community/tutorials/getting-started-with-rspec를 읽으면 좋은 시작점이 될 것이다. 

Jenkins + RSpec 연동을 위해서는 https://sephinrothcn.wordpress.com/2014/04/24/run-rspec-with-jenkins/을 참조하면 좋을 것 같다)


(참고로 Rspec 말고도 최근에는 여러 Spec By Example 프레임워크가 나오고 있다.)






Ruby를 전혀 몰랐지만, Rspec을 통한 테스트 환경이 얼마나 서비스를 강력하게 만드는지 깨닫게 되었다.

격리된 환경에서 수십 개의 컴포넌트(자바 서버, MariaDB, Redis, ES, Mongo 등등......)에 수천 개의 시나리오 테스트가 동작하는 구조로 튼튼한(robust)할 수 있었다.


- 최대한 상용 환경과 비슷하다. 

- 리눅스, Mac OS에서 잘 동작한다.

- 인수 인계 문서가 필요없다. 시나리오 코드가 결국 어떻게 동작되는지를 설명한다.

- 코드 리팩토링하더라도 Spec 이 깨지는 일을 찾아내 문제를 빨리 해결할 수 있다.

- API를 기반으로 하기 때문에 스토리지를 변경하는 마이그레이션(Migration)을 쉽게 할 수 있다.

- 버전 별 API가 진행된다. 

- 운영을 하지만, Spec이 있기 때문에 

- Continuous Deployment의 근간, Continous


이외에 수 많은 장점이 있었다.





수천 개의 테스트를 시퀀셜(sequencial)로 진행될 때 가상 장비에서 50분이 넘어가기 시작했다. 이를 위해 병렬로 테스트를 진행하려 했지만, Rspec의 이슈가 아니라 수 천개의 테스트를 돌리다보니 서비스의 제약 사항을 넘지 못해 병렬로 진행하는 것은 실패했다.


중형 장비를 사용해 50분의 소요시간을 20분대로 줄였다.


이후, 시간이 흘러 Spec By Example 코드가 많아지면서 중형 장비가 다시 57분이 되었다. 


이를 해결하기 위해 여러 개발자와 함께 Docker와 Redis를 준비했다.그래서 8~9분 대로 줄어들도록 진행했고, Continuous Delivery(테스트가 성공되면 자동 개발 서버 배포)를 진행시켰다. 만족도는 좋았다!!!

Devops의 시간을 절약했다!!






이제 그 얘기를 진행한다. 


보통 Continuous Integration, Continuous Delivery, Continous Deployment의 근간은 Spec By Example이라고 생각한다. 그리고, 절제된 아키텍처가 필요했다.


Docker를 활용했지만, Docker의 이미지에 모든 것을 넣지 않았다. 변경이 자주 될만한 컴포넌트와 변경이 자주 안될만한 컴포넌트로 나눴다. 



변경이 자주 되지 않은 이미지를 Docker 이미지로 만들었다. 무척 큰 용량의 이미지가 생성된다.


- Docker 이미지를 생성한다

- Docker 이미지를 registry에 업로드한다. 

- Docker 이미지를 테스트 중형 서버에 배포된다. 




Jenkins에서 Job이 돌면서 Docker에 추가될 부분을 추가하고 여러 중장비에 Docker 이미지를 잘 사용할 수 있도록 했다.(정확히 말하면 병목 지점을 최대한 병렬화했다)


- 소스 커밋이 되면, Jenkins job 실행한다. (multijob + multijob)

- Job 실행시 docker 이미지를 사용해서 여러 데몬을 띄워 최대한의 성능을 낸다.

- 결과를 취합한다. (rspec + redis)

(주의할 사항은 Docker가 메모리 사용량이 높기 때문에 메모리에 대한 관리가 특별히 필요하다)

- 수천 개의 테스트를 모두 통과(정상)하면 개발 서버에 자동으로 배포한다. - Continuous Delivery



그리고, 여러 대의 중장비에 수십 개의 테스트 환경으로 분할할 때는 제대로 만든 분배 알고리즘이 만들었고, 잘 동작했다.


 



이를 기반으로 QA없이 테스트를 진행하고 빠른 Spec By Example을 통해 배포 시간을 최대한 줄였다. 








배운점 : 

1. 병목부분을 정확하게 아는 것이 중요하다. 

2. Docker, RSpec, Jenkin가 무엇인지 아는 것은 쉽지만, 제대로 아는 것은 쉽지 않다. 

   Docker를 무조건 이미지로 만들고 다운로드하는 것은 Continuous Integration에 적합치 않을 수 있다. 

   효율적인 구조로 만드는 개선 노력이 필요하다.

3. Docker를 여러 대의 중장비에 수십 개의 테스트 환경(Spec By Example)으로 동작시킬 때는 

     중장비를 사용하는 것이 좋다. 






소감 : 외국의 모델을 따라하는 게 아니라, 세계에 없는 우리 만의 모델을 만들었다는 점에서 기분이 좋았고, DevOps 사례로 남을 듯 하다. 


다음 시도 : 일부러 프레임워크를 쓰지 않았다. robust한 환경을 위해 Mesos + Marathon 대신 Google의 Kubernetes를 적용해서 반-자동화를 테스트해보고 적용할 예정이다. 





Posted by '김용환'
,


스칼라의 HashSet은 자바의 HashSet보다 빠른 것으로 알려져 있다.

그 이유는 HashSet의 구현이 해시 트라이(Hash trie)로 구현되어 있다. 


참고로 해시 트라이는 스칼라 공식 문서에서 간단히 소개하고 있다. 


http://docs.scala-lang.org/overviews/collections/concrete-immutable-collection-classes.html#hash-tries


Their representation is similar to vectors in that they are also trees where every node has 32 elements or 32 subtrees. But the selection of these keys is now done based on hash code. For instance, to find a given key in a map, one first takes the hash code of the key. Then, the lowest 5 bits of the hash code are used to select the first subtree, followed by the next 5 bits and so on. The selection stops once all elements stored in a node have hash codes that differ from each other in the bits that are selected up to this level.






또한, 해시 트라이는 속도를 빠르게 개선하는 분할상환 상수 시간( amortized constant time)인데, 

원래 Hash가 검색은 빠르지만, 추가/삭제가 나쁜 구조인데, 스칼라는 이를 개선했다. 


http://docs.scala-lang.org/overviews/collections/performance-characteristics.html




 lookupaddremovemin
immutable    
HashSet/HashMapeCeCeCL



aCThe operation takes amortized constant time. Some invocations of the operation might take longer, but if many operations are performed on average only constant time per operation is taken.


여기 나오는 분할상환 시간 분석에 대해서는 아래 위키를 참조하거나 인터넷을 참조한다. 

분할상환 시간 분석은 알고리즘의 전반적인 연산 집합에 대해 비용이 높은 연산, 그리고 비용이 덜한 연산 모두를 함께 고려하는 기법이다.


https://ko.wikipedia.org/wiki/%EB%B6%84%ED%95%A0%EC%83%81%ED%99%98%EB%B6%84%EC%84%9D






또한 아래 자료에도 분할상환 시간에 대한 시간에 대해서 설명되어 있다. 


http://scabl.blogspot.kr/2014/10/what-heck-is-amortized-constant-time.html








Posted by '김용환'
,


대수적 자료형(ADT)를 시작할 때 참조한 문서를 기반으로 공부했다. 

http://tpolecat.github.io/presentations/algebraic_types.html







스칼라 문서를 보다보면, Algebraic Data Types, ADT(대수적 자료형)이 언급된다. 

ADT를 이해하려면 카테시안 곱의 이해를 배경으로 한다.  먼저 수학적인 집합부터 얘기해 본다.


(참조)

https://en.wikipedia.org/wiki/Algebraic_data_type




먼저 Ordered Set(순서 집합)이라는 것을 보자.

Ordered Set은 2 개 이상의 변수가 순서를 가진 함수 또는 레코드를 표현한 타입이다. 

(x, y) 이런 형태를 가지고 있다. 




이제, Cartesian Product(카테시안 프로덕트, 카테시안 곱, 곱 집합)을 이해해 보자. 


SQL에서 Join을 다룰 때 나오는 용어와 같은데, 두 집합 A와 B의 원소가 있을 때, 이를 통해 만들어지는 모든 순서쌍 (a, b)들의 집합이다. 수학적으로 표현하면, 'a∈A 이고 b∈B 인 모든 순서쌍 (a,b)들의 집합'이라 할 수 있겠다.

그리고, AxB라고 한다. 


A = { 1, 2}, B = {x, y} 라면, A x B = { (1,x), (1,y), (2,x), (2,y) }이다. 





이제, 스칼라로 집중해 본다.


스칼라의 특정 타입에 대한 '값의 개수'를 가진다. Nothing은 없고, Unit은 1개, Boolean은 true와 false, Byte는 256개, String은 엄청나게 많은 값의 개수를 가진다. 


카테시안 곱을 사용하면, 아래와 같은 타입을 가질 때 가질 수 있는 모든 타입의 개수는 다음과 같다. 


(Unit , Boolean) = 1 × 2 = 2 

(Byte , Boolean) = 256 × 2 = 512



ADT는 합성된 타입(Composit Type)을 기반으로 유한한 값의 개수와 잘 정해진 타입으로 이루어지는 형태를 말한다. 즉, 집합적 개념에서 보면, 모두 합쳐면 모든 세상을 만들 수 있는 것을 말한다. 


Option이 대표적인 ADT라 할 수 있다. Option은 다양한 값을 표현하는 Some과 아무 것도 없음을 의미하는 None으로 만들어져 있다. 이와 비슷하게 Either와 Try가 있고, List는 Lil과 일반 리스트가 존재한다. 



sealed trait Option[+A] case object None extends Option[Nothing] case class Some[A](a: A) extends Option[A]



sealed trait Either[+A, +B] case class Left[A](a: A) extends Either[A, Nothing] case class Right[B](b: B) extends Either[Nothing, B]




sealed trait Try[+A] case class Success[A](a: A) extends Try[A] case class Failure[A](t: Throwable) extends Try[A]




sealed trait List[+A] case object Nil extends List[Nothing] case class ::[A](head: A, tail: List[A]) extends List[A] object List { def apply[A](as: A*): List[A] = ... }







스칼라에서 ADT는 sum type을 의미한다. 


아래 코드 처럼 Pet이라는 sealed trait가 있고, Pet은 딱 3 종류의 타입 Cat, Fish, Squid만 존재하게 한다. ADT에서 말한대로 한정된 타입이 존재한다. 


스칼라에서 Pet 하위 클래스를 사용할 때, ADT와 스칼라 컴파일러 간의 연관성을 확인해 본다.


sealed trait Pet
case class Cat(name: String) extends Pet
case class Fish(name: String, color: String) extends Pet
case class Squid(name: String, age: Int) extends Pet

val bob: Pet = Cat("matt")

스칼라에서의 ADT 타입은 sum type을 의미하는데, 특정 상위 클래스를 상속한 여러 case class 클래스로 만드는 경우를 의미한다. 


Encoding을 Pet <-- Cat, Fish, Squid로 진행하고,

Decoding을 Pet match { ... } 으로 진행하는 경우이다. 



sum type은 tagged_union(태그 유니온, 혹은 꼬리가 붙은 유니온)이라 불린다.  자세한 내용은 위키를 참조한다. 


(참고)

https://en.wikipedia.org/wiki/Tagged_union






예문을 실행해본다.



Pet의 하위 클래스인 Cat과 Squid에 대한 패턴 매칭을 시행한다. 

object Pet {
sealed trait Pet
case class Cat(name: String) extends Pet
case class Fish(name: String, color: String) extends Pet
case class Squid(name: String, age: Int) extends Pet
}

object Main extends App {

import com.google.Pet._

val bob: Pet = Cat("Matt")

def sayHi(pet: Pet): String =
pet match {
case Cat(n) => "Meow " + n + "!"
case Fish(n, _) => "Hello fishy " + n + "."
case Squid(n, _) => "Hi ssss " + n + "."
}

println(sayHi(Cat("Bob")))
println(sayHi(Squid("Steve", 10)))
}

결과는 다음과 같다. 


Meow Bob!

Hi ssss Steve.




하지만, Squid 에 주석을 달면 컴파일 에러가 난다. 



object Pet {
sealed trait Pet
case class Cat(name: String) extends Pet
case class Fish(name: String, color: String) extends Pet
case class Squid(name: String, age: Int) extends Pet
}

object Main extends App {

import com.google.Pet._

val bob: Pet = Cat("Matt")

def sayHi(pet: Pet): String =
pet match {
case Cat(n) => "Meow " + n + "!"
case Fish(n, _) => "Hello fishy " + n + "."
//case Squid(n, _) => "Hi ssss " + n + "."
}

println(sayHi(Cat("Bob")))
println(sayHi(Squid("Steve", 10)))
}


런타임 아닌 컴파일 타임 때 exhautive 에러를 발생한다. (전문 용어로 패턴 매칭의 Exhaustiveness Checking 이라 한다) 컴파일러로 하여금 코드를 신뢰성 있게 개발할 수 있도록 도와준다.

 

Warning:(39, 5) match may not be exhaustive.

It would fail on the following input: Squid(_, _)

    pet match {








ADT에 대해서 감이 잡혔으면, 아래 블로그로 공부하면 감이 더욱 잡힌다.


https://bertails.org/2015/02/15/abstract-algebraic-data-type/




Posted by '김용환'
,