구부러지거나 부러지거나
유연한 코드를 작성하기 위한 자세한 기법들을 소개함
Topic 28 결합도 줄이기
여기서는 문제가 되는 여러 상황들에 대해서 소개한다 자세한 기법들은 뒤에 토픽에서 하나씩 다룬다
열차사고
여기서 처음 예시로 인스턴스가 메소드 체이닝형태로 호출하는 코드를 보여준다. 여기서 강조하는 게 TDA(tell don’t ask)인데 리팩터링 한 코드 보면 코드 내부상태 묻는 코드를 수정한다. 기존 코드 대로 객체를 노출시켜서 호출하면 캡슐화가 의미가 없어진다고 한다.
//수정전 코드
public void applyDiscount(customer, order_id, discount){
totals = customer.orders.find(order_id).getTotals();
totals.grandTotal = totals.grandTotal -discount;
totals.discount = discount;
// 객체 직접 수정하면 진짜 안좋음
//리팩토링 후 코드
public void applyDiscount(customer, order_id ,discount) {
customer.findOrder(order_id).applyDiscount(discount);
}
좀 더 추가하자면 여기서 아예 고객이 주문객체를 가지고 있는 사실도 숨길 수도 있지만, 최상위 개념이 고객과 주문이라는 가정이면 굳이 숨길 필요가 없다고 한다. 오히려 다숨기고 applyDiscountToOrder 이런 식이 더 어색해 보인다.
추가적으로 메서드 호출을 엮지마라고 강조한다. 파이프라인이랑 비슷해 보인데 파이프라인은 숨겨진 구현 세부 사항에 의존하지 않기 때문에 오히려 선호한다. 뒤에 변환 프로그래밍에서 자세히 다룬다. 메소드체이닝을 쓸 거면 엮는 것들이 절대 바뀌지 않는 가정에 쓰라는데, 현실적으로 모든 건 변하니까 웬만하면 그냥 쓰지 말라는 거다.
글로벌화의 해악
전역변수 쓰지 마라, 싱글톤이라고 괜찮지 않다. 전역변수를 쓴다는 거 자체가 결합력을 높이기 때문에 좋지 않다.
상속은 결합을 늘린다
상속에 대한 자세한 내용도 뒤에 상속세 토픽에서 자세히 다룬다.
Topic 29 실세계를 갖고 저글링 하기
여기서는 반응적인 애플리케이션을 작성하는 법을 다룬다.
이벤트
애플리케이션을 이벤트에 반응하도록 만들면 상호작용이 더 원활한 애플리케이션이 될 것이다. 여기서는 4가지 전략을 소개한다
유한 상태 기계
기본적으로 FSM은 이벤트를 어떻게 처리할지 정의한 명세일 뿐이다. 각 상태가 있고 상태마다 이벤트가 들어왔을 때 하는 행동들이 정의된다. 상태 이행을 일으키는 이벤트에 따라서 행동한다.
다음은 예시 코드 완성 시켜봤는데 책의 의도랑 완전히 맞는지는 모르겠다.
class StringFSM:
TRANSITIONS = {
'look_for_string': {
'"': ['in_string', 'start_new_string'],
'default': ['look_for_string', 'ignore'],
},
'in_string': {
'"': ['look_for_string', 'finish_current_string'],
'\\': ['copy_next_char', 'add_current_to_string'],
'default': ['in_string', 'add_current_to_string'],
},
'copy_next_char': {
'default': ['in_string', 'add_current_to_string'],
},
}
def __init__(self):
self.state = 'look_for_string'
self.output = []
self.escape_next_char = False
# Helper functions for actions
def ignore(self, _):
pass
def start_new_string(self, _):
self.output.append('')
def add_current_to_string(self, c):
if self.escape_next_char:
self.output[-1] += c
self.escape_next_char = False
else:
self.output[-1] += c
def finish_current_string(self, _):
self.escape_next_char = False
def process(self, input_string):
# Action map
ACTIONS = {
'ignore': self.ignore,
'start_new_string': self.start_new_string,
'add_current_to_string': self.add_current_to_string,
'finish_current_string': self.finish_current_string,
}
# Process the input string
for c in input_string:
if c in self.TRANSITIONS[self.state]:
next_state, action = self.TRANSITIONS[self.state][c]
else:
next_state, action = self.TRANSITIONS[self.state]['default']
ACTIONS[action](c)
if self.state == 'in_string' and c == '\\':
self.escape_next_char = True
self.state = next_state
# Return the output
return self.output
# Test the FSM
fsm = StringFSM()
print(fsm.process('this is a "test" with "multiple" strings and "escaped \\" quotes"'))
감시자 패턴
감시자 패턴은 이벤트를 일으키는 observable과 이벤트에 관심 있는 ‘감시자’로 나뉜다. 감시자는 관심 있는 이벤트를 감시 대상에 등록함. 보통은 호출될 함수의 참조도 등록할 때 함께 넘긴다.
감시자 패턴은 문제가 하나 있음 ⇒ 모든 감시자가 감시 대상에 등록을 해야 하기 때문에 결합이 생기는 것이다. 더군다나 위에서 언급한 것처럼 감시 대상이 콜백함수를 직접 호출하기 때문에 성능 병목이 될 수 있음. 여기서 헷갈린 게 밑에 게시-구독 전략이랑 비슷하다 생각했는데 책에선 감시자 패턴은 기본적으로 동기적 처리를 전제로 하는 거 같다. 그래서 위 같은 문제도 발생하는 거 같다. 그래서 이 문제를 게시-구독에서 해결한다고 함
게시-구독
이 전략은 publisher, subscriber 그리고 channel로 이루어진다. 보통 구독자가 채널에 관심사를 등록하고 게시자가 이벤트를 채널로 보낸다. 이렇게 중간에서 channel이 처리해 주는 거 같다. 그래서 기본적으로 비동기 처리를 전제로 하나보다. 그니까 게시-구독 모델은 공통 인터페이스인 채널을 추상화 함으로써 결합도를 줄인 것이다. 대신 문제는 너무 많이 사용하게 되면 어떤 일이 벌어지고 있는지 파악하기가 힘들다.
반응형 프로그래밍과 스트림
스프레드시트 같은 게 반응형 프로그램의 예시. 이벤트를 사용하여 반응하도록 만든 프로그램인데 이걸 가능하게 해 주는 게 스트림임. 스트림은 이벤트를 일반적인 자료구조 다루는 것처럼 해줌. 스트림 또한 병렬적으로 동작함.
Topic 30 변환 프로그래밍
원래 모든 프로그램은 데이터를 변환한다. 즉 입력을 출력으로 바꾼다. 이게 가장 간단한 원칙이다. 우리는 설계를 고민할 때 오히려 클래스, 모듈, 자료구조, 알고리즘 등에 매몰되어 이런 사실을 잊어버린다. 가끔 코드에서 좀 멀어져서 ‘프로그램은 단순히 입력을 출력으로 바꾸는 것’이라는 사고방식으로 돌아갈 필요가 있다.
여기서 터미널 명령어 예시로 설명한다.
$ find . -type f | xargs wc -l | sort -n | tail -5
여기서 하나 기억에 남는 게 ‘프로그래밍은 코드에 관한 것이지만, 프로그램은 데이터에 관한 것이다.’라는 팁이다.
그리고 이 토픽이 많이 헷갈리는데, 토픽 28에서부터 자꾸 파이프라이닝이랑 메서드 체이닝이랑 비슷한데 뭐가 다른 거지 생각이 들었다. 어차피 결괏값 내놓고 함수 호출하는 건 똑같지 않나? 밑에 글을 보니까 내가 코드만 신경 쓰고 있었던걸 깨달았다. 읽다 보니까 뭔가 함수형 프로그래밍이 생각이 났다.
객체 지향 프로그래밍 경험이 많다면 반사적으로 데이터를 숨기고, 객체 안에 캡슐화한다고 느낄 것이다. 이런 객체들은 서로 이리저리 이야기하며 서로의 상태를 변경한다. 이런 방식은 결합을 많이 만들어내고 … 생략
변환 모델에서는 이런 사고를 근본적으로 뒤엎는다. 데이터를 전체 시스템 여기저기의 작은 웅덩이에 흩어 놓는 대신, 데이터를 거대한 강으로, 흐름으로 생각하라. 데이터는 기능과 동등해진다. 파이프라인은 코드→ 데이터→ 코드→ 데이터… 의 연속이다. 데이터는 더 이상 클래스를 정의할 때처럼 특정한 함수들과 묶이지 않는다. 대신 우리 애플리케이션이 입력을 출력으로 바꾸어 나가는 진행 상황을 데이터로 자유롭게 표현할 수 있다. … 생략. 어떤 함수든 매개 변수가 다른 함수의 출력 결과와 맞기만 하면 어디서나 사용하고 또 재사용할 수 있다.
오류처리는 Wrapper를 통해 처리함. 파이프라인 어디서든 오류가 나면 파이프라인 전체 값이 되게 해서 후속 함수들 진행 안 함.
Topic 31 상속세
여기서는 상속의 문제랑 그에 따른 대안 3가지를 제시함
상속의 역사에 따라 상속을 2가지 형태로 나눌 수 있긴 함. 둘 다 문제 있음 문제는 결합이 많아진다는 거임.
- 코드 공유를 위한 상속
- 타입 정의를 위한 상속
대안 3가지
인터페이스와 프로토콜
자바에서 인터페이스 쓰는 걸 볼 수 있음. 인터페이스는 텅 비어있고 이걸 구체적으로 구현하면서 클래스를 정의함. 이 둘이 강력한 까닭은 이들을 타입으로 사용할 수 있고, 해당 인터페이스를 구현하는 클래스는 타입과 호환이 됨. 그래서 다형성 인터페이스로 표현하는 게 좋음
위임
위임은 보통 상속 대신 사용하는 대안으로 대표적인데, 새로 작성하려는 클래스에서 기능이 필요한 클래스의 인스턴스를 호출해서 사용함. 근데 이렇게 하면 보통 반복되는 코드가 많아짐 그래서 믹스인이 있음
믹스인과 트레이트
특정 클래스의 필요한 기능만 묶어서 새로 작성되는 클래스들을 확장 가능하게 해 줌. 검증 코드 같은 거 만들 때 많이 쓰임
Topic 32 설정
기본적으로 애플리케이션이 바뀔 수도 있는 값에 코드가 의존한다면 그 값을 외부에서 관리하라고 한다. 그것들을 모은 게 설정 파일이 될 것이다. 보통 정적 설정을 많이 쓴다. 환경변수 파일 말하는 거 같다. 근데 여기서는 이 설정값들도 API뒤로 숨기라고 한다. 서비스형 설정이라는 방식을 설명한다. 이렇게 하면 몇 가지 장점이 있다.
- 여러 애플리케이션이 설정 정보 공유 가능함. 인증과 접근 제어를 붙여서 애플리케이션마다 보이는 정보가 다르게 만들 수 있음.
- 여러 인스턴스에 걸쳐서 전체 설정을 한 번에 바꿀 수 있음.
- 설정 데이터를 전용 UI로 관리 가능
- 설정 데이터를 동적으로 변경 가능
이걸 보고 생각해 봤는데 모노리식 서비스 아키텍처 보다 마이크로서비스 시스템에 필요할 거 같다는 생각이 들었다. 그래서 나는. env나. json 같은 건 많이 봤는데 서비스형 설정은 많이 낯설다.
동시성
여기서는 동시성과 병렬성에 대해서 다룸. 시간적 결합이라는 개념이 나옴. 꼭 필요하지 않은 일 처리 순서를 코드가 강제할 때 생긴다고 함. 동시성이나 병렬성이 어려운 이유는 프로그래밍을 순차적 시스템으로 배워서 그렇기도 하고 가장 큰 문제는 ‘공유 상태’ 때문이라고 함. 공유 상태를 우회하는 방법을 뒤에 토픽에서 다룬다
Topic 33 시간적 결합 깨트리기
우리는 보통 특정 메서드 A 다음에 메서드 B가 호출되어야 한다 와 같이 생각함 그런데 이런 과정은 현실과 많이 동떨어져 있다고 함. 동시성을 확보해야 유연성도 얻을 수 있고 아키텍처, 설계, 배포와 같은 여러 측면에서 시간과 관련된 의존성을 함께 줄 일 수 있다.
먼저 동시성을 찾는 게 우선이다. 간단하게 활동 다이어 그램 같은 표기법으로 작업 흐름 기록하는 것도 한 방법이라고 함. 작업들 간에 의존성이 꼭 필요한지 아니면 독립적으로 실행 가능한지를 구분할 수 있어야 함. 이런 작업으로 전체 과정을 최적화할 수 있음.
동시성은 소프트웨어 동작 방식이고, 병렬성은 하드웨어가 하는 것임. 위에서 말한 것처럼 동시 작업이나 병렬 작업을 해서 이득 볼 수 있는 부분을 찾았으면 다음 토픽들에서 어떤 식으로 구현할지 알려준다.
Topic 34 공유 상태는 틀린 상태
동시적으로 작업을 하려고 하면 문제가 되는 건 공유 상태이다. 이런 문제 해결방안으로는 세마포어 같은 상호 배제 방법이 있다. 세마포어를 사용해서 코드를 작성하는 예시를 보여주는데, 한 가지 생기는 문제가 세마포어를 사용하면 모든 요청자가 세마포어를 사용해야만 제대로 동작함. 누군가 깜빡한다면 제대로 동작하지 않음.
여기서는 예시 상황을 보여주는데 나는 일반적인 이야기로 정리했다.
코드예시에서 문제는 자원을 사용하려는 요청자에게 책임을 전가해서 그렇다. 제어를 중앙으로 집중시켜야 한다. 하나의 호출로 자원을 확인함과 동시에 자원을 가져가게 함. 각 동작을 한 트랜잭션으로 묶어서 원자적으로 처리했다고 보면 될 거 같다. 근데 결국 묶인 트랜잭션을 실행하는 메서드 자체도 여러 스레드에서 호출된다면 여전히 세마포어로 보호해야 한다.
여기서 더 문제가 생기는 상황은 필요한 자원이 1개가 아니라 2개면 여러 문제가 나타날 수 있다. 일단 조건이 복잡해지고 데드락 같은 상황이 생길 수 있다. 원래 책에서는 원래 쿠키랑 아이스크림을 합친 파이 알라모드라는 디저트 만드는 걸 예시로 들었는데, 결론적으로 그냥 따로 자원 점유 하는 게 아니라 파이 알라모드 메뉴 자체를 자원으로 보고 코드 작성하는 게 낫다고 한다. 그러면 각각 자원에 대한 문제 생길일이 없음. 그냥 성공 아니면 실패로 됨.
아무튼 그래서 불규칙한 문제는 보통 동시성 문제가 많다고 함. 그래서 다음 토픽에서는 공유 상태 없이 동시성을 구현하는 방법을 살펴본다.
Topic 33 액터와 프로세스
액터라는 개념은 처음 들어 봐서 좀 낯설었다. 책에서 액터는 다음과 같이 정의되어 있다.
‘액터’는 자신만의 비공개 지역 상태를 가진 독립적인 가상 처리 장치다. 각 액터는 우편함을 하나씩 보유하고 있다. 액터가 잠자고 있을 때 우편함에 메시지가 도착하면 액터가 깨어나면서 메시지를 처리한다. 처리가 끝나면 우편함의 다른 메시지를 처리한다. 만약 우편함이 비어 있으면 다시 잠든다. 메시지를 처리할 때 액터는 다른 액터를 생성하거나, 알고 있는 다른 액터에게 메시지를 보내거나, 다음 메시지를 처리할 때의 상태가 될 새로운 상태를 생성할 수 있다.
다음은 액터의 정의에서 찾아볼 수 없는 것이 몇 가지 있다.
- 액터를 관리하는 것이 하나도 없다. 다음에 무엇을 하라고 계획을 세우거나, 정보를 입력 데이터에서 최종 결과로 바꾸는 과정을 조율하는 것이 없다.
- 시스템이 저장하는 상태는 오직 메시지 그리고 각 액터의 지역 상태뿐이다. 메시지는 수신자가 읽는 것 외에는 확인할 방법이 없고, 지역 상태는 엑터 바깥에서는 접근이 불가능하다.
- 모든 메시지는 일방향이다. 답장이란 개념은 없다. 액터에서 답장을 받고 싶다면 처음 메시지를 보낼 때 답장받을 우편함 주소를 메시지에 포함해서 보내야 한다. 나중에 이 주소로 보내는 답장도 결국 또 하나의 메시지일 뿐이다.
- 액터는 각 메시지를 끝날 때까지 처리하고 중간에 다른 일을 하지 않는다. 즉, 한 번에 하나의 메시지만 처리한다.
물리적인 프로세서가 넉넉하다면 각각 액터를 하나씩 돌릴 수 있다. 프로세서가 하나뿐이라면 실행 환경이 액터마다 콘텍스트를 전환해 가면서 실행시킬 수 있다.
여기서 내 나름대로 정리한 건데 액터 시스템에서 구성요소는 크게 액터, 메시지, 지역상태로 이뤄지는 거 같다. 액터 보면서 뭔가 이벤트랑 상태 얘길 하니까 FSM이 생각이 났다. 물론 FSM이 비동기를 전제로 하고 있지 않기 때문에 다르지만 만약 여러 FSM과 비동기성이 포함되면 비슷하다고 볼 수 있지 않을까? 액터가 하나의 FSM 아닌가??. 뭔가 새로운 개념을 익힐 때는 개념 간의 바운더리를 정하는 게 약간 어려운 거 같다. 비슷해도 아무튼 다른 점을 명확히 해서 나누긴 해야 하니까.
확실히 이렇게 이벤트마다 정리해 놓고 상태 변환을 병렬적으로 처리하니까 명시적으로 어떻게 하라고 하는 코드가 없다. 물론 처음 요청하는 코드는 있겠지만. 액터 내부에 그런 게 없다.
Topic 33 칠판
칠판 시스템 이것도 신박했다. 인공지능 분야에서 많이 사용된다는데, 여러 개의 독립적인 에이전트가 중앙의 ‘칠판’이라 불리는 공유 메모리를 통해 협력하여 문제를 해결하는 방식이다.
종속성이 있는 문제라도 순서가 상관없는 거 같다. 문제가 칠판으로 들어오고 그걸 해결하는 에이전트가 해결하고, 만약 그 문제 중간에 필요한 데이터가 있다면 그 데이터를 누군가 칠판에 다시 올리고 그럼 그 이벤트를 받아서 다시 처리하고. 결국 이벤트를 통해서 종속성 문제도 해결 가능하다.
메시징 시스템에 대해 추가로 소개하는데, 위에 말한 기술들과 잘 맞는다. 메시지를 이벤트 로그 형태로 저장하여 영속성을 제공한다. 그럼 안전성이 일단 확보된다. 또한 패턴 매칭 형태로 메시지를 가져올 수 있다고 한다. 메시징 시스템이 중앙에 메시지를 저장하고 공유하는 방식으로 칠판 시스템처럼 동작한다. 그래서 액터가 메시지를 주고받는 플랫폼 역할 도 할 수 있다. 위에서 말한 에이전트가 액터로도 대체해서 생각해도 될 거 같다.
참고 및 출처: <실용주의 프로그래머(20주년 기념판)> (데이비드 토머스, 앤드류 헌트 지음, 정지용 옮김, 김창준 감수 인사이트 , 2022)
'Book' 카테고리의 다른 글
MySQL 퍼포먼스 최적화 (1~3장) (0) | 2023.08.17 |
---|---|
실용주의 프로그래머(7장 ~마지막) (0) | 2023.07.31 |
실용주의 프로그래머(3~4장) (0) | 2023.07.28 |
실용주의 프로그래머(1~2장) (0) | 2023.07.25 |
Book. 수학이 필요한 순간 (0) | 2022.02.06 |