[인프런 워밍업 스터디 클럽 2기 백엔드] 1주차

해당 글은 인프런 박우빈 강사님의 「Readable Code: 읽기 좋은 코드를 작성하는 사고법」을 바탕으로 작성하였습니다.

그 외 글에 등장하는 모든 이미지는 직접 그린 이미지입니다.

 

동기와 스터디 목표

인프런 워밍업 클럽 0기 우수 러너로 운 좋게 선정된 후, 반년 이상이 훌쩍 지났다. 다시금 기강이 해이해지고 할 일을 미뤄간다고 (이 반년 간 대시보드가 0기를 진행할 때보다 많이 휑해진 걸 보면 마음이 쓰리다...) 생각할 무렵 2기 소식이 들려왔다. 내가 신청한 코스는 박우빈 님의 코스! 사실 우빈 님의 명성도 잘 모른 채 '테스트 코드'라는 달콤한 단어와 커리큘럼만 보고 바로 신청서를 제출해버렸다. 추후 스터디를 같이 진행하는 러너 분께서 우빈 님의 명성을 알려주셔서 놀란 기억이 있다.

 

나는 작년, 학원에서 5개월 정도의 부트캠프로 코딩을 처음 배웠기 때문에, 기초적인 문법만 알지 자바 컨벤션이나 리팩토링 같은 건 수박 겉 핥기 정도로 밖에 알지 못했다. 이번 기회로 우빈 님의 사고법과 노하우를 10퍼센트라도 챙겨가는 것이 이번 스터디의 소박하지만 원대한 목표라고 할 수 있겠다.

 


1주차 요약

해당 블로그 글에는 강의에 등장하는 예시 코드는 작성하지 않고, 이론적인 부분만 요약해 작성합니다. 예시 코드는 우빈 님의 git repository에서 다운 받을 수 있습니다.

git repository 받기

이 부분은, 내용이 있는 것은 아니지만 개인적으로 당부하고 싶은 부분이 있다. 아직 해당 강의를 들어본 적이 없는 분이라면, 강의에서 제공해주는 파일을 받고 꼭! 로직을 천천히 읽으며 흐름을 정확히 이해하고 강의를 시작했으면 좋겠다. 이를 제대로 파악해보지 않고 처음부터 들이받았다가 바로 다음 섹션부터 후회해버렸다... 😂😂

 

프로그램이란?

프로그램 = 설치하는 것 = 데이터와 코드의 집합체

위 개념을 유념하며 추상화에 대해 깊이 알아가자.

 

추상과 구체

추상 ⇔사물을 정확히 이해하기 위해 사물이 지닌 여러 측면 가운데 특정 면 만을 가려내어 포착

위 추상의 정의에서 가장 중요한 단어는 '가려낸다.'라고 우빈 님께서는 짚어주셨다. 가려낸다는 것은 일부를 생략하고 버리는 것이며, 이는 정보를 함축하고 제거한다는 것과 같다.

 

추상화 레벨

추상화 레벨이란 추상과 구체 사이 존재하는 레벨들이다.

추상화 레벨이라는 단어만 보면 굉장히 어렵게 느껴지지만, 간단하게 생각해보자. 바로 윗 문단에서 우리는 추상을 사물의 특정 정보를 함축, 제거한다고 이해했다. 그렇다면 이 정보들을 함축, 제거하지 않은 것을 뭐라고 할까? 바로 '구체'이다. 구체의 사전적 정의는 '사물이 직접 경험하거나 지각할 수 있도록 일정한 형태와 성질을 갖춤.' 이다. 우리가 이해한 추상이란 개념에 맞춰 생각한다면 구체의 정의에서 중요한 부분은 '지각할 수 있는' 이겠지만 확 와 닿지는 않는다. 그래서 간단하게 도식화해보았다.

 

image

예시를 한 번 들어보자. 친구에게 '저녁 식사로 라면을 끓여 먹었어.'라고 한 문장으로 끝내버릴 수 있는 간단한 문장을 극단적으로 늘려보자. '물 550 ml를 냄비에 담고 가스레인지 위에 올려 불을 점화했어. 물이 끓으면 스프와 면을 넣어 3~4분을 기다리고, 완성 후 불을 소화했어. 면을 젓가락으로 집어 입에 넣고, 저작 운동을 통해 섭취하고 소화했어.' 이렇게 말한다면 대화의 흐름이 엉망일 것이다. 이 보다 더더욱 길게 말한다면 친구가 대체 무슨 말을 하고자 하는 것인지 파악조차 어려울 것이다.

여기서 우리가 일상적으로 대화하듯이 말하는 '저녁 식사로 라면을 끓여 먹었어.' 라는 한 문장이 추상, 뒤의 호흡이 긴 문장을 구체라고 생각할 수 있다. 예시를 들어보면 전혀 어려운 개념이 아니라는 것을 알 수 있다.

 

컴퓨터 과학과 추상

그렇다면 이제 한 가지 의문이 든다. '그래서 추상의 정의를 왜 이렇게 자세히 이해하려고 하는거야?' 답은 간단하다. 컴퓨터는 인간의 언어를 알지 못하고 1과 0으로만 소통할 수 있다는 것은 모두가 알 것이다. 하지만 우리는 영어로 코드를 작성하고 있다. 이 의미는 우리가 쓰고 있는 고수준 언어는 기계어를 사람이 알기 쉽도록 추상화한 것이다. 하드웨어와 운영체제 사이도, 운영체제와 애플리케이션 사이에서도 마찬가지이다. 컴퓨터 과학이란 겹겹이 쌓인 추상화의 모음인 것이다.

 

누구나 코딩을 처음 접할 때, 메서드, 클래스 개념도 자세히 알지 못해 main 메서드에 줄줄이 로직을 다 때려넣어 본 적이 있을 것이다. 이렇게 되면 눈이 굉장히 피로해지고 로직 파악도 되지 않는다는 것도 알고 있다. 이런 것들을 적절히 추상화해 복잡한 데이터와 로직은 단순화하는 것이 이 강의의 핵심 목표인 것이다.

 

잘못된 추상화

복잡한 로직들 중 일부를 똑 떼서 메서드로 추출하고, 클래스로 다 만들고 이름도 내가 읽기 좋게 바꿔버리자! 하고 무턱대고 리팩토링한다면 이게 좋은 리팩토링 방식일까?

⇒ 그렇지 않다! 잘못된 추상화는 구체화 과정에서 유추가 안 된다!

 

친구가 이런 말을 했다고 해보자.

image친구들끼리 있을 때는 모두가 아~ 하고 단어의 의미를 유추하고 대화를 이어나갈 수 있을 것이다. 하지만 저 말을 가족들끼리 있을 때 한다면? 혹은 직장 상사와 있을 때 한다면? 사람마다 성향 차이가 있겠지만 아마 대부분은 대화의 흐름이 끊길 것이다. 듣는 이를 고려하지 못한 어휘 선택이라고 할 수 있다.

 

즉, 우리는 단어의 선택 조차도 대화하는 집단이 어디냐에 따라 적절히 선택하고는 한다. 이를 코드에 적용하자면 우리가 담당하는 도메인 영역에 따라 추상화 기준이 다를 수도 있다. 가장 중요한 것은 문맥을 파악해야 한다는 것이다.

 

이름 짓기

이름 짓기는 추상화의 중요한 첫걸음이다.

  • 끝에 -(e)s를 붙여 데이터가 단수인지 복수인지 구분하기

  • 이름 줄이지 않기

    • 가독성 vs 효율성 에서 가독성을 택하는 게 좋다.

    • 단, 관용어 처럼 이용하는 것이 있으니 내가 속한 집단에 잘 맞춰가야 한다.

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

    • 현재 팀원들이 다같이 아는 단어여도, 신입이 왔을 때를 고려해야 한다.

  • 도메인 용어 사용하기

    • 상점을 store라고 할지, shop이라 할지 우선적으로 논의해야 한다.

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

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

    • poop, candidate, threshold 등...

 

메서드와 추상화

  • 한 메서드에는 하나의 기능만!

  • 메서드의 이름으로 구체적 내용을 추상화

    • 추상화된 구체를 유추 가능한 적절한 의미가 담기는 이름

    • 파라미터랑 연결지어 더 풍부한 의미 전달 가능(ex: ~~From (파라미터))

    • 항상 동사로 시작해야 한다는 강박은 불필요!

       

  • ★메서드 선언부★

    • 반환 타입 메서드명 (파라미터) {구현부} 에서 메서드 명과 파라미터를 합쳐 메서드 시그니쳐라고 한다.

  • 하나의 세계(=메서드) 안에서는 추상화 레벨이 동등해야 한다.

 

파라미터

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

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

  • 요리 레시피의 재료 같은 의미

 

반환 타입

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

  • void 대신 충분히 반환할 값이 있는지 고민하기

    • 반환 값이 있어야 테스트가 용이하다.

 

매직 넘버, 매직 스트링

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

  • 매직 넘버, 매직 스트링을 상수로 추출해 이름을 짓고 의미 부여

    • 가독성과 유지 보수성↑

 

Early return

  • 일찍 리턴할 수 있는 것은 빠르게 리턴!

  • if 문의 else를 지양한다.

    • if 문단을 메서드로 추출 후, 조건을 충족 시 바로 return 시키는 방식

 

사고의 depth 줄이기

  • 중첩 분기문, 중첩 반복문 줄이기

    • 각각의 반복문을 메서드로 쪼갠다!

    • 무조건 depth를 1로 만들자는 의미 X!

    • 2중 중첩이 더 도움이 된다고 판단 시 메서드 분리는 금물.

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

i = 10; 

...
무언가의 복잡한 로직
...

j = i + 10; // 이렇게 되면 i의 값이 기억이 안 난다!
  • 메서드 리팩토링 시 컴파일 에러 주의!

    • 리팩토링할 메서드를 복제해 리팩토링 완료 후 원 메서드를 지우는 방식으로 진행 권장

 

공백 라인 대하기

  • 공백도 의미를 갖는다! 의미가 달라진다고 생각하는 부분들에 공백 라인을 준다!

 

부정어를 대하는 자세

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

  • 부정의 의미를 다른 단어가 있는지 고민하기

    • ! 같은 부정 연산자는 가독성이 떨어지니 지양

해피 케이스와 예외 처리

  • 사람은 해피 케이스에 몰두하게 되기 때문에 예외 가능성을 최대한 낮춰야 한다.

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

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

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

    • 사용자에게 보여줄 예외 vs 개발자가 처리해야 하는 예외

 

null을 대하는 자세

  • 자바에서는 null 처리가 중요하다. NullPointException의 지옥...

    • NullPoint를 최대한 방지하도록 경각심 갖기

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

      • 이것이 불가능하면 Optional 사용을 고려

      • 단, Optional은 꼭 필요할 때만 사용할 것

      • Optional을 파라미터로 받지 않도록 하기. 분기 케이스가 3개나 되어 버린다.

      • Optional 반환 시 빠르게 해소한다.

        • ★ orElseGet(), orElseThrow(), ifPresent(), ifPresentOfElse() 등을 사용하자★

 

객체와 추상화 레벨

  • 객체의 비공개 필드와 비공개 로직은 공개 메서드 선언부로 외부와 소통

     

    ⇒ 객체의 책임공개 메서드로 드러난다.

  • 책임에 따라 분리된 각 객체들이 상호작용 하며 협력한다.

  • 외부에서는 구체적 구현을 알 수 없고 몰라도 된다!

 

객체 생성 시 주의점

  • 1개의 관심사가 명확하게 정의되었는가? 외부와 어떤 소통을 해야하는가?

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

    • 도메인 특화 검증 로직이 들어갈 수 있음

  • setter 사용 자제!

    • 데이터는 불변이어야 사이드 이펙트가 없다!

    • 객체 내부에서 외부의 개입 없이 자체 변경 및 가공을 하도록 하자

    • 변경이 불가피하다면 update~ 같은 의미있는 네이밍을 하자.

  • getter 사용 자제!

    • getter도 꼭 필요할 때만 사용한다!

    • getter는 폭력적인 메서드라 외부에서 getter를 호출하는 것은 강도를 불러오는 것과 다름이 없다!

    • 객체에 메세지를 보내기!

  • 필드 수는 적을 수록 좋다

  • 불필요 데이터가 많을수록 복잡도가 높아진다.

  • 중복 데이터 필드는 최대한 줄이고, 파생 데이터 필드는 메서드로 풀어본다.

    • 단, 미리 가공하는 것이 성능에 이점이 있다면 필드로 쓰자.

 

SOLID

SRP(Single Responsibility) / 단일 책임 원칙 - 한 객체가 책임 1, 책임 2를 모두 갖고 있다면, SRP를 위반 - 하나의 클래스는 단 한 가지의 변경 이유(= 책임) 만을 가져야 한다. - 객체가 가진 공개 메서드, 필드, 상수 등은 해당 객체의 단일 책임에 의해서만 변경 되는가? - 관심사의 분리 - 높은 응집도(클래스나 모듈 내 요소들이 긴밀하게 작용), 낮은 결합도(=의존성 / 한 객체가 변경되었을 때 다른 객체가 변화되는가) - 클래스와 객체 레벨에서 강조되는 원칙 - 책임을 갖는다의 의미? 지금 객체의 책임은? ← 책임이란 판단이 어려움!

- 책임을 볼 줄 아는 눈이 필요 OCP(Open-Closed)

- 확장에는 열려 있고 수정에는 닫혀 있기

- 기존 코드 변경 없이, 시스템 기능 확장해야 한다.

- 추상화와 다형성을 활용

 

LSP(Liskov Substitution)

- 상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환 가능해야 한다.

- 자식 클래스는 부모 클래스의 책임을 준수하고 부모 클래스의 행동을 변경하면 안 된다!

- LSP 위반 시 상속 클래스 사용 시 오동작, 예상 밖 예외 발생

 

ISP(Interface Segregation)

- 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.

- 인터페이스 잘게 쪼개기!

- ISP 위반 시 불필요한 의존성으로 인해 결합도가 높아진다.

- 인터페이스에 A,B 기능이 존재하고, 구현체 1은 A,B를 모두 사용하고 구현체 2가 A만 사용한다면, 인터페이스를 A, B로 쪼개야 한다.

 

DIP(Dependency Inversion)

- 상위 수준의 모듈은 하위 수준의 모듈에 의존하면 안 된다.

- 둘 모두 추상화에 의존(=의존성의 역방향)해야 한다.

- 고수준 모듈이 저수준 모듈을 참조하는 것을 의존성의 순방향이라고 한다.

- 저수준 모듈 변경 시에도 고수준 모듈에 영향이 가면 안된다.

- 카페(고수준)에서 커피(저수준)를 판다(sell 함수)라는 행위를 한다고 가정

- 카페에서 다른 음료를 판매하고 싶다면?

- 음료라는 인터페이스를 둬야 한다!

 

상속과 조합

  • 상속보다는 조합!

    • 상속은 수정이 어렵다.

    • 부모 자식 간의 결합도가 높다...

  • 조합과 인터페이스를 잘 활용하는 것이 관건!

    • 상속으로 중복 코드 제거 하기 <<< 중복이 생겨도 유연한 구조

    • 중복 제거로 극한의 효율을 얻고자 하는 것은 옛 이야기이다.

 

value object

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

  • 불변, 동등, 유효성 검증 보장이 필수

  • 불변성: final 필드 사용하기, setter 금지하기

  • 동등성: 서로 다른 인스턴스여도 내부 값이 같으면 같은 객체

    • 지폐의 일련번호가 달라도 같은 값으로 보는 것과 같은 개념이다!

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

 

VO? Entity?

  • Entity식별자가 존재!

    • 식별자 외의 필드 값이 달라도 식별자가 같으면 동등한 객체이다.

  • VO는 식별자가 없고, 내부 필드의 모든 값이 다 같아야 동등한 객체이다.

 

일급 컬렉션

  • 일급 시민

    • 다른 요소에게 사용 가능한 모든 연산을 지원하는 요소

    • 변수 할당 가능

    • 파라미터 전달 가능

    • 함수 결과 반환 가능

  • 일급 컬렉션

    • 컬렉션을 포장하면서 컬렉션만을 유일하게 필드로 가지는 객체

      • 컬렉션을 다른 객체와 동등 레벨로 다룰 수 있다.

    • 의미 부여와 가공 로직의 보금자리가 생긴다!

    • getter로 컬렉션을 반환해야 한다면 외부 조작을 피하기 위해 새 컬렉션을 만들어 반환해야 한다.

      • ex) getter로 리스트를 가져와 add() 등의 함수로 조작 위험성 존재

 

Enum의 특성과 활용

  • 상수의 집합

  • 상수와 관련된 로직을 담는 공간!

     

    → 상태와 행위를 한 곳에서 관리 가능한 객체

  • 특정 도메인 개념에 대해 종류와 기능을 명시적 표현 가능

  • 단, 변경이 잦은 개념은 Enum 보단 DB 관리가 나을 수도 있다.

 

+ α

  1. orElse() vs orElseGet() vs orElseThrow()의 차이

    1. orElseThrow는 값이 있으면 쓰고 없으면 예외.

    2. orElse()는 괄호 안이 항상 실행되는 값이다. 확정된 값일 때만 사용한다. (호출할 필요가 없어도(null이어도...) 항상 실행)

       

    3. orElseGet()은 null인 경우 실행된다. 값을 제공하는 동작을 정의한다. (null인 경우에만 괄호 안 동작이 실행)

      - orElseGet()은 Supplier를 통해 람다식 형태로 매개변수를 전달한다. 이는 자바의 람다식 동작 방식으로 인해, 즉시 실행하지 않고 호출할 때만 실행한다. 이를 통해 orElse와 다르게 지연 평가가 가능

  2. 안티 패턴

    1. 강의에서 지나가듯 말씀하신 내용으로, e.printStackTrace()는 대표적 안티 패턴!

    2. 나머지 대표 안티 패턴은 God Object, God Class, 매직 넘버, 매직 스트링, 싱글톤 남용 등이 존재

  3. 자바의 람다식 동작 방식

    1. 추가 학습 필요

 


미션

미션은 총 2개가 있었고, 각 풀이는 노션 포스트 링크를 첨부한다.

미션 1

미션 2

댓글을 작성해보세요.

채널톡 아이콘