Posts 실용주의프로그래머 5장 구부러지거나 부러지거나
Post
Cancel

실용주의프로그래머 5장 구부러지거나 부러지거나

이번 장에서는 되돌릴 수 있는 의사결정을 내리는 구체적인 방법을 설명한다.

  • topic 28 : 결합도 줄이기
  • topic 29 : 실세계를 갖고 저글링하기. 이벤트에 반응하는 네 가지 서로 다른 전략
  • topic 30 : 변환 프로그래밍. 함수 파이프라인
  • topic 31 : 상속세. 유연하고 바꾸기 쉬운 코드를 만들 수 있는 대안
  • topic 32 : 설정. 세부사항을 코드 밖으로 옮기는 방법

Topic 28 : 결합도 줄이기

코드에서 나타나는 결합의 증상

  • 관계없는 모듈이나 라이브러리 간의 희한한 의존 관계
  • 한 모듈의 간단한 수정이 이와 관계없는 모듈을 통해 시스템 전역으로 퍼져나가거나 시스템의 다른 곳에서 무언가를 꺠뜨리는 경우
  • 개발자가 수정하는 부분이 시스템에 어떤 영향을 미칠지 몰라 코드의 수정을 두려워하는 경우
  • 변경 사항에 누가 영향을 받는지 파악하고 있는 사람이 없어서 결국 모든 사람이 참석해야하는 회의

열차 사고

1
2
3
4
5
public void applyDiscount(customer, order_id, discount) {
	totals = customer.orders.find(order_id).getTotals();
	totals.grandTotal = totals.grandTotal - discount;
	totals.discount = discount
}

이 코드를 지원하기 위해 앞으로 바꾸면 안되는 것들이 너무 많다. 기차의 객차가 연결되어있듯이 메서드나 속성들이 모두 연결되어있다. 이런 코드를 열차 코드라고 부른다.

만약 할인율을 변경해야할때는 어떻게 해야할까? totals 객체의 필드는 어디서든 변경할 수 있다. 따라서 다른 곳에서 필드 값을 바꾸는 부분이 있다면 모두 변경해야한다.

이것은 책임의 문제이다. totals 객체가 합계를 관리하는 책임을 져야하는데 그렇게 하고 있지 않다. 누구나 질의하고 갱신할 수 있는 다수의 필드를 가진 컨테이너일 뿐이기에 그렇다.

이를 고치려면 다음 원칙을 적용해야한다.

묻지말고 말하라

다른 객체의 내부 상태에 따라 판단을 내리고 그 객체를 갱신해서는 안된다. 즉, 할인 처리를 totals 객체에 위임해야한다.

1
2
3
public void applyDiscount(customer, order_id, discount) {
	customer.orders.find(order_id).getTotals().applyDiscount(discount);
}

더 개선하면 customer 객체에서 주문 컬렉션을 가져오지 말아야한다.

1
2
3
public void applyDiscount(customer, order_id, discount) {
	customer.findOrder(order_id).getTotals().applyDiscount(discount);
}

추가로 order 객체가 totals 객체를 가졌다는 걸 굳이 드러낼 필요도 없다.

1
2
3
public void applyDiscount(customer, order_id, discount) {
	customer.findOrder(order_id).applyDiscount(discount);
}

여기서 더 숨길 수도 있지만, 여기서는 customerorder가 최상위 개념이다. 이정도는 드러내도 괜찮을 것이다.

데메테르 법칙

LoD(데메테르 법칙) : 어떤 클래스 C에 정의된 메서드는 다음 목록에 속하는 것만 사용할 수 있다.

  • C의 다른 인스턴스 메서드
  • 메서드의 매개변수
  • 스택이나 힙에 자신에 생성하는 객체의 메서드
  • 전역 변수

다음 말이 좀 더 쉬울 수 있다.

메서드 호출을 엮지 말라

무언가를 접근할 때 .을 딱하나만 사용하도록 노력하라. 다만 엮는 것들이 절대로 바뀌지 않을 것 같다면 괜찮다. (언어에서 기본적으로 포함된 기능들. 외부 라이브러리는 아님)

연쇄와 파이프라인

함수에서 함수로 데이터를 넘겨가며 데이터를 변환하는 것은 열차사고와는 다르다. 숨겨진 구현 세부 사항에 의존하지 않기 때문이다. 하지만 결합이 생기지 않는 것은 아닌데, 한 함수의 결과물이 다음 함수의 입력 형식이어야하기 때문이다. 하지만 이러한 결합은 열차사고보단 문제가 되는 경우가 적다.

글로벌화의 해악

전역 데이터는 애플리케이션 컴포넌트 간의 결합을 만들어낸다. 전역 데이터는 시스템 전체에 영향을 줄 수 있다. 변경될 때 바꿔야할 곳을 전부 바꿨는지 확인하기 어려울 수 있다.

전역 데이터를 피하라.

싱글턴도 전역 데이터이다.

외부로 노출된 인스턴스 변수가 많은 싱글턴은 여전히 전역 데이터이다. 메서드 안으로 데이터를 숨기면 한층 낫긴하지만 설정데이터를 단 한 벌만 가지고 있는 것에는 변함이 없다.

외부 리소스도 전역 데이터다

수정 가능한 외부 리소스(DB, API 등)는 모두 전역 데이터다. 이러한 리소스들을 코드로 모두 감싸는 것이 좋다.

전역적이어야 할 만큼 중요하다면 API로 감싸라.

결국은 모두 ETC

직접적으로 아는 것만 다루는 부끄럼쟁이 코드를 계속 유지하라. 그러면 결합도를 크게 낮출 수 있다.


Topic 29 : 실세계를 갖고 저글링하기

이번 장은 반응적인 애플리케이션을 작성하는 법을 다룬다.

이벤트

이벤트는 무언가 정보가 있다는 것을 의미한다. 정보는 외부에서 올 수도 있고, 내부에서 생길 수도 있다. 어디에서 온 것이든 애플리케이션이 이런 이벤트에 반응하고 하는 일을 조절하도록 만들면, 실세계에서 더 잘 작동하는 애플리케이션을 만들 수 있다.

이러한 애플리케이션을 만드는 다음 네가지 전략이 있다.

  • 유한 상태 기계
  • 감시자 패턴
  • 게시 - 구독
  • 반응형 프로그래밍과 스트림

유한 상태기계

FSM은 작성해야하는 부분은 적지만, 한번 작성하면 이벤트를 어떻게 처리할지 한눈에 알기 쉽다.

감시자 패턴

감시자 패턴은 이벤트를 발생시키는 쪽인 감시대상과, 이벤트에 관심이 있는 감시자로 이루어진다.

감시자는 자신이 관심 있는 이벤트를 감시대상으로 등록한다. 보통은 호출될 함수의 참조도 같이 넘긴다. 추후 해당 이벤트가 발생 시, 등록된 감시자 목록을 보며 함수들을 일일이 호출한다.

감시자 패턴은 수십년간 잘 작동해왔지만 한가지 문제점이 있다. 바로 모든 감시자가 감시 대상에 등록해야하는 결합이 생긴다는 것이다. 또 감시 대상이 콜백을 직접 호출하기에 성능 병목이 생길 수 있다. 이러한 문제점은 게시-구독으로 해결한다.

게시 - 구독

감시자 패턴을 일반화한 것으로 감시자 모델의 결합도를 높이는 문제와 성능 문제를 해결한다.

게시-구독 모델은 게시자구독자가 있고, 채널로 연결된다. 채널은 코드 밖에 있으며, 게시자가 채널로 이벤트를 보내고, 구독자는 채널을 통해 이벤트를 받는다. 이는 비동기적으로 이루어진다.

이러한 게시-구독 모델은 대부분의 클라우드 서비스가 제공하기에 직접 구현할 필요는 없다. 또한 추가적인 결합이 없어 기존 코드를 수정하지 않고 이벤트 처리코드를 추가하거나 교체할 수 있다.

단점은 너무 많이 사용하는 시스템에서는 현재 어떤 일이 벌어지는지 파악하기가 어렵다는 것이다. 게시자가 이벤트를 보내고 어떤 구독자가 처리하는지 확인하기 어렵다.

반응형 프로그래밍과 스트림 그리고 이벤트

반응형 프로그래밍 : 하나의 값이 바뀌면, 그 값을 사용하는 다른 값이 반응하는 것

  • ex) 엑셀, 리액트, 뷰

스트림 : 이벤트를 일반적인 자료구조처럼 다룰 수 있게 해줌. 이벤트의 리스트. 이벤트를 처리하고, 조합하고, 골라내는 등 일반적인 자료구조처럼 다룰 수 있음

  • 비동기적으로 작동할 수도 있다.

반응형 이벤트의 표준은 https://reactivex.io/ 에서 정의했다. 이 사이트에서는 언어에 무관한 원칙들을 정의하고 몇가지 공통 구현사항을 문서화 했다. 이를 구현한 것으로는 자바스크립트 RxJS 등이 있다.

후기)

스트림 부분은 잘 이해가 안갔음. 사용해본적이 없어서 정확히 어떤 것에서 큰 장점이 있는지 모르겠음. 한번 예제라도 건드려 보는게 좋을거 같다

어디에나 이벤트가 있다

이벤트는 모든 곳에 있다. 하지만 이벤트가 어디서 발생하든 이벤트를 중심으로 공들여 만든 코드는 일직선으로 수행되는 코드보다 더 잘 반응하고 결합도가 낮다.


Topic 30 : 변환 프로그래밍

모든 프로그램은 데이터를 변환한다. 코드에만 집중하여 핵심을 놓치지말고, 프로그램이란 입력을 출력으로 바꾸는 것이라고 생각하라. 이렇게하면 프로그램 구조는 명확해지고, 결합도 대폭 줄어들 것이다.

프로그래밍은 코드에 관한 것이지만, 프로그램은 데이터에 관한 것이다.

변환 찾기

요구사항에서 입력과 출력이 무엇인지 찾으면 전체 프로그램을 나타내는 함수가 정해짐. 이후에는 입력을 출력으로 바꾸는 단계를 차례대로 찾으면 됨. (top-down 방식)

변환 모델의 좋은점

객체지향프로그래밍을 하다보면 데이터를 숨기고, 객체 안에 캡슐화해야한다고 생각한다. 이런 객체들이 서로 대화하며 서로의 상태를 변경하는데, 이런 방식은 결합을 많이 만들어내고, 이는 이후 유지보수가 어려운 코드를 만들게한다.

상태를 쌓아놓지말고 전달하라.

변환 모델은 데이터를 전체 시스템에 흩어놓는 대신, 데이터를 거대한 강으로 생각한다. 데이터는 기능과 동등해져서 파이프라인은 코드 -> 데이터 -> 코드 -> 데이터의 연속이다. 데이터는 특정한 함수들과 묶이지 않는다. 대신 우리 애플리케이션이 입력을 출력으로 바꾸어 나가는 진행 상황을 데이터로 자유롭게 표현하여 결합을 크게 줄일 수 있다. 어떤 함수든 매개변수가 다른 함수의 출력결과와 맞기만 하면 어디서나 재사용할 수 있다.

ex) 보통 객체지향에서는 두번째처럼하는데, 두번째는 앞단계에서 반환하는 객체에 다음 함수가 구현되어있어야한다. 만약 새로운 요구사항이 생겼을때, 두번째는 해당 객체에서 함수를 변경해야하는데 다른 코드에 영향이 갈까 두려움이 생길 것이다. 첫번째 방식은 그냥 한단계를 추가하면 된다.

  • 데이터 변환
    1
    2
    3
    
    const content = File.read(fileName);
    const lines = findMatchingLines(content, pattern);
    const result = truncateLines(lines);
    
  • 메서드 연결
    1
    2
    3
    
    const result = contentOf(fileName)
                  .findMatchingLines(pattern)
                  .truncateLines();
    

오류 처리는 어떻게 하나?

공통적으로 연쇄 변환 도중 변환 사이에 값을 날것으로 넘기지 않는 관례가 있다. Wrapper 역할을 하는 자료구조로 값을 싸서 넘긴다.

이런 개념을 이용하여 오류 검사를 변환 안에서 하거나, 변환 바깥에서 할 수 있다.


Topic 31 : 상속세

코드를 공유하기 위해 상속을 쓸때의 문제

상속도 일종의 결합이다. 만약 최상위 클래스만 바꾸고 싶을때도 하위 클래스에서 어떻게 사용하고 있는지에따라 코드가 망가질 수 있다. 최상위 클래스의 담당자가 자기는 한 클래스의 변수명을 변경하였는데, 코드 전체가 망가지면 당황할 것이다.

타입을 정의하기 위해 상속을 쓸때의 문제

상속을 타입을 정의하기 위해 사용하는 경우도 있다. 이때 클래스 계층도 같은 것을 사용할 수 있다. 하지만 클래스 사이의 미묘한 차이를 위해 계층을 계속 더하다보면 클래스 계층도는 매우 복잡해진다.

다중 상속을 지원하지 않는 것도 문제가 될 수 있는데, 어떤 클래스를 제대로 모델링하려면 다중 상속이 필요해질 수 있기 때문이다.

더 나은 대안

인터페이스

자바의 인터페이스 같은 것을 사용할 수 있다. 인터페이스는 단순한 선언이고, 아무런 코드도 만들지 않으며 단순히 선언된 메서드를 구현해야한다고 지시할 뿐이다. 또한 이들을 타입으로 사용할 수 있고, 해당 인터페이스를 구현한 클래스라면 무엇이든 그 타입과 호환된다.

다형성은 인터페이스로 표현하는 것이 좋다.

1
2
3
4
public interface Locatable {
	Coordinate getLocation();
	boolean locationIsValid();
}

위임

상속을 사용하게 되면 딱 2개의 메서드만 필요해도 부모의 20개의 메서드가 한번에 딸려온다. 클래스가 자신의 인터페이스를 제어할 수 없게 되는 것이다.

대신에 위임을 사용하면 클래스는 필요한 것만 노출할 수 있다.

서비스에 위임하라. Has-A 가 Is-A보다 낫다.

믹스인, 트레이트, 카테고리, 프로토콜 확장 등

언어마다 이름은 다르지만, 클래스나 객체에 상속을 사용하지 않고 새로운 기능을 추가하여 확장하고 싶을 떄 사용하는 기능이다.

1
2
3
4
5
6
7
mixin CommonFinders {
	def find(id) {...}
	def findAll() {...}
}

class AccountRecord extends BasicRecord with CommonFinders
class OrderRecord extends BasicRecord with CommonFinders

믹스인으로 기능을 공유하라

예를들어 검증을 적용할 수 있는 여러 계층이 있을때, 각 상황에 맞는 전문화된 클래스를 믹스인을 사용하여 만들 수 있다.

1
2
3
class AccountForCustomer extends Account with AccountValidations, AccountCustomerValidations

class AccountForAdmin extends Account  with AccountValidations, AccountAdminValidations

상속이 답인 경우는 드물다

  • 인터페이스
  • 위임
  • 믹스인과 트레이트

상황에 따라 상속보다 더 나은 방법이 있을 것이다. 타입 정보를 교환하고 싶은건지, 기능을 더하고 싶은건지, 메서드를 공유하고 싶은건지에 따라 다르다. 우리의 목표는 의도를 가장 잘 드러내는 기법을 사용하는 것이다. 그리고 정글 전체를 끌어들이지 않도록 조심하다


Topic 32 : 설정

프로그램 출시 이후 변경될 것이라 생각되는 값들은 프로그램 외부에서 관리하라. 이렇게하면 프로그램을 조정할 수 있게 된다.

외부 설정으로 애플리케이션을 조정할 수 있게하라.

소스 코드 본체 바깥에 표현할 수 있는 것을 찾아라.

정적 설정

대부분의 프레임워크에서는 설정을 일반 파일이나 데이터베이스 테이블로 관리한다. 어떤 형태로 사용하든 설정을 자료구조 형태로 불러온다. 보통 처음 애플리케이션이 실행 될 때 읽어올 것이다.

보통 이 설정을 전역에서 접근할 수 있도록 하는데, 그것보다는 API 뒤로 숨기는 것이 좋다. 그러면 설정을 표현하는 세부 사항으로부터 코드를 떼어놓을 수 있다

서비스형 설정

설정을 외부에서 관리하긴하지만, 일반 파일이나 데이터베이스가 아니라 서비스 API 뒤에서 관리하는 방식이다. 여기에는 몇가지 장점이 있다.

  • 여러 애플리케이션이 설정 정보를 공유할 수 있다.
  • 여러 인스턴스에 걸쳐서 전체 설정을 한번에 바꿀 수 있다.
  • 설정 데이터를 전용 UI로 관리할 수 있다.
  • 설정 데이터를 동적으로 계속 바꿀 수 있다. -> 새로이 빌드-배포가 필요없다.

설정 정보를 바꾸기 위해 코드 빌드가 필요없는 방식이다.

지나치게 하지는 말라

지나치게 모든 변수를 설정할 수 있게하지는 말라.

여담

실무를 경험해본 결과 서비스형 설정이 확실히 좋았다. 이러한 값들은 빌드가 필요없이 바로 참조가 되어 이슈가 발생했을 경우 곧바로 대응을 할 수가 있기 때문이다.

물론 스프링에서 properties 파일과 같은 정적 설정도 장점이 있다. 설정 정보의 성격에 따라 적절히 분리해서 관리하면 될 거 같다.

  • 정적 설정 : 자주 변경되지 않지만 애플리케이션에 거쳐 많이 사용되거나 외부로 뺄 필요가 있어보이는 값들. 급하게 변경하지 않아도 이슈가 없는 값들
  • 서비스형 설정 : 자주 변경될거라 예상되는 값들. 급하게 변경이 발생할 수 있는 값들.

후기

이번 장은 전체적으로 결합도를 줄이는 것에 집중한 장이다.

topic 28 : 책임을 분명히하고, 자신이 직접적으로 아는 것만 다루는 방식을 통해 결합도를 줄일 수 있다. 전역 변수도 일종의 결합인데, 전역변수 변경 시 필요한 부분을 모두 바꾸었는지 확인하기 어렵기 때문이다.

이 토픽에서는 데메테르 법칙에 대해서도 얘기하였는데, 쉽게 풀이한 “무언가에 접근할 때 .을 두번 이상 사용하지 마라”라는 팁은 이해하기가 쉬웠다.

topic 29 : 이벤트를 구현하는 방식에 네가지 방식(유한 상태 기계, 감시자 패턴, 게시 - 구독, 반응형 프로그래밍과 스트림)에 대해 설명하였다. 유한상태기계를 작성하면 이벤트를 어떻게 다뤄야하는지 알기 쉽다는 팁이 도움될거 같다.

topic 30 : 프로그램은 결국 데이터를 변환하는 것이고, 데이터를 중심으로 구조를 생각하면 어떻게 파이프라인을 구성해야할지 한눈에 보인다.

topic 31 : 상속은 결합을 만들어내기에 사용을 지양해야한다는 파트이다. 상속 대신 인터페이스, 위임, 믹스인 등을 통해 기능을 추가하는 것이 좋다고 한다. 개인적으로 동감하는 파트이고, 상속은 프레임워크에서 제공하는 것 외에는 사용을 자제하는 것이 좋다고 생각한다.

topic 32 : 설정을 소스 본체 바깥에 저장하는 방식을 설명한다. 개인적으로 서비스형 설정이 변화에 대응이 쉬워 더 선호한다.

This post is licensed under CC BY 4.0 by the author.

실용주의프로그래머 4장 실용주의 편집증

실용주의프로그래머 6장 동시성

Comments powered by Disqus.