[Five Lines of Code] 코드 구조 따르기
🔖 주제
Five Lines of Code는 TypeScript를 기반으로 코드를 어떻게 리펙토링하는지 A to Z 형식으로 소개하는 책입니다.
사내 스터디에서 총 13챕터로 나누어 소개하고 있으며, 이번 주제는 코드 구조 따르기 11챕터 입니다
📓 설명
콘웨이의 법칙
"소프트웨어 구조는 개발 조직의 커뮤니케이션 구조를 반영한다."
실용주의 프로그래머라는 서적에서 콘웨이의 법칙을 역으로 이용하는 방법으로 다음과 같은 예시를 이야기합니다.
“여러 지역으로 나뉜 팀은 시스템의 모듈화 및 분산화된 소프트웨어를 만드는 경향이 있다.”
- 개발 결과물을 주고받는 과정에 모듈화를 발생시킬 수 있다.
- 자료, 동작의 책임이 지역에 묶일 때 분산화를 고려할 수 있다.
현재 사내 조직도[백엔드 / 프론트엔드]를 비교해 보면 어떤 상황일까?
백엔드 / 프론트엔드가 독립적으로 나뉘어 있기에 각각의 API와 인터페이스를 설계할 수 있었을 것입니다. 하지만 개발 과정에 특정 상황에 특정 UI가 보이기 위해 어떠한 값이 필요하고 예외처리 동작이 필요할지 고민합니다.
- 도메인 분석(기획) 팀의 존재에서 영향을 받지 않았을까
도메인 분석(기획) 팀은 비즈니스 요구사항을 이해하고, 처리해야 할 로직을 정의하여 각 팀에 전달하게 됩니다.
이 커뮤니케이션 과정에 서로 간의 정보를 알게 되어 팀 간의 결합이 높아진 것이 아닌가 생각해 봅니다.
행위를 코드화하는 세 가지 방법
행위(Behavior)는 “프로그램으로 모델링하고자 하는 현실 세계의 행동”으로, 우린 일반적으로 ‘동작’이라는 용어로 표현합니다.
첫 번째 “제어 흐름에 행위 표현하기(코드화하기)”
작성한 구문에 따라 프로그램이 순차적으로 동작하는 흐름을 제어 흐름이라 합니다. 책에선 “제어문(control flow statements)”을 중점에 두고 이야기합니다.
메서드 추출(3.2.1) 예시는 minimum 메서드의 제어흐름에서 min을 추출(행위 표현)하였습니다.
// 변경 전
function minimum(arr: number[][]) {
let result = Number.POSITIVE_INFINITY;
for (let x = 0; x < arr.length; x++) {
for (let y = 0; y < arr[x].length; y++) {
if (result > arr[x][y]) result = arr[x][y];
}
}
return result;
}
// 변경 후
function minimum(arr: number[][]) {
let result = Number.POSITIVE_INFINITY;
for (let x = 0; x < arr.length; x++) {
for (let y = 0; y < arr[x].length; y++) {
result = min(result, arr, x, y);
}
}
return result;
}
function min(result: number, arr: number[][], x: number, y: number) {
if (result > arr[x][y]) result = arr[x][y];
return result;
}
두 번째 “데이터 구조에 행위 표현하기(코드화하기)”
책에선 “무한 루프(스트림)”라는 주제로 while, for 같은 제어문의 제어 흐름에서 타입으로 재귀 데이터 구조를 옮기는 것을 이야기합니다.
class Rec {
constructor(public readonly f: (_:Rec) => void) { }
}
function loop() {
let helper = (r: Rec) => r.f(r);
helper(new Rec(helper));
}
세 번째 “데이터에 행위 표현하기(코드화하기)”
마지막 방식은 아래의 코드로 유추할 수 있듯 컴파일러의 도움을 받기 어려운 사각지대에 맞닥뜨리기 쉬운 구조입니다.
function loop() {
let a = [() => { }];
a[0] = () => a[0]();
a[0]();
}
🆘 구조 노출을 위한 코드 추가
추가라는 단어가 무색하게 책에선 “구조를 노출하는 방법에 대한 코드를 기술”하지 않아 이해에 어려움을 겪었습니다.
이해한 내용은 “기본 구조에 대한 확신이 없다면 리펙터링을 고려하는 것보다 정확성을 집중해야한다.” 입니다.
리펙터링을 대비하여 작은 단위로 기능을 나누다보면 복잡성이 증가하며, 설계를 고려한 개발이 어려울 수 있습니다.
- 빠른 변경이 적용될 수 있도록 클래스, 인터페이스로 행위를 표현하는 것 보다 앞서 설명된 데이터, 제어 흐름에 행위를 표현하는 방법이 나을 수 있다 합니다.(이는 모든 상황에 정답은 아니며 각자의 정답이 존재한다 생각합니다.)
🚂 예측 대신 관찰, 그리고 경험적 기술 사용
향후 개발할 기능, 변경될 기능을 예측하는 행동은 도움이 되기보다 방해가 된다고 이야기합니다.
처음 읽었을 때는 “여태 개발의 경험에서 변경이 발생하지 않았던 기능이 적었는데 미리 예측하여 준비해두는 것도 방해인가?” 라는 생각을 했습니다.
하지만 앞서 이야기한 구조 노출 이라는 주제를 다시 본 후 생각이 조금은 변했습니다.
책에서 이야기하는 리펙토링은 “행위의 결과를 변경하는 것이 아닌, 취약성을 제거하는 과정”이 주를 이루고 제 경험은 결과의 변경으로 발생하여 생긴 것에 차이가 있기 때문입니다.
경험이 더욱 쌓인 분들은 각자 다른 해결방법을 갖고 있을 수 있으며, 다만 이야기의 본질인 “자신 없으면 예측하지마”는 기억해두고자 합니다.
📵 활용되지 않은 구조 이용
공백을 활용하자
코드 작성 시 연관성이 있는 내용을 붙여서 작성하는 특징(습관)은 메서드 추출 패턴을 활용할 수 있습니다.
// 변경 전
function signUp(credentials: UserCredentials) {
const { loginId, password } = credentials;
if (loginId === undefined || loginId === null) throw new Error('올바른 아이디 형식이 아닙니다.');
if (password === undefined || password === null) throw new Error('올바른 패스워드 형식이 아닙니다.');
if (isLoginIdExists(loginId!)) throw new Error('이미 존재하는 아이디 입니다.');
const userAccount = UserAccount(loginId, password);
save(userAccount);
}
// 변경 후
function signUp(credentials: UserCredentials) {
validateUserCredentials(credentials);
const userAccount = UserAccount(credentials.loginId, credentials.password);
save(userAccount);
}
function validateUserCredentials(credentials:UserCredentials) {
const { loginId, password } = credentials;
if (loginId === undefined || loginId === null) throw new Error('올바른 아이디 형식이 아닙니다.');
if (password === undefined || password === null) throw new Error('올바른 패스워드 형식이 아닙니다.');
if (isLoginIdExists(loginId)) throw new Error('이미 존재하는 아이디 입니다.');
}
공통접사를 활용하자
//변경 전
interface Protocol {...}
class StringProtocol implements Protocol {...}
class JSONProtocol implements Protocol {...}
class ProtobufProtoco implements Protocol {...}
let p = new StringProtocol()
///...
//변경 후
namespace protocol {
export interface Protocol {...}
export class String implements Protocol {...}
export class JSON implements Protocol {...}
export class Protobuf implements Protocol {...}
}
/// ...
let p = new Protocol.String()
///...
런타임 유형을 활용하자
// 변경 전
function foo(obj: Any) {
if (obj instanceof GoodFoo) {
obj.methodGood()
} else if (obj instanceof BadFoo) {
obj.methodBad()
}
}
class GoodFoo {
methodGood(){}
}
class BadFoo {
methodBad() {}
}
// 변경 후
interface Foo {
foo(): void
}
function foo(obj: Foo) {
obj.foo()
}
class GoodFoo implements Foo {
foo() {
this.methodGood()
}
methodGood(){}
}
class BadFoo implements Foo {
foo() {
this.methodBad()
}
methodBad() {}
}
📚 정리
콘웨이의 법칙
- 해당 법칙에서 지적하는 것은 “초기 시스템 설계가 최선의 설계로 이어지지 않는다” 입니다.
- 즉, 설계의 변경이 발생할 수 있으며 이를 자유롭게 하려면 조직 역시 변화에 대비해야함을 의미합니다.
행위를 코드화하는 세 가지 방법
- 제어문에 행위 표현
- 인터페이스, 클래스 등 타입을 통한 행위 표현
- 자료구조형에 기반한 행위 표현
구조 노출을 위한 코드 추가
- 기본 구조에 대한 확신이 없다면 리팩터링 노력을 줄이고 먼저 정확성에 집중해야한다.
- 빠르게 변경할 수 있도록 클래스보다는 열거형이나 루프를 사용하는 것이 좋다. (다만 협업과정에서 충분한 논의가 필요함)
예측 대신 관찰, 그리고 경험적 기술 사용
- 변경 범위를 예측하려는 시도는 코드베이스에 도움이 되기보다 손상을 준다.
- 변경 되지 않으면 아무것도 하지말라
- 예측할 수 없이 변경되는 경우 취약성을 피하기 위해서 리팩터링하라
📦 공유 자료
'Backend > 질문 시리즈' 카테고리의 다른 글
[Tidy First?] Part3 이론을 읽고 나서 (2) | 2024.05.19 |
---|---|
[Five Lines of Code] 코드 삭제의 미학을 읽고 나서 (0) | 2023.07.18 |
[질문-시리즈] Validate! 유효성 검사는 어디에서 해야할까? (0) | 2023.04.07 |
[질문-시리즈] Data에게 TDA(Tell Don’t Ask)를 적용해야 할까? (0) | 2022.10.11 |
[질문-시리즈] 생성자? 정적 팩토리 메서드? 빌더? (0) | 2022.10.06 |