[좋은 코드, 나쁜 코드] 7장. 코드를 오용하기 어렵게 만들라

cje·2023년 7월 15일
0
post-thumbnail

코드가 오용하기 쉽게 작성된다면, 조만간 오용될 가능성이 있고 소프트웨어가 올바르게 작동하지 않을 것이다.


불변 객체로 만드는 것을 고려하라

가변 클래스는 오용하기 쉽다

입력 매개변수를 변경 가능하게 하는 것은 바람직하지 않은 관행이다.

TextOptions 클래스는 가변적이라 해당 인스턴스를 전달받는 모든 코드는 이 객체를 변경할 수 있어 오용의 위험이 있다.

class TextOptions {
	constructor(private font: Font, private fontSize: number) {}

	setFont(font: Font) {
		this.font = font;
	}

	setFontSize(fontSize: number) {
		this.fontSize = fontSize;
	}
}
class MessageBox {
	//...
  	renderTitle(title: string, baseStyle: TextOptions) {
      	// TextOptions 인스턴스 객체 변경
    	baseStyle.setFontSize(25);
    }
}

가장 쉽게 TextOptions 클래스를 불변 객체(클래스)로 만들기 위해서 setter를 모두 제거하고 getter만 남기는 것이다.

또는 불변성에 대한 디자인 패턴을 사용하는 것인데, 빌더 패턴쓰기 시 복사 패턴이 존재한다.


빌더 패턴

빌더 패턴은 값의 일부(또는 전체)가 선택 사항일 때 불변적 객체를 만드는 유용한 방법이다.

class TextOptions {
	constructor(private font: Font, private fontSize: number) {}

	getFont() {
		return this.font;
	}

	getFontSize() {
		return this.fontSize;
	}
}

class TextOptionsBuilder {
	constructor(private font: Font, private fontSize?: number) {} // 빌더는 생성자를 통해 필수값을 받는다.
  
	setFontSize(fontSize: number) { // 필수적이지 않은 값을 받는다.
		this.fontSize = fontSize;
		return this; // setter 함수는 함수 호출을 연이어 할 수 있도록 this를 반환한다.
	}

	build() {
		reutrn new TextOptions(this.font, this.fontSize); // 모든 값이 정해지고 나면 호출하는 쪽에서는 TextOptions 객체를 얻기 위한 build를 호출한다.
	}
}
const getDefaultTextOptions = () => {
	return new TextOptionsBuilder(FONT.ARIAL)
      .setFontSize(25)
      .build();
}

쓰기 시 복사 패턴

이렇게 작성하면 TextOptions 객체의 변경된 버전이 필요한 경우, 원본 객체에 영향을 미치지 않고도 변경된 복사본을 쉽게 얻을 수 있다.

class TextOptions {
	constructor(private font: Font, private fontSize: number) {}

	withFont(newFont: Font) {
		return new TextOptions(newFont, this.fontSize); // 폰트만 변경된 TextOptions 객체 반환
	}

	withFontSize(newFontSize: number) {
		return new TextOptions(this.font, newFontSize); // 폰트 크기만 변경된 TextOptions 객체 반환
	}
  
  	//...
}
function getDefaultTextOptions() {
	return new TextOptionsBuilder(FONT.ARIAL)
      .withFontSize(12);
}

오용이 불가능한 클래스를 만드려면 setter 함수를 제거하고 인스턴스를 생성할 때만 값을 제공하면 된다.


지나치게 일반적인 데이터 유형을 피하라

지나치게 일반적인 유형은 오용될 수 있다.

만약 지도에서 위치를 처리하는 코드를 작성할 때 위도와 경도를 배열로 관리하면 오용하기 쉬워진다. 만약 위치 값의 타입을 number[]로 나타낸다면 개발자는 타입 자체로 어떤 값을 나타내는지 알 수 없다. 또한 개발자는 배열에서 어떤 요소가 위도인지, 경도인지 혼동하기 쉽고, 타입 안정성이 없어 정상적으로 컴파일되지만 런타임에 문제가 발생할 수 있다.

즉, 설명이 부족하고 허용하는 범위가 넓을수록 코드 오용은 쉬워진다.


페어 유형은 오용하기 쉽다.

페어 유형은 동일하거나 다른 종류의 값을 두 개 저장하는 것을 말한다.

type Locations = [number, number][];

만약 [number, number][]와 같은 타입을 사용한다면 위도와 경도의 순서를 혼동하기 쉬우며 무슨 의미인지 이해하기 어렵다.

무언가를 나타내기 위해 새로운 클래스(구조체, 타입)를 정의하는 것은 많은 노력이 들거나 불필요한 것처럼 보일 수 있지만, 대부분 보기보다 노력이 덜 들어가고 다른 개발자가 코드를 읽을 때 이해하기 쉽고 버그의 가능성도 줄여준다.

오해의 소지를 줄이는 방법은 위도와 경도를 나타내는 전용 타입(클래스)를 만들어 해결할 수 있다.


시간 처리

시간을 다룰 때 코드를 잘못 사용하고 혼동을 일으킬 여지가 굉장히 많다.

시간을 다룰 때 일반적으로 정수를 사용하는데 정수는 매우 일반적인 유형이므로 코드가 오용되기 쉽다.

그리고 가장 일반적인 단위는 ms 또는 s이지만 경우에 따라 μs(마이크로초)와 같은 단위도 사용하기에 함수 이름이나 매개변수 이름, 주석문으로 단위를 나타낼 수 있지만 여전히 개발자에게 혼동을 주기 쉽다.

또한 서버가 다른 위치에서 실행되고 시스템을 다른 표준 시간대로 설정한 경우 시간대에 대한 처리 오류가 발생할 수 있다. 예를 들어 유럽에 있는 서버가 처리한 날짜값이 캘리포니아에 있는 서버에 의해 저장될 수도 있다는 것이다.


date-fns

날짜 값을 다룰 때, 여러 형식을 출력해야 할 때가 있다. 예를 들어, 23년 7월 16일, 16일 전, 16분 전과 같이 말이다. 이렇게 여러 형식으로 쉽게 제공할 수 있는 라이브러리이다.

  • tree-shaking 지원
  • TypeScript
  • Immutable & Pure

데이터에 대해 진실의 원천을 하나만 가져야 한다

데이터는 기본 데이터와 파생 데이터로 나뉜다. 파생 데이터는 주어진 기본 데이터에 기반해 계산된 데이터를 말한다.

// 계좌 잔액은 credit - debit이어야 함!
new UserAccount(credit, debit, debit - credit);

기본 데이터와 파생 데이터를 모두 처리하는 코드를 작성할 때 위와 같이 논리적으로 올바르지 않은 코드를 작성하기 쉽다.

만약 UserAccount 클래스를 생성할 때, 대변, 차변, 계좌 잔액(대변 - 차변)의 값으로 생성한다면 계좌 잔액에 대한 값은 대변과 차변의 값에서 파생될 수 있기에 중복 정보를 제공하는 셈이며 논리적으로 잘못된 인스턴스를 생성할 수 있다.


기본 데이터를 유일한 진실의 원천으로 사용하라

데이터 모델이 논리적으로 잘못된 상태를 허용하는지 항상 확인할 필요가 있다.

class UserAccount {
	constructor(private credit: number, private debit: number) {}

	getBalance() {
		return this.credit - this.debit;
	}
}

SSR환경에서의 Micro-Frontend 구현과 퍼포먼스 향상을 위한 캐시전략

profile
💭

0개의 댓글