04-02 16:52
Notice
Recent Posts
관리 메뉴

독산구너

Fine-Grained vs Coarse-Grained 본문

카테고리 없음

Fine-Grained vs Coarse-Grained

독산구너 2024. 12. 12. 00:36

글의 목적

API를 설계하고 개발하면서 고민했던 주제인 '페이지별 API 제공'과 '공통 API 제공'에 대해 적어보고, 지금 저희 서비스에는 뭐가 적절한지 알아보고자 합니다.

 

글의 계기

백엔드 개발 인턴쉽 과정에서, DB, Architecture, API 등 서버측 소프트웨어를 처음부터 설계할 기회가 주어졌습니다. API 설계 과정에서 이사님과 논의했던 주제 중 하나가 바로 '공통 API'를 만들것인지, '페이지별 API'를 만들것인지에 대한 것이었습니다. 두 설계 방식의 장단점을 생각해보고, 지금 개발중인 서비스에는 뭐가 적절할지 이야기하면서 몇가지 인사이트를 얻을 수 있었습니다.

 

Fine-Grained API

Fine-Grained, 말 그대로 섬세한, 세분화되있는 이라는 뜻입니다. 작업의 단위를 잘게 쪼게어 다수의 호출로 원하는 데이터 집합 결과를 이루어낸다고 볼 수 있습니다.

 

장점

  1. 작고 구체적인 작업에 초점을 맞추어 재사용성이 높다.
  2. 쉽게 결합, 분리가 가능해 요구사항의 변동이 있을 때 서버 측 코드변경 없이 수정이 가능하다.
  3. 클라이언트가 여러 API를 조합해 원하는 데이터를 세부적으로 구성할 수 있다. 자유도가 증가한다. 결합도가 낮아진다.

단점

  1. 여러 API를 조합하는 과정에서 네트워크 비용이 늘어난다.
  2. 사용하지 않는 데이터를 반환받는 경우가 많아 리소스 낭비로 이어질 수 있다.
  3. 클라이언트 측에서 다양한 API를 사용하므로 구현 복잡도가 높아진다.
  4. 하나의 API가 여러군데에서 사용되므로, 디버깅이 어렵고 어떤 요청에서 문제가 일어났는지 알기 복잡하다.

 

Coarse-Grained API

 

다소 거친(영어 표현 그대로), 복합적인 작업을 처리한다고 이해할 수 있습니다. 특정 화면/기능 요구사항에 최적화된 로직/데이터를 제공합니다.

 

장점

  1. 요청 횟수가 줄어들어 네트워크 효율이 높아진다.
  2. 필요한 데이터를 한번에 가져올 수 있으며, 클라이언트 설계가 단순해진다.
  3. 특정 화면/기능을 지원하므로 디버깅이 쉬워진다.

단점

  1. 각 페이지나 화면별로 API를 설계하면서 중복된 로직이 증가할 수 있습니다.
  2. 새로운 화면/기능은, 새로운 API 설계 및 개발로 이어집니다. 확장성이 낮아집니다.
  3. 클라이언트 측과의 강한 결합도, 낮은 자유도
  4. 테스트 복잡성 증가

 

고민한 부분

이전까지는 재사용성이 높고 결합도가 낮은, 단일 책임 원칙을 지키는 API와 코드로 이루어진 소프트웨어가 좋은 소프트웨어라고 생각해왔습니다. 그렇다면 Fine-Grained 하게 소프트웨어를 개발하면 그만일 것입니다.

 

하지만 프로젝트의 특성을 고려할 필요가 있었습니다.

  1. 프로토타입 서비스 개발단계이고, 이에따라 코드의 재사용성 보다는 빠른 개발과 클라이언트 측의 단순한 설계가 우선이었습니다.
  2. 기능 장애에 빠른 대응이 가능하도록 디버깅이 쉬워야 합니다.
  3. 어떤 요청에서 어떤 응답값을 줬는지 로깅이 쉽고 로그 모니터링이 쉬워야 합니다.

이러한 특성으로 Coarse-Grained, 각 페이지/화면 별 API를 개발할 필요성이 생겼습니다.

 

 

간단한 예를 들면 다음과 같습니다.

 

요구사항: 로그인이 완료되면 가입되어있는 워크스페이스, 초대된 워크스페이스 리스트가 화면에 보이게 된다.

Fine-Grained API를 설계해본다면 이렇습니다

 

로그인 API [POST] /auth/login

// request body
{
  "email": "john.doe@example.com",
  "password": "password123"
}
// response body
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

 

가입된 워크스페이스 리스트 조회 [GET] /members/{memberId}/workspaces?type=joined

// response body
[
  {
    "workspaceId": "456",
    "name": "Project Alpha",
    "role": "Admin",
    "invitedBy": null
  },
  {
    "workspaceId": "789",
    "name": "Project Beta",
    "role": "Member",
    "invitedBy": "alice@example.com"
  }
]

 

초대된 워크스페이스 리스트 조회 [GET] /members/{memberId}/workspaces?type=invited

[
  {
    "workspaceId": "112",
    "name": "Project Gamma",
    "role": null,
    "invitedBy": "alice@example.com"
  }
]

 

 

위와 같이 설계한다면 다른 기능에서도 해당 API를 재활용하기 좋습니다.

하지만 클라이언트 측에서 한번의 호출로 모든 조합된 데이터를 사용하고자 한다면, 다음과 같이 설계할 수 있습니다.

 

로그인하면 가입된/초대된 워크스페이스 리스트를 반환하는 API [POST] /auth/login

// request body
{
  "email": "john.doe@example.com",
  "password": "password123"
}
// response body
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "joinedWorkspaces": [
    {
      "id": "456",
      "name": "Project Alpha",
      "role": "Admin"
    },
    {
      "id": "789",
      "name": "Project Beta",
      "role": "Member"
    }
  ],
  "invitedWorkspaces": [
    {
      "id": "112",
      "name": "Project Gamma"
      "invitedBy": "alice@example.com"
    }
  ]
}

 

이를 통해 로그인 API로 해당 화면에 필요한 데이터를 불러올 수 있습니다.

누군가는 이 API를 보고 비난을 할 순 있겠지만, 그래도 장점이 있고 지금 프로젝트에서 적용하기에 나쁘지 않다고 말씀드리고 싶습니다.

 

그렇다고 Fine-Grained를 지양하는 것은 아닙니다. 페이지별 API를 제공하되 코드 재사용성을 높이기위해, 저는 Service 계층을 Fine-Grained 하게 만들었습니다. Controller에서 여러 Service를 조합해야하는 문제점이 있었지만,

 

  1. 잘못된 데이터 반환 인한 에러(Service 계층의 결과를 바탕으로 재구성한 DTO) 디버깅을 쉽게 하고
  2. Response DTO 구성 로직(여러 Service의 결과를 결합)과 Service 로직을 분리하고
  3. 분리된 로직을 따로 테스트

하기위해 이 방법을 택했습니다. 예시를 들면 다음과 같습니다.

  @ApiOperation({ summary: '로그인' })
  @ApiResponseData(LoginResponseDto)
  @UseInterceptors(TransactionInterceptor)
  @Post('login')
  async login(@Body() body: LoginRequestDto,
        @CommonTxManager() queryRunnerManager: EntityManager) {
    const memberInfo = await this.memberService.login(queryRunnerManager, body.email, body.authorizationNumber);
    
    const joinedWorkspaceList = this.workspaceService.getList(memberInfo.id, true);
    const invitedWorkspaceList = this.workspaceService.getList(memberInfo.id, false);
    
    return LoginResponseDto.create(
      memberInfo,
      joinedWorkspaceList,
      invitedWorkspaceList,
    )
  }

 

login 로직을 담당하는 memberService.login 메서드가 존재하고, 가입한 워크스페이스와 초대된 워크스페이스르 조회해 ResponseDTO를 생성합니다.

 

 

이 방법을 통해

컨트롤러에게 로직을 순서대로 수행하고 response dto를 생성하는 책임을 부과했습니다. 이것이 바람직하지는 않을 수 있지만, 서비스 계층을 Fine-Grained 하게 설계, 개발하는 방식으로 적용해 봤습니다.

 

좋은 코드란 무엇인가요? 개발을 시작한지 3년이 안된 (주니어도 아닌) 저에겐 대답하기 어려운 질문입니다. 다만 이번 인턴쉽을 하면서 프로젝트, 동료의 특성에 맞게 설계하고 코드를 작성하는 태도를 가지게 되었습니다.