포인트3 테크니컬 하우스

좋은 소프트웨어

김재우
김재우May 1, 2024
6 min read|

Software?

전세계 개발자 수는 매 5년마다 2배씩 늘어난다고 합니다 (O(2n)O(2^n) 이 되겠네요 ㅎㅎ).

현재 개발자 인구가 대략 2천 9백만명인데, 거꾸로 추산해보면 1930년대에는 전세계에는 고작 100명의 개발자가 있었다고 말할 수 있어요. 2차 세계대전때 튜링의 컴퓨터가 처음 등장하고, 나치의 에니그마가 그 시절의 산물임을 생각해보면 얼추 정확한 수치라고 볼수 있을것 같아요.

이 시절 소프트웨어는 하드웨어와 강하게 결합되어 있었고, 연산 명령의 주체는 다름 아닌 사람이였어요. 연산을 위한 기계 로서 의미를 가지던 컴퓨터는 사람이 운영하고 직접 명령을 내려야 했어요. 컴퓨터를 맞추면 윈도우를 깔고 화려한 인터페이스를 통해 앱을 실행하는 현재의 컴퓨터 이용방식에 비하면 1930-40년대의 컴퓨터는 이용하기 많이 까다로웠던거죠.

(거의) 첫 컴퓨터라고 불릴 수 있는 튜링의 Turing Bombe 의 경우, 사람이 직접 테이프의 위치를 옮겨서 기계에 명령을 입력하는 방식으로 작동했었는데, 이 방식으로 결제를 한건한건 처리해야한다고 생각하면… 이건 말이 안되겠죠?

이미테이션 게임의 튜링 컴퓨터
이미테이션 게임의 튜링 컴퓨터

트렌지스터가 등장하고, 연산에 전자적 메커니즘을 결합하면서 연산속도는 기하급수적으로 늘어났고, 비교적 쉽게 연산장비를 마련할 수 있는 환경이 마련된 1950년대에 이르러서야 수학자, 그리고 과학자들이 연구에 쉽게 이용할 수 있도록 명령을 정형화하여 기계에 전달할 수 있는 기법들이 연구되기 시작해요. 기계는 많아지는데, 운용하기 위한 명령 전달 체계가 필요해진거죠. 이 명령 전달 체계가 소프트웨어인 겁니다!

소프트웨어의 사명(?)

각 기기별 명령 체계를 각각 만드는것이 소프트웨어의 초기 모습이였어요. 3대의 다른 컴퓨터에는 3개의 운용체계가 있고, 각각 명령을 전달하는 메커니즘은 다른거죠. 하드웨어의 스팩과 기능에 정말 맞춤화된 소프트웨어는 성능은 높았지만, 새로운 하드웨어 모델을 만들때마다 새로 운용체계를 만드는건 비효율적이였어요. 하드웨어의 발전속도가 어마무시해지는 만큼 새로 생산되는 모델의 수도 무수히 많아지게 된거죠. 이제 몇몇 하드웨어 개발자들은 통일된 명령 전달 체계를 구상하기 시작하고, 어셈블리 언어를 거쳐 제일 범용적으로 사용되게되는 고수준 (지금은 저수준이죠…) 언어 인 C 가 등장하면서 소프트웨어를 통합하는 시도가 성공하게 됩니다.

명령 전달 체계가 통일되면서, 소프트웨어는 하드웨어에 (비교적) 종속되지 않고, 플러그인(Plug-in) 형식으로 창발적으로 개발되기 시작합니다. 이젠 IBM 개발자가 애플로, 애플의 개발자가 IBM으로 이직하기 쉬워지고, 또 프리랜서로 소프트웨어를 개발하는 사람이 많아지는 대 오픈소스의 시대가 열리게 된거죠.

오픈소스 시대는 소프트웨어가 최대한 범용적이어야하고, 변경과 수정, 그리고 플러그인으로서 다른 소프트웨어와 결합과 분리가 쉬워야한다는 새로운 정의이 내려지게되는 가장 큰 배경이였어요.

소프트웨어는 변경과 수정이 쉽고, 서로 결합과 분리가 간단한 범용적인 플러그인이다. -by me

좋은 소프트웨어

객체지향

💡
이 글은 독자가 객체지향 프로그래밍에 어느정도 익숙하다는 가정 속에 작성되었습니다. 따라서 설명이 비어있다고 느끼신다면 객체지향에 대한 선행 지식이 필요할 수 있습니다!

객체지향의 핵심이 물리적 세계를 객체라는 개념으로 치환하여 프로그램 속 프로세스를 마치 물체와 현상의 직관적 조화로 구현해내는 것에 있다고 이해하는 분이 많은 것으로 알고 있습니다. 하지만 객체지향이 바라보고 있는 지향점은 상당부분 다른곳에 있다고 생각하는데요, 바로

  • 다형성
  • 캡슐화
  • 추상화

이 3가지 특성을 객체지향의 지향점이라고 보고 있습니다.

음… 좀 어려울 수 있을 것 같습니다. 위에서 정의해본 좋은 소프트웨어를 다시 상기해볼까요?

소프트웨어는 변경과 수정이 쉽고, 서로 결합과 분리가 간단한 범용적인 플러그인이다. -by me

결합과 분리가 단순하고 간단하기 위해서 소프트웨어가 갖추어야 할 특징이 있을까요?

좀 엽기적인 발상을 해보고 싶어요. 대체되기 힘든 남자친구 (혹은 여자친구)는 어떤 관계여야 할까요? 음… 일단 기억에 좀 많이 남아야 할것 같구요. 그리고 서로 공유하는 기억이나 추억이 많으면 헤어졌을때 다른 사람을 만나기 힘들것 같습니다. 한마디로 요약해야 한다면 저는 공유하고 있는게 많은 남자친구 (여자친구) 대체되기 힘들다- 라고 말할 것 같습니다.

대체되거나 분리하기 힘든 소프트웨어란 어떤걸까요? 대체되기 힘든 남자친구와의 관계에서 쉽게 답을 얻을 수 있는데요, 바로 접합되어 있는 두 소프트웨어가 공유하는 추억과 물건들이 많을때 생긴다고 말할 수 있겠네요.

마치 추억을 공유하듯이 서로가 서로의 함수나 메소드를 이용하고, 같은 옷, 같은 이불을 사용해왔던것 처럼 서로가 서로의 변수를 이용해왔다면 둘은 분리하고 대체하기 힘든 소프트웨어가 되지 않을까요? 아마 분리해도 제대로 동작하기 어려울 것 같습니다. 아마 다른 새로운 소프트웨어를 가져와도 결합시키기 힘들겠죠. (왜 눈물이…)

이런 결합과 분리의 가능성을 열어두기 위해서 소프트웨어는 내부적으로 숨겨야 할 정보, 그리고 공개해야 하는 정보를 명확하게 구분하고 있어야 합니다. 회사에서 공유해야하는 나의 모습과 가족에게 보여주어야 하는 나의 모습이 달라야 하는것 처럼, 소프트웨어도 서로에게 공유할 수 있는 모습을 정형화하고 불필요한 정보는 감추는 캡슐화 가 필요합니다. 자판기는 음료를 선택할 버튼과 음료가 나오는 구멍만 알고 있어도, 내부의 각종 기계장치는 몰라도, 저는 충분히 자판기에서 음료를 뽑아먹을 수 있습니다! 그 이상, 그 이하의 정보도 필요 없는거죠.

이렇게 캡슐화가 잘 이루어지면, 비슷한 역할을 하는 소프트웨어들이 보입니다. 임원이 보는 회사원들이 비유가 될 수 있겠네요 (포인트3는 다행히 캡슐화가 잘 되지 않은 결합도 높은 팀이에요. 각기 다른 색을 가진 여러분을 항상 기다리고 있습니다 ㅎㅎㅎ)

임원이 보는 김 사원과 이 사원은 다른 얼굴을 가진 사람일 수 있을지언정, 그들에게 있어서의 각자의 역할은 비슷할 것 같습니다. 단순한 사원 인거죠. 사원이 해야할 일들을 가이드라인으로 만들고, 정해놓으면 교체도 쉽고, 임원이 보는 넓은 비즈니스 로드맵에서 사용하기도 쉽다고 생각할 수 있겠네요. (이렇게 쓰고 보니까 좀 잔인합니다…)이런 가이드라인, 혹은 정규화된 메뉴얼이 바로 인터페이스이겠구요, 이걸 추상화라고 볼 수 있겠습니다.

어쩌면 임원들도 사장에게는 단순한 사원일 수 있겠습니다. 임원들도 사원들이 사용하는 가이드라인을 따라야하긴 하니까요. 임원을 사원이지만, 조금 더 추가적인 기능이 공개된 임원 가이드라인을 따르는 소프트웨어로 정의 할 수 있지 않을까요? 이러면 임원은 사원이면서 임원인겁니다. 이렇게 소프트웨어가 하나의 추상화된 가이드라인이 아닌, 여러 가이드라인을 따를 수 있게하는 다형성 을 설명해보았습니다.

💡
객체지향의 강력한 상속의 경우도 객체지향의 특성 4가지 중 하나로 소개하는 경우도 많습니다. 하지만 필자는 상속의 특성은 위의 3가지의 강력한 특성을 보조하는 기능이라고 생각하여 3가지 특성만 소개하였습니다. 하지만, 이것은 상속을 부정하는것이 아닌, 더 넓은 의미에서 보았을때 3가지 특성에 포함 될 수 있다고 생각하여 제외하였습니다. Constructive 한 논쟁은 언제나 환영이에요! 여러분의 이야기를 듣고 싶습니다!

자, 이제 어떻게 객체지향의 3가지 특성이 좋은 소프트웨어의 조건을 대변하는지 공감되시나요! 그럼 포인트3는 어떻게 좋은 소프트웨어를 만들고자 노력하고 있을까요?

포인트3가 생각하는 좋은 소프트웨어

다양한 결제 수단의 가능성

현재 포인트3는 파격적인 수수료율의 계좌기반 결제를 유일한 결제 수단으로 제공하고 있습니다. 하지만 비즈니스 요구사항은 이렇다 할지라도, 소프트웨어가 진화해 나갈 수 있는 가능성은 열어두어야 좋은 소프트웨어라고 볼 수 있습니다. 따라서 결제 시스템 개발팀은 오픈뱅킹 기반의 계좌결제 뿐만 아니라, 다양한 결제 수단을 수용할 수 있는 확장 가능한 구조를 갖추고자 노력하고 있어요.

좋은 결제 소프트웨어는 결제 수단 변경사항에 유연하게 대처할 수 있어야 합니다
좋은 결제 소프트웨어는 결제 수단 변경사항에 유연하게 대처할 수 있어야 합니다
// payment.application.go

package payment_apps

type PaymentApplication struct {
	// ... repo 등 다른 인프라 
	paymentService       payment_domain_services.IPaymentService
	retryService         payment_domain_services.IPaymentService
	// ... logger 등 다른 인프라
}

func (a *PaymentApplication) Execute(orderId string) {
	// ... 생략된 로직

	if order.IsCheckNeeded() {
		order, err = a.paymentService.CheckPaymentStatus(order)  // (1)
	} else if order.IsReserved() && order.HasScheduledTimeArrived() {
		order, err = a.paymentService.ResolvePayment(order)  // (2)
	}

	// ... 생략된 로직

	// Retry payment
	if order.IsCheckNeeded() {
		order, err = a.retryService.CheckPaymentStatus(order)  // (3)
		if err != nil {
			a.failLogger.Log(err.Error())
		}
	} else if order.IsReserved() {
		order, err = a.retryService.ResolvePayment(order)  // (4)
		if err != nil {
			a.failLogger.Log(err.Error())
		}
	}
}
go

결제를 다루는 핵심 결제 도메인의 응용 서비스 계층 코드입니다.

이 응용 계층은 외부에서 이벤트로 받은 주문 (결제주문) 번호를 통해 주문을 가져오고, 주문 처리를 하는 역할을 하고 있습니다. Execute 메소드가 결제 처리를 하는 핵심적인 업무로직이라고 이해할 수 있겠습니다. 결제 처리 확인이 필요한 주문은 처리확인 메소드로, 처리할 시간이 된 주문의 경우 바로 처리합니다. 이후 결제 처리가 완료 되었는지 다시 확인하는 작업을 통해서 이후 재실행할지 여부를 결정하는 로직입니다.

(1), (2), (3), (4) 에서 직접 결제 처리를 하는 paymentService 와 retryService 가 눈에 띄는것 같습니다.

두 핵심적인 객체 모두 같은 IPaymentService 라는 타입을 가졌는데요,

// payment/domain/domain.service/payment.service.go
package payment_domain_services

type IPaymentService interface {
	ResolvePayment(orderItem payment.Order) (payment.Order, error)

	CheckPaymentStatus(orderItem payment.Order) (payment.Order, error)
}
go

결제처리와 결제 처리 확인 로직을 기대하고 IPaymentService를 확인해보니 좀 황당합니다. 핵심적인 결제 업무로직을 다루어야 하는데, 카드 결제인지, 계좌 결제인지, 그 어떤 정보도 보이지 않는데요, 이게 어떻게 가능한 걸까요?

주입

// main.go
package main

func main() {
	// ... 다양한 선행 작업들
	injectPaymentApplication()
	// ... 다양한 후앵 작업들
}

// ... 여러 다른 설정 함수들

func injectPaymentApplication() {
	// ... 생략된 코드들
	openbankingPaymentService := 
		openbanking_payment_domain_service_adapter.NewOpenBankingTransferService(
			openbankingRequestIdService,
			openbankingAccessTokenService,
			openbankingAPIService,
		)
	openbankingRetryPaymentService := 
		openbanking_payment_domain_service_adapter.NewOpenBankingOrderPostponeService(
			eventRegistrar,
			openbankingOrderParser,
		)
	// ... 생략된 코드들

	// payment application
	orderApp = *payment_apps.NewPaymentApplication(
		// ... 여러 다른 인자
		openbankingPaymentService,
		openbankingRetryPaymentService,
		// ... 여러 다른 인자
	)
}
go

파일을 타고타고 올라가 entry 포인트를 찾아보면 main.go 으로 올라가게 됩니다. 여기서 결제 응용 서비스가 선언되는것 같습니다.

IPaymentService 자리에 오픈뱅킹을 이용한 결제 로직이 들어가는걸 확인해볼 수 있는데요, 바로 여기에서 결제가 되는 결제 수단에 대한 정보와 로직이 주입 된다고 말할 수 있습니다.

이렇게 결제 수단에 따른 세부적인 구현사항을 IPaymentService로 추상화하고, 이 추상화된 서비스를 이용하여 다른 업무로직을 구현하게 된다면, 어떤 결제 수단으로 변경이 되어도 해당 업무로직과 결제 수단 로직은 분리되어 결합도를 낮출 수 있게 되는거죠. 이제 오픈뱅킹이 아닌, 카드를 결제수단으로 추가해야할 상황이 발생해도, 카드 결제 개발팀은 걱정없이 전체 업무 개발팀과 분리되어 개발을 진행할 수 있습니다. 전체 개발팀은 카드 개발팀의 구현 세부사항을 신경 쓸 필요도 없구요! (캡슐화)

어떤가요! 포인트3 팀은 마이크로서비스 아키택처로 각 업무 도메인 별로 분리하여 장애 대응에 확실하고 안전한 결제 시스탬 구축을 위해 노력하고 있습니다. 짧게 소개한 좋은 소프트웨어란 무엇인가를 항상 치열하게 고민하고 있는 포인트3 팀에 합류하고 싶지 않으신가요!

도메인 수준의 결합과 분리가 쉬운 소프트웨어를 만들기 위해서 서버 개발팀이 사용하는 이벤트 기반 아키택처, 그리고 정합성 보장을 위한 노력을 다음편에 소개하고자 합니다. 기대해주세요!