[척척학사] Supabase를 알아보자 With Java, Spring

박상민·2025년 2월 27일
0

척척학사

목록 보기
1/15
post-thumbnail

⭐️ 들어가기 전

이번에 수원대학교 졸업 학점 계산 서비스를 제공하는 척척학사에 백앤드 책임으로 참여하게 되었습니다.
제 역할은 우선 기존 Next.js로 작성된 Server 로직을 Java, Spring 환경으로 옮기는 것입니다.

서버 코드가 안정화 되기 전까지는 기존에 사용하던 Supabase를 사용해야 하는데, 사용 경험이 없어 글로 정리하며 공부하려고 합니다.

⭐️ Supabase?

Supabase는 오픈소스 Firebase 대체 서비스로, 백엔드 기능을 손쉽게 구축할 수 있도록 지원하는 Backend-as-a-Service (Baas) 플랫폼이다. PostgreSQL을 기반으로 하며

  • 실시간 데이터베이스
  • 인증
  • 스토리지
  • 서버리스 함수

등을 제공한다.

📌 주요 기능

PostgreSQL 기반 데이터베이스

  • 완전 관리형 PostgreSQL
    • 강력한 확장성, ACID 트랜잭션 지원
    • PostgreSQL의 고급 기능 활용 가능

      왜 PostgreSQL일까?

  • 자동 API 생성
    • 테이블을 생성하면 자동으로 RESTful API가 생성
  • 리얼타임 데이터
    • PostgreSQl의 Realtime 기능을 활용해 WebSocket 기반으로 실시간 데이터 동기화 가능

인증 & 권환 관리 (Authentication)

  • OAuth 지원
    • Google, Github, Apple, Kakao 등 다양한 소셜 로그인 지원

      개인적으로 크게 감탄한 부분이다.
      척척학사에선 Kakao 소셜 로그인 방식을 채택해서 사용하고 있는데, 기존 코드를 분석할 때 OAuth 2.0 설정 코드가 거의 없는 것을 발견하고 질문을 했습니다.
      "OAuth 2.0으로 소셜 로그인 기능을 개발하신거 같은데 관련 코드는 어디에 있나요?"라는 질문에
      "Supabase"가 소셜 로그인 기능을 지원해줍니다.
      Spring Security OAuth 2.0으로 소셜 로그인을 구현해본 경험이 있기에 OAuth 플로우 처리, JWT 발급 및 관리가 꽤 까다롭다는 것을 알고 있습니다. 이걸 지원해준다니.. 정말 신세계였습니다.
      물론 Spring Security의 장점은 명확합니다. 완전히 커스터마이징이 가능하고 자유로운 확장이 가능합니다. 상황에 따라 선택하면 될 것 같습니다.

  • Magic Link 로그인
    • 이메일을 통해 로그이할 수 있는 비밀번호 없는 로그인 방식 지원
  • OTP, WebAuthn, SAML 지원

스토리지 (Storage)

  • 파일 스토리지
    • 이미지, 비디오, 문서 등 업로드 및 관리 기능
  • RLS(Role Level Security 적용 가능)
    • 사용자의 접근 권한을 PostgreSQL 수준에서 관리 가능

서버리스 함수 (Edge Functions)

  • V8 엔진 기반의 서버리스 함수
    • Next.js의 API Routes처럼 서버리스 로직을 작성 가능
  • TypeScript 지원
    • JavaScript와 TypeScript로 작성 가능
  • PostgreSQL과 직접 통합 가능
    • 데이터베이스 트리거와 함께 사용 가능

📌 Java, Spring 환경에서는 어떻게 사용할까?

Java용 SDK는 없기 때문에
1. Supabase의 REST API를 활용

  • Supabase는 데이터베이스, 인증, 스토리지 등의 기능을 REST API 형태로 제공하므로, Spring에서 HTTP 요청을 보내 활용 가능
  • RestTemplate or WebClient를 사용하여 API 호출
  1. PostgreSQL 연결을 통해 직접 사용
  • Supabase는 기본적으로 PostgreSQL 기반으로, JDBC, JPA, MyBatis 등을 사용해 직접 DB 연결 후 데이터 활용 가능

방법 중 선택해야 한다.
나의 경우는 Spring Data JPA를 사용할 예정이라 PostgreSQL을 직접 연결하여 사용하려고 한다.

[Spring Data JPA + Supabase를 함께 활용하는 방법]

PostgreSQL 연결을 통해 직접 사용

  • 기존의 JPA, Hibernate 방식 그대로 사용 가능
  • Supabase를 PostgreSQL 호스팅 서비스로 활용
  • Native Query, QueryDSL 등도 사용 가능

📌 Step 1: Supabase PostgreSQL 연결 정보 확인
1. Supabase 로그인 -> 프로젝트 선택
2. Settings -> Database -> Connection info 확인

예시

Host: project.supabase.co  
Database: postgres  
User: postgres  
Password: db-password  
Port: 5432  

📌 Step 2: application.yml에서 PostgreSQL 연결 설정
Spring Boot에서 PostgreSQL에 직접 연결하도록 application.yml 설정

spring:
  datasource:
    url: jdbc:postgresql://project.supabase.co:5432/postgres
    username: postgres
    password: db-password
    driver-class-name: org.postgresql.Driver
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    hibernate:
      ddl-auto: update // 상황에 따라 주의해서 변경
    show-sql: true

[기존의 Supabase 소셜 로그인 방식은 어떻게 할까?]

PostgreSQL을 직접 연결하여 JPA를 활용하는 방식을 선택했으므로, 소셜 로그인(OAuth) 부분만 Supabase Auth를 그대로 활용하면 된다.

  • 프론트엔드에서는 Supabase의 소셜 로그인 기능을 유지
  • 백엔드(Spring)에서는 Supabase가 발급한 JWT를 검증하고 사용자 데이터를 관리

🎯 Spring Boot + TypeScript(Supabase Auth) OAuth 로그인 방식 플로우
1. 프론트엔드(TypeScript)에서 Supabase Auth를 통해 소셜 로그인 진행
2. Supabase가 JWT(Access Token) 발급
3. 프론트엔드에서 이 JWT를 백엔드(Spring)로 전달
4. Spring에서 Supabase의 JWT를 검증하고, 유저 데이터를 JPA로 관리
5. 필요하면 DB에 사용자 정보 저장 및 추가 처리

FE 환경에서 API 요청마다 JWT를 기반으로 인증된 사용자인지 확인하고 있는데 Spring 환경에서는?
FE와 같은 방식으로 JWT를 확인하고 사용자 인증을 진행하면 된다.
1. FE에서 Supabase JWT를 BE로 전달
2. BE에서 Supabase의 JWT를 검증

  • jwt 라이브러리 사용
  • Supabase에서 발급한 Secret 키로 서명 검증
  1. 사용자가 인증된 경우, API 요청 허용

검증 JWT 필터 예시

public class SupabaseJwtFilter extends GenericFilterBean {

    private final String SUPABASE_JWT_SECRET = "supabase-jwt-secret"; // ignore 처리 필요

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        String token = httpRequest.getHeader("Authorization"); // 헤더에서 토큰 정보 가져오기

        if (token == null || !token.startsWith("Bearer ")) { // 토큰이 null 이거나 보안 규칙(Bearer Start)을 어길시
            httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpResponse.getWriter().write("Unauthorized - Token is missing");
            return;
        }

        try {
            token = token.replace("Bearer ", "");

            // JWT 서명 검증
            Algorithm algorithm = Algorithm.HMAC256(SUPABASE_JWT_SECRET);
            DecodedJWT jwt = JWT.require(algorithm).build().verify(token); // Supabase에서 발급한 키로 서명 검증

            // 만료 시간 확인
            if (jwt.getExpiresAt().before(new Date())) {
                httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpResponse.getWriter().write("Unauthorized - Token expired");
                return;
            }

            // 블랙리스트 확인 (로그아웃된 토큰인지 체크)
            String jti = jwt.getClaim("jti").asString();
            if (BlacklistService.isBlacklisted(jti)) {
                httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpResponse.getWriter().write("Unauthorized - Token is revoked");
                return;
            }

            // 사용자 검증 (DB에서 조회)
            String email = jwt.getClaim("email").asString();
            if (!userService.existsByEmail(email)) {
                httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
                httpResponse.getWriter().write("Unauthorized - User not found");
                return;
            }

            // 요청 계속 진행
            chain.doFilter(request, response);

        } catch (JWTVerificationException e) {
            httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpResponse.getWriter().write("Unauthorized - Invalid JWT: " + e.getMessage());
        }
    }
}

그러나 추후 소셜 로그인 방식도 모두 백엔드로 이관했다.

0개의 댓글