/C:/Users/user/AppData/Local/Pub/Cache/hosted/pub.dartlang.org/firebase_auth-3.8.0/lib/src/firebase_auth.dart:623:25: Error: The method 'signInWithAuthProvider' isn't defined for the class 'FirebaseAuthPlatform'.
 - 'FirebaseAuthPlatform' is from 'package:firebase_auth_platform_interface/src/platform_interface/platform_interface_firebase_auth.dart' ('/C:/Users/user/AppData/Local/Pub/Cache/hosted/pub.dartlang.org/firebase_auth_platform_interface-6.8.0/lib/src/platform_interface/platform_interface_firebase_auth.dart').
Try correcting the name to the name of an existing method, or defining a method named 'signInWithAuthProvider'.
        await _delegate.signInWithAuthProvider(provider),

위와 같은 에러가 떴다........

 

내가 사용하고 있는 firebase_auth_platform_interface 버전이 이상한거 같아서(오류에는 6.8.0)으로 표시 pubspec.lock파일에 있는 버전을 6.7.0으로 낮췄더니 해결되었다.

플러터에서는 한 위젯의 여러 인스턴스를 만든다. 변경할 수 없는 위젯 인스턴스는 성능이 좋으므로 가능하면 const를 사용하는 것이 좋다. new, const 키워드를 사용하지 않으면 프레임워크가 가능한 const로 위젯을 추론하므로 크게 신경 쓰지 않아도 된다.

Widget build(BuildContext context){
	return Button(
    	child: Text("hello"),
        );
}

// 다음과 비교
Widget build(BuildContext context){
	return new Button(
    	child: Text("hello"),
        );
}

위의 Button 위젯은 new 키워드가 없고 아래의 Button은 new 키워드가 있다. 하지만 플러터가 알아서 처리하기 때문에 어떤 위젯이 상수(const)이고 아닌지 지정할 필요가 없다. 또한 클래스 인스턴스를 만들 때 new 를 사용할 필요가 없다. 이는 위젯뿐 아니라 객체에도 적용된다.

StatelessWidget을 서브클래싱 하는 대신 함수로 만들어서 위젯을 만들수도 있다.

Widget function({ String title, VoidCallback callback }) {
  return GestureDetector(
    onTap: callback,
    child: // some widget
  );
}
class SomeWidget extends StatelessWidget {
  final VoidCallback callback;
  final String title;

  const SomeWidget({Key key, this.callback, this.title}) : super(key: key);

  @override
  Widget build(BuildContext context) {
      return GestureDetector(
        onTap: callback,
        child: // some widget
      );
  }
}

첫 번째는 함수, 두번째는 클래스이다. 함수로 만드는게 훨 짧고 편해보이는데 두 방법의 차이는 무엇일까?

 

둘의 차이점은 크게 없어 보인다. 하지만 작으면서도 큰 차이점이 존재한다. 바로 위젯트리를 그릴경우이다.

 

함수의 경우에는

Container
  Container

위와 같이 그리지만 클래스의 경우에는

ClassWidget
  Container
    ClassWidget
      Container

위와 같이 그려 프레임워크가 업데이트 할때 작동하는 방식에 차이가 생긴다.

중요한 이유

기능을 사용하여 위젯 트리를 여러 위젯으로 분할하면 버그에 노출되고 일부 성능 최적화를 놓치게 됩니다.

함수를 사용하여 버그가 발생한다는 보장은 없지만 클래스를 사용하면 이러한 문제에 직면하지 않을 수 있습니다.

다음은 문제를 더 잘 이해하기 위해 직접 실행할 수 있는 Dartpad의 몇 가지 예제입니다.

결론

다음은 함수와 클래스 사용의 차이점을 보여줍니다.

  1. 클래스:
  • 성능 최적화 허용(const 생성자, 보다 세분화된 재구축)
  • 두 개의 다른 레이아웃 간 전환이 리소스를 올바르게 처리하는지 확인가능(함수는 이전 상태를 재사용할 수 있음).
  • 핫 리로드가 제대로 작동하는지 확인합니다(함수를 사용하면 핫 리로드가 중단될 수 showDialogs있음)
  • 위젯 인스펙터에 통합되어 있습니다.
    • ClassWidget화면에 무엇이 있는지 이해하는 데 도움이 되는 devtool이 표시하는 위젯 트리에서 볼 수 있습니다.
    • debugFillProperties 를 재정 의하여 위젯에 전달된 매개변수가 무엇인지 인쇄 할 수 있습니다.
  • 더 나은 오류 메시지
    예외가 발생하면(예: ProviderNotFound) 프레임워크는 현재 빌드 중인 위젯의 이름을 제공합니다. 기능 +에서만 위젯 트리를 분할한 경우 Builder오류에 유용한 이름이 없습니다.
  • 키를 정의할 수 있습니다
  • 컨텍스트 API를 사용할 수 있습니다
  1. 기능:
  • 코드가 적습니다(코드 생성 기능적 위젯을 사용하여 해결할 수 있음 ).

 

들어가면서

소프트웨어에서 이름은 어디나 쓰인다. 여기저기 도처에서 이름을 사용한다. 이름을 잘 지으면 여러모로 편하다.


의도를 분명히 밝혀라

좋은 이름을 지으려면 시간이 걸리지만 좋은 이름으로 절약하는 시간이 훨씬 더 많다. 그러므로 이름을 주의깊게 살펴 더 나은 이름이 떠오르면 개선해야 한다.

굵직한 질문에 모두 답할수 있어야 한다. ( 변수의 존재 이유는?, 수행 기능은?, 사용 방법은? 따로 주석이 필요한지?)

int d; // 경과 시간(단위: 날짜)

위의 이름 d는 아무 의미가 없다.

int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

위와 같이 의도가 드러나는 이름을 사용하면 코드 이해와 변경이 쉽다.

다음 코드는 무엇을 할까?

public List<int[]> getThem() {
 List<int[]> list1 = new ArrayList<int[]>();
for (int[] x : theList)
 if (x[0] == 4)
 list1.add(x);
 return list1;
}

코드가 하는 일을 짐작하기 어렵다. 문제는 코드의 단순성이 아니라 코드의 함축성이다. 코드 맥락이 코드 자체에 명시적으로 드러나지 않는다. 위 코드는 암암리에 독자가 다음의 정보를 안다고 가정한다.

  1. theList에 무엇이 들었는가?
  2. theList에서 0번째 값이 어째서 중요한가?
  3. 값 4는 무슨 의미인가?
  4. 함수가 반환하는 리스트 list1을 어떻게 사용하는가?

단순 이름을 고치는 것만으로 함수가 하는 일을 이해하기 쉬워진다.


그릇된 정보를 피하라

프로그래머는 코드에 그릇된 단서를 남겨서는 안 된다. 이는 코드의 의미를 흐린다.

예를 들어 hp라는 변수는 유닉스 변종을 가리키는 이름이기 때문에 직각삼각형의 빗변(hypotenuse)의 약어로 좋아 보일지라도 독자에게 혼란을 줄 수 있다.

서로 흡사한 이름을 사용하지 않도록 주의한다. 한 모듈에서 XYZController orEfficientHandlingOfStrings라는 이름을 사용하고, 조금 떨어진 모듈에서 XYZControllerForEfficientStorageOfStrings 을 사용하면 ..... 겁나 헷갈린다.

유사한 개념은 유사한 표기법을 사용한다. 일관성이 떨어지는 표기법은 그릇된 정보다.


의미 있게 구분하라

이름이 달라야 한다면 의미도 달라져야 한다. 컴파일러를 통과할지라도 연속된 숫자를 덧붙이거나 불용어를 추가하는 방식은 적절하지 못하다.

public static void copyChars(char a1[], char a2[]) {
 for (int i = 0; i < a1.length; i++) {
 a2[i] = a1[i];
 }
}

함수 이름으로 source와 destination을 사용한다면 코드 읽기가 훨씬 쉬워진다.

불용어는 중복이다. variable이나 table같은 단어는 이름으로 쓰면 안된다.

getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();

위와 같이 애매한 변수로 이름을 지으면 혼란만 야기한다.


발음하기 쉬운 이름을 사용하라

사람들은 단어에 능숙하고 단어는 개념의 전달이 용이하다.

class DtaRcrd102 {
 private Date genymdhms;
 private Date modymdhms;
 private final String pszqint = "102";
 /* ... */
};

class Customer {
 private Date generationTimestamp;
 private Date modificationTimestamp;
 private final String recordId = "102";
 /* ... */
}

위의 코드를 보면 명확해진다.


검색하기 쉬운 이름을 사용하라

문자 하나를 사용하는 이름과 상수는 텍스트 코드에서 눈에 띄지 않는다.

또한 나중에 grep을 이용한 검색에서 모든 단어가 검색되어 제대로 된 검색이 불가능해진다.

for (int j=0; j<34; j++) {
 s += (t[j]*4)/5;
}

int realDaysPerIdealDay = 4;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j < NUMBER_OF_TASKS; j++) {
 int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
 int realTaskWeeks = (realTaskDays / WORK_DAYS_PER_WEEK);
 sum += realTaskWeeks;
}

인코딩을 피하라

유형이나 범위 정보 까지 인코딩에 넣으면 해독도 어려워지고 또 인코딩을 익히느라 시간또한 쓰게 된다. 개발자는 할게 많다......불필요한 부담은 없애자!

  • 헝가리식 표기법

요즘 시대는 변수이름에 타입을 인코딩할 필요가 없다(실제로 나는 사용해본적 없음). 그러므로 이름에 변수이름에 헝가리안 표기법으로 변수 타입을 넣지 않아도 된다.

  • 멤버 변수 접두어

이전에는 멤버 변수 접두어에 m_를 붙였었나 보다. 그러지 말자.

public class Part {
 private String m_dsc; // 설명 문자열
 void setName(String name) {
 m_dsc = name;
 }
}
_________________________________________________
public class Part {
 String description;
 void setDescription(String description) {
 this.description = description;
 }
}
  • 인터페이스 클래스와 구현 클래스

인터페이스와 구현클래스의 명명을 어떻게 할 것인가. 이건 사람마다 다르겠지만 쉽게 글쓴이는 인터페이스 뒤에 Impl이라고 붙이는 것을 선호한다고 한다.

ShapeFactoryImp나 심지어 CShapeFactory가 IShape Factory보다 좋다고 한다.


자신의 기억력을 자랑하지 마라

보통 프로그래머는 머리가 똑똑하다고 한다. 그렇다고 그들이 만능은 아니다.

클래스 이름

클래스 이름과 객체 이름은 명사나 명사구가 적합하다.

메서드 이름

메서드 이름은 동사나 동사구가 적합하다. 접근자, 변경자, 조건자는 javabean 표준에 따라 값 앞에 get,set,is를 붙인다.

기발한 이름은 피하라

이름이 너무 기발하면 공감대가 비슷한 사람만 이름을 기억해 낸다. 재미난 이름보다 명료한 이름을 선택하라

한 개념에 한 단어를 사용하라

추상적인 개념 하나에 단어 하나를 선택해 이를 고수한다.

말장난을 하지 마라

한 단어를 두 가지 목적으로 사용하지 마라. 다른 개념에 같은 단어를 사용한다면 이는 말장난에 불과하다.

해법 영역에서 가져온 이름을 사용하라

코드를 읽을 사람도 프로그래머이다. 전산 용어, 알고리즘 이름, 패턴 이름, 수학 용어를 사용하면 서로에게 친숙하다.

문제 영역에서 가져온 이름을 사용하라

적절한 프로그래밍 언어가 없다면 문제 영역에서 이름을 가져온다. 그러면 그 배경지식을 습득하면서 배울수 있다.


의미 있는 맥락을 추가하라

스스로 의미가 분명한 이름이 없지 않다. 하지만 대다수 이름은 그렇지 못하다. 그래서 클래스, 함수, 이름 공간에 넣어 맥락을 부여한다. 모든 방법이 실패하면 마지막 수단으로 접두어를 붙인다.

private void printGuessStatistics(char candidate, int count) {
 String number;
 String verb;
 String pluralModifier;
 if (count == 0) {
 number = "no";
 verb = "are";
 pluralModifier = "s";
 } else if (count == 1) {
 number = "1";
 verb = "is";
 pluralModifier = "";
 } else {
 number = Integer.toString(count);
 verb = "are";
 pluralModifier = "s";
 }
 String guessMessage = String.format(
 "There %s %s %s%s", verb, number, candidate, pluralModifier
 );
 print(guessMessage);
}

함수 이름은 맥락 일부만 제공하며, 알고리즘이 나머지 맥락을 제공한다. 위의 함수를 끝까지 읽어보고 나서야 변수 세 개가 통계 추측 메시지에 사용된다는 사실이 드러난다.

불필요한 맥락을 없애라

일반적으로는 짧은 이름이 긴 이름보다 좋다. 단, 의미가 분명한 경우에 한해서다. 이름에 불필요한 맥락을 추가하지 않도록 주의한다.


마치면서

좋은 이름을 선택하려면 설명 능력이 뛰어나야 하고 문화적인 배경이 같아야 한다. 이것이 제일 어렵다. 좋은 이름을 선택하는 능력은 기술, 비즈니스, 관리 문제가 아니라 교육 문제다.

여느 코드 개선 노력과 마찬가지로 이름 역시 나름대로 바꿨다가는 누군가 질책할지도 모른다. 그렇다고 코드를 개선하려는 노력을 중단해서는 안 된다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16
[Clean Code] 8장 경계  (2) 2022.09.15
[Clean Code] 7장 오류 처리  (0) 2022.09.14

코드가 존재하리라

코드가 사라질 가망은 없다. 왜? 코드는 요구사항을 상세히 표현하는 수단이니까!

프로그래밍 언어에서 추상화의 수준은 점차 높아지겠지만 요구사항을 모호하게 줘도 우리 의도를

꿰뚫어 프로그램을 완벽하게 만드는건 불가능하다.

그러므로 코드는 항상 존재하리라


나쁜 코드

좋은 코드는 중요하다. 이전 버전에 있던 버그가 다음 버전에도 계속 남을 것이고 이것으로 인해

프로그램이 죽는 일도 발생할 것이며 다음 버전에끼치는 영향은 배가 아닌 제곱이 되므로

나중에 돌아와 정리하겠다고 생각하지 말고 깨끗한 코드를 짜려고 현재에 노력하자

*르블랑의 법칙! 나중은 결코 오지 않는다.


나쁜 코드로 치르는 대가

나쁜 코드는 개발 속도를 크게 떨어뜨린다.

프로젝트 초반에 번개처럼 잘 나아가다가도 나쁜 코드가 있다면 고칠 때마다 계속 시간이 늘어난다.

사람이 더 투여되더라도 생산성만 안 좋아질뿐 코드 자체가 좋아지진 않는다.


원대한 재설계의 꿈

나쁜 코드의 중첩으로 더 이상 좋아질 기미가 보이지 않아 재설계를 진행한다.

결국 타이거 팀이 만들어 진다.

타이거 팀의 진행 방향

1. 새로운 타이거 팀의 구성

2. 모두가 타이거 팀에 합류하고 싶어 하지만 유능한 사람이 차출 나머지는 현 시스템 유지보수

3. 유지보수팀과 타이거 팀의 경주 시작

4. 기존 시스템을 100% 기능 재현 할 때까지 개발 진행

하지만 기존 코드가 레거시 코드라면 결국 위의 진행 방향도 얼마나 걸릴지 알 수 없다는게 함정


태도

어째서 좋은 코드가 순식간에 나쁜 코드로 전락했는가?

1. 원래 설계를 뒤집는 방향으로 요구사항이 변해서?

2. 일정이 촉박해서?

3. 멍청한 관리자와 조급한 고객과 쓸모없는 마케팅 부서와 전화기 살균제 탓에?

아니다. 전적으로 우리(프로그래머)가 전문가 답지 못해서이다.

우리는 요구사항을 우리에 맞게 자문해줘야한다.

우리는 프로젝트 계획에 관여하여 일정을 잡아야한다.

우리는 관리자와, 고객과, 마케팅 부서에게 만든 프로그램의 정보를 제공해야 한다.

즉, 프로젝트의 모든 부분에 우리는 책임이 있다.

*나쁜 코드의 위험을 이해하지 못하는 관리자 말을 그대로 따르는 행동은 전문가답지 못하다.


원초적 난제

우리는 빨리 가려고 좋은 코드를 만드는데 시간을 들이지 않는다.

흔히 하는 착각은 프로그래머가 기한을 맞추려면 나쁜 코드를 양산할 수밖에 없다고 생각한다.

하지만 나쁜 코드가 시간을 늦춘다.


깨끗한 코드라는 예술?

그림을 그리는 행위와 비슷하다. 잘 그린 그림을 구분하는 능력이 그림을 잘 그리는 능력이 아닌것처럼

깨끗한 코드와 나쁜 코드를 구분할 줄 안다고 해서 깨끗한 코드를 작성할 줄 아는 뜻은 아니다.

절제와 규율을 적용해 나쁜 코드를 좋은 코드로 바꾸는 '코드 감각'이 있어야한다.


나는 우아하고 효율적인 코드를 좋아한다. 논리가 간당해야 버그가 숨어들지 못한다. 논리가 간단해야 버그가 숨어들지 못한다. 의존성을 최대한 줄여야 유지보수가 쉬워진다. 오류는 명백한 전략에 의거해 철저히 처리한다. 성능을 최적으로 유지해야 사람들이 원칙 없는 최적화로 코드를 망치려는 유혹에 빠지지 않는다. 깨끗한 코도는 한 가지를 제대로 한다.

-비야네(c++창시자이자 The c++ Programming Language 저자)

 

 

 

 

비야네는 효율을 중요하게 여긴다 하지만 글을 보면 우아한! 보기에 즐거운 이라는 표현을 쓴다. 즉 효율이라는 단어가 단순히 속도만을 뜻하는건 아니라는 뜻이다.

유혹이라는 단어도 잘 주의해서 봐야 한다. 나쁜 코드는 나쁜 코드를 유혹한다!

실용주의 프로그래머 데이브 토마스와 앤디 헌트는 이를 깨진 창문에 비유했다.

비야네는 철저한 오류 처리도 언급한다. 세세한 사항까지 꼼꼼하게 신경 쓰라는 말이다.

메모리 누수, 경쟁 상태, 일관성 없는 명명법 같이 사소하지만 세세한 사항도 신경써야 한다.

(지옥의 명명법.....;;;;;;)

마지막으로 비야네는 깨끗한 코드란 한 가지를 잘 한다고 단언한다. 수많은 저술가들이 나름의 깨끗한 코드를 가지고 있지만 깨끗한 코드는 한 가지에 '집중'한다는 것이다.


 

깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 잘 쓴 문장처럼 읽힌다. 깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.

-그래디 부치(Object Oriented Analysis and Design with Application 저자)

 

 

 

 

그래디는 비야네와 흡사한 의견을 표명하지만 가독성을 강조한다. 좋은 소설과 마찬가지로 깨끗한 코드는 해결할 문제의 긴장을 명확히 드러내야 한다.코드는 추측이 아니라 사실에 기반해야 한다. 반드시 필요한 내용만 담아야 한다. 코드를 읽는 사람에게 프로그래머가 단호하다는 인상을 줘야 한다.


깨긋한 코드는 작성자가 아닌 사람도 읽기 쉽고 고치기 쉽다. 단위 테스트 케이스와 인수 테스트 케이스가 존재한다. 깨끗한 코드에는 의미 있는 이름이 붙는다. 특정 목적을 달성하는 방법은 하나만 제공한다. 의존성은 최소이며 각 의존성을 명확히 정의한다. API는 명확하며 최소로 줄였다. 언어에 따라 필요한 모든 정보를 코드만으로 명확히 표현할 수 없기에 코드는 문학적으로 표현해야 마땅하다.

-Big 데이브 토마스(OTI 창립자이자 이클립스 전략의 대부)

 

 

빅 데이브는 가독성을 강조하지만 한 가지 중요한 반전을 더한다. 다른 사람이 고치기 쉽다는 것이다. 데이브는 테스트 케이스와 깨끗한 코드를 연관짓는다. TDD(테스트 주도 개발)가 중요해진 만큼 테스트 케이스가 없으면 깨끗한 코드는 나올수 없다. 데이브는 최소 즉 작은 코드와 문학적 즉 읽기 좋은 코드를 작성하나는 것을 강조한다.


깨긋한 코드의 특징은 많지만 그 중에서도 모두를 아우르는 특징이 하나 있다. 깨끗한 코드는 언제나 누군가 주의 깊게 짰다는 느낌을 준다. 고치려고 살펴봐도 딱히 손 댈 곳이 없다. 작성자가 이미 모든 사항을 고려했으므로, 고칠 궁리를 하다보면 언제나 제자리로 돌아온다. 그리고는 누군가 남겨준 코드, 누군가 주의 깊게 짜놓은 작품에 감사를 느낀다.

-마이클 페더스(Working Effectively with Legacy Code의 저자)

 

 

 

 

한 마디로 요약하면 '주의'다. 깨끗한 코드는 주의 깊게 작성한 코드다.누군가 시간을 들여 깔끔하고 단정하게 정리한 코드다. 세세한 사항까지 꼼꼼하게 신경쓴 코드다. 주의를 기울인 코드다.


최근 들어 나는 켄트 벡이 제안한 단순한 코드 규칙으로 구현을 시작한다. (그 리고 같은 규칙으로 구현을 거의 끝낸다.) 중요한 순으로 나열하자면 간단한 코 드는

· 모든 테스트를 통과한다.

· 중복이 없다.

· 시스템 내 모든 설계 아이디어를 표현한다. · 클래스, 메서드, 함수 등을 최대한 줄인다.

물론 나는 주로 중복에 집중한다. 같은 작업을 여러 차례 반복한다면 코드가 아 이디어를 제대로 표현하지 못한다는 증거다. 나는 문제의 아이디어를 찾아내 좀 더 명확하게 표현하려 애쓴다.

내게 있어 표현력은 의미 있는 이름을 포함한다. 보통 나는 확정하기 전에 이 름을 여러 차례 바꾼다. 이클립스와 같은 최신 개발 도구는 이름을 바꾸기가 상 당히 쉽다. 그래서 별 고충 없이 이름을 바꾼다. 하지만 표현력은 이름에만 국한되지 않는다. 나는 여러 기능을 수행하는 객체나 메서드도 찾는다. 객체가 여러 기능을 수행한다면 여러 객체로 나눈다. 메서드가 여러 기능을 수행한다면 메서 드 추출Extract Method 리팩터링 기법을 적용해 기능을 명확히 기술하는 메서드 하 나와 기능을 실제로 수행하는 메서드 여러 개로 나눈다.

중복과 표현력만 신경 써도 (내가 생각하는) 깨끗한 코드라는 목표에 성큼 다 가선다. 지저분한 코드를 손볼 때 이 두 가지만 고려해도 코드가 크게 나아진다. 하지만 나는 한 가지를 더 고려한다. 이는 설명하기 조금 까다롭다.

오랜 경험 끝에 나는 모든 프로그램이 아주 유사한 요소로 이뤄진다는 사실을 깨달았다. 한 가지 예가 ‘집합에서 항목 찾기’다. 직원 정보가 저장된 데이터베이 스든, 키/값 쌍이 저장된 해시 맵이든, 여러 값을 모아놓은 배열이든, 프로그램을 짜다 보면 어떤 집합에서 특정 항목을 찾아낼 필요가 자주 생긴다. 이런 상황이 발생하면 나는 추상 메서드나 추상 클래스를 만들어 실제 구현을 감싼다. 그러면 여러 가지 장점이 생긴다. 이제 실제 기능은 아주 간단한 방식으로, 예를 들어 해시 맵으로, 구현해도 괜 찮다. 다른 코드는 추상 클래스나 추상 메서드가 제공하는 기능을 사용하므로 실 제 구현은 언제든지 바꿔도 괜찮다. 지금은 간단하게 재빨리 구현했다가 나중에 필요할 때 바꾸면 된다. 게다가 집합을 추상화하면 ‘진짜’ 문제에 신경 쓸 여유가 생긴다. 간단한 찾기 기능이 필요한데 온갖 집합 기능을 구현하느라 시간과 노력을 낭비할 필요가 없 어진다. 중복 줄이기, 표현력 높이기, 초반부터 간단한 추상화 고려하기. 내게는 이 세 가지가 깨끗한 코드를 만드는 비결이다.

-론 제프리스(Extreme Progamming Installed와 Extreme Programming Adventure in c#의 저자)

 

결론은 중복을 피하라. 한기능만 수행해라. 제대로 표현하라. 작게 추상화해라.


코드를 읽으면서 짐작했던 기능을 각 루틴이 그 대로 수행한다면 깨끗한 코드라 불러도 되겠다. 코드가 그 문제를 풀기 위한 언어처럼 보인다면 아름다운 코드라 불러도 되겠다.

-워드 커닝햄Ward Cunningham (위키Wiki 창시자, 피트Fit 창시자, 익스트림 프로그래밍eXtreme Programming 공동 창시자, 디자인 패턴을 뒤에서 움직이는 전문가, 스몰토크Smalltalk와 객체 지향OO의 정신적 지도자, 코드를 사랑하는 프로그래머들의 대부 )

 

명백하고 단순해 마음이 끌리는 코드가 깨끗한 코드다. 너무도 잘 짜놓은 코드라 읽는 이가 그 사실을 모르고 넘어간다.


우리들 생각

절대적으로 옳은 것은 없다. 이 책은 오브젝트 멘토 진영이 생각하는 깨끗한 코드를 설명한다.

*보이스카웃 규칙

잘 짠 코드가 전부는 아니다. 시간이 지나도 언제나 깨끗하게 유지해야 한다.

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라.!!!!

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 8장 경계  (2) 2022.09.15
[Clean Code] 7장 오류 처리  (0) 2022.09.14

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 때로는 패키지를 사고, 때로는 오픈 소스를 이용한다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다. 어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.


외부 코드 사용하기

패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넒히려 애쓴다. 더 많은 환경에서 돌아가야 더 많은 고객이 구매하니까. 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.

한 예로 Map은 다양한 인터페이스를 제공한다. 하지만 그만큼 위험도 크다.

• clear() void – Map
• containsKey(Object key) boolean – Map
• containsValue(Object value) boolean – Map
• entrySet() Set – Map
• equals(Object o) boolean – Map
• get(Object key) Object – Map
• getClass() Class<? extends Object> – Object
• hashCode() int – Map
• isEmpty() boolean – Map
• keySet() Set – Map
• notify() void – Object
• notifyAll() void – Object
• put(Object key, Object value) Object – Map
• putAll(Map t) void – Map
• remove(Object key) Object – Map
• size() int – Map
• toString() String – Object
• values() Collection – Map
• wait() void – Object
• wait(long timeout) void – Object
• wait(long timeout, int nanos) void – Object

Map을 깔끔하게 사용한 코드다. Sensors 사용자는 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없다. 제네릭스의 사용 여부는 Sensors 안에서 결정한다.

public class Sensors {
 private Map sensors = new HashMap();
 public Sensor getById(String id) {
 return (Sensor) sensors.get(id);
 }
 // 이하 생략
}

경계 인터페이스인 Map을 Sensors 안으로  숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 제네릭스를 사용하든 하지 않든 더 이상 문제가 안 된다. Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문이다.


경계 살피고 익히기

외부 코드를 사용하면 쉽고 빠르다. 하지만 이 코드의 테스트 및 안정성은 우리의 몫이다.

 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떻까?

짐 뉴커크는 이를 학습 테스트라 부른다.

 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.


Log4j 익히기

기초적인 "hello"를 출력하는 테스트 케이스를 한다.

@Test
public void testLogCreate() {
 Logger logger = Logger.getLogger("MyLogger");
 logger.info("hello");
}

테스트 케이스를 돌렸더니 Appender라는 오류가 나오고 이후에는 출력 스트림이 없다고 나온다. 그래서 코드를 수정한다.

@Test
public void testLogAddAppender() {
 Logger logger = Logger.getLogger("MyLogger");
 logger.removeAllAppenders();
 logger.addAppender(new ConsoleAppender(
 new PatternLayout("%p %t %m%n"),
 ConsoleAppender.SYSTEM_OUT));
 logger.info("hello");
}

위처럼 학습을 통해 지식을 얻는다.


학습 테스트는 공짜 이상이다.

학습 테스트는 이해도를 높여주면서 비용은 없는 실험이다.

 학습 테스트는 공짜 이상이다. 투자하는 노력보다 얻는 성과가 더 크다. 패키지가 예상대로 도는지 검증한다. 일단 통합한 이후라고 하더라도 패키지가 우리 코드와 호환되라라는 보장은 없다.


아직 존재하지 않는 코드를 사용하기

경계와 관련해  또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다. 때로는 우리 지식이 경계를 너머 미치지 못하는 코드 영역도 있다. 때로는 알려고 해도 알 수가 없다. 때로는 더 이상 내다보지 않기로 결정한다.


깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다. 변경이 대표적인 예다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 엄청난 시간과 노력과 재작업을 요구하지 않는다. 통제하지 못하는 코드를 사용할 때는 시간과 노력이 많이 필요하다.

 

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16
[Clean Code] 7장 오류 처리  (0) 2022.09.14

앱의 맨 윗부분에 main 함수가 있다.

void main() => runApp(MyApp));

다트 프로그램 처럼 앱도 main 함수가 진입점이다. 플러터에서는 runApp이라는 메서드로 최상위 위젯을 감싼다. 극단적으로 이 한 행의 코드만으로도 앱을 만들 수 있다. 기능이 다양한 앱은 main 함수에서 더 많은 작업을 수행한다. 하지만 이런 앱도 반드시 최상위 위젯을 runApp의 인수로 전달해 호출해야 한다.

오류 처리는 프로그램에 반드시 필요한 요소 중 하나일 뿐이다. 잘못될 가능성은 늘 존재한다.

깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 상당수 코드 기반은 전적으로 오류 처리 코드에 좌우된다.


오류 코드보다 예외를 사용하라

얼마 전까지만 해도 예외를 지원하지 않는 프로그래밍 언어가 많았다.

public class DeviceController {
 ...
 public void sendShutDown() {
 DeviceHandle handle = getHandle(DEV1);
 // 디바이스 상태를 점검한다.
 if (handle != DeviceHandle.INVALID) {
 // 레코드 필드에 디바이스 상태를 저장한다.
 retrieveDeviceRecord(handle);
 // 디바이스가 일시정지 상태가 아니라면 종료한다.
 if (record.getStatus() != DEVICE_SUSPENDED) {
 pauseDevice(handle);
 clearDeviceWorkQueue(handle);
closeDevice(handle);
 } else {
 logger.log("Device suspended. Unable to shut down");
 }
 } else {
 logger.log("Invalid handle for: " + DEV1.toString());
 }
 }
 ...
}

위와 같은 방법을 사용하면 호출자 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다.

위와 같은 단계는 잊어버리기 쉽다. 그래서 오류가 발생하면 예외를 던지는 편이 낫다.

public class DeviceController {
 ...
 public void sendShutDown() {
 try {
 tryToShutDown();
 } catch (DeviceShutDownError e) {
 logger.log(e);
 }
 }
 private void tryToShutDown() throws DeviceShutDownError {
 DeviceHandle handle = getHandle(DEV1);
 DeviceRecord record = retrieveDeviceRecord(handle);
 pauseDevice(handle);
 clearDeviceWorkQueue(handle);
 closeDevice(handle);
 }
 private DeviceHandle getHandle(DeviceID id) {
 ...
throw new DeviceShutDownError("Invalid handle for: " + id.toString());
 ...
 }
 ...
}

코드의 품질이 나아졌다. 알고리즘과 오류 처리 부분을 분리했기 때문이다. 이제는 각 개념을 독립적으로 살펴 보고 이해할 수 있다.


Try-Catch-Finally 문부터 작성하다.

어떤 면에서 try 블록은 트랜잭션과 비슷하다. try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다. 그러므로 예외가 발생할 코드를 짤 때는 rry-catch-finally 문으로 시작하는 편이 낫다. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.

@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
 sectionStore.retrieveSection("invalid - file");
}
public List<RecordedGrip> retrieveSection(String sectionName) {
 // 실제로 구현할 때까지 비어 있는 더미를 반환한다.
 return new ArrayList<RecordedGrip>();
}

아래 코드는 예외를 던진다.

public List<RecordedGrip> retrieveSection(String sectionName) {
 try {
 FileInputStream stream = new FileInputStream(sectionName)
 } catch (Exception e) {
 throw new StorageException("retrieval error", e);
 }
 return new ArrayList<RecordedGrip>();
}
public List<RecordedGrip> retrieveSection(String sectionName) {
 try {
 FileInputStream stream = new FileInputStream(sectionName);
 stream.close();
 } catch (FileNotFoundException e) {
 throw new StorageException("retrieval error", e);
 }
 return new ArrayList<RecordedGrip>();
}

미확인 예외를 사용하라

여러 해 동안 자바 프로그래머들은 확인된 예외의 장단점을 놓고 논쟁을 벌여왔다. 메서드를 선언할 때는 ㅔㅁ서드가 반환할 예외를 모두 열거했다. 게다가 메서드가 반환하는 예외는 메서드 유형의 일부였다. 코드가 메서드를 사용하는 방식이 메서드 선언과 일치하지 않으면 아예 컴파일도 못했다.

확인된 예외는 OCP(open closed principle)을 위반한다. 메서드에서 확인된 예외를 던졌는데 catch 블록이 세단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다. 즉, 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다는 말이다.


예외에 의미를 제공하라

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다. 자바는 모든 예외에 호출 스택을 제공한다. 하지만 실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다.

오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 애플리케이션이 로깅 기능을 사용한다면 catch블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.


호출자를 고려해 예외 클래스를 정의하라

오류를 분류하는 방법은 수없이 많다. 오류가 발생한 위치로 분류가 가능하다. 예를 들어, 오류가 발생한 컴포넌트로 분류한다. 아니면 유형으로도 분류가 가능하다.

ACMEPort port = new ACMEPort(12);
try {
 port.open();
} catch (DeviceResponseException e) {
 reportPortError(e);
 logger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {
 reportPortError(e);
 logger.log("Unlock exception", e);
} catch (GMXError e) {
reportPortError(e);
 logger.log("Device response exception");
} finally {
 ...
}

위는 외부 라이브러리가 던질 예외를 모두 잡은것으로 형편없이 분류한 사례다.

LocalPort port = new LocalPort(12);
try {
 port.open();
} catch (PortDeviceFailure e) {
 reportError(e);
 logger.log(e.getMessage(), e);
} finally {
 ...
}

호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 된다.

감싸기 기법을 사용하면 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다. 프로그램이 사용하기 편리한 API를 정의하면 그만이다.

흔히 예외 클래스가 하나만 있어도 충분한 코드가 많다. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그렇다. 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우라면 여러 예외 클래스를 사용한다.


정상 흐름을 정의하라

위의 흐름대로 예외를 정의하다 보면 오류 감지가 프로그램 언저리로 밀려난다. 외부 API를 감싸 독자적인 예외를 던지고, 코드 위에 처리기를 정의해 중단된 계산을 처리한다. 대개는 멋진 처리 방식이지만, 때로는 중단이 적합하지 않은 때도 있다.


null을 반환하지 마라

오류 처리를 논하는 장이라면 우리가 흔히 저지르는 바람에 오류를 유발하는 행위도 언급해야 한다고 생각한다. 그 중 첫째가 null을 반환하는 습관이다.

public void registerItem(Item item) {
 if (item != null) {
 ItemRegistry registry = peristentStore.getItemRegistry();
 if (registry != null) {
 Item existing = registry.getItem(item.getID());
 if (existing.getBillingPeriod().hasRetailOwner()) {
 existing.register(item);
 }
 }
 }
}

위 코드는 두번째 행에서 null확인이 누락된다.


null을 전달하지 마라

정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 더 나쁘다. 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

public class MetricsCalculator
{
 public double xProjection(Point p1, Point p2) {
return (p2.x – p1.x) * 1.5;
 }
 ...
}

인수로 누군가 null을 전달하면??

당연히 NullPointException이 발생한다.

대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다. 그렇다면 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.


결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 이 둘은 상충하는 목표가 아니다. 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

' > Clean Code' 카테고리의 다른 글

[Clean Code] 4장 주석  (0) 2022.09.20
[Clean Code] 3장 함수  (0) 2022.09.19
[Clean Code] 2장 의미 있는 이름  (2) 2022.09.17
[Clean Code] 1장 깨끗한 코드  (1) 2022.09.16
[Clean Code] 8장 경계  (2) 2022.09.15

+ Recent posts