[워밍업 클럽 스터디 2기 - BE]  1주차 발자국

[워밍업 클럽 스터디 2기 - BE] 1주차 발자국

해당 내용을 바탕으로 작성된 블로그 입니다.

추상

우리가 클린 코드를 추구하는 이유

가독성 - Readability

글이 잘 읽힌다 = 이해가 잘 된다. = 유지보수 하기 수월해진다. = 우리의 시간과 자원이 절약된다.

 

클린코드를 관통하는 아주 중요한 주제

바로 추상(抽象)

 

추상과 구체

도메인 영역에서 핵심만 남기는 행위가 추상, 이를 보고 유추할 수 있는게 구체

중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다

 

추상화의 가장 대표적인 행위 : 이름 짓기

 

이름 짓기

이름을 짓는다는 행위는, 추상적 사고를 기반으로 한다.

 

이름 짓기 팁

  • 단수와 복수를 구분하기

  • 이름 줄이지 않기

  • 은어/방언 사용하지 않기

  • 좋은 코드를 보고 습득하기

단수와 복수를 구분하기

끝에 '(e)s'를 붙여 어떤 데이터(변수, 클래스 등)가 단수인지, 복수인지를 나타내는 것만으로도 읽는 이에게 중요한 정보를 같이 전달할 수 있다.

이름 줄이지 않기

줄임말이라는 것은 가독성을 제물로 바쳐 효율성을 얻는 것으로, 대부분 잃는 것에 비해 얻는 것이 적다.

은어/방언 사용하지 않기

  • 농담에서 파생된 용어, 일부 팀원/현재의 우리 팀만 아는 용어 금지

    • Q. 새로운 사람이 합류했을 때 이 용어를 단번에 이해할 수 있는가?

  • 도메인 용어 사용하기

    • 도메인 용어를 먼저 정의하는 과정(ex. 도메인 용어 사전)이 먼저 필요할 수도 있다.

    좋은 코드를 보고 습득하기

비슷한 상황에서 자주 사용하는 단어, 개념 습득하기

→ ex. pool, candidate, threshold 등

 

이름을 지으면서 코드를 읽을 수 있게 됐다.

 

메서드와 추상화

국어/영어 독해할 때 잘 쓰여진 글이라면 한 문단의 주제는 반드시 하나다.

메서드의 이름으로 구체를 추상화 하는 것이다.

잘 쓰여진 코드라면 한 메서드의 주제는 반드시 하나다.

 

메서드 선언부

* 메서드 시그니처 : 메서드명 + 파라미터 (메서드 오버로드)

메서드명

  • 추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름

  • 파라미터와 연결지어 더 풍부한 의미를 전달할 수도 있다.

파라미터

  • 파라미터의 타입, 개수, 순서를 통해 의미를 전달

  • 파라미터는 외부 세계와 소통하는 창

반환타입

  • 메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기

    • 반환 타입이 boolean인데, 이게 이 메서드에서 무엇을 의미하는지?

  • void 대신 충분히 반환할 만한 값이 있는지 고민해보기

    • 반환값이 있다면 테스트도 용이해진다.

[지뢰찾기 - 메서드 추출](https://github.com/iamminseongKim/readable-code/commit/66523910ce932a0e103f22bc3ecf2d93f418e116#diff-9f9444b48fc52a78913dc852f9b4c9a5cc42aa4e5525c09fe92c5d24b6e41182)

 

추상화 레벨

하나의 세계 안에서는, 추상화 레벨이 동등해야 한다.

public static void main(String[] args) {

	showGameStartComments();
	initializeGame();
	showBoard();

	if (gameStatus == 1) {  
	    System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!");  
	    break;  
	}

	...

	checkIfGameIsOver();
}

자 이 코드에서 메서드를 쭉쭉 호출하다가 갑자기 gameStatus == 1 이런 코드가 나와 멈칫하게 된다.

이러한 점이 레벨이 안맞다는 점이다.

그럼

public static void main(String[] args) {
	showGameStartComments();
	initializeGame();
	showBoard();

	if (doesUserWinTheGame()) {  
	    System.out.println("지뢰를 모두 찾았습니다. GAME CLEAR!");  
	    break;  
	}
	...
	checkIfGameIsOver();
}

private boolean doesUserWinTheGame() {
	return gameStatus == 1;
}

다음과 같이 레벨을 맞춰주자.

 

매직 넘버, 매직 스트링

 

매직 넘버, 매직 스트링?

  • 의미를 갖고 있으나, 상수로 추출되지 않는 숫자, 문자열 등

  • 상수 추출로 이름을 짓고 의미를 부여함으로써 가독성, 유지보수성 증가

     

[추상화 레벨 & 매직 넘버, 매직 스트링](https://github.com/iamminseongKim/readable-code/commit/ccdb4d9894b82c6f340acfb7543414423669a296)

상수를 뽑아내므로써 가독성, 유지보수성이 증가한걸 느낌.

 

논리, 사고의 흐름

뇌 메모리 적게 쓰기

  • 정리하는 뇌 : 정리 시스템에서 중요한 과제는 최소의 인지적 노력으로 최대의 정보를 제공하는 것이다.

  • 도둑맞은 집중력 : 뇌는 한 번에 한 가지 일밖에 하지 못한다. 멀티테스킹? 그건 저글링일 뿐.

인지적 경제성

뇌 메모리 적게 쓰기.. 읽는 사람이 뇌를 적게 쓰기 위해 만들 때 잘 쓰자..

 

Early return

if (a > 3) {
	doSomething1();
} else if (a <= 3 && b > 1) {
	doSomething2();
} else {
	doSomething3();
}

이 if문을 쭉쭉 내려가면서 생각해보면

마지막 else를 생각할 때 쯤이면 모든 if 조건을 다 인지하고 있어야 그 예외를 생각하고 doSomething3()이 실행되겠구나 생각할 것이다.

이걸 해결하기 위해서 사용하면 좋은 것이 Early return이다.


extracted();

void extracted() {
	if(a > 3) {
		doSomething1();
		return;
	}
	
	if(a<=3 && b>1) {
		doSomething2();
		return;
	}
	
	doSomething3();
}

 

Early return으로 else의 사용을 지양, else를 쓰지 않아도 되면 쓰지 않도록 노력하자.

[Early return 코드](https://github.com/iamminseongKim/readable-code/commit/31dc6569a71028141c665e2f341eb7d05cb0896f)

사고의 depth 줄이기

중첩 분기문, 중첩 반복문

중첩 for문을 지양하고 바깥이 어떻게 도는지 알 필요 없게 만들자.

 

주의

  • "무조건 1depth로 만들어라"가 아니다.

    • 보이는 Depth를 줄이는데 급급한 것이 아니라 추상화를 통한 사고 과정의 depth를 줄이는 것이 중요

    • 2중 중첩 구조로 표현하는 것이 사고하는데에 더 도움이 된다고 판단한다면, 메서드 분리보다 그대로 놔두는 것이 더 나은 선택일 수 있다. 떄로는 메서드를 분리하는 것이 더 혼선을 줄 수 있다.

[예제 코드 - 중첩 for문 (Stream 사용)](https://github.com/iamminseongKim/readable-code/commit/37eaf9ebc8f5c4774cf295d5e3be3dfb82b1c9bb)

 

사용할 변수는 가깝게 선언하기

int i = 10;

// 저 ~~아래 한 20줄 

int j = i + 30;

이런 식으로 사용하지 말자.

// 저 아래 20줄 

int i = 10;
int j = i + 30;

가깝게 선언하자.

[예제 코드 - scanner 수정(상수로 또 뺌)](https://github.com/iamminseongKim/readable-code/commit/bd15a345918cf602506cb8182a4aa5b90a826877)

 

공백 라인을 대하는 자세

공백 라인도 의미를 가진다

  • 복잡한 로직의 의미 단위를 나누어 보여줌으로써 읽는 사람에 추가적인 정보를 제공할 수 있다.

부정어를 대하는 자세

  • 부정어구를 쓰지 않아도 되는 상황인지 체크

  • 부정의 의미를 담은 다른 단어가 존재하는지 고민하기 or 부정어구로 메서드명 구성

    • 부정 연산자(!) 는 가독성이 떨어질 수 있다.

[예제 코드 - isLandMineCell()에 부정연산자 제거한 과정](https://github.com/iamminseongKim/readable-code/commit/1070112c4dfd0d230f1edbda6ad0a2d8f612509c)

 

해피 케이스와 예외 처리

  • 사람은 해피 케이스에 몰두하는 경향이 있다.

    • 예외처리를 꼼꼼히 해야 소프트웨어가 견고해진다.

  • 예외가 발생할 가능성 낮추기

  • 어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점

    • 사용자 입력, 객체 생성자, 외부 서버의 요청 등

  • 의도한 예외와 예상하지 못한 예외를 구분하기

    • 사용자에게 보여줄 예외와, 개발자가 보고 처리해야 할 예외 구분

Null을 대하는 자세

  • 항상 NullPointException을 방지하는 방향으로 경각심을 가지자.

  • 메서드 설계 시 return null을 자제한다.

    • 만약 어렵다면, Optional 사용을 고민해본다.

Optional에 관하여

  • Optional은 비싼 객체이다. 꼭 필요한 상황에서 반환 타입에 사용한다.

  • Optional을 파라미터로 받지 않도록 한다. 분기 케이스가 3개나 된다.

    • Optional이 가진 데이터가 null인지 아닌지, + Optional 그 자체가 Null인지

  • Optional을 반환 받았다면 최대한 빠르게 해소한다.

Optional을 해소하는 방법

  • 분기문을 만드는 isPresent()-get() 대신에 풍부한 API 사용

    • ex) orElseGet(), orElseThrow(), ifPresent(), ifPresentOrElse()

  • orElse(), orElseGet(), orElseThrow()의 차이 숙지

    • orElse() : 항상 실행, 확정된 값일 때 사용

    • orElseGet() : null인 경우 실행, 값을 제공하는 동작(Supplier) 정의

    • orElseThrow() : 그냥 쓰면됨, 없으면 예외 만들어서 던짐.

[해피 케이스와 예외처리 - 예제](https://github.com/iamminseongKim/readable-code/commit/cb02070c415ed2f2c70cf417cca0701015fe2f18)

 



객체 지향 패러다임

정해진 순서 차례대로 진행하는 프로그래밍 절차 지향

객체라는걸 만들어서 상호 작용을 하도록 개발하는 객체지향

사이드 이펙트가 없는 a를 넣으면 항상 같은 리턴을 주는 순수 함수

이런 함수를 기반으로 개발하는 함수형 프로그래밍

 

추상의 관점으로 바라보는 객체 지향

객체 : Object, 추상화된 (데이터 + 코드)

협력과 책임

객체간의 협력 + 객체가 담당하는 책임

캡추상다

  • 캡슐화 : 객체가 가지고있는 데이터, 로직을 숨기고 일부만 보여줌

  • 추상화 : 요약

  • 상속 : 진짜 필요한 곳에만 사용

  • 다형성 : 인터페이스화 추상화

관심사의 분리

Seperation Of Concern

높은 응집도, 낮은 결합도

a라는 관점을 모아서 관리 -> 유지보수성 증가

그리고 이렇게 모인 관심사들 끼리는 결합도가 낮게 개발.

a를 수정했는데 b에 영향이 없게 개발.

 

객체 설계하기 1

오브젝트도 데이터나 로직은 숨기고, 이를 공개적인 메서드를 통해서만 외부에서 소통할 수 있도록 추상화 해야 한다.

  • 비공개 필드 (데이터), 비공개 로직(코드)

  • 공개 메서드 선언부를 통해 외부 세계와 소통

    • 각 메서드의 기능은 객체의 책임을 드러내는 창구

  • 객체의 책임이 나뉨에 따라 객체간 협력이 발생

객체가 제공하는 것

  • 절차 지향에서 잘 보이지 않았던 개념을 가시화

  • 관심사가 한 군데로 모이기 때문에, 유지보수성 증가

    • 객체 내부에서 객체가 가진 데이터의 유효성 검증 책임을 가질 수 있다.

  • 여러 객체를 사용하는 입장에서는, 구체적인 구현에 신경쓰지 않고 보다 높은 추상화 레벨에서 도메인 로직을 다룰 수 있다.

새로운 객체를 만들 때 주의할 점

  • 1개의 관심사로 명확하게 책임이 정의되었는지 확인하기

    • 메서드를 추상화 할 때와 비슷하다.

    • 객체를 만듦으로써 외부 세계와 어떤 소통을 하려고 하는지 생각해보자.

  • 생성자, 정적 팩토리 메서드에서 유효성 검증이 가능하다.

    • 도메인에 특화된 검증 로직이 들어갈 수 있다.

 

  • setter 사용 자제

    • 데이터는 불변이 최고다. 변하는 데이터더라도 객체가 핸들링할 수 있어야 한다.

    • 객체 내부에서 외부 세계의 개입 없이 자체적인 변경/가공으로 처리할 수 있는지를 확인

    • 만약 외부에서 가지고 있는 데이터로 데이터 변경 요청을 해야하는 경우, 'set~'이라는 단순한 이름보다는 update~같이 의도를 드러내는 네이밍을 고려하자.

  • getter도 처음에는 사용 자제. 반드시 필요한 경우에 추가하기

    • 외부에서 객체를 내 데이터가 필요하다고 getter를 남발하는 것은 무례한 행동이다!

    • 객체에 메시지를 보내라!

 

  • 필드의 수는 적을수록 좋다.

    • 불필요한 데이터가 많을 수록 복잡도가 높아지고 대응할 변화가 많아진다.

    • 필드 A를 가지고 계산할 수 있는 A'필드가 있다면, 메서드 기능으로 제공

    • 단, 미리 가공하는 것이 성능 상 이점이 있다면, 필드로 가지고 있는 것이 좋을 수도 있다.

도메인 지식은 만드는 것이 아니라 발견하는 것

[Board 객체화](https://github.com/iamminseongKim/readable-code/commit/2c04b02413a879e3135a1fd905abb445a4d9dcd9)

[Board - Sign Cell 넣기](https://github.com/iamminseongKim/readable-code/commit/7a593983ce867b9233172344189b781c6bf207c1)

[cell을 도메인 지식을 통해 리팩토링](https://github.com/iamminseongKim/readable-code/commit/295a3d84a208216fc630e52dee08cbfb1385abfc)

 

SOLID

  • SRP : Single Responsibility Principle

  • OCP : Open-Closed Principle

  • LSP : Liskov Substitution Principle

  • ISP : Interface Segergation Principle

  • DIP : Dependency Inversion Principle

 

 

객체 지향 적용하기

상속과 조합

  • 상속보다 조합을 사용하자!

[객체지향 적용하기 - 상속과 조합 - 셀을 상속에서 조합으로 바꾸기](https://github.com/iamminseongKim/readable-code/commit/86a49db553a752eca745a0d462125ee0d0b4ad70)

Value Object

  • 도메인의 어떤 개념을 추상화하여 표현한 값 객체

  • 값으로 취급하기 위해서 불변성, 동등성, 유효성 검증 등을 보장해야 한다.

    • 불변성 : final 필드, setter 금지

    • 동등성 : 서로 다른 인스턴스여도(동일성이 달라도), 내부의 값이 같으면 같은 값 객체로 취급한다. equals() & hashCode() 재정의 필요

    • 유효성 검증 : 객체가 생성되는 시점에 값에 대한 유효성을 보장하기

만원 지폐가 일렬번호(인스턴스)가 다르다고 가치가 다른 즉 다른 만원인가?

 

VO vs Entity

 

class UserAccount {
	private String userId; // 식별자
	private String 이름;
	private String 생년월일;
	private Address 집주소;
}

이건 Entity

class Address {
	private String 시도;
	private String 시군구;
	private String 도로명;
	private String 건물번호;
}

이건 VO 이다.

 

  • Entity는 식별자가 존재한다. 식별자가 아닌 필드의 값이 달라도, 식별자가 같으면 동등한 객체로 취급한다.

    • equals() & hashCode()도 식별자 필드만 가지고 재정의할 수 있다.

       

    • 식별자가 같은데 식별자가 아닌 필드의 값이 서로 다른 두 인스턴스가 있다면, 같은Entity가 시간이 지남에 따라 변화한 것으로 이해할 수 있다.

  • VO는 식별자 없이 내부의 모든 값이 다 같아야 동등한 객체로 취급한다.

    • 개념적으로, 전체 필드가 다 같아야 식별자 역할을 한다고 생각해도 된다.

       

[Value Object - Cell의 상태를 value object로 만들기.](https://github.com/iamminseongKim/readable-code/commit/b667b43de3dfa2fa78fc6a4baf24af570acf69d6)

 

미션

미션 1 추상과 구체 예시

내가 생각한 추상은 모두가 이해할 수 있도록 말을 요약이라 생각해서

내가 좋아하는 야구에서 이 예시를 찾아봤다.

야구를 좀 본사람은 4-6-3 병살 저 6자만 봐도 어떤 일이 일어났는지 바로 이해할 것이다.

그래서 나는 이걸로 미션을 제출했다.

 

미션 2 코드 리팩토링

주문관련 코드를 리팩토링 하는 미션이였다.

코드를 보면 if-else로 많이 감싸져 있어서 그걸 제일 먼저 Early return을 이용해서 바꿔줬고,

그 다음엔 return false보다는 예외로 값이 잘못된 것을 알려줬다.

마지막으로 if문 안에 추상화 레벨을 맞추기 위해 메서드를 추출했다.

 

아쉬운 점은 예외를 커스텀 예외로 만드는 걸 안했고, if문 검증 로직을 굳이 service단이 아니라 order객체 내에서

했어도 좋았을 것 같다는 생각을 했다.

 

느낀점

자바를 2년이상 써오면서 원래 알았던 개념도 있고, 처음 봤던 개념도 있었다.

그런데 이번 내용들을 학습하면서 항상 클린코드 클린코드 해야지 생각만 하던걸

이젠 직접 어떻게 작성 해야하는지, 어떤 점을 고려해야 하는지 좀 감을 잡을 수 있었던 것 같다.

 

솔직히 공부를 많이 안했던 것 같아서 좀 부끄럽지만 이번 계기로 클린코드와 테스트 코드에 자신감을 가질 수 있도록 노력하겠다.

 

 

댓글을 작성해보세요.

채널톡 아이콘