CS공부(개념)/독후감

클린코드 3주차: 예외처리, 경계 그리고 클래스

cantor 2023. 8. 22. 15:07

로버트 C. 마틴- 클린코드

230822 클린코드 3주 차

클린코드 북 스터디를 하며 작성한 독후감 겸 요약글입니다.

7장 오류처리

오류와 예외의 구분을 알게 되었습니다.
저는 이전까지는 오류와 예외를 모두 오류로 간주하고 있었습니다.

이전에 생각하던 오류의 종류

  1. 프로그램을 중단시키는 오류
  2. 의도하지 않은 방식으로 실행되는 오류

두 번째 항목을 예외라고 지칭하는 것이었습니다.
잘못된 실행을 감지하고 처리하는 것을 try-catch 블록을 통한 예외 처리라고 합니다.

 

이전에는 기존 로직에서 오류가 발생하면, 원래 실행시킬 코드를 끊고,
다른 코드를 처리하므로 그 코드가 예외코드라고 생각했었습니다.

 

명확한 정의를 알게 되어 기분이 좋습니다.

 

책 내용으로 들어가자면, 마틴 아저씨는 오류를 예외로 처리해야 한다고 권고하십니다.
아마도 오류가 발생하더라도 프로그램이 죽지 않도록 하기 위함인 것 같습니다.

try-catch-finally부터 작성하라

 

이미 3장 함수에서 try-catch문에 대해 언급한 적이 있습니다.

 

try-catch 블록은 비즈니스 로직 처리 함수 내부에 포함시키는 것이 아니라,
해당 함수를 둘러싸는 실행 및 예외 처리 함수를 생성하는 방식으로 구현되어야 합니다.

 

이번 쳅터에서도 비슷하게 비즈니스 로직과 예외 처리의 분리 방법에 대해 이야기합니다.

 

 

미확인 예외 사용

 

확인 예외처리 vs 미확인 예외처리

 

둘 중 어느 것을 사용하는 게 좋은지는 해묵은 논쟁거리라고 합니다.

 

확인 예외:

  • 전체 로직에서 예외가 발생하는 상황마다 try-catch 블록을 만들어주는 것

미확인 예외:

  • 실행 단위별로 가장 바깥쪽 함수에 한 번만 try-catch 블록을 사용하는 것

저자는 미확인 예외 사용을 추천합니다.

 

확인 예외 방식의 단점은 다음과 같습니다.

 

함수의 내부에서 다른 예외가 발생하는 함수를 사용할때에  

모든 상위 스코프마다 try-catch 블록을 삽입해 주어야 합니다.

 

이것은 저도 경험해 본 적 있는 작업입니다.

 

예외가 발생할 수 있는 함수가 반환하는 데이터를
또 다른 함수에서 사용 시, 해당 함수 내에서도 예외처리를 해주어야 하지요.

// 팟캐스트에 속한 모든 에피소드를 가져옵니다.
getAllEpisodes(id: string): any[] {
    try {
        const podcast = await this.getOne(id);
        const episodes = podcast.episodes;
        return episodes;
    }
    catch (error) {
        console.log(error)
    }
}

// 팟캐스트 하나를 검색합니다.
getOne(id: string): Podcast {
    try {
        const podcast = await podcastsRepository.findOne({where: {id}});
        if (!podcast) {
            throw new NotFoundException(`Podcast with ID ${id} not found`);
            }
        return podcast;
        } catch (error) {
            console.log(error);
            return null;
        }
    }

위 코드를 보면 getOne 메서드 내부에 이미 try-catch 블록이 있습니다.

그 메서드를 호출하는 getAllEpisodes에서도 try-catch 블록이 존재합니다.


이렇게 확인 예외방식은 try-catch의 반복사용으로 작업량이 엄청나게 늘어나게 됩니다.

 

 

정상흐름을 정의하라

 

특정 분기마다 전혀 다른 함수를 실행하게 하는 것은 좋지 않습니다.

 

기본적인 함수를 다 같이 사용하게 합니다.

분기가 생긴다면 분기별 특수값을 얹어주는 방식을 사용하세요.

 

null을 입력받거나 반환하는 함수를 사용하지 마라.

 

 

null을 반환하는 함수는 nullPointerException을 발생시킵니다.

잘 모르고 null을 참조하면 프로그램이 종료되어 버립니다.


그러니 null을 반환할 가능성이 있는 함수를 하나 실행할 때마다
null 체크를 위한 로직을 추가해야 합니다.

 

이러한 작업은 개발자가 로직에 집중하는 것을 막고, 프로그램의 안정성도 해칩니다.

 

값이 없을 경우 빈 객체를 반환하게 한다면 더 안정적인  프로그램을 만들 수 있습니다.

8. 경계

책에서 말하는 경계란 무지의 세계: 내가 모르는 세계와
내가 속한 세계의 구분지점을 뜻합니다.

 

프로그램을 만드는 도중 모르는 라이브러리를 사용하거나,
협업에서 내 코드와 타인의 코드가 협력을 하는 부분이 곧 경계와 만나는 지점입니다.

 

이런 경계를 어떻게 대처하는것이 좋을까요?

 

외부 코드사용: 학습테스트 제작

 

본인이 사용한 적 없는 라이브러리 코드나 패키지를 사용할 때,
일단 비즈니스 로직을 작성하지 말고, 학습을 위한 테스트 코드를 작성하는 것이 좋습니다.

 

테스트코드를 작성하려면 우리는 해당 함수나 클래스가
어떤 값을 입력받는지, 또 어떤 값을 반환하는지 알아야 합니다.

 

따라서 테스트코드를 작성하는 것 하나가
내가 속한 세계로 편입해 온 경계밖의 요소에 대한 이해도를 점검하는 행위입니다.

 

협업: 모르는 코드를 위한 코드 만들기

 

특정 프로그램의 부분을 나눠 각자 코드를 만들기로 했다면,
누군가 완성하기 전까지 그 코드는 다가설 수 없는 경계 밖에 위치하게 됩니다.

 

타인의 코드에서 나온 요소를 내 코드가 활용해야 한다면,
어떤 요소가 나올지를 예측해야 합니다.

 

입력으로 받는 인터페이스를 미리 정의해 놓는 것이 하나의 방식입니다.

백엔드와 프런트엔드의 협력방식이 좋은 예가 될 수 있을 것 같습니다.

 

- 예시

 

DB에서 데이터를 가져와서 화면에 표시하는 로직을 구현하려합니다.

 

직접 구현하기 전에 API가 유저로부터 입력받을 값과 DB에서 출력하여
반환하는 값의 형태 및 이름을 미리 정의하고 시작한다면 , 프론트와 백엔드가

서로의 작업이 완료될 때까지 기다릴 필요 없이 각자의 일을 할 수 있습니다.

10. 클래스

함수가 한 가지 책임을 져야하는것처럼, 클래스도 한가지 책임을 져야 합니다.
단, 함수보다는 한 단계 더 추상적인 책임을 갖습니다.

  • 함수의 책임: 더하기, 빼기, 곱하기 등등
  • 클래스의 책임: 사칙연산

함수가 갖는 책임들을 특정 기준에 따라 여러 종류로 구분하면
그것이 클래스가 갖는 책임이라 할 수 있습니다.

 

클래스는 작아야 한다.

 

함수의 인자가 3개 이하인 것이 이상적이라면,
클래스의 메서드는 5개 이하로 갖는 것이 이상적입니다.

 

그렇다면 이미 존재하는 거대한 클래스는 어떤 것을 기준으로 나누어야 할까요?

 

함수의 분리기준은 하나의 책임이고, 클래스의 기준은 한 종류의 책임입니다.

즉 메서드를 책임별로 그룹화할 수 있다면 그것이 클래스를 쪼개는 기준이 됩니다.

 

조금 더 구체적인 예로 메서드가 사용하는 속성변수가 기준이 될 수 있습니다.

 

객체의 메서드가 많아지면, 일반적으로 관리하는 속성도 많아질 것입니다.
그중 각 속성별로 이용하는 메서드가 다를 테고, 기겠을 기준으로 클래스를 분리하면 됩니다.

SOLID

 

SRP: 단일책임 원칙

 

Single Responsibility Principle

 

클래스를 수정하는 이유는 오직 한 가지여야 합니다.

 

클래스가 여러 가지 책임을 갖게 된다면 책임별로 변경 사항이 생기므로,
한 가지 책임을 갖게 해야 합니다.

 

OCP: 개방/폐쇄 원칙

 

 

Open/Closed Principle

 

클래스는 기능 추가에는 열려있고, 수정에는 닫혀있어야 합니다.

 

함수와 마찬가지로, 클래스를 만들면 그 클래스에 의존하는 여러 로직이 생깁니다.

클래스 기능의 추가는 자유롭지만, 기존 기능의 수정은 닫혀있게 디자인해야 합니다.

그렇게 하면 재사용성이 좋고, 유지보수가 쉬운 클래스를 만들 수 있습니다.

 

함수도 비슷한 것 같습니다.

 

지난 시간에 이야기했던 fetch 쿼리의 책임은 어디까지인가 에서
응답 데이터 가져오기까지만 fetch의 책임이 되고
가져온 데이터 처리는 다른 함수로 분리하는 것이 좋은지 의아했습니다.

 

단순히 "함수는 한 가지 책임을 가져야 한다"를 고집하기 위한 쓸모없는 행위라고 생각했지요.
하지만 OCP에 대해 읽으면서 생각이 바뀌었습니다.

 

fetch는 응답 데이터 가져오기책임만을 갖게한다면
기능을 추가할때 기존 로직을 수정할 필요가 없는,

유지보수가 쉬운 코드가 만들어진다는것을 느꼈습니다.

 

// 쿼리후 데이터 처리까지 하는 함수
const getWeather = function (lan, lon) {
    const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${lan}&lon=${lon}&appid=${API_KEY}&units=metric`;

     fetch(apiUrl)
     .then((response)=> response.json())
     .then((data)=>{
        console.log(data);
        city.innerText = data.name;
        weather.innerText = `${data.weather[0].main} / ${data.main.temp}`;
    });
}

 

//쿼리 함수
const fetchToOpenWeatherMap =  async (lan, lon) => {
    const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${lan}&lon=${lon}&appid=${API_KEY}&units=metric`;    
    const response = await fetch(apiUrl)
    return response.json()
    }
//데이터처리 함수
const paintWeather = (data) => {
    const {
        name,
        weather:{0: {main: weather}} ,
        main: { temp },
        } = data;
    cityElement.innerText = name;
    weatherElement.innerText = `${weather} / ${temp}`;
}

// 쿼리 결과를 활용하는 로직을 추가하기 수월하다.
const getWeather = async (lan, lon)=> {
    const data = await fetchToOpenWeatherMap(lan, lon);
    paintWeather(data);
    paintDetailWeatherBox(data);
}

 

 

 

LSP: 리스코프 치환법칙

 

Liskov Substitution Principle

 

부모객체를 상속받는 하위 객체가 있다면,

 구현 부분에서 부모객체를 하위객체로 바꾸어도이전과 같은 기능을 실행해야 한다.

 

이것은 부모클래스는 자신에대해 좀더 구체적인 형태를 갖는

자식을 갖게 하는것으로 실현 할 수있습니다.

 

즉 부모클래스의 성분이 필요한 전혀 다른 용도의 클래스는

자식으로 만들면 안됩니다.

 

그런경우엔 부모와 가져야하는 속성을  공유하는

조부모 추상클래스를 만들어야합니다.

 

 

ISP: 인터페이스 분리법칙

 

 

Interface Segregation Principle

 

클라이언트가 자신이 사용하지 않는 메서드에 의존해서는 안됩니다.

 

그래서 객체를 초기화할 때 넣어주는 옵션인자는

디테일한 설정을 해주는 메서드가 아니라, 단순히 설정값들을 담은 객체인 게 좋습니다.

 

설정을 세팅해 주는 로직은 클래스 내부에서 처리하게 해야 합니다.

 

DIP: 의존성 역전법칙

 

Dependency Inversion Principle

추상화 수준이 높은 모듈이, 더 구체적인 모듈에 의존해선 안됩니다.


어떤 모듈이던, 상속받는 모듈은 다른 구현모듈이 아닌 추상모듈이어야 합니다.

 

구현 클래스는 또 다른 구현클래스가 아닌, 추상클래스를 상속받는 것이 좋다는 이야기는 많이 들었습니다.

그렇지만 추상화 수준이 높은 모듈이 더 구체적인 모듈에 의존한다는 것이 무슨 뜻일까요?

 

유저 종합정보관리 클래스 A 가있고, 유저의 댓글관리 클래스 B가 있습니다.
이름만 보아도 알 수 있듯이 B가 더 구체적인 책임의 종류를 가지고 있습니다.

 

만약 A의 인스턴스 내부에서 직접 B의 인스턴스를 생성하여 사용한다면
그것이 추상화 수준이 높은 모듈이 더 구체적인 모듈에 의존하는 상태가 됩니다.

 

이런 경우에는 B의 인스턴스를 A의 인스턴스 생성자 함수의 인자로 넣어준다면,
해당 의존성관계가 역전됩니다.

 

B와 같은 여러 종류의 클래스를 더 많들 수 있고,  A 가 해당 클래스들의 

구현 기준이 됩니다.  이것이 의존성 역전 법칙입니다.

 

 

나누고 싶은 이야기



  1. 자바스크립트에선 finally문을 많이 보지 못한 거 같습니다.

언어가 담당하는 특성상 잘 사용하지 않는 것일까요? 아니면 니콜라스 스타일인 것일까요?

 

 

  1. 예외처리 로직 및 종류

자바스크립트 클린코드 깃허브에선
예외처리 로직에 console.log(e)만 사용하지 말고,
유저에게 알리는 로직, 서비스에게 알리는 로직을 추가하라고 합니다.

 

어떤 식으로 서비스와 유저에게 예외사항을 전달할 수 있을까요?

 

 

  1. 테스트 라이브러리

테스트 코드에 대한 중요성을 점점 인지하고 있고, 실제로 JEST의 라이브러리를 정리해놓았었지만

대부분 까먹어서 다시공부를 해야할것같습니다. 자바스크립트에서 배워볼 만한 것을 추천받습니다.

 

 

  1. 도구 A를 이용해 만든 B를 이용하여 협업 시,
    컨벤션이나 사용기준은 A와 B 중 어떤 것을 따라가나요?
  • 타입스크립트에선 클래스 생성자 타입정의 부분에서 직접 인스턴스 속성을 초기화할 수 있습니다.
    constructor(이 내부)

하지만 속성을 생성자 타입정의 부분에도 넣고, 실행부에도 넣고 하다 보면
혼란이 올 거 같습니다.

 

이런 경우에는 어떤 툴의 방식을 기준으로 삼는것이 좋을까요?

 

참고자료: clean-code-javascript
https://github.com/ryanmcdermott/clean-code-javascript#solid