Try ~ Catch VS if Statement (feat. 클린코드, 가독성)

Uno·2023년 10월 23일
0

dart

목록 보기
4/7

Go 언어에서의 에러 핸들링

Go 언어에서의 에러 핸들링 코드를 보면서, 신기한 점을 발견했다. 아래 Go 코드를 보자.

  • 의존성 코드
package main

import (
	"fmt"
	"net/http"
	"io/ioutil"
	"errors"
)
  • 로직 정의
// ViewModel을 대신할 함수
func ButtonClickHandler(ch chan string) {
	// HTTP 요청 로직
	resp, err := http.Get("https://api.example.com/data")
	if err != nil {
		ch <- fmt.Sprintf("HTTP 요청 에러: %s", err.Error())
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		ch <- fmt.Sprintf("서버 에러: %s", resp.Status)
		return
	}

	// Response 처리
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		ch <- fmt.Sprintf("응답 읽기 에러: %s", err.Error())
		return
	}

	// 화면에 보여줄 데이터 (이 경우, 콘솔 출력)
	ch <- fmt.Sprintf("성공: %s", string(body))
}
  • 호출 영역
func main() {
	// 사용자가 버튼을 클릭한다고 가정
	messageChannel := make(chan string)
	go ButtonClickHandler(messageChannel)

	// 에러 핸들링과 화면에 보여주는 데이터 처리
	message := <-messageChannel
	fmt.Println(message)
}

이 전체 코드 중에서 아래 부분을 이야기하고 싶다.

resp, err := http.Get("https://api.example.com/data")
if err != nil {
	ch <- fmt.Sprintf("HTTP 요청 에러: %s", err.Error())
	return
}

나도 Go 언어는 할 줄 모르지만, 이정도는 이해가 된다.
http.Get(...) 메서드를 통해서 HTTP 통신을 하고, 그 결과 값으로 2 개의 값을 받고 있다.

resp, err

이 두 값 중에서 만약에 에러가 있다면(= if err != nil) 그에 맞는 처리를 하고, 스텍 메모리에서 할당해제 된다.

Go 언어의 방식을 Result 타입을 통한 에러 핸들링이라고 칭하겠다.
내가 작명한 것이니 다른 곳에서 사용 시, 책임 못짐

Dart 언어의 방식을 try ~ catch 를 통한 에러 핸들링이라고 칭하겠다.

Dart 언어 방식에서도 Go 언어 방식처럼 처리가 가능하다. 바로 Result 타입을 정의하거나 Map 을 활용하면 된다.


Go Error Handling VS Dart Error Handling

Go 언어 스타일

Go 언어 스타일도 자세히 보면 2 가지 구현 방법이 있다.
1. Result Type 을 정의하여 구현하는 방법
2. Map 을 활용하여 구현하는 방법

1. Result Type 을 정의하여 구현하는 방법

가장 먼저, Result 객체를 정의한다.

class Result<T> {
	final T? value;
	final String? error;

	Result({this.value, this.error});
}
  • T 로 제네릭 타입을 정의한다.
  • immutable 하도록 final 로 값들을 정의한다.

이후에 예시용 메서드를 하나 정의한다.

Result<int> divied(int a, int b) {
	if (b == 0) {
		return Result(error: '0 으로 나눌 수 없음...');
	}
	return Result(value: a ~/ b);
}
  • 메서드의 Return Type 을 보자. Result<int> 객체를 리턴하고 있다.
  • 정상적인 처리일 때와, 에러가 발생했을 때 처리가 다르게 리턴을 하고 있다.

이제 마지막으로 호출부의 코드를 보자.

1) 정상적으로 호출된 케이스

void main() {
	var result1 = divied(10, 2);
	if (result1.error) != null) {
		print('Error: ${result1.error}');
		return;
	}

	print('Success: ${result1.value}');
}
Success: 5

2) 에러가 호출된 케이스

void main() {
	var result2 = divied(10, 0);
	if (result1.error) != null) {
		print('Error: ${result1.error}');
		return;
	}

	print('Success: ${result1.value}');
}
Error: 0 으로 나눌 수 없음...

2. Map 을 활용하여 구현하는 방법

메서드 정의 부분이 변경되었습니다.

Map<String, dynamic> divide(int a, int b) {
	if (b == 0) return {'error': '0 으로 나눌 수 없음..'};
	
	final value = a ~/ b;
	return {'value': value};
}

호출부는 아래와 같습니다.

1) 정상적으로 호출된 케이스

void main() {
	final result1 = dived(10, 2);
	if (result1.cotainKey('error')) {
		print('Error: ${result1['error']}');
		return;
	}
	print('Success: ${result1.value}');
}

2) 에러가 호출된 케이스

void main() {
	final result2 = dived(10, 0);
	if (result1.cotainKey('error')) {
		print('Error: ${result2['error']}');
		return;
	}
	print('Success: ${result2.value}');
}

두 방식 모두, 결론적으로 리턴 값으로 "결과 & 에러" 를 보내고 있다. 호출부에서는 에러가 오면 어떤식으로 처리할 지, 그리고 결과값이 오면 어떤식으로 처리할지 고민하면 끝이다. 이것은 try ~ catch 라고 다를바 없다.

Dart 언어 스타일

Dart 언어 스타일은, 내맘대로 작성 안하고, 공식문서의 예시를 가져오겠다.
공식문서 링크

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}

에러 유발가능성이 있는 코드는 이곳이다.

try {
  breedMoreLlamas();
}

이곳에서 런타임 에러가 발생하게 되면, 메모리에 breedMoreLlamas() 가 할당되어 있을 것이다. 그러나 함수 중간에 에러가 발생되면서, throw Exception(...) 이 호출되었을 것이다. 그러면 현재 breedMore... 메서드에서 외부로 나와서 on 키워드나 catch 키워드를 찾아 가야 한다. (이것을 Stack Unwinding 이라고 부른다. 함수 외부에서 에러 핸들링하는 코드를 찾아가기 위해, 스택을 되감아서 찾아가는 방식)

위 코드를 의사코드로 바꿔보면, 다음과 같다.

try {
	// 오류가 발생할 수 있는 코드 
} on (type1) {
	// type1 형식의 오류를 처리하는 코드 
} on (type2) { 
	// type2 형식의 오류를 처리하는 코드 
} ... 
finally {
	// 오류가 발생하든 안 하든 항상 실행되는 코드 
}

이 의사 코드는 DartLangSpeckDraft 에 있는 아래 코드를 내맘대로 바꿔본 것이다.
p206 에 매칭을 통한 에러 핸들링을 설명해주고 있다.

조금더 위에 보면, 논리적인 구조로만 tryStatement 를 설명하고 있다. 각 기호가 정확히 무슨 뜻인지는 몰라도, 논리적인 관계정도는 대충 보일 것이다.

다시 본론으로 돌아와서, Dart 언어에서 가장 기본적으로 에러를 다루는 방식은 try ~ catch 이다.

그래서 어떤 스타일이 좋은데?

당연히, 그런 것은 없지만, 이렇게 말하는 방식을 안좋아해서 굳이 하나를 선택하자면

Dart 언어에서 공식적으로 추천하는 방식인 try ~ catch 를 추천한다.

  • 제작자들이 try ~ catch 를 사용하라고 만들어 두었다.
  • 다른 언어 개발자들도 쉽게 이해하기 좋으므로, 진입 장벽이 낮아지는 효과가 있다.
    물론 Result Type 으로 다루는게 어렵다는 건 아니지만...
    이건 Dart Spec 문서 상에도 JavaScript 에서도 동일하게 동작하도록 되어 있다고 작성되어 있는걸 보아, 다른 언어 개발자들도 쉽지 않을까 생각해봤다
  • Future 타입에서 Stream 타입으로 변경되어도 손쉽게 .catchError 와 같은 오퍼레이터를 사용할 수 있다. 혹은, 에러 핸들링 타입을 통일하기 용이하다.

물론 이전에, Swift 언어로 iOS 했을 때는 Result 타입이 언어 레벨에시 지원해주다보니, 사용하곤 했다. 이 또한 장점이 있다.

  • 함수 레벨에서 어떤식으로 처리할지 다루기 편하다. (눈에 로직이 보인다는 뜻, try ~ catch 시스템 레벨에서 내부적으로 동작하는 코드가 생기므로, 추론해야함)
  • 이름 자체에서 오는 가독성 향상; Result 타입으로 먼저 성공과 실패를 나눠지므로 빠르게 코드 이해하기 좋다. 그에 비해 try ~ catch 는 try 읽고 -> catch 읽고 마지막 finally 까지 읽어야 한다.

둘 다 장단점이 있는데 나는 왜 try ~ catch 냐. 아래 이유와 같다.

  1. 공식문서의 공신력에 따른다.
    굳이 혼자서 어긋나게 코드를 작성할 이유가 없다. try ~ catch 가 치명적인 불편함이 있지 않는 한, 사용하는 게 동료를 위해서도 좋다고 생각한다.

  2. Exception 정의 자체가 하나의 문서화 역할을 한다.
    Exception 을 생성해야하므로 보일러 플레이트 코드가 생겨난다고 말하는 사람도 있지만, 나는 생각이 다르다. 오히려 그 클래스 정의 자체가 문서화라고 생각한다. 프로젝트 내부에 문서화를 한다고 생각하면 오히려 장점으로 다가온다.

  3. 코드의 구조 자체가 실수를 줄여준다.
    try 만 코드를 작성하면, 컴파일 에러가 발생한다. catch 에 대한 코드르 정의해달라고. 하지만 Result 로 다뤘을 시, 상당히 지저분해 진다. (물론 Dart 언어의 경우, Swift 는 enum - switch 를 통해서 좀 더 자유롭긴 함)

마무리

성능적인 차이가 메이저하지 않다면, 가독성의 문제 이다. 하지만 가독성을 정의하는 기준이 각양각색이다. 어떤 누구는 Clean Code를 바이블처럼 한 문장 한 문장을 몸에 새기듯 공부하는 사람도 있는 반면에, 포프TV - 클린코드 때문에 취업 실패한 썰 처럼, CleanCode에 대해서 다른 견해를 가진 개발자도 있다.


cf) 또 다른 클린 코드 글: Goodbye, Clean Code
몇 몇 글만 인용하자면, 아래와 같습니다.

전체 요약)

 클린 코드와 중복 제거에 대해 논의하고 있다.
 작성자는 중복된 코드를 발견하고 리팩토링을 시도하였지만,
 팀 리더로부터 이전 코드로 돌리라는 요구를 받았다.
 작성자는 코드의 깔끔함에 집착하는 것은 좋지만 완벽한 개선이란 없다는 것을 깨달았다. 
 또한, 코드 변경에 있어 팀원들과 의논하고 신뢰를 만들어야 한다는 것을 배웠다. 
 클린 코드는 좋지만 클린 코드에 너무 집착하면 중요한 변화들을 놓칠 수 있다는 점을 기억해야 한다.
  • 코드를 깔끔하게 유지하는 것은 좋지만, 깔끔함에 너무 집착하지 말아야 한다.
  • 중복된 코드를 제거하는 것은 좋지만, 완벽한 개선이란 없으며 다른 시각에서 많은 변화가 있을 수 있다.
  • 코드를 변경할 때에는 팀원들과 의논하고 신뢰를 만들어야 한다.

그러면, 이럴 때마다 어떤 것을 기준으로 코드를 작성해야 할까? 이 고민은 아직도 하고 있다. 그 중 요즘 눈에 들어온 책과 언어가 몇 개 있다. 소개하고 글을 마무리 한다.

  1. Go Language
    심플함을 극한으로 추구하고 있는 언어이다. 이 언어에서 오는 규칙들을 다른 언어에 100% 적용할 이유는 없겠지만, 가독성을 논의할 때, 좋은 래퍼런스가 될 것 같다.

  2. Five Lines of Code(파이브 라인스 오브 코드)
    이 책은 크레이에티드 프로그래머 책에서 나온 "제약 조건" 을 보면서 다시 떠오른 책이다. 스스로 제약조건부과 했을 때, (적절한 강도의 제약조건) 오히려 창의적인 문제 해결 접근 방법이 떠오른다는 내용이다. 이 책에서 가독성을 생각할 때, 특정 조건을 통해(Lint 라고 보면됨) 가독성을 다루는 것도 좋아 보인다.

  3. 만약 헤밍웨이가 자바스크립트로 코딩한다면
    개인적으로 가장 마음에 드는 책이긴 하다. 옳은지 좋은지 실용적인지 등등의 관점을 떠나서 나는 코드의 가독성이 그냥 읽기 좋은 글이였으면 좋겠다. 위에서부터 아래로 읽으면 하나하나 읽기 쉽다. 주석을 많이 써도 좋다. 그런 것보다는 읽기 쉬운 가독성 하나에 초점을 맞췄으면 좋겠는 개인적인 생각은 있다.


참고자료

profile
iOS & Flutter

0개의 댓글