공용체 타입은 서로 직접적인 동작은 없지만, 모두 동일한 방식으로 동작해야 하는 클래스들의 모음이 있을 때 유용합니다.
Swimlane의 소프트웨어 개발자로서 저는 때때로 저와 팀 전체에 도움이 될 뿐만 아니라 제품의 효율성을 높이는 새로운 것을 배울 기회를 얻습니다. 최근 참여했던 프로젝트에서 저는 감사 로그를 추가하는 업무를 맡았습니다. 요구 사항은 데이터베이스 컬렉션에 대한 모든 변경 사항(예: 생성, 업데이트, 삭제)을 AuditLog라는 새 컬렉션에 기록하고 각 레코드에 발생한 작업을 표시하는 것이었습니다. 간단해 보이죠?.
하지만 업데이트 작업에는 문제가 있었습니다. 워크플로 엔진의 여러 객체는 각기 다른 방식으로 업데이트될 수 있기 때문입니다(예: 사용자 활성화, 비활성화 또는 로그인, 플러그인 업그레이드). 또한 업데이트 함수 자체에 로깅 로직을 추가하고 싶지 않았습니다. 문서가 수정되기 전과 후의 상태를 인수로 받는 함수를 작성해야 했습니다. 그런 다음 문서의 변경 사항을 기반으로 해당 문서를 수정한 작업을 판별해야 했습니다. 그런데 '수정 전'과 '수정 후' 인수는 어떤 데이터 유형이어야 할까요?
클래스 AuditLogsService { async log(before: ???, after: ???): Promise } { //... } }
한 가지 해결책은 인터페이스를 사용하는 것이었습니다. 우리 문서들은 아직 기본 클래스나 인터페이스를 구현하고 있지 않으므로, 이 방법은 간단해 보였습니다. BaseDocument 인터페이스를 만들고 8개의 문서 클래스 모두가 이를 구현하도록 하면 됩니다. 그러면 'before'와 'after' 속성을 해당 인터페이스에 지정할 수 있습니다.
인터페이스 AuditLogable {}
그 해결책의 문제점은 문서 클래스들이 공통 속성이나 메서드를 전혀 공유하지 않기 때문에 인터페이스가 무의미해진다는 것입니다. 정확히 말하자면, "id" 필드는 공유합니다. 결국, 단 하나의 속성과 메서드도 없는 인터페이스를 구현하기 위해 여덟 개의 서로 다른 클래스를 수정해야 한다는 뜻입니다.
인터페이스 AuditLogable { id: 문자열; }
인터페이스에 log 메서드를 추가하고 각 문서 유형이 자체 로깅 동작을 정의하도록 하는 건 어떨까요? 그게 바로 제대로 된 객체 지향 방식 아닌가요? 하지만 이제 거의 비슷한 동작을 하는 로깅 메서드를 여덟 개나 구현해야 합니다. DRY(Don't Repeat Yourself, 반복하지 않기) 원칙을 지키기 위해 인터페이스 대신 기본 클래스를 사용하는 게 낫지 않을까요? 각 로깅 메서드의 동작이 각각 다르기 때문입니다. 주로 비슷합니다. 예를 들어 플러그인은 "생성" 작업이 없는 유일한 문서 유형입니다. 대신 "설치"라고 부릅니다.
제가 이 문제를 거꾸로 접근하고 있었다는 것을 깨달았습니다. 문서 자체가 감사 로깅을 수행하는 것이 아니라, 문서에 감사 로깅이 적용되도록 해야 합니다. 그렇게 하면 의미 없는 인터페이스를 구현하거나 복잡한 추상화를 만들 필요 없이 모든 기능을 하나의 함수로 작성할 수 있습니다.
다행히 TypeScript는 이 패턴을 구현하는 간단한 방법인 유니온 타입을 제공합니다. 유니온 타입을 사용하면 속성이나 동작을 추상화할 필요가 없습니다. 효과적으로 말하자면,
“"인수 'before'는 감사 로그에 기록될 수 있는 모든 유형 중 하나입니다." type AuditLogable = User | Plugin | Playbook; // … class AuditLogsService { async log(before: AuditLogable, after: AuditLogable): Promise { //... } }
런타임에 타입 정보가 손실되기 때문에, 공용체 내에서 어떤 타입을 before 및 after 인수로 전달할지 나타내는 추가 매개변수를 전달해야 합니다.
enum SwimType = { user, plugin, playbook, // ... } class AuditLogsService { async log(before: AuditLogable, after: AuditLogable, type: SwimType): Promise { //... } }
이제 단일 함수와 새로운 유니온 타입을 사용하여 감사 로깅을 구현했습니다. 로깅되는 문서는 수정할 필요가 없으므로, 로거에 새 클래스를 추가하는 것은 AuditLogable에 추가하고 단일 로깅 함수에 필요한 추가 동작을 추가하는 것만큼 간단합니다. 모든 것이 테스트하기 쉽고 이해하기 쉽습니다.

