배움은 설렘이다.
배움은 겸손이다.
배움은 이타심이다.
지속 가능한 코드
지속 가능한 코드는 마치 한 편의 아름다운 시와 같습니다. 시간이 흘러도 그 의미와 가치가 바래지 않고, 읽는 이에게 꾸준히 감동을 주는 것입니다. 이런 코드를 작성하는 일은 단순히 기능을 구현하는 것을 넘어서, 코드의 아름다움과 명료함을 추구하는 일입니다.
코드는 한 그루의 나무에 비유되곤 합니다. 나무는 처음 심을 때는 작고 연약하지만, 시간이 흐르면서 뿌리를 내리고 가지를 뻗어 결국 울창한 숲을 이루게 됩니다. 지속 가능한 코드도 마찬가지입니다. 처음 작성할 때는 작은 부분에 불과할지 모르지만, 시간이 지나면서 점점 더 많은 기능을 포함하게 되고, 더 많은 사람이 그 코드를 사용하고 유지보수하게 됩니다.
지속 가능한 코드를 작성하는 것은 단순한 기술적 문제를 해결하는 것을 넘어, 미래를 내다보는 안목과 책임감을 요구하는 일입니다. 코드는 작성자의 철학과 가치관을 반영하는 작품이기 때문입니다. 코드를 작성할 때는 항상 다음과 같은 질문들이 필요합니다.
- 이 코드가 시간이 지나도 쉽게 이해되고 유지보수될 수 있을까?
- 이 코드가 다른 사람들과 협업하는 데 있어 장애물이 되지 않을까?
- 이 코드가 변화하는 요구사항에 유연하게 대응할 수 있을까?
이러한 질문들은 지속 가능한 코드를 작성하는 데 있어 나침반 역할을 해줍니다. 가독성이 높은 코드, 잘 정의된 인터페이스, 그리고 적절한 테스트 케이스는 지속 가능한 코드의 핵심 요소들입니다.
그러나 지속 가능한 코드를 작성하는 일은 결코 쉬운 일이 아닙니다. 때로는 당장의 마감 기한에 쫓겨 임시방편으로 문제를 해결하고 싶은 유혹에 빠지기도 합니다. 그러나 언제나 긴 호흡으로 바라봐야 합니다. 일시적인 해결책보다는 장기적인 관점에서의 품질을 더 중요하게 생각해야 합니다.
제가 꿈꾸는 것은, 제가 작성한 코드가 나중에 누군가에게 읽고 싶어하는 코드가 되는 것입니다. 그것이 코드를 작성하는 저의 이유이며, 제가 지속 가능한 코드를 위해 끊임없이 노력하는 이유입니다. 그리고 이 여정은 혼자가 아닌 같은 꿈을 꾸는 많은 개발자들이 함께 만들어가는 길입니다.
결국, 지속 가능한 코드를 작성하는 것은 단순한 기술적 문제를 넘어서, 인간적인 문제이기도 합니다. 서로를 배려하고, 존중하며, 함께 성장하는 것이야말로 지속 가능한 코드를 위한 가장 중요한 요소라고 믿습니다.
이것이 바로 지속 가능한 코드를 향한 이야기입니다.
회귀 버그(Regression Bug)
- 기존에 잘 작동하던 기능이 새로운 코드 변경이나 업데이트로 인해 갑자기 오작동하게 되는 문제를 말합니다.
- 회귀 버그는 지속 가능한 코드 작성 과정에서 가장 큰 기술적 도전 중 하나입니다.
개발
- 부수 효과(Side Effect)
- 성능
운영
- 재배포 속도
- 장애 사전 예측 속도
- 장애 사후 복구 속도
- 표준 운영 절차 문서(SOP, Standard operating procedure)
- 개발은 글쓰기와 같습니다.
개발자는 프로그래밍 언어로 이야기를 코드로 써 내려갑니다. - 솔루션 탐색기의 폴더 구성은 책의 목차와 같습니다.
각 폴더(레이어)는 책의 챕터처럼 관련 내용(관심사)을 담고 있습니다. - 목차를 따라 코드를 읽으면 비즈니스의 흐름을 이해할 수 있습니다.
코드는 비즈니스의 작동 방식을 설명하는 상세한 설명서와 같기 때문입니다.
- 읽고 싶은 코드(지속 가능한 코드: 코드가 문서입니다)
비즈니스 이해 --{글 쓰기}--> 코드
: 비즈니스를 이해하면 코드를 배치할 수 있습니다.비즈니스 이해 <--{글 읽기}-- 코드
: 코드를 읽으면 비즈니스를 이해할 수 있습니다.
- 아키텍처 관점에서 Pure Function을 중심에 배치하고 Impure Function을 외곽에 베치하는 이유를 이해합니다.
- 아키텍처 관점에서 로그 예제를 통해 Pure Function 중앙 배치의 가치를 이해합니다.
- 참고 자료
건축업자가 프로그래머의 프로그램 작성 방식에 따라 건물을 짓는다면 가장 먼저 도착하는 딱따구리가 문명을 파괴할 것입니다.
If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization. - Gerald Weinberg
- Architecting is a series of trade-offs.
- The architecture should scream the intent of the system.
※ 출처: Making Architecture Matter, 소프트웨어 아키텍처의 중요성
- 아키텍처는 제품의 지속 가능한 성장을 주도하는 중요한 모든 것(
The important stuff whatever that is
)입니다.- 예. 기능을 추가할 때?
- 관련 코드의 시작 지점을 찾는 것은 쉽지만, 그 기능이 미치는 영향을 끝까지 파악하는 것은 어렵습니다.
- 끝 지정(부수 효과, Side Effect)를 모두 인지하는 것은 쉽지 않습니다.
- 예. 기능을 추가할 때?
- 관심사의 분리(SoC, Separation of Concerns)
- 아키텍처 수준에서 비즈니스 관심사와 기술적 관심사를 명확히 분리하여, 각각 독립적으로 설계하고 관리합니다.
- 이는 요구사항을 코드로 분해하고 배치를 결정하는 첫 번째 기준이 됩니다.
- 이러한 분리와 격리를 통해, 기술적 구현에 의존하지 않고 비즈니스 로직을 독립적으로 테스트하고 개선할 수 있습니다.
※ 출처: Making old applications new again
※ 출처: DDD 및 CQRS 패턴을 사용하여 마이크로 서비스에서 비즈니스 복잡성 처리
Application Architecture
├─ Backend
│ ├─ Monolithic Architecture
│ ├─ Modular Monolithic Architecture
│ ├─ N-tier Architecture
│ └─ Microservices Architecture
│ ├─ Internal Architecture
│ │ └─ Layered Architecture
│ │ ├─ Hexagonal Architecture
│ │ ├─ Onion Architecture
│ │ ├─ Clean Architecture
│ │ └─ Vertical Slice Architecture
│ │
│ └─ External Architecture
│ └─ 외부 시스템 구성 아키텍처: 예. CNCF Landscape
│
└─ Frontend
└─ Internal Architecture
└─ Layered Architecture
├─ Hexagonal Architecture
├─ Onion Architecture
├─ Clean Architecture
├─ Vertical Slice Architecture
└─ + UI 특화 Architecture: 예. MVVM, ...
- 백엔드와 프론트엔드 대부분 관심사를 계층(Layer)로 관리하는 계층형 아키텍처 기반의 진화된 아키텍처를 사용합니다.
- 1992년부터 아키텍처 수준에서는 관심사를 계층(Layer)으로 나누고, 객체 수준에서는 관심사를 엔티티(Entity)로 관리하는 방법이 제시되었습니다.
- 즉, 시스템의 큰 구조는 여러 계층으로 나누어 관리하고, 각 계층 내의 세부 사항은 엔티티로 나누어 관리하는 방식입니다.
- 아키텍처는 제품의 선순환(Good Cycle) 성장의 시작점입니다.
ADR(Architectural Decision Records)
아키텍쳐와 관련된 중요한 의사 결정을 기록해 두는 문서입니다.
전략 설계(Strategic Design) -> 전술 설계(Tactical Design)
- TODO.
Note
사용 사례(Use Case)가 아키텍처를 주관합니다.
Important
-
요구사항을 트랜잭션 단위로 나눕니다.
- 트랜잭션은 하나의 비즈니스 작업이 완료되는 단위입니다.
- 예를 들어, "사용자가 물건을 구매한다"는 하나의 트랜잭션입니다.
-
각 트랜잭션에 맞는 사용 사례(Use Case)을 정의합니다(<-- Application 레이어).
- 트랜잭션이 처리되는 과정을 단계별로 설명하는 로직을 정의합니다.
- 사용 사례는 상태(멤버 변수)를 가지고 있지 않습니다.
-
사용 사례는 입력과 출력을 외곽에 배치하고, 비즈니스 규칙을 중심에 배치합니다.
≒ 사용 사례는 I/O에 의존하는 코드을(Impure) 외곽에 배치하고, I/O에 의존하지 앟는 코드을(Pure, 비즈니스 규칙) 중심에 배치합니다.- Biz. 관심사: I/O에 의존하지 앟는 코드(<-- Domain 레이어)
- 순수한 코드(Pure)은 외부 시스템이나 데이터베이스에 의존하지 않습니다.
- 그 자체로 작동하고, 테스트도 쉽게 할 수 있습니다.
- Tech. 관심사: I/O에 의존하는 코드(<-- Adapter 레이어)
- 비순수한 코드(Impure)은 데이터베이스 저장이나 외부 서비스 호출처럼 시스템의 외부 환경에 영향을 받습니다.
- 그 자체로 작동하지 않고, 테스트도 쉽게 할 수 없습니다(외부 환경 조건이 만족해야 테스트할 수 있습니다).
- Biz. 관심사: I/O에 의존하지 앟는 코드(<-- Domain 레이어)
-
Lifecycle 기준으로 비즈니스 규칙(순수한 코드, Pure)을 정의합니다.
구분 생명 주기 상태 행위 비교 Entity
있음(有) 가변, Mutable O Identifier Equality: Id 비교 ValueObject
없음(無) 불변, Immutable O Structural Equality: Value 비교 - 불변: 생성 후 변경할 수 없다.
레이어 단위로 특정 관심사를 정의하고 처리합니다
- Tech. 관심사: Non-deterministic
- Tech. 입/출력: Adapter 레이어(사용자 인터페이이스, 데이터 저장소, ...)
- Biz. 관심사: Deterministic
- Biz. 로직: Application 레이어
- Biz. 단위: Domain 레이어
- 레이어 이름 규칙
T1.T2{.T3} T1: 프로젝트 T2: 레이어(Domain, Application, Adapter) T3: 세부 레이어(생략 가능)
- 레이어 이름 적용
{프로젝트} // Tech. 관심사, Non-deterministic, Host {프로젝트}.Adapters.Infrastructure // Tech. 관심사, Non-deterministic {프로젝트}.Adapters.Persistence // Tech. 관심사, Non-deterministic {프로젝트}.Adapters.Presentation // Tech. 관심사, Non-deterministic {프로젝트}.Application // Biz. 관심사(Biz. 로직), Deterministic {프로젝트}.Domain // Biz. 관심사(Biz. 단위), Deterministic
- Biz. 관심사(Biz. 로직과 Biz. 단위)는 비결정론적(Non-deterministic) Tech 관심사(Tech. 출력)에 의존하기 때문에 결정론적(Deterministic) 성질을 잃게 됩니다.
- 결정론적(Deterministic): 예측 가능
- 정확한 수학적 관계식에 의해 예측
- 오차(불확실성)를 허용하지 않음
- 비결정론적(Non-deterministic): 예측 불가능
- 다만, 통계적인 방법으로 만 추정
- 오차(불확실성)를 허용함
- 결정론적(Deterministic): 예측 가능
- 인터페이스: Strategy 패턴
- TODO. 세부 설명, 예제 코드
테스트
- 단위 테스트(Unit Test): Biz. 관심사을 외부 환경에 의존하지 않고 테스트합니다.(Deterministic).
- 통합 테스트(Integration Test): Tech. 관심사부터 테스트합니다(Non-deterministic).
- 레이어 이름 규칙
T1.T2.T3 T1: 프로젝트 T2: 레이어(Test) T3: 세부 레이어(Unit, Integration, E2E)
- 레이어 이름 적용
{프로젝트}.Tests.Unit // Biz. 관심사, deterministic {프로젝트}.Tests.Integration // Tech. 관심사, Non-deterministic
- 중재자와 메시지: Mediator 패턴
- 입/출력 메시지를 이용하여 Mediator로 "Biz. 관심사"를 간접적으로 호출합니다.
- 런타임에도 메시지 핸들러 인스턴스를 직접 참조하지 않습니다.
- Mediator 패턴
- 입/출력 메시지는 메시지 핸들러 Signature에 정의되어 있습니다(Known).
- TODO. 예제 코드
- Strategy 패턴
- 메시지 핸들러 Signature에 정의 안된 출력을 처리합니다.
- 인터페이스: Decorator 패턴
- 메시지 핸들러 호출 전후에 추가적인 공통 기능을 손쉽게 추가할 수 있습니다.
- TODO. 예제 코드
- 데이터 모델 분리
- 메시지 분리
- TODO
- TODO
- TODO: Railway-Oriented Programming
- TODO: 로그 통합
{솔루션}
├─ README.md
├─ {솔루션}.sln
│
│ // 형상관리
├─ .gitignore # Git 형상관리 제외 대상
├─ .gitattributes # Git 형상관리 파일 처리
│
│ // .NET
├─ global.json # 빌드 버전
├─ nuget.config # NuGet 저장소
├─ dotnet-tools.json # .NET 로컬 도구
├─ Directory.Build.props # 빌드 옵션
├─ Directory.Packages.props # 패키지 버전
├─ .editorconfig # 코드 컨벤션
│
│ // 컨테이너
├─ .dockerignore # Dockerfile 빌드 제외 대상
├─ Dockerfile # 도커 파일
├─ docker-compose.yml # 도커 컴포즈
├─ docker-compose.override.yml # 도커 컴포즈
├─ launchSettings.json # 도커 컴포즈 구성
├─ docker-compose.dcproj # 도커 컴포즈 프로젝트
.gitignore
: Git 형상 관리에서 제외할 파일과 폴더를 지정하는 파일입니다.dotnet new gitignore
- Verify Received and Verified files
*.received.*
- Verify Received and Verified files
.gitattributes
: Git 형상 관리에서 파일의 속성과 처리 방식을 지정하는 파일입니다.- Verify Received and Verified files, Text file settings
*.verified.txt text eol=lf working-tree-encoding=UTF-8 *.verified.xml text eol=lf working-tree-encoding=UTF-8 *.verified.json text eol=lf working-tree-encoding=UTF-8
- text: .verified.txt, .verified.xml, .verified.json 확장자를 가진 파일들이 모두 텍스트 파일로 처리되며,
- eol=lf: 체크아웃 시 개행 문자가 LF(Line Feed)로 설정되고,
- working-tree-encoding=UTF-8: UTF-8 인코딩이 사용되도록 합니다.
- Verify Received and Verified files, Text file settings
global.json
파일: .NET SDK 버전을 설정하는 파일입니다.- 버전 형식: "global.json 개요", 지정된 버전에서부터 상위 버전(rollForward)
x.y.znn
- x:
major
- y:
minor
- z:
feature
, 0 ~ 9 - n:
patch
, 0 ~ 99
- x:
- 예제
latestFeature
: 8.0.302 이상 8.0.xxx 버전(예: 8.0.303 또는 8.0.402){ "sdk": { "version": "8.0.302", "rollForward": "latestFeature" } }
latestPatch
: 8.0.102 이상 8.0.1xx 버전(예: 8.0.103 또는 8.0.199){ "sdk": { "version": "8.0.102", "rollForward": "latestPatch" } }
dotnet --info
: 현재 경로의 .NET SDK 정보를 출력합니다.dotnet --info .NET SDK: # global.json으로 결정된 .NET SDK 버전 Version: 8.0.100 Commit: 57efcf1350 Workload version: 8.0.100-manifests.aea97431 Host: # 호스트에 설치된 .NET Runtime 최진 버전 Version: 8.0.7 Architecture: x64 Commit: 2aade6beb0 .NET SDKs installed: # 호스트에 설치된 .NET SDK 버전 목록 5.0.301 [C:\Program Files\dotnet\sdk] 6.0.100 [C:\Program Files\dotnet\sdk] 7.0.100 [C:\Program Files\dotnet\sdk] 8.0.100 [C:\Program Files\dotnet\sdk] 8.0.303 [C:\Program Files\dotnet\sdk] global.json file: # 인지된 global.json C:\Workspace\Helloworld\global.json
dotnet --version
: 현재 경로의 global.json으로 결정된 .NET SDK 버전을 출력합니다.dotnet --version 8.0.100 # global.json으로 결정된 .NET SDK 버전
nuget.config
: NuGet 패키지 관리에서 패키지 소스, 설정, 자격 증명 등을 구성하는 파일입니다.dotnet new nuget.config
- nuget.config 파일
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageRestore> <add key="enabled" value="True" /> <add key="automatic" value="True" /> </packageRestore> <packageSources> <clear /> <add key="nuget.org" value="https://api.nuget.org/v3/index.json" /> </packageSources> <packageSourceMapping> <packageSource key="nuget.org"> <package pattern="*" /> </packageSource> </packageSourceMapping> <bindingRedirects> <add key="skip" value="False" /> </bindingRedirects> <packageManagement> <add key="format" value="0" /> <add key="disabled" value="False" /> </packageManagement> </configuration>
Directory.Build.props
: 여러 프로젝트에 공통 빌드 속성을 지정하는 파일입니다.{솔루션} ├─ {솔루션}.sln ├─ Directory.Build.props (1) 모든 프로젝트만 대상 │ ├─ Src │ ├─ {프로젝트1} │ └─ {프로젝트2} │ └─ Tests ├─ Directory.Build.props (2) 모든 프로젝트만 대상 + 테스트 프로젝트만 대상 ├─ {테스트 프로젝트1} └─ {테스트 프로젝트2}
(1), Directory.Build.props
: 모든 프로젝트 대상<Project> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <!-- 경고를 Error로 취급합니다 --> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> </Project>
(2), Directory.Build.props
: 테스트 프로젝트만 대상<Project> <!-- 상위 Directory.Build.props 파일 지정--> <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" /> <!-- 테스트 프로젝트 공통 속성 --> <PropertyGroup> <IsPackable>false</IsPackable> <IsTestProject>true</IsTestProject> </PropertyGroup> <!-- 솔루션 탐색기에서 TestResults 폴더 제외 --> <ItemGroup> <None Remove="TestResults\**" /> </ItemGroup> <!-- xunit.runner.json 설정 --> <ItemGroup> <Content Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" /> </ItemGroup> </Project>
Directory.Packages.props
: 여러 프로젝트에 공통 패키지 버전을 지정하는 파일입니다.
.editorconfig
dotnet-tools.json
: %USERPROFILE%.dotnet\tools
.dockerignore
: Docker가 이미지를 만들 때 제외할 파일과 폴더를 지정하는 파일입니다.
public class TimeUtility
{
// Pure
public static string IsAmOrPm(DateTime specificTime)
{
return specificTime.Hour < 12 ? "AM" : "PM";
}
}
// Pure
DateTime specificTime = new DateTime(2024, 8, 7, 10, 30, 0);
string isAmOrPm = TimeUtility.IsAmOrPm(specificTime);
// Impure
DateTime now = DateTime.Now();
string isAmOrPm = TimeUtility.IsAmOrPm(now);
- .NET 8.x
- Visual Studio Code
- C#
C# Dev Kit- Code Spell Checker
- Git Graph
- Paste Image
- Trailing Spaces
- Markdown Preview Enhanced
- VSCode Progressive Increment
GitHub ActionsCodecov YAML Validator- REST Client
Ulid
: GUIDQuartz
: 백그라운드 작업MediatR
: Mediator 패턴EF Core
: ORMOpenTelemetry
: TelemetryFluentValidation
: 유효성 검사 선언형
xunit
: 단위 테스트Verify.Xunit
: Snapshot 테스트FluentAssertions
: Assert 선언형NetArchTest.Rules
: 아키텍처 테스트coverlet.collector
: 코드 커버리지Xunit.DependencyInjection
: xUnit 의존성Microsoft.AspNetCore.Mvc.Testing
: 통합 테스트
docusaurus
verify.tool
- DDD-NoDuplicates
- DDDPracticeDomainEvents: 11장 예제 확장
- CleanArchitecture | ardalis
- CleanArchitecture: gRPC 테스트 포함
- pluralsight-ddd-fundamentals
- modular-monolith-with-ddd: CQRS
- clean-architecture-core: EF Core 분리
- DDD 핵심만 빠르게 이해하기
- Bounded Contexts | The difference between domains, subdomains and bounded contexts
- Bounded Contexts | Strategic DDD Remote Collaboration Toolkit
- Tell Don't Ask(≒ Anemic Domain Model, Law of Demeter, CQRS)
- watch
- /tmp
- Collect metrics: 콘솔 기반 매트릭 -> Prometheus
- ASP.NET Core metrics: ASP.NET WebApi, 통합 테스트
- Best way Health Checks in .NET Core
- Automatic Web API Heath Checking using .NET with Dapper
- .NET app health checks in C#: 콘솔 기반 HealthCheck
- ASP.NET Core의 상태 검사
- .net 5.0 Integration Test, Health check endpoint not found: 테스트
- CodeMaze | Architecture | Clean Architecture in .NET
- CodeMaze | Architecture | Onion Architecture in ASP.NET Core
- CodeMaze | Architecture | What are the Differences Between Onion Architecture and Clean Architecture in .NET?
- CodeMaze | Architecture | Vertical Slice Architecture in ASP.NET Core
- CodeMaze | Tesing | How to Test IServiceCollection Registrations in .NET
- CodeMaze | Tesing | Architecture Tests in .NET with NetArchTest.Rules
- CodeMaze | Tesing | CQRS and MediatR in ASP.NET Core
- CodeMaze | MediatR | CQRS Validation Pipeline with MediatR and FluentValidation
- CodeMaze | MediatR | CQRS and MediatR in ASP.NET Core
- CodeMaze | MediatR | MediatR Publish and Send Methods
- CodeMaze | MediatR | Global Exception Handling for MediatR Requests
- CodeMaze | MediatR | How to Solve The type ‘TRequest’ Cannot be Used as Type Parameter in MediatR
- Packt | DDD | Hands-On-Domain-Driven-Design-with-.NET-Core
- Packt | Architecture | Clean-Architecture-with-.NET