누구나 실수/실패의 상황을 마주하게 되며, 이것은 당신의 실수일 경우도 있고, 타인의 실수 있을 수 도 있으며, 서버/네트워크상에서 일어나는 실패(장애)일 수 도 있다. 이런 다양한 실수/실패는 개발자의 삶과 항상 함께 하는 것이기 때문에, 어떻게 이것을 다루는지가 관건이 된다. 이 글에서는 어떻게 실수/실패등 의도치 않은 상황을 처리하는지, 다양한 패턴들과 함께 살펴보겠다.  

1. 디폴트값 처리 

val value = getValue() 
val gretting = value?: "hi"

정상적인 상황하에서 값을 얻지 못하였을 경우 우리는 디폴트값을 할당하여 사용 할 수가 있다. 디폴트을 사용할 수 없는 경우에는 대개 예외를 던지거나 실패값을 리턴해서 상위에서 처리하길 기대 할 수 밖에 없다.

2. Require / Check / Assert  

보통 의도치 않은 결과를 받았을때 무시하고 조용히 넘어가기도 하나 "나 문제 있어요" 라고 공격적으로 알리기도 한다. 코틀린언어로 설명하면 (대부분의 언어가 동일한 기능을 제공한다) 아래와 같은 키워드들이 존재 한다.

require : 주로 파라미터가 기대하는 값인지에 대한 검사 
check : 주로 객체가 기대하는 상태를 가지고 있는지에 대한 검사 
assert : 테스팅 모드에서 어떤 값이 true 인지를 체크하는 검사 (JVM에서는 -ea옵션을 줬을때 작동) 

fun pop(num: Int = 1): List<T> {
	require(num <= size) { "Cannot remove more elements than current size" }
    
    check(isOpen)

	val ret = collection.take(num) 
    collection = collection.drop(num) 
	assert(ret.size == num) 
	return ret 
}


3. 예외 상황

무엇인가 원하는 결과가 발생되지 않았을 경우 당장 적절한 해법이 없을 때,  우리는 예외 처리를 하게 되는데, 예외 처리라는 것은 굉장히 까다롭다. 해당 예외를 발생한 가장 가까운 곳에서 처리 할 수도 있으며, (가장 잘아니깐) 가장 먼곳까지 거슬러 올라가서 처리 될 수도 있다. 또한 예외 발생시 프로그램을 죽여야 하는 경우도 있고, 살리되 로그라든지 클라이언트측에 잘 설명해야 하는 경우도 있다. 

젤 어려운 부분 중 하나는 모두 되돌릴 것인가? 이미 벌어진 일은 무시,포기할 것인가? 인데 
그냥 살리는게 중요 포인트라면 예외 발생시 그냥 로그정도 출력하든지 클라이언트에게 알려주고 지나치면 된다. 그 메소드를 호출한 사람은 아마 한번 더 호출해 보겠지. 아니면 그 사람은 망하더라도 다른 모든 사람들에게는 서비스가 진행 될 수도 있고.. 하지만 전체 흐름을 되돌려야한다는 문제의식을 갖게되는 어플리케이션이라면 정말 거대한 도전이 된다. 트랜잭션정합성, Redo,Undo,커맨드패턴 ,사가패턴 같은 단어들을 떠올려야 하기때문이다.

자 갤럭시 7 을 생산하는 공장에서 갤럭시 7 에 들어가는 전자기판(pcb)을 검사하는 프로그램이 있다고 해보자.  그 프로그램은 기판을 검사하기 위해 많은 지식이 사용자에 의해 학습되어야 한다. 그 기판에는 어떤 부품들이 있고 어떻게 생겼고 어떤 방식으로 불량이 아닌지를 확인하는등.. 이것을 학습된 검사조건들 이라고 하자.

이 검사조건 중 하나의 값을 30-> 50 으로 올릴때  발생하는  시나리오가 아래와 같이  흘러 간다고 하자.

a. 50을 입력받아서 
b. 50을 포함한 새로운 임시 객체를 만들어서 적용해 나간다.
c. 변경 된 데이터를 많은 뷰에 적용시키고 (즉 화면뷰에도 50으로 바뀌고 , 리스트뷰에도 바뀌고) 
d. 네트워킹을 통해 중앙저장소에도 바꾸어주고 
e. 검사조건을 모아놓은 메모리 자료구조를 수정한다. 
f. 로컬 xml 파일에 새로운 50을 써준다.
g.완료 (버튼 활성화)

이렇게 메소드들이 호출된다고 할때 갑자기  d에서 문제가 생기면 어떻게 될까?  메모리에는 이 어플리케이션이 갖는 최종적인 마지막 값이 50으로 갱신되지 못한 채 예외가 생겨서 30으로 남겨져 있다. 그리고 검사조건들을 저장해놓은 파일도 갱신되지 못하여 나중에 프로그램을 시작할때 30으로 읽어들일 것이다. 하지만 화면뷰와 중앙저장소의 값은 50인데??

먼가 문제가 생겼을때 돌이키는 방법으로는 메멘토패턴 혹은 커맨드패턴을 주로 사용하는데 커맨드 패턴인 경우 undo / redo 를 처리 할때 execute 와 unexecute 함수를 가지고 내부에 이전정보,이후정보를 속성으로 가진 커맨드 객체를 이용한다. 근데 이때 unexecute 에서 처리해 줘야할 것들이 간단치 않다는게 문제이다. 저 전체를 한 묶음으로 묶어서 처리하는 트랜잭션 처리시  데이타베이스처럼 상대적이고 일관되고 한정된 로직에 있는 것도 아니고 메소드의 역할에 따라 다른 행동에 대해 어떻게 트랜잭션 처리를 일관되게 할 수 있을까?  맨붕이다..

4. 예외를 던질 것인가? 리턴 값으로 fail을 던질 것인가?  

함수에서 문제가 생겼을때 문제 상황을 외부로 전파하는 방식은 대표적으로 2가지가 있다. 

- 리턴을 null이나 Failure/Option/Try 객체를 리턴한다. (가까운곳에서 처리하길 기대)
- 예외를 던진다. (먼곳에서 처리되길 기대) 

예외를 통한 처리 보다는 리턴을 통해 처리하는 것을 우선시 하며, 예외는 좀 더 처리하기 힘든 상황에 대한 핸들링을 하면 된다. (애매할 수 있는데, 가장 윗단에서 처리되길 기대하는 상황? 예로 백엔드의 controller나 UI프로그램의 시작지점), 예외는 사용자의 가독성이 약하며, (자바처럼 강제하지 않는다면 더더욱), 프로그래머가 무심코 넘기기 쉽다. 그리고 정확한 진단을 내리는데 두리뭉실 할 수가 있으며, try~catch문은 컴파일러가 최적화하는데도 방해하곤 한다. 


5. 커맨드패턴 

커맨드 패턴은 보통 메소드(동사)를 클래스(명사)화 하여 사용하여, 행위를 수집 할 수 있게 한다.
보통 실행/역실행의 2가지 메소드를 포함하는데 무슨 말인고 하니... 아래 코드를 보자. 

class Painter {
  fun draw(){ .. }
  fun remove(){ .. }
}

보통 클래스는 위처럼 명사이고 메소드는 동사임을 알 수 있다.
여기서 draw라는 동사(메소드)를 명사(클래스) 화 해보면 

interface Command {
  fun execute()
  fun unexecute() 
} 

class DrawCommand (val start: Point, val end: Point) : Command {
 fun execute() {
    // draw start to end 
 }
 
 fun unexecute() {
   // remove start to end 
 } 
}

위처럼 DrawCommand라는 클래스가 생기고, 메소드로는 실행/실행취소 2가지 주요 메소드가 만들어진다. 
이렇게 DrawCommand라는 클래스를 만들게 되면, 그림판에서 무엇을 열심히 그리는 동안 그려지는 과정을 내부적으로 Undo 스택을 만들어서 push해 놓게 하였다면, 나중에 내가 잘못 드로잉을 했구나 라고 깨닫게 되었을때, 스택을 되돌리며 unexecute를 실행해서 다시 실수를 "원상복구" 할 수 있게 된다. 이때 unexecute의 구현은 어떤 command냐에 따라서 천차만별 달라 질 수 있을 것이다. 이런 어려움은 위의 3번에서 설명한 부분이다.


6. 서킷브레이커

외부의 서비스에 요청 콜을 날리고 나서 대개는 바로 응답을 받으나, 외부 서비스의 문제 혹은 네트워크의 문제로 응답이 안오거나 늦어질 경우가 있다. 이때 아무런 조치를 안 해 놓는다면 매번 느려진 응답을 기다리느라 장애가 전파되며 전체 시스템에 문제가 생길 수 있으며,  클라이언트측을 화나게 할 수 있다. 문제가 있으면 바로 문제가 있다고 알려주는게 나을 것이며, 1번의 디폴트값 처리식으로 처리 할 수도 있을 것이다. 이런 상황에서 사용 하는게 서킷브레이커인데, 문제가 생기면 더 이상 호출하지 않도록 중간에서 차단시켜 주는 방식이다. 문제가 생기자마다 차단 한 후에 n번까지는 기다려 줄 수 도 있겠으며, wait 인터벌을 다양하게 가져갈 수 도 있을 것이다. 

문제가 해결되었음을 감지한 경우에도 처리하는 방식이 달라질 수 있는데, 모든 리퀘스트를 정상상태와 같이 처리 하게 할 수도 있겠고, 이게 또 무슨 문제가 일어날 지 모르니 살살 간을 보면서 시나브로 오픈하게 만들 수도 있겠다.  
 

7. Consistent Hashing 

어떤 데이터들을 N개의 노드에 분산시켜서 저장한다고 해보자. 혹은 리퀘스트를 분산시켜서 처리 한다고 해보자.
이때 가장 먼저 쉽게 떠오르는 방식은 데이터 d를  hash(d) mod n 해서 나온 노드에서 처리하게 만드는 것이다. 근데 이런 방식은 노드 하나 이상이 실패(장애)가 생기는 경우 큰 비용을 초래하게 되는데, 기존 노드에 있던 데이터들이 모두 재계산되어야 하기 때문이다. (hash(d) mod n <-- 이 값이 달라지니깐)

Consistent Hashing 은 이런 경우를 최소비용으로 해결하는 멋진 방법이다. 이것은 일련의 해시 값을 가상의 링(ring)에 순서대로 나열했다고 가정하고 각 노드는 링에서 각자 맡은 범위만 처리하는 방법이다. 만약 노드를 추가하면 특정 노드(많은 데이터를 가진 노드)가 맡고 있던 범위를 분할하여 새 노드에 할당한다. 노드를 삭제할 때는 링에서 삭제된 노드가 맡고 있던 범위를 인접 노드가 맡는다. 따라서 서비스 중에 노드의 추가/삭제로 인해 영향을 받는 노드 수를 최소화할 수 있다. 

8. Saga패턴

 
마이크로서비스 도입의 환상을 깨주는 가장 큰 문제는 트랜잭션 처리일 것이다. 하나의 단일 RDBMS를 사용 할 경우, 신경 쓰지 않아도 되는 ACID문제가 , 여러 서비스에서 여러 DB를 사용하며, 해당 서비스간에 강력한 의존성을 가지고 있다면 지옥으로 다가오게 된다. CID는 어느정도 참아줄 수 있다고 해도, A(원자성)에 문제가 생기다면 큰 손실을 입을 것이기 때문이다. 즉 A가 B에게 돈을 1억 줄때, 양쪽이 모두 변화가 있어야지, 한쪽만 변화가 있다면?? OTL ..이때 해결방법으로 분산트랜잭션이라는 허상을 사용 할 수 도 있는데, two way commit은 제대로 작동하기엔 너무 부족하며 좀 더 나은 방식으로 우리는 Saga패턴을 고려 할 수 있다.  (그래서 마이크로 서비스를 설계 할 때, 서비스끼리 너무 많은 상태를 공유/의존하는 것을 지양해야 한다) 

사가 패턴에서는 보상이라는 개념으로 (위에 커맨드패턴에서 말하는 것 처럼, unexecute()) 이벤트의 흐름 중 전체가 완결되지 않았다면, 뒤로 롤백하면서 하나씩 Undo(보상)하는 것이다.이 흐름의 조율을 중앙에서 매니징 할 수 도 있고, 각 서비스들이 서로 이벤트를 전달하며 분산해서 처리 할 수도 있다 


9. 이중화,복제   

동일한 것(서비스,데이터)을 n개로 두고,  사용하게 하는 모든 것을 총칭.
듀얼디바이스(RAID,PSU등) , L4/L7 스위치, HAProxy, NginX, Zuul, ELB, API Gateway 등등 

 

'코딩'은 소설가의 '글쓰기'와 비슷하다고 생각 합니다. 

소설가가 '글쓰기'를 잘하기 위해 매일 매일 습작을 하듯이..'코딩'을 잘하기 위해 서는 매일 매일 코딩 하는 버릇을 들이고, 평생 '장인' 정신을 가지고 노력해야하는 일인거 같습니다.  글쓰기는 잘하는 사람이 코딩도 잘할거라는 확신이 있습니다. (여담으로 자신의 생각을 표현하는데 적극적인 작가형 사람은 무엇이든 만들어내는걸 잘 할 것이고,  짧은 생각으로 비판만 하는 습관만 있는 사람이 독창적으로 무엇인가 스스로 만들어 내기 힘들 거란건 충분히 어리짐작 할 수 있겠지요.)

재능과 반복

지나가는 길에 방망이 깍는 노인을 보고, 일기장에 "오늘 방망이 깍는 노인을 봤다, 신기했다" 라고 단편적으로 적는것과 그것을 주제로 수필을 쓰는 능력은 천지차이 겠지요. 꼬아보기,상상력 및 끈기가 필요합니다. 토지같은 장편소설을 글쓰기 하는것은 스프링프레임워크 혹은 데이타베이스를 코딩하는것과 일견 일맥상통 합니다. 글쓰기와 코딩은 이미 모든것을 준비(설계) 해 놓고 쓰는 것이 아니라, 쓰다 보면 쓸거리가 더 생기게 마련인 반복적인 작업 입니다. 어떤 분야든 재능이 중요하지만 반복을 통해 극복가능하리라 생각합니다.

언어의 풍미

글쓰기/코딩을 잘 하려면 해당 언어에 대한 풍부한 지식을 가지고 있어야 합니다. 자기가 알고 있는 어휘가 많을수록(습관도 들여진) 단어와 그 단어와 어울리는 연속된 단어와 문장들을 서술 할 수 있게 됩니다. 언어를 잘 알면 가독성도 좋아지며 적은 묘사만으로 풍부한 해석을 가능케도 합니다.  대부분의 언어는 단순히 자/모음을 가지고 있지만 언어의 풍미는 상당히 다를 겁니다. 해당 언어의 풍미를 살리기 위해서는 네이티브가 아니고서는 파악하기 오래 걸릴 겁니다. 즉 언어를 깊게 배운다는 것은 문화를 배우는 것을 포함합니다. 

클리세와 디자인 패턴 

수천년의 시간동안 건축에 대한 보편적인 구조/배치등이 정립되어 왔듯이 아침드라마 작가가 되기 위해서는 수 많은 이전 아침드라마에서의 클리세를 참고해서 글을 쓰면 주부들의 마음을 뺏을 수 있을 것입니다. 코딩 또한 기존 선배들이 작성했던 방식을 알고 있다면 일반적으로 유연하고 읽기 좋은 코드를 작성 할 수 있을 것 입니다.

컨텍스트(기반기술)

두 작업(코딩,소설) 모두 해당 컨텐츠에 관련된 컨텍스트를 연구하고 수집해야 합니다. 목적과 주제를 분명히 알고 집중 해야 합니다. 쓰기로 마음 먹은 주제에 대해서 옳은 방향으로 잘 풀어 나가려면, 근거가 되는 컨텍스트에 대해서 면밀하게 조사/분석을 해야 합니다. 해당 컨텍스트를 근거로 실질적 글쓰기는 설계되고 구축이 되어져 가게 됩니다. 컨텍스트에 대한 지식이 부족하면 글쓰기가 마음먹은데로 진행되지 않으며, 의미 없는 고민에 빠지게 됩니다. 그래서 문서나 API를 조사하고 읽는법이 중요한것입니다. 어려운 조립식 장난감의 설명서를 잘 읽고 적용도 잘하는 사람들이 있죠. 

잘 읽혀지기  

소설과 마찬가지로 코드는 쓰는 시간 보다 읽혀지는 시간이 훨씬 많습니다. 쓰는 사람도 자기가 쓴 글을 쓰는 시간보다 훨씬 많이 읽어보게 되며, 독자나 유지,업데이트를 해야하는 사람들은 더더욱 그러하겠지요. 따라서 잘 읽혀지는 것은 매우 중요합니다. 두고 두고 다른사람들이 보아야 할 것이고 고쳐야 할 것이기 때문에 잘 읽히고, 어떤 부분은 편하게 어떤 부분은 불편하게 사용하도록 변주(變奏)가 필요하며 조율(調律)을 해나가야 합니다. 그리고 한 문장에 하나의 주장을 담아서 간략히 문장을 만들어야 합니다. 만연체로 쓰여진 문장은 읽기 괴롭지요.

근데 "잘 읽혀진다"라는 의미는 단순하진 않습니다. 평소 풍부한 어휘와 배경지식을 많이 가지고 있는 토종 한국인에게 읽혀지는 "토지"에 대한 읽기는 한국어를 어지간히 배운 외국인이라고 어려울 것이기 때문입니다. 그들에게는 모든 언어에서 공통으로 사용하는 단순한 어휘와 문장구조로 쓰여진것이 외국인에겐 더 가독성이 좋겠지요. 그렇다고 토지를 그렇게 작성하는게 옳은거라 생각하진 않습니다.

퇴고와 점진적 향상 

마지막으로 이런 모든것을 반복적으로 고쳐가며 개선시키는 퇴고의 과정이 반드시 필요합니다. 아무리 천재소설가라도 천재 개발자라도 퇴고 없이는 부족한 결과물만 나올 뿐입니다. 보면 볼 수록 쓰면 쓸 수록 나아집니다. 쓰는 것을 두려워 하면 안됩니다.누구나 처음은 초라하며 고쳐가며 좋아집니다. 주변사람들의 의견은 많은 도움을 줄 수 있습니다.  많은 피드백을 받을 수 있는 인기 오픈소스의 품질이 좋은 이유도 그것입니다. 


p.s


코딩은 공학과 문학의 조화에 있습니다.
공학을 확실한 답이 있는 것(예: 상황에 맞는 자료구조, 정해진 스펙 구현)이라고 정의 하고 ,
문학은 답이 자유분방한 것(예: 예외 처리 설계, 추상화)이라고 정의 한다고 가정 해 볼 경우에
보통 개발자들이 평소에 마주치는 대다수 고민들은 문학적 질문과 답에 있다고 생각합니다.
그래서 더욱더 코딩을 글쓰기와 비견되는 거 같습니다.

 

 

1. 빠른 시간안에 온보딩을 할 수 있게 해준다. 
2. 모든 참여자가 전체 상황에 대한 높은 가시성을 갖게 된다.
3. AML & KYC 프로세스를 표준적으로 도입할 수 있다. 
4. 참여자들의 행위에 대한 투명성을 가져왔다.
5. 데이터 가용성을 높여 준다.
6. 대사,감사과정이 투명하다. (기능을 제공한다) 
7. 신뢰비용을 낮춰 준다. 
8. 제품의 유통과정과 제조사에 대한 출처에 대한 확인을 빠르게 할 수 있다. 
9. 내부토큰을 활용하여 안정적으로 금융 거래의 서브시스템으로 활용 할 수 있다. (추후 CDBC와의 연계 고려)
10. 외부 시스템 및 데이터와의 인터그레이션이 원할 할 수 있다. 
11. 특정 트랜잭션은 특정 조직(개인)들만 볼 수 있도록 조작 할 수 있다. 
12. 일정한 트랜잭션 성능을 보장한다. 
13. 미래에 블록체인 인프라가 더 일반적이 될 경우에 대한 대비 
14. RDB보다 높은 보안성을 가지고 있다. (불가역성,무결성 보유) 
15. RDB보다 다양한 기능을 제공 한다.
16. RDB보다 투명하다.
17. Identity 에 대해 매우 높은 관리를 할 수 있다.  (영지식증명,DID등 활용) 
18. 여러 조직에서 동일한(내장된)시스템만으로 위의 모든 것을 가질 수 있다. RDB만으로는 못하며, 굉장히 다양한 기술들을 RDB와 접목해야하는데, 그렇게 달성하기는 어렵다. 
19. 즉 보안이 강화된 분산 시스템을 쉽게 도입 할 수 있게 한다.
20. 조직간의 고버넌스 기능을 제공한다. 

map이라는 추상화...
reduce라는 추상화..
iterator라는 추상화..
future/promise라는 추상화...
async/await라는 추상화..
journal 이라는 추상화..
expression이라는 추상화..
vistor / Facade라는 추상화..
Traits라는 추상화..
match 라는 추상화..
executePlan이라는 추상화..
InvocationFilter라는 추상화..
Try 라는 추상화..
Composition이라는 추상화..
Channel이라는 추상화..

딱 봐도 저건 이것들을 보편화/간략화 한것이다라고 실체를 바로 판단 가능한 추상화가 있고,
대략적인 느낌 하에 세부 설명을 듣거나 해부해 봐야만, 실체에 대해 판단 가능한 추상화가 있다.

피카소의 황소그림 추상화(抽象畫) 는  전자이고, 

칸딘스키의 "즉흥"은 후자의 것이다. 

소프트웨어의 추상화(抽象化, Abstraction)는 어떤 개념들의 공통의 속성이나 기능을 묶어 이름을 붙이는 것인데,
이 것을 잘하면 먼가 대가 같아 보이긴 한다. 하지만 자동차를 움직인다고, 움직이는 것으로 추상화 하는 경우 

자동차로 명시적으로 표현하는 것보다 나을게 없을 수도 있는데 이런 경우도 많이 존재한다. 

사실 추상화는 오버엔지니어링과 구분하기 어렵다. 
치켜세워주면 유연한 설계를 한 것이고, 비판하면 시간만 잡아먹고 오히려 복잡하게 만든거 아니냐 할 수있다. 

그렇지만 나는 먼가 추상화를 하려고 노력해 놓은 코드를 보면, 어쨌거나 사랑스럽다. 
물론 GoF의 패턴에서 정의 해놓은 그 단어들은 그 책에서 나온 설계/의도대로만 사용해 줬으면 하기도 하지만, 
그 "의도"를 개인적으로 재설계해서 사용하는것을 뭐라고 하기도 참 머시기 하다...

...





"코딩을 잘하는 사람" 보다는 ooo 사람이 되라. <-- ?? 
"코더보다는 프로그래머가 되라" <-- ??
"코더/프로그래머 다음에는 관리자로 업그레이드 되야 한다" <- 아니다
" 코딩과 설계는 분리 가능하며 설계가 상위능력자의 일이다" <- 아니다


코딩이라는 단어에 대한 감각이 다들 다르니,  저런 식의 말들도 많이 있는거 같습니다.
저는 저런 식의 말들이 틀렸다라고 생각하진 않습니다. 옳을 수도 있는 말입니다.다만 그냥 저는 조금 다른 감각을 가지고 있습니다. (더 광의적으로 해석합니다. 아니 굳이 구분을 하는 의도에 불순함이 있어 보입니다) 

개발자의 '코딩'은 소설가의 '글쓰기'와 같다고 생각 합니다. (일맥상통하는 부분이 있다.)
소설가가 '글쓰기'를 잘하는 것은 가장 중요한 재능이나 노력의 결과인 겁니다. 
어떤 대학의 소설 한편 못써본 국문과 교수가, 직업 소설가를 바라보며 '글쓰기'는 사소한거다.
라고 말해바짜 비웃음만 당할 뿐이겠지요.  

'코딩'도 마찬가지입니다. '코딩'은 가장 중요한 덕목이며,
'코딩'을 잘하기 위해 서는 매일 매일 습작 하는 버릇을 들이고, 평생 '장인' 정신을 가지고 노력해야하는 일입니다.
'코딩' 도 잘하려면  어느정도의 재능과 하루하루 쌓아가는  오랜기간의 부단한 노력이 필요합니다. 

'코딩'을 잘한 다는 것은 

1. 어떤 주제에 대해서 유려하게 구성을 잡고, 꼼꼼하게 빈틈을 채워나가는 능력을 보유 한다는 것을 말합니다. 누구는 어떤 주제에 대해 겨우 10줄밖에 못짜겠지만, 누군가는 200줄로 짤 수 있을 수 있습니다. 
여기서 이 200줄이 쓸모없는 것이 아니라, 모두 필요한 요소들이라면 이 격차는 어마어마 한 것일 겁니다.

2. 문제해결을 위한 기본적인 지식을 기반으로 가지고 있거나, 상황에 따라서  빠르게 공부 할 수 있는 능력을 가지고 있어야 합니다.

3. 문제해결을 유연한 사고 방식으로 다른 각도에서 바라 볼 수 있는 창의력을  가지고 있어야 합니다.

4. 반복해서 고치고, 고민하고 덧붙히는 끈기가 있어야 합니다.

을 기반으로 코드를 작성해 나간다는 의미라고 생각합니다.

그러므로, '코딩'을 잘한다는 것은 우리 소프트웨어 개발자의 평생의 목적이지 않을까 싶습니다.
이렇게 정의를 내려 놓고 보니, '코딩' 을 잘하는 사람들에게 절로 존경심이 들며,  그 코딩의 다음 단계로 관리자가 되어야지, 코딩만 잘해서는 안되다는 말을 하는 사람들에게 역시 쓴 웃음이 나거나 고깝게 보이기도 하지만, 감각이 다르거니 하고 흘려 버려야 겠지요..

ps) 

코딩잘하는 사람을 보기란 정말 어렵습니다.
물론 모든 사람들이 최고의 능력을 보일 필요는 없습니다. 평범한 직업으로써의 능력만으로도 충분할 수 있으며, 삶을 영위 하는데 존중 받을 수 있습니다. 이게 "직업으로써의 소설가" 와는 조금 다를 수있는 부분 인거 같습니다. 

'소프트웨어 사색' 카테고리의 다른 글

스타트업 CTO 가 해야 할 것들 (링크)  (0) 2021.10.20
추상화  (1) 2021.08.29
왜 패턴이 중요한가?  (3) 2020.08.13
왜 야근해야 하는지 모르겠는데 설명 좀 해주실분?  (0) 2020.07.16
Rust  (0) 2020.06.16


소프트웨어 엔지니어링에서 풀의 종류는 다양한데요.

쓰레드풀,메모리풀,캐쉬풀,커넥션풀,객체풀 등등이 있습니다. "풀"어서 말하면 미리 만들어두고 돌려막기로 사용하자 라고 볼 수 있는데요. 미리 만들어 두는 방식 / 쓰레드가 태스크를 처리하는 방식/ 동기,비동기에 따라서 다양한 풀의 구현체들이 있을 수 있습니다.  이 글에서는 Kotlin으로 객체풀을 만드는 간단한 예제를 보여 줍니다. 

1. 리스트를 이용한 고정크기 동기 객체풀 

import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock

interface ObjectPool<T> {
  fun take(): T
  fun release(obj: T)
  fun poll(waitTime: Long, waitUnit: TimeUnit): T
  fun newInstance(): T

  fun <R> use(function: (T) -> R): R {
      take().apply {
        return try {
          function(this)
        } finally {
          release(this)
        }
      }
    }

  fun <R> useAsync(function: (T) -> CompletableFuture<R>): CompletableFuture<R> {
    return CompletableFuture.supplyAsync{
      take()
    }.thenCompose {
      try{
        function(it)
      }
      finally {
        release(it)
      }
    }
   }
}

interface ObjectFactory<T> {
  fun newInstance(): T
}

class FixedObjectPool<T>(factory: ObjectFactory<T>, size: Int, val waitSize: Int = 0 ): ObjectPool<T>{
  
  val factory : ObjectFactory<T>
  val lock = ReentrantLock()
  val condition = lock.newCondition()
  val pool = mutableListOf<T>()
  var pause = false
  
  
  init {
    this.factory = factory
    for (i in 1 .. size) {
      pool.add(newInstance())
    }
  }
  
  fun size(): Int {
    return pool.size
  }
  
  override fun take(): T {
    lock.withLock {
      while (pool.isEmpty()){
        condition.await()
      }
      val obj = pool.first()
      pool.removeFirst()
      return obj
    }
  }
  
  override fun poll(waitTime: Long, waitUnit: TimeUnit): T {
    lock.withLock {
      while (pool.isEmpty()){
        if (!condition.await(waitTime, waitUnit)) throw IllegalStateException("fail to get object")
      }
      val obj = pool.first()
      pool.removeFirst()
      return obj
    }
  }
  
  override fun release(obj: T) {
    lock.withLock {
      if (pool.isEmpty()) {
        pause = true;
      }
      
      pool.add(obj)
      
      if (pause && pool.size > waitSize) {
        condition.signalAll()
        pause = false;
      }
    }
  }
  
  override fun newInstance(): T {
    return factory.newInstance()
  }
}

class EthereumWallet{
  /*
   something
  */
}

class EthereumWalletObjectFactory : ObjectFactory<EthereumWallet>{
  override fun newInstance(): EthereumWallet {
    return EthereumWallet()
  }
}

fun main() {
  println("object pool start")
  
  val pool = FixedObjectPool(EthereumWalletObjectFactory(), 4)
  println(pool.size())
  
  val obj1 = pool.take()
  println(obj1)
  val obj2 = pool.take()
  println(obj2)
  val obj3 = pool.take()
  println(obj3)
  
  val result = pool.use { it -> it.print() }
  println("use")
  println(result)
  
  val result2 = pool.useAsync { it -> CompletableFuture.supplyAsync{it.print()} }
  println("useAsync")
  println(result2.get())
  
  val obj5 = runCatching {
    pool.poll(1, TimeUnit.MILLISECONDS)
  }.onFailure {
    println("failed")
  }
  
  println("object pool end")
  
}

- 고정된 숫자를 가진 객체풀이다. 
- mutable list를 통해 객체가 관리되며, 쓰레드안전을 위해서 lock 과 condition을 직접 구현하였다. 
- use를 이용하여 take와 release를 신경 안 쓸수도 있다. 
- 눈여겨 봐야 할 부분은 2개가 있는데 첫째로 take의 while 문인데, 여러개의 쓰레드가 동시에 signal을 받을때, 뒤늦게 깨어난 쓰레드가 풀의 갯수를 확인해야 문제가 생기지 않는다. 
- 두번째로는 waitSize가 있는데, 이는 빈번한 wait/signal을 사용하지 않기 위해, 어느정도 일정 갯수의 객체가 풀안에 들어 왔을때만 동작하게 함으로써 성능을 개선 시킬 수 있다. 

2. DisruptorBlockingQueue 를 이용한 단순한 객체풀 

import com.conversantmedia.util.concurrent.DisruptorBlockingQueue
import java.util.concurrent.TimeUnit

interface ObjectPool<T> {
  fun take(): T
  fun release(obj: T)
  fun poll(waitTime: Long, waitUnit: TimeUnit): T
  fun newInstance(): T
}

interface ObjectFactory<T> {
  fun newInstance(): T
}

class FixedObjectPool<T>(factory: ObjectFactory<T>, size: Int): ObjectPool<T>{
  
  private val pool: DisruptorBlockingQueue<T>
  private val factory: ObjectFactory<T>
  
  init {
    this.factory = factory
    this.pool = DisruptorBlockingQueue<T>(size).apply {
      for (i in (1 .. size)) {
        add(newInstance())
      }
    }
  }
  
  fun size(): Int {
    return pool.size
  }
  
  override fun take(): T {
    return pool.take()
  }
  
  override fun release(obj: T) {
    pool.add(obj)
  }
  
  override fun newInstance(): T {
    return factory.newInstance()
  }
  
  override fun poll(waitTime: Long, waitUnit: TimeUnit): T {
    return pool.poll(waitTime,waitUnit)
  }
}


class EthereumWallet{
  /*
    something
  */
}

class EthereumWalletObjectFactory : ObjectFactory<EthereumWallet>{
  override fun newInstance(): EthereumWallet {
    return EthereumWallet()
  }
}


fun main() {
  val pool = FixedObjectPool(EthereumWalletObjectFactory(), 4)
  println(pool.size())
}

- LinkedBlockingQueue를 사용 할 수도 있지만... 
DisruptorBlockingQueue는 초고속 Concurrent 라이브러리를 이용한 것으로써 아래 링크를 참고한다.
https://github.com/conversant/disruptor


Coroutine

// THREAD 방식 

fun main() {
  
  val startTime = System.currentTimeMillis()
  val counter = AtomicInteger(0)
  val numberOfCoroutines = 100_00
  val jobs = List(numberOfCoroutines) {
    thread(start = true) {
      Thread.sleep(100L)
      counter.incrementAndGet()
    }
  }
  jobs.forEach { it.join() }

  val timeElaspsed = System.currentTimeMillis() - startTime
  println(timeElaspsed)
}
// Coroutine 방식

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.atomic.AtomicInteger
fun main() {
  val startTime = System.currentTimeMillis()
  runBlocking<Unit> {
    val counter = AtomicInteger(0)
    val numberOfCoroutines = 100_00

    val jobs = List(numberOfCoroutines) {
      launch {
        delay(100L)
        counter.incrementAndGet()
      }
    }
    jobs.forEach { it.join() }
  }

  println(System.currentTimeMillis() - startTime)
}

Thread를 통해서 작업을 하면 2130밀리초가 걸리는 작업을 Coroutine을 통해서하면 209 밀리초 밖에 걸리지 않는다. Coroutine은 경량쓰레드를 제공하기 때문에 가능한데, 이 말은 코루틴 수만개가 단 몇개의 쓰레드를 다시 나누어 사용 한다는 의미이다. 즉 코루틴은 놀고 있는 쓰레드에 부착되어 작동 하므로 시작되는 쓰레드와 종료 시점의 쓰레드가 달라질 수 있다. Go언어에서 제공되는 경량쓰레드(goroutine)와는 달리 내부적으로 자바 쓰레드풀을 사용하기 때문에, 진정한 경량쓰레드를 쓰냐에 관련된 말이 있지만, 어차피 Go언어도 자신만의 추상층을 가지고 있기 때문에 별 의미는 없어 보인다.

스택오버플로우에서 설명된 go goroutine vs kotlin Coroutine차이

주요 차이점에 대한 요약을 정리하자면 아래와 같다.

1. 코틀린 코루틴은 Go 고루틴보다 간단한 인스턴스당 더 적은 메모리를 필요로 한다. 코틀린에 있는 단순한 코루틴은 힙 메모리의 수십 바이트만 차지하고, 고 고루틴은 스택 공간의 4KiB로 시작한다. 말 그대로 수백만 개의 코루틴을 가질 계획이라면 코틀린의 코루틴은 Go 에 비해 우위를 점할 수 있습니다. 또한 코틀린 코루틴은 생성기 및 게으른 시퀀스와 같은 매우 짧고 작은 작업에 더 적합하게 만든다.

2. 코틀린 코루틴은 임의의 스택 깊이까지 갈 수 있지만, 함수를 일시 중단할 때마다 힙에 개체를 할당한다. 코틀린 코루틴의 호출 스택은 현재 힙 객체의 링크된 목록으로 구현되어 있다. 반대로 Go의 고루틴은 선형 스택 공간을 사용한다. 이것은 Go에서 딥 스택의 서스펜션을 더 효율적으로 만든다. 따라서, 당신이 쓰고 있는 코드가 매우 깊게 구성되어 있다면, 고루틴이 당신에게 더 효율적이라는 것을 알게 될 것이다.

3. 효율적인 비동기 IO는 매우 다차원적인 설계 문제이다. 한 종류의 애플리케이션에 효율적인 접근 방식은 다른 종류의 애플리케이션에 최상의 성능을 제공하지 못할 수 있다. 코틀린 코루틴의 모든 IO 연산은 코틀린이나 자바 언어로 작성된 라이브러리에 의해 구현된다. 코틀린 코드에서 사용할 수 있는 IO 라이브러리는 매우 다양하다. In Go에서 비동기 IO는 일반 Go 코드에서 사용할 수 없는 기본 요소를 사용하여 Go 런타임에 구현된다. IO 작업 구현에 대한 Go 접근 방식이 애플리케이션에 적합하다면 Go 런타임과의 긴밀한 통합이 이점을 제공할 수 있다. 한편, Kotlin에서는 라이브러리를 찾거나 애플리케이션에 가장 적합한 방식으로 비동기 IO를 구현하는 라이브러리를 직접 작성할 수 있다.

4. Go runtime은 물리적 OS 스레드에서 실행 일정을 완전히 제어한다. 이 접근 방식의 장점은 모든 것을 생각할 필요가 없다는 것이다. 코틀린 코루틴을 사용하면 코루틴의 실행 환경을 세밀하게 제어할 수 있다. 이는 오류가 발생하기 쉽다(예: 단순히 너무 많은 다른 스레드 풀을 만들고 이들 사이의 컨텍스트 전환에 CPU 시간을 낭비할 수 있다). 그러나 응용 프로그램에 대한 스레드 할당 및 컨텍스트 스위치를 미세 조정할 수 있다. 예를 들어 Kotlin에서는 단일 OS 스레드(또는 스레드 풀)에서 전체 응용 프로그램 또는 코드의 하위 집합을 실행하는 것이 쉬워서 적절한 코드를 작성하는 것만으로 OS 스레드 간에 컨텍스트를 완전히 전환하는 것을 피할 수 있다.

ps)
코틀린의 코루틴은 Golang의 코루틴과는 다른 방식으로 구현되기 때문에 어떤 것이 더 '빠른' 지는 푸는 문제와 작성하는 코드의 종류에 따라 달라진다.  즉 여러분이 당면한 문제에 대해 어떤 것이 더 잘 작동할지 미리 말하는 것은 매우 어렵다. 결국 특정 워크로드에 대한 벤치마크를 실행하여 이를 파악해야 한다. 

 

(* python의 코루틴에서 주로 사용하는 generator / send / yield 같은 기능은 없어 보인다. 다만 아래서 설명할 channel로 비슷하게 만들 순 있다.)

fun main() {
  runBlocking<Unit> {

    val time = measureTimeMillis {
      // given
      val one = async {
        delay(1000L)
      }
      val two = async {
        delay(2000L)
      }
      
      // when
      runBlocking {
        one.await()
        two.await()
      }
    }
    println(time) // 2013 mills
  }
}

launch와 async의 주요 차이점은 launch는 job을 리턴하고, async는 deffered를 리턴하는 것이다. 
job은 코루틴 자체를 의미하므로, launch로 실행되는 코루틴을 취소 할 수도 있고 기다릴 수도 있다. 즉 라이프 사이클에 관심이 있으며, deffered는 future처럼 미래의 결과 값을 의미하므로 완료 되길 기다리다가 리턴되는 값에 관심이 있다.
deffered는 job을 상속받으므로 job의 역할도 할 수 있다. 즉 non-blocking cancellable future 라 볼 수 있다.  

따라서 결과값에 관심이 있으면 async를 사용하고, 아니면 launch를 사용하면 된다. 


Channel 

개인적으로 golang을 매우 좋아하는데, 그 이유는 오로지 심플한 goroutine과 go channel의 존재에 있다. CSP기법의 하나인 이것은 동시성 프로그래밍을 매우 간단하고 직관적으로 만들어 준다. 세상은 단순한 기술이 승리하더라. 그리고 중요한건 재밌다는 사실!!  kotlin에도 go channel식으로 개발하는 것을 지원해 주니 사용 안 할 이유가 없다. 

https://www.baeldung.com/kotlin/channels 에서 코드를 가져왔다. 별다른 설명이 필요 없을 정도로 코드가 깔끔하다.

import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

internal class ChannelTest {
  
  @Test
  fun `should_pass_data_from_one_coroutine_to_another`(){
    runBlocking {
      // given
      val channel = Channel<String>()
      
      // when
      launch { // coroutine1
        channel.send("Hello World!")
      }
      val result = async { // coroutine2
        channel.receive()
      }
      // then
      assertEquals(result.await(),"Hello World!")
    }
    
  }
}

하나의 코루틴에서 다른 코루틴으로 데이터를 전송하고 받는 기본적인 코드이다. 

import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() {
  runBlocking {
    // given
    val channel = Channel<Channel<String>>()
    val eventChannel = Channel<String>()
    
    // when
    launch { // coroutine1
      channel.send(eventChannel)
      print(eventChannel.receive())
    }
    launch { // coroutine2
      val eventChannel = channel.receive()
      eventChannel.send("hi there")
    }
  }
}

go channel에서는 채널을 채널에 전송 할 수 있어서, 해 보았는데 코틀린도 잘 된다.

아래는 Pub-Sub패턴의 코드이다. 

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun CoroutineScope.producePizzaOrders(): ReceiveChannel<String> = produce {
  var x = 1
  while (true) {
    send("Pizza Order No. ${x++}")
    delay(100)
  }
}

fun CoroutineScope.pizzaOrderProcessor(id: Int, orders: ReceiveChannel<String>) = launch {
  for (order in orders) {
    println("Processor #$id is processing $order")
  }
}

fun main() = runBlocking {
  val pizzaOrders = producePizzaOrders()
  repeat(3) {
    pizzaOrderProcessor(it + 1, pizzaOrders)
  }
  
  delay(1000)
  pizzaOrders.cancel()
}

produce는 ReceiveChannel<T>을 리턴하는 코루틴이다. (async는 deffered, launch는 job)

마지막으로 채널을 파이프라이닝으로 연결 할 수도 있다.
병렬로 파이프라이닝/체이닝/필터링 패턴을 적용 할 때 좋을 거 같다. 

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.produce

fun CoroutineScope.baking(orders: ReceiveChannel<PizzaOrder>) = produce {
  for (order in orders) {
    delay(200)
    println("Baking ${order.orderNumber}")
    send(order.copy(orderStatus = BAKED))
  }
}

fun CoroutineScope.topping(orders: ReceiveChannel<PizzaOrder>) = produce {
  for (order in orders) {
    delay(50)
    println("Topping ${order.orderNumber}")
    send(order.copy(orderStatus = TOPPED))
  }
}

fun CoroutineScope.produceOrders(count: Int) = produce {
  repeat(count) {
    delay(50)
    send(PizzaOrder(orderNumber = it + 1))
  }
}

fun main() = runBlocking {
  val orders = produceOrders(3)
  
  val readyOrders = topping(baking(orders))
  
  for (order in readyOrders) {
    println("Serving ${order.orderNumber}")
  }
  
  delay(3000)
  coroutineContext.cancelChildren()
}


더 제대로된 코틀린 채널 예제와 설명은 아래 링크를 참고하자.
https://proandroiddev.com/kotlin-coroutines-channels-csp-android-db441400965f

아무튼 Golang 사용하다가 코틀린 코루틴 스터디해보면 정말 지져분해서 사용하기 싫다는 기분이 많이 든다...
언어 자체 제공 vs 라이브러리인것을 훨씬 넘어서는 복잡함
비록 kotlinx.coroutines은 리치라이브러리로써 많은 기능을 제공해 주고 있긴 하지만...
빌트인 기능을 좀 더 깔끔하게 만들 순 없었을까?  코틀린 코루틴 라이브러리 vs 빌트인 

+ Recent posts