요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

프론트엔드 개발자가 API를 설계하는 이유

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

국내 유명 IT 기업은 한국을 넘어 세계를 무대로 할 정도로 뛰어난 기술과 아이디어를 자랑합니다. 이들은 기업 블로그를 통해 이러한 정보를 공개하고 있습니다. 요즘IT는 각 기업의 특색 있고 유익한 콘텐츠를 소개하는 시리즈를 준비했습니다. 이들은 어떻게 사고하고, 어떤 방식으로 일하는 걸까요?

 

이번 글에서는 성형수술 및 피부 시술 정보 제공 플랫폼 ‘강남언니’가 병원 관리 소프트웨어 KOS(이하 KOS)를 개발하면서, 새로 도입한 인터페이스 정의 언어를 이용한 인터페이스 관리 전략을 소개합니다.

 

인터페이스 정의 언어

B2B SaaS 애플리케이션 개발을 시작하면서 KOS팀은 인터페이스 정의 언어(이하 IDL, Interface Definition Language)를 도입하게 되었습니다. IDL은 소프트웨어의 인터페이스를 정의하는 명세 언어를 말하고, 특정한 언어에 국한되지 않는 방법으로 인터페이스를 정의하여 애플리케이션들의 언어가 다르더라도 동일한 인터페이스로 통신할 수 있도록 합니다.

 

여러 종류의 IDL 중, KOS팀은 Google Protocol buffers(a.k.a. Protobuf)를 선택했습니다. KOS팀이 Protobuf를 선택한 이유는 다양한 언어로 Generate 할 수 있다는 특징이 있고, 무엇보다 인터페이스 작성이 매우 쉽습니다. 그리고 추후 gRPC라는 강력한 경량 통신을 사용할 수 있기에 도입했습니다. KOS팀은 IDL을 도입함으로써, 제품 개발 프로세스에 있던 비효율을 해결할 수 있었습니다.

 

IDL을 도입하기 전의 제품 개발 프로세스입니다.

  1. 프로덕트 오너프로덕트 디자이너가 이터레이션을 계획하고 와이어프레임을 만듭니다.
  2. 모든 팀 구성원이 참여해 이터레이션 계획 스펙을 확정합니다.
  3. 프로덕트 디자이너가 제품을 디자인합니다.
  4. 백엔드 개발자가 API를 설계하고 노션 문서에 API 명세를 작성(또는 Swagger)합니다.
  5. 프론트엔드 개발자가 해당 인터페이스가 계획된 제품 구현을 반영할 수 있는 스펙인지 확인합니다.

 

위 프로세스의 4, 5번 과정에서 비효율적인 문제들이 발생하곤 했는데요. 자세하게 어떤 비효율적인 문제가 있었고, IDL을 통해 어떻게 해결했는지에 대해 이야기해보겠습니다.

 

 

비효율적인 문제들

1. 유지보수 되지 않는 API 문서

<출처: 강남언니>

 

IDL을 도입하기 이전 관리되던 API 문서의 일부입니다. 위에서 언급한 과정대로 백엔드 개발자가 노션 문서에 통신하는데 필요한 통신 규약들에 대해 정의합니다. 그 뒤, 변경사항이 생긴 이후에는 변경을 인지한 개발자가 문서를 수정을 하게 되는데요. 이때 해당 API 문서는 현재 구현된 코드가 제대로 반영된 상태인지 알기 어렵고, 응답 필드가 많으면 많을수록 어떤 불일치가 있는지 찾아내기 어렵습니다. 이렇듯 시스템이 커질수록 API의 수는 계속 늘어날 것이고, 모든 통신 규약을 노션과 같은 외부 문서로 유지보수하는 데는 꽤 많은 노력을 필요로 하게 됩니다.

 

2. 시스템 관점의 인터페이스 설계

백엔드 개발자가 인터페이스를 주도하여 설계할 때, 데이터 무결성 및 시스템 성능 최적화와 같은 시스템적인 부분이 가장 먼저 고려되는 경우가 많습니다. 이는 기술적 측면에 있어 분명 필수적인 고려 사항이지만 성능에 초점이 맞춰지다 보니 오버 스펙을 가진 인터페이스가 정의될 여지가 생기며, 이로 인해 프론트엔드 개발자와 이를 통합하기 위해 지속적인 합의를 하게 됩니다. 해당 과정에서 커뮤니케이션 오버헤드가 발생하고 운영 비용이 높아질 수 있다는 문제가 있습니다. 이제 이러한 비효율적인 문제를 해결하기 위해 어떠한 변경들이 있었는지 설명드리겠습니다.

 

 

해결

프론트엔드 주도로 인터페이스를 설계한다

프론트엔드 개발자가 API를 설계하고 백엔드 개발자는 설계된 해당 인터페이스가 제품 구현을 반영할 수 있는지 리뷰하고 변경을 요청합니다.

 

백엔드 주도로 인터페이스를 설계하면서 직면한 문제를 대응하기 위해 설계 프로세스를 변경했습니다. 이제 UI와 UX 디자인의 복잡성에 대해 더 잘 알고 있는 프론트엔드 개발자가 인터페이스 설계를 주도하여 최종 엔드 유저의 요구 사항과 기대에 부합하는 방식으로 인터페이스를 설계합니다.

 

또한, 이 변경된 프로세스를 통해 백엔드와 프론트엔드 개발자 간 지식 전파도 자연스럽게 이어집니다. 예를 들어, 사용자 경험에 미숙할 수 있는 백엔드 개발자와 시스템 관점에서 생각하기 어려울 수 있는 프론트엔드 개발자 간의 지식 전파가 상호 리뷰를 통해 자연스럽게 만들어지는 효과를 거둘 수 있었습니다. 결론적으로, 프론트엔드 주도로 인터페이스를 설계하는 방식의 전환은 단순한 개발 프로세스의 전환뿐만 아니라 사용자 경험을 우선시하는 문화적 전환이라고 볼 수 있습니다.

 

 

프론트와 백엔드 애플리케이션 코드에 IDL을 적용한다

인터페이스로 정의된 Protobuf 코드를 서버와 클라이언트에서 정의하여 동일한 통신 계약 모델을 사용합니다. 아래에서 실제 Protobuf로 정의된 Message를 이용하여 백엔드와 프론트엔드에서 어떻게 관리되는지 알아보겠습니다.

 

e.g. 예약 API

message ReservationOption {
  string optionId = 1;
  string optionTitle = 2;
}

message ReserveCommand {
  string visitorId = 1;
  repeated ReservationOption options = 2;
  optional string memo = 3;
  string startDateTimeUtc = 4;
  string endDateTimeUtc = 5;
}

message ReserveCommandResponse {
  string reservationId = 1;
}

service ReservationController {
  rpc reserve(ReserveCommand) returns (ReserveCommandResponse) {
    option (google.api.http) = {
      post: "/tenants/{tenantId}/schedule/reservation/v1/commands/reserve"
      body: "*"
    };
  }
}

 

(병원에서 고객 예약을 생성하는 인터페이스의 일부입니다.)
위처럼 Protobuf로 정의된 메시지를 protoc로 컴파일을 하면, 아래와 같이 코드가 생성됩니다.

 

참고

  • IDL을 정의하기 위해 사용하고 있는 언어인 Google Protocol Buffers는 protoc라는 컴파일러를 통해 여러 언어(Java, C++, C#, …)로 Generate 할 수 있습니다. 또한, 위 Generate 된 코드를 KOS팀은 Git Submodule로 각 애플리케이션에서 Import 하여 사용하고 있습니다.

 

TypeScript Code로 변환된 인터페이스

export interface ReserveCommand {
  visitorId: string;
  options: ReservationOption[];
  memo?: string | undefined;
  startDateTimeUtc: string;
  endDateTimeUtc: string;
}

export interface ReserveCommandResponse {
  reservationId: string;
}

export const ReserveCommand = {
  encode(message: ReserveCommand, ...) {
    if (message.name !== "") {
      //...
    }
  },
  decode(input) {
    //...
  },
  fromJSON(object: any): ReserveCommand {
    return {
      visitorId: isSet(object.visitorId) ? String(object.visitorId) : "",
      //...
    }
  }
  toJSON(message: ReserveCommand): unknown {
    //...   
  }
}

export class ReservationControllerClientImpl implements ReservationController {
  private readonly rpc: Rpc;
  private readonly service: string;

  constructor(rpc: Rpc, opts?: { service?: string }) {
    //...
  }

  reserve(request: ReserveCommand): Promise<ReserveCommandResponse> {
    const data = ReserveCommand.encode(request).finish();
    const promise = this.rpc.request(this.service, "reserve", data);
    return promise.then((data) => ReserveCommandResponse.decode(new _m0.Reader(data)));
  }
}

 

이제 프론트엔드 개발자는 IDL에 정의된 Interface를 Submodule로부터 Import 하여 코드에 반영합니다. 물론, 백엔드 개발자도 동일하게 해당 Interface를 Import 하여 반영합니다.

 

예제 코드로 IDL 적용 방법 알아보기 (with Client/ Server)

이제 IDL에 명시한 예약 생성의 코드가 각 애플리케이션 언어에 맞게 생성되었으니, 실제 클라이언트와 서버 간 위 코드를 어떻게 적용하여 사용하는지 의사 코드(pseudo-code)로 살펴보겠습니다.

 

Client Code

// TypeScript로 Generate된 Code를 Import합니다.
import {
  ReserveCommand,
  ReserveCommandResponse,
  ReservationController
} from '@/idl/gen/pb-typescript/reservation';

// 프론트에서 API 호출할 때도, IDL에 정의한 Controller부를 가져와서 선언해줍니다.
const ReservationApi: ReservationController = {
  reserve(command: ReserveCommand): Promise<ReserveCommandResponse> {
    return axios.post(
      '/tenants/:currentTenantId/reservation/v1/commands/reserve',
      ReserveCommand.toJSON(command)
      ).then(ReserveCommandResponse.fromJSON);
  },
  //...
};

 

Server Code

//Java로 generate된 code를 Import합니다.
import protos.reservation.ReserveCommand;
import protos.reservation.ReserveCommandResponse;

//Server에서도 IDL에 정의한 Controller에 맞춰서 선언해줍니다.
@PostMapping("/tenants/{tenantId}/reservation/v1/commands/reserve")
public ReserveCommandResponse reserve(
    @RequestBody ReserveCommand command
) {
    return reserveCommandExecutor.execute(command);
}

 

위 클라이언트와 서버의 Import 부를 보면 알 수 있듯, 이제 클라이언트와 서버는 항상 동일한 통신 계약 모델을 사용하게 됩니다. 이로써 문제 파트에서 정의한 유지보수 이슈에 대해서 많은 부분을 소화할 수 있게 되었습니다.

 

 

Server to Server Contract, Event Contract 문제 해결

KOS팀은 현재 MSA 구조로 많은 서비스를 운영하고 있습니다. 각 서비스 간 느슨한 결합을 위해, 이벤트 드리븐 아키텍처를 적절하게 사용하여 만들어지고 있는데요. 이때 많은 도메인 서비스들에서 발행/ 소비되는 이벤트의 계약 모델, 그리고 Server to Server로 이루어진 통신 규약들에 대해서도 문서로만 관리된다면, 하나의 서비스에서 변경이 이루어졌을 때 어디서부터 불일치가 발생했는지 찾는데 많은 리소스가 들게 됩니다.


따라서 각 서비스들 간에서도 위의 문제 섹션에 명시한 동일한 문제를 겪을 수 있게 됩니다. 그러나 각 서비스에서 IDL로 정의된 동일한 인터페이스를 사용한다면, 여러 도메인 서비스에서 발행/ 소비되는 이벤트 계약 모델과 서버 간 통신 규약들에 대해서도 동일한 인터페이스를 가져갈 수 있다는 해결 방법이 생깁니다.

 

이제 읽으시면서 궁금증이 들었을만한 부분들을 Self QnA를 통해 알아보면서 마무리 지어보겠습니다.

 

 

QnA

1. IDL 소스는 어떻게 관리하고 있나요?

<출처: 강남언니>

 

KOS팀은 IDL을 Git에 독립 리포지토리로 만들어서 정의한 뒤, 사용하는 애플리케이션에서 Git Submodule을 이용하여 각 애플리케이션 리포지토리에 불러와 사용하고 있습니다. 또한, 각 애플리케이션에서 Generate 된 코드를 Submodule을 통해 관리하는 이유는 Protobuf가 Protoc, Plugin 버전에 따라 생성된 결과물이 다를 수 있기 때문에 모든 파일을 각 애플리케이션 별 독립된 리포지토리에서 관리하고 있습니다.

 

2. 프론트엔드와 백엔드 간 인터페이스 합의는 어떤 방식으로 하고있나요?

<출처: 강남언니>

 

위는 전체적인 프로세스를 나타낸 도식표입니다. KOS팀은 Code Review 단계에서 인터페이스를 상호 합의를 하고 있는데요. 아래와 같은 방식으로 진행됩니다.

 

<출처: 강남언니>

 

작업에 관련된 프론트엔드와 백엔드 개발자들이 리뷰를 통해 UI/ UX를 반영할 수 있는 데이터 구조인지, 혹은 이 설계가 도메인 바운더리를 위배하고 있진 않는지, 그리고 최종적으로 시스템적으로 구현하는데 문제가 없는 인터페이스인지 확인하는 절차를 가집니다. 이제 PR을 병합한 뒤, Github Actions의 워크 플로우가 트리거 되어 GitHub Bot이 각 애플리케이션의 언어들로 Generate 시켜 메인 브랜치에 커밋하는 방식으로 관리되고 있습니다.

 

3. 그럼 Protobuf를 사용하면서 느낀 단점들은 없었나요?

  • Null Handling이 번거롭다.
    • Protobuf로 생성된 코드는 Null을 할당하면 NPE가 발생하도록 설계되어 있습니다.
    • 따라서, Nullable 한 특성을 갖고 있는 언어로 개발할 때는 Protobuf로 생성된 코드로 변환 시 Null이 들어갈 수 있는 속성인지 확인 후 반드시 Null Handling을 구현해주어야 합니다.
    • 아래는 상담 엔터티(Java)를 상담 계약모델(Protobuf)로 변환하는 과정을 나타낸 예시 코드입니다.

 

message ConsultationContract {
  Counselor counselor = 1;
  optional string consultationMemo = 2;
}
    
message Counselor {
  string id = 1;
  string name = 2;
}
import com.healingpaper.solution.reservation.ConsultationEntity;
import protos.reservation.ConsultationContract;
    
private ConsultationContract convertToContract(
    ConsultationEntity consultation
) {
    return ConsultationContract.newBuilder()
        //counselor는 required한 속성으로, NPE가 발생하지 않습니다.
        .setCounselor(consultation.getCounselor())
        //만약 consultationMemo의 값이 Null이였다면, NullPointException이 발생하게 됩니다.
       .setConsultationMemo(consultation.getConsultationMemo())
       .build();

    // ==================================================

    //따라서, NPE 방지를 위해 상담 메모를 넣어줄 때, Optional Handling 로직을 추가해야합니다.
    return ConsultationContract.newBuilder()
        .setCounselor(consultation.getCounselor())
            .setConsultationMemo(
                Optional.ofNullable(consultation.getConsultationMemo)
                .orElse("")
            )
        .build();
    }

 

  • 위 예시에서 consultationMemo와 같이 Optional 한 속성을 가진 프로퍼티에 만약 Null Handling 로직이 없었다면, NPE 발생으로 시스템 장애로 이어질 수 있습니다.
    • 따라서, 해당 값이 Optional 한 값인지 체크가 반드시 필요하며 Null Handling을 해줘야 하는 번거로움이 있습니다.

 

4. Swagger를 채택하지 않은 이유는 무엇인가요?

Swagger는 현재 많은 기업들이 사용하고 있는 안정성 있고 다양한 기능들이 내포된 유용한 도구로 알려져 있습니다. 그러나 KOS팀에서 채택하지 않은 결정적인 이유는 백엔드 개발자에게 종속된 API Document가 될 수 있기 때문이었는데요. 인터페이스 문서의 주체가 백엔드 개발자에게 강하게 종속되어 있는 경우 어떤 문제들이 발생할 지에 대해 나열해 보면 다음과 같습니다.

 

  • Swagger API Document가 나오기 전까지 프론트엔드 개발자의 업무 블락커가 생길 수 있다.
    • 백엔드 개발자가 Swagger를 사용하기 위해 애플리케이션에 문서 자동화 기반 작업을 하기 전까지 프론트엔드 개발자는 어느 인터페이스가 나올지 예상하기 어려움으로, 프론트엔드 개발자의 업무에 블락커가 생길 수 있습니다.
  • 인터페이스에 대한 변경이 생길 경우, 정황상 백엔드 개발자의 손길로만 수정이 가능한 구조가 된다.
    • 사용하는 애플리케이션 언어에 따라 다를 수 있겠지만, 예를 들어 TypeScript로 개발하는 프론트엔드 개발자가 Spring에 적용된 Swagger 정보를 수정하는 건 쉽지 않을 수 있습니다.
    • 이는 곧, 1번에서 말한 내용과 같이 설계의 주체가 백엔드 개발자에게 모두 종속되어 수정이 필요한 경우에도 수정사항이 반영되기 전까지 업무 블락커가 동일하게 발생할 수 있는 문제가 존재합니다.
  • 단일 진실 공급원(Single Source Of Truth) 관리가 안된다.
    • Swagger를 사용할 경우 인터페이스의 원천에 대해서 파편화가 이뤄질 수밖에 없습니다. 게다가 MSA 환경으로 이루어진 서비스라면 더욱이 각 애플리케이션 별로 파편화되는 것을 막기는 쉽지 않을 수 있습니다.

 

 

마치며

지금까지 강남언니 KOS팀에서 도입 후 모두가 만족해하고 있는 IDL의 사용 전략에 대해 소개드렸습니다. IDL을 이용하여 클라이언트와 서버 간 통신 규약을 코드로 명시하고 형상 관리를 통해 변경 기록을 관리할 수 있다는 특징 하나만으로도 충분히 매력적이나, 이를 모든 애플리케이션에서 동일한 통신 계약 모델을 강력하게 강제하여 사용할 수 있다는 특징이 저는 가장 강력한 무기라고 생각이 듭니다.


혹시 비슷한 문제 상황을 겪고 있는 팀이라면, 이번 글을 통해 조금이나마 해결에 도움이 되셨으면 좋겠습니다. 마지막으로 KOS팀에서는 이 외에도 빠르고 효율적인 개발을 위해 또 다른 여러 도구와 방식들을 채택하고 있습니다.

 

<원문>

[SaaS] 프론트엔드 개발자가 API를 설계하는 이유

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 5
acepm83
            공감안됨
          
2023.11.20. 오후 20:12
작가
8
명 알림 받는 중

작가 홈

작가
8
명 알림 받는 중
누구나 더 좋은 의료서비스를 누릴 수 있게 만드는 것을 꿈꾸고 있습니다. 강남언니의 목표를 빠르고 가치있게 달성하기 위해 끊임없이 일하는 방식과 조직문화를 관찰하고 변화하고 있습니다.

좋아요

댓글

스크랩

공유

공유

지금 회원가입하고,
요즘IT가 PICK한 뉴스레터를 받아보세요!

회원가입하기
요즘IT의 멤버가 되어주세요! 요즘IT의 멤버가 되어주세요!
요즘IT의 멤버가 되어주세요!
모든 콘텐츠를 편하게 보고 스크랩해요.
모든 콘텐츠를 편하게 보고 스크랩 하기
매주 PICK한 콘텐츠를 뉴스레터로 받아요.
매주 PICK한 콘텐츠를 뉴스레터로 받기
로그인하고 무료로 사용하기