반응형
Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

지식조각모음

4장 타입 코드 처리하기 본문

책/Five Lines of Code

4장 타입 코드 처리하기

y00 2023. 8. 17. 17:17
반응형

이번 장에서 다룰 내용

- if 문에서 else 사용하지 말 것과 switch를 사용하지 말 것으로 이른 바인딩 제거하기
- 클래스로 타입 코드 대체와 클래스로의 코드 이관으로 if 문 제거하기
- 메서드 전문화로 문제가 있는 일반성 제거하기
- 인터페이스에서만 상속받을 것으로 코드 간 커플링(결합) 방지하기
- 메서드의 인라인화 및 삭제 후 컴파일하기를 통한 불필요한 메서드 제거

첫 번째 규칙

if 문에서 else를 사용하지 말 것

정의

프로그램에서 이해하지 못하는 타입(형)인지를 검사하지 않는 한 if 문에서 else를 사용하지 말 것

설명

if-else는 하드코딩된 결정이다. 하드코딩된 상수가 좋지 않는 것처럼 하드코딩된 결정도 좋지 않다. 이를 막기 위해선 if 구문을 else 구문과 함께 사용하지 않는 것이 좋다. 그러기 위해서 우리가 무엇을 검사하는지에 주의를 기울여야 한다.

예를 들어 사용자가 어떤 키(string)를 눌렀는지 검사하는데, 이 경우에는 else if 구문을 사용해야 한다. 하지만 이렇게 외부에서 입력 받는 프로그램 경계에서 일어나는 일은 그래도 괜찮다. 하지만 그 외의 경우에는 규칙을 적용하자. 

이 규칙을 검증하기 위해서는 다음과 같은 방법이 있다.

  • else 찾기
  • 외부의 데이터 타입을 판단해야 하는 경우 내부에서 제어 가능한 데이터 타입으로 매핑
  • 조기에 return 값을 결정하기 어려운 경우, 시작 시에 검증 수행하기

예 - 시작 시에 검증 수행하기

// 변경 전
function average(arr: number[]) {
	if (size(arr) === 0)
		throw "Empty array not allowed";
	else
		return sum(arr) / size(arr);
}

// 변경 후
function assertNotEmpty(arr: number[]) {
	if (size(arr) === 0)
		throw "Empty array not allowed";
}

function average(arr: number[]) {
	assertNotEmpty(arr);
	
	sum(arr) / size(arr);
}

스멜

이른 바인딩(early binding)이란?
프로그램을 컴파일할 때 if-else 같은 의사결정 동작은 컴파일 시 처리되어 애플리케이션에 고정되며 재컴파일 없이는 수정할 수 없다.
이와 반대되는 개념이 늦은 바인딩(late binding)이다.

이른 바인딩은 if 문을 수정해야 변경할 수 있기 때문에 추가에 의한 변경을 방해한다. OCP를 어기게 된다.

의도

if는 조건 연산자로 흐름을 제어한다. 하지만 객체지향 프로그래밍에서는 객체를 사용하여 흐름을 제어할 수 있다. 예를 들어 인터페이스를 사용한 두 가지 다른 구현이 있는 경우 인스턴스화하는 클래스에 따라 실행할 코드를 결정할 수 있다.

참조

  • 클래스로 타입 코드 대체
  • 전략 패턴의 도입

 

리팩터링 패턴: 클래스로 타입 코드 대체

만약 타입 코드가 있다면 열거형으로 변환한다. 그리고 열거형을 인터페이스로 변환하고 열거형의 값들은 클래스가 된다. 열거형 대신 인터페이스(클래스)를 사용하면 인터페이스를 구현한 새로운 클래스가 추가되어도 다른 코드를 수정하지 않아도 되는 장점이 있다.

// 다음과 같은 타입 코드를
const SMALL = 33;
const MEDIUM = 37;
const LARGE = 42;

// 열거형으로 변환
enum TShirtSizes {
    SMALL = 33,
    MEDIUM = 37,
    LARGE = 42
}

절차

  1. 임시 이름을 가진 새로운 인터페이스를 도입한다. 인터페이스에는 열거형의 각 값에 대한 메서드가 있어야 한다.
  2. 열거형의 각 값에 해당하는 클래스를 만든다.
  3. 열거형을 다른 이름으로 바꾼다. 그러면 기존 코드에서 오류가 발생한다.
  4. 타입을 인터페이스로 변경하고 새로운 메서드로 대체한다.
  5. 열거형 값에 대한 참조가 남아있다면 새로운 클래스를 인스턴스화하여 교체한다.
  6. 오류가 없다면 완전히 바꾼다.

예제

enum TrafficLight {
	RED, YELLOW, GREEN
}

function updateCarForLight(currnet: TrafficLight) {
	if (current === TrafficLight.RED)
    	car.stop();
    else
    	car.drive();
}

// 1. 임시 이름을 가진 새로운 인터페이스를 도입
interface TrafficLight2 {
	isRed(): boolean;
	isYellow(): boolean;
	isGreen(): boolean;
}

// 2. 열거형의 각 값에 해당하는 클래스를 만든다.
class Red implements TrafficLight2 {
	isRed() { return true; }
	isYellow() { return false; }
	isGreen() { return false; }
}

class Yellow implements TrafficLight2 { ... }
class Green implements TrafficLight2 { ... }
// 3. 열거형을 다른 이름으로 변경하여 기존에 열거형을 사용하는 모든 곳에서 에러를 발생시킨다.
enum RawTrafficLight {
	RED, YELLOW, GREEN
}

// 4. 타입을 인터페이스로 변경하고 새로운 메서드로 대체한다. 
function updateCarForLight(currnet: TrafficLight) {
	if (current.isRed)
    	car.stop();
    else
    	car.drive();
}

 

리팩터링 패턴: 클래스로의 코드 이관

설명

기능을 클래스로 옮긴다. 클래스로 타임 코드 대체 패턴의 연장선이다. 특정 값과 연결된 기능이 값에 해당하는 클래스로 이동하기 때문에 불변속성을 지역화 하는 데 도움이 된다. 가장 간단한 방법으로 항상 메서드 전체를 클래스로 옮긴다고 생각하자

절차

  1. if 문에 해당하는 내용을 제거하기 위해 인터페이스에 새로운 메서드를 만든다.
  2. 구현된 모든 클래스에 if 문의 내용을 붙여넣어 새로운 메서드를 구현한다. 이제 컨텍스트를 this로 대체할 수 있다.
  3. 새로 구현된 메서드를 점검한다.
    1. 조건식의 true, false를 결정한다.
    2. 미리 계산할 수 있는 계산은 수행해놓는다.
    3. 메서드명을 적절한 이름으로 변경한다.
  4. 원래 함수의 본문을 새로운 메서드에 대한 호출로 바꾼다.

예제

위의 예제를 리펙토링 해보자.

interface TrafficLight {
	isRed(): boolean;
	isYellow(): boolean;
	isGreen(): boolean;
}

class Red implements TrafficLight {
	isRed() { return true; }
	isYellow() { return false; }
	isGreen() { return false; }
}

class Yellow implements TrafficLight { ... }
class Green implements TrafficLight { ... }

// 클래스로 이관할 코드
function updateCarForLight(current: TrafficLight) {
	if (current.isRed)
		car.stop();
	else
		car.drive();
}

1. 인터페이스에 새로운 메서드를 만든다

interface TrafficLight {
	isRed(): boolean;
	isYellow(): boolean;
	isGreen(): boolean;
	updateCar(): void;
}

2. 이관할 코드를 모든 클래스에 붙여넣는다. 아직 함수명이 맞지 않기 때문에 에러가 발생한다.

class Red implements TrafficLight {
	isRed() { return true; }
	isYellow() { return false; }
	isGreen() { return false; }
    
	updateCarForLight() {
		if (this.isRed)
			car.stop();
		else
			car.drive();
	}
}

3. 새로 구현된 메서드를 점검한다.

class Red implements TrafficLight {
	// 생략
	updateCar() {
    	// 조건식의 true, false 결정
        // if문은 항상 true이므로 car.stop()함수만 남는다.
    	// if (true)
    		car.stop();
    }
}


class Yellow implements TrafficLight {
	// 생략
	updateCar() {
    	// 조건식의 true, false 결정
        // if문은 항상 false이므로 car.drive()함수만 남는다.
    	// if (false) car.stop();
        // else
    		car.drive();
    }
}

class Green implements TrafficLight {
	// 생략
	updateCar() { car.drive(); }
}

4. 원래 함수의 본문을 새로운 메서드에 대한 호출로 바꾼다.

function updateCarForLight(current: TrafficLight) {
	current.updateCar();
}

추가 자료

  • 메서드 이동

 

리팩터링 패턴: 메서드의 인라인화

설명

가독성에 도움이 되지 않는 메서드를 제거하기 위해 메서드를 호출하는 모든 곳으로 코드를 옮긴다. 흔히 메서드가 한 줄만 있는 경우 이 작업을 수행한다.

다만 주의할 점은 이렇게 인라인화를 했을 때 '호출 또는 전달, 한 가지만 할 것'이라는 규칙을 어기는지 확인해야 한다. 잘못하면 동일한 추상화 수준에 있는 코드를 어긋나게 할 수 있다. 또는 인라인화를 했을 때 오히려 가독성이 떨어지는 경우가 있다. 이런 경우가 없는지 주의하면서 리텍터링을 적용한다.

절차

  1. 메서드 이름을 임시로 변경한다. 그러면 함수를 사용하는 모든 곳에서 컴파일러 오류가 발생한다.
  2. 메서드 본문을 복사하여 메서드가 호출된 모든 곳을 복사된 코드로 교체한다.
  3. 오류가 없다면 원래 메서드를 삭제한다.

 

리팩터링 패턴: 메서드 전문화

설명

일반화하고 재사용 하는 것은 좋지만 책임이 흐려지면 오히려 문제가 생길수 있다. 이 리팩터링 패턴을 사용해서 더 적은 위치에서 메서드를 호출하게 한다.

절차

  1. 전문화하려는 메서드를 복제한다.
  2. 메서드 중 하나의 이름을 새로 사용할 메서드의 이름으로 변경하고 전문화하려는 매개변수를 제거(또는 교체)한다.
  3. 매개변수 제거에 따라 메서드를 수정해서 오류가 없도록 한다.
  4. 이전의 메서드를 새로 전문화한 것을 사용하도록 변경한다.

두 번째 규칙

switch를 사용하지 말 것

정의

default 케이스가 없고 모든 case에 반환 값이 있는 경우가 아니라면 switch를 사용하지 마십시오

설명

switch는 아래와 같은 두 가지 '편의성'을 허용하기 때문에 버그가 발생할 수 있다.

  1. case를 분석할 때 모든 값에 대한 처리를 실행할 필요가 없다.
    • default 값을 중복 없이 여러 값을 지정할 수 있다.
    • switch를 사용할 경우 어떤 것을 처리할지는 불변속성이다.
    • 이런 두 가지 이유 때문에 default가 우리가 의도한 것인지, 추가할 case를 놓친 것인지 알 수 없다.
    • 해결 방법: 기능을 default에 두지 않는다.
  2. break 키워드를 만나기 전까지 케이스를 연속해서 실행한다.
    • 케이스를 연속해서 실행하는 동안 break 키워드를 누락했는지 아닌지 알아채지 못할 수 있다.
    • 해결 방법: 모든 case에 return을 지정해서 폴스루(fall-through) 문제를 해결한다.

 

세 번째 규칙

인터페이스에서만 상속받을 것

정의

상속은 오직 인터페이스를 통해서만 받는다

설명

인터페이스 대신 추상 클래스를 사용할 수 있다. 하지만 추상 클래스를 사용하는 것 보다 인터페이스를 사용하는 것이 더 좋다. 왜냐하면 인터페이스는 새로운 클래스를 구현할 때마다 개발자가 능동적으로 작업해주어야 한다. 이를 통해 속성을 잊어버리거나 오버라이드를 잘못하는 경우를 방지할 수 있다.

추상 클래스를 사용하면 중복이 줄고 코드의 줄을 줄일수 있다. 하지만 코드 공유는 커플링을 유발한다.

예를 들어 추상 클래스에 methodA와 methodB라는 두 가지 메서드가 구현되어 있다고 가정한다. 한 하위 클래스에서는 methodA만 필요하고 다른 하위 클래스에서는 methodB만 있으면 된다고 할때, 불필요한 methodB가 상속된다. 유일한 옵션은 메서드 중 하나를 빈 버전으로 재정의하는 것이다. 하지만 이를 잊어버리고 지나가도 컴파일러에서는 아무런 문제도 생기지 않는다. 이렇게 되면 런타임에서 이슈가 발생할 가능성이 있다. 반면 인터페이스를 사용하는 경우 무조건 모든 메서드를 구현해야하므로 이런 이슈를 피할 수 있다.


코드 중복

코드 중복은 필요한 경우 여기저기 흩어져 있는 코드를 찾아 전체를 바꾸어야 하기 때문에 유지보수에 좋지 않다. 만약 한 군데만 바꾼다면 두 가지 다른 기능이 생기게 된다.

리팩터링 패턴: 삭제 후 컴파일하기

설명

인터페이스의 전체 범위를 알고 있을 때 인터페이스에서 사용하지 않는 메서드를 제거하는 방법이다. 메서드를 삭제한 뒤 컴파일러에서 에러가 나는지 확인한다. 일반적인 메서드는 보통 IDE가 사용여부를 알려주지만 인터페이스는 범위 내에서만 사용된다는 것을 알 수 없다.

절차

  1. 컴파일한다. 오류가 없어야 한다.
  2. 인터페이스에서 메서드를 삭제한다.
  3. 컴파일 한다.
    1. 컴파일 에러가 발생하면 삭제를 취소한다
    2. 그렇지 않으면 오류 없이 해당 메서드를 삭제할 수 있는지 확인한다.

예제

interface A {
	m1(): void;
	m2(): void;
}

class B implements A {
	m1() { console.log("m1"); }
	m2() { this.m3(); }
	m3() { console.log("m3"); }
}

let a = new B();
a.m1();

m2() 메서드를 삭제하려면

  1. 컴파일하여 오류가 없음을 확인한다.
  2. 인터페이스에서 m2 메서드를 삭제한다.
  3. 에러가 나지 않으므로 성공
interface A {
	m1(): void;
}

class B implements A {
	m1() { console.log("m1"); }
	m3() { console.log("m3"); }
}

let a = new B();
a.m1();
반응형

' > Five Lines of Code' 카테고리의 다른 글

7장 컴파일러와의 협업  (0) 2023.09.04
8장 주석 자제하기  (0) 2023.09.04
3장 긴 코드 조각내기  (0) 2023.08.05
2장 리팩터링 깊게 들여다보기  (0) 2023.08.01
1장 리팩터링 리팩터링하기  (0) 2023.08.01