⚠️ 프로그래밍 언어 F#에 대한 약팔이가 담겨있습니다.

😈 악마의 달콤한 속삭임

현대 대부분의 프로그래밍 언어는 미리 선언되지 않은 모듈(패키지)을 참조해 사용할 수 있도록 지원한다. 이렇게 하면 코드가 작성된 순서를 신경쓰지 않고 프로그램을 작성할 수 있으니 매우 편리해진다.

출처: https://www.jetbrains.com/help/idea/auto-completing-code.html#smart_type_matching_completion

게다가 프로그램을 작성하다가 필요한 함수가 있으면 IDE에서 어떤 모듈에 있는 함수인지 바로바로 찾아주고 import까지 자동으로 선언해주니 모듈을 어떻게 디자인할지 걱정하지 않아도 프로그램을 완성하는 데 별로 어려움이 없다. IDE에서 모듈이나 패키지도 클릭 몇번이면 새로 만들 수 있으니 프로젝트가 진행될 수록 파일, 모듈, 패키지 수는 늘어가고 어떤 생각으로 이 클래스를, 이 함수를 이 모듈에 넣었는지 기억도 안난다. 결국 프로젝트는 산으로 간다...

📦 뒤엉킨 모듈

// foo.js
import { doRandomJob } from './bar.js';

export function alsoRandomJob() {
  // ...
}

// ...
doRandomJob();
// ...
// bar.js

import { alsoRandomJob } from './foo.js';
export function doRandomJob() {
  // ...
  alsoRandomJob();
  // ...
}

이런 식으로 프로그램이 제대로 구조를 이루지 못하고 점점 거대해지는 것은 매우 흔한 일이 되어버렸다. 이런 경우는 사실 모듈 단위로 서로 엉켜있다는 말도 무안하게, 함수는 함수끼리 클래스는 클래스끼리 뒤엉켜서 스파게티 코드 저리가라 하는 막국수 코드가 된다.


출처: https://programmingideaswithjake.wordpress.com/2018/11/27/converting-a-cyclic-dependency-into-a-directed-dependency/

그래서 위 그림과 같이 A는 B의 코드를 호출하고, B는 C의, C는 D의, D는 다시 A의 코드를 호출한다. 스스로 온전히 존재하는 코드가 없는 것이다. 그리고 더 최악인 것은 이런 상황을 감지하는 게 매우 어렵다는 것이다.

어쨋든 작동하는 프로그램이니, 상관없지 않나요?

틀린 답은 아니다. 실용주의 관점에서 만약 이 코드가 완벽하게 작동하고 있다면 굳이 나서서 수정할 필요는 없다. 하지만 대부분의 경우 우리가 작성한 코드는 정말 완벽한 코드가 아니라 진짜 필요한 사항을 어찌저찌 커버하고 있는 코드다. 결국 코드를 수정해야 하는 상황은 무조건 온다는 뜻이다.
그런데 모든 코드가 서로에게 의존하고 있다보니 한 부분을 수정하기 위해서는 의존하고 있는 모든 코드를 수정해야 하는 상황이 벌어진다. 게다가 이 코드를 내가 작성한 것이 아니라면? 코드를 일일히 파고 들어가 조사해봐야 한다면? 😡 구글에 how to structure a node app? 이나 how to eliminate circular dependency 같이 검색하면 참고할 만한 수많은 글들이 나온다. 다들 고통받고 있는 듯 하다. 구조를 잘 설계하는 것은 쉬운 일이 아니다.

참고 할만한 문서

👍 F#: 필요한 원칙을 버리지 말자

모두가 위에서 설명한 것처럼 편리함에 속아 넘어가 모듈을 다시 정리하는 악몽을 한번쯤은 겪어봤을 것이다. 아니면 모듈이 뒤엉킨 채로 언젠가 다가올 악몽을 기다리고 있을 수도...

이런 문제를 진지하게 받아드린다면 F#은 매력적인 선택지가 될 수 있다. F#은 미리 선언하지 않으면 사용할 수 없다는 원칙을 지키고 있다. 따라서 의존성 관계에서 상위에 있는 내용을 먼저 작성하는 것이 강제 된다. 그렇기 때문에 프로그램에서 중요한 로직일 수록 다른 모듈에 의존하지 않는 바람직한 설계가 자연스럽게 유도된다. 프로그래밍 환경은 개발자의 프로그래밍 실력만큼 많은 영향을 미친다. 아무리 빠른 달리기 선수라도 기차를 타고 여행하는 사람보다 더 멀리 갈수는 없는 법이다.

❓ F#이 의존성 관계를 관리하는 법

// Program.fs

module Program

open Application

[<EntryPoint>]
let main argv =
    createApp ()
    |> storeAddResultOf 5 3

    0
// Application.fs

module Application

open Core.Calc

type InMemoryApp () =
    let calcResults = ResizeArray<int>()
    
    member _.Store x = calcResults.Add x
    member _.Read () = calcResults

let createApp () = InMemoryApp()

let storeAddResultOf x y (store : InMemoryApp) =
    add x y
    |> store.Store
// Core.fs

module Core

module Calc =
    let add x y = x + y
    
    let sub x y = x - y
    
    let mul x y = x * y
    
    let tryDiv x y =
        if y = 0  then None
        else Some (x / y)

예를 들어보자. 3개의 소스코드 파일이 있다. 살펴보면

  • Program의 main 함수는 Application에서 정의된 함수들을 사용하고 있다.
  • Application 모듈은 Core에서 정의된 함수들을 사용하고 있다.

따라서 Program은 Application에, Application은 Core에 의존하며 Core는 그 무엇에도 의존하고 있지 않다. 따라서 Core가 수정되면Application에 영향이 미치고, Application이 수정되면 Program에 영향이 미치는 구조를 볼 수 있다.

따라서 위와 같은 순서로 코드가 작성될 수밖에 없다. 다른 순서는 존재하지 않는다. 만약 소스코드의 순서가 바뀐다면 어떨까? F#에서는 프로젝트 설정파일을 통해 이 의존관계를 설정할 수 있다.

  <ItemGroup>
    <Compile Include="Core.fs" />
    <Compile Include="Application.fs" />
    <Compile Include="Program.fs" />
  </ItemGroup>
  
  // 아래로 수정해보자

  <ItemGroup>
    <Compile Include="Program.fs" />
    <Compile Include="Application.fs" />
    <Compile Include="Core.fs" />
  </ItemGroup>

어떻게 될까?

error FS0039: The namespace or module 'Application' is not defined.
error FS0039: The value or constructor 'createApp' is not defined.
error FS0039: The value or constructor 'storeAddResultOf' is not defined.
error FS0039: The namespace 'Calc' is not defined.
error FS0039: The value or constructor 'add' is not defined.

미리 선언하지 않으면 사용할 수 없다 는 원칙에 의해 컴파일되지 않는다. 이처럼 F#을 이용해 프로그램을 작성하면 모듈이 서로 뒤엉킨다든지, 소스코드 간의 의존성이 꼬여서 어쩔 수 없는 리팩토링을 해야한다든지 하는 일을 사전에 예방하기 쉬워진다. Visual Studio나 Rider를 이용하면 소스코드의 순서를 손쉽게 수정할 수 있으니 별로 복잡할 것도 없다.

📝 마치며

F#을 이용하면 프로젝트의 구조를 흐트림없이 관리할 수 있다. 만약 프로젝트가 장기적으로 운영될 것이라는 생각이 든다면 F#의 이러한 기능은 엄청난 강점으로 다가올 것이다. 소프트웨어의 복잡도를 한층 낮춰주고 프로그램 구조를 이해하는 데 도움을 주기 때문이다. 코드가 어디서 잘못 돌아가고 있는지 머리를 쥐어뜯고 있다면 F#를 고려해보는 것도 좋겠다.

지금 내가 프로그램을 '잘못' 작성하고 있다고 컴파일러가 에러를 띄워주면 그만큼 고마운 것도 없다.

profile
Functional Lifestyle

0개의 댓글