오픈소스 CATS로 간단히 퍼즈(Fuzz) 테스트

퍼징이란

퍼즈(Fuzz) 또는 퍼징(Fuzzing)이란 블랙박스 테스트 의 한 방법으로 정상/비정상 데이터를 자동으로 생성하고 함수나 API에 전달한다. 어떻게 보면 암호 해독(crypto anaylysis) 과정과 비슷하게 보이지만 퍼징은 프로토콜이나 입력 데이터 타입에 의존적이다. 그래서 실패한 테스트 결과들은 암호 해독과는 다르게 실제로 유의미하다.

여담으로 몇년전 대기업에서는 유사한 테스트를 수행하기 위해서 전문 인력까지 두었었다. 그때도 도구를 사용하긴 했지만 테스트 설정부터 수행 후 보고서 작성까지 상당히 많은 부분에서 고급 인력들을 투입했었는데, 이번에 발견한 도구는 만약 테스트 대상이 Rest API이고 Open API 스펙 (i.e. swagger)을 가지고 있다면 CI/CD에 바로 연동하고 바로 깔끔한 리포트를 확인할 수 있을 정도로 고도화 되었다. 소프트웨어 테스트쪽은 정말 빠르게 컴퓨터로 대체되어가고 있는 것 같다.

CATS

https://github.com/Endava/cats

CATS는 자바 기반의 라이브러리로 거의 코딩을 하지 않고도 몇백가지의 API테스트를 자동으로 수행해준다. 6.0 버전 기준으로 76개의 퍼저(Fuzzer)가 존재한다.

퍼저는 크게 5가지로 분류된다

  • 필드 퍼저 – Post 요청의 몸통이나 URL의 경로 변수(path variable)을 대상으로 하는 퍼저
  • 헤더 퍼저 - HTTP 헤더들을 대상으로하는 퍼저
  • HTTP 퍼저 - 필드나 헤더와 관계없는 HTTP 요청을 대상으로하는 퍼저
  • API 계약 검증 퍼저 – Open API 정의가 베스트 프랙티스를 따르고 있는지 검사하는 퍼저.
  • 특수 퍼저 - 보안이나 특별한 절차를 필요로 하는 조금 더 복잡한 행동들을 테스트하기 위한 퍼저

팀에서 관리중인 내부 API는 이미 swagger 2.0 으로 관리되고 있었기 때문에 CATS jar를 다운 받은 뒤 아주 간단하게 테스트를 실행할 수 있었다.

cats.jar --contract=swagger.yaml --server=$FUZZ_SERVER_ADDR --headers=header.yml --refData=refData.yml

아틀라시안은 내부 API 인증에 JWT토큰의 일종인 ASAP을 사용하기 때문에 모든 API 호출에 Authorization 헤더를 제공해줘야 한다. herader.yml 에는 다음과 같이 작성한다. CI/CD에서는 매 빌드마다 값을 갱신 시켜준다.

all:
  Authorization: Bearer TOKEN_PLACE_HOLDER

그 다음이 데이터 파일로 API에 비즈니스 특유의 값을 제공한다. 여기서는 간단히 고정된 값을 사용했지만 Apache Common의 라이브러리등도 호출이 가능해 보인다.

all:
    boardId: 29c3fd3c-0239-3cbb-ac02-3ef08e267d4f
    columnId: 52

이것으로 기본적인 준비는 모두 끝이다. 로컬에서 돌아가는 것을 확인하고 바로 Bitbucket Cloud 의 custom Pipe를 생성해 CATS jar를 포함하는 도커이미지를 생성한 뒤 API 리포지토리의 파이프라인에서 일주일에 한번씩 테스트가 수행되도록 설정해 놓았다. Github Action을 사용해서도 유사한 결과를 얻을 수 있으리라 생각한다.

테스트 결과

실제 내가 관리하고 있는 API는 다음과 같은 테스트들이 실패했다. 실패한 모든 테스트들을 수정하지는 않을 것 이다. 아마 REST API가 외부에 공개된 것이라면 대단히 유용할 것 같다.

권당되는 헤더를 찾을 수 없다는 에러, 자세한 내용은 아래 링크에 있고  사설 API이기 때문에 이는 무시해도 좋을 것 같다.   [{name=X-XSS-Protection, value=1; mode=block}]

X-XSS-Protection – Preventing Cross-Site Scripting Attacks – KeyCDN

중복된 헤더입력

중복된 헤더입력을 허용하고 내부적으로는 리스트로 변환되고 있음. HTTP 스펙에서는 허용되지만 보안상 권장되지 않음.

HTTP Desync Attacks in the Wild and How to Defend Against Them | Imperva

유니코드 제어 문자를 인식하지 못함

유니코드 제어문자는 시각적으로 표현되는 데이터가 아니기 때문에 입력에 포함되어 있을 경우에 일반 문자가 아니라 제어문자로 인식되어야 한다.

Unicode control characters

권장되는 REST API 네이밍 규칙을 따르지 않음

API경로에 복수형, 명사, 소문자를 사용해야 하며 엔드포인트에는 스네이크 케이스나 케밥 케이스를 사용해야한다. JSON 프로퍼티에는 스네이크 케이스와 카멜 케이스를 허용함.

새로운 JSON 필드 입력을 허용함

요청의 몸통에 새로운 JSON 필드 입력을 허용하고 있는데 OWASP 에서는 이경우에 요청을 거절하도록 권장한다.

REST Security – OWASP Cheat Sheet Series

존재하지 않는 HTTP 메서드에 대해서 405(Method Not Allowed)를 반환하지 않음

제목 그대로 CONTENT와 같이 존재하지 않는 HTTP 메서드에 대해 403 에러를 반환하고 있다.

지원하지 않는 Content Types 헤더를 허용함

Content-Type 헤더에 OpenAPI 계약아 정의되지 않은 값을 보냈을 때 요청을 허용함. OWASP 는 검증을 수행하도록 권장한다.

REST Security – OWASP Cheat Sheet Series

API 사양의 GET 경로에 권장되는 헤더인 TracedId/CorrelationId가 존재하지 않음.

마치며

실제 jar 파일을 다운받아서 로컬에서 실험해 보는데는 한시간도 걸리지 않았다. Open API 스펙을 가지고 있다면 반드시 돌려보면 좋을 것 같다. CATS에서 좀 아쉬운 점은 테스트 갯수가 엄청나게 많은데 비해 클라이언트 측 Rate limiting 이나 Throttling 기능이 있어야 할 것 같다. 많은 API 응답이 503 에러를 반환해서 의도치 않게 성능 테스트가 되어버릴 수도 있다.

오픈소스 CATS로 간단히 퍼즈(Fuzz) 테스트

Clean (Reactive) Code

Clean Code is an excellent book. One of the key takeaways from the book is that we code not only for the computer but also for the other developers we work alongside.
I believe that Clean Code is a subjective topic even though it’s shadowed by the popularity of the book. Many people would have their way of writing code in a more readable and maintainable way. They simply would have not published a book about the topic or they did but not gained as much attention as Uncle Bob’s one.

Ever since I started using WebFlux in 2018 from stock trading service, I’ve been quite content with it. As I started to review more and more code written in reactive functional style including both Reactor and RxJava, I realized that many people tend to write (reactive) code in their way which makes me spend more time to understand the logic behind the various types of looking.

Putting aside all pros/cons of reactive programming, discussion of which would take more time than the subject I’m handling here, reactive programming indeed lacks a common style guide to help speed up the cycle of development and review.

So, here I am listing some of the practices I follow while writing reactive code and find it easier to read.
The code below is written in Kotlin, but I believe it’s not much different from Java otherwise I mention it separately.

1. Reactive Operator is the key to Reactive programming

In reactive programming, an operator is a basic unit. Consider arranging operators from left-to-right then top-to-bottom so it is much easier to understand the whole flow of data when code is undergoing a review.

//Bad
userService.getFavorites(userId).map(Favorite:toRequestModel)
           .flatMap(favoriteService::getDetails) 
 
// Good
userService.getFavorites(userId) 
           .map(Favorite:toRequestModel)
           .flatMap(favoriteService::getDetails) 

2. Reduce the distance between Operators

Like rule number 1, in an effort to emphasize operators, Keep the lambda function short and simple inside the operator such as map or flatMap. If it is going to be longer, extract the it as a separate function.

//Bad
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               val userRequest = user.toUserRequest()
               GetFavoriteDetailRequest(
                   favorites = favorites,
                   user = userRequest
               )
            }
           .flatMap(favoriteService::getDetails) 
 
//Good
userService.getFavorites(userId)
           .map(this:toRequestModel)
           .flatMap(favoriteService::getDetails) 
 
fun toRequestModel(input: Tuple2<Favorites, Users>) {
     val (favorites, user) = input
     userRequest = user.toUserRequest()
     GetFavoriteDetailRequest(
        favorites = favorites,
        user = userRequest
     )
} 

3. Use Operators matching the name

The name of operators is as much important as how it operates. For instance, map should be used for mapping a value to another value, not for side effects. Similarly, flatMap should be used for mapping a value to Publisher type that is an abstraction of computation, inherently asynchronous.
The consistency between the name and the usage will reduce the cognitive load when reading code.

//Bad
userService.getFavorites(userId)
           .map { 
               log.info("Received favoirtes, $it")
               it.toRequestModel()
            }
           .flatMap(favoriteService::getDetails) 
 
//Good
userService.getFavorites(userId)
           .doOnNext { log.info("Received favoirtes, $it") }
           .map(this:toRequestModel)
           .flatMap(favoriteService::getDetails) 

4. Use Reactive type discreetly

Like I mentioned in rule number 3, Publisher, whose type is a supertype of Mono and Observable, is an abstraction of future computation. Check whether it’s accessible in runtime before declaring the property as a subtype of Publisher otherwise it brings more complexity into code without much gain.

//Bad
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               GetRequestModel(
                   Mono.just(favorites),
                   Mono.just(user)
               )
            }
           .flatMap(favoriteService::getDetails) 
 
data class GetRequestModel(
     val favorites: Mono<Favorites>,
     val user: Mono<User>
)
 
//Good
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               GetRequestModel(favorites, user)
           }
           .flatMap(favoriteService::getDetails) 
 
data class GetRequestModel(
     val favorites: Favorites,
     val user: User
)

5. Null equivalent in reactive is Empty

If you return null when the function is expected to return Publisher type, it will throw an exception because null equivalent in reactive is an empty Publisher. For example, it is allowed to return null in map operator but in flatMap.
In Kotlin, we would rarely come across such situation since it’s supporting non-nullable type, whereas, in Java, one need to take care more about null handling in reactive programming.

// Bad
Mono
   .just("test")
   .flatMap { testFunc(it) }
 
// Good
Mono
  .just("test")
  .flatMap { 
      testFunc(it) ?: Mono.empty()
   }
 
private fun testFunc(seed: String): Mono<String>? =
    if (seed  == "test") {
        null
    } else {
        Mono.just("Mono - test")
    }

6. Use a method reference

It is hard to avoid using nested functions also it’s not helping readability since variable scopes start to be affecting each other. Soon code base is likely to be covered with it, an implicit name of a lambada parameter in Kotlin, or with the series of a similar variable name in Java. Always prefer method reference that allows us to skip the boilerplate. If you could use SAM(Single Abstract Method) instead, It’s even better! We don’t even have to look at the method name. The type name will tell us spontaneously.

// Bad
userService.getUser(userId)
           .map { it.toFavoriteReq() }
           .flatMap { 
               favoriteService
                       .getFavorites(it)
                       .flatMap { 
                          favoriteService.getDetails(it.toDetailRequest) 
                       }
 
           }
 
// Good 
userService.getUser(userId)
           .map(User::toFavoriteReq)
           .flatMap { favoriteReq ->
               favoriteService
                       .getFavorites(favoriteReq)
                       .map(Favorite:toDetailRequest)
                       .flatMap(favoriteService::getDetail) 
                           
           }

7. Don’t let Collection API shadow Operators

The name of most reactive operators make a pair with that of Collection API. map, flatMap, reduce, filter, the list goes on. If these names appear in the same place, it will confuse the reviewer. Which is which? Separate the usage of API or use an unambiguous name from the other.

// val books: List<Book>
// Bad
Flux.merge(
  books.map { book ->
     if  (book.id == null) {
       Mono.just(card.copy(id = UUID.randomUUID()))
     } else {
       Mono.just(book)
     }
  }
)
 
// Good
Flux.merge(
  books.jdkMap { book ->
     if  (book.id == null) {
       Mono.just(card.copy(id = UUID.randomUUID()))
     } else {
       Mono.just(book)
     }
  }
)
 
private fun <T, R> Iterable<T>.jdkMap(transform: (T) -> R): List<R> = this.map(transform)

8. Make variable name distinguishable

It’s unnecessary to include Mono or Flux for the variable name unless the reader needs attention. When there are Publisher and non-publisher type in one place, or performing an operation between Flux and Mono, we better to include flux or mono in the variable name. Sometimes, the operation between two types would end up in an unexpected result. For example, the result of zipping Flux and Mono will emit only one item ignoring the rest part flux.

// Bad
val numberKor = Mono.just("hana")
val numberEng = Flux.just("one", "two")
numberEng
      .zipWith(numberKor)
      .doOnNext { zipped ->
         val (eng, kr) = zipped
         log.info("English:$eng, Korean:$kr")
       }
 
// Good
val numberKorMono = Mono.just("hana")
val numberEngFlux = Flux.just("one", "two")
 
numberEngFlux
     .zipWith(numberKorMono)
     .doOnNext { zipped ->
        val (eng, kr) = zipped
        log.info("English:$eng, Korean:$kr")
     }

9. Avoid calling subscribe() directly

Avoid calling subscribe() inside of reactive chain except in a clear case, such as spawning Zombie process. There is a danger that the process would be lingered over longer than we had expected because there is no way for us to control over the dangling process. Ideally, we should able to adjust the flux of data by handling Disposable type.

// Bad
userService
         .getUser(req)
         .flatMap(userService::changePassword)
         .doOnNext {
              auditLogger
                      .auditLog(it)
                      .subscribe()
         }

10. Key operator to the performance is flatMap

Speaking of performance improvement in reactive programming, the parallelization approach is usually suggested and is dealt with by flatMap(). Positioning operators that change data flow such as delayElement() around flatMap should be carefully considered as the flux of data might be changed after passing through flatMap. So if you seek to improve the performance in reactive programming, take a gander at the usage of flatMap.

Reference

https://projectreactor.io/docs/core/release/reference/

http://reactivex.io/documentation/operators.html

Clean (Reactive) Code

에드워드 스노든 그리고 샌드웜

연말에 시간 가는줄 모르고 읽었던 보안관련을 소개한다. 이 두책은 서로 얽혀 있는 부분이 있지만 스노든의 책은 정부의 민간인 감청에 대해서, 샌드웜은 정보기관 사이의 사이버전쟁에 대해서 다룬다. 결론은 IT분야 종사자라면 반드시 한번 읽어 보기를 추천한다. 안타까운건 두책 다 원서인데 스노든의 책은 아마도 번역이 될 것 같지만 샌드웜은 저자가 2014년에 출간한 책도 한국에 없는 걸 보면 번역 될 것 같지 않다.

영구기록(Permanent Record) – 에느워드 스노든

이 책은 스노든의 유년시절부터 시작해서 어떻게 미국 정부의 불법 적인 대중 감시를 폭로하게 되었는지 까지 과정을 설명한 회고록이다. 실제 스파이 업무나 사용한 기술에 대해 깊게 다루진 않아서 누구나 읽을 수 있다.

유년시절

스노든의 유년시절은 많은 IT 종사자들과 닮아있다. 게임을 좋아하고 그게 컴퓨터에 대한 관심으로 커졌으며 학교에서는 크게 인기 있는 유형은 아닌 전형적인 컴퓨터 너드였다. 당시에 해커로 활동하기보단 인터넷 커뮤니티에서 활발한 활동을 한 것 같다. 그리고 이 시절의 경험이 후에 스노든이 미국 정부의 불법에 큰 위기 의식을 느끼도록 만든다. 아마 비슷한 시기에 하이텔,나우누리 같은 PC통신을 해본 사람들은 크게 공감할 이야기 일듯. 현재와 비교해서 가격도 비싸고 접속하기 불편하며 서로 이야기를 나누는 것 외에는 크게 할 것없는 그런 장소였지만 커뮤니티는 지금보다 더 따뜻했으며 익명뒤에서 느끼는 안전함도 더 컸기에 스노든은 포함한 우리들은 그 시절의 인터넷에 낭만을 느낀다.

9/11

그런 스노든의 인생이 확 바뀌게 된 시점은 9/11이다. 그 사건이후 그린베레에 지원했지만 훈련 도중 부상을 입고 전역하게 된다. 그 뒤에 NSA 시설의 경비원으로 1년정도 일한다. 어떻게 보면 스노든은 소위 말하는 고스펙을 가진 인물은 아니다. 부모님이 모두 정부기관에서 일했고 군에 자원한 경험도 있기 때문에 높은 보안 등급을 받을 수 있었고 9/11 이후의 정보기관의 확장 시기와 맞물려서 CIA에서 일하게 된다. CIA라고 해도 막상 컴퓨터에 정통한 사람은 얼마 없었기에 스노든은 곧 두각을 나타낸다.

내부고발까지

그 뒤에 동경으로 옮겨 NSA에서 IT 시스템을 구축하는 업무를 맏게 되는데 거기서 부터 미국 정부의 불법을 알게 된다. 정보기관내의 파편화가 굉장히 심해서 실제 폭로의 대상인 감시 도구 프리즘을개발한 것은 아니었지만 운영을 잠시 도와주며 충격적인 사실을 발견한다. NSA는 우리가 상상하는 지휘통제실 같은 곳에서 구글에 검색하듯이 원하는 사람을 입력하기만 하면 그사람의 메일, 문자등을 전부 보고 있던 것이다. 실제 요원들 중에는 개인 용도로 자기 부인이나 여자친구를 대상으로 사용하는 사람들도 많았고 무작위로 사람들이 주고받는 사진들을 돌려보기도 했다니, 스노든의 폭로후에 제발 변화가 있었길 바란다..참고로 이 시스템은 아직도 운영중이다. 다만 많은 사이트가 HTTPS로 넘어가는 등 서비스 제공자들도 보안을 강화했기 때문에 아직도 그 수준으로 감시가 가능한지는 의문이다.

그렇게 언론에 제보할 결심을 한뒤에는 시스템 내부에서 문서를 크롤링하는 Heartbeat를 개발한뒤 기자에 접근하고 결국 홍콩에서 기사를 작성하기 까지의 상황을 설명한다. 당연하겠지만 정말 많은 고민을 한 것 같으며 미국 정부와 의회가 서로 견제하는데 실패 했기 때문에 이런 방법을 택하게 됐다고 한다. 스노든이 그 고민 속에서 다음과 같은 비유를 했는데 마음에 와 닿았다.

Ultimately, saying that you don’t care about privacy because you have nothing to hide is no different from saying you don’t care about freedom of speech because you have nothing to say. Or that you don’t care about freedom of the press because you don’t like to read. Or that you don’t care about freedom of religion because you don’t believe in God. Or that you don’t care about the freedom to peaceably assemble because you’re a lazy, antisocial agoraphobe.

대충 번역하면 자기는 당당하기 때문에 프라이버시에 별로 신경쓰지 않는다고 말하는 것은 읽는 것을 좋아하지 않기 때문에 출판의 자유도 신경쓰지 않으며 신을 믿지 않기 때문에 종교의 자유도 신경쓰지 않는다고 말하는 것과 별반 다르지 않다는 내용.

스노든의 미래는?

스노든은 대의를 위해 저질러버렸지만 정말 힘든 결정이었음이 책의 후반부에 그대로 드러난다. 스노든은 수사가 시작 됐을때 피해가 갈 수 있기 때문에 다른 가족들과 동거중인 여자친구에게도 단 한마디도 하지 않았다. 하지만 실제 스노든이 홍콩에서 뉴스에 등장했을 때 그들이 겪었을 고충은 상상하기가 힘들다.. 다행히도 여자친구는 스노든이 망명중인 러시아로 건너가 그와 결혼했으며 미국내에서는 스 노든을 사면하기 위한 목소리도 존재한다. 하지만 비슷한 사례를 참고해봤을 때 스노든은 엄청난 중형을 받게될 가능성이 높고 공화당은 물론 민주당에서도 사면을 허용해서는 안된다는 의견도 많다.. 그리고 최근 미국 법원은 책의 인세와 강연료등으로 번 50억 달러를 몰수하는 결정을 내렸다..(기사)

이사건을 계기로 미국 뿐만 아니라 전세계의 많은 사람들이 각자 정부기관의 잘못된 행태에 관심을 가지도록 만들었다. 나도 책을 읽으면서 정부의 잘못된 관행에 견제의 시그널을 – 원래라면 국회가 해야할 – 보내준 스노든에게 크게 감사함을 느꼈다.

관련기사

스노든도 사용한 메신저 ‘시그널’ 보안성 최강

에드워드 스노든 “K방역 정보수집, 효과 확실치 않다”

샌드웜(Sandworm) – 앤디 그린버그

Sandworm

샌드웜은 러시아 GRU소속의 해킹 전담 부대의 코드명이다. 이 책은 최근에 벌어진 일련의 해킹 사건들의 배후를 다루는 논픽션으로 책에서 언급된 사건을 정리한 아래 목록은 기승전러 대부분 러시아가 그 배후에 있다. 많은 사람들이 아래 사건들중 한두가지는 단신 뉴스로 봤을 것이다.

저자는 놀라운 취재력으로 일련의 사건들 사이의 단서를 차근차근 맞춰나간다. 미국과 우크라이나를 포함한 세계 각국의 멀웨어 분석가, 보안전문가와의 심도깊은 취재를 통해 실제 멀웨어의 분석 방법은 물론이고 어떤 원리로 해킹이 작동하는지도 설명하고 있다. 특히나 초반의 흡입력이 대단한데, 실제 사건을 다룸에도 왠만한 소설보다 더 긴장감이 느껴진다. 사이버 워에 관해서 인터넷 기사나 위키피디아의 글로서는 파악하기가 힘든 전체 맥락을 한번에 다 이해할 수 있게된다.

미미카츠

책에서 언급된 대부분의 제로데이 취약점은 윈도우 기반이다. 프랑스 정부기관의 IT매니져로 일하던 델피는 메모리에 사용자의 크레덴셜을 저장하는 윈도우 WDigest 기능의 취약점을 발견하고 이를 MS에 즉각 보고하지만 MS는 해당 취약점은 컴퓨터가 이미 탈취당한 상태에서만 문제가 되기 때문에 당장 큰 문제가 아니다 라는 식으로 대응을한다. 델피는 경각심을 일깨우기위해 POC개념으로 Mimikatz을 깃헙에 공개한다. 그렇게 MS의 무책임한 대응으로 네덜란드의 인증서 발급 업체였던 DigiNotar는 인증서를 부정 발급하게 되고 파산에 이르게 된다.

피해액만 해도 조단위가 넘어가는 페트야는 NSA가 나중에 써먹기 위해 꽁꽁 숨겨뒀던 이터널블루 취약점을 사용해 공격을 사용한다. 이렇게 의도치않게 정보기관끼리 취약점들을 공유해서 더욱 더 발전된 멀웨어가 나오게 한다.

러시아의 사이버 워

러시아는 왜이렇게 해킹을 하는 걸까? 냉전 이후에 러시아의 세계 경쟁력은 내리막길을 걸었고 남은건 막대한 군사력뿐 그마저도 미국 국방비의 1/10에 그치기 때문에 예전같은 영향력을 되찾고 싶어하는 시도라는 분석이다. 2014년에 일어난 러시아 우크라이나 전쟁에서 러시아는 승리했지만 여러가지 판단 착오로 인해 첩보 기관인 GRU는 신뢰도에 큰 타격을 입는다. 이를 복구하기 위해 적은 비용으로 큰 혼란을 일으킬 수 있는 사이버 전쟁에 적극적으로 뛰어들고 있다는 분석이다.

작가는 2015년에 일어난 우크라이나 정전사건때 미국이 조금 더 강력한 메시지를 보냈어야 한다고 말한다. 오바마 정부는 미국내에서 일어난 일이 아니라서 이를 단순히 무시했고 러시아는 우크라이나를 미래의 공격을 위한 놀이터로 적극 활용한다. 책에서는 다루지 못했지만 2020년 다시 한번 러시아발 해킹사건이 발생한다. 그리고 이게 마지막도 아닐 것이다.

배후(attribution)문제

러시아 보안업체인 카스퍼스키의 보안 전문가는 저자와 인터뷰중에 평창 올림픽 해킹은 북한 소행이 아니며 누군가 일부러 그렇게 보이게 설계했다고 말한다. 저자는 그래서 북한이아니라면 누구 소행이라고 생각하냐? 라고 답정남 질문을 하지만 해당 전문가는 가방에서 attribution dice(배후? 주사위)를 꺼내서 보여주는데 이부분은 조금 웃겼다:) 이는 그만큼 해킹의 책임자를 가려내기가 힘들다는 사실을 뜻한다.

배후를 밝혀내가 힘든 사이버워의 특성상 앞으로의 세상은 예전 전쟁처럼 개전과 종전으로 명확하게 구분되지 않고 항시 존재할 것이라는 분석이다.

복원력

그리고 저자는 사이버워에 대한 대응으로 댄 지어가 주장하는 복원력(Resilence)를 언급하며 책을 마무리한다. 댄지어는 미국 정보기관의 벤처캐피탈 역할을 하는 비영리기관인 In-Q-Tel 의 최고보안 책임자이다. 많은 보안 전문가들이 해킹 공격에 대해서 더 많은 보안 패치, 머신러닝에 의한 모니터링등 강력한 조치를 이야기 하지만 그는 방지보다는 피해를 최소화하고 복구시간을 줄이는데 더욱 집중해야 한다고 한다. 그 핵심 기념이 복원력으로 독립성을 의미한다. 각 구성요소들이 네트워크에 의존하지 않고 아날로그로도 독집적으로 수행될 수 있어야 한다는 의미이다. 현대 사회는 아주 복잡한 시스템들이 단계적으로 엮여 있어서 그 기반이 무너지면 전체가 다 무너기 쉽다. 그래서 이메일 시스템이 멈추면 우편시스템이 작동해야 하고 이동전화가 작동하지 않으면 유선전화를 사용할 수 있는 사회가 되어야 한다. 그 예로 우크라이나에서 해킹으로 대규모 정전 일어났어도 생각보다 빠르게 복구가 가능했다고 말한다. 우크라이나의 기반 시설들은 선진국보다 평소에도 더 자주 멈추고 많은 부분들이 사람손으로 아직 움직이고 있었기에 해킹하기는 쉬웠지만 피해는 적었다. 하지만 선진국의 시설들은 해킹하기는 더욱 어렵지만 해킹 당해서 멈추게 되면 그 피해는 훨씬 비극적일 것이다.

에드워드 스노든 그리고 샌드웜

더 라스트 오브 어스 파트2 리뷰

이 리뷰는 강력한 스포일러를 담고 있습니다. 

“영화 같은 게임”으로 유명한 더 라스트 오브 어스 파트2(이하 라오어2)가 올해 6월에 출시되었다.  나중에 플레이하려고 맘먹었기 때문에 출시 직후 최대한 리뷰는 읽지 않으려고 했지만 너무나 실망이라는 소식과 게임 관련 웹진의 후한 리뷰에 속았다는  사람들의 반응만 전해 들었다.  미루다가 아마존에서 29달러 세일을 하기에 빠르게 구매해서 플레이해 보았는데, 웬걸. 개인적으로는 정말 집중해서 재미있게 플레이 했고 각종 웹진에서 출시 직후 했던 10년에 한 번 나오는 타이틀이라는 평가에도 공감한다.

사람들이 분노하는 이유, 이야기 때문

인터넷에서 본 박한 평가들은 주로 전작의 팬들을 고려하지 않은 개연성 없는 전개, 공감할 수 없는 메시지 등 주로 캐릭터와 스토리에 관한 평가가 주를 이룬다. 라오어1 에서 플레이어들이 그토록 감정이입을 했던 조엘이  PC설정으로 추가된 새로운 캐릭터에게 골프공 취급을 당하고 친딸보다 더 정성스럽게 지켜낸 엘리가 레즈비언이라는 설정, 그리고 플레이어에게 그녀를 쥐 잡듯이 두드려 패도록 하는 진행까지..정확히 그 시점부터 엔딩까지 이어지는 이야기는 나도 모르게 얼굴이 찌푸리면서 “아 나는 이렇게 하기 싫어”라고 외치게 만든다. 이 게임은 플레이어들이 주말 드라마의 상황에 완전히 몰입한 주부처럼 만들어 버렸다.

스토리 위주의 게임 만들기 쉽지 않다

덧붙여서 요즘 같은 시기에 이야기 위주의 게임을 만드는 일은 절대 쉬운 일이 아니다. 근 10년간 출시된 게임을 보면 스토리는 대부분 부수적인 역할을 한다. 8, 90년 대에는 이야기 중심의 게임이 나왔지만, 오히려 컴퓨터 그래픽 기술이 월등하게 발전한 최근에는 스토리 위주의 게임보다는 화면의 화려함이나 참신한 플레이 등이 주로 AA타이틀로 등장한다. 화려한 그래픽과 스토리를 다 잡으려면 헤비레인이나 디트로이트 비컴 휴먼같은 장르 말고는 선택지가 없었다.

그렇게 어려웠던 것을 라오어는 해냈다. 플레이어 앞에 펼쳐지는 스토리에 대한 선호는 사람마다 다를 수 있지만, 이야기에 몰입하게 만든 그 기술과 실행 능력에 100점을 주고 싶다. 이 게임의 메뉴부터 게임 모드까지 거의 모든 요소가 스토리에 집중하도록 의도적 의로 배치되어있다. 최소한의 UI, 게임 내 오브젝트와 캐릭터 간의 상호작용, 실제에 가까운 음향 효과까지. 아마 플레이어는 게임 내에서 존재하는지도 몰랐을 사소한 섬세함이 모이고 모여서 그 몰입감을 만들어 낸다. 최근에 갓 오브 워도 비슷한 시도를 했고 결과는 매우 성공적이었지만 라오어는 그것을 뛰어넘는다.

내가 개발자라면 만든 맵이 아까워서 온라인 모드나 여러 가지 더 오락적인 내용들을 추가 했을 것이다. 하지만 너티독은 플레이어가 감독이 정해놓은 스토리만을 따라가도록 모든 걸 다 버리고 디테일에 집중했다. 이러한 게임의 작가주의 성격 때문에 평가가 더욱더 양극화된 것 같다.

라오어2 디테일에 관한 유튜브 비디오 1
라오어2 디테일에 관한 유튜브 비디오 2

개인적으로 만족했던 이야기

많은 라오어1 팬들의 치를 떨게 만들었던 스토리도 사람별로 여러 가지 해석이 가능하다는 점에서 나는 맘에 들었다. 1의 이야기도 좋았지만 여태까지 접해봤던 이야기 형태라면 라오어2의 이야기과 주제는 참신하게 다가왔다.

많은 사람이 가족 같은 조엘과 엘리를 갑자기 튀어나온 애비와 레브가 파괴한 것에 분노하고 (특히나 플레이어의 손에 직접) 복수는 복수를 부를 뿐이라는 메시지에 공감하지 못하겠다고 하지만 나는 세상에는 수많은 조엘이나 엘리가 존재할 수 있고 철저한 악인도 누구에게는 생명의 은인이 될 수 있다는 메시지를 받았다. 그리고 마지막에 엘리가 그 고리를 끊어 내려는 모습에 큰 안도와 감동을 했다.

PC설정의 순기능

엘리를 레즈비언으로 설정한 것도 단순히 PC설정이라기 보다는, 조엘이 소중한 사람의 생명이 인류의 미래보다 중요하다고 판단했던 것처럼 유사하게 엘리도 인류의 미래 보다 자신의 인생을 살아가기로 한 것 아닐까? 공공의 이익을 보면 엘리는 이성과 연애해서 자신의 면역력을 널리 퍼트리거나 적어도 보존해야만 한다.  이런 자유주의적 메시지를 위해서 엘리를 레즈비언으로 설정한 것은 아닐까 생각해본다.

그리고 설령 PC 설정이라고 해도 단순히 성 대결로 몰고 갔다기보다는 다양한 인종을 어우르는 시도로 보이며 이는 게임 전체 설계에도 반영되어 있다. 이전까지는 많은 장애인이 게임을 플레이하는데 많은 장애물이 존재했지만 라오어2는 이러한 장벽을 없애주는 게임 접근성 부분에서 역대 최고라는 평가를 받고 있다. 비쥬얼 모드나 조작에 관한 세세한 설정까지 왜 존재하는지 알 수 없었던 많은 부분이 의도된 것임을 알고 다시 한번 놀랐다. 아래 유튜브 링크는 시각 장애인 게이머가 라어오2를 극찬하는 영상이다. 이걸 보고도 라오어2에서 PC 설정을 택한 것을 가지고 비난하긴 힘들 것이다.

끝으로

여려 면에서 많은 생각을 하게 하고 영감을 주는 게임이었다. 닐 드럭만이 트위터에서 그를 비난하는 사람들을 향해 공격적인 대응으로 욕을 먹고 있지만 라어오2로 인해 이제 그 누구도 닐 드럭만이 게임 업계에서 최고의 Bigshot이 되었음을 부정할 수는 없을 것이다.

더 라스트 오브 어스 파트2 리뷰

클린 (리액티브) 코드

클린코드(Clean Code)는 소프트웨어 엔지니어링 분야에서 굉장히 의미가 깊은 책이다. 클린코드로 많은 개발자들이 자기가 작성한 코드의 정량적인(Quantitative) 부분 뿐만 아니라 정성적인(Qualitative) 부분도 신경쓰게 되었다. 책을 관통하는 주제는 일관되다. 사람이 읽기 쉬운 코드가 유지보수가 더 쉽기 때문에 높은 품질을 가진다는 것이다.

나는 3~4년에 걸쳐 스프링 WebFlux를 사용해 증권앱의 서버 그리고 GraphQL의 게이트웨이 프로젝트를 진행했다. WebFlux를 사용한 결과에 충분히 만족했고 Graphql 게이트웨이는 안정화 단계에 접어들어서 최근에는 여러번의 코드리뷰를 진행했다. 다양한 사람들의 코드를 리뷰하면서 많은 사람들이 자신들만의 방식으로 코드를 작성하고 있음을 깨달았다. 공식 매뉴얼을 읽고 작동하는 코드를 만들긴 했지만 어떻게 해야 다른 사람이 더 쉽게 이해할 수 있을지에 대한 배려는 부족했다.

이는 아직 리액티브 프로그래밍이 본격적으로 사용 된지 얼마 지나지 않았기 때문에 클린코드나 이펙티브 자바와 같이 많은 엔지니어들이 이정표로 삼을 수 있는 자료들이 부족한 것이 주된 이유라고 생각한다. 엔지니어들 사이에 합의된 관습등이 부족해서 리액티브 코드는 작성자 별로 중구난방이 되기 쉬우며 코드 리뷰시에도 더 많은 시간을 필요로 한다.

여기서는 내가 개인적으로 리액티브 코드 리뷰시에 자주 언급하는 항목들을 정리해 본다. 여기서 말하는 내용들이 반드시 옳다는 이야기는 아니며 프로젝트 상황마다 다르겠지만 일반적으로 리뷰어의 입장에서 읽기 편안함에 중점을 두고 나열해본다.

모든 예제는 코틀린코드로 작성되었다.

1. 연산자를 중심으로 코드를 작성

연산자(Operator)는 리액티브 프로그래밍에서 사용 가능한 연산의 기본 단위이다. 그래서 코드를 작성할 때는 연산자를 중심으로 왼쪽에서 오른쪽, 위에서 아래 방향으로 마치 책을 읽는 것처럼 작성하는 것이 다른 사람들이 제일 이해하기 편하다.

//Bad
userService.getFavorites(userId).map(Favorite:toRequestModel)
           .flatMap(favoriteService::getDetails) 

// Good
userService.getFavorites(userId) 
           .map(Favorite:toRequestModel)
           .flatMap(favoriteService::getDetails) 

2. map, flatMap의 함수는 최대한 간결하게

1번 에서 언급한 것처럼 모든 코드는 연산자 중심으로 읽혀져야 한다. 많은 사람들이 제일 자주 사용하는 map과 flatMap의 함수 내부에 장황한 비즈니스 로직을 작성하곤 한다. 이것은 코드의 흐름을 끊기게 해 가독성에 좋지 않다. 중요한 큰 흐름은 연산자의 체인과 그 인자만으로 읽어낼 수 있어야 하기 때문에 장황한 로직은 외부 함수로 빼내도록 하자.

//Bad
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               val userRequest = user.toUserRequest()
               GetFavoriteDetailRequest(
                   favorites = favorites,
                   user = userRequest
               )
            }
           .flatMap(favoriteService::getDetails) 

//Good
userService.getFavorites(userId)
           .map(this:toRequestModel)
           .flatMap(favoriteService::getDetails) 

fun toRequestModel(input: Tuple2<Favorites, Users>) {
     val (favorites, user) = input
     userRequest = user.toUserRequest()
     GetFavoriteDetailRequest(
        favorites = favorites,
        user = userRequest
     )
} 

3. 연산자는 목적과 그 이름에 걸맞게 사용

리액티브 연산자의 이름은 실제 작동 방식만큼이나 중요하다. 예를들어 map은 값을 다른 값으로 매핑할때, flatMap은 값을 Publisher 타입으로 매핑할 때 사용한다. 여기서 Publihser타입은 값의 계산 자체를 추상화한 타입이다. doOnNext 연산자는 전체 연산의 결과에 영향을 미치지 못하는 부수 효과(Side effect)를 주고 싶을 때 사용한다. 이름에 맞는 연산자를 사용하면 코드리뷰시에 인지부하를 줄여 준다.

//Bad
userService.getFavorites(userId)
           .map { 
               log.info("Received favoirtes, $it")
               it.toRequestModel()
            }
           .flatMap(favoriteService::getDetails) 

//Good
userService.getFavorites(userId)
           .doOnNext { log.info("Received favoirtes, $it") }
           .map(this:toRequestModel)

           .flatMap(favoriteService::getDetails) 

4. Mono, Flux 사용이 필요한지 확인

3번에서 언급했듯이 Mon, Flux의 상위 타입인 Publisher 타입은 “계산” 자체를 추상화 한다. 이 타입은 리액티브의 프로그래밍의 장점인 비동기 연산을 제어하는데 사용되어야 하기 때문에 중구난방으로 사용되어서는 안된다. 되도록 데이터 클래스의 멤버는 모두 Non-Publihser타입으로 선언한다.

//Bad
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               GetRequestModel(
                   Mono.just(favorites),
                   Mono.just(user)
               )
            }
           .flatMap(favoriteService::getDetails) 

data class GetRequestModel(
     val favorites: Mono<Favorites>,
     val user: Mono<User>
)

//Good
userService.getFavorites(userId)
           .map { 
               val (favorites, user) = it
               GetRequestModel(favorites, user)
           }
           .flatMap(favoriteService::getDetails) 

data class GetRequestModel(
     val favorites: Favorites,
     val user: User
)

5. Publihser의 Null 타입은 Mono.empty

다시 한번 이야기 하지만 Publisher 타입은 미래의 계산에 대한 추상화이다. 그렇기 때문에 Publihser타입을 반환해야 하는 장소에서 Null을 반환하면 에러가 발생한다. 코틀린에서는 non-nullible 타입을 제공해서 한결 처리하기 수월하지만 자바는 Mono를 반환하는 함수가 Null을 반환하는지 확인하고 Mono.empty 타입을 반환하도록 한다. 반대로 값에서 값으로의 맵핑을 수행하는 map은 null을 반환해도 에러가 발생하지 않고 Mono.empty 타입과 동일하게 처리된다.

// Bad
Mono
   .just("test")
   .flatMap { testFunc(it) }

// Good
Mono
  .just("test")
  .flatMap { 
      testFunc(it) ?: Mono.empty()
   }

private fun testFunc(seed: String): Mono<String>? =
    if (seed  == "test") {
        null
    } else {
        Mono.just("Mono - test")
    }

6. Mono나 Flux가 중첩되는 경우에는 메소드 참조를 활용

1번에서 언급한 것처럼 연산자를 중심으로 메소드 체인 형식으로 작성하는 것이 최선이나 어쩔 수 없이 연산자들을 중첩해서 사용할 때가 있다. 그럴 때는 반드시 메서드 참조를 사용할 수 있도록 한다. 특히 람다에서 묵시적 타입 “it” 을 허용하는 코틀린에서 모든 람다의 매개 변수로 it을 사용하면 변수 스코프가 가려져서 (Variable shadowing) 가독성에 심각한 문제가 발생한다. 자바에서도 메서드 참조가 유리한 이유는 일반적으로 개발자가 한 클래스 내에서 여러 번 변수명을 지정해야 할 경우 특별한 의미 없이 비슷한 이름을 연속해서 사용하기 쉽기 때문이다.

// Bad
userService.getUser(userId)
           .map { it.toFavoriteReq() }
           .flatMap { 
               favoriteService
                       .getFavorites(it)
                       .flatMap { 
                          favoriteService.getDetails(it.toDetailRequest) 
                       }

           }

// Good 
userService.getUser(userId)
           .map(User::toFavoriteReq)
           .flatMap { favoriteReq ->
               favoriteService
                       .getFavorites(favoriteReq)
                       .map(Favorite:toDetailRequest)
                       .flatMap(favoriteService::getDetail) 
                          
           }

7. Collection API와 겹치지 않게 사용

Mono 타입의 가장 대표적인 연산자인 map과 flatMap, filter 등은 자바, 코틀린의 컬렉션 API뿐만 아니라 여러 곳에서 사용된다. 동일한 범위에서 사용되면 소스 타입이 Publisher인지 Iterable인지 바로 알기가 힘들다. 두 가지가 서로 겹치게 되면 분리할 수 있는 방법을 찾자.

// val books: List<Book>
// Bad
Flux.merge(
  books.map { book ->
     if  (book.id == null) {
       Mono.just(card.copy(id = UUID.randomUUID()))
     } else {
       Mono.just(book)
     }
  }
)

//Good
Flux.merge(
  books.collectionMap { book ->
     if  (book.id == null) {
       Mono.just(card.copy(id = UUID.randomUUID()))
     } else {
       Mono.just(book)
     }
  }
)

private fun <T, R> Iterable<T>.collectionMap(transform: (T) -> R): List<R> = this.map(transform)

8. Mon, Flux의 구분이 필요한 곳은 변수명에 명시하기

일반적으로 모든 Publisher타입에 mono나 flux를 변수 명에 넣어줄 필요는 없다. 하지만 해당 변수가 Publisher임을 명시적으로 하고 싶거나 Flux와 Mono 사이에 연산을 수행할 때는 변수명에 flux나  mono를 포함 시키도록 한다. 특히 flux 와 mono 간의 연산이 필요할 때는 변수명을 mono나 flux를 붙여서 서로 다른 타입과 연산을 수행함을 알 수 있게 한다. 다음 예제에서는 Flux 와 Mono를 zip하는데 그 결과는 Mono타입이 된다.

// Bad
val numberKor = Mono.just("hana")
val numberEng = Flux.just("one", "two")
numberEng
      .zipWith(numberKor)
      .doOnNext { zipped ->
         val (eng, kr) = zipped
         log.info("English:$eng, Korean:$kr")
       }

// Good
val numberKorMono = Mono.just("hana")
val numberEngFlux = Flux.just("one", "two")

numberEngFlux
     .zipWith(numberKorMono)
     .doOnNext { zipped ->
        val (eng, kr) = zipped
        log.info("English:$eng, Korean:$kr")
     }

9. 명시적인 subscribe() 호출은 신중하게

리액티브 체인 내부에서 subscibe를 호출하는 것은 되도록 피하자. 이는 엔지니어가 인식하지 못하는 사이 장기간 돌아가는 스레드를 만들 가능성이 있다. 리액티브 프로그래밍은 subscribe가 반환하는 Disposable 타입을 사용해 처리량을 조절할 수 있어야 한다.

// Bad
userService
         .getUser(req)
         .flatMap(userService::changePassword)
         .doOnNext {
              auditLogger
                      .auditLog(it)
                      .subscribe()
         }

10. 가장 중요한 연산자는 flatMap

리액티브에서 이야기 하는 성능의 향상, 즉 높은 동시성은 거의 대부분은 flatMap을 중심으로 이루어진다. 그래서 데이터 흐름을 조절하는 delayElement등을 flatMap 주변에서 사용할 때는 유의하고 자매 연산자인 flatMapSequential, concatMap 등과의 차이점을 확실히 파악하도록 하자.

클린 (리액티브) 코드

채식을 시작한 이유

2020년 초부터 식단을 채식으로 바꿨는데 다른 사람들에게 이제 채식해 라고 말했을 때 반드시 갑자기?” 라는 질문을 여러번 받았기에 이후에 조금 더 설득력있게 대답하기 위해 글로 남겨본다.

계기는 비욘드미트

비욘드미트는 2009년에 Ethan Brown이 창업한 대체육 회사이다. 상대적으로 흉내내기 쉬운 햄버거 패티, 소시지, 연육등을 대형마트와 자사 온라인 쇼핑몰을 통해 판매하고 있다. 물론 현재는 실제 고기보다 가격도 비싸며 맛도 떨어진다. 그럼에도 불구하고 2020년 초에 처음으로 맛본 비욘드 미트는 잊고있었던 채식에 대한 의지를 다시한번 일깨워줬다. 호주의 유명 햄버거 체인에서 판매하는 비욘드 미트를 사용한 햄버거는 실제 햄버거와 80% 정도 유사하고 가격도 일반 햄버거와 동일했다. 그리고 집에서 비욘드 미트를 조리했을 때 사료(?) 비슷한 향기가 나서 조금 맘에 걸렸지만 일반 식당에서 이렇게 대체육 옵션을 제공한다면 주저없이 선택할 수 있을 것 같았다. 바로 그때 이번에는 정말 채식이 가능하겠다 라는 느낌이 왔다.

갑자기 왜 채식을?

무엇을 먹고 살 것인가 라는 의문은 존재론적인 고민들과 굉장히 유사하다. 많은 사람들이 크게 괘념치 않고 살아가지만 한번 의문을 품으면 그야말로 스스로 질문과 답을 반복하게 상태에 빠지고만다. 처음 채식을 해야겠다고 결정했을 때 여러가지 이성적이고 감성적인 요인들이 포함되어 있었다. 그중에서도 시작은 특히 스스로 동물을 좋아한다고 말하면서 삼시세끼 소,돼지,닭을 먹는것에 대한 자기배반적인 감정이었다.

영양 넘치는 채식

시작은 그러했지만 나도 처음에는 채식으로 성인 남성에게 적절한 영양소를 공급할 수 있을까? 하는 의문을 가졌다. 웹으로 여러가지 조사를 진행하고 몇달간 해본결과 그런 고민은 기우였음이 밝혀졌다. 이상한 출처를 알 수 없는 자료들 보다 영국의 국민건강보험에 해당하는 NHS에서 내놓은 자료들이 훨씬 신뢰가 갔다. 물론 영국과 한국간의 인종적인 차이는 있겠지만 한국에서 이뤄지는 다양한 연 채식을 건강식으로 받아들이기 시작한 것 같다. 기사 “금연·예방접종·채식·운동하면 암 70% 예방”

고기를 먹었을 때보다 섭취하는 식품의 영양에 신경 쓰게 된 점도 아주 긍정적인 변화이다. 그러면서 한국의 식습관이 채식에 유리한점도 발견할 수 있었다. 그 예로 비타민 B12는 채식을 하게 되면 결핍을 주의해야하는 영양소로 자주 언급되는데 한국인들이 자주먹는 김에 바로 그 B12가 풍부하게 포함되어 있다는 것이다.

아직 1년이 되지 않았고 건강검진을 받지 않은 상태라 채식이 어떻게 내 몸에 영향을 끼쳤는지 수치화 할 수는 없지만 느끼기엔 아주 건강해진 것 같다. 채식 이전에도 특별히 어디가 아프거나 한것은 아니었지만 눈에 띄는 변화중 하나는 채식후에 배가 아팠던 적이 한번도 없었다는 것이다. 예전에 고기를 먹고 특히 밤에 소화가 잘 안되거나 아랫배가 아파서 밤에 자다가 깨는 경우도 있었는데 정말 화장실에서 기절 직전까지 간 경우도 있다. 채식을 하고는 몸안이 굉장히 깨끗해진 느낌인데 몸 뿐만 아니라 냉장고와 쓰레기통까지 깔끔해진 것은 덤이다.

기후변화 대응

개인적인 범위의 변화뿐만 아니라 지구를 위해서도 채식은 권장할 만하다. 내가 살아오면서 느낀 환경운동이 가지는 문제점중 하나는 개인이 무엇을 해야할지 뚜렷하지 않은 경우가 많고 정책의 변화나 의식의 전환만을 요구하는 것에서 그친다는 것이다.

반면 채식은 바로 실천 가능하다. 전세계 인구가 기후변화의 주체임을 깨달을 수 있다는 것이다. 채식으로 전환하거나 섭취하는 육류를 조금이라도 줄이는 것만으로도 기후변화를 막는데 힘을 보탤 수 있다. 가끔 축산이 온실효과에 끼치는 영향이 확실히 않다고 채식의 기후변화 대응효과를 축소하려는 사람들을 자주 보았다. 농축산이 온실효과에 끼치는 영향은 15%에서 50% 까지 다양한 연구 결과가 보고 되고 있는데 이산화탄소, 메탄등 각종 요인들이 서로 영향을 끼치고 있기 때문에 앞으로도 정확한 숫자를 알아 내는 것은 힘들 것이다. 설령 농축산이 온실효과에 10%만 기여 한다고 해서 당장 전기차를 구매하는 것이 채식보다 더 기후변화에 앞장서는 대응일 수는 없다. 모든 부분에서 걸쳐서 노력을 해야하지만 화석연료 사용을 줄이는 부분은 각 나라의 정책 방향에 크게 의존하는 것이 사실이다. 예를들어 전기차가 사용하는 전기가 석탄으로 생성된 것이라면 결국 제자리이다. 결국 채식은 개인이 오늘당장 실천할 수 있는 가장 손쉬운 기부변화 대응 행동이다. 기사 – 기후위기 시대, 채식이 지구를 살린다

채식, 쉽지는 않다

나는 동물들을 사랑하지만 다른 사람들도 다 그런것은 아니며 꼬리에 꼬리를 무는 질문을 해서 그럼 식물들은 먹어도 괜찮은 것인가? 라고 듣게 될 수 있다. 결국 뭔가를 먹는다는 행위가 유기물을 분해해서 자신의 영양소로 만드는 과정이 아니던가. 채식주의자들은 필요한 영양소들은 이미 식물에 인간이 섭취할 수 있는 상태로 존재하기 때문에 그것을 섭취한 동물들을 다시 도축해 먹는 과정은 불필요하다고 이야기 하지만 솔직히 나를 포함한 많은 채식주의자들의 채식을 결심하게 된 계기는 동물에 대한 애착에서 시작된 경우가 많다. 앞서 언급했듯이 채식을 시작한 요인은 동물에 대한 애착, 감성적인 요인이다. 반대로 육식을 끊을 수 없었던 만드는 감성적이고 문화적 요인도 존재한다.

회식은 삼겹살아니면 치킨..쩝쩝

한국회사에서 일 마치고 먹었던 삽겹살과 치킨. 과연 다음에 똑같은 기회가 생겨도 저항할 수 있을까?

일단 나는 현재 채식을 하기 굉장히 좋은 조건임에는 분명하다. 우리 회사는 COVID와 상관없이 앞으로도 재택근무가 기본이 될것임을 발표했다. 그외에 나라별 차이는 장볼때나 식당에서 많이 드러난다. 호주는 한국보다 채식인구의 비중이 높으며 개인의 식단에 크게 신경쓰지 않는 문화다. 그리고 재배 가능한 땅이 넓다 보니 마트에 지역산 채소나 과일들이 많으며 (물론 고기도 많다) 아직은 시범적이긴 하지만 다양한 대체육들도 눈에 띄인다. 지금은 운영하지 않는 회사 카페테리아도 샐러드바 형태여서 개인의 식단에 맞춰 점심식사가 가능했다.

반면 한국에서는 학교, 회사, 군대등 다양한 장소에서 급식을 하는데 채식을 유지하는 것은 개인에게 큰 시련이 될 수도 있다. 다행히도 한국도 집단 급식을 하는 학교나 군대에서 채식을 선택할 수 있도록 바뀌고 있는데 이는 점점 한국도 개인의 개성과 다양성을 존중하는 방향으로 옮겨가고 있다는 증거로 보여진다.

서울 학교급식에 ‘채식선택권’ 도입…”초중고 점차 확대”

[단독]군대서도 비건 급식 먹는다… 채식주의자, 짬밥을 바꾸다

그럼에도 한국 직장인들의 생활패턴을 생각해보면 한국에서 채식을 유지하는 것은 굉장히 힘들다. 점심이나 회힉등 다 같이 식사하는 자리가 많은데 그런 식당에는 채식메뉴가 없을 확률이 굉장히 높다. 그리고 아직 한국의 외식비는 직접 조리하는 것에 비해 많이 비싸지 않다. 집에서 스스로 요리를 할 수 있다면 채식식단을 꾸리기 더 쉬울테지만 만원으로 고기를 먹을 수 있는데 힘들게 직접 재료를 사서 조리하기엔 동인이 부족하다.

채식을 유지하기 위해선

그래서 초기에는 좀 잘알려진 채식관련 식당들을 다니면서 다양한 채식 음식을 먹어보는게 중요할 듯 하다. 나도 채식을 시작하고 가장 힘들었던게 메뉴구성이었다. 경험해본 메뉴가 적으니 매일 같은 메뉴가 반복되는 것인데 여러나라의 다양한 음식들을 접해서 스스로 만들 수 있는 채식의 구성을 최대한 늘려놔야 한다. 그래도 역시 고기가 없이는 만들기 힘든 음식들이 생각날때 채식의 풍미를 더해줄 수 있는 대체육이 정말 가뭄에 단비같은 존재이다. 왜 채식주의자라고 스팸, 소시지, 베이컨 등을 맛있다고 느끼지 못하겠는가. 대다수의 사람들에게 고기를 먹지말라고 한다면 그사람이 가진 얼마 되지 않는 기쁨을 빼앗는 것일수도 있다. 사람은 항상 건강한 방식만을 찾아서 생활하는 것은 아니기 때문에 대체육은 채식에서 대항해시대의 향신료와 같은 역할을 할 수 있을 것이라고 믿는다.

한국도 남의일이 아니다

채식에 대한 관심이 높은 미국이나 호주는 식량 자급률이 굉장히 높은 편인데 한국은 자급률이 50%가 되지 않는다. 참고 – [데스크의눈] 코로나發 식량 위기론 다른말로 한국은 세계 식량 사정에 따라 비자발 적으로 식습관을 급격하게 바꿔야 할수도 있음을 의미한다. 그게 채식이 될지는 아무도 모르지만 적어도 육식보다는 자급률을 높일 수 있는 채식 바람에 대비해놓아야 하지 않을까. 한국에서도 채식하는 인구가 많이 늘어사서 다양한 음식들이 새로 개발 되고 식당에서도 자연스럽게 채식 옵션이 제공될 수 있기를 기대해본다.

Aside

아치유닛(ArchUnit) 테스트

아치유닛(ArchUnit)을 사용하는 이유

“두 패키지 사이에 순환 참조(Circular Dependency)가 존재합니다. 변경이 필요해요.”

“@SpringBootTest 어노테이션을 사용하는 통합 테스트 코드는 test 폴더가 아니라 integration-test 폴더에 위치해야합니다.”

“Service 레이어는 Controller와 Model 패키지 에서만 접근해야 합니다”

팀내 경력이 오래된 시니어 개발자는 위와 같은 커멘트를 작성할 때가 많다. 그러면서 왜 그래야 하는지 코드나 패키지 단위로 다이어그램을 그려서 어떻게 컴포넌트들이 서로 작동을 해야하는지 설명한 경험이 있지 않은가?

새롭게 팀에 들어왔거나 해당 지식을 가지지 않은 사람들은 제일 경험이 많은 사람이 친절하게 알려주는 것이 팀내 지식 공유 차원에서는 제일 바람직할 것이다. 하지만 확장 가능하지 않고 팀내 고급인력의 끊임 없는 관심을 요구한다. 갑자기 사람이 늘어나거나 담당자가 휴가를 가버리거나 퇴사하면 잘 작동하지 않는 모델인 것이다.

우리는 이미 유사한 상황들에 많이 대처해봤다. 아치유닛을 사용하면 아키텍처상의 결정사항이나 결함들도 통합 빌드의 한 부분으로 테스트를 작성하고 자동화 하는 것이 가능하다.

홈페이지에서도 언급하고 있듯이 아치유닛이 아니어도 AspectJ나 CheckStyle, Findbugs를 사용해서 유사한 테스트를 수행할 수 있다. 하지만 해당 도구들은 조금 더 범용적인 성격을 가지고 있기 때문에 코드의 구조를 분석해서 읽기 쉬운 테스트를 작성하기 위해서는 아치유닛을 사용하는 것을 것을 추천한다.

아치유닛 구성

아치유닛은 Core 레이어, Lang 레이어, Library 레이어가 존재한다. Core API를 통해 대상을 특정하고 Lang API를 사용해 규칙을 정의한다. Library 레이어는 미리 정의된 규칙들, 예를들어 3 Tier 아키텍처, 6각형 아키텍처(Hexagonal Architecture) 등을 위한 규칙들을 제공한다. 아직은 실험적인 상태로 보이며 추후 확장의 여지가 있는 부분이다.

Core 레이어의 ClassImporter

리플렉션과 유사한 기능을 제공하는 Core레이어에서는 ClassImporter가 가장 중요한 API들을 제공한다. ClassImporters는 컴파일된 클래스들을 불러오기 위해 사용한다. 다음 코드는 com.book 패키지내의 클래스중에 Jar나 테스트 폴더등을 빼놓고프로덕션 코드만 대상으로 지정하는 설정이다.

ClassFileImporter()
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES)
      .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
        .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
        .importPackages("com.book")

Lang 레이어는 아키텍처 규칙을 정의하는 API들을 제공한다. 규칙을 정하고 나면 다음과 같이 실제 검증을 수행한다.

JavaClasses importedClasses = new ClassFileImporter().importPackage("com.myapp");
ArchRule rule = // 아래 예와 같이 다양한 룰 생성
rule.check(importedClasses);

패키지 의존성 확인

service 패키지는 controller와 resource에서만 접근 가능하다.

classes()
   .that()
   .resideInAPackage("..service..")
   .should()
   .onlyHaveDependentClassesThat()
   .resideInAnyPackage("..controller..", "..resource..")

클래스 의존성 확인

*Service 클래스는 Controller 클래스 에서만 접근 가능하다.

classes()
  .that()
  .haveNameMatching(".*Service")
  .should()
  .onlyBeAccessed()
  .byClassesThat()
  .haveSimpleName("Controller")

클래스와 패키지 관계 확인

Book으로 시작하는 클래스는 com.book 패키지에 위치해야 한다

classes()
  .that()
  .haveSimpleNameStartingWith("Book")
  .should()
  .resideInAPackage("com.book")

상속 관계 확인

Connection 인터페이스를 구현하는 클래스는 이름이 Connectiond으로 끝나야한다.

classes()
  .that()
  .implement(Connection.class)
  .should()
  .haveSimpleNameEndingWith("Connection")

EntityManger클래스로 할당 가능한 클래스들은 persistence 패키지에 위치해야 한다.

classes()
  .that()
  .areAssignableTo(EntityManager.class)   
  .should()
  .onlyBeAccessed()
  .byAnyPackage("..persistence..")

주석 테스트

com.book 패키지 중에서도 “build/classes/kotlin/test” 폴더에 위치한 테스트들은 SpringBootTest를 사용해서는 안된다.

 classes()
    .that()
    .resideInAPackage("com.book")
    .should()
    .notBeAnnotatedWith(SpringBootTest::class.java)
    .check(ClassFileImporter()
         .importPath("build/classes/kotlin/test"))

레이어 테스트

논리적인 레이어를 구성해서 그 관계를 검증한다. 패키지로 구분된 controller, service, persistence 레이어를 각각 정의하고 각 레이어 별로 접근가능한 레이어들을 정의한다.

layeredArchitecture()
    .layer("Controller").definedBy("..controller..")
    .layer("Service").definedBy("..service..")
    .layer("Persistence").definedBy("..persistence..")
    .whereLayer("Controller")
    .mayNotBeAccessedByAnyLayer()
    .whereLayer("Service")
    .mayOnlyBeAccessedByLayers("Controller")
    .whereLayer("Persistence")
    .mayOnlyBeAccessedByLayers("Service")

순환 참조 테스트

패키지 com.book 의 하위 패키지들을 slice로 구성해서 각 slice들이 순환 참조 하지 않는 지 검사한다.

slices()
 .matching("com.book.(**)")
 .should().beFreeOfCycles()
 .check(javaClasses)

Aside

유용한 애자일 의식들 (Agile Rituals)

이전 아틀리시안 취업 후기에서 짧게 언급했지만 아틀리시안에서는 애자일 관련된 미팅을 할 때 사용하는 여러가지 템플릿들이 존재한다. 보통 애자일 의식(ritual, ceremony)이라고 하면 스프린트 플래닝, 데일리 스크럼, 회고 등을 떠올리는데 그 외에도 팀이 업무를 하면서 마주치는 여러가지 상황에 맞게 활용할 수 있는 다양한 의식들이 존재한다.

애자일 의식으로 얻는 것

정확한 의사전달

이렇게 미리 정의된 도구들을 사용할 때 얻는 가장 큰 장점은 구성원간 의미전달이 아주 명확해 진다는 점이다. 예를 들어 “이번주 금요일에 회의 있어요”. “오늘은 일하는 날이에요” 라고 말하는 것 보다 “이번주 금요일에 스파링 있습니다”, “오늘은 GSD날이에요”. 라고 말하는게 세세한 차이까지 전부 전달할 수 있다.

생산성 증가

선진국을 중심으로 근로자의 월 평균 근무시간은 계속 줄어들고 있다. 어떻게 적게 일하면서 더 높은 생산성을 유지할 수 있을까? 회사 전체의 생산성을 위해서는 위해선 여러 사람이 모이는 회의나 의사결정의 생산성이 무엇보다 중요하다. 팀원이 5명인 경우 1시간 걸릴 의사 결정을 3시간 걸려서 끝낼 경우 10시간의 추가 근무가 필요하다.

결과물 명확화

각 의식 별로 단계별 결과물이 확실히 정해져 있기 때문에 회의가 중간에 다른 길로 샐 우려가 적다. 각 참가자들은 회의에 앞서 어떤 내용을 준비해야 하는지 진행되면서 어떤 행동을 해야하는지 미리 알 수 있게 된다. 이는 여러 사람이 모였을 때 쓸데 없는 시간 낭비를 줄일 수 있다.

10년전에 한국 회사에 근무할 때 목적을 알 수 없는 미팅이 참 많았다. 2-3시간을 내리 미팅을 하지만 회의록만 늘어날 뿐 결정된것은 하나 없고 미팅이 끝나도 구성원들이 무엇을 해야할지 감을 잡을 수 없는 그런 상황, 참 많이 겪어봤다. 그런상황에서는 단순히 지칭하는 용어를 미팅,회의에서 회고, 데일리 스크럼 바꾸는 것 자체가 큰 효과를 가진다. 기술회사에서 근무하는 대부분의 사람들은 해당 용어를 접했을 때 회의의 목적이 무엇인지, 참가자로서 어떻게 행동해야 하는지 대부분 바로 이해할 수 있다. 데일리 스크럼이나 회고 같이 업계 수준에서 사용하는 의식들은 훨씬 도입하기 쉽다.

아틀라시안의 팀플레이 북은 아틀라시안 내부에서 자주 사용되는 여러 의식들을 플레이(Play) 형태로 제공한다. 여기서는 플레이 북의 의식들을 위주로 어떤 상황에서 활용할 수 있는지 알아 본다. (위키삽입)

[insert page=’agile-ritual’ display=’all’]

Aside

Renovate로 의존성 관리

MSA에서 의존성 관리

MSA에서 라이브러리 업데이트는 꼭 해야 하지만 잊기 쉬운 특성을 가진다. 이는 손씻기나 양치질등과 닮아 있다. 열심히 해도 티가 안난다. 문제가 생기기 전까진!

백엔드 개발자의 백미는 자동화를 통해 적은 리소스로 많은 사용자들을 대상으로 한 서비스에서 발생하는 문제를 해결하는데 있다. Renovate는 버전업 프로세스를 사용자가 원하는 만큼 자동화 시켜준다.

주기적인 버전업은 잠재적인 기술부채를 줄이고 서비스를 더욱 안정적으로 쓸 수 있게 해준다. 최근에는 보안 관련 패치가 버전 업그레이드를 통해 자주 일어난다.

때문에 주기적으로 라이브러리를 업데이트 하지 않으면 기술 부채를 조금씩 저축하는 것과 같다. 지금 팀에서는 매 빌드시마다 SourceClear 를 사용해 소스코드와 라이브러리의 취약점을 분석한다.

작성한 코드에서 취약점이 발견 되는 경우 직접 수정을 하면 되지만 Spring이나 Jackson과 같이 사용중인 라이브러리 취약점이 발생하면 라이브러이 버전 업그레이드로 필요하다. 문제는 이 취약점이 꽤나 빈번하게 발견된다는 점이다. SourceClear와 연동하고 나서 버전 업그레이드가 팀내의 잡일처럼 되어버렸다.

그와중에 Renovate는 가뭄에 단비같은 존재, 사용자 설정한 내용대로 버전 업그레이드를 위한 풀리퀘스트를 생성해준다. 자동으로 master머지도 가능하며 시간당 생성하는 풀리퀘스트의 수 등 아주 다양한 설정이 가능하다.

팀에서는 kotlin + spring + gradle + docker 플러그인을 사용하고 있는데 이외에도 아주 다양한 플러그인을 지원한다. 아래 스크린샷에서도 확인할 수 있듯이 change log까지 전부 첨부해준다. 이건 개발자가 PR을 생성할떄 잘 안해주는 부분인데, Renovate는 세심하다.

Github에서는 App으로 제공되고 있으며 Gitlab이나 기타 설치형 저장소에서는 Renovate Bot을 직접 호스팅 하는 것도 가능하다. 아래는 팀의 일부 저장소에서 사용중인 renovate.json 설정 파일이다. 원하는 만큼 세세하게 설정이 가능하다. https://docs.renovatebot.com/self-hosted-configuration/

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "schedule": [
    "after 10am and before 5pm every weekday"
  ],
  "timezone": "Australia/Sydney",
  "prConcurrentLimit": 20,
  "prHourlyLimit": 1,
  "automerge": true,
  "dockerfile": {
    "enabled": true
  },
  "maven": {
    "enabled": true
  },
  "terraform": {
    "enabled": true
  },
  "extends": [
    "config:base"
  ]
}

Bitbucket에서 혼자서 열심히 버전 업중인 Renovate
Pull Request 템플릿

문제점

하지만 소프트웨어 문제가 언제나 그렇듯이, 항상 좋기만 한 건 없다. 그렇지 않아도 몇일전에 관련된 장애가 한 건 있었다. reactor-netty 에서 중대한 메모리 누수가 발견되서 부랴부랴 전부 롤백해야했다.

사실 Renovate의 문제라기 보다 시스템적으로 버전업시의 충격을 흡수 할 수 있는 쿠션이나 버퍼가 존재했어야 한다. 배포가 잦지 않다면 스테이징 서버가 이런 역할을 할 수 도 있겠지만 문제가 일어난 서비스는 배포가 아주 빈번하게 일어나서 버전 업그레이드가 문제인지 개발자가 머지한 PR이 문제인지 판단하기 힘들었다. 

그래서 우리는 모든 스프링기반 프로젝트의 베이스가 되는 프로젝트를  만들고 해당 프로젝트는 자동적으로 의존성을 갱신하고 배포되도록 했다. 

이글을 읽는 분들도 저장소에 Renovate를 적용해 보고 어느정도 자동화가 가능할지 테스트 해보길 권한다.

Aside

라인 증권 프로젝트 회고

한국에서는 크게 알려지지 않았지만 2019년에 라인 증권 서비스를 일본을 대상으로 공개했다. 라인에서 근무한 시간은 길지 않지만 운 좋게 파이낸셜 개발실에 소속되어 정보 벤더(블룸버그, 로이터등과 같은 회사)들과 내부시스템의 연동, 가격 결정 시스템 등 나름 핵심적인 업무들을 1년간 수행했다. 배운 것들을 정리해두려고 했지만, 국제이사에 이직까지 겹쳐서 하루 이틀 미루다가 더 늦기 전에 이곳에 옮겨 본다.

라인 증권에 대해서

2020년 상반기, 코로나바이러스로 실물 경제는 어려워지는데 젊은 사람들은 부동산 시장에서 놓친 기회를 만회해보려고 주식 투자를 늘리고 있다. 일본의 증권 업계도 일본인들의 장롱 속에 된 현금을 어떻게든 주식 시장으로  가지고 오고 싶어 한다. 그러기 위해서 일본 증권 업계가 해결해야 하는 문제들은 수도 없이 많지만, 그중에 라인 증권이 해결하고자 하는 문제는 다음과 같았다.

소액으로 투자가 가능

최근 주식을 시작한 사람들은 잘 모르겠지만 몇 년 전 까지만 해도 코스피는 10주 단위로만 거래가 가능했다. 예를 들어 액면 분할 전의 삼성전자를 사기 위해서는 1,000,000~ * 10, 적어도 천만 원 이상의 현금이 필요했다. (2015년 코스피는 증시 활성화를 위해 1주 단위 거래를 허용했다, 관련기사) 반면 닛케이의 거래 단위는 무려 100주. 닌텐도 주식이 한 주에 4만엔 정도 하니 닌텐도에 투자하기 위해선 400만 엔이 필요하다. 그래서인지 일본에서 주식에 투자하는 사람들은 여유돈을 몇억 씩 굴릴 수 있는 부자의 이미지가 강하다. 일본의 2, 30 대의 사회 초년생들이 주식을 할 수 있을 턱이 없다. 라인 증권은 이를 파고들어 젊은 사람들이 소액 투자가 가능하도록 하는 걸 목표로 했다.

편리한 UI

내가 UI/UX의 전문가는 아니지만 적어도 초심자를 대상으로는 라쿠텐(楽天)이나 다이와(大和)보다는 로빈후드가 제공하는 UI/UX가 몇 배는 사용하기 쉬워 보인다. 라인 증권은 기존 일본 증권사보다 사용하기 쉬운 UI와 손쉬운 거래 플로우를 제공하려고 했다.

로빈 후드

결과적으로 첫 번째 두 번째 모두 장외거래(Over The Counter)를 도입해 시스템을 밑바닥 부터 개발함으로서 해결이 가능했다. 일반적인 주식 거래는 증권 거래소를 통해 이뤄지고 거래 당사자들은 중개 수수료만 증권사에 지불하는 구조다. 장외 거래를 사용해 라인이 미리 선정한 주식들을 보유하고 고객들은 거래소를 통해 타인과 거래하는 대신 라인과 거래를 하게 되는데 그렇게 해서 100주 단위가 아니라 1주 단위 거래는 물론 정규 거래 시간을 지나서 야간에도 거래가 가능해지며 사자/팔자 주문의 흐름도 단순해진다.  더불어 라인도 주식을 보유하고 거래하는 리스크를 지니기 때문에 일반 거래소를 통한 거래 보다는 수수료가 더 붙는다. 이 수수료를 붙인 가격을 상황에 맞게 다양하게 계산해 리얼타임으로 가격을 생성하는 것으로  일본 거래소가 가지는 여러 가지 제약으로부터 풀려나는 것이 가능하다.

일반적인 증권회사의 시스템들이 한국 일본 모두 어느 정도 패키지화가 많이 되어 있어서 계좌 부분을 제외하고는 벤더의 기존 코드를 빠르게 재사용해 개발하는 것이 가능한데 이렇게 새로운 거래 방법을 도입하면 되면 추가적인 개발이 굉장히 많이 필요하다는 점이다. 라인이 이런 모델을 택하게 되면서 사실 개발 난이도도 높아지고 프로젝트가 실패할 가능성도 높아지게 된 것이다. 다음은 추가 개발이 필요했던 내용이다.

  • 라인 증권이 제시하는 팔자/주문 가격 생성
  • 라인이 손해를 보지 않게 하기 위해 다양한 거래 검증 로직 구축 (자세한 내용은 영업비밀)
  • 라인이 주식을 보유할 수 있도록 재고 관리 및 구매 시스템 구축
  • 등등등..

라인 증권의 아키텍쳐 방향성

나는 지금도 그렇지만 절대적으로 이 정도 규모의 새로운 프로젝트는 MSA로 가야 하며 2~3명으로 나누어진 도메인 팀끼리는 API로 대화하며 도메인 개발을 진행하길 원했다.  내가 경험한 모노리스 서비스는 git 커맨드 조차도 쉽게 실행되지 않고 로컬에서 서버를 구동하기 위해선 8기가 이상의 메모리가 필요했으며 제일 중요하게는 배포가 제한적으로 일어날 수밖에 없다는 단점이 있었기 때문에 나름대로 계속 MSA를 주장했다.

그러다 결국은 나와 한두 명의 개발자 vs 나머지 개발자들의 구도가 되었고 어떻게 보면 굉장히 소외감을 느낄 수도 있는 상황이었다. 하지만 결과적으로는 다른 팀보다 훨씬 적은 인원으로 6개월 동안 3개 정도의 서브 시스템을 구현하는 데 성공했다. 단순 곁다리 시스템이 아니라 가격 생성/검증부터 외부 정보 벤더 연결 등 핵심적인 부분이었으며  내가 생각하기에 프로젝트 비용 중에 가장 가성비가 잘 나왔던 영역이라고 생각한다. 오히려 인원이 적어서 멤버들끼리 지식 공유가 잘되었고 의견 충돌도 훨씬 적었던 것 같다. 물론 우리 팀의 개발자들은 모두 한국, 중국, 한국계 캐나다인 등 이었고 나머지 전부는 일본인 팀이었다.  나는 최대한 문서작업을 줄이고 비즈니스 로직은 코드로 표현하는 이상적인 환경을 원했기 때문에 문서 작성을 게을리했지만, 일본인으로 구성된 팀은 문서화를 정말 철저히 했으며 나도 그에 따르기를 원했다. 그래서 프로젝트의 후반에는 많은 시간을 문서작성에 사용한 것 같다.

라인 증권 개발언어

개발 언어는 스프링 기반의 자바를 주로 사용했는데 나는 WebFlux의 도입을 적극적으로 이끌었던 반면 일본인 팀은 사용이유에 대해서 잘 이해하지 못했고 본인들이 기존에 사용해서 익숙한 Spring Web을 사용하길 원했다. 기술 선택에 있어서 익숙함이 그것을 선택하는 유일한 이유가 되어서는 안된다고 생각했고 다른 팀원들이 따라와 주길 바랬는데 사실 그들도 시니어였기 때문에 몇번 충돌이 일어났었다. 프로젝트 진행시에 가끔 본질적인 업무가 아닌 부가적이라고 생각하는 부분에서 시간을 더 잡아먹거나 해결되지 않아서 답답함을 느끼거나 짜증을 내곤한다. 그 당시에는 팀 내 커뮤니케이션을 부가적인 업무라고 생각해 설득에 소홀했던 것 같다. 지금 와서 생각해보면 과연 우선순위가 떨어지는 문제였을까?  팀내에 다수의 사람이 이해하지 못한 상황에서 WebFlux를 그대로 사용했던 결정은 옳았던 걸까? 성공적으로 출시는 가능했지만, 다시 개발하게 된다면 그 부분은 조금 고쳐보고 싶다. 개발자는 코드만 보겠다는 자기 암시에 빠지기 쉬운데 경력이 10년이 넘어가면서부터 팀 내의 소통이야말로 높은 수준의 코드작성 만큼 우선순위를 높게 두어야 할 문제임을 느낀다.

WebFlux를 사용하길 원한 이유는 실시간 가격변동이 푸시 서비스를 통해 이루어지는데  푸시 서비스를 대상으로 리액티브 프로그래밍을 사용하면 동시성 제어가 더 쉬워지고 가독성 좋은 코드를 작성할 수 있다고 생각했기 때문이다. 구현한 코드를 CompletableFutre 등과 같이 Java가 제공하는 비동기 기능만을 가지고 작성했다면 훨씬 이해하기 힘들고 테스트하기도 힘들었을 것이다. 실제 WebFlux의 많은 예제가 증권 거래 시스템을 다루고 있는 점은  기술셋과 도메인이 그만큼 잘 맞는다는 방증이다. 더불어 증권은 특정 시점이 되면 돌아가야 하는 배치 작업이 많았기 때문에 WebFlux를 사용해 작업을 겹치지 않는 부분 작업으로 분리해 동시성을 높일 수 있었다.

라인 증권 개발/배포 인프라

인프라의 경우 클라우드는 전혀 고려하지 못했고 레디스, 엘라스틱 서치, 카프라, MySQL등 을 다양하게 사용했다. 금융 서비스라서 외부에 뭔가 안정적인 느낌을 주기위해서 MySQL을 사용했을 뿐 계좌 시스템을 제외한 증권 서비스는 레디스, 엘라스틱 서치만 가지고도 안정적으로 운용하는 것이 가능하다고 본다. 라인은 자체적으로 아주 대용량의 레디스 클러스터를 운용하고 있어서 노하우도 풍부하고 Lettuce 가 제공하는 리액티브 연동도 아주 마음에 들었다. 하지만 그만큼 잘 운영하기 위해서는 여러 번의 시행착오가 필요했으며 이곳에 사례들을 열거하지 않겠지만 증권 조직 자체에서 SPOF가 될 수 있는 부분을 찾아 이중화하고 에러가 발생해도 쉽게 복구할 수 있도록 시스템을 구성하자는 분위기가 있었기 때문에 많은 좋은 사례들을 발견할 수 있었다.

많은 일본인 개발자들은 쿠버네티스나 도커등을 적극적으로 활용하기를 원했지만 나는 배포가 자주 일어날 수 없는 금융 환경에서 해당 시스템들을 굳이 도입해야 할 이유가 없다고 생각했다. 배포를 위해서는 각 사업부서의 허가를 받는 시스템이 있었는데 배포해야 할 내용을 쫙 늘어놓고 사업 부서에서 배포하는 목적과 리스크 등을 평가하는 방식이었다. 사업 부서의 의견은 중요하고 피드백 루프에 포함되는 것이 필수적이지만 전체적인 배포에 관련한 프로세스는 굉장히 쓸모없는 절차라는 생각이 들었다. 왜냐하면 사업 에서 각 배포의 리스크를 이해하기 쉽지 않기 때문이었다. 결국 시스템의 완결성을 책임지는 조직은 개발팀인데 라인 내부에서는 PM, QA, 개발조직으로 권한을 나눠주는, 내가 보기엔 이상적이지 않은 암묵적인 규칙이 존재했으며 특히 증권 에서는 노무라 출신의 사업 조직까지 겹쳐지며 더욱 혼란스러운 구조가 되었다. (사실 이런 삼권분립과 유사한 이상한 구조는 일본과 한국으로 나눠진 라인 내부의 사정도 기인하는 것 같다)

기타외부 요인

노무라 증권 사람들하고 일해본 것은 굉장히 재미있는 경험이었다. 노무라 증권은 일본 내에서는 최고의 직장으로 꼽히며 직원들도 엘리트라는 인식이 있지만, 역시 오래된 회사답게 웹 서비스 관련해서는 굉장히 뒤쳐져 있었다. 하지만 주식 관련 업무에 관해선 정말 노련하고 능숙한 사람들이 많았으며 기술 부분 쪽 사람과는 다르게 뭔가 시원시원 하기도하고 일본인 답지 않게 의견을 바로바로 개진하는 점도 좋았다. 다만 내가 원하는 서비스 개발 방향과 맞지 않는 의견들을 자주 이야기해 내 매니저는 노무라 사람들로 부터 불만을 들어야 했지만 다행히 끝까지 나를 신뢰해 주었기에 때문에 나와 팀원들은 개발에 집중해 프로젝트를 무사히 마무리할 수 있었다.

사업부분과 가장 대표적인 의견 차이는 정보 벤더 선택시에 있었는데, 나는 당연히 해외 벤더들을 선호했다. 그 이유는 모든 문서가 영어로 잘 되어있고, 높은 수준의 SDK를 보유했으며 회사 자체의 기술력도 훨씬 높은 축에 속했기 때문이다. 그러나 일본 쪽에서는 일본 출신의 벤더들을 선호했고 나를 설득시키기 위해서 굉장히 노력했는데, 나는 일본 회사는 API가 잘 정리되어 있지 않고 개발환경조차 존재하지 않고 심지어 API 문서를 요구하면 사전 두께로 프린트해서 보내주는 회사였다.  나는 일본 벤더를 선택하면 프로젝트 일정이 늦어질 것이다 라고 이야기 했다. 다행히도 나의 반 허풍은 먹혀서 내가 원하는 외국계 벤더가 될 수 있었지만, 릴리즈하고 나서도 집요하게 벤더 교체에 대한 요구를 지속 했다. 사업쪽에서는 벤더의 서포트 조직이 외국에 있는 사실을 엄청나게 못미더워 했고 벤더 쪽에서 개발자 입장에서 이해할 수 있는 작은 실수라고 발견하면 엄청나게 쏘아붙이기 일쑤였는데 보는 내가 안타까워질 정도였다.

해당 정보 벤더 사하고 일하게 되면서 정말 몇억을 줘야 써볼 수 있는 여러 가지 시스템들을 다뤄 보았으며 나름 재미있었다. 장의 움직임을 그대로 재현할 수 있는 도구, 전 세계 모든 회사의 기업 평가 정보를 제공하는 시스템등 관련 업무를 하지 않으면 존재자체를 모를 도구들이 있었으며 그쪽 분야에서 또다른 사업 기회가 있지 않을까 생각도 들었다.

마무리

이것으로 대충 정리를 마친다. 사실 라인에 입사하고 2년도 안 돼서 뛰쳐나오긴 했지만,  매니저의 배려와 운이 적절하게 맞아떨어져서 정말 재미있는 시스템 개발을 해볼 수 있었다.  (1년만에  증권 시스템을 개발하고 출시하는 경험은 흔치 않다)  아쉬운 점으로는 군 생활을 전역 후에 돌아보는 것과 유사하게 프로젝트에 대한 기억이 좋은 추억 위주로 남아서 가끔 팀원들에게 조금 더 협조적으로 둥글둥글하게 대할걸 하는 아쉬움이 든다.  인생 선배들이 자주 하는 조언중에 “Don’t burn your bridges”가 마음에 와닿는데, 회사내에서는 티격태격 주도권을 위해 다투지만 결국 이직하고 나면 같은 업계 사람일 뿐이다. 채용 기준이 무엇 이냐는 질문을 받았을 때 “인상”이라고 대답했던 매니져의 말을 당시에는 농담으로 받아들였지만, 업계 20년 이상의 경력에서 나오는 6감, 직관력등 그 능력을 이제는 인정해 줄만 하다. 내가 부족한 언어 능력으로 인해 독불장군처럼 고집을 굽히지 않았을 때 철저히 위임하고 맡겨준 그 판단이 맞았다. 한국,일본,미국, 호주등 다양한 곳에서 개발을 했지만 개인적으로는 라인 증권을 개발할 때 일적으로는 제일 재미있었다. 일본의 개발 문화와 비교해 보면 아이러니하다고 생각하는데 사용하는 기술셋들의 화려함이 아니라 본인이 스스로 느끼는 프로젝트 기여도 몰입도 등이 만족도에 큰 영향을 미치는 것 같다.  사실 네이버, 카카오 같은 조직에서도 새로운 서비스를 개발하고 출시하고 성장하는 모습을 처음 부터 보는 사람들은 몇 안되고 그럴 수 있는 사람들은 굉장히 운이 좋은 축에 속한다. 나는 라인 증권에서 느낄 수 있는 익숙함을 뒤로하고 아예 도메인이 바뀌어 버렸지만 가끔은 그곳에서 집중하면서 개발했던 시절이 그립다.

곧 라인 증권의 거래소 서비스가 오픈한다는 소식을 들었는데 매니저와 팀원들에게 노력의 열매가 모두 골고루 돌아가길 바란다.

Aside