Google Calendar + Workspace 연동 예약 시스템 개발기
사내 프로젝트로 회의실 예약 시스템을 개발했다. Google Calendar와 완전 동기화되는 예약 시스템인데, 리드로 참여해서 상용 서비스 수준의 품질을 확보했다.
1. 프로젝트 개요
Roomly란?
회사 회의실 예약을 Google Calendar와 실시간 동기화하는 시스템이다.
핵심 기능:
- Google Workspace 계정으로 로그인
- 회의실 예약 → Google Calendar에 자동 반영
- Google Calendar에서 수정 → Roomly에 실시간 동기화 (Webhook)
- Slack 연동 리마인더 알림
flowchart LR
User[사용자] --> Roomly[Roomly 서버]
Roomly <--> GCal[Google Calendar API]
Roomly <--> Slack[Slack API]
GCal -->|Webhook| Roomly2. Google OAuth 2.0 연동
인증 플로우
Workspace 계정 인증을 위해 OAuth 2.0 Authorization Code Flow를 구현했다.
// Google OAuth 토큰 교환
public async Task<OAuthTokenResponse> ExchangeAuthorizationCodeForTokensAsync(
string authorizationCode,
string? redirectUri)
{
const string tokenEndpoint = "https://oauth2.googleapis.com/token";
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["code"] = authorizationCode,
["client_id"] = _config.ClientId,
["client_secret"] = _config.ClientSecret,
["redirect_uri"] = redirectUri ?? _config.RedirectUri,
["grant_type"] = "authorization_code"
});
var response = await _httpClient.PostAsync(tokenEndpoint, content);
// ...
}Workspace 계정 검증
개인 Gmail은 허용하지 않고, Workspace 도메인만 허용한다.
// hd(Hosted Domain) 필드로 Workspace 계정 검증
if (string.IsNullOrEmpty(idToken.Hd))
{
throw new RoomlyException(
ResponseCode.WorkspacePersonalAccountNotAllowed,
"Google Workspace account required."
);
}3. Google Calendar API 연동
예약 생성 시 캘린더 동기화
Roomly에서 예약 생성 → Google Calendar에 이벤트 생성
public async Task<Event> CreateEventAsync(
string calendarId,
Reservation reservation,
IEnumerable<string> attendeeEmails)
{
var calendarEvent = new Event
{
Summary = reservation.Title,
Start = new EventDateTime
{
DateTimeDateTimeOffset = reservation.StartAt
},
End = new EventDateTime
{
DateTimeDateTimeOffset = reservation.EndAt
},
Attendees = attendeeEmails.Select(e => new EventAttendee { Email = e }).ToList()
};
return await _calendarService.Events.Insert(calendarEvent, calendarId).ExecuteAsync();
}반복 일정 (Recurrence) 처리
iCalendar RFC 5545 RRULE 규격으로 반복 일정을 처리한다.
// RRULE 예시
"RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR;UNTIL=20260331T000000Z"
// This Event Only / This and Following Events / All Events
// 세 가지 수정 모드 모두 지원4. Google Calendar Webhook
가장 까다로운 부분이었다. Google Calendar에서 이벤트가 변경되면 Roomly에 동기화해야 한다.
Webhook 등록
public async Task RegisterWebhookAsync(long companyId, string calendarId)
{
var channel = new Channel
{
Id = Guid.NewGuid().ToString(),
Type = "web_hook",
Address = $"{_serverDomain}/api/calendar/webhook",
Expiration = DateTimeOffset.UtcNow.AddDays(7).ToUnixTimeMilliseconds()
};
await _calendarService.Events.Watch(channel, calendarId).ExecuteAsync();
}Webhook 갱신 (Background Service)
Webhook은 최대 7일까지만 유효하다. 백그라운드 서비스에서 자동 갱신한다.
// 매일 새벽에 만료 임박한 Webhook 갱신
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await RefreshExpiringWebhooksAsync();
await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
}
}5. Redis 기반 리마인더 시스템
멀티 테넌시 설계
100개 회사가 사용해도 성능 저하 없도록 회사별 Redis 키를 분리했다.
// Before: 단일 키에 모든 회사 데이터
"reminders:pending" → 100K items
// After: 회사별 분리
"reminders:pending:1" → 1K items (Company 1)
"reminders:pending:2" → 1K items (Company 2)성능 개선 효과:
- Sorted Set 크기 99% 감소
- ZRANGEBYSCORE 성능 50배 향상
- 회사 간 완전 격리
6. 기술 스택 정리
| 영역 | 기술 |
|---|---|
| Backend | .NET 8, ASP.NET Core |
| Database | Redis |
| Google API | Calendar API, Workspace Admin API, OAuth 2.0 |
| Realtime | SignalR |
| Messaging | Slack API (OAuth, Webhook) |
| Standard | iCalendar (RFC 5545), RRULE |
7. 배운 점
Google API 연동의 현실
- Rate Limit: Calendar API는 초당 호출 제한이 있어서 배치 처리 필수
- Webhook 불안정성: 간헐적으로 누락되므로 주기적 풀링 백업 필요
- 토큰 갱신: Refresh Token 만료 케이스 핸들링 중요
사내 프로젝트지만 상용 수준
- 멀티 테넌시 설계
- 회사별 데이터 격리
- 타임존 처리 (KST 기준)
- 에러 핸들링 및 재시도 로직
리드로 참여하면서 설계부터 구현까지 전체 아키텍처를 경험할 수 있었다.
마무리
Google Calendar 연동은 생각보다 복잡하다. OAuth, Webhook, RRULE, 타임존... 각각은 간단해 보이지만 조합하면 많은 엣지 케이스가 생긴다.
상용 서비스 수준의 안정성을 목표로 개발했고, 실제로 사용 중이다.
Loading comments...