반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 4장을 기반으로 작성된 글입니다.

 

 

 


오류의 복구 가능성

  • 복구 가능한 오류 → 시스템 전체에 영향을 주지 않도록 적절히 처리
    • ex: 네트워크 오류 (몇 초 기다리고 재시고) / 통계 기록 오류 (무시)
  • 복구할 수 없는 오류 → 신속한 실패, 요란한 실패
  • 호출하는 쪽에서만 오류 복구 가능 여부를 알 때 → 오류를 복구하기 원할 것이라고 판단 (대부분의 경우)
    • 호출하는 쪽에서 오류를 인지하도록 해야 한다.

견고성 vs 실패

오류가 있더라도 계속 진행하는 견고성, 오류를 처리하게 하거나 프로그램 작동을 멈추는 실패 사이의 트레이드 오프 → 대부분의 경우 실패가 적절

신속하게 실패해야 하고, 요란하게 실패해야 한다.

오류 전달 방법

  • 명시적 방법 : 코드 계약 상 명확한 부분에 나타나 있어, 오류를 반드시 인지할 수 있음
    • ex : Checked exception, null 반환(null 안정성의 경우), Optional, Result, Outcome 반환(반환값 확인 필수인 경우)
  • 암시적 방법 : 오류를 인지하지 못 할 수 있음
    • ex : Unchecked exception, 매직값, Promise(Future), Assertion, Check, Panic

Result형의 경우, 올바른 사용 방법을 팀 내에서 공유하고 있다고 가정해야, 명시적 방법이 된다.

자바의 @CheckReturnValue 애너테이션을 사용하면 결과값(Outcome) 확인을 반드시 해야해서, 명시적 방법이 된다.

복구할 수 없는 오류의 전달

Unchecked exception을 발생하거나, panic이 되도록 하거나, check 또는 assertion을 사용한다.

Unchecked exception을 사용하는 이유는, 어차피 복구가 불가능한 상황에서 호출하는 모든 계층에 extends를 다는 것이 번거롭기 때문이다.

그저 오류를 호출한 쪽에 전달하면 되고, 스택 트레이스를 통해 위치를 파악할 수 있다.

호출하는 쪽에서 복구하기를 원할 수도 있는 오류의 전달

이 경우, Unchecked exception과 Checked exception(또는 명시적 기법) 사용을 두고 의견이 분분하다.

  • Unchecked exception을 사용해야 한다는 주장
    • 코드의 구조가 개선된다 (일관된 계층에서 오류 처리)
    • 실용적임
  • 명시적 기법을 사용해야 한다는 주장
    • 오류를 반드시 처리할 수 있고, 매끄럽게 처리할 수 있다.
      • 오류가 발생하는 지점에서 처리 or 던지기가 강제되므로, 발생 지점에서 오류를 인식하는 것만으로 매끄럽게 처리할 가능성이 높아진다.
    • 처리를 적절하게 하지 않아도, 코드 상에서 티가 잘 난다.
    • Unchecked exception이 적절하게 문서화되는 것은 어렵다.

필자는 명시적 방법을 추천한다.

하지만 많은 언어들이 Checked exception을 지원하지 않는 것에는, 실용적인 것을 포함하여 많은 이유가 있을 것이다.

아래 구절이 인상 깊다.

 

 

There is still one question left. What do water wings and checked exceptions have in common?

At the beginning you feel safer with them, but later they prevent you from swimming quickly.

Water wings가 안전하게 헤엄치는 것에 도움을 줄 수 있어도, 숙련되고 나면 빠르게 헤엄치는 것을 방해할 것이다.

컴파일러의 경고를 무시하지 마라

컴파일러의 경고를 잘 수정하는 것만으로도, 코드의 잠재적 문제를 해결할 수 있다.

반응형
반응형

 

* 본 아티클은, '좋은 코드 나쁜 코드' 3장을 기반으로 작성된 글입니다.

 

 

 


코드 품질의 핵심 요소 6가지

  • 가독성
  • 모듈화
  • 재사용성 및 일반화성
  • 테스트 용이성
  • 예측 가능한 코드를 작성하라
  • 코드를 오용하기 어렵게 만들라
    본 장은 이 중 ‘예측 가능한 코드를 작성하라’와 ‘코드를 오용하기 어렵게 만들라’는 2개와 깊게 관련이 있다.

코드를 작성할 때 다음의 3가지를 반드시 기억해야 한다.

  • 자신에게 분명하다고 해서 다른 사람에게도 분명한 것은 아니다.
  • 다른 개발자는 무의식 중에 당신의 코드를 망가뜨릴 수 있다.
  • 시간이 지나면 자신의 코드를 기억하지 못한다.

따라서, 코드를 작성할 당시에 다른 사람(미래의 자신 포함)에게도 분명하며, 망가뜨리기 어려운 코드를 작성해야 한다.

내가 작성한 코드를 다른 사람들이 파악하는 방법

  • 이름 확인
  • 데이터 타입 확인
  • 문서 읽기

여기까지가 실용적인 방법이고,

  • 직접 묻기
  • 자세한 구현 코드 읽기

위 두 가지는 비현실적이고 비효율적인 방법이다.

코드 계약

서 다른 코드 간의 상호작용을 계약처럼 생각한다.

계약에서 정의한 대로 분명하게 실행되고, 예상과 다르게 실행되는 것이 없어야 한다.

  • 선결 조건 : 코드를 호출하기 전에 사실이어야 하는 것
    • ex: 실행 전 시스템의 상태, 코드에 공급해야 하는 입력
  • 사후 조건 : 코드가 호출된 후에 사실이어야 하는 것
    • ex: 시스템이 새로운 상태에 놓였는지, 반환값
  • 불변 사항 : 코드 호출 전후가 동일해야 하는 사항

명백한 사항 / 세부 조항

전기 스쿠터 렌탈 앱을 예로 들자.

  • 핵심 사항
    • 전기 스쿠터 대려
    • 시간 당 10달러
  • 세부 조항
    • 충돌 사고 시 수리 비용 지불
    • 시속 30마일 이상 → 벌금 부과

이중, 시속 30마일 이상에 벌금을 부과하는 항목은 당황스러운 조항일 수 있다.

코드에서는,

  • 핵심 사항
    • 함수와 클래스 이름
    • 인자 타입
    • 반환 타입
    • Checked exception
  • 세부 조항
    • 주석, 문서
    • Unchecked exception

핵심 사항으로 조건을 명백하게 하는 것이 좋다.

문서, 주석은 업데이트가 어려워 믿기 힘들다.

Unchecked exception은 처리가 강제되지 않아서 놓칠 수 있다.

세부 조항에 대한 의존성을 최대한 줄이고,

핵심 사항만으로 명확하게 코드를 설명할 수 있도록 하자.

코드 예시

class UserSettings {
    UserSettings() { ... }

    // 이 함수를 사용해 설정이 로드되기 전에 다른 함수 호출 X
    Boolean loadSettings(File location) { ... }

    // 이 함수는 loadSettings() 함수 호출 이후, 다른 함수 호출 이전에 호출해야 함
    void init() { ... }

    // 사용자가 선택한 UI의 색상 반환
    // 선택된 색상이 없으면 null 반환
    // 설정이 로드 또는 초기화되지 않았으면 null 반환
    Color? getUiColor() { ... }
}

클래스, 메서드명을 통해 역할을 충분히 파악할 수 있지만,

loadSettings() → init() → getUiColor() 순서로 실행되지 않으면 문제가 발생한다.

void setUiColor(UserSettings userSettings) {
    Color? chosenColor = userSettings.getUiColor();
    if (chosenColor == null) {
        ui.setColor(DEFAULT_UI_COLOR);
        return;
    }
    ui.setColor(chosenColor);
}

위의 setUiColor에서 chosenColor의 null 가능성을 체크하지만, 이것이 상태의 오류인지, 사용자가 선택한 색상이 없음을 나타내는 것인지 알 수 없다.

상태의 오류가 발생해도 → 프로그램은 계속 동작한다.

세부 조항을 제거하는 방법

  • 정적 팩토리 메서드 → 설정 로딩, 초기화가 완료된 인스턴스만 반환하도록 함
    • getUiColor()가 null을 반환하면 → 색상을 선택하지 않았다는 뜻임
  • 생성자를 private으로 설정
  • 설정 관련 메서드 private 설정
class UserSettings {
    private UserSettings() { ... }

    static UserSettings? create(File location) {
        UserSettings settings = new UserSettings();
        if (!settings.loaadSettings(locations)) {
            return null;
        }
        settings.init();
        return settings;
    }

    private Boolean loadSettings(File location) { ... }

    private void init() { ... }

    Color? getUiColor() { ... }
}    

상태, 가변성이 클래스 외부로 노출되는 것을 없앴다. → 버그 침투 가능성을 낮췄다.

체크, 어서션 (Check, Assertion)

  • 체크 (Check)
    • 전제 조건 검사 : 입력 인수가 올바르거나, 초기화가 수행되었거나, 일부 코드를 실행하기 전에 시스템의 유효성을 확인
    • 사후 상태 검사 : 반환값 또는 코드 실행 후 유효성 확인
  • 어서션 (Assertion)
    • 체크와 매우 비슷하지만, 배포를 위해 빌드할 때 보통 컴파일에서 제외된다.
      • 때문에, 실제 서비스 환경에서의 실패를 명백하게 보여주지는 않는다
Color? getUiColor() {
    assert(hasBeenInitialized(), "UserSettings가 초기화되지 않음");
    ...
}

위의 코드는 어서션의 예시이다. (테스트 코드에서 많이 보았을 것이다.)

두 가지 모두 개발 또는 테스트 단계에서 코드의 오용을 파악하는 것을 목적으로 하지만, 효과가 보장되지는 않는다.

코드 계약에 세부 조항이 있을 때 체크나 어서션을 사용하면 좋다.

하지만 가능하다면 애초에 세부조항을 피하는 것이 좋다.

반응형
반응형

* 본 아티클은, '좋은 코드 나쁜 코드' 2장을 기반으로 작성된 글입니다.

 

 

 


추상화 계층을 만들면 복잡한 문제를 간단하게 해결할 수 있다.

다음은 HTTP 통신을 통해 서버에 메시지를 보내는 코드의 예시이다.

HttpConnection connection = HttpConnection.connect("http://example.com/server");
connection.send("Hello server");
connection.close();

높은 단계에서 바라보면 이것은 꽤 간단해 보이는데,

사실 이 과정에서는 문자열 직렬화, HTTP 프로토콜의 복잡한 동작, TCP 연결, 네트워크 연결 확인, 신호 변조, 데이터 전송 오류 수정 등의 복잡한 과정들이 일어난다.

하지만 HttpConnection 라이브러리(모듈)를 사용하는 입장에서는 추상화된 개념만 이해하면 될 뿐이다.

이를 통해 코드 품질의 4가지 핵심 요소를 달성할 수 있다.

  • 가독성 : 추상화된 개념만 이해하면 된다.
  • 모듈화 : 추상화된 모듈을 다르게 구현된 모듈로 쉽게 변경할 수 있다.
  • 재사용성 및 일반화성 : 간결하게 추상화된 계층은 재사용하기 쉽고, 다른 상황에서 일반화되기 쉽다.
  • 테스트 용이성 : 간결하게 나눠진 부분에 대해서만 완벽하게 테스트하면 된다.

추상화 계층은 다음의 단위로 코드를 분할하고 의존하게 함으로써 나눌 수 있다.

  • 함수(메서드)
  • 클래스(및 구조체나 믹스인과 같이 클래스와 비슷한 요소도 가능)
  • 인터페이스
  • 패키지, 네임스페이스, 모듈

API, 함수

API는 인자, 반환 타입, 메서드명에 대해서만 공개하고, 세부 구현은 숨긴다.

API로 노출함으로써 추상화 계층을 유지할 수 있다.

함수가 너무 많은 일을 한다면, 작은 함수로 나눌 수 있다.

클래스

클래스의 추상화 기준에 대한 다양한 이론과 경험 법칙이 있다.

  • 줄 수 : 코드 300줄 이하 같은 기준은 대체로 맞지만, 상황에 따라 다를 수 있어 정확하지 않다.
  • 응집력
    • 순차적 응집력 : 한 요소의 출력이 다른 요소에 대한 입력으로 필요하다면 순차적인 응집력이 있다고 볼 수 있다.
      • ex: 커피 만들기(원두를 갈아서 → 추출)
    • 기능적 응집력 : 하나의 일을 성취하는 데 기여하느닞
  • 관심사의 분리 : 시스템이 각각 별개의 문제를 다루는 개별 구성 요소로 분리되어야 한다
    • 게임 콘솔은 TV와 독립적이다.

응집력을 고려하고 관심사의 분리가 적절하게 이루어졌다면,

코드 가독성이 좋아지고,

코드가 모듈화 되고 상호작용을 몇 가지 퍼블릭 함수를 통해서만 이루어져 구현 클래스 교체가 쉬워지고,

재사용과 일반화과 쉬워지고,

테스트가 쉬워진다.

텍스트 요약 클래스 예시

TextSummarizer 라는 클래스는,

  • 텍스트를 단락으로 분할
  • 텍스트 문자열의 중요도 점수 계산
  • 중요도에 따라 텍스트 요약

위와 같은 책임을 가진다.

이는 코드 품질의 4가지 핵심 요소에 반한다.

단락을 분할하는 ParagraphFinder와, 문자열의 중요도를 계산하는 TextImportanceScorer를 분리하고,

TextSummarizer에 의존성을 주입시킨다.

class TextSummarizer {
    private final ParagraphFinder paragraphFinder;
    private final TextImportanceScorer importanceScorer;

...
}

이를 통해 가독성이 좋고, 모듈화 되고, 재사용 일반화가 쉽고, 테스트가 쉬운 코드가 될 수 있다.

인터페이스

위의 예시에서 TextImportanceScorer의 로직을 바꾸어, 기계학습 모델링을 적용하고 싶다고 하자.

이럴 때는, TextImportanceScorer 클래스를 인터페이스로 추출한 다음,

이 문제(문자열 중요도 점수 계산)를 해결하는 각각의 방식을 서로 다른 클래스로 구현하며 된다.

WordBasedScorer / ModelBasedScorer

코드를 작성하다 보면, 주어진 추상화 계층에 한 가지 구현만 있고, 향후에 다른 구현을 추가할 계획이 없는 경우가 있다.

그럼에도 인터페이스를 통해 추상화 계층을 표현하면 몇몇 이점이 있다. (이 경우 상위 계층은 인터페이스에 의존할 뿐, 구현 클래스에 직접 의존하지 않는다.)

  • 퍼블릭 API를 명확하게 알 수 있다 (인터페이스를 보고)
  • 나중에 구현이 추가될 가능성이 있다
  • 테스트가 쉽다 (목, 페이크로 대체 가능)
  • 같은 클래스로 두 가지 하위 문제를 해결할 수 있다.

물론 작업이 더 필요하고, 코드가 복잡해질 수 있다는 단점도 있다.

층이 너무 얇아질 때

무리하게 계층을 나누면, 계층을 나누기 위해서만 계층을 나누는 무의미한 일이 될 수 있다.

하지만, 나눌지 말지 헷갈리는 경우에는 대부분 나누는 것이 더 좋다.

안 나눴을 때의 단점이 더 크기 때문.

마이크로서비스에 대해서는 어떤가?

개별 문제에 대한 해결책을, 독립적으로 실행되는 서비스로 배포하는 구조를 MSA(MicroService Architecture)라고 한다.

예를 들어, 온라인 소매업체에서 재고를 확인하고 수정하는 마이크로서비스를 개발할 수 있겠다.

이 경우, MSA 자체가 시스템을 분리하고 모듈화하지만,

서비스 구현을 위해 해결하는 하위 문제들을 추상화할 필요성은 변하지 않는다!

반응형
반응형

 

* 본 아티클은, '좋은 코드 나쁜 코드' 1장을 기반으로 작성된 글입니다.

 

 

 


코드 품질의 목표

  • 작동해야 한다.
  • 작동이 멈추면 안 된다.
  • 변경된 요구 사항에 적응할 수 있어야 한다.
    • 수정, 기능 추가, 소비자 선호나 비즈니스 요구 변경 등
  • 이미 존재하는 기능을 다시 구현해서는 안 된다.

코드 품질의 핵심 요소

  1. 코드는 읽기 쉬워야 한다.
    • 코드의 가독성이 떨어지면, 다른 개발자가 그 코드를 이해하는 데 많은 시간을 들여야 한다.
    • 코드의 기능이나 세부 사항을 놓칠 가능성도 크다. → 버그 확률 up
  2. 코드는 예측 가능해야 한다.
    • 사람의 정신 모델에 반하도록 구현하면 안 된다.
    • 중요한 데이터가 손상되는 재앙 등이 발생할 수도 있다.
  3. 코드를 오용하기 어렵게 만들라.
    • TV 뒷면의 전원 소켓과 HDMI 소켓처럼
  4. 코드를 모듈화하라.
    • 잘 정의된 인터페이스를 사용하여 두 개의 인접한 모듈 사이 상호작용을 만든다.
  5. 코드를 재사용 가능하고 일반화할 수 있게 작성하라.
    • 재사용성 : 드릴로 벽과 바닥에 모두 구멍을 뚫을 수 있음
    • 일반화성 : 드릴의 회전으로 나사도 박을 수 있음
  6. 테스트가 용이한 코드를 만들고, 제대로 테스트하라.
    • 테스트는 필수적이다.
    • 테스트 용이성 : 테스트 대상이 되는 ‘실제’코드가 얼마나 테스트하기 적합한지 나타내는 개념으로, 코드를 작성하면서 테스트 용이성을 반드시 고려해야 한다.

고품질 코드를 작성하는 것은 오래 걸리지만, 중장기적으로는 개발 시간이 단축된다.

 

 

 

 

반응형
반응형

1장 - 효율적으로 언어 배우기

프로그래밍 언어에 의존하지 않는 지식이 필요하다

본 책에서는 서로 다른 프로그래밍 언어 간의 비교, 프로그래밍 언어 탄생과 발전의 역사를 통해 학습한다.

 

 

2~4장

프로그래밍 언어와 규칙은 필요에 의해 탄생했다.

반복적인 작업을 줄이거나, 가독성을 개선하는 등의 이유로.

언어마다 탄생하게 된 배경, 해결하고자 한 문제가 다르기에, 가지고 있는 특징과 강점 모두 다르다.

언어를 사용하는 사용자는 그러한 배경과 특징을 알고서 자신에게 유리한 언어를 고민하고 사용해야 한다.

 

연산 순서는 기본적으로 모두 구문 트리(이진 트리 형태)를 따른다. 그것을 표기하는 방식만 다를 뿐이다.

 

if, else, for, while 등은 모두 C의 goto((어셈블리어에서는 jump))로 처리 가능하다.

하지만 가독성, 반복을 줄이기 위해서 등의 이유로 탄생하고 사용되는 것이다.

 

 

5장 함수

함수는 반복되는 큰 조직을 작은 부서로 나누는 것과 비슷하다.

또한 반복되는 작업을 줄이기 위함도 있는데, 반복되는 코드를 재실행하는 비용은 그리 크지 않다.

 

함수를 호출하려면 다시 돌아갈 위치를 기록할 메모리가 필요하다.

호출한 함수가 또 다른 함수를 호출한다면, 메모리에 하나의 주소만 저장해서는 프로그램을 제대로 실행시킬 수 없다.

그래서 호출한 함수들을 실행하고 돌아갈 주소 값을 스택에 저장한다.

이를 응용하여 재귀 호출도 구현할 수 있다.

 

깊이가 n으로 깊어지는 반복문은, n이 정해져 있지 않다면 구현할 수 없다.

하지만 재귀 호출을 통해 해결할 수 있다.

HTML처럼 수많은 내포 구조의 데이터를 다루기엔 재귀 호출이 적절하다

 

 

6장 예외 처리

코드 실행이 실패하는 경우가 있다.

이 실패를 처리하는 방식은 두 가지로 나뉜다.

  1. 반환값으로 확인 후 처리한다.
  2. 실패할 경우 점프한다.

C언어를 비롯한 많은 언어들은 1번을, Java를 비롯한 많은 언어들은 2번을 사용한다.

 

예외를 처리할 때 가장 중요한 것은 놓치지 않는 것이다.

놓치지 않은 예외는 프로그램 종료로 이어질 수 있으므로.

그를 위한 효율적 수단인 Java의 Checked exception은 thorws를 통해 반드시 명시적으로 발생 가능성을 표현하거나 try-catch를 통해 예외를 처리해야 한다. 하지만 하나의 메서드가 thorws할 경우 상위의 모든 메서드가 throws를 해야할 수 있다. 이는 매우 불편하므로 잘 사용되지 않는다.

 

예외를 처리하는 방식을 선택할 때는 놓치지 않는 것과 코드의 복잡성 두 가지를 모두 고려해야 한다.

 

 

7장 이름과 스코프

변수와 함수에 이름을 붙이는 것은, 번호로 무언가를 찾는 것보다 인간에게 직관적이기 때문이다.

 

그렇게 붙인 이름이 중복되는 경우가 발생한다.

초기에는 하나의 대응표를 사용했는데, 여러가지 문제가 발생했다.

 

해결하기 위해 나온 방법인 동적 스코프는, 함수 내부에서 전역 변수의 값을 바꿔서 사용하고, 종료 전에 다시 바꿔놓는 방법이다.

하지만 이 방법은, 해당 함수 내부에서 새로 호출한 함수가 전역 변수를 사용할 경우, 바뀐 값을 참조한다는 문제가 발생한다.

 

이를 다시 해결한 방법이 정적 스코프이다. 함수마다, 정확히는 함수 호출마다 새로운 대응표를 만들고, 함수가 종료되면 대응표를 삭제하는 방식이다.

 

이 또한 완벽한 방법은 아니지만, 대부분의 상황에서 잘 작동하므로 널리 사용되고 있다.

 

 

8장 형

컴퓨터는 숫자를 어떻게 표현할까?

0~9에 대응되는 비트를 할당하고, 각각을 on/off로 표현함으로써 숫자를 표현할 수 있을 것이다.

이는 지극히 비효율적이므로, 자연스러운 과정을 거쳐 이진법을 사용하는 것으로 변모했다.

 

그러다 소수를 표현할 일이 생겼고, 소수점 자리수를 고정하는 고정 소수점을 사용했었으나,

유연함을 위해 부동 소수점을 사용할 일이 생겼다. 소수점이 어디 위치하는지 자료 자체가 값을 가지고 있는 형태를 의미한다.

 

그리고 정수와 부동 소수점 자료를 구분하기 위해 형이 탄생했다.

 

발전된 형태의 형은, 여러 형을 묶어서 새롭게 만들 수도 있게 되었고,

형 자체를 인자로 받아, 내부의 형을 결정할 수도 있게 되었다(C++의 Templete, Java의 Generics 등)

 

 

9장 컨테이너와 문자열

배열과 연결 리스트처럼 값을 넣기 위한 상자가 있다.

배열은 인덱스를 통해 값을 조회하기에 O(1)의 시간 복잡도로 값을 조회할 수 있다.

하지만 값을 추가하려면, O(n)의 계산량을 가진다. 특정 인덱스 이후의 값을 모두 복사해야하기 때문.

연결 리스트는 반대로, 값 추가는 O(1)의 시간 복잡도를 가지지만, 값 조회는 O(n)의 시간 복잡도를 가진다.

 

문자열을 통해 값을 찾는 사전은, 해쉬 테이블과 트리로 대개 구현한다.

해쉬 테이블은 값 조회 시 해쉬 함수 1회만 호출하면 주소값을 알 수 있기에, O(1)의 시간 복잡도를 가진다. 하지만 메모리 소비량이 매우 크다.

트리는 값을 꺼내는 처리가 O(log n)의 시간 복잡도를 가진다.

증가할 가능성이 있는 것과, 그것의 시간 복잡도를 고려해서 자료 구조를 선택해야 한다.

 

모스 부호로부터 발전되어온 문자 체계는, 아스키를 거쳐 유니코드에까지 이르렀다.

 

 

10장 병행 처리

병행 처리는 병렬(하드웨어)과 다르게 소프트웨어적 개념이다.

처리를 변경하는 2가지 방법이 있는데,

하나는 협력적 멀티태스크로, 처리가 일단락되는 시점에 자발적으로 처리 교대를 하는 방법이다.

둘째는 선점적 멀티태스크로, 태스크 스케줄러가 처리를 강제로 중단시킨다. (시간 등의 이유로)

하지만 스레드 세이프하지 않은 상황이 발생할 수 있다.

 

스레드 세이프하지 않은 상황은

  1. 2가지 처리가 변수를 공유
  2. 적어도 하나의 처리가 그 변수를 변경
  3. 한쪽 처리가 마무리 되기 전에 다른 한쪽의 처리가 끼어들 가능성이 있음

위 세 가지 조건을 모두 만족해야만 한다.

반대로, 어느 한 조건만 막을 수 있어도 스레드 세이프하다.

 

공유하지 않는 접근법은 너무 엄격해서 구현하기 어려웠다.

변경하지 않는 방법도 존재한다.

끼어들지 않는 방법은, 협력적 스레드를 만드는 것이다.

또는 끼어들면 곤란해지는 처리에 표식을 붙인다. (락, 뮤텍스, 세마포어)

하지만 데드락이 발생할 수 있다.

데이터베이스의 트랜잭션 기법을 메모리에 적용해, 별도의 버전에 변경을 적용하다가, 마지막 단계에서 실제 메모리에 변경을 적용하는 방법이 있다. 하지만 잦은 호출이 일어나면 성능이 나빠질 수 있다.

트랜잭션 메모리 구현은 하드웨어, 소프트웨어를 거쳐 다시 하드웨어로 구현하는 것이 대세가 되었다.

 

 

11장 객체와 클래스

객체지향의 탄생에 앞서, 프로그래머에겐 변수와 함수를 합쳐서 모형을 만들고 싶다는 목적이 있었다.

모듈과 패키지로 인수, 초기화 처리 등의 해쉬를 묶는 방법이 있다.

Javascript에서처럼 함수도 해쉬에 넣는 방법이 있다. 함수를 퍼스트 클래스(변수에 대인, 인수로 전달, 반환값으로 사용 등이 가능한 값)로 사용한다는 것이다.

이를 통해 복수 개의 동일하지만 주소가 다른 객체를 생성할 수 있었고, 공유하는 내용을 프로토타입으로 이동시킬 수도 있었다.

그리고 클래스는 새로운 형을 만들고, 조작에 대한 사양을 정의하려고 탄생했다.

  1. 결합체를 만드는 생성기
  2. 어떤 조작이 가능한지에 대한 사양
  3. 코드를 재사용하는 단위

1번은 앞선 사례에서도 사용된 예시이고, 2번은 Java에서 인터페이스로 만들어졌다.

3번은 상속을 통한 것인데, 다음 장에서 다룬다.

 

 

12장 상속을 통한 재사용

상속은 3가지 측면으로 접근할 수 있다.

1. 일반화 / 특수화

흔히 말하는 is-a 관계에 해당한다.

 

2. 공통 부분을 추출

이는 is-a 관계에 해당하지 않는다.

 

3. 차분 구현

대부분 is-a 관계가 아니며, 상속을 재사용을 위해 사용하고, 변경된 부분만 추가로 구현한다.

 

리스코프 치환원칙을 지켜야 하지만, 속성이 늘어날 때 상속 관계로부터 리스코프 치환원칙이 지켜지지 않을 수 있다.

 

다중 상속은 편리하지만 다루기 어려운 도구이다.

코드 재사용에 매우 편리하지만, 메서드나 변수가 충돌하는 문제가 생길 수 있다.

해결책을 짚어보자.

 

1. 다중 상속을 금지한다. → Java

위임 (조합) → 이는 객체를 상속하는 게 아니라 보유함으로써 중복 코드를 제거한다. 이를 하드코딩하는 것이 아니라, 설정 파일을 통해 실행 시에 주입하는 것이 편리하다는 발상에서 ‘의존성 주입’이 탄생했다.

Java는 다중 상속을 금지하지만, Interface에 한해서는 가능하다.

 

2. 메소드 해결 순서를 고민한다.

상속받은 클래스의 메소드 중 어느 것을 사용할 지 해결하는 알고리즘을 사용하는 것이다. DFS, C3 선형화 등의 방법이 사용된다.

 

3. 처리를 섞는다.

Python의 Mixin처럼 재사용하고 싶은 기능만을 모은 작은 클래스를 만들어, 해당 기능을 추가하고 싶은 클래스에 섞어 넣는다.

 

4. 트레이트(Trait)

클래스는 2가지 상반되는 역할이 있다.

첫째는, 인스턴스를 만들기 위한 것.

둘째는, 재사용 단위.

Java에서는 클래스를 첫째의 역할로 주로 사용하고, 둘째의 역할로는 사용하는 것을 권장하지 않는다.

이 중 둘째의 역할에 특화된 작은 구조(메소드 묶음)가 트레이트이다.

 

어느 것이 정답인지는 모른다. 그리고 각각의 상황에 따라 정답이 달라질 수도 있다.

트레이트는 다양한 언어에서 도입되고 있다. 이후에 가장 유망한 기술로 자리잡을 수도 있다.

하지만 10년, 20년 후 보다 좋은 해결책이 고안되어, 동적 스코프 같이 잊혀질 수도 있다.

 

단, 클래스가 ‘재사용 단위’, ‘인스턴스 생성기’로서의 역할을 모두 가지고 있고, 그것은 상반된 것이라는 ‘트레이트’의 생각 방식은 매우 훌륭해 보인다고 저자는 말한다.

과연 트레이트는 정적 스코프나 while문 같이 당연한 존재로 자리잡을 것인가?

 

 

 

 

 

 

반응형
반응형

책 소개

객체지향을 공부한다면, 누구나 한 번 쯤 읽어보면 좋을 책.

 

객체지향의 개념을 처음 접하는 사람도 쉽게 이해할 수 있는 설명과 예시로 이루어져 있고,

객체지향의 개념을 실무에서 적용해본 사람도, 새로운 인사이트를 얻어갈 수 있는 책이라고 생각합니다.

 

개인적으로는 반복되는 설명이 많아 읽는 데에 피로감이 다소 있었습니다만,

어느 모로 보나 쉽고, 재밌고, 유익한 책인 것만은 확실합니다.

 

 

저는 객체지향 개념에서 사용되는 단어들,

예컨대 책임, 상태, 추상화, 캡슐화 등..

단어만으로는 뜻이 와닿았던 적이 없었습니다.

 

하지만 모방이 아닌 은유(저는 차용이라고도 이해했습니다)를 통해

실세계의 개념을 객체지향에 적용하고,

객체지향만의 고유의 특징을 이해하며, (음료수가 스스로 마셔질 수 있다..!)

그 안에서 어려운 단어들을 풀어서 설명해주신 방식이 너무 좋았습니다.

 

아직 갈 길이 멀었지만,

그래도 내가 객체지향을 하고 있구나 라는 생각은 들게 된 것 같습니다.

 

 

아래에서는 책의 내용을 되짚어보고자 합니다.

 

 

 

 


책 내용 상세 정리

(* 편의상 반말을 사용함을 양해 부탁드립니다.)

 

 

1장 - 협력하는 객체들의 공동체

객체지향은 역할, 책임, 협력으로 이루어진다.

 

[카페 예시]

  • 역할 : 손님, 캐셔, 바리스타
  • 책임 : 주문, 커피 오더, 커피 만들기
  • 협력 : 커피 주문 전 과정

객체지향이란 시스템을 ‘상호작용하는 자율적인 객체들의 공동체’로 바라보는 것

자율적인 객체란, 상태와 행동을 함께 지니며, 스스로를 책임지는 객체

 

각 역할은 협력하고, 역할은 관련된 책임의 집합.

 

객체 사이 상호작용은 ‘메시지’로만 이루어지고,

메시지를 수신한 객체는 적합한 메서드를 자율적으로 선택

 

 

 

 

2장 - 이상한 나라의 객체

객체는 상태, 행동, 식별자로 이루어져 있다.

 

'이상한 나라의 앨리스 예시'로 그것을 설명한다.

예시 내용을 요약하면 다음과 같다.

 

높이가 40cm인 문으로 나가 아름다운 정원에서 뛰어 놀고 싶은 앨리스.

앨리스에게 주어진 것은 '마시면 키가 줄어드는 음료수', '먹으면 몸이 커지는 케이크'이다.

 

 

상태

  • 엘리스의 키
  • 음료수의 양

행동

  • 음료수를 마시다
  • 케이크를 먹자
  • (음료수 입장에서) 마셔지다 → 실세계와 객체지향 세계가 다른 점!

식별자

  • 고유의 객체를 구별할 수 있는 값
  • 엘리스의 국적 + 주민번호 쯤이 되겠다..

 

캡슐화란?

  • 객체의 상태를 (캡슐 안에 숨겨두어) 외부로 노출하지 않는다.
  • 예를 들면, 앨리스 객체의 키에 alice.height 로는 접근할 수 없고, 앨리스 객체에 getHeight() 메서드가 있다면 그것으로 값을 구해올 수 는 있는 것.
  • 객체는 외부에 상태가 아닌 행동만을 노출하고, 외부에서는 행동만을 통해서 객체에 접근한다.

 

행동이 상태를 결정한다.

 

앨리스 예시에서, 키와 위치를 먼저 생각하는 것이 아니라,

행동을 먼저 생각하고 필요한 상태를 정의한다.

 

객체지향은 현실세계의 모방이 아닌 은유다!

객체지향 세계는 현실세계의 비유를 통해 쉽게 이해할 수 있다. (변수명을 지을 때도, 현실세계의 은유를 빌리면 훨씬 직관적이다!)

하지만 현실과 다른 무궁무진한 상상을 구현할 수 있으니 현실세계 사고에 갇히지 말자. (음료수가 스스로 마셔지는 것 또한 그렇다.)

--> 객체스럽게 설계하라!

 

 

 

 

3장 - 타입과 추상화

3장에서는 트럼프 카드의 몸체를 지닌 하트 여왕, 스페이드 정원사, 클로버 병사의 행렬을 발견하는 앨리스를 묘사한다.

 

 

추상화

명확한 이해를 위해, 특정 절차나 물체를 생략하거나 감춰 복잡도는 줄이는 방법.

  • 첫 번째 차원 : 공통점은 취하고 차이점은 버리는 일반화
  • 두 번째 차원 : 불필요한 세부 사항을 제거

개념

특정 객체가 어떤 그룹에 속하는지 결정하는 것

  • 비행기, 자동차, 사람 등..
  • 앨리스 예시 : 트럼프!

 

타입

컴퓨터공학에서의 개념이자, 타입의 관점에서 본 객체

  1. 객체가 타입에 속하는지 결정하는 것은 객체의 행동
  2. 객체들이 동일한 행동을 수행할 수 있다면, 동일한 타입
  3. 객체의 내부적 표현(행동의 구현 방식, 상태)은 외부로부터 감춰진다

 

타입의 계층

트럼프 - 트럼프 인간

트럼프는

  • 납작 엎드리기
  • 뒤집어지기

트럼프 인간은

  • 납작 엎드리기
  • 뒤집어지기
  • 걸을 때마다 몸이 종이처럼 좌우로 펄럭이기

트럼프는 트럼프 인간의 일반화(슈퍼타입)

트럼프 인간은 트럼프의 특수화(서브타입)

 

정적 모델과 동적 모델

객체는 동적이다 -> 시간에 따라 상태는 변한다

타입은 정적이다 -> 정적인 관점에서 객체를 묘사한다

결국 타입은 추상화다!

 

 

 

 

4장 - 역할,  책임,  협력

 

재판이라는 협력

  1. 왕이 판사로서 증인을 불러오라고 요청,
  2. 토끼는 증인인 모자 장수를 부름,
  3. 모자 장수는 법정에 입장,
  4. 왕은 모자 장수에게 증언을 요청,
  5. 모자 장수는 증언

객체지향 설계는

  1. 협력이라는 문맥 속에서 책임을 고민
  2. 책임을 적절한 객체에 부여

하는 순서로 작성되어야 한다.

 

역할

역할은 책임의 집합이다.

 

역할은 협력을 추상화하고, 유연한 객체지향 설계를 만든다.

 

판사 역할 : 왕, 왕비 수행 가능

증인 : 모자 장수, 공작 부인의 요리사, 앨리스 수행 가능

 

이처럼 ‘판사’, ‘증인’의 역햘로 구성된 ‘재판’이라는 협력은 추상화되어 있고, 유연한 설계이다.

 

객체지향 설계 기법

  • 책임 주도 설계
    • 책임들을 식별하고, 적합한 객체에게 할당하는 방식으로 설계
  • 디자인 패턴
    • 책임 주도 설계의 결과를 표현하는 것으로, 반복되는 문제 해결을 위한 방법과 절차를 만들어 놓은 틀
    ex: MVC, MVP, MVVM
  • 테스트 주도 개발 : 응집도가 높고 결합도가 낮은 클래스로 구성된 시스템을 개발할 수 있는 좋은 방식.

 

 

 

 

5장 - 책임과 메시지

 

책임

책임은 적당히 추상적이면서 구체적이어야 한다.

→ ‘증언하라’는 책임

책임은 how가 아닌 what을 설명

 

메시지

메시지 → 메서드 : 모자장수.증언하라(어제, 왕국)

 

모자장수 : 수신자

증언하라 : 메시지 이름

어제, 왕국 : 인자

 

같은 메시지를 보내도, 수신자가 다르게 결정할 수 있다. → 다형성!

 

What/Who 사이클 : 책임 주도 설계의 핵심 → 어떤 행위가 필요한지 결정하고, 누가 수행할 것인지를 결정한다.

 

묻지 말고 시켜라! (Don’t ask, 디미터 법칙) → 객체가 자율적으로 구현 → 캡슐화 보장, 결합도 낮게 유지

 

인터페이스와 구현

인터페이스 : 객체 외부

구현 : 객체 내부

인터페이스는 구현과 분리되어야 한다.

 

인터페이스의 3가지 원칙

  • 좀 더 추상적인 인터페이스 : 객체의 자율성 보장
  • 최소 인터페이스 : 객체 내부 동작에 대해 가능한 적은 정보만 노출
  • 인터페이스와 구현 간 차이를 인식

캡슐화 : 자율성 보존을 위해 구현을 외부로부터 감추는 것

데이터 캡슐화 : 상태와 행동을 하나의 단위로 묶음

 

자율적인 책임은!

  • 협력을 단순하게 만든다 (추상화)
  • 외부와 내부가 명확하게 분리 (캡슐화)
  • 내부를 변경해도 외부에 영향 X (변경의 파급효과가 객체 내부로 캡슐화 → 객체 간 결합도가 낮아짐)
  • 협력의 대상을 다양하게 선택할 수 있는 유연성 제공 (+ 재사용성 증가)
  • 객체 역할 이해가 쉬워짐 (응집도 증가)

 

 

 

6장 - 객체 지도

  • 소프트웨어 제품 설계 → 기능 설계 + 구조 설계

도메인 모델

  • 구조를 설계하기 위한 모델
  • 도메인 : 프로그램 사용 분야 (은행, 게임, 병원 등)
  • 모델 : 대상을 단순화한 구조
  • 도메인 모델 : 대상 영역의 지식을 단순하게 구조화한 것

계좌

  • 계좌번호
  • 예금액

이자

  • 금액
  • 지급일자

위의 도메인 모델은 코드에서 사용할 개념과 관계를 제공 -> 그것을 구현하여 코드 작성

 

Account 객체 설계

상태

  • accountNumber
  • amount

메서드

  • calculateInterest(when)

 

유스케이스

사용자와 시스템 간 상호작용을 텍스트로 정리한 것

 

예시)

유스케이스명 : 중도 해지 이자액을 계산한다

일차 액터 : 예금주

주요 성공 시나리오:

  1. 예금주가 정기예금 계좌를 선택한다.
  2. 시스템은 정기예금 계좌 정보를 보여준다.
  3. 예금주가 금일 기준으로 예금을 해지할 경우 지급받을 수 있는 이자 계산을 요청한다.
  4. 시스템은 중도 해지 시 지급받을 수 있는 이자를 계산한 후 결과를 사용자에게 제공한다.

확장 : 3a. 사용자는 해지 일자를 다른 일자로 입력할 수 있다.

 

유스케이스의 특징

  1. 텍스트다.
  2. 하나가 아닌 여러 시나리오들의 집합이다.
  3. 단순한 피처가 아닌, 연관된 기능의 묶음이다.
  4. 사용자 인터페이스와 관련된 세부 정보를 포함하면 안 된다.
  5. 내부 설계 정보를 포함하면 안 된다.

 

책임 주도 설계는

유스케이스 작성 + 도메인 모델 → 협력 관계 및 메시지 정의 → 코드 구현

의 순서로 진행되어야 한다.

 

 

 

 

7장 - 함께 모으기

 

개념 관점 : 도메인의 개념과 관계를 보는 관점

명세 관점 : 인터페이스 관점

구현 관점 : 코드 관점

코드에 각 관점이 잘 드러나게 설계해라

 

도메인 개념 참조 → 변화에 쉽게 대응 가능(안정적이고 이해가 쉽다)

인터페이스와 구현을 분리하라! → 변화에 유연하게 하라!

 

 

 

 


 

 

 

감사합니다!

 

 

 

 

 

반응형

+ Recent posts