- 우리가 클린코드를 추구하는 이유 코드가 잘 읽힌다 = 이해가 잘된다 = 유지보수 하기가 수월하다 = 우리의 시간과 자원이 절약된다 클린코드라 지칭하는 수많은 원칙들, 조언들 => 당연히 지키면 좋다, 왜? 클린코드를 관통하는 아주 중요한 주제 => 추상
- 프로그램의 정의
- 프로그램 = 데이터 + 코드
- 데이터 자체가 추상화 레벨을 가지고 있다.
- 데이터가 식재료라면 코드는 요리방법이다.
- 추상과 구체
- 추상 : 뽑을 추, 코끼리 상 => 중요한 정보는 가려내어 남기고, 덜 중요한 정보는 생략하여 버린다.
- 구체 <-> 추상
- 추상화 레벨 : 구체에서 추상으로 가는 과정의 레벨
- 고수준 : 추상화 레벨이 높다 => 저수준 : 추상화 레벨이 낮다 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) : 의존성 역전 원칙
- 고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 세부 사항에 의존해서는 안된다. 세부 사항이 추상화에 의존해야 한다.
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. -> 둘 다 추상화에 의존해야 한다.
- 의존성의 순방향 : 고수준 모듈이 저수준 모듈을 참조하는 것
- 의존성의 역방향 : 고수준, 저수준 모듈이 모두 추상화에 의존하는 것
- DIP를 위반하면, 고수준 모듈과 저수준 모듈 간의 의존성이 강해져서 결합도가 높아지고, 변경이 어려워진다.
- Spring 3대 요소 DI/IoC, PSA, AOP
- DI(Dependency Injection) : 의존성 주입
- "3" 이라는 숫자의 관계를 기억
- 객체 간의 의존성을 외부에서 주입하는 것
- DI를 통해 객체 간의 결합도를 낮추고, 유연한 구조를 만들 수 있다.
- DI를 통해 객체 간의 의존성을 명시적으로 표현할 수 있다.
- DI를 통해 객체 간의 의존성을 변경할 수 있다.
- IoC(Inversion of Control) : 제어의 역전
- 프로그램의 흐름을 개발자가 아닌 Framework 이 주도하는 것,
- 객체의 생성과 생명주기를 프레임워크가 관리하는 것
- IoC 컨테이너가 객체를 생성하고, 의존성을 주입한다.
- IoC를 통해 객체 간의 결합도를 낮추고, 유연한 구조를 만들 수 있다.
- PSA(Program Structure Abstraction) : 프로그램 구조 추상화
- 프로그램의 구조를 추상화하는 것
- PSA를 통해 프로그램의 구조를 명확하게 표현할 수 있다.
- PSA를 통해 프로그램의 구조를 변경할 수 있다.
- AOP(Aspect Oriented Programming) : 관점 지향 프로그래밍
- 공통 관심사를 분리하는 것
- AOP를 통해 공통 관심사를 분리할 수 있다.
- AOP를 통해 공통 관심사를 재사용할 수 있다.
- AOP를 통해 공통 관심사를 변경할 수 있다.
- 키워드 정리
- 객체, 협력과 책임, 관심사의 분리, 높은 응집도와 낮은 결합도
- getter / setter 자제하기, 객체에 메시지 보내기
- SOLID: SRP , OCP, LSP, ISP, DIP
- 상속과 조합
- 상속보다 조합을 사용하자
- 상속은 시멘트처럼 굳어지는 구조다. 수정이 어렵다. -> 부모와 자식의 결합도가 높다.
- 조합과 인터페이스를 활용하는 것이 유연한 구조 -> 상속을 통한 코드의 중복 제거가 주는 이점보다, 중복이 생기더라도 유연한 구조 설계가 주는 이점이 더 크다.(하드웨어 성능의 발전)
- 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% 반영하는 도구가 아니라, 훙내내는 것이다 -> 현실 세계에서 쉽게 인지하지 못하는 개념도 도출해서 사용해야 할 때가 있다.
- 설계할 때는 근시적, 거시적 관점에서 최대한 미래를 예측하고, 시간이 지나 만약 틀렸다는 것을 인지하면 언제든 돌아올 수 있도록 코드를 만들어야 한다. -> 완벽한 설계는 없다. 그 당시의 최선이 있을뿐.
- 패키지 나누기
- 패키지는, 문맥으로써의 정보를 제공할 수 있다.
- Beginner, Middle, Advanced가 BeginnerGameLevel, MiddleGameLevel, AdvancedGameLevel이
- 아니어도 괜찮은 이유 중하나는, gamelevel 패키지 안에 있기 때문이다.
- 패키지를 쪼개지 않으면 관리가 어려워진다.
- 패키지를 너무 잘게 쪼개도 마찬가지로 관리가 어려워진다.
- 대규모 패키지 변경은 팀원가의 합의를 이룬 시점에 하자.
- 패키지는, 문맥으로써의 정보를 제공할 수 있다.