You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
우리가 클린코드를 추구하는 이유
코드가 잘 읽힌다
= 이해가 잘된다
= 유지보수 하기가 수월하다
= 우리의 시간과 자원이 절약된다
클린코드라 지칭하는 수많은 원칙들, 조언들 => 당연히 지키면 좋다, 왜?
클린코드를 관통하는 아주 중요한 주제 => 추상
프로그램의 정의
프로그램 = 데이터 + 코드
데이터 자체가 추상화 레벨을 가지고 있다.
데이터가 식재료라면 코드는 요리방법이다.
추상과 구체
추상 : 뽑을 추, 코끼리 상 => 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다.
구체 <-> 추상
추상화 레벨 : 구체에서 추상으로 가는 과정의 레벨
고수준 : 추상화 레벨이 높다 => 저수준 : 추상화 레벨이 낮다
Ex) 기계어 => 프로그래밍 언어 / 하드웨어 => 운영체제 => 애플리케이션
적절한 추상화는 복잡한 데이터와 복잡한 로직을 단순화하여 이해하기 쉽도록 돕는다.
추상으로부터 구체를 유추하지 못하는 경우
추상화 과정에서 중요한 정보를 부각시키지 못했다
해석자가 동일하게 공유하는 문맥(Context)이 없다
서로 다른 도메인에서는 전혀 다르게 해석될수 있다.
잘못된 추상화가 야기하는 side effect 는 정말 크다
적절한 추상화는 해당 도메인의 문맥 안에서 정말 중요한 핵심 개념만 남겨서 표현하는 것
이름 짓기
이름을 짓는다는 행위는, 추상적 사고를 기반으로한다. => 가장 단순하면서도, 아주 중요한, 고도의 추상적 사고 행위
표현하고자 하는 구체에서 정말 중요한 핵심 개념만을 추출하여 잘 드러내는 표현
우리 도메인의 문맥 안에서 이해되는 용어
단수와 복수를 구분하기
이름 줄이지 않기
좋은 코드를 보고 습득하기
ex. pool, candidate, threshold
메서드와 추상화
잘 쓰여진 글이라면, 한 문단의 주제는 반드시 하나다 => 잘 쓰여진 코드라면, 한 메서드의 주제는 반드시 하나다.
메서드 선언부
메서드 선언부 = 반환타입 메서드명 (파라미터) {메서드 구현부}
메서드 시그니처 = 메서드명 (파라미터)
메드명
추상화된 구체를 유추할 수 있는, 적절한 의미가 담긴 이름
파라미터와 연결지어 더 풍부한 의미를 전달할 수도 있다.
파라미터
파라미터의 타입, 개수, 순서를 통해 의미를 전달 🌟
파라미터는 외부 세계와 소통하는 창
반환타입
메서드 시그니처에 납득이 가는, 적절한 타입의 반환값 돌려주기
-> 반환 타입이 boolean인데, 이게 이 메서드에서 무엇을 의미하는거지?
void 대신 충분히 반환할 만한 값이 있는지 고민해보기
-> 반환값이 있다면 테스트도 용이해 진다.
추상화 레벨
하나의 세계 안에서는, 추상화 레벨이 동등해야 한다.
추상화 레벨을 주변과 동등하게 해야한다.
메서드를 추출한다는 의미는 로직이 길어서 줄이기 위해서나 메서드 자체에 의미를 부여하기 위해서도 있지만
중요한건 추상화 레벨을 동등하게 맞춰줌으로써 코드를 읽는 사람의 사고가 자연스럽게 흘러갈수 있도록 하는것이다.
매직넘버, 매직스트링
상수로 추출한다는 것의 의미 => 이름을 부여하기 때문에 일종의 추상화라고 볼수 있다.
의미를 갖고 있으나, 상수로 추출되지 않은 숫자, 문자열 등
상수 추출로 이름을 짓고 의미를 부여함으로써 가독성, 유지보수성🆙️
<논리, 사고의 흐름>
뇌 메모리 적게 쓰기
뇌는 한가지 일만 할수있다.(멀티태스킹이 된다는 것은 일처리의 전환이 빠르다는것)
뇌 메모리에 적게 올릴수록 당연히 읽기 좋은 코드가 된다.
Early return
else의 사용을 지양
사고의 depth 줄이기
중첩 분기문, 중첩 반복문
무조건 1depth로 만들어라 가 아니다.
=> 보이는 depth를 줄이는 데에 급급한 것이 아니라, 추상화를 통한 사고 과정의 depth를 줄이는 것이 중요
=> 2중 중첩 구조로 표현하는 것이 사고하는 데에 더 도움이 된다고 판단한다면,
메서드 분리보다 그대로 놔두는 것이 더 나은 선택일 수 있다.
때로는 메서드를 분리하는 것이 더 혼선을 줄 수도 있다.
사용할 변수는 가깝게 선언하기
공백 라인을 대하는 자세
공백 라인도 의미를 가진다.
부정어를 대하는 자세
부정어구를 쓰지 않아도 되는 상황인지 체크하기
ex) isRightDirection()
부정의 의미를 담은 다른 단어가 존재하는지 고민하기 or 부정어구로 메서드명 구성 -> 부정 연산자(!)는 가독성 ⬇️
ex) isNotLeftDirection()
해피 케이스와 예외처리
예외가 발생할 가능성 낮추기
어떤 값의 검증이 필요한 부분은 주로 외부 세계와의 접점
=> 사용자 입력, 객체 생성자, 외부 서버의 요청 등
의도한 예외와 예상하지 못한 예외를 구분하기
=> 사용자에게 보여줄 예외와, 개발자가 보고 처리해야 할 예외 구분
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() 의 차이를 숙지하고 있어야한다.(성능상 차이가 있을수 있음)
orElse(): 항상 실행, 확정된 값일 때 사용
orElseGet(): null인 경우 실행, 값을 제공하는 동작(supplier) 정의
<객체 지향 패러다임>
추상의 관점으로 바라보는 객체 지향
절차지향/객체지향/함수형
캡추상다
캡슐화/추상화/상속/다형성 => 의미를 정확하게 이해하고 사용해야함.
관심사의 분리
높은 응집도, 낮은 결합도
객체 설계
객체로 추상화하기
비공개 필드(데이터), 비공개 로직(코드)
공개 메서드 선언부를 통해 외부 세계와 소통
각 메서드의 기능은 객체의 책임을 드러내는 창구
객체의 책임이 나뉨에 따라 객체 간 협력이 발생
객체가 제공하는 것들
절차 지향에서 잘 보이지 않았던 개념을 가시화
관심사가 한 군데로 모이기 때문에, 유지보수성 🆙
여러 객체를 사용하는 입장에서는, 구체적인 구현에 신경 쓰지 않고 보다 높은 추상화 레벨에서 도메인 로직을 다룰 수 있다.
새로운 객체를 만들 때 주의할 점
메서드를 추상화할 때와 비슷하다
객체를 만듦으로써 외부 세계와 어떤 소통을 하려고 하는지 생각해보자.
생성자, 정적 팩토리 메서드에서 유효성 검증이 가능하다.
도메인에 특화된 검증 로직이 들어갈 수 있다.
setter 사용 자제
데이터는 불변이 최고다. 변하는 데이터더라도 객체가 핸들링할 수 있어야 한다.
객체 내부에서 외부 세계의 개입 없이 자체적인 변경/가공으로 처리할 수 있는지를 확인
만약 외부에서 가지고 있는 데이터로 데이터 변경 요청을 해야 하는 경우,
'set~' 이라는 단순한 이름 보다는
'update~' 같이 의도를 드러내는 네이밍을 고려하자.
Getter 도 처음에는 사용자제. 반드시 필요한 경우에 추가하기
외부에서 객체 내 데이터가 필요하다고 getter를 남발하는 것응ㄴ 무례한 행동이다.
객체에 메시지를 보내라.
필드의 수는 적을수록 좋다.
불필요한 데이터가 많을수록 복잡도가 높아지고 대응할 변화가 많아진다.
필드A를 가지고 계산할 수 있는 A필드가 있다면, 메서드 기능으로 제공
단, 미리 가공하는 것이 성능 상 이점이 있다면, 필드로 가지고 있는 것이 좋을 수도 있다.
SOLID 원칙
SRP(Single Responsibility Principle) : 단일 책임 원칙
하나의 클래스는 단 한 가지의 변경 이유만을 가져야 한다. -> '변경 이유' = 책임
객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경되는가?
관심사의 분리
높은 응집도, 낮은 결합도
OCP(Open-Closed Principle) : 개방-폐쇄 원칙
소프트웨어 개체는 확장에 대해서는 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 한다.
상속, 인터페이스, 추상 클래스 등으로 구현
추상화와 다형성을 활용해서 OCP 를 지킬 수 있다.
LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
자식 클래스는 부모 클래스의 기능을 모두 포함해야 한다.
자식 클래스는 부모 클래스의 기능을 확장할 수 있다.
자식 클래스는 부모 클래스의 기능을 재정의할 수 있다.
자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야한다.
LSP를 위반하면, 상속 클래스를 사용할때 오동작, 예상 밖의 예외가 발생하거나, 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.
ISP(Interface Segregation principle) : 인터페이스 분리 원칙
인터페이스는 그 인터페이스를 사용하는 클라이언트에 맞게 분리되어야 한다.
클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안된다. -> 인터페이스를 잘게 쪼개라!
ISP를 위반하면, 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.
DIP(Dependency Inversion Principle) : 의존성 역전 원칙
고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.
추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. -> 둘 다 추상화에 의존해야 한다.
조합과 인터페이스를 활용하는 것이 유연한 구조 -> 상속을 통한 코드의 중복 제거가 주는 이점보다, 중복이 생기더라도 유연한 구조 설계가 주는 이점이 더 크다.(하드웨어 성능의 발전)
Value Object
도메인의 어떤 개념을 추상화하여 표현한 값 객체
값으로 취급하기 위해서, 불변성, 동등성, 유효성 검증 등을 보장해야 한다.
-> 불변성 : final 필드, setter 사용 금지
-> 동등성 : 서로 다른 인스턴스여도(동일성이 달라도), 내부의 값이 같으면 같은 같은 값 객체로 취급한다. -> equals(), hashCode() 오버라이딩 필요
-> 유효성 검증 : 객체가 생성되는 시점에 값에 대한 유효성을 보장하기
VO vs Entity
Entity 는 식별자가 존재한다. 식별자가 아닌 필드의 값이 달라도, 식별자가 같으면 동등한 객체로 취급한다.
-> equals() & hashCode() 도 식별자 필드만 가지고 재정의할 수 있다.
-> 식별자가 같은데 식별자가 아닌 필드의 값이 서로 다른 두 인스턴스가 있다면, 같은 Entity가 시간이 지남에 따라 변화한 것으로 이해할 수도 있다.
VO 는 식별자 없이, 내부의 모든 값이 다 같아야 동등한 객체로 취급한다.
-> 개념적으로, 전체 필드가 다같이 식별자 역할을 한다고 생각해도 된다.
-> VO는 값으로 취급한다. VO는 불변성을 보장해야 한다.
-> equals() & hashCode() 도 모든 필드로 재정의해야 한다.
일급시민
변수를 담을 수 있는 그릇으로 사용되는 객체
다른 요소에게 사용 가능한 모든 연산을 지원하는 요소
변수로 할당될 수 있다.
파라미터로 전달될 수 있다.
함수의 결과로 반환될 수 있다.
ex) 일급함수 : 함수형 프로그래밍 언어에서, 함수는 일급 시민이다.
함수는 변수에 할당될 수 있고, 인자로 전달될 수 있고,
함수의 결과로 함수가 반환될 수도 있다.
일급 컬렉션
컬렉션을 포장하면서, 컬렉션만을 "유일하게" 필드로 가지는 객체
컬렉션을 다른 객체와 동등한 레벨로 다루기 위함
단 하나의 컬렉션 필드만을 가진다.
컬렉션을 추상화하며 의미를 담을 수 있고, 가공 로직의 보금자리가 생긴다.
가공 로직에 대한 테스트도 작성할 수 있다.
만약 getter로 컬렉션을 반환할 일이 생긴다면,
외부 조작을 피하기 위해, 꼭 새로운 컬렉션으로 만들어서 봔환해주자.
Enum
상수의 집합을 표현하는 클래스
Enum은 상수의 집합이며, 상수와 관련된 로직을 담을 수 있는 공간이다.
-> 상태와 행위를 한 곳에서 관리할 수 있는 추상화된 객체
특정 도메인 개념에 대해, 그 종류와 기능을 명시적으로 표현해줄 수 있다.
만약 변경이 정말 잦은 개념은, Enum 보다 DB로 관리하는 것이 나을 수 있다.
다형성 활용
상속을 활용하여, 부모 클래스에 공통된 메서드를 두고, 자식 클래스에서 각자의 로직을 구현하는 방법
반복적인 if문을 단순하게 만들어볼 수 없을까?
변하는 것과 변하지 않는 것을 분리하여 추상화하고, OCP를 지키는 구조
숨겨져 있는 도메인 개념 도출하기
도메인 지식은 만드는 것이 아니라 발견하는 것
객체 지향은 현실을 100% 반영하는 도구가 아니라, 훙내내는 것이다
-> 현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용해야 할 때가 있다.
설계할 때는 근시적, 거시적 관점에서 최대한 미래를 예측하고,
시간이 지나 만약 틀렸다는 것을 인지하면 언제든 돌아올 수 있도록 코드를 만들어야 한다.
-> 완벽한 설계는 없다. 그 당시의 최선이 있을뿐.