DataGSM OAuth SDK Java / Kotlin

개요

DataGSM OAuth SDK Java는 DataGSM OAuth 인증을 Java 및 Kotlin 애플리케이션에서 쉽고 안전하게 구현할 수 있도록 설계된 공식 클라이언트 SDK입니다.

이 SDK를 사용하면 복잡한 OAuth 인증 플로우를 직접 구현할 필요 없이, 간단한 인터페이스를 통해 Authorization Code 교환, PKCE 지원, 토큰 갱신, 사용자 데이터 조회 등의 기능을 손쉽게 사용할 수 있습니다.

주요 특징

특징설명
타입 안전성강타입 시스템으로 컴파일 타임에 오류를 검출하여 런타임 에러를 방지합니다.
Kotlin 완벽 호환Java와 Kotlin 프로젝트 모두에서 자연스럽게 사용할 수 있습니다.
빌더 패턴직관적이고 유연한 클라이언트 구성을 지원합니다.
자동 리소스 관리AutoCloseable 구현으로 리소스 누수를 자동으로 방지합니다.
PKCE 지원RFC 7636 표준 PKCE를 지원하여 모바일/SPA 앱에서 안전하게 사용할 수 있습니다.
다양한 Grant 타입 지원Authorization Code, Refresh Token, Client Credentials Grant를 지원합니다.
구체적인 예외 처리HTTP 상태 코드별 전용 예외 타입을 제공하여 정확한 에러 핸들링이 가능합니다.
검증된 HTTP 클라이언트OkHttp 기반으로 안정적이고 효율적인 통신을 보장합니다.

시스템 요구사항

항목최소 버전권장 버전
JavaJDK 13JDK 25 이상
Kotlin1.3.722.3.0 이상
Gradle6.09.1.0 이상
Maven3.6.33.9.9 이상

설치

프로젝트 빌드 도구에 맞는 의존성을 추가하세요.

dependencies {
  implementation("com.github.themoment-team:datagsm-oauth-sdk-java:1.1.0")
}

repositories {
  maven { url = uri("https://jitpack.io") }
}

빠른 시작

1. 클라이언트 초기화

DataGsmOAuthClient 인스턴스를 생성합니다. Client ID와 Client Secret은 필수 파라미터입니다.

import team.themoment.datagsm.sdk.oauth.DataGsmOAuthClient;

// 기본 설정으로 클라이언트 생성
DataGsmOAuthClient client = DataGsmOAuthClient.builder("your-client-id", "your-client-secret").build();

// 커스텀 Base URL 설정 (선택사항)
DataGsmOAuthClient clientWithCustomUrl = DataGsmOAuthClient.builder("your-client-id", "your-client-secret")
  .authorizationBaseUrl("https://oauth.data.hellogsm.kr")
  .userInfoBaseUrl("https://oauth-userinfo.data.hellogsm.kr")
  .build();

2. OAuth 인증 플로우

DataGSM OAuth는 Authorization Code Grant 방식을 사용합니다.

Authorization URL 생성

사용자를 DataGSM OAuth 로그인 페이지로 리다이렉트하기 위한 URL을 생성합니다.

import team.themoment.datagsm.sdk.oauth.model.AuthorizationUrlBuilder;

String redirectUri = "https://myapp.com/callback";

AuthorizationUrlBuilder urlBuilder = client.createAuthorizationUrl(redirectUri)
  .state("random-csrf-token") // CSRF 방지 (권장)
  .scope("read write");       // Scope 설정 (선택)

String authorizationUrl = urlBuilder.build();
// 사용자를 authorizationUrl로 리다이렉트

Authorization Code를 토큰으로 교환

import team.themoment.datagsm.sdk.oauth.model.TokenResponse;

// 콜백에서 받은 Authorization Code와 Redirect URI로 토큰 교환
String authorizationCode = "사용자로부터_받은_인증_코드";
String redirectUri = "https://myapp.com/callback";
TokenResponse tokenResponse = client.exchangeCodeForToken(authorizationCode, redirectUri);

// 토큰 사용
String accessToken = tokenResponse.getAccessToken();
String refreshToken = tokenResponse.getRefreshToken();
String tokenType = tokenResponse.getTokenType();   // 보통 "Bearer"
Long expiresIn = tokenResponse.getExpiresIn();     // 만료 시간(초)

토큰 갱신

// Refresh Token으로 새로운 토큰 발급
TokenResponse newTokenResponse = client.refreshToken(refreshToken);

String newAccessToken = newTokenResponse.getAccessToken();
String newRefreshToken = newTokenResponse.getRefreshToken();

// Scope를 줄여서 갱신 (다운스코핑)
TokenResponse limitedTokenResponse = client.refreshToken(refreshToken, "read");

3. 사용자 데이터 조회

import team.themoment.datagsm.sdk.oauth.model.UserInfo;
import team.themoment.datagsm.sdk.oauth.model.Student;

// Access Token으로 사용자 데이터 조회
UserInfo userInfo = client.getUserInfo(accessToken);

// 기본 사용자 데이터
System.out.println("ID: " + userInfo.getId());
System.out.println("이메일: " + userInfo.getEmail());
System.out.println("역할: " + userInfo.getRole());

// 학생인 경우 추가 데이터 조회
if (userInfo.isStudent()) {
  Student student = userInfo.getStudent();
  System.out.println("이름: " + student.getName());
  System.out.println("학번: " + student.getStudentNumber());
  System.out.println("학년: " + student.getGrade());
  System.out.println("반: " + student.getClassNum());
  System.out.println("전공: " + student.getMajor());
}

PKCE 지원

PKCE(Proof Key for Code Exchange, RFC 7636)는 모바일 앱, SPA 등 Public Client 환경에서 Authorization Code 탈취 공격을 방지합니다.

PKCE 플로우

import team.themoment.datagsm.sdk.oauth.model.AuthorizationUrlBuilder;

String redirectUri = "myapp://callback";

// Step 1: PKCE가 활성화된 Authorization URL 생성
AuthorizationUrlBuilder urlBuilder = client.createAuthorizationUrl(redirectUri)
  .enablePkce()              // PKCE 자동 생성 (S256 메소드)
  .state("random-state");

String authorizationUrl = urlBuilder.build();
String codeVerifier = urlBuilder.getCodeVerifier(); // 반드시 저장!

// 사용자를 authorizationUrl로 리다이렉트

// Step 2: 콜백에서 Authorization Code를 받은 후 Code Verifier와 함께 토큰 교환
String authorizationCode = "received-code-from-callback";
TokenResponse tokenResponse = client.exchangeCodeForToken(
  authorizationCode,
  redirectUri,
  codeVerifier  // PKCE 검증용
);

System.out.println("Access Token: " + tokenResponse.getAccessToken());

수동 PKCE 설정

// 수동으로 Code Verifier를 지정하는 경우
AuthorizationUrlBuilder urlBuilder = client.createAuthorizationUrl(redirectUri)
  .enablePkce("my-custom-code-verifier-43-to-128-chars", "S256"); // S256 또는 plain

Client Credentials Grant

사용자 인증 없이 클라이언트 자격증명만으로 토큰을 발급합니다. 서버-to-서버 통신에 사용합니다.

// 기본 scope로 토큰 발급
TokenResponse tokenResponse = client.getClientCredentialsToken();

// scope를 지정하여 토큰 발급
TokenResponse tokenResponseWithScope = client.getClientCredentialsToken("read");

System.out.println("Access Token: " + tokenResponse.getAccessToken());
System.out.println("Token Type: " + tokenResponse.getTokenType());
System.out.println("Expires In: " + tokenResponse.getExpiresIn());
// Client Credentials Grant에서는 Refresh Token이 발급되지 않습니다.

API 레퍼런스

SDK는 다음과 같은 API 메서드를 제공합니다.

OAuth API

client.createAuthorizationUrl(redirectUri) - Authorization URL 생성

파라미터타입설명
redirectUriString인증 후 돌아올 Redirect URI
반환 타입설명
AuthorizationUrlBuilderURL 빌더 (state, scope, PKCE 등 추가 설정 가능)

client.exchangeCodeForToken(code, redirectUri) - Authorization Code를 토큰으로 교환

파라미터타입설명
codeStringAuthorization Code
redirectUriString원본 Authorization 요청에 사용한 Redirect URI
반환 타입설명
TokenResponseAccess Token, Refresh Token, 만료 시간 등 포함

client.exchangeCodeForToken(code, redirectUri, codeVerifier) - PKCE 포함 토큰 교환

파라미터타입설명
codeStringAuthorization Code
redirectUriString원본 Authorization 요청에 사용한 Redirect URI
codeVerifierStringAuthorization URL 생성 시 생성한 Code Verifier
반환 타입설명
TokenResponse새로운 토큰 응답

client.refreshToken(refreshToken) - 토큰 갱신

파라미터타입설명
refreshTokenStringRefresh Token
반환 타입설명
TokenResponse새로운 토큰 응답

client.refreshToken(refreshToken, scope) - Scope를 지정하여 토큰 갱신

파라미터타입설명
refreshTokenStringRefresh Token
scopeString요청할 권한 범위 (원본보다 좁아야 함)

client.getClientCredentialsToken() - Client Credentials로 토큰 발급

반환 타입설명
TokenResponse토큰 응답 (Refresh Token 없음)

client.getClientCredentialsToken(scope) - Scope를 지정하여 Client Credentials 토큰 발급

파라미터타입설명
scopeString요청할 권한 범위

Account API

client.getUserInfo(accessToken) - 사용자 데이터 조회

파라미터타입설명
accessTokenStringAccess Token
반환 타입설명
UserInfo사용자 데이터 (학생인 경우 학생 데이터 포함)

모델 레퍼런스

TokenResponse

OAuth 토큰 응답 모델 (RFC 6749)

필드타입설명
accessTokenStringAccess Token (필수)
tokenTypeStringToken 타입, 보통 "Bearer" (필수)
expiresInLongAccess Token 만료 시간 (초 단위, 선택)
refreshTokenStringRefresh Token (선택, Client Credentials에서는 없음)
scopeString부여된 권한 범위 (선택)

AuthorizationUrlBuilder

Authorization URL 빌더 모델

메서드설명
redirectUri(String)Redirect URI 설정 (필수)
state(String)State 파라미터 설정 (CSRF 방지 권장)
scope(String)Scope 설정 (선택)
enablePkce()PKCE 자동 활성화 (S256 메소드)
enablePkce(String, String)PKCE 수동 활성화 (codeVerifier, method 지정)
build()Authorization URL 생성
getCodeVerifier()생성된 Code Verifier 조회 (PKCE 사용 시)

UserInfo

사용자 데이터 모델

필드타입설명
idLong사용자 고유 ID
emailString이메일 주소
roleAccountRole계정 역할
isStudentBoolean학생 여부
studentStudent학생 데이터 (학생인 경우)

Student

학생 데이터 모델

필드타입설명
idLong학생 고유 ID
nameString이름
sexSex성별
emailString이메일
gradeInteger학년
classNumInteger
numberInteger번호
studentNumberInteger학번
majorMajor전공
roleStudentRole학생 역할
dormitoryFloorInteger기숙사 층
dormitoryRoomInteger기숙사 호실
majorClubClubInfo전공동아리 데이터
autonomousClubClubInfo창체동아리 데이터

ClubInfo

동아리 데이터 모델

필드타입설명
idLong동아리 고유 ID
nameString동아리 이름
typeClubType동아리 유형

Enum 타입

AccountRole

설명
ROOT최고 관리자
ADMIN관리자
USER일반 사용자
API_KEY_USERAPI 키 사용자

Sex

설명
MAN남성
WOMAN여성

Major

설명
SW_DEVELOPMENT소프트웨어개발과
SMART_IOT스마트IoT과
AI인공지능과

StudentRole

설명
STUDENT_COUNCIL학생회
DORMITORY_MANAGER기숙사자치위원회
GENERAL_STUDENT일반학생
GRADUATE졸업생
WITHDRAWN자퇴생

ClubType

설명
MAJOR_CLUB전공동아리
AUTONOMOUS_CLUB창체동아리

예외 처리

SDK는 HTTP 상태 코드별로 구체적인 예외 타입을 제공합니다.

예외 종류

예외 타입HTTP 상태발생 상황권장 처리 방법
UnauthorizedException401토큰이 유효하지 않음 또는 만료토큰 갱신 또는 재인증
ForbiddenException403필요한 권한 범위 부족권한 범위 확인 및 요청
BadRequestException400잘못된 요청 파라미터요청 파라미터 검증
NotFoundException404요청한 리소스를 찾을 수 없음요청 경로 확인
RateLimitException429Rate limit 초과재시도 로직 구현 (Exponential Backoff)
ServerErrorException500서버 내부 오류잠시 후 재시도 또는 관리자 문의
DataGsmException기타기타 API 오류에러 메시지 확인 후 적절한 처리

예외 처리 예제

import team.themoment.datagsm.sdk.oauth.exception.*;

try {
  UserInfo userInfo = client.getUserInfo(accessToken);
  // 성공적인 응답 처리
} catch (UnauthorizedException e) {
  // 토큰 만료 - 갱신 필요
  logger.error("Token expired or invalid", e);
  TokenResponse newToken = client.refreshToken(refreshToken);
} catch (ForbiddenException e) {
  // 권한 범위 부족
  logger.warn("Insufficient permissions", e);
} catch (RateLimitException e) {
  // Rate limit 초과 - 재시도 로직 구현 필요
  logger.warn("Rate limit exceeded", e);
  // exponential backoff 등의 재시도 전략 구현 권장
} catch (BadRequestException e) {
  // 잘못된 요청 파라미터
  logger.error("Invalid request parameters: {}", e.getMessage());
} catch (NotFoundException e) {
  // 리소스를 찾을 수 없음
  logger.error("Resource not found: {}", e.getMessage());
} catch (ServerErrorException e) {
  // 서버 오류
  logger.error("Server error occurred", e);
} catch (DataGsmException e) {
  // 기타 예외
  logger.error("API error: {}", e.getMessage(), e);
}

리소스 관리

SDK는 AutoCloseable 인터페이스를 구현하여 자동 리소스 정리를 지원합니다.

Try-with-resources 사용

// Java의 try-with-resources 사용
try (DataGsmOAuthClient client = DataGsmOAuthClient.builder("your-client-id", "your-client-secret").build()) {
  TokenResponse token = client.exchangeCodeForToken(code, redirectUri);
  UserInfo userInfo = client.getUserInfo(token.getAccessToken());
  // 클라이언트 사용
} // 블록이 끝나면 자동으로 리소스 정리

Spring Boot 프로젝트에서 사용하기

Spring Boot 애플리케이션에서는 DataGsmOAuthClient를 Bean으로 등록하여 의존성 주입 방식으로 사용할 수 있습니다.

Configuration 클래스 작성

package com.example.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import team.themoment.datagsm.sdk.oauth.DataGsmOAuthClient;

@Configuration
public class DataGsmOAuthConfig {

  @Bean
  @ConfigurationProperties(prefix = "datagsm.oauth")
  public DataGsmOAuthProperties dataGsmOAuthProperties() {
      return new DataGsmOAuthProperties();
  }

  @Bean
  public DataGsmOAuthClient dataGsmOAuthClient(DataGsmOAuthProperties properties) {
      return DataGsmOAuthClient.builder(properties.getClientId(), properties.getClientSecret())
          .authorizationBaseUrl(properties.getAuthorizationBaseUrl())
          .userInfoBaseUrl(properties.getUserInfoBaseUrl())
          .build();
  }

  public static class DataGsmOAuthProperties {
      private String clientId;
      private String clientSecret;
      private String authorizationBaseUrl;
      private String userInfoBaseUrl;

      public String getClientId() { return clientId; }
      public void setClientId(String clientId) { this.clientId = clientId; }
      public String getClientSecret() { return clientSecret; }
      public void setClientSecret(String clientSecret) { this.clientSecret = clientSecret; }
      public String getAuthorizationBaseUrl() { return authorizationBaseUrl; }
      public void setAuthorizationBaseUrl(String url) { this.authorizationBaseUrl = url; }
      public String getUserInfoBaseUrl() { return userInfoBaseUrl; }
      public void setUserInfoBaseUrl(String url) { this.userInfoBaseUrl = url; }
  }

}

프로파일 설정

datagsm:
  oauth:
    client-id: ${DATAGSM_CLIENT_ID}
    client-secret: ${DATAGSM_CLIENT_SECRET} # 환경 변수에서 로드
    authorization-base-url: https://oauth.data.hellogsm.kr
    user-info-base-url: https://oauth-userinfo.data.hellogsm.kr

Service 클래스에서 사용

package com.example.service;

import org.springframework.stereotype.Service;
import team.themoment.datagsm.sdk.oauth.DataGsmOAuthClient;
import team.themoment.datagsm.sdk.oauth.model.*;

@Service
public class AuthService {

  private final DataGsmOAuthClient dataGsmOAuthClient;

  public AuthService(DataGsmOAuthClient dataGsmOAuthClient) {
      this.dataGsmOAuthClient = dataGsmOAuthClient;
  }

  public TokenResponse authenticate(String authorizationCode, String redirectUri) {
      return dataGsmOAuthClient.exchangeCodeForToken(authorizationCode, redirectUri);
  }

  public UserInfo getCurrentUser(String accessToken) {
      return dataGsmOAuthClient.getUserInfo(accessToken);
  }

  public TokenResponse refreshUserToken(String refreshToken) {
      return dataGsmOAuthClient.refreshToken(refreshToken);
  }

}

일반 Java 프로젝트에서 사용하기

Spring Boot를 사용하지 않는 일반 Java 애플리케이션에서는 직접 클라이언트를 생성하여 사용합니다.

전체 예제

import team.themoment.datagsm.sdk.oauth.DataGsmOAuthClient;
import team.themoment.datagsm.sdk.oauth.exception.*;
import team.themoment.datagsm.sdk.oauth.model.*;

public class Main {
  public static void main(String[] args) {
      // try-with-resources로 자동 리소스 관리
      try (DataGsmOAuthClient client = DataGsmOAuthClient.builder("your-client-id", "your-client-secret").build()) {

          // 1. Authorization URL 생성 및 사용자 리다이렉트
          String redirectUri = "https://myapp.com/callback";
          String authUrl = client.createAuthorizationUrl(redirectUri)
              .state("random-csrf-token")
              .enablePkce()
              .build();
          // 사용자를 authUrl로 리다이렉트 (실제 구현 필요)

          // 2. Authorization Code로 토큰 교환
          String authCode = "authorization_code_from_callback";
          TokenResponse tokenResponse = client.exchangeCodeForToken(authCode, redirectUri);

          System.out.println("Access Token 발급 완료");
          System.out.printf("만료 시간: %d초%n", tokenResponse.getExpiresIn());

          // 3. 사용자 데이터 조회
          UserInfo userInfo = client.getUserInfo(tokenResponse.getAccessToken());

          System.out.printf("사용자 ID: %d%n", userInfo.getId());
          System.out.printf("이메일: %s%n", userInfo.getEmail());
          System.out.printf("역할: %s%n", userInfo.getRole());

          // 4. 학생 데이터 조회 (학생인 경우)
          if (userInfo.isStudent()) {
              Student student = userInfo.getStudent();
              System.out.printf("이름: %s%n", student.getName());
              System.out.printf("학번: %d%n", student.getStudentNumber());
              System.out.printf("학년/반/번호: %d학년 %d반 %d번%n",
                  student.getGrade(), student.getClassNum(), student.getNumber());
              System.out.printf("전공: %s%n", student.getMajor());

              // 동아리 데이터
              if (student.getMajorClub() != null) {
                  System.out.printf("전공동아리: %s%n", student.getMajorClub().getName());
              }
          }

          // 5. 토큰 갱신
          TokenResponse newToken = client.refreshToken(tokenResponse.getRefreshToken());
          System.out.println("토큰 갱신 완료");

      } catch (UnauthorizedException e) {
          System.err.println("인증 오류: 토큰이 유효하지 않습니다.");
          e.printStackTrace();
      } catch (RateLimitException e) {
          System.err.println("요청 제한 초과: 잠시 후 다시 시도해주세요.");
          e.printStackTrace();
      } catch (DataGsmException e) {
          System.err.println("API 오류 발생: " + e.getMessage());
          e.printStackTrace();
      } catch (Exception e) {
          System.err.println("예상치 못한 오류 발생: " + e.getMessage());
          e.printStackTrace();
      }
  }

}

보안

Client Secret 관리

Client ID와 Client Secret은 민감한 데이터이므로 절대 코드에 직접 하드코딩하지 마세요. 환경 변수나 설정 파일을 통해 안전하게 관리하세요.

환경 변수 사용 (권장)

public class Main {
  public static void main(String[] args) {
      // 환경 변수에서 Client ID, Secret 로드
      String clientId = System.getenv("DATAGSM_CLIENT_ID");
      String clientSecret = System.getenv("DATAGSM_CLIENT_SECRET");

      if (clientId == null || clientId.isEmpty()) {
          System.err.println("환경 변수 DATAGSM_CLIENT_ID가 설정되지 않았습니다.");
          System.exit(1);
      }
      if (clientSecret == null || clientSecret.isEmpty()) {
          System.err.println("환경 변수 DATAGSM_CLIENT_SECRET이 설정되지 않았습니다.");
          System.exit(1);
      }

      try (DataGsmOAuthClient client = DataGsmOAuthClient.builder(clientId, clientSecret).build()) {
          // OAuth 사용
      }
  }

}

환경 변수 설정 방법

Linux/macOS

export DATAGSM_CLIENT_ID="your-client-id-here"
export DATAGSM_CLIENT_SECRET="your-client-secret-here"
java -jar your-application.jar

Windows (PowerShell)

$env:DATAGSM_CLIENT_ID="your-client-id-here"
$env:DATAGSM_CLIENT_SECRET="your-client-secret-here"
java -jar your-application.jar

토큰 보안 가이드라인

  • Access Token: 짧은 유효기간을 가지며, 메모리에서만 관리하세요.
  • Refresh Token: 안전한 저장소(암호화된 DB, Secure Storage 등)에 보관하세요.
  • PKCE 사용: 모바일/SPA 앱에서는 반드시 PKCE를 활성화하여 Code 탈취를 방지하세요.
  • HTTPS 사용: 모든 통신은 반드시 HTTPS를 통해 이루어져야 합니다.
  • State 파라미터: Authorization URL 생성 시 CSRF 방지를 위해 state 파라미터를 설정하세요.

마이그레이션 가이드

1.0.x → 1.1.0

버전 1.0.1부터 DataGsmClient가 deprecated되고 DataGsmOAuthClient로 대체되었습니다.

변경 전 (1.0.x)변경 후 (1.1.0)
DataGsmClientDataGsmOAuthClient
builder("client-secret")builder("client-id", "client-secret")
exchangeToken(code)exchangeCodeForToken(code, redirectUri)
client.refreshToken(token)client.refreshToken(token) (동일, scope 오버로드 추가)

문제 해결

자주 발생하는 문제

401 Unauthorized 오류

원인: Access Token이 유효하지 않거나 만료됨

해결 방법:

  • Access Token의 유효기간 확인 (tokenResponse.getExpiresIn())
  • refreshToken() 메서드로 새 토큰 발급
  • Refresh Token도 만료된 경우 재인증 필요

403 Forbidden 오류

원인: 요청한 리소스에 대한 권한 범위 부족

해결 방법:

  • OAuth 권한 범위 확인
  • 필요한 권한 범위가 부여되었는지 확인
  • 권한 범위 추가가 필요한 경우 관리자에게 문의

400 Bad Request 오류

원인: 잘못된 요청 파라미터

해결 방법:

  • Authorization Code가 유효한지 확인
  • Code가 이미 사용되었는지 확인 (일회용)
  • Client ID, Client Secret이 올바른지 확인
  • redirectUri가 Authorization 요청 시와 동일한지 확인
  • PKCE 사용 시 codeVerifier가 올바른지 확인

추가 리소스

문의사항이나 버그 리포트는 GitHub Issues를 통해 제출해 주세요.