Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

11장 API 리팩터링 #161

Closed
Tracked by #132
fkdl0048 opened this issue Sep 19, 2023 · 0 comments
Closed
Tracked by #132

11장 API 리팩터링 #161

fkdl0048 opened this issue Sep 19, 2023 · 0 comments
Assignees
Milestone

Comments

@fkdl0048
Copy link
Owner

fkdl0048 commented Sep 19, 2023

11. API 리팩터링

모듈과 함수는 소프트웨어를 구성하는 빌딩 블록이며, API는 이 블록들을 끼워 맞추는 연결부다.

이런 API를 이해하기 쉽고 사용하기 쉽게 만드는 일은 중요한 동시에 어렵기도 하다.

그래서 API를 개선하는 방법을 새로 깨달을 때마다 그에 맞게 리팩터링해야 한다.

11.1 질의 함수와 변경 함수 분리하기

public Customer GetTotalOutstandingAndSendBill() {
    double result = 0;
    Enumeration e = _orders.elements();
    while (e.hasMoreElements()) {
        Order each = (Order) e.nextElement();
        result += each.GetAmount();
    }
    SendBill();
    return result;
}
public void SendBill() {
    // 구현부
}

배경

우리는 외부에서 관찰할 수 있는 부수효과가 전혀 없이 값을 반환해주는 함수를 추구해야 한다.

이런 함수는 어느 때건 원하는 만큼 호출해도 아무 문제가 없다.

호출하는 문장의 위치를 호출하는 함수 안 어디로든 옮겨도 되며 테스트하기도 쉽다.

한마디로, 이용할 때 신경 쓸 거리가 매우 적다.

부수효과가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋다.

이를 위한 한 가지 방법은 질의 함수는 모두 부수효과가 없어야 한다.는 규칙을 따르는 것이다.

이를 명령-질의 분리라 하는데, 이 규칙을 절대적으로 신봉하는 프로그래머도 있다.

필자는 100% 동의하지 않지만 그래도 되도록이면 따르려 노력하며 실제로 효과도 많이 봤다.

값을 반환하면서 부수효과가 있는 함수를 발견하면 상태를 변경하는 부분과 질의하는 부분을 분리하려 시도한다. (무조건)

절차

  • 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
  • 새 질의 함수에서 부수효과를 모두 제거한다.
  • 정적 검사를 수행한다.
  • 원래 함수를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값을 사용한다면 질의 함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할 때마다 테스트한다.
  • 원래 함수에서 질의 관련 코드를 제거한다.
  • 테스트한다.

11.2 함수 매개변수화하기

public void TenPercentRaise() {
    _salary *= 1.1;
}

public void FivePercentRaise() {
    _salary *= 1.05;
}
public void Raise(double factor) {
    _salary *= (1 + factor);
}

배경

구 함수의 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐서 중복을 없앨 수 있다.

이렇게 하면 매개변수 값만 바꿔서 여러 곳에서 쓸 수 있으니 함수의 유용성이 커진다.

절차

  • 비슷한 함수 중 하나를 선택한다.
  • 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
  • 이 함수를 호출하는 곳 모두에 적절한 리터럴 값을 추가한다.
  • 테스트한다.
  • 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다. 하나 수정할 때마다 테스트한다.
  • 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. 하나씩 수정할 때마다 테스트한다.

11.3 플래그 인수 제거하기

public void SetDimension(string name, int value)
{
    if (name.Equals("height")) {
        _height = value;
        return;
    }
    if (name.Equals("width")) {
        _width = value;
        return;
    }
}
public int Height {
    set { _height = value; }
}

public int Width {
    set { _width = value; }
}

C#은 프로퍼티로 더 유용하게 처리

배경

플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.

플래그 인수는 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지를 이해하기가 어려워지기 때문에 사용하지 않는 편이 좋다.

문장 구조 자체가 읽는 사람이 해석하며 읽어야 하기에 부담이 큼

특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 편이 훨씬 깔끔하다.

절차

  • 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
  • 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.

11.4 객체 통째로 넘기기

const int low = aRoom.DaysTempRange.Low;
const int high = aRoom.DaysTempRange.High;
if (aPlan.WithinRange(low, high))
if (aPlan.WithinRange(aRoom.DaysTempRange))

배경

하나의 레코드에서 값 두어 개를 가져와 인수로 넘기는 코드 보다 그 값들 대신에 레코드를 통째로 넘기고 함수 본문에서 필요한 값을 꺼내 쓰는 편이 더 나을 때가 많다.

레코드를 통째로 넘기면 변화에 대응하기 쉽다.(공감)

예컨대 그 함수가 더 다양한 데이터를 사용하도록 바뀌어도 매개변수 목록은 수정할 필요가 없다.

그리고 매개변수 목록이 짧아져서 일반적으로는 함수 사용법을 이해하기 쉬워진다.

레코드에 담긴 데이터 중 일부를 받는 함수가 여러 개라면 그 함수들끼리는 같은 데이터를 사용하는 부분이 있을 것이고, 그 부분의 로직이 중복될 가능성이 커진다.

레코드를 통째로 넘긴다면 이런 로직 중복도 없앨 수 있다.

하지만 함수가 레코드 자체에 의존하기를 원치 않을 때는 이 리팩터링을 수행하지 않는데, 레코드와 함수가 서로 다른 모듈에 속한 상황이면 특히 더 그렇다.

어떤 객체로부터 값 몇 개를 얻은 후 그 값들만으로 무언가를 하는 로직이 있다면 그 로직을 객체 안으로 집어넣어야 함을 알려주는 악취로 봐야 한다.

그래서 객체를 통째로 넘기기는 특히 매개변수 객체 만들기 후, 즉 산재한 수많은 데이터 더미를 새로운 객체로 묶은 후 적용하곤 한다.

한편, 한 객체가 제공하는 기능 중 항상 똑같은 일부만을 사용하는 코드가 많다면, 그 기능만 따로 묶어서 클래스로 추출하라는 신호일 수 있다.

절차

  • 매개변수들을 원하는 형태로 받는 빈 함수를 만든다.
  • 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
  • 정적 검사를 수행한다.
  • 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트한다.
  • 호출자를 모두 수정했다면 원래 함수를 인라인한다.
  • 새 함수의 이름을 적절히 수정하고 모든 호출자에 반영한다.

11.5 매개변수를 질의 함수로 바꾸기

AvailableVacation(anEmployee, anEmployee.Grade);

public int AvailableVacation(Employee anEmployee, int grade) {
    // ...
}
AvailableVacation(anEmployee);

public int AvailableVacation(Employee anEmployee) {
    int grade = anEmployee.Grade;
    // ...
}

배경

매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다.

즉, 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다.

다른 코드와 마찬가지로 이 목록에서도 중복은 피하는 게 좋으며 짧을수록 이해하기 쉽다.

피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개변수로 건네는 것도 일종의 중복이다.

이런 함수를 호출할 때 매개변수의 값은 호출자가 정하게 되는데, 이 결정은 사실 하지 않아도 되었을 일이니 의미 없는 코드만 복잡해질 뿐이다.

매개변수를 제거하면 값을 결정하는 책임의 주체가 달라진다.

매개변수가 있다면 결정 주체는 호출자가 되고, 매개변수가 없다면 피호출 함수가 된다.

습관적으로 호출하는 쪽을 간소화 하는 것이 좋다.

즉, 책임의 소재를 피호출 함수로 옮긴다는 뜻인데, 물론 피호출 함수가 그 역할을 수행하기에 적합할 때만 그렇게 한다.

매개변수를 질의 함수로 바꾸지 말아야 할 상황도 있다.

가장 흔한 예는 매개변수를 제거하면 피호출 함수에 원치 않는 의존성이 생길 때다.

즉, 해당 함수가 알지 못했으면 하는 프로그램 요소에 접근해야 하는 상황을 만들 때다.

새로운 의존성이 생기거나 제거하고 싶은 기존 의존성을 강화하는 경우라 할 수 있다.

이런 상황은 주로 함수 본문에서 문제의 외부 함수를 호출해야 하거나 나중에 함수 밖으로 빼내길 원하는 수용 객체에 담긴 데이터를 활용해야 할 때 일어난다.

만약 제거하려는 매개변수의 값을 다른 매개변수에 질의해서 얻을 수 있다면 안심하고 질의 함수로 바꿀 수 있다.

다른 매개변수에서 얻을 수 있는 값을 별도 매개변수로 전달하는 것은 아무 의미가 없다.

주의사항으로는 대상 함수가 참조 투명 해야 한다는 것이다.

참조 투명이란?
함수에 똑같은 값을 건네 호출하면 항상 똑같이 동작한다라는 뜻으로 동작을 예측하고 테스트하기가 훨씬 쉬우니 이 특성이 사라지지 않도록 주의하자.
따라서 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일을 하면 안 된다.

절차

  • 칠요하다면 대상 매개변수의 값을 계산하는 코드를 별도 함수로 추출 해놓는다.
  • 함수 본문에서 대상 매개변수로의 참조츨 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다. (하나 수정할 때마다 테스트한다.)
  • 함수 선언 바꾸기로 대상 매개변수를 없앤다.

11.6 질의 함수를 매개변수로 바꾸기

const int value = TargetTemperature(Plan aplan);

public int TargetTemperature(Plan aplan)
{
    currentTemperature = thermostat.currentTempature;
    // 생략
}
const int value = TargetTemperature(Plan aplan, thermostat.currentTemperature);

public int TargetTemperature(Plan aplan, int currentTemperature)
{
    // 생략
}

배경

모드를 읽다 보면 함수 안에 두기엔 거북한 참조를 발견할 때가 있다.

싱글톤을 이용한 데이터를 받아오는 코드?

전역 변수를 참조한다거나 제거하길 원하는 원소를 참조하는 경우가 여기에 속한다.

이 문제는 해당 참조를 매개변수로 바꿔 해결할 수 있다.

참조를 풀어내는 책임을 호출자로 옮기는 것이다.

이런 상황의 대부분은 코드의 의존 관계를 바꾸려 할 때 벌어진다.

생각

극단적인 의존관계의 역전이 과연 리팩터링에 도움이 될까?

위 코드와 같이 의존성, 여기선 응집성을 제거하고 매개변수로 호출받아 사용하는 이 리팩터링 기법은 이해가 되지만,

리팩터링을 결심한 시기에는 이미 리팩터링이 늦었다는 말 처럼 의존관계를 바꾸기 시작한 시기에는 이미 의존성이 복잡하게 엉켜있을 것 같다는 생각..

책도 그 극단 사이에 균형을 잘 찾아야 한다고 말한다.

대다수의 까다로운 결정이 그렇듯, 이 역시 한 시점에 내린 결정이 영원히 옳다고 할 수는 없는 문제다.

참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃게 되는데, 이 문제는 해당 원소를 매개변수로 바꾸면 해결된다.

책임이 호출자로 옮겨진다는 점을 고려해야 하지만, 모듈은 참조 투명하게 만들어 얻는 장점은 대체로 아주 크다.

그래서 모듈을 개발할 때 순수 함수들을 따로 구분하고, 프로그램의 입출력과 기타 가변 원소들을 다루는 로직으로 순수함수들을 겉을 감싸는 패턴을 많이 활용한다.

전역클래스의 올바른 예

이 리팩터링의 단점은 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다.

결국 호출자가 복잡해지는데, 앞서 말한 것과 같이 호출자가 단순해지도록 설계하는 것이 유리하다.

절차

  • 변수 추출하기로 질의 코드를 함수 본문의 나머지 코드와 분리한다.
  • 함수 본문 중 해당 질의를 호출하지 않는 코드들을 별도 함수로 추출한다.
  • 방금 만든 변수를 인라인하여 제거한다.
  • 원래 함수도 인라인한다.
  • 새 함수의 이름을 원래 함수의 이름으로 고쳐준다.

이 리팩터링을 수행하면 호출하는 쪽 코드는 전보다 다루기 어려워지는 게 보통이다. '의존성을 모듈 바깥으로 밀어낸다'함은 그 의존성을 처리하는 책임을 호출자에게 지운다는 뜻이기 때문이다.

11.7 세터 제거하기

public class Person
{
    public string Name {get; set;}
}
public class Person
{
    public string Name {get; private set;}
}

C#의 경우는 좀 더 유연하게 세터를 제거 할 수 있다.

배경

세터 메서드가 있다고 함은 필드가 수정될 수 있다는 뜻이고 객체 생성 후에는 수정되지 않길 원하는 필드라면 세터를 제공하지 않았을 것이다.

(그래서 그 필드를 불변으로 만들었을 것이다.)

그러면 해당 필드는 오직 생성자에서만 설정되며, 수정하지 않겠다는 의도가 명명백백해지고, 변경될 가능성이 봉쇄된다.

세터 제거하기 리팩터링이 필요한 상황은 크게 두 가지다.

첫째, 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때다.

생성자에서도 세터를 사용하려 하는데, 이 논쟁에 대해 C#readonly라는 키워드를 통해 동적으로 값을 불변으로 만들 수 있다.

생성자 단위에서 초기화 되는 값들을 불변으로 만들어줘 실제로 const보다 유용하게 사용된다.

Effective C#에서도 해당 글을 다루고 있어서 같이 첨부한다.

const보다 readonly를 사용하라

두 번째 상황은 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때다.

생성 스크립트란?
생성자를 호출한 후 일련의 세터를 호출하여 객체를 완성하는 형태의 코드를 말한다.

설계자는 스크립트가 완료되면 그 뒤로는 객체가 변경되지 않을 것이라고 기대한다.

즉, 해당 세터들은 처음 생성될때만 호출되리라 가정한다.

이런 경우도 세터들을 제거하여 의도를 더 정확하게 전달하는게 좋다. (이거 만지면 안됨!)

절차

  • 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
  • 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나 수정할 때마다 테스트한다.
  • 세터 메서드를 인라인한다. 가능하다면 해당 필드를 불변으로 만든다.
  • 테스트한다.

11.8 생성자를 팩터리 함수로 바꾸기

var leadEngineer = new Employee(document.leadEnginerr, 'E');
var leadEngineer = createEngineer(document.leadEngineer);

배경

많은 객체 지향 언어에서 제공하는 생성자는 객체를 초기화하는 특별한 용도의 함수이다.

실제로는 새로운 객체를 생성할 때면 주로 생성자를 호출한다.

하지만 생성자에는 일반 함수에는 없는 이상한 제약이 따라 붙기도 한다.

자바의 경우는 반드시 생성자를 정의한 클래스의 인스턴스를 반환하애 한다.

팩터리 함수는 이러한 제약 없이 다양한, 필요로 하는 기능을 추가하여 우아하게 작성가능하다.

개인적으로는 팩터리 함수 또한 생성자의 한 겹의 래퍼라는 생각이 든다.

팩토리 메서드 패턴 정리글

모든 생성자를 팩토리 함수로 바꾸라는 것이 아닐 것이다.

모든 데이터를 레포 객체로 다루는 것이 아닌말과 같은..

많이 사용되고 불변 객체가 보장되어야 하며 앞 뒤로 다양한 로직이 필요할 때 적합하다.

절차

  • 팩토리 함수를 만든다. 팩토리 함수의 본문에서는 원래의 생성자를 호출한다.
  • 생성자를 호출하던 코드를 팩토리 함수 호출로 바꾼다.
  • 하나씩 수정할 때마다 테스트한다.
  • 생성자의 가시 범위가 최소가 되도록 제한한다.

11.9 함수를 명령으로 바꾸기

public void Score(int condidate, int medicalExam, int scoringGuide)
{
    int result = 0;
    int healthLevel = 0;
}
public class Scorer
{
    private int result;
    private int healthLevel; 
    private int condidate;
    private int medicalExam;
    private int scoringGuide;
    
    public Scorer(int condidate, int medicalExam, int scoringGuide)
    (
        this.condidate = condidate;
        this.medicalExam =  medicalExam;
        this.scoringGuide = scoringGuide;
    )

    Excute()
    {
        this.result = 0;
        this.healthLevel = 0;
    }
}

배경

함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다.

이런 객체를 가리켜 명령 객체혹은 단순히 명령이라 한다.

명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다.

명령은 평범한 함수 메커니즘보다 훨씬 유연하게 함수를 제어하고 표현할 수 있다.

명령은 되돌리기 같은 보조 연산을 제공할 수 있으며, 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드를 제공할 수 있따.

상속과 훅을 이용해 사용자 맞춤형으로 만들 수도 있다.

소프트웨어 개발 용어 중에는 여러 가지 의므로 사용되는 게 많은데, 명령도 마찬가지다.
지금 맥락에서의 명령은 요청을 캡슐화한 객체로, 디자인 패턴 중 명령 패턴을 말하는 것과 같다.
메서드 호출을 실체화한 것이다.

명령 패턴 정리 글

절차

  • 대상 함수의 기능을 옮길 빈 클래스를 만든다. 클래스의 이름은 함수 이름에 기초해 짓는다.
  • 방금 생성한 빈 클래스로 함수를 옮긴다.
    • 명령관련 이름은 사용하는 프로그래밍 언어의 컨벤션을 따른다.(excute, call)
  • 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.

11.10 명령을 함수로 바꾸기

public class ChargeCalator
{
    private Customer customer;
    private Usage usage;

    public ChargeCalator(Customer customer, Usage usage)
    {
        this.customer = customer;
        this.usage = usage;
    }

    excute()
    {
        return this.customer.rate * this.usage;
    }
}
public void Charge(Customer customer, Usage usage)
{
    this.customer = customer;
    this.usage = usage;
}

배경

명령 객체는 복잡한 연산을 다룰 수 있는 강력한 메커니즘을 제공한다.

구체적으로는, 큰 연산 하나를 여러 개의 작은 메서드로 쪼개고 필드를 이용해 쪼개진 메서드들끼리 정보를 공유할 수 있다.

또한 어떤 메서드를 호출하냐에 따라 다른 효과를 줄 수 있고 각 단계를 거치며 데이터를 조금씩 완성해갈 수도 있다.

명령의 이런 능력은 공짜가 아니다.

명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 주로 쓰인다.

로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는 게 낫다.

절차

  • 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추출한다.
  • 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인 한다.
  • 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
  • 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꾼다. 하나씩 수정할 때마다 테스트한다.
  • 생성자 호출과 명령의 실행 메서드 호출을 호출자 안으로 인라인 한다.
  • 테스트한다.
  • 죽은 코드 제거하기로 명령 클래스를 없앤다.

11.11 수정된 값 반환하기

int totalAscent = 0;
CalculateAscent();

public void CalculateAscent()
{
    for (int i = 1; i < points.length; i++)
    {
        const verticalChange = points[i].elevation - points[i - 1].elevation;
        totalAscent += (verticalChange > 0) ? verticalChange : 0;
    }
}
const int totalAscent = CalculateAscent();

public int CalculateAscent()
{
    int result = 0;

    for (int i = 1; i < points.length; i++)
    {
        const verticalChange = points[i].elevation - points[i - 1].elevation;
        result += (verticalChange > 0) ? verticalChange : 0;
    }

    return result;
}

배경

데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 부분 중 하나다.

특히 같은 데이터 블록을 읽고 수정하는 코드가 여러 곳이라면 데이터가 수정되는 흐름과 코드의 흐름을 일치시키기가 상당히 어렵다.

그래서 데이터가 수정된다면 그 사실을 명확히 알려주어서, 어느 함수가 무슨일을 하는지 쉽게 알 수 있게 하는 일이 중요하다.

데이터가 수정됨을 알려주는 좋은 방법은 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 하는 것이다.

이 방식으로 코딩하면 호출자 코드를 읽을 때 변수가 갱신될 것임을 분명히 인지하게 된다.

해당 변수의 값을 단 한 번만 정하면 될 때 특히 유용하다.

이 리팩터링은 값 하나를 계산한다는 분명한 목적이 있는 함수들에 가장 효과적이고, 반대로 값 여러 개를 갱신하는 함수에는 효과적이지 않다.

한편, 함수 옮기기의 준비 작업으로 적용하기에 좋은 리팩터링이다.

절차

  • 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
  • 테스트한다.
  • 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다.
  • 테스트한다.
  • 계산이 선언과 동시에 이뤄지도록 통합한다. (즉, 선언 시점에 계산 로직을 바로 실행해 대입한다.)
  • 테스트한다.
  • 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
  • 테스트한다.

11.12 오류 코드를 예외로 바꾸기

if (data)
    return new ShippingRules(data);
else
    return -23;
if (data)
    return new ShippingRules(data);
else
    throw new OrderProcessingError(-23);

배경

과거에는 오류코드를 사용하는 것이 보편적이였다. 실제 C 표준 라이브러리 함수들을 보면 매직넘버 -1을 반환하는 함수가 존재한다.

예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다.

오류가 발견되면 예외를 던진다.

그러면 적절한 예외 핸들러를 찾을 때까지 콜스택을 타고 위로 전파된다.

예외를 사용하면 오류 코드를 일일이 검사하거나 오류를 식별해 콜스택 위로 던지는 일을 신경 쓰지 않아도 된다.

예외는 독자적인 흐름이 있어서 프로그램의 나머지에서는 오류 발생에 따른 복잡한 상황에 대처하는 코드를 작성하거나 읽을 일이 없게 해준다.

예외는 정교한 메커니즘이지만 대다수의 다른 정교한 메커니즘과 같이 정확하게 사용할 때만 최고의 효과를 낸다.

예외는 말 그대로 예상 밖의 동작일 때만 쓰여야 한다.

달리 말하면 프로그램의 정상 동작 범주에 들지 않는 오류를 나타낼 때만 쓰여야 한다.

괜찮은 경험 법칙이 하나 있다.

예외를 던지는 코드를 프로그램 종료 코드로 바꿔도 프로그램이 여전히 정상 동작할지를 따져보는 것이다.

정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다.

이때는 예외 대신 오류 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.

절차

  • 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
  • 테스트한다.
  • 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
  • 정적 검사를 수행한다.
  • catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
  • 오류 코드를 반환하는 곳에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  • 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.

11.13 예외를 사전확인으로 바꾸기

public double GetValueForPeriod(int periodNumber)
{
    try
    {
        return values[periodNumber];
    }
    catch (ArrayIndexOutOfBoundsException e)
    {
        return 0;
    }
}
public double GetValueForPeriod (int periodNumber)
{
    return (periodNumber >= values.length) ? 0 : values[periodNumber];
}

배경

예외라는 개념은 프로그래밍 언어의 발전에 의미 있는 한걸음이었다.

오류 코드를 연쇄적으로 전파하던 긴 코드를 예외로 바꿔 깔끔히 제거할 수 있게 되었으니..

하지만 좋은 것들이 늘 그렇듯, 예외도 과용될 수 있다.

함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야 한다.

절차

  • 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. catch 블록의 코드를 조건문의 조건절중 하나로 옮기고, 낭믄 try 블록의 코드를 다른 조건절로 옮긴다.
  • catch 블록에 어서션을 추가하고 테스트한다.
  • try문과 catch블록을 제거한다.
  • 테스트한다.

느낀점

이번 장을 읽으면서 대부분의 트레이트 오프가 있는 리팩터링들이 역이 되는 기법이 왜 있는지 생각하게 된 것 같습니다.

논의사항

  • 이번 장에서 가장 적용하기 어렵다고 생각한 리팩터링이 있다면 그 리팩터링에 대해 같이 이야기해보면 좋을 것 같습니다.
@fkdl0048 fkdl0048 mentioned this issue Sep 19, 2023
13 tasks
@fkdl0048 fkdl0048 added this to Todo Sep 19, 2023
@fkdl0048 fkdl0048 added the 2023 label Sep 19, 2023
@fkdl0048 fkdl0048 self-assigned this Sep 19, 2023
@github-project-automation github-project-automation bot moved this to Todo in Todo Sep 19, 2023
@fkdl0048 fkdl0048 added this to the Refactoring milestone Sep 19, 2023
@fkdl0048 fkdl0048 moved this from Todo to In Progress in Todo Sep 21, 2023
@github-project-automation github-project-automation bot moved this from In Progress to Done in Todo Sep 24, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

No branches or pull requests

1 participant