Rust vs Kotlin vs Java

Rust의 컴파일 타임 vs Kotlin/Java의 런타임

특징 Rust (async / Future)  Kotlin Coroutines  Java Virtual Threads
비동기 실행 모델 Future 기반 (poll-driven) Suspend 함수 기반 (Continuation) 사용자 모드 스레드 기반 (Thread API)
상태 관리 컴파일 타임에 상태 기계로 변환 런타임에서 상태를 관리 런타임에서 사용자 모드 스레드 관리
최적화 컴파일 타임 최적화 (zero-cost abstraction) 일부 컴파일 타임, 주로 런타임 최적화 주로 런타임 최적화
런타임 필요성 런타임 스케줄러 필요 (Tokio, async-std 등) 코루틴 디스패처 필요 Java 런타임 자체에서 관리
제어권 개발자에 의해 언어&라이브러리 차원의 키워드로 명시적 으로 제어권 반환해야함  개발자에 의해 언어&라이브러리 차원의 키워드로 명시적으로 제어권 반환 해야함  명시적 제어권 반환하는 키워드가 새로 생긴건 없으며, 기존 자바 동기 코드를 사용해도 내부에서 알아서 비동기 처리 
난이도 중간  중간  쉬움 & 세밀한 제어 및 부모-자식관계 관리등 안됨 

 

Rust의 컴파일 타임

Rust의 비동기 프로그래밍 모델에서는 컴파일 타임에 많은 작업을 수행하여 효율적인 비동기 실행을 지원합니다. Rust의 비동기 실행 모델은 async/await 키워드와 Future 트레이트를 기반으로 하며, 다른 언어들에 비해 컴파일 타임 최적화가 더 강력한 특징을 가지고 있습니다.

Rust의 async와 Future 트레이트

  • Rust에서 **async fn**은 **컴파일 타임에 상태 기계(state machine)**로 변환됩니다.
  • async fn이 호출되면 미리 생성된 상태 기계 객체가 반환되며, 이 객체는 Future 트레이트를 구현합니다.
  • Future는 poll 기반의 실행 모델로, 런타임에서 필요한 시점에 실행됩니다.

Rust의 강점: 컴파일 타임에 많은 것을 결정

  • Rust는 async fn을 Future 객체로 변환하여, 비동기 함수의 모든 상태를 컴파일 타임에 정의합니다.
  • 이는 런타임 오버헤드를 줄이고, 높은 성능과 안전성을 보장합니다.
  • 비동기 코드의 런타임 비용을 최소화하기 위해 설계되었으며, 이 점에서 Kotlin이나 Java보다 더 낮은 레벨의 최적화를 제공합니다.

사실 위의 말 가지고는 이해 할 수 가 없습니다.  세상은 좋은 선생님들 덕분에 돌아갑니다. :-) 
아래에 직접 구현된 예시가 있습니다. (컴파일 타임에 코루틴이 구현되는 방식 - 예제 코드 솔직히 킹왕짱.) 
https://github.com/cfsamson/Asynchronous-Programming-in-Rust/tree/main/ch08 

 

Asynchronous-Programming-in-Rust/ch07 at main · cfsamson/Asynchronous-Programming-in-Rust

Asynchronous Programming in Rust, published by Packt - cfsamson/Asynchronous-Programming-in-Rust

github.com

핵심코드만 발췌)

// coroutine fn async_main() {
//     println!("Program starting");
//     let txt = http::Http::get("/600/HelloAsyncAwait").wait;
//     println!("{txt}");
//     let txt = http::Http::get("/400/HelloAsyncAwait").wait;
//     println!("{txt}");

// }


// 위의 코드가 아래 코드로 컴파일 시점에 변경 // 

// 설명) 상태를 가진 코루틴 객체가 첫번째 wait만나기 전까지 상태를 저장하고, 자식 비동기 호출하고 
// 이런식으로 함수가 특정 위치부터 이전 상태를 기반으로 재실행됨 


fn async_main() -> impl Future<Output=String> {
    Coroutine0::new()
}

enum State0 {
    Start,
    Wait1(Box<dyn Future<Output = String>>),
    Wait2(Box<dyn Future<Output = String>>),
    Resolved,
}

struct Coroutine0 {
    state: State0,
}

impl Coroutine0 {
    fn new() -> Self {
        Self { state: State0::Start }
    }
}


impl Future for Coroutine0 {
    type Output = String;

    fn poll(&mut self, waker: &Waker) -> PollState<Self::Output> {
        loop {
        match self.state {
                State0::Start => {
                    // ---- Code you actually wrote ----
                    println!("Program starting");

                    // ---------------------------------
                    let fut1 = Box::new( http::Http::get("/600/HelloAsyncAwait"));
                    self.state = State0::Wait1(fut1);
                }

                State0::Wait1(ref mut f1) => {
                    match f1.poll(waker) {
                        PollState::Ready(txt) => {
                            // ---- Code you actually wrote ----
                            println!("{txt}");

                            // ---------------------------------
                            let fut2 = Box::new( http::Http::get("/400/HelloAsyncAwait"));
                            self.state = State0::Wait2(fut2);
                        }
                        PollState::NotReady => break PollState::NotReady,
                    }
                }

                State0::Wait2(ref mut f2) => {
                    match f2.poll(waker) {
                        PollState::Ready(txt) => {
                            // ---- Code you actually wrote ----
                            println!("{txt}");

                            // ---------------------------------
                            self.state = State0::Resolved;
                            break PollState::Ready(String::new());
                        }
                        PollState::NotReady => break PollState::NotReady,
                    }
                }

                State0::Resolved => panic!("Polled a resolved future")
            }
        }
    }
}



Kotlin/Java의 런타임

코틀린 코루틴의 협력적 스케줄링 (Cooperative Scheduling)

특징:

  • 스레드나 작업이 자발적으로 제어권을 넘겨주는 방식으로 동작합니다.
  • 작업 중간에 명시적으로 제어권을 반환하는 지점 (yield, await, suspend 등)을 코드에 삽입해야 합니다.
  • 런타임 스케줄러는 이 반환 지점에서 다른 작업으로 전환합니다.

장점:

  • 컨텍스트 스위칭 비용이 매우 낮음: 협력적 스케줄링은 커널의 간섭 없이 사용자 공간에서 이루어지므로 매우 빠름.
  • 개발자가 작업 흐름을 더 세밀하게 제어할 수 있음.
  • 대량의 경량 작업을 실행할 때 효율적.

단점:

  • 비협력적인 코드가 있다면 문제가 발생: 작업이 제어권을 반환하지 않으면, 다른 작업이 실행되지 못하고 시스템이 멈출 수 있음.
  • 협력적 스케줄링은 블로킹 작업을 자체적으로 처리하지 못하므로, I/O 작업 등을 비동기로 처리해야 함.

동작 방식

  1. 협력적 스케줄러가 관리하는 작업은 사용자 공간에서 실행됩니다.
  2. 각 작업은 명시적으로 제어권을 반환해야 다른 작업이 실행될 수 있습니다.
  3. 모든 작업은 동일한 스레드에서 실행될 수 있습니다 (멀티스레드 환경도 가능).
import kotlinx.coroutines.*
import kotlin.random.Random

suspend fun fetchData(id: Int): String {
    delay(Random.nextLong(100, 500)) // 비동기 대기 (명시적 제어권 반환)
    return "Data for request $id"
}

fun main() = runBlocking {
    val jobs = List(100) { id -> // 100개의 코루틴 생성
        launch {
            val data = fetchData(id)
            println("Coroutine $id: $data on thread ${Thread.currentThread().name}")
        }
    }

    jobs.forEach { it.join() } // 모든 작업이 끝날 때까지 대기
}

delay() 호출 시 제어권을 반환해 다른 작업이 실행됩니다.

 

Java Virtual Thread - OS 커널 스레드와 분리된 사용자 모드 스레드 (User-mode Threads)

특징:

  • 사용자 모드 스레드는 OS 커널 스레드와 독립적으로 관리됩니다.
  • Java Virtual Threads와 같이 사용자 공간에서 구현된 가벼운 스레드입니다.
  • 작업 실행은 OS의 스케줄러가 아닌 사용자 모드 스케줄러에서 관리됩니다.

장점:

  • OS 커널 스레드의 제한 없이 대량의 사용자 모드 스레드를 실행할 수 있음.
  • 스레드 생성과 스위칭이 커널 스레드보다 훨씬 빠름.
  • 기존의 동기 프로그래밍 모델(블로킹 호출 포함)을 유지하면서 비효율을 제거 가능.

단점:

  • I/O 작업이나 블로킹 호출은 내부적으로 비동기로 처리되어야 함.
  • 커널 스레드에 비해 구현이 복잡할 수 있음.

동작 방식

  1. 사용자 모드 스케줄러는 사용자 모드 스레드를 관리하며, 이 스레드는 필요 시 커널 스레드에서 실행됩니다.
  2. 사용자 모드 스레드가 실행을 중단하면(예: I/O 대기) 커널 스레드가 다른 사용자 모드 스레드를 실행합니다.
  3. 이 과정은 투명하게 이루어지며, 기존 동기 코드를 그대로 사용할 수 있습니다.

Java의 Virtual Threads는 사용자 모드 스레드입니다.

  • **Thread.startVirtualThread**를 통해 가벼운 사용자 모드 스레드를 생성합니다.
  • 내부적으로 스케줄러가 I/O 대기 및 실행을 관리하며, OS 스레드가 비효율적으로 차단되지 않습니다.
import java.util.concurrent.*;
import java.util.Random;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        Random random = new Random();

        for (int i = 0; i < 100; i++) { // 100개의 Virtual Threads 생성
            int id = i;
            executor.submit(() -> {
                try {
                    Thread.sleep(random.nextInt(400) + 100); // 블로킹 호출
                    System.out.println("Virtual Thread " + id + ": Data fetched on thread " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES); // 모든 작업이 끝날 때까지 대기
    }
}

 

협력적 스케줄링과 사용자 모드 스레드의 차이

특징협력적 스케줄링사용자 모드 스레드

작업 전환 방식 작업이 명시적으로 제어권을 반환해야 함 사용자 모드 스케줄러가 작업 전환을 관리
스케줄링 비선점형 (Non-preemptive) 선점형 (Preemptive)
I/O 처리 비동기 I/O와 결합하여 동작 내부적으로 비동기 I/O 처리, 블로킹 호출과 호환
스레드 관리 단일 OS 스레드에서 실행 가능 여러 사용자 모드 스레드가 OS 스레드에서 매핑되어 실행
사용 예시 Kotlin 코루틴, Python async/await Java Virtual Threads, Go 언어의 goroutine
성능 컨텍스트 스위칭 비용이 낮음 더 많은 스레드를 처리할 수 있지만 스위칭 비용이 더 높음
코드 스타일 비동기 코드 (async/await, suspend) 동기 스타일과 비동기 스타일 모두 지원

Rust Future와 다른 모델 비교

  Rust Future Kotlin Coroutine Java Virtual Threads  Go Goroutine
스케줄링 방식 협력적 스케줄링 협력적 스케줄링 선점형 스케줄링 선점형 스케줄링
제어권 반환 명시적 (Poll::Pending) 명시적 (suspend, yield) 자동 자동
실행 트리거 Lazy (스케줄러 필요) Lazy (런타임 필요) Eager (즉시 실행 가능) Eager (즉시 실행 가능)
I/O 처리 방식 논블로킹 논블로킹 동기 호출도 비동기로 처리 가능 논블로킹
스케줄러 제어 가능성 개발자가 직접 제어 (poll 호출) Dispatchers로 스케줄링 제어 JVM 스케줄러 자동 관리 Go의 GMP 스케줄러 자동 관리
사용 스타일 비동기 (async/await, poll) 비동기 (async/await) 동기 (Thread API와 동일) 동기 (go 키워드)

별책부록1)

Java Virtual ThreadsKotlin Coroutines 모두 결국 JVM 스레드 풀(예: ForkJoinPool) 위에서 실행됩니다.
둘 모두 OS Thread를 직접 사용 할 때 이루어지는 블로킹 및  컨텍스트 스위칭을 줄여서 동시성 처리 효율을 극대화 하는 방향으로 설계됩니다. Java Virtual Threads 를 사용하면 학습곡선이 낮아집니다. 명시적인 동시성 처리를 안하니깐~
대신 디테일한 조정을 못하게 되겠지요. Go언어의 Goroutine생각하면 Kotlin Coroutine보다 Java Virtual Threads가 훨씬 잘 쓰일듯. (한데.. 코프링이 워낙 좋아서 또.. 몰러)

좀 편하게 살자 !!!    (https://tech.kakaopay.com/post/ro-spring-virtual-thread/
)


JVM과 OS 스레드 관계

  • JVM은 기본적으로 OS 스레드를 기반으로 동작합니다.
  • Java에서의 **스레드(Thread)**는 OS 커널 스레드에 직접 매핑됩니다.
  • 따라서 Java의 스레드 API를 통해 생성한 스레드는 JVM에서 OS 스레드로 매핑되어 커널 스케줄러에 의해 관리됩니다.


별책부록2)

Kotlin Coroutines에서는 간단히 처리할 수 있지만 Java Virtual Threads로는 더 많은 코드와 복잡한 제어가 필요한 경우의 예제

시나리오 1: 부모-자식 관계로 작업 그룹 관리

  1. 문제:
    • 100개의 작업을 병렬로 실행하지만, 특정 조건에서 중간에 멈춰야 합니다(예: 하나의 작업이 실패하면 전체 작업을 취소).
    • 모든 작업이 끝난 후, 결과를 종합해서 처리해야 합니다.
  2. 목표:
    • 작업의 그룹 단위 관리 (부모-자식 관계).
    • 작업 중 하나라도 실패하면 나머지 작업을 취소.
    • 모든 작업이 성공하면 결과를 합산.

시나리오 2: 병렬 작업에서 타임아웃과 취소 관리

  1. 문제:
    • 100개의 작업을 병렬로 실행하며, 각각 최대 300ms 안에 완료되어야 합니다.
    • 작업이 실패하거나 타임아웃이 발생하면, 해당 작업을 취소하고 나머지 작업은 계속 진행합니다.
    • 결과를 모두 모아 성공적으로 완료된 작업만 처리합니다.

Kotlin Coroutines 구현

Kotlin 코루틴은 **구조화된 동시성(Structured Concurrency)**를 제공하므로, 부모-자식 관계를 자연스럽게 관리할 수 있습니다. coroutineScope나 supervisorScope를 사용해 간단히 구현 가능합니다.

//// 시나리오 1

import kotlinx.coroutines.*
import kotlin.random.Random

suspend fun fetchData(id: Int): Int {
    delay(Random.nextLong(100, 500)) // 비동기 대기
    if (Random.nextBoolean()) throw RuntimeException("Error in task $id") // 실패 가능성
    return id * 10
}

fun main() = runBlocking {
    try {
        val results = coroutineScope { // 부모 코루틴 스코프
            (1..100).map { id ->
                async { fetchData(id) } // 자식 코루틴 실행
            }.awaitAll() // 모든 결과를 기다림
        }
        println("All tasks completed successfully: ${results.sum()}")
    } catch (e: Exception) {
        println("Failed: ${e.message}")
    }
}


//// 시나리오 2


import kotlinx.coroutines.*
import kotlin.random.Random

suspend fun fetchData(id: Int): Int {
    delay(Random.nextLong(100, 500)) // 비동기 대기
    if (Random.nextBoolean()) throw RuntimeException("Error in task $id") // 실패 가능성
    return id * 10
}

fun main() = runBlocking {
    val results = (1..100).map { id ->
        async {
            try {
                withTimeout(300) { fetchData(id) } // 300ms 안에 완료되지 않으면 취소
            } catch (e: Exception) {
                null // 실패하거나 타임아웃 발생 시 null 반환
            }
        }
    }.awaitAll()

    val successfulResults = results.filterNotNull()
    println("Successful results: ${successfulResults.sum()}")
}

Kotlin 코루틴 동작 방식:

  1. **coroutineScope**는 부모 코루틴으로, 자식 코루틴들의 생명 주기를 관리합니다.
  2. 작업 중 하나라도 실패하면, 나머지 작업을 자동으로 취소합니다.
  3. 결과를 awaitAll()로 간단히 모아서 처리합니다.

Java Virtual Threads 구현

Java Virtual Threads는 구조화된 동시성을 기본적으로 제공하지 않으므로, 비슷한 기능을 구현하려면 더 많은 코드와 수작업이 필요합니다

/// 시나리오 1

import java.util.concurrent.*;
import java.util.*;
import java.util.stream.*;
import java.util.Random;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        List<Future<Integer>> futures = new ArrayList<>();
        Random random = new Random();

        // 부모 작업: 결과 수집
        try {
            for (int i = 1; i <= 100; i++) {
                final int id = i;
                futures.add(executor.submit(() -> {
                    try {
                        Thread.sleep(random.nextInt(400) + 100); // 블로킹 대기
                        if (random.nextBoolean()) throw new RuntimeException("Error in task " + id); // 실패 가능성
                        return id * 10;
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }));
            }

            // 결과 처리
            int sum = 0;
            for (Future<Integer> future : futures) {
                sum += future.get(); // 작업 완료 대기 및 결과 수집
            }
            System.out.println("All tasks completed successfully: " + sum);
        } catch (ExecutionException e) {
            System.out.println("Failed: " + e.getCause().getMessage());

            // 자식 작업 취소
            for (Future<Integer> future : futures) {
                future.cancel(true);
            }
        } finally {
            executor.shutdown();
            executor.awaitTermination(1, TimeUnit.MINUTES);
        }
    }
}

/// 시나리오 2

import java.util.concurrent.*;
import java.util.*;
import java.util.stream.*;
import java.util.Random;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
        Random random = new Random();
        List<Future<Integer>> futures = new ArrayList<>();

        for (int i = 1; i <= 100; i++) {
            final int id = i;
            futures.add(executor.submit(() -> {
                try {
                    Thread.sleep(random.nextInt(400) + 100); // 블로킹 대기
                    if (random.nextBoolean()) throw new RuntimeException("Error in task " + id);
                    return id * 10;
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }));
        }

        List<Integer> results = new ArrayList<>();
        for (Future<Integer> future : futures) {
            try {
                results.add(future.get(300, TimeUnit.MILLISECONDS)); // 300ms 타임아웃 설정
            } catch (TimeoutException | ExecutionException e) {
                future.cancel(true); // 타임아웃 시 작업 취소
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }

        System.out.println("Successful results: " + results.stream().mapToInt(Integer::intValue).sum());
        executor.shutdown();
    }
}

Java Virtual Threads 동작 방식:

  1. Virtual Threads는 ExecutorService에서 관리되며, 각 작업은 Future 객체로 반환됩니다.
  2. 작업 실패 시 예외를 처리하려면 추가적인 로직(try-catch와 future.cancel())이 필요합니다.
  3. 구조화된 동시성이 제공되지 않으므로, 부모-자식 관계를 직접 관리해야 합니다.

고품질의 코드란 무엇일까?  예외처리가 잘 된 ? 주석이 잘 달린? 성능,메모리 최적화된? 변수명이 좋은? 
다 옳은 말이다. 추가적으로 관련 프레임워크/라이브러리의 이해가 잘된 코드, 도메인을 잘 이해한 코드도 물론이고..

이 글에서 고품질의 코드란 새로운 기능을 구현 할 때 최대한 기존 구현된 코드는 건드리지 않는 구조라고 정하고
그것에 대한 이야기를 써 본다.

이 글은 아래에  나오는 지식 기반을 엮어서 설명한다. 

  • Ralph E. Johnson, Martin Fowler et al. DI,DIP,IoC(Dependency Injection etc) 
  • 마틴파울러의 리팩토링 - Refactor Conditional To Polymorphism
  • 로버트 C. 마틴 클린코드 -  Data/Object Anti-Symmetry
  • 버틀란트메이어 객체지향 소프트웨어 설계 - Open/Closed Principle
  • Gof의 디자인패턴 - Visitor Pattern (사실 Visitor말고도 수 많은 패턴들이 해당된다)
  • 더블 디스패칭 - Visitor패턴에서 엘리먼트(데이터)가 하나이고 Visitor (행위자)가 여럿일 경우 비슷 
  • Duncan McGregor, Nat Pryce의 Java to Kotlin - Open to Sealed Classes

위에 나오는 법칙들은 모두 한가지를 가르키는데 변경은 좁은/분리된 범위에서 하게 하자이다.

먼저 리펙토링(Refactor Conditional To Polymorphism)은 if 문의 중첩으로 코딩된 코드를 다형성으로 처리하여 if 문을 밑으로 더 길게 수정하는 행위보다는 해당 부분은 전혀 건드리지 말고, 다른 파일에 새로운 타입의 구현을 작성하기게 만들자라는 것이고, 그 방식을 다형성의 힘을 이용했다.  Data/Object Anti-Symmetry는 다형성을 이용해서 구현을 하게 되면 생기는 문제도 있음을 지적한다. 우리가 개발을 하다보면 동종의 타입들이 늘어나는 경우도 있고, 행위가 늘어나는 경우가 있다.예를들어 Shap(도형) 이라면 draw라는 작동방식은 한정되어 있지만 Circle, Rectange,Star,Square,Ellipse 등 타입은 계속 늘어날 가능성이 높은 경우가 있고, 반대로 타입은 남자,여자로 거의 정해졌지만 행위는 먹다,자다,놀다,걷다 등으로 무한정 늘어날 가능성이 높은 경우가 있을 것이다. 이때 후자의 경우 다형성 기반으로 코딩을 한다면 행동하나가 늘어 날 때 마다 모든 서브클래스의 파일을 찾아서 수정해 주어야하는 고생을 해야 할 것이라서, 그냥 타입만 구분하고 그것에 강결합되 있는 인터페이스의 동작을 작성하지 말고, 파일하나에 행위 하나를 작성한다면 새로운 행위가 추가되더라도 기존 파일에는 전혀 영향이 없이 새로운 파일을 만들어서 추가하면 될 것이다. 이런 것들은 OCP와도 일맥상충하는데 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다는 의미입니다.

이제 말보단 코드로 봅시다. 코틀린의 sealed class를 사용합니다.

sealed interface Error

class FileReadError(val file: String): Error
class DatabaseError(val source: String): Error
class RuntimeError : Error

위와 같은 코드가 있다고 합시다. Error라는 인터페이스를 구현하는 타입이 3가지가 있습니다.
만약 Error 라는 타입의 가지고 있는 행위는 log()만 있을 확률이 높고, Error 하위 타입이 늘어날 가능성이 높은 어플리케이션을 님이 만들고 있다면 이것은 다형성을 이용하는게 좋을 것입니다.

sealed interface Error {
    fun log()
}

class FileReadError(val file: String): Error {
    override fun log() { println("Error while reading file $file") }
}
class DatabaseError(val source: String): Error {
    override fun log() { println("Error while reading from database $source") }
}
class RuntimeError : Error {
    override fun log() { println("Runtime error") }
}

위의 클래스들이 모두 다른 파일에 작성되어 있다면, 기존 파일은 전혀 신경 쓰지 않고 다른 파일 하나 더 만들면 됩니다.
하지만 log 말고 다른 메소드들이 계속 생긴다면? 그 때 마다 다른 파일들 모두 찾아야 겠지요.

이젠 타입은 정해져있고 log()라는 행위 말고, 다른 행위들이 생긴다고 생각해 봅시다.

fun log(e: Error) = when(e) {
    is FileReadError -> { println("Error while reading file ${e.file}") }
    is DatabaseError -> { println("Error while reading from database ${e.source}") }
    is RuntimeError ->  { println("Runtime error") }
}

이때는 이런식으로 파일 하나에는 하나의 행위만 들어가기 때문에 , 다른 행위를 위해 다른 파일에서만 추가하면  됩니다.

이번에는 Visitor 패턴이며  이 패턴 역시 OCP에 관련된 기존 구현을 건드리지 않으면서 행위를 수행합니다.
예를 봅시다.

class Itninerary (
    val id : String,
    val route : Route,
    val accomodations: List<Accomodation> = emptyList()
        ){
}


대략 이런 여행계획서 엔터티가 있다고 합시다. 계획서 안에는 어디를 방문하는지에 관한 Route와 숙소들의 리스트가 있습니다. 해당 여행 계획에 대한 경비를 산출해주는 로직을 만든다고 할 경우 가장 먼저 떠오르는 것은 Route와 Accomodation의 내용을 읽어서 더해주면 될 거 같습니다. 

class CostSummaryCalculator {

    fun calc(itninerary: Itninerary): CostSummary {

        var total : BigDecimal = BigDecimal.ZERO
        
        itninerary.accomodations.forEach {
            total.add(it.cost.amount)
        }

        itninerary.route.paths.forEach{
            total.add(it.cost.amount)
        }
        
        // ...

        return CostSummary(Money.of(total, Currency.WON))
    }
}

그런데 이런식으로 구현 하게 되면 여행 계획에 무엇인가 행위 (숙박,교통요금,관광..) 가 추가 될 때 마다 이 기존 calc 함수는 변경되야 할 것입니다. 그럼 기존 코드는 안건드리고 새로운 코드만 추가해서 해결 하려면 어떻게 해야 할 까요?

interface CostCalc {
    fun addCostsTo(calc: CostSummaryCalculator);
}
interface ItnieraryItem : CostCalc

data class Path(val cost : Money) :  ItnieraryItem {
    override fun addCostsTo(calc: CostSummaryCalculator) {
        calc.addCost(cost)   
    }
}
class Route :  ItnieraryItem{
    val paths = mutableListOf<Path>()
    override fun addCostsTo(calc: CostSummaryCalculator) {
        paths.forEach{
            it.addCostsTo(calc)
        }
    }

    fun addPath(path: Path) {
        paths.add(path)
    }

}

class Accomodation(val cost : Money) : ItnieraryItem{

    override fun addCostsTo(calc: CostSummaryCalculator) {
        calc.addCost(cost) 
    }

}

class Itninerary  (
    val id : String,
    val items: List<ItnieraryItem> = emptyList()
        ){

    fun addCostTo(calc: CostSummaryCalculator){
        items.forEach {
            it -> it.addCostsTo(calc)
        }
    }
}


data class CostSummary (val cost: Money)

class CostSummaryCalculator {

    val list = mutableListOf<Money>()
    fun addCost(money: Money) {
        list.add(money)
    }

    fun getTotal() : BigDecimal {
        return list.sumOf { it.amount }
    }
}


이렇게 하게 되면 새로운 여행아이템이 등장해도 해당 아이템에만 addCost 메소드를 추가 구현 해 주면 됩니다. Visitor패턴의 accept 가 addCostTo가 될 것이고, Visitor 는 CostSummaryCalulator이며 visit 에는 Money가 들어가네요.
(Visitor패턴이 모든곳에 좋은 건 아니며 위의 코드도 부실합니다. 설명하고자하는 것에 집중된 단순 예제 일 뿐) 



기존의 언어를 가지고 무엇인가 만들다 보면 그 코드의 쓰임새에 있어서 패턴/정형화가 된다.
그것을 구체적으로 이름 붙히고 규율을 정해 놓으면 사람들이 이해하기 더 쉬워 지는건 인지상정.

예를들어 고양이과 동물을 다 고양이라고 하고, 호랑이에 대해서는 메우 큰 고양이, 사자에 대해서는 목에 털이 많은 고양이라고 말을 해도 알아먹기야 하겠지만, 고양이과를 사자,호랑이로 명확히 나누어서 소통하면 더 빨리 이해 할 수 있게 될 것고 효율적이게 될것이다. 프로그래밍 언어도 마찬가지이다.

새로운 언어는 보통 이전 언어/ 경쟁 대상 언어보다 더 키워드가 많아 지게 되는데 예를들어
C++에는 없던 interface 키워드가 자바에서는 생겼는데  C++에는 비록 interface가 없더라도 class가 그 역할을 겸하였지만, 자바에서는 Interface라고 키워드를 추가하였기 때문에 자바 개발자들은 인터페이스를 적용하는면에 있어서 더 나아졌다. 즉 처음 자바로 프로그래밍을 공부한 사람은 그냥 그게 너무 당연한 Class와 구분된 기능인 것이다. 

마찬가지로 보통 자바 개발자들은 class에 대해 보통 한가지를 떠올리겠지만. 자바 class를 많이 사용한 자바구루들은 어떤 정형화 된 패턴들을 발견 할 수 있었을 것이다. 이런식으로 사용되는 클래스 저런 제한이 있는 클래스.. 

코틀린에서는 신생언어 답게 이런 것들을 세분화 시켜서 키워드를 추가하였다. 
그런 덕분에 코틀린을 공부한 사람은 자연스럽게 Class를 더 품격(?) 높게 구분해서 사용 할 수 있는 힘을 갖게 되었다. 

data class, value class, sealed class, enum class 등이 바로 이런 것들이다. 






 

이 말에 대한 비트겐슈타인의 진정한 함의는 모르겠지만,  문장 자체를 그대로 생각해 보면 소프트웨어 개발과 정말 잘 맞는 듯 하다. 이에 따라서 개발자는 

- 영어를 잘 아는게 중요하다?
- 글쓰기를 잘 하는게 중요하다?
- 패턴을 잘 아는게 중요하다?


개발에 추상화란 매우 중요한 요소이다. 소프트웨어 개발에 있어서 추상화를 하기 위해 사용되는 단어들은 생각보다 어려운 단어들이 아니다. Convert, Proxy, Execute, Provider, Factory, Map, Command, Future, Journal, Match, Invocate, Filter, Channel등 근데 막상 개발 할 때 저런 중학생용 영어를 사용하는데 어려움을 겪는다.

즉 개발에 있어서 영어를 원어민처럼 하는거와는 상관 없어 보인다. 언어의 한계가 의미하는 바는 단어를 많이 아는지 와는 크게 상관없고, 흔한 그 단어들이 얼마나 나의 머리속에 잘 추상화되어 자리잡혀있는지가 중요해보인다. 물론 다양한 어휘를 적재적소에 잘 사용 했던 사람이 코딩을 함에 있어서 유리할 것이란 것은 자명하다. 소프트웨어 개발은 공학이기도 하지만 문학이기도 하니까...

글쓰기를 잘 하는 건 그런 추상화를 잘 하게 해주는데 큰 도움을 줄 것이다.
만약 당신이 구체적인 것들을 지칭하는 것에 대해서 공통적인  무엇을 뽑아내야 할 때 사용할 만한 단어를 생각해 내는 것은 
먼저 공통된 무엇인가를 뽑아낼 감각과 의지가 있느냐와 동시에 어떻게 그것을 구체화 할 것이냐에 대한 기능적인 지식이 있어야 할 것이다. 

예를들어 각 OS별로 버튼을 그리는 구체적인 방식은 다를지라도 공통적으로 draw 라든지 paint라는 메소드를 사용 할 것이며, 다양한 블록체인에 접근하기 위한 공통메소드로는 connect 라든지 sendTransaction 따위가 있을 것인데, 그것들의 구체적인 구현체를 선택할 때 사용 할 수 있는 인터페이스의 단어로는 Provider, Factory 등을 사용 할 수 있을 것 같다.


Factory.of("Metamask") ,  Factory.of("Windows") 라든지
Provider.of("Metamask"), Provider.of("Windows") 식으로 사용 할 수 있을 거란 말이다.

근데 이런 단어들을  떠올리게 하고 영감을 주는데 있어 도움을 주는 것은 평소에 읽기,쓰기를 많이 해본 인문학적 수련이나  수 많은 오픈소스를 읽는 것이겠지만 선현들이 만들어 놓은 잘 알려진 패턴을 학습하는게 가장 효율적이지 않을까?  

                                                        (이미지 출처 : https://www.bitdeal.net/)


블록체인 업계에 대해서 - 블록체인 전직 혹은 블록체인 대학원/국비학원/각종교육으로 고민중인 분들이
최근 많이 있는 것 같아서 대략 정리 해봅니다. 

먼저 

블록체인 업계가 앞으로 어떻게 될 지는 아무도 모릅니다.
다만 블록체인 관련한 업계는 최소 10년이상은 건재 할 거 같습니다. 
저는 "블록체인"이 탈중앙 디지털 자산의 기반으로 미래에 핵심적인 인프라로 남기를 희망합니다. 
(요 문장에서 만큼은 블록체인==이더리움을 말합니다) 

블록체인은 "스포츠" 와 같습니다. 즉 "스포츠" 전문가란 세상에 없습니다. 
블록체인에는 수 많은 한 우물을 파야 하는 분야가 있습니다. 그 한 우물을 파는 것에 대해서 우려가 됩니다.
현재 상황에선 물이 안나올 가능성이 매우 크거든요. 

일단 요약 부터 하자면 (범) 블록체인 업계는 크게 아래와 같은 5가지로써 바라보고 있습니다.

0. 블록체인 코어 개발 (극소수TO) 
1. 대중들 대상 -  블록체인을 화폐(코인)로 보고 보관/교환/투자/투기 하는 것에 대해 집중합니다.

2. 엔터프라이즈(기업) 대상 - 블록체인을 신뢰성 있는 분산DB로써의 역할에 주목합니다.
3. 블록체인의 힘을 빌릴 수도 있고 아닐 수도 있는 응용분야들 (예: SSI 신원인증, 금융권에서 시도중인 STO)
4. 블록체인이라는 이름을 빌려서 (훔쳐서?) 블록체인 본질과 전혀 다른 일을 하는것에 확장합니다.

위의4번은 아래내용에는 없습니다. 좀 민감한 주제라...ㅎㅎ


자 이제 아래 부터는 블록체인을 공부한다라는 의미와 업계에 대해서 좀 더 디테일하게 설명을 좀 해 보겠습니다

블록체인을 공부한다? 


"블록체인"을 공부한다라는 의미를  "구조공학","아파트/건설", "부동산" 을 공부 한다로 대략 비교해보겠습니다. 

블록체인 컴퓨팅 시스템 자체를 공부한다..)
1. 아파트 건설에 필요한 구성 요소인 구조공학기술 그 자체를 깊이있게 공부하는 것을 원하는 가  
   -> 블록체인에서 모두 신뢰성 있게 동일한 데이터를 갖게 하기 위한 합의시스템을  공부한다. 
2. 아파트 건설에 필요한 다른 구성 요소인 전기배선기술에 집중하려는가  
   -> 블록체인에서 각 노드간에 서로를 탐색하고, 신뢰성 있게 서로 연결되는 방법을 공부한다. 
   -> 블록체인에서 암호/보안에 관련된 연구를 한다. 
3. 아파트를 짓는 기술 전반을 원하는 가  (건설사에 들어가려는가?) (이더리움  오픈소스 기여 및  ConsenSys, 클레이튼 등 )
   -> 각 블록체인 전체의 핵심 아키텍쳐를 공부한다. 이와중에 유니크한 체인을 발명해 본다.  (+사이드체인,서브체인등) 
4. 각 아파트 건설사들을 감사/유지/보수를 하기 위한 기반 기술에 집중 한다. 
    -> 각 블록체인의 노드들을 관리 하며, 네트워크 개선을 위한 투표에 참여한다. (DSRV등)

블록체인 시스템의 존재 의미를 공부한다.)
5. 건축이 인간에게 어떤 의미를 갖는지? 어떻게 활용되야 인간을 위하는 길이 되는지 철학적 탐색..
   -> 블록체인의 진정한 쓰임새, 회계시스템, 탈중앙 시스템의 의미와 로드맵에 대한 고민..(블록체인경제연구소)

블록체인을 이용하기 편하게 하기위한 솔루션을 공부한다..)
6. 아파트를 사고 파는 부동산업에 대해 공부하려는가? 부동산 앱을 만들고 싶은가?  
  -> 탈중앙에 의한 코인이지만, 사용하기 익숙한 중앙에 의해 통제되는 교환/거래/투자 서비스를 만든다. (빗썸, 업비트 등)
   -> 탈중앙 교환/거래/투자 서비스를 만든다 (UniSwap등) 
7. 아파트에 대한 소유권을 편하게 관리하기 위한 앱을 만들고 싶은가?
  -> 다양한 코인들을 관리/보관 할 수 있는 지갑을 만든다. (메타마스크,코인베이스지갑등) 
8. 건축을 아마추어가 하기 편하도록 도와주는 중간 도구들을 만들고 싶은가?
   -> 백엔드 개발자가 블록체인을 몰라도 기존 DB(JDBC Driver) 사용하듯이 테이블,인덱스,제약,CRUD,SQL,모니터링을 사용하여 블록체인상에 데이터를 관리하게 하기 위한 미들웨어를 만든다.(오퍼스엠) 
9. 아파트를 만들거나 사는 것은 어렵다. 그냥 빌트인 아파트를 저렴하게 제공해서 몸만 와서 월세로 살게 하자. 
   -> 블록체인을 설치하고 관리하는 것은 매우 귀찮은 일이다. 해당 부분을 클라우드에서 BaaS(BlockChain as a Service)로 서비스한다. (kaleido,Alchemy,람다256등

블록체인을 이용한 응용서비스를 공부한다..)
10. 아파트에 기반한  대출/적금 해주는 금융에 대해 공부하려는가?
   -> 블록체인 네트워크 위에서 스마트컨트랙트를 이용해 SWAP, ICO, Crowdsoursing, DeFi 시스템을 구축한다.(상당수 업체)

11. 아파트에 기반으로 ...... 을 하려는가? (NFT, DID, 메타버스, Web3, 민간SI등등)
   ->블록체인과 내부의 스마트컨트랙트를 이용해 NFT, DID, 메타버스, Web3, 민간/공공 SI시스템을 구축한다.(오픈씨등 상당수 업체) 

블록체인과 다른 기술을 융합 하기 위해 공부한다..)
12. 아파트와 병원/생태학을 융합 하길 원하는가?  (블록체인을 활용한 정부과제들)
13. 아파트와 인공지능을 융합 하길 원하는가?   (블록체인을 활용한 정부과제들)
14. 블록체인을 이용한 증권의 토큰화 (STO)
15. 블록체인을 이용한 자기 주권 신원 (SSI)


블록체인을 악용하여 한탕 하려는가?)
16. 다단계, 전세 사기 및 부동산 사기를 공부하려는가?
17. 아파트로 투기 하는 방법을 공부하려는가? 

기타)
18. 아파트를 사고 팔 때 법적 문제에 대해 공부하려는가? (변호사들) 
19. 부동산 언론? 부동산 유튜버?  등등 

위의 모든 것들이 블록체인업계와 링크되어 있습니다. 

그럼 학원은 무엇을 가르치는가?

학원의 교육과정은 위에서 9번을 하기 위함이며, 보통 아래와 같은 순서로 이루어 지는 거 같습니다.

1. 블록체인 플랫폼 하나를 선정하여 전반적인 이론들을 가르친다. (주로 이더리움)
2. 이더리움에 데이터를 넣고 빼는 것을 가르친다. 그 과정에서 이더리움의 내부언어(솔리디티)를 가르친다. with ERC21,ERC721
3. 자 이제 이더리움은 DB라고 생각하고, 해당 DB를 사용하기 위한 SDK(Web3j)를 가르친다.  |
4. 일반적인 웹프론트엔드, 웹백엔드를 가르친다. (Node.js, Spring, Html, CSS, Javascript, React 등)
5. 그 동안 배운 기술들을 가지고 솔루션을 개발해본다. 

결국 IT소프트웨어 생태계의 보편적 기술인 웹프론트엔드,백엔드가 핵심이며,
블록체인을 활용하기 위한 기본적인 기술을 양념으로 첨가 하게 됩니다.
이 기준으로 공부해야할 양은 웹프론트엔드, 웹백엔드가 압도적으로 많습니다.
결국 정해진 교육 개월수가 짧다면 모든게 겉핡기이라 어정쩡한 커리큘럼이 될 수 밖에 없습니다.
블록체인에 진심이라면 웹프론트엔드,벡엔드는 줄이고 솔리디티와 블록체인 서비스 환경에 대해서 많은 조사와 공부에 집중하는게 좋습니다. 이때 
블록체인에 대한 자기 확신이 필요 합니다.  블록체인에 대한 확신에 가득찬 사람의 이야기를 들으면 도움이 될 수 있습니다. 에르메스가 가치를 갖는 것과 포켓몬 카드가 가치를 갖는 것은 신봉자들이 있기 때문입니다. 블록체인도 그런 신봉자들이 많아 진 다면 가치를 갖게 되는 건 당연한거죠. 요즘 특히 NFT,메타버스,Web3 부분에서 그렇습니다. 

블록체인을 공부 할 것인가? 말 것인가?


1. 블록체인 그 자체를 개인적으로 공부하는 것은 매우 좋습니다. 훌륭한 분산시스템 + 컴파일러 + 보안 코드들이 오픈되어 있습니다. 하지만 역시 트레이드 오프입니다. 웹 백엔드, 프론트엔드만 해도 공부해야 할 것은 몇 만 페이지일 겁니다. 정해진 시간에 이걸 더 공부 할 수 있다고 생각한다면, 이 일을 더 잘하고 싶다면 블록체인 코어 공부하는 시간이 좀 아깝습니다. 여담인데 코테 수개월 공부 할 시간에 백엔드,프론트 공부를 하면 매우 많은 기반지식 및 실제 서비스 개발에 필요한 업무 지식을 얻을 수 있을 겁니다. 안타깝게도 블록체인  그 자체를 만드는 곳에 취업해서 그 자체를 개발 하는 일을 하게 될 가능성은 매우 가능성이 낮습니다.    

2. 블록체인을 신뢰성 있는 DB로 두고  웹프론트,백엔드를 만들고자 할 경우는 대략적인 이더리움 개념을 공부하면 됩니다. 사실 웹프런트엔드,백엔드에만 집중하는게 좋습니다. 블록체인은 블록체인 회사 입사하고 나서 필요한 부분을 공부해도 됩니다. 

3. 금융,인증등의 어떤 비지니스 로직을 해당 블록체인 내부언어로 블록체인에 넣는 것을 공부하고자 할 때는 그 부분에 대해서 집중적으로 공부해야합니다. 이것도 TO는 일반 벡엔드,프론트엔드 개발자에 비해 극히 적으며, 앞으로 더 많아질 거라 생각치도 않으며 딱히 개발자로서 메리트가 더 있는것도 아닙니다. 그리고 한방(돈)은 개발자와 무관합니다. ㅎㅎ  커리어에 대한 위험부담만 있을 수도 있습니다. 따라서 블록체인의 분산시스템 혹은 회계시스템등 활용에 관한 매력에 빠지신게 아니라면 그냥 하시던거 하는게 좋지 않을까 합니다. 이 분야는 애정이 있는자들의 놀이터가 되길 바랍니다.



아래는 제 블로그의 스폰서의 글입니다. 실제 강의 내용은 저의 의도와 상의 할 수 있습니다.


블록체인 업계와 개발, 이제 좀 감이 오시나요? 개발은 보면서 배우는 것이 아니라 직접 해봐야 하는거 아시죠? 그래서 이번에 패스트캠퍼스에서 지금 가장 핫한 블록체인 DApp 개발부터 클론코딩까지 싹 다 실습해볼 수 있는 무려 100시간 초격차 강의를 준비했다고 합니다!

블록체인 DApp 클론코딩만 배우는 것이 아니라 웹개발 기초부터 탄탄히 쌓을 수 있는 커리큘럼이라고 하네요.
1. 블록체인으로만 채운 100시간 커리큘럼
2. 한 번 구매하면 평생 소장 가능!
3. 웹개발 기초부터 탄탄히!
4. Solidity, Rust, NFT, P2E
5. DApp 개발과 클론코딩 실습
6. 40가지 기술스텍 습득
7. 질의응답 커뮤니티
8. 기술면접 기출문제집까지!

관심 있으신 분들은 패스트캠퍼스의 '한 번에 끝내는 블록체인 개발 A to Z' 강의를 통해 블록체인 개발자로 거듭나기 위한 인사이트를 얻고 실무에 활용할 수 있는 수준까지 나아가시면 좋을 것 같습니다!

▶ 지금 바로 강의 보러 가기 https://bit.ly/3xy040z

*본 포스팅은 패스트캠퍼스로부터 소정의 원고료를 지급받았으나, 작성자 본인의 경험을 토대로 주관적으로 작성한 게시물로 실제 강의 내용과는 상이할 수 있습니다.





잘 짜여진 코드가 무엇일까요??

코드를 잘 짠다는 것은 매우 어려운 일입니다. 많이 아는 것도 중요하지만, 많이 바꾸는 것도 중요합니다. 
코드에 애정이 있다면 자연스럽게 많이 바꾸게 될 것이고, 아름답게 가꾸고 싶어질 것이며 자연스레 코드는 점점 더 좋아 질 것입니다. 물론 공부를 안하면 애정이 있어도 어떻게 아름답게 해야하는지 모를 수도 있겠지요.하지만 애정이 있다면 역시 공부를 하게 되겠죠?? - 한번에 좋은 코드를 짜는 건 세상 어떤 프로그래머도 불가능 합니다.- 

소설가들은 좋은 글을 작성 하기 위해서 한 문장에 대해서 한 달 이상 고민을 한 적도 있다고 합니다.
근데 그렇게 쓰여진 문장을 몇 년 후에 다시 보았을 때, 후회가 밀려오기도 한다고도 합니다.
더 좋은 문장이 생각나기도 하기 때문입니다. 

국어/영어 자체를 뛰어나게 잘 안다고 해서 소설(글짓기)를 잘하는 것과는 또 다른 영역의 문제입니다. 
SF 소설을 쓰기 위한 컨텍스트를 조사하고 이해 하기 위해 수년간 해당 분야 전공을 공부하는 경우도 있다고 합니다. 역사소설도 마찬가지일 것입니다. 

이 글에서는 잘 짜여진 코드에 대해서만 언급합니다.
즉 코드를 잘 짜는 것과 해당 주제를 잘 수행하는 것과는 조금 다른 면이 있습니다. (이 글에선 구분해서 설명합니다.) 

유려하게 글을 잘 썼지만, 팩트가 엉망이면 .......

좋은 작품을 만들기 위해서는 해당 주제를 잘 수행하기 위해서는 아래에 나올 코드를 잘 짜는 것에 추가적으로 해당 주제에 대한 깊은 공부를 해야 합니다.  따라서 아래의 글은 "해당 주제에 대해 용도에 맞게 정확히 실행되는 코드" 라는 점을 가정하고 그 코드가 잘 짜여진 것 처럼 느껴지려면 어떤 점이 나타나야 하는지에 대해서 서술합니다.

잘 짜여진 코드는 아는 만큼 보이며, 아는게 많아도 상호간 기준에 따라 조금은 다르게 판단 할 수도 있습니다.
이제 제가 느끼는  잘 짠 코드가 가지는 요소들에 대해 11가지로 정리해 보겠습니다.
(여유가 생길 때마다  좀 더 풀어서 설명 하도록 하죠) 

1. 변경이 쉽다. 

ㅡ 변경함에 있어서 해당요소의 경계가 명확해야 합니다.
ㅡ 변경함에 있어서 주변소스의 변경은 최소화 해야 합니다.

2. 반복이 없다

ㅡ 반복되는 부분을 하나로 만듭니다.

3. 이름짓기를 잘한다.

ㅡ 추상층이 높아 질 수록 보편적으로 사용하는 키워드를 사용하며, 가급적 GoF패턴의 명칭들은 그 의도와 비슷한 경우에만 사용합니다. (자바쪽에 특히 오용된것들이 많습니다 )

4. 해당언어의 이디엄과 추구하는 방향에 맞춰 코딩한다.

ㅡ 예를들어 자바 코드짜는데 C처럼 짜면안되겠죠. 마찬가지로 코틀린 코드 짜는데 자바처럼 짜면 후지다라는 느낌이 듭니다.

5. 팀이 정한 컨벤션에 일치한다.

ㅡ 예외처리(가까이서 처리? 멀리서 처리? 등)나 로깅의 경우 답이 없기 때문에 일치화가 중요합니다.
ㅡ 탭사용규정,줄바꿈규정 등등

6. 바퀴를 재발명하지 않는다.

ㅡ 이미 잘 짜여진 기술이 있고, 변경 필요가 없다면 있는것을 사용해서 의사소통 비용줄임. 예를 들어 JVM에서 프록시 패턴을 굳이 스스로 만들기 보단 다이나믹 프록시 사용합니다.  스프링지원기술들 사용하는것도 같은 맥락입니다.

물론 발명 할 필요성은 추후에 검토 합니다. C++의 STL이나 자바의 컬렉션이나 보편적 성능일뿐이지 특정상황에선 엄청느리니까요

7. 후임자가 실수 할 여지를 줄인다

ㅡ 제약 상황을 많이 만들어 넣어서 후임자가  잘못하면 가급적 컴파일 타이밍에 에러를 내게 합니다. 

8. 추상화가 잘 된 코드

ㅡ 코드 흐름에 대해서 추상화(일반화)를 잘 해서 뽑아 내면,  유연성을 가지게 됩니다.
    다만
오버엔지니어링 이슈가 있습니다.


9.잘 알려진 패턴이 적절하게 적용된 코드

ㅡ 익히 알려진 패턴을 사용하는 것은 대게 좋은 설계를 선택하게 만들며, 의사소통 비용도 줄 일 수 있습니다.


10. 시대흐름에 맞는 코드 

ㅡ 모던C++, 모던자바를 사용합니다. 언어의 업그레이드에 대해서 관심을 갖습니다.


11.테스트 친화적 코드

ㅡ 테스트 하기 쉬운 코드는 대게 좋은 설계를 의미합니다. 


이런것들이 잘 적용되어 작성된 코드를 보면 감동이 밀려 오지요

20년 경험으로 두서 없이 적어 보았습니다. 당연히 모든 경우가 아래의 경우에 해당하진 않습니다. 
회사 규모,제품과 회사 지향점, 리더들의 특성 및  본인의 위치등에 따라 달라 질 수 있으며 
정해진 답이 없기에  맞다 틀리다 보다는 본인의 경험에 대해서 댓글에 적어주면 후배들이 보기 좋을 거 같습니다.

SI 와 정부과제)

장점 :

1. 새로운 기술 분야/도메인의 도전을 할 기회가 비교적 쉽게/자주 생깁니다.
  1-1. 새로운 기술분야를 도전(경험)할 기회가 있다. 전공이 아닐지라도(인공지능,빅데이터,블록체인등등) 
  1-2. 새로운 도메인 분야를 도전(경험) 할 수 있다. (은행,병원,통신 등등)
2. Low risk / Low Return이다. 즉 정부 돈, 갑의 돈으로 할 수 있으니 부담이 없습니다.
3. 결과물의 품질에 대한 기대치가 낮으므로 해당 스트레스는 덜 합니다.

4. 갑들이 다 망하기전에 내(을)가 망하지 않습니다. 밑에서 주워먹으니까..위에 몇개라도 있으면 됩니다. 
5. 글쓰기를 연습해 볼 수 있습니다. 제안서를 50페이지 잘 써보는것은 중요한 통찰력을 줍니다. (업계현황분석, 기술분석, 제품의 목적 및 로드맵 분석, 시장현황 분석등등) 
6. 진입장벽이 낮습니다. Low risk이기도하고 틀에 박힌 업무를 하는 경우 신규채용의 허들이 매우 낮아 질 수 있습니다.
7. 팀원뿐만 아니라 발주처(국가조직 및 외부회사)와 지속적인 의사소통을 하며 다양한 사람을 만날 수 있다. 개발 이외의 것들에 대한 경험도 많아 집니다.
8. 이직이 비교적 쉽습니다. 프리를 할 기회도 비교적 많습니다.

단점:

1. 발주처와 수주관계에 있어 갑,을 이 명확하기 때문에 고객사로부터 심리적 스트레스를 받을 수 있습니다. 이와 동시에 외부에서 일 할시 소속감이 없어 질 수 있습니다.
2. 정부과제도 그렇고 굳이 쓸 필요가 없어보이는 문서를 써야하는게 괴롭습니다.
3. 5명이 해야 할 일을 혼자 하나의 프로젝트를 맡아서 하는 경우 중압감이 있을 수 있습니다. 
    (다만 계약이 5명으로 되있다고 해도 혼자 할 수있는 분량일 가능성도 높습니다.) 
4. 제안서의 내용은 보통 과하고 애매하기 때문에 , 제안서를 작성한  사업부와 마찰이 생길 수 있으며,
    그걸 구현하는 완벽주의자에겐 고통일수도..
5. 도메인이 자주 달라져서 힘듭니다. 계속 도메인을 배워야 합니다.
6. 대기업이 아닌 이상 연봉이 높진 않습니다. High Return이 아니니 당연합니다.
7. DevOps나 AWS와 최신버전등 먼가 신선해 보이는 경험을 할 확률이 줄어듭니다.
8. 정부과제를 따내기 위한 억지 문서를 만들 때 자괴감이 들 수 있습니다.
9. 먼가를 해 냈다는 성취감이 비교적 적습니다.
10. 계속 똑같은 강도로 일을 해야 돈을 벌 수 있습니다.


서비스)

장점 :

1. 24시간 서비스가 돌아가기 때문에 적절한 긴장감을 갖게 되며,  서비스에 대해 애정이 있다면 오너쉽이 비교적 잘 생깁니다. 
2. 정부과제나 SI에 비해서 테스트코드,소스리뷰가 더 필수적입니다. 역시 실시간성을 갖기 때문입니다. 
3. 한번 잘 개발해 놓으면 투입은 적고 수익이 많이 나는 캐쉬카우가 됩니다. 다른 것을 도전 할 수 있습니다.
4. 고객사로부터 심리적 스트레스를 받을게 없습니다. 다만 오너와 소비자에서 스트레스 받습니다. 
5. 문서 쓸 일이 비교적 적습니다.
6. DevOps나 AWS등 먼가 신선해 보이는 경험을 할 확률은 큽니다.
7. 한번 서비스를 해보면 또 다른 것을 해보는데 자신감이 붙습니다. 이에 따라 기술 스타트업을 창업할 자양분을 얻을 수 있습니다.

단점)

1. 새로운 기술 분야/도메인의 도전을 할 일이 비교적 없습니다. 
2.. High risk / High Return이라 중압감이 큽니다.서비스 죽거나~해당 시간에 구현을 못했을 경우등
3. 오너/팀/소비자에서 스트레스 받습니다. 회사 내에서 서로 탓할 꺼리가 많아집니다.
4. 깊이 있는 기술력이 보다는 빠르고 기민하게 요구하는일이 많습니다. 대신 더 완벽성을 필요로합니다.
5. 뭘 해야할지 뚜렷한 로드맵이 없거나 자주 바뀌는 경우가 있습니다.
6. 망하기 쉽습니다. (오너가 갈아타기 비교적 쉽습니다) 
7. 먼가 한방터지면 크게 벌것 같지만, 본인이 거기에 포함되는 일은 없을 겁니다. 


솔루션/미들웨어) 

장점:
1. 어느 한 분야에 대한 장인이 될 가능성을 높여줍니다. 
2. 특정언어에 대한 많은 것을  깊이있게 사용 해 볼 기회가 생깁니다.

    (각종패턴,동시성,리플렉션,템플릿, DSL 등) 
3. 알고리즘/자료구조를 직접 구현 해 볼 기회가 더 자주 생깁니다. (표준이 아닌 상황에 맞는 구현체) 
4. 기술적으로 성공 하고 사업적으로 성공 해 냈다면 성취감이 매우 큽니다.
5. 성공적인 솔루션을 가지고 있으면, 서비스보다 더 효율적인 캐쉬카우가 됩니다.
6. 엉덩이 무거운 스타일의 분에게 적합합니다.
7. 기민함보다는 충실함이 중요한 분에게 적합합니다.
8.뛰어난 1명이 100명보다 중요합니다.  뛰어난 장인이 있을 가능성이 크며 그에게 배울 수 있습니다. 
(이 분야의 장점은 스승이 공개되어 있다는 겁니다. 어느 회사에 들어가지 않아도 됩니다. 오픈소스가 열려 있으며,배움은 오로지 자신에게 달려있습니다. 기인에 의해서 자신의 실력이 급상승하는 상황은 없습니다.) 


단점:
1. 성공하기 어렵기 때문에 사업적 성취감을 느낄 가능성은 크지 않습니다. (기술적 성취감은 자주느낌) 
2. 새로 조인했을때 보편적이지 않은 기술들에 대한  경험이 부족으로 오너쉽을 쉽게 갖기 힘들 수 있으며, 그때 의욕이 떨어 질 수 있습니다. 
3. 어려운 도전과제가 많습니다. 자존감이 떨어질수도..

4. 이직이 비교적 쉽지 않습니다. 
5. 솔루션을 가지고 SI할 확률이 매우 높습니다. 그때 SI의 단점은 모두 가져옵니다.


각 업종에 대해서 살펴보았습니다.

누군가에겐 좋은 사람도 누군가에겐 나쁜 사람이 되는 곳이 직장이며, 
점점 좋아질 확률 보단 점점 나빠질 확률이 높은 곳도 직장입니다. (엔트로피? ㅎ) 
불안정성이 가득한 직장 생활이지만, 개발자라는 직업은 비교적 안정적이라고 생각합니다.

저는 사람마다 자신의 적성에 맞는 곳이 따로 있다고 생각합니다. 
어떤 곳에서는 실패하거나 적응하지 못했다고 해서, 당신이 실패한건 아닙니다.
더 좋은 곳에서 더 좋은 결과를 낳을 수 있으리라 생각합니다. (다만 코딩을 대하는 자세는 어딜 가나 잘 안 바뀝니다. 매사 진지하게 대하는 버릇이 필요합니다.)
자신에게 좋은 직장을 찾는데 용기를 내시길 바랍니다. 인생 새옹지마입니다.


PS) 업무강도와 연봉은 현재 회사상황이 어떠냐, 얼마나 버는 회사냐, 어떤 리더들이 있냐에 따라서 달라집니다. 어떤 분야의 회사냐, 어떤 직군이냐 보다는~

1. 디폴트 값으로 대처 

interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
    val data: String
}

class Token(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
class NFT(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }
}

먼저 이런 코드가 있다고 가정 해 본다.

@Test
fun setUp() {
    val account = Account("1", null)
    val name = account.name?:"default_name"
    Assertions.assertEquals("default_name", name )
}


해당 Account의 name을 가져왔을 때 만약 null이라면 "default_name"을 넣어 줄 수 있다.
많은 경우 이렇게 처리 할 수 있을 것이다. 특히 설정값을 처리할 때 많이 사용되는데 특별히 설정된 값이 없을 경우 디폴트 값을 넣어주는 경우가 그럴 것이며 굉장히 처리하기 쉬운 경우이다.

2. nullable  

그리고 위에 보듯이 특정 값이 없을 경우 우리는 null을 리턴 할 수 있다.  비교적 일상적으로 벌어지는 문제이다.
즉 어떤 컬렉션에 특정 값이 없을 경우엔 null을 리턴하는게 정상이며 이후에 어떻게 처리 할 지는 사용자의 몫이다.

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    
    fun getAccount(id: String): Account? {
        return accounts.get(id) 
    }
}

위에서 getAccount를 호출할 경우 해당 정보가 없을 수 도 있는데, 이것은 예외라고 보긴 어려울 것이다. (기술적으로 예외로 처리할 수도 있지만, 나는 그렇게 처리하지 않는다. 이유가 너무 명확하기 때문이다.) 

 @Test
 fun getAccount() {
   val bank = Bank()
   bank.createAccount(Account("1", "hama"))
   val result = bank.getAccount("1")?.name

  Assertions.assertEquals("hama", result)
}

만약 "2"를 넣어서  null을 리턴한다면 아무것도 처리하지 않으며, "1"을 넣는다면 name을 출력 할 것이다.  
? 는 null이 아닐 경우만 처리하라는 의미로, 위의 코드에서 ? 없이 name을 코딩 할 순 없다.
근데 위의 코드는 resultnull이 담길 수 있으므로 위험 할 수 있다. ?: 엘비스 연산자로 디폴트값을 넣거나 예외를 전파하거나 무엇인가 해야한다. NPE를 막아주는게 아니라 주의를 줄 뿐이다.

 val bank = Bank()
    bank.createAccount(Account("1", "hama"))
    bank.getAccount("1")?.let {
        val result = it.name
        println(result)
    }

적어도 let을 사용해 이렇게 하면 null을 전파하진 않는다.

3. Execption으로 전파 

null로 처리하게 된 다면, 호출당한 함수내부에서 어떤 이유로 null이 리턴 됬는지 궁금 할 수가 있다.일반적으로 예상되는 경우라면 호출한 측에서 이유를 작성 할 수도 있지만,  즉 예외가 왜 일어 났는지 잘 모를 수 있는 경우라면 호출당한 함수에서 이유를 알려주는 것이 일반적이다. 

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    fun dipositToken(accountId: String, token: Token){
        accounts.get(accountId)?.dipositToken(token)
    }

만약 위의 dipositToken을 호출했는데 accountId에 해당하는 계좌가 없으면 아무일도 안 일어날 것이다. 뭐 이렇게 되도 상관없을 수도 있겠다. 일관성이 깨지는 것 같은 큰 문제는 일어나지 않았으니깐, 대신 이 함수를 호출한 측에서는 diposit이 잘 됬는지 아닌지 알수가 없다. 잘 됬다고 생각하고 다음 단계를 밟을 수도 있는데 그렇게 되면 문제가 된다. 따라서 이 경우에는 리턴 값으로 문제 상황을 알리거나 예외를 전파해야하는데, 예외로 전파하는것을 추천한다. 나는 command 행동에서 문제가 생기면 예외를 던지고, query행동에서문제가 생기면 null (일반적인 상황일때) 혹은 예외(진짜 예외상황일때)를 전파하는 컨벤션을 갖는다.

@Throws(NoAccountException::class)
fun createToken(accountId: String, token: Token){
    accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
}

이렇게 예외를 던저주고 함수에 명시해 둔다. (호출하는 클라이언트에 대한 배려) 

@Test
fun dipositToken() {
    Assertions.assertThrows(NoAccountException::class.java, {
        bank.createToken("2", exception.Token("token_1", "opusm", 1000))
    })
}

Junit5를 통해서 테스트를 성공한다. 

 

4. Result 

1.5 버전 부터 Result<T> 을 사용 할 수 있다. 함수안에서 일어나는 일의 결과를 성공,실패로 단순히 알 수있는 Boolean과 다른점은 성공 했을 경우의 타입과 실패했을 경우의 예외 타입 및 메세지를 포함 시켜서 보다 풍부한 결과를 알고 싶을 때 사용한다. Result<T>구현은 유틸리티성 메소드가 굉장히 많이 포함되어 있다. 5번의 runCatching에서 더 설명한다.

import java.lang.IllegalArgumentException


data class Passport (val name: String, val age: Int)
data class EntranceData(val purposeCode: Int, val jobCode : Int, val passport: Passport)

data class PassportException(override val message: String?) : IllegalArgumentException(message)
data class JobException(override val message: String?) : IllegalArgumentException(message)


enum class Nation(val exportValid : List<ExportCheck>, val importValid : List<ImportCheck>) {
    KOREA(listOf(ExportCheck.Passport, ExportCheck.Job),listOf(ImportCheck.Passport) ),
    JAPAN(listOf(ExportCheck.Passport),listOf(ImportCheck.Passport)),
    USA(listOf(ExportCheck.Passport),listOf(ImportCheck.Passport));

    enum class ExportCheck {
        Passport {
            override fun check(data : EntranceData) : Result<String> {
                return if(data.passport.age > 10){
                    Result.success("Export passport ok")
                }
                else {
                    Result.failure(PassportException("reason why ~~"))
                }
            }
        },
        Job {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.jobCode == 1){
                    Result.success("Export Job ok")
                }
                else {
                    Result.failure(JobException("reason why ~~"))
                }
            }
        };

        abstract fun check(data : EntranceData): Result<String>
    }

    enum class ImportCheck {
        Passport {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.passport.age > 10){
                    Result.success("Export passport ok")
                }
                else {
                    Result.failure(PassportException("reason why ~~"))
                }
            }
        },
        Job {
            override fun check(data : EntranceData) : Result<String>{
                return if(data.jobCode > 1){
                    Result.success("Export Job ok")
                }
                else {
                    Result.failure(JobException("reason why ~~"))
                }
            }
        };

        abstract fun check(data : EntranceData): Result<String>
    }

    companion object {
        fun getNumberOfNations() = values().size
    }
}

fun tradeTest(entranceData: EntranceData, nation: Nation): List<Result<String>> {
  return nation.exportValid.map{
       it.check)
   }
}

fun main(args: Array<String>) {

    val korea = Nation.KOREA;
    val data = EntranceData(1,1, Passport("john",3))

    val results = tradeTest(data,korea)

    results.forEach { result ->
        result.onSuccess {
            println(it)
        }
        .onFailure {
            println(it.message)
        }
    }

}

5. runCatching 

runCatching은 Result<T>를 사용하는데 도움이 될 수 있다.

account)

fun withrawToken(id: String, balance: Int): Int {
    val token = assets.find { it -> it.id == id  } as Token
    require(token.balance > balance )
    token.balance = token.balance - balance
    return balance
}

bank)

fun withrawToken(accountId: String, tokenId: String, balance: Int): Int{
    val result = kotlin.runCatching {
        accounts.get(accountId)?.withrawToken(tokenId, balance)
    }

    return result.getOrDefault(0)!!
}

test)

@Test
fun withrawToken() {
    bank.createToken("1", exception.Token("token_1", "opusm", 1000))
    val result = bank.withrawToken("1", "token_1", 5000)
    Assertions.assertEquals(0, result)
}


아래와 같이 runCatching 문은 좀 더 다양한 방식으로 처리 될 수 있다. 

fun withrawToken(accountId: String, tokenId: String, balance: Int):String{
    return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
    .onFailure { println(" withraw token failure") } // 특정 예외를 구분 할 수도 있다.
    .onSuccess { println(" withraw token success") }
    .mapCatching { it -> it.toString() }
    .getOrDefault("0") // getOrNull, getOrThrows
}

/////// 아래와 같이 특정 예외별로 구분해서 처리 할 수도 있다.
getOrElse {
    when(it) {
        is LowBalanceException -> "LowBalanceException"
        is IllegalStateException -> "IllegalStateException"
        is NullPointerException -> "null"
        else -> throw it
    }
}

 

전체 코드)

package exception

import exception.bankexception.LowBalanceException
import exception.bankexception.NoAccountException
import kotlin.jvm.Throws


interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
}

enum class AssetKind{
    Token,
    NFT
}

data class Token(
    override val id: String,
    override val name: String,
    var balance: Int) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
data class NFT(
    override val id: String,
    override val name: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

data class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

    fun createToken(token: Token) {
        assets.add(token)
    }

    fun withrawToken(id: String, balance: Int): Int {
        val token = assets.find { it -> it.id == id  } as Token
        require(token.balance > balance )
        token.balance = token.balance - balance
        return balance
    }
}

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    @Throws(NoAccountException::class)
    fun createToken(accountId: String, token: Token){
        accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
    }

    fun withrawToken(accountId: String, tokenId: String, balance: Int):Int{
        return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
        .onFailure { println(" withraw token failure") }
        .onSuccess { println(" withraw token success") }
        .getOrDefault(0)!!
    }
}

 




자바와 체크 예외 

- 어떤 예외에 대해 명시적으로 메서드 시그니처에 포함시켜서 좀 더 처리를 강제한다.
-
체크드예외는 클래스 못찾는거, 파일 못찾는거 IO예외, SQL예외 같은거...즉 언제든지 자신의 탓이 아닌데도 문제가 발생할 수 있고, 메모리가 없는것처럼 아예손놓고 있지 않아도 될 만한것
- 오류Error는 메모리나 HDD가 없는 것 처럼 멀 할 수 없는 경우를 말한다. 그냥 끝내는게 최선
-
런타임예외는 Error긴 한데 좀 다른 유행이다. 근데 런타임예외는 체크드예외인 Exception 아래에 있다.의도는 개발자의 실수에 의한 예외를 말한다. 널문제, 잘못된인자나 프로그램 상태 같은거. 즉 개발자의 탓이고 멍청하게 프로그램을 사용했으니 그냥 연산 중단 되버려지만 Error처럼 무조건 끝내지 말고복구를 시도 해 볼 순 있다 정도
-
문자열에서 데이터추출시 URL은 체크예외인 MalformedURLExcetion 던지지만
  Integer.parseInt
는 언체크예외인 NumberFormatException 던짐. ???
-
자바 8 람다나 함수형 인터페이스는 체크예외를 전파할 수 없게 됨. 개발자들 체크예외 사용 포기
-
그냥 모두 RuntimeException으로 다루자. (코틀린)

자바의 Optional

1. 자바 8이전에는 예외를 던지거나 null을 반환하는 것
2. 
두 방법 모두 허점이 있는데 예외는 진짜 예외적인 상황(?)에서 사용해야 하며, 예외를 생성할 때 스택트레이싱 전체를 캡쳐하므로 비용도 많이 든다. null을 반환하면 이런 문제는 없지만 그것을 처리 안하면 원래 오류가 아니라 그냥 어디선가 갑자기 NullPointerException이 튀어 나올 수 있다.
3. Optional
은 예외를 반드시 사용할 필요 없는 곳에 null대신 사용하기 좋다.
4. illegalArgumentException
을 던지는 곳에 Optional을 던지는게 더 나은 경우가 많다. Optional은 검사를 강제하므로 검사예외와도 비슷한데 더 간단하고 함수형 처럼 체이닝 하기도 좋다.
6. 
다만 컬렉션,스트림,배열,옵셔널 같은 컨테이너 타입은 옵셔널로 감싸면 안된다.
7. 
물론 그냥 null을 던지는게 성능상은 더 유리하다.

+ Recent posts