회원 시스템 MSA 전환 도전기: MAU 1,900만 당근 유저를 위한 선택

Seungchan Yuk
당근 테크 블로그
21 min readAug 6, 2024

안녕하세요, 당근 아이덴티티 서비스(Identity Service)팀의 소프트웨어 엔지니어 루(Lu)입니다.

아이덴티티 서비스팀은 당근의 회원 및 인증을 책임지고 있는 팀입니다. 당근 내 다양한 서비스가 회원 및 인증 시스템을 기반으로 동작하기 때문에 저희 팀은 안정성을 최우선으로 여기죠.

당근은 초기 중고거래 서비스를 기반으로 시작해 폭발적으로 성장했고, 그 과정에서 마이크로서비스 아키텍처(MSA)를 채택하여 현재는 크고 작은 다양한 서비스가 당근을 지탱하고 있습니다.

자연스럽게 전사 관점에서 회원 시스템과 인증 시스템도 별도 서비스로 분리하는 프로젝트를 시작하게 되었는데요. 아이덴티티 서비스팀에서 무중단으로 시스템을 분리하기 위해 선택했던 몇 가지 전략을 공유합니다.

AS-IS, TO-BE

당근의 회원 시스템과 인증 시스템은 초기부터 존재했던 중고거래 서비스에 포함되어 있었습니다. 중고거래 서비스는 월간 활성 사용자(MAU) 1900만 이상의 당근을 지탱해 준 든든한 서비스였습니다.

하지만 서비스가 고도화되고 사용자가 늘어나면서, 회원 관련 정책은 여러 변화를 겪었습니다. 자연스럽게 회원 시스템과 인증 시스템의 엔지니어링 복잡도도 매우 높아졌죠. 예를 들어, 중고거래 관련 기능 변경이 회원 서비스에 장애를 초래하거나, 배포 블락으로 인해 관련 없는 서비스까지 개발 생산성에 영향을 받는 상황이 빈번하게 발생했습니다. 빠르게 성장하는 서비스에 맞춰 유연하게 확장하거나 요구사항을 신속하게 처리하기 힘든 상황이 되어 버렸습니다.

이러한 문제를 해결하고 당근이 더 큰 플랫폼으로 성장할 수 있도록 시스템을 분리하기로 결정했습니다. 분리된 서비스가 어떤 모습일지 상상하며 아래 목표를 세웠고 본격적으로 시스템 분리 작업을 시작하게 되었습니다.

  1. 데이터와 비즈니스 로직 모두 분리하여 완전히 독립된 서비스를 만든다.
  2. 유연하게 확장 가능하고, 엔지니어링 복잡도를 최소화한다.
  3. 99.999% 이상의 가용성을 제공하는 아키텍처를 설계한다.

레거시 시스템 이해하기

가장 먼저 한 일은 기존 시스템을 철저히 이해하고 체계적으로 계획하는 것이었습니다. 회원 시스템은 초창기부터 존재해 오래된 코드가 많았고, 여러 차례 정책이 변경되면서 복잡한 로직이 많이 포함되어 있었습니다. 또한 회원 라이프사이클을 완전히 이해하는 사람이 없었고, 남아 있는 문서에는 누락되거나 잘못된 내용이 많았습니다.

그래서 시스템을 분리하는 과정에서 항상 참조하고 신뢰할 수 있는 문서를 만들기로 했습니다. 처음에는 몰랐던 내용들이 뒤늦게 발견되는 경우가 많아 그때마다 문서를 최신화했습니다. 저희 팀은 “회원 라이프사이클 정답지”, “정책서”, “합의서” 등 여러 종류의 문서를 만들었습니다. 그중 가장 많이 참조한 “회원 라이프사이클 정답지”와 레거시 시스템을 분석한 과정을 소개하겠습니다.

회원 라이프사이클 정답지

현재 클라이언트를 기준으로 회원 라이프사이클 정답지를 작성했습니다. 이를 기준으로 각 화면에서 호출되는 API를 모두 수집하고, 레거시 시스템에서 각 API에 연결된 비즈니스 로직을 분석하기 시작했습니다. 분석한 코드와 실제 데이터를 비교하면서 예상치 못한 문제를 많이 발견했습니다. 대표적으로 아래와 같은 문제들이 있었습니다.

  1. 분석한 로직과 다른 데이터가 저장됨 (⁉️)
  2. 이 로직이 왜 있는지 모르겠음 (🤷🏻)

문제 원인을 찾기 위해 레거시 시스템에서 옵저버빌리티를 확보하려 했습니다. 관심 대상 API의 비즈니스 로직에 로그를 추가해, 어떤 OS/버전의 클라이언트에서 요청이 들어오는지 가시적으로 보기 시작했습니다.

# ruby on rails

def login
Datadog::StatsdClient.instance.increment("services.flea_market.action.login.something1", tags: ["api_version: #{api_version}", "app_version: #{app_version}", "os: #{app_os}"])

...something logic

if something2?
Datadog::StatsdClient.instance.increment("services.flea_market.action.login.something-2", tags: ["api_version: #{api_version}", "app_version: #{app_version}", "os: #{app_os}"])
else
Datadog::StatsdClient.instance.increment("services.flea_market.action.login.something-2-else", tags: ["api_version: #{api_version}", "app_version: #{app_version}", "os: #{app_os}"])
end
end

OS 또는 버전에 따라 데이터가 다르게 저장될 수 있는 부분을 확인했고, 몇몇 코드들이 특정 버전 버그를 대응하기 위해 존재했다는 것도 알게 되었습니다. 비슷한 방식으로 오랫동안 호출되지 않은 비즈니스 로직을 찾아서 제거하기도 했습니다.

로직을 분석하면서 의도를 알 수 없거나 실제 데이터와 다른 경우, 로그를 추가해 분석하는 것이 당연하게 여겨졌습니다. 하나둘씩 수집하기 시작한 지표가 각 엔드포인트별로 수십 개가 되었습니다. 또한, 새롭게 알게 된 내용들을 회원 라이프사이클 정답지 문서에 기록하고 팀이 싱크하도록 했습니다. 이런 노력을 통해 레거시 시스템을 더 잘 이해하게 되었고, 팀의 자신감도 크게 향상되었습니다.

다음 섹션으로 넘어가 신규 데이터베이스를 어떻게 구축하고 마이그레이션 했는지를 이야기해 보겠습니다.

신규 데이터베이스 구축

시스템 분리 작업의 목표는 데이터와 비즈니스 로직을 모두 분리하여 완전히 독립된 서비스를 만드는 것이었습니다. 이를 위해 완전히 독립된 데이터베이스를 구축하는 두 가지 선택지를 검토했고, 각 선택지의 장단점을 정리했습니다.

[1] 기존 중고거래 서비스 데이터베이스에서 회원 도메인의 테이블만 분리하여 신규 회원 데이터베이스를 구축

  • pros
    – 이미 설계된 테이블을 그대로 사용하기 때문에 레거시 시스템의 비즈니스 로직을 100% 그대로 사용할 수 있어 구현 난이도가 낮습니다.
  • cons
    – 기존 설계된 테이블은 확장하기 어려운 구조로 되어 있어, 미래의 요구사항을 반영하기 어렵습니다.

[2] 스키마를 다시 설계하여 신규 회원 데이터베이스를 구축

  • pros
    – 다양한 요구사항에 대응할 수 있도록 확장 가능한 구조로 만들 수 있습니다.
  • cons
    – 레거시 비즈니스 로직을 이해하고 신규 스키마에 맞게 비즈니스 로직을 새롭게 작성해야 하므로 구현 난이도가 높습니다.

두 선택지는 상반되는 장단점을 가지고 있었고, 난이도 측면에서 1번 선택지가 유리했습니다. 하지만 여러 팀 내 논의를 진행한 끝에 다음과 같은 이유로 2번 선택지를 선택했습니다.

  • 당근이 더 성장하기 위해 확장 가능한 구조는 선택이 아닌 필수 조건이라고 판단했습니다.
  • 여러 요구사항을 반영해 추가된 수많은 컬럼 때문에 기존 테이블의 복잡도가 매우 높았습니다.
  • 이미 레거시 시스템을 분석하며 자신감이 높아져 있었기 때문에, 난이도가 높아도 충분히 해낼 수 있다고 판단했습니다.

스키마 재설계를 위해 먼저 중고거래 서비스 데이터베이스에서 회원 도메인의 데이터를 분류했습니다. 방향성 결정이 필요한 데이터에 대해서는 유관 팀과 오너십 논의를 진행하며 최종 회원 도메인 데이터를 결정했습니다.

스키마 재설계를 진행하면서 지속 가능한 명세 관리를 매우 중요하게 생각했습니다. 기존 명세 관리가 잘 되지 않아 히스토리가 누락되는 문제가 많았기 때문입니다. 이를 해결하기 위해 변경 이력 추적이 가능한 Git과 팀에 익숙한 Protobuf를 활용했습니다.

enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
// 활성
USER_STATUS_ACTIVE = 1;
// 비활성
USER_STATUS_INACTIVE = 2;
// 탈퇴
USER_STATUS_WITHDRAWN = 3;
}

message User {
option (daangn.ddl.protobuf.v1.datastore_type) = DATASTORE_TYPE_MYSQL;

int64 id = 1 [(daangn.ddl.protobuf.v1.key) = true];

google.protobuf.Timestamp created_at = 2;
google.protobuf.Timestamp updated_at = 3;
optional google.protobuf.Timestamp deleted_at = 4;

// 예시 데이터 1
string user_data_1 = 5 [
(daangn.ddl.protobuf.v1.type) = "name=VARCHAR(50)",
(daangn.ddl.protobuf.v1.unique) = "name=ux_userdata1"
];

// 예시 데이터 2
optional google.protobuf.Timestamp user_data_2 = 6;

// 예시 데이터 3
UserStatus user_data_3 = 7;
}

이러한 과정을 통해 확장 가능하고 유지 관리가 용이한 신규 회원 데이터베이스를 구축하였습니다. 앞으로 당근이 성장함에 따라 더 많은 요구사항이 추가되더라도 유연하게 대응할 수 있는 기반을 만든 것입니다.

데이터 마이그레이션 JOB

시스템 분리를 완전히 종료하려면 레거시 데이터베이스를 참조하는 모든 비즈니스 로직이 신규 데이터베이스를 참조하도록 변경해야 합니다. 이를 위해 두 데이터베이스에 동일한 데이터를 기록하는 Dual-Write 과정이 필요했습니다. Dual-Write를 시작하면 필연적으로 데이터 차이가 발생할 수 있습니다. 이러한 차이를 제어하기 위해 데이터 마이그레이션 Job을 만들게 되었습니다.

데이터 마이그레이션 Job은 두 데이터베이스를 비교하여 차이가 전혀 없다면 동일한 데이터를 가지고 있다고 판단합니다. 이 Job은 두 데이터베이스의 차이를 확인하고, 신규 데이터베이스를 레거시 데이터베이스와 일치하도록 업데이트하는 역할을 합니다.

마이그레이션 Job

데이터 마이그레이션 Job은 일종의 데이터 파이프라인으로, 다음과 같은 6개의 컴포넌트로 구성됩니다.

Selector: 레거시 데이터베이스에서 회원 데이터를 일정 수량씩 읽어옵니다.

Filter: 읽어온 레거시 회원 데이터 중 마이그레이션 대상이 아닌 데이터를 필터링합니다. 예를 들어 필수 데이터가 없는 행은 제외됩니다.

Converter: 레거시 회원 데이터를 신규 데이터베이스에 맞춰 추상화된 모델로 변환합니다.

Decider: 마이그레이션 Job의 핵심으로, 레거시 회원 데이터와 신규 데이터베이스에 저장된 회원 데이터를 비교합니다. 두 데이터가 동일하다면 스킵하고, 다르다면 아래 3가지 동작을 수행합니다.

  1. 레거시 회원 DB에 없는데 신규 DB에 있는 경우 → 신규 DB에서 삭제
  2. 레거시 회원 DB에 있는데 신규 DB에 없는 경우 → 신규 DB 데이터 삽입
  3. 양쪽 DB 모두 존재하지만 데이터가 다른 경우 → 신규 DB 리셋
    데이터를 비교하는 중에 데이터 변경이 있어서 순간적으로 다를 수 있으므로 리셋하기 전에 한번 더 비교 작업을 진행하면 정합성을 올릴 수 있습니다.

Analyzer: 내부 회원 정책에 맞춰 데이터를 분석하고, 마이그레이션 작업이 끝나면 분석 결과를 로그로 리포팅합니다.

Notifier: 마이그레이션 Job 결과를 슬랙에 리포팅합니다.

마이그레이션 리포트

데이터 마이그레이션 Job을 통해 두 데이터베이스의 정합성을 99.999% 보장하는 목표를 세웠습니다. 마이그레이션 대상 데이터를 8개의 Phase로 나누어 진행하였고, 각 Phase 별로 데이터 차이를 보여주는 리포트를 받도록 했습니다. 시스템 분리를 종료하기 직전까지 매일 트래픽이 가장 적은 시간에 데이터 마이그레이션 Job을 수행하여 두 데이터베이스가 같은 데이터를 유지할 수 있도록 했습니다.

매일 같은 시각에 마이그레이션 Job이 수행되면 리포트의 Inserted / Reset / Deleted 총합이 하루 동안 일어난 데이터 변화량을 나타냅니다. 앞서 언급했던 가용성이 목표치에 기반하여 변화량이 0.001% 미만이 된다면 목적을 달성했다고 볼 수 있습니다.

이후 Dual-Write 과정으로 넘어가, 서로 다른 API를 호출하고 데이터 구조도 다른 상황에서 어떻게 데이터 변화량을 0.001% 미만으로 만들었는지 API 마이그레이션을 통해 이야기해 보겠습니다.

API 마이그레이션

데이터 마이그레이션 Job 과정에서 정의한 8개의 Phase를 순서대로 진행했습니다. Phase는 데이터를 기준으로 나누었습니다. 이때 각 Phase에 포함되는 API는 해당 Phase에 포함된 데이터를 변경하는 API입니다. 예를 들어, Phase 1에 “nickname”과 “profile_image_id” 데이터가 포함되어 있다면, 이 두 데이터를 변경하는 API들이 Phase 1의 마이그레이션 대상이 됩니다.

API 마이그레이션은 아래의 4단계를 거쳐 진행되었습니다. 각 Phase에 포함된 모든 API에 대해 이 단계를 완료하면, 해당 Phase를 종료하고 다음 Phase로 넘어갔습니다.

Step 1. 신규 서비스 API 설계

시스템 분리 작업에서 가장 중요하게 생각한 부분은 “이상적인 API 설계”였습니다. 앱 클라이언트에서 사용되기 시작한 API는 변경하기 어렵기 때문에 처음부터 잘 설계하는 것이 중요했습니다. 레거시 서비스 API를 N:N 구조로 신규 서비스 API를 설계했습니다. 이는 하나의 API가 여러 개로 분리되거나 여러 API가 하나로 합쳐질 수 있음을 의미합니다.

예를 들어 회원가입 과정에서 약관 동의 페이지와 가입 페이지가 분리되어, 각각 약관 동의 API와 회원가입 API를 호출하는 경우를 생각해 보겠습니다. 클라이언트 메모리에 약관 동의 여부를 저장하고 이를 가입 시 전달하도록 변경하면, 두 개의 API를 하나로 합칠 수 있습니다. 반대로 하나의 API를 두 개로 분리할 수도 있습니다.

신규 버전 클라이언트의 이상적인 플로우를 기준으로 API를 설계하고, Protobuf를 활용하여 API를 명세했습니다.

service UserService {
rpc CreateKarrotUserProfile(CreateKarrotUserProfileRequest) returns (CreateKarrotUserProfileResponse) {
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
description: "Create My KarrotUser Profile"
};
option (google.api.http) = {
post: "/v1/karrot-users/me/profile",
body: "*"
};
}
}

message CreateKarrotUserProfileRequest {
string nickname = 1;
optional string profile_picture_id = 2;
optional string biography = 3;
}

message CreateKarrotUserProfileResponse {
string nickname = 1;
optional string profile_image_url = 2;
optional string biography = 3;
}

Step 2. 프록시 사이드카를 활용한 Dual-Write

이 단계에서는 비즈니스 로직을 구현하지 않고 단순히 데이터를 저장하기만 합니다. 이는 현재 Phase의 데이터 변화 지점을 모두 알고 있는지 확실하지 않기 때문입니다.

예를 들어 현재 진행하고 있는 Phase의 타켓 데이터가 “nickname”, “profile_image” 이라고 가정해 보겠습니다. 두 데이터가 변경되는 엔드포인트를 모두 찾아서 Dual-Write 했다면, 마이그레이션 Job 리포트상의 해당 Phase 변화량은 0이 되어야 합니다. 만약 그렇지 않다면 조사 과정에서 놓친 지점이 있을 수 있고 이 부분을 찾아서 동일하게 Dual-Write 해주어야 합니다.

레가시 서비스 앞단에 프록시 사이드카를 도입하여 우아하게 Dual-Write 할 수 있습니다. Istio Request Routing 기능을 활용하여 프록시 사이드카가 먼저 요청을 받도록 했습니다.

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: legacy-http
spec:
hosts:
- legacy-service.kr.daangn.com
gateways:
- legacy
- mesh
http:
- match:
- uri:
regex: /v[0-9]+/users/me/profiles
method:
exact: POST
route:
- destination:
host: new-user-service.svc.cluster.local
port:
number: 80

프록시 사이드카는 자체 구현한 경량 HTTP 서버로, 커스텀 로직 구현이 가능합니다. 클라이언트 요청을 가장 먼저 받아서 레가시 서비스와 신규 서비스 양쪽에 요청을 보냅니다. 이 과정에서 프록시 사이드카가 레가시 서비스 Internal API를 사용할 수도 있습니다. 레가시 서비스 요청 페이로드로 신규 서비스 API 요청 페이로드를 만들 수 있으면 다행이지만, 아닌 경우도 있기 때문입니다.

레가시 서비스가 요청을 받으면 레가시 DB에 데이터가 저장되고, 신규 서비스가 요청을 받으면 신규 DB에 데이터가 저장하며 Dual-Write 를 달성하게 됩니다.

필요에 따라 신규 서비스에서 레가시 서비스에 Internal API를 호출해야 하는 경우도 있습니다. 예를 들어 요청 페이로드에 없는 어떤 데이터를 레가시 서비스 비즈니스 로직으로 만들어서 저장하는 경우입니다. 현재 단계에서 신규 서비스는 비즈니스 로직을 구현하지 않으므로 해당 데이터를 획득하기 위해 Internal API를 사용하여 데이터를 획득하고 저장해야 합니다.

현재 단계에서 요청을 프록시하는 로직 작성 시 주의해야 할 점이 하나 있습니다. 반드시 레가시 서비스 응답을 사용자에게 응답하도록 해야 합니다. 클라이언트와 서버 간의 동작을 변경하지 않고 Dual-Write 하는 것이 목적이기 때문입니다.

해당 Phase의 엔드포인트를 모두 프록시한 후 데이터 마이그레이션 Job 리포트를 확인하여 변화량이 0이 되면 다음 단계로 넘어갑니다.

Step 3. 비니스 로직 Replaying & Shadowing

이 단계에서는 신규 서비스 API에 비즈니스 로직을 구현합니다. 이를 “비즈니스 로직 Replaying”이라고 표현합니다. 레가시 서비스 비즈니스 로직 기준으로 비즈니스 로직을 작성하고, 만약 API 가 합쳐지거나 분리되는 경우에는 앞뒤 흐름 등을 고려해야 합니다.

주의할 점은 비즈니스 로직 작성 시 반드시 신규 데이터베이스만 참조해야 합니다. Internal API 등으로 레거시 데이터베이스를 참조하게 되면 Dual-Write를 끊을 때 문제가 발생할 수 있습니다.

작성한 비즈니스 로직을 검증하기 위해 응답 Shadowing 전략을 사용했습니다. 이는 프록시 사이드카에서 각 서비스로부터 받은 두 응답을 비교하는 것입니다. 신규 서비스 API의 성공 응답과 모든 에러 응답을 레거시 서비스 응답 형태에 맞춰 변환하는 작업이 필요합니다.

실제 작업했던 것과 비슷한 예시를 하나 가져왔습니다. 신규 서비스 API 응답에 있는 데이터로 레가시 서비스 API 응답을 만들 수 있으면 다행이지만, 그렇지 않은 경우가 더 많았습니다. 위 예시에서는 “id” 필드가 신규 서비스 API 응답에는 없기 때문에 어디선가 데이터를 가져와서 채워주어야 합니다. “id” 같은 경우는 “인증” 정보에서 가져와 넣어줄 수 있습니다. 만약 프록시 사이드카 컨텍스트에 없는 데이터라면 Internal API를 통해 데이터를 가져와서 채워줄 수 있습니다.

응답을 비교할 준비가 되었다면 모든 요청에 대해 응답을 비교해서 일치 여부와 함께 스텟을 찍었습니다. 또 일치하지 않는 경우, 두 응답 Diff를 로그로 남기고 이후 분석 데이터로 활용했습니다.

위 그림은 특정 엔드포인트에 대한 Shadowing 지표를 보여줍니다.

  • 왼쪽 지표
    (일치한 응답 수 / 전체 비교 수) * 100
  • 오른쪽 지표
    (비교한 응답 수 / 전체 요청 수) * 100

응답 일치율 99.999% 이하로 떨어지는 케이스는 비교 로그를 확인하고 어떻게 다른지 분석하여 신규 서비스 API 비즈니스 로직을 수정합니다.

응답 일치율 99.999% 목표를 달성한다고 데이터 정합성도 99.999%가 되는 것은 아닙니다. 비즈니스 로직 Replaying을 하고 나서 데이터 정합성이 떨어지게 되면 똑같이 문제가 있는 지점을 찾아서 수정해야 합니다.

이러한 과정을 반복하여 99.999% 목표를 달성하면 해당 엔드포인트에 대한 비즈니스 로직 Replaying 과정이 종료됩니다.

작업했던 모든 엔드포인트가 그랬지만, 정말 꼼꼼하게 비즈니스 로직을 작성해도 한 번에 90% 이상 나오는 경우는 없었습니다. 하지만 문제가 있는 엔드포인트를 가시적으로 확인할 수 있고, 어떤 문제가 있는지도 즉시 파악하는 게 가능하기 때문에 목표를 달성할 수 있었습니다.

Step 4. 신규 서비스 API 응답 Rollout

엔드포인트에 대한 비즈니스 로직 Replaying 과정이 종료되면 데이터 정합성도 보장하면서 응답 일치율도 보장할 수 있는 상태가 됩니다. 이제 신규 서비스 API 응답을 클라이언트에게 전달하는 Rollout 과정을 진행합니다.

Rollout 과정은 점진적으로 진행됩니다. 전체 요청 중 1%만 신규 서비스 API 응답으로 내보내기 시작하면서 문제가 발생하지 않는지 검증합니다. 문제가 없다면 점진적으로 늘려가며 100%까지 신규 서비스 API 응답을 내보냅니다.

Rollout 과정 전까지는 클라이언트에게 레가시 서비스 응답이 나갔지만 이제부터 클라이언트에게 나가는 응답이 변경되므로 롤백 플랜이 필요합니다.
저희 팀은 Rollout을 늘려가는 플랜을 작성하고, 내부 유관 부서 및 CS 채널에 공유하고, 지표 모니터링과 내부 제보를 활용했습니다. 지표가 흔들리거나 제보가 있다면 즉시 Rollout을 중단하고 레가시 서비스 응답이 100% 나가도록 롤백했습니다.

Rollout 100%가 진행되면 드디어 특정 API에 대한 마이그레이션 작업을 종료할 수 있습니다.

글을 마치며

API 마이그레이션을 완료한 후, 레거시 서비스에서 모든 읽기 요청을 신규 서비스 데이터베이스로 전환하고 Dual-Write를 끊음으로써 시스템을 완전히 분리할 수 있었습니다.

시스템 분리를 통해 당근은 훨씬 더 안정적이고 확장성 높은 회원 시스템을 갖추게 되어, 현재는 회원 및 인증과 관련된 다양한 프로젝트를 진행하며 사용자에게 더 나은 경험을 제공하기 위해 열심히 노력하고 있습니다.

회원 시스템 분리 작업은 1년이 넘는 시간 동안 긴 호흡으로 진행된 프로젝트였습니다. 돌이켜 보니, 프로젝트를 진행하면서 “중꺾마(중요한 것은 꺾이지 않는 마음)”라는 말을 가장 많이 했던 것 같습니다. 긴 시간 동안 어려운 부분도 많았지만, 그만큼 기술적인 성장을 많이 이루었다고 생각하여 매우 뿌듯하기도 합니다. 긴 여정 동안 함께 해준 모든 팀원들에게 감사의 마음을 전합니다.

출처 : 대한축구협회

Identity Service 팀은 확장 가능하고 안전한 회원 서비스를 통해 모든 로컬 서비스가 당근 하나로 연결되는 세상을 꿈꾸고 있습니다. 서비스의 성장에 따라 발생하는 다양한 기술적 문제를 함께 해결해 나갈 동료를 찾고 있습니다. 아래 당근 채용 공고를 통해 저희 팀에 합류할 수 있으니 많은 관심 가져주세요!

Software Engineer, Backend — Identity Service

--

--