처음에는 두 거래 기능이 차트와 주문 버튼, 보유 목록을 공유해 하나의 화면으로 관리하면 효율적이라고 생각했다. 공통 코드가 많았고 외형도 비슷했다. 기능이 늘자 한쪽에서만 필요한 조건이 if로 계속 붙었고, 다른 쪽을 수정할 때 예상하지 못한 영향이 생겼다.
파일을 둘로 복사하는 건 해결이 아니었다. 정말 같은 부분과 업무 규칙 때문에 달라지는 부분을 먼저 찾아야 했다.
변경 이유를 기준으로 나눴다
차트 렌더링과 연결 상태 표시는 두 기능에서 같은 이유로 바뀌었다. 주문 가능 조건, 만료 방식, 결과 표시는 서로 다른 정책으로 움직였다.
공통 기반: 연결 상태, 차트 표시, 계좌 선택
기능 A : 주문 만료와 결과 처리
기능 B : 포지션, 증거금, 청산 관련 표시
업무 용어까지 억지로 공통 모델에 넣으면 nullable 필드와 조건문이 늘어났다.
공통 인터페이스는 작게 유지했다
화면이 꼭 필요한 행동만 계약으로 두고 기능별 서비스가 구현했다.
public interface ITradingFeature
{
Task LoadAsync(Account account);
bool CanPlaceOrder(OrderInput input);
Task<OrderResult> PlaceAsync(OrderInput input);
}
실제 주문 데이터와 검증 결과는 기능별 타입으로 더 나뉘었다. 공통 인터페이스를 모든 차이를 숨기는 상자로 만들지 않았다.
화면 전환보다 상태 분리가 먼저였다
씬이나 프리팹만 나누고 전역 상태를 그대로 공유하면 한쪽의 선택 상품과 요청 결과가 다른 쪽에 남았다. 기능별 상태 저장소와 요청 생명주기를 분리하고 화면을 떠날 때 구독과 진행 중 요청을 정리했다.
공통 차트는 입력 데이터만 받아 표시했고 어느 거래 기능에서 왔는지 몰라도 됐다. 덕분에 차트 수정은 두 기능에 같이 적용하면서 주문 정책은 서로 독립적으로 바꿀 수 있었다.
분리 뒤에는 두 기능을 오가며 선택 상태, 연결 구독, 진행 중 요청이 새 경계를 넘지 않는지 확인했다. 공통 코드를 나누는 것보다 종료 시 정리되지 않은 상태를 찾는 데 시간이 더 들었다. 구조 변경은 컴파일 성공만으로 끝낼 수 없었다.
중복보다 잘못된 결합이 더 비쌌다
코드를 나눈 뒤 비슷한 몇 줄이 생겼다. 예전에는 중복이 보이면 실패처럼 느꼈지만, 자주 다른 이유로 바뀌는 코드를 하나로 묶어 조건문을 늘리는 비용이 더 컸다.
이 작업을 통해 공통화는 현재 코드 모양이 아니라 앞으로 함께 바뀔 가능성을 기준으로 해야 한다는 걸 배웠다. 비슷해 보이는 화면을 나누는 결정은 파일 정리보다 기능의 경계를 인정하는 일이었다.