사실 완전한 FP라는 것은 운용가능하진 않습니다. IO가 항상 발생하기 때문입니다.
DB, disk, rest api, grpc 등등 IO를 유발하는 작업들을 사용하기 때문입니다.
이러한 것들을 순수하지 않은 함수들(impure)이라고 하는데 부작용(side effect) 를 만들기 때문입니다.
이러한 부작용을 제어하려면 FP인 부분과 아닌 부분을 나눌 필요가 있습니다.
IO 경계(boundary)와 서비스와 도메인 로직을 분리합니다.
모든 것이 정확하게 분리가 되지는 않지만, 도메인 로직과 IO 경계를 분리하는 것이 핵심입니다.
다음 예시에서는 express에서 res, req를 받는 부분과, domain로직이 들어가는 부분, 여러 infra를 활용하여 pipeline을 만드는 service, 직접적으로 IO를 유발하는 infra로 나뉜 예시입니다.
flowchart TD
%% Node Definitions
Infra["<b>[ Infrastructure Effects ]</b><br/>HTTP / DB / FS / Cache / MQ<br/><br/><i>IO boundary using TaskEither</i>"]
App["<b>[ Application Services ]</b><br/><br/><i>Composed effectful pipelines</i>"]
Domain["<b>[ Domain Logic ]</b><br/><br/><i>Pure functions, ADTs, rules</i>"]
%% Relationships
Infra <-->|"DI (Functional)"| App
App <-->|"pure values"| Domain
%% Styling
%% Red/Orange for Side Effects/Infrastructure
classDef dirty fill:#ffe6e6,stroke:#b30000,stroke-width:2px,color:#000;
%% Yellow/Neutral for Orchestration
classDef glue fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#000;
%% Green for Pure/Safe Domain
classDef pure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000;
%% Apply Classes
class Infra dirty
class App glue
class Domain pure
여기서 핵심은 의존성은 반드시 한 방향으로만 흘러야 합니다.
상화에 따라 이름은 다르지만.
domain 혹은 library는 fp의 형태를 가진 코드들로 구성되며, service를 호출하여 사용합니다.
보통 service는 singleton인 경우가 많습니다.
infra는 io를 유발하는 경계에 위치하며, 이것이 외부와 상호작용을 대신 처리해 줍니다.
flowchart LR
%% Nodes
Infra["<b>[ Infrastructure ]</b><br/><i>Impure / Outer Shell</i>"]
App["<b>[ Application ]</b><br/><i>Orchestration Layer</i>"]
Domain["<b>[ Domain ]</b><br/><i>Pure Core / Inner</i>"]
%% The Dependency Rule (Source Code Imports)
%% In functional architecture, the outer layer depends on the inner layer.
Infra -->|imports / depends on| App
App -->|imports / depends on| Domain
%% Styling to match previous color coding
classDef dirty fill:#ffe6e6,stroke:#b30000,stroke-width:2px;
classDef glue fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
classDef pure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;
class Infra dirty
class App glue
class Domain pure
%% Arrow Styling (Thick to emphasize strict rule)
%% linkStyle 0,1 stroke-width:3px,fill:none,stroke:#333;
모든 FP 코드베이스가 이렇게 되는 것은 아닙니다.
제가 작업을 했던 코드베이스에서는 사용자의 요청에 관한 정보가 있는 graphql layer과 사용자 정보 없이 로직만 담당하는 lib, 그리고 io를 유발하는 service로 나누어서 분리를 했습니다.
단순한 예시
src/
├─ domain/
│ ├─ model.ts (ADTs & types)
│ ├─ rules.ts (Pure business logic)
│ └─ validation.ts (Pure validators)
│
├─ app/
│ └─ userService.ts (TaskEither pipelines)
│
├─ infra/
│ ├─ dbRepo.ts (TaskEither DB access)
│ └─ httpClient.ts (TaskEither external API)
│
└─ http/
└─ userController.ts (Express handlers)
예시
// Controller (Express)
const activateUserHandler = (req, res) =>
activateUser(req.params.id)().then(result =>
result._tag === "Right"
? res.status(200).json(result.right)
: res.status(400).json({ error: result.left })
);
// Domain
type ActivateError = "Undersage" | "AlreadyInactive";
const validateActivation = (u: User): Either<ActivateError, User> =>
u.age < 18 ? left("Underage")
: u.status !== "active" ? left("AlreadyInactive")
: right(u)
// Application
const activateUser = (id: string): TaskEither<AppError, User> =>
pipe(
findUser(id),
flatMapTE(user =>
validateActivation(user)._tag === "Left"
? leftTE({ type: "DOMAIN", reason: validateActivation(user).left })
: rightTE(user)
),
flatMapTE(saveUser)
);
// Infra
const findUser = (id: string) : TaskEither<InfraError, User> => ...;
const saveUser = (u: User): TaskEither<InfraError, User> => ...;