사내 회의실 예약 시스템 Roomly를 만들고 있다.
회의실 예약이 Google Calendar와 양방향으로 동기화되는 시스템이고, 백엔드 리드로 참여해 설계와 구현을 맡았다.
1. 프로젝트 개요
Roomly란?
회사 회의실 예약을 Google Calendar와 실시간 동기화하는 시스템이다.
주요 기능은 네 가지다.
- Google Workspace 계정으로 로그인
- 회의실 예약 → Google Calendar에 자동 반영
- Google Calendar에서 수정 → Roomly에 실시간 동기화 (Webhook)
- Slack 연동 리마인더 알림
2. Google OAuth 2.0 연동
인증 플로우
Google OAuth 2.0 인증 체계 자체는 Service Account 인증을 다룬 글에서 정리했다.
Roomly는 사용자가 본인 Workspace 계정으로 로그인해야 하므로 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);
// ...
}Access Token은 Refresh Token으로 갱신하지만, Refresh Token 자체가 만료되거나 회수되는 경우도 있다.
이 경우를 놓치면 동기화가 조용히 멈추기 때문에, 갱신 실패를 감지해 재인증으로 떨어뜨리는 처리가 필요하다.
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();
}한 가지 주의할 점은 호출 제한이다.
Calendar API는 초당 호출 수 제한이 있어서, 여러 건을 한꺼번에 동기화할 때는 배치 처리로 묶어야 한다.
반복 일정 (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);
}
}갱신보다 운영에서 더 문제가 되는 것은 알림 누락이다.
Webhook 알림은 간헐적으로 유실되고, Google 공식 문서도 알림이 전달되지 않을 수 있다는 전제로 주기적인 동기화를 권장한다.
그래서 Webhook에만 의존하지 않고 주기적으로 캘린더를 다시 읽어 상태를 맞추는 폴링 백업을 두는 방향으로 보완하고 있다.
5. Redis 기반 리마인더 시스템
멀티 테넌시 설계
100개 회사가 사용해도 성능 저하 없도록 회사별 Redis 키를 분리했다.
// Before: 단일 키에 모든 회사 데이터
"reminders:pending" → 100K items
// After: 회사별 분리
"reminders:pending:1" → 1K items (Company 1)
"reminders:pending:2" → 1K items (Company 2)회사 100곳이 각각 1K개 안팎의 리마인더를 쌓는다고 가정하면, 단일 키에서는 ZRANGEBYSCORE가 100K짜리 Sorted Set을 훑지만 키를 나누면 회사당 1K로 줄어든다.
성능은 실측으로 비교하지 않았다.
근거는 조회 대상 집합이 1/100로 줄어든다는 점, 한 회사의 데이터 증가가 다른 회사 조회에 영향을 주지 않게 됐다는 점이다.
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 |
마무리
Google Calendar 연동은 생각보다 복잡하다.
OAuth, Webhook, RRULE, 타임존까지 각각은 간단해 보여도 조합하면 엣지 케이스가 계속 나온다.
사내 도구라고 해서 대충 넘길 수 있는 것도 아니어서, KST 기준 타임존 처리나 API 호출 실패 시 재시도 로직처럼 운영에서 바로 드러나는 부분부터 챙겼다.
지금은 1차 릴리스를 준비하며 내부 테스트 중이고, Webhook 누락처럼 미리 대비해야 할 지점부터 다듬고 있다.