안녕하세요. 인시퀀스 Technical Group의 Back-End Developer & DBA 추일권입니다.

이번 포스팅에선 2022년도 1월부터 5월까지 진행한 사이트 고도화 중 비대면 회의(ZOOM) 연계에 대한 소개를 해볼까 합니다.

API 연계의 시작

API 사용 개요

대중적인 API 서비스의 경우 블로그에 정리가 잘 되어 있어서 굳이 영문으로 된 Reference를 참고하지 않아도 충분히 구현하기에 무리가 없었습니다. apiKey나 Token은 발급 받는 부분만 따로 설정해주면 제공된 예제로 구현이 가능했습니다.

하지만 ZOOM은 너무 많은 정보를 제공하고 있어 저에게 필요한 정보를 찾기가 힘들었습니다.

제가 찾는 검색 결과는 미팅 일정을 만들고 일정에 대한 기능에 대한 상세한 페이지를 원했지만, 상세하게 설명된 블로그는 찾기 매우 어려웠습니다.🤯

이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
SDK나 파이썬에서 사용하는 예제위주들이었습니다.

이럴 바엔 내가 작성해서 소개하겠다.

포스팅을 통해 소개해드리기 좋은 소재라 이번 기회를 통해 ZOOM API 관련 소개와 연계 기능 일부를 차근차근 소개해드리고자 합니다.

API 기능 A-Z

1) App 생성
  1. 1. 우선 계정을 로그인 한 뒤 Manage에서 사용할 App Type을 생성하도록 합니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
  2. 2. 단순히 하나의 계정으로 화상회의를 생성하는 것만 사용하므로 간편한 JWT로 선택하여 Create버튼을 눌러 생성하였습니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
  3. 3. 생성하면 App에 대한 최초의 정보를 입력하게 됩니다. App명과 개발자 정보를 입력합니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
  4. 4. 이후 App 인증서에 있는 고유 값들을 확인 할 수 있습니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
  5. 5. 추가 기능화면으로 여기서 Webhooks의 설정을 할 수 있습니다. endpoint URL과 이벤트 추가로 Webhooks 알람을 받을 수 있는지 설정 하는 곳입니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요

API 사용하기 위한 모든 준비가 끝났습니다.

2) API Reference

소개 첫 장부터 사용법 등은 블로그에서 작성할 양이 많으므로 API소개사용법 오류코드 정의 등은 페이지에서 방문하셔서 확인하시면 되겠습니다.

  1. 1. Docs 여러 API 문서 중에서 이번 프로젝트에서 사용할 연계인 Meetings로 이동하도록 합니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
  2. 2. 미팅에 대한 자세한 결과가 화면에 나옵니다. 레이아웃 좌측은 API 목록, 중앙은 API Request Parameter, 우측은 Response Json Sample결과를 보여줍니다. 좌측에서 Create a meeting을 빠르게 찾아줍니다.

    이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
    이미지 정보
  3. 3. Request Parameter의 예시 화면입니다.

    {
      "agenda": "My Meeting",
      "default_password": false,
      "duration": 60,
      "password": "123456",
      "pre_schedule": false,
      "recurrence": {
        "end_date_time": "2022-04-02T15:59:00Z",
        "end_times": 7,
        "monthly_day": 1,
        "monthly_week": 1,
        "monthly_week_day": 1,
        "repeat_interval": 1,
        "type": 1,
        "weekly_days": "1"
      },
      "schedule_for": "jchill@example.com",
      "settings": {
        "additional_data_center_regions": [
          "TY"
        ],
        "allow_multiple_devices": true,
        "alternative_hosts": "jchill@example.com;thill@example.com",
        "alternative_hosts_email_notification": true,
        "approval_type": 2,
        "approved_or_denied_countries_or_regions": {
          "approved_list": [
            "CX"
          ],
          "denied_list": [
            "CA"
          ],
          "enable": true,
          "method": "approve"
        },
        "audio": "telephony",
        "authentication_domains": "example.com",
        "authentication_exception": [
          {
            "email": "jchill@example.com",
            "name": "Jill Chill"
          }
        ],
        "authentication_option": "signIn_D8cJuqWVQ623CI4Q8yQK0Q",
        "auto_recording": "cloud",
        "breakout_room": {
          "enable": true,
          "rooms": [
            {
              "name": "room1",
              "participants": [
                "jchill@example.com"
              ]
            }
          ]
        },
        "calendar_type": 1,
        "close_registration": false,
        "cn_meeting": false,
        "contact_email": "jchill@example.com",
        "contact_name": "Jill Chill",
        "email_notification": true,
        "encryption_type": "enhanced_encryption",
        "focus_mode": true,
        "global_dial_in_countries": [
          "US"
        ],
        "host_video": true,
        "in_meeting": false,
        "jbh_time": 0,
        "join_before_host": false,
        "language_interpretation": {
          "enable": true,
          "interpreters": [
            {
              "email": "interpreter@example.com",
              "languages": "US,FR"
            }
          ]
        },
        "meeting_authentication": true,
        "meeting_invitees": [
          {
            "email": "jchill@example.com"
          }
        ],
        "mute_upon_entry": false,
        "participant_video": false,
        "private_meeting": false,
        "registrants_confirmation_email": true,
        "registrants_email_notification": true,
        "registration_type": 1,
        "show_share_button": true,
        "use_pmi": false,
        "waiting_room": false,
        "watermark": false,
        "host_save_video_order": true,
        "alternative_host_update_polls": true
      },
      "start_time": "2022-03-25T07:32:55Z",
      "template_id": "Dv4YdINdTk+Z5RToadh5ug==",
      "timezone": "America/Los_Angeles",
      "topic": "My Meeting",
      "tracking_fields": [
        {
          "field": "field1",
          "value": "value1"
        }
      ],
      "type": 2
    }
  4. 4. Response Json Sample의 예시 화면입니다.

    {
      "assistant_id": "kFFvsJc-Q1OSxaJQLvaa_A",
      "host_email": "jchill@example.com",
      "id": 92674392836,
      "registration_url": "https://example.com/meeting/register/7ksAkRCoEpt1Jm0wa-E6lICLur9e7Lde5oW6",
      "agenda": "My Meeting",
      "created_at": "2022-03-25T07:29:29Z",
      "duration": 60,
      "h323_password": "123456",
      "join_url": "https://example.com/j/11111",
      "occurrences": [
        {
          "duration": 60,
          "occurrence_id": "1648194360000",
          "start_time": "2022-03-25T07:46:00Z",
          "status": "available"
        }
      ],
      "password": "123456",
      "pmi": 97891943927,
      "pre_schedule": false,
      "recurrence": {
        "end_date_time": "2022-04-02T15:59:00Z",
        "end_times": 7,
        "monthly_day": 1,
        "monthly_week": 1,
        "monthly_week_day": 1,
        "repeat_interval": 1,
        "type": 1,
        "weekly_days": "1"
      },
      "settings": {
        "allow_multiple_devices": true,
        "alternative_hosts": "jchill@example.com;thill@example.com",
        "alternative_hosts_email_notification": true,
        "alternative_host_update_polls": true,
        "approval_type": 0,
        "approved_or_denied_countries_or_regions": {
          "approved_list": [
            "CX"
          ],
          "denied_list": [
            "CA"
          ],
          "enable": true,
          "method": "approve"
        },
        "audio": "telephony",
        "authentication_domains": "example.com",
        "authentication_exception": [
          {
            "email": "jchill@example.com",
            "name": "Jill Chill",
            "join_url": "https://example.com/s/11111"
          }
        ],
        "authentication_name": "Sign in to Zoom",
        "authentication_option": "signIn_D8cJuqWVQ623CI4Q8yQK0Q",
        "auto_recording": "cloud",
        "breakout_room": {
          "enable": true,
          "rooms": [
            {
              "name": "room1",
              "participants": [
                "jchill@example.com"
              ]
            }
          ]
        },
        "calendar_type": 1,
        "close_registration": false,
        "cn_meeting": false,
        "contact_email": "jchill@example.com",
        "contact_name": "Jill Chill",
        "custom_keys": [
          {
            "key": "key1",
            "value": "value1"
          }
        ],
        "email_notification": true,
        "encryption_type": "enhanced_encryption",
        "enforce_login": true,
        "enforce_login_domains": "example.com",
        "focus_mode": true,
        "global_dial_in_countries": [
          "US"
        ],
        "global_dial_in_numbers": [
          {
            "city": "New York",
            "country": "US",
            "country_name": "US",
            "number": "+1 1000200200",
            "type": "toll"
          }
        ],
        "host_video": true,
        "in_meeting": false,
        "jbh_time": 0,
        "join_before_host": true,
        "language_interpretation": {
          "enable": true,
          "interpreters": [
            {
              "email": "interpreter@example.com",
              "languages": "US,FR"
            }
          ]
        },
        "meeting_authentication": true,
        "mute_upon_entry": false,
        "participant_video": false,
        "private_meeting": false,
        "registrants_confirmation_email": true,
        "registrants_email_notification": true,
        "registration_type": 1,
        "show_share_button": true,
        "use_pmi": false,
        "waiting_room": false,
        "watermark": false,
        "host_save_video_order": true
      },
      "start_time": "2022-03-25T07:29:29Z",
      "start_url": "https://example.com/s/11111",
      "timezone": "America/Los_Angeles",
      "topic": "My Meeting",
      "tracking_fields": [
        {
          "field": "field1",
          "value": "value1",
          "visible": true
        }
      ],
      "type": 2
    }
3) API 회의 생성

ZOOM 직접 미팅을 만드는 것보다 훨씬 많은 옵션을 제공하고 있었습니다. 양이 많은 데이터로 각자의 옵션을 확인하는데 시간이 걸릴 것 같아 중요한 몇 가지 옵션에 대해서 정의하고 그에 대한 DTO(데이터 전송 객체 [Data transfer object])를 작성했습니다.

이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요

ZOOM API에 전달에 대하여 프로젝트에 필요한 옵션에 대해서 정의했으며, 부가적인 옵션을 자유롭게 사용할 수 있도록 settings 옵션은 Map을 이용하여 작성했습니다.

각 옵션 별로 간단한 설명을 적어봅니다.

private String agenda; --회의 의제 이 값의 최대 길이는 2,000자
private boolean default_password = false; --사용자 설정을 사용하여 기본 암호를 생성할지 여부
private int duration = 60; --회의의 예약된 시간(분).
private boolean pre_schedule = false; -- 예약된 반복 회의를 만들지 여부
private String schedule_for; -- 회의를 예약할 사용자의 이메일 주소 또는 사용자 ID
private String start_time; -- 회의 시작 시간 
			이 필드는 고정된 시간의 예약 및/또는 반복 회의에만 사용되며, 
			이것은 현지 시간 및 GMT 형식을 지원
private String timezone = "Asia/Seoul"; --start_time값 에 할당할 시간대 이 필드는 예약된 회의( 2)에만 사용
private String topic; --회의의 주제
private int type = 2; -- 회의타입 예약된회의(2)
private Map<String,Object> settings; --부가옵션
	settings.put("approval_type", 0); --승인유형 0— 등록을 자동으로 승인합니다.1— 수동으로 등록을 승인합니다.2— 등록이 필요하지 않습니다.            
	settings.put("join_before_host", true); --참가자가 호스트보다 먼저 미팅에 참여할 수 있는지 여부 
	settings.put("waiting_room", false); --대기실 기능 을 활성화할지 여부
	settings.put("registrants_email_notification", false); --등록자에게 등록 승인, 취소 또는 거부에 대한 이메일 알림을 보낼지 여부
	settings.put("alternative_hosts_email_notification", false); --호스트에게 알림 여부
	settings.put("email_notification", false); --이메일 알림여부

이 프로젝트에서 받는 부분은 RestTemplate을 사용하여 전송하고, JsonObject로 전달 받았습니다.

토큰 연동 소개

작업 중 갑작스런 이슈사항을 전달 받게 되었는데 고객사 부서에서 ZOOM 마스터 계정을 가지고 있지 않고, 상위 부서에서 관리하고 있어 JWT 만료 시 Token을 지속적으로 바꿔줄 수 부분을 할 수 없던 사항이 변수로 작용했습니다. 그리하여 JWT까지 자동으로 발급하는 형태로 소스를 변경 수정 하였습니다.

RestTemplate restTemplate = new RestTemplate(requestFactory);
	//RestTemplate restTemplate = new RestTemplate();
String result = null;
	
/*JWT 토큰 헤더 생성 */
	Map<String, Object> headers = new HashMap<>();
	headers.put("alg", "HS256");
	headers.put("typ", "JWT");
	Map<String, Object> payloads = new HashMap<>();
		
/*JWT 토큰 payloads 생성*/
  	payloads.put("aud", null);
	payloads.put("iss", ZOOM_KEY);
	Long expTm = 1000 * 600L;

/*JWT 토큰 생성 만료일 지정*/	
	Date ext = new Date();
	ext.setTime(ext.getTime()+expTm);
	
/*JWT 토큰 암호화 생성 */		
	String jst = Jwts.builder()
		.setHeader(headers)
		.setClaims(payloads)
		.setExpiration(ext)
		.signWith(SignatureAlgorithm.HS256, ZOOM_SECERET.getBytes())
		.compact();
		
/*JWT 토큰 을 헤더 Bearer로 저장 */	
	HttpHeaders header = new HttpHeaders();
	header.setContentType(MediaType.APPLICATION_JSON);
	header.set("authorization", "Bearer ".concat(jst));
		
	logger.info("ZOOM_KEY {}",ZOOM_KEY);
	logger.info("ZOOM_KEY {}",ZOOM_SECERET);
		
/*전송정보 URL 생성 */	
	HttpEntity<PgmPltfmBurnMtgVO> entity = new HttpEntity<>(pgmPltfmBurnMtgVO, header);
	String url = ZOOM_URL.concat("/v2/users/")
		.concat(ZOOM_USER_ID)
		.concat("/meetings");
	logger.info("jst : {} ",jst);
	logger.info("url : {}",url);

/*전송정보 RestFul 발송 */	
	result = Optional.ofNullable(restTemplate.postForEntity(url, entity, String.class))
		//.map(HttpEntity::getBody)
		.map(r -> {
		logger.info("header ",r.getHeaders());
		logger.info("StatusCode ",r.getStatusCode());
		logger.info("body ",r.getBody());
		return r.getBody();
					})
		.orElse(null);
/*받은 정보를 jsonObject로 변경 */
ObjectMapper objectMapper = new ObjectMapper();
	JSONObject json = new JSONObject(result);

/*json을 Map 변경 */		
	Map<String, Object> jsonMap =  objectMapper.readValue(json.toString(), Map.class);

통신 후 받은 정보에서 일부분을 미팅 정보로 업데이트 하였습니다.


"id": 92674392836, -- 발급받은 ID 저장
"join_url": "https://example.com/j/11111" 화상회의 접속 URL 정보 저장
이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
생성한 회의는 관리자 페이지에서 확인 가능합니다.

Webhooks 사용

앞서 말한 API 연계 외에 회의가 끝나고 자동으로 상태값을 바꿔주는 작업도 필요했습니다. 이 경우 적당한 방식으로 Webhooks이 최선이라 생각되어 연동하기로 준비합니다.

1) endpointURL 설정

앞서 설명해드린 APP 설정화면에 추가 작업이란 항목으로 설정을 할 수 있습니다.

이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요

상단에 토큰을 통해서 보안 설정을 할 수 있고, 이벤트 구독 중간에 Event notification endpoint URL 에서 결과 값을 돌려받을 수 있습니다. 이벤트 방식에 대해서는 이렇게 선택하여 확인이 가능합니다.

이미지 캡션이 필요하지 않을경우 alt값 내에 이미지 정보를 포함시켜주세요
2) Webhooks Reference

참고할만한 주소는 여기로 이동하면 자세하게 확인 할 수 있습니다.

마치며

Rest API는 Reference정리만 잘 되어있어도 손쉽게 데이터의 CRUD를 처리할 수 있는 HTTP Protocol입니다. WebHooks도 API와 마찬가지로 callback을 통한 역방향 API 통신으로 Reference 정리를 해두면 받아오는 곳 서비스 통신과 쉽게 연동할 수 있습니다. 이 포스팅을 통해 효율적으로 개발할 수 있는 밑거름이 되었으면 좋겠습니다.

한계점

다양한 옵션으로 통해서 허가된 사용자만 채팅에 들어올 수 있고 녹화 및 이력 등을 서버에 저장 할 수 있는 여러가지 기능을 제공하지만, 개발 기간의 한정으로 다양한 옵션을 사용하지 못한 부분이 아쉬움에 남습니다.

읽어주셔서 감사합니다. 혹시 이 글을 읽고 좀 더 이야기를 나누고 싶다면 help@inseq.co.kr으로 메일 부탁드릴게요! 😊

참고사이트

추일권 프로필 이미지

안녕하세요. 인시퀀스에서 Back-End Developer & DBA를 맡고 있는 추일권입니다. 개발 디테일 향상에 관심을 가지고 있습니다.