이번에 수원대학교 졸업 학점 계산 서비스
를 제공하는 척척학사에 백앤드 책임으로 참여하게 되었습니다.
제 역할은 우선 기존 Next.js로 작성된 Server 로직을 Java, Spring 환경으로 옮기는 것입니다.
서버 코드가 안정화 되기 전까지는 기존에 사용하던 Supabase를 사용해야 하는데, 사용 경험이 없어 글로 정리하며 공부하려고 합니다.
Supabase는 오픈소스 Firebase 대체 서비스로, 백엔드 기능을 손쉽게 구축할 수 있도록 지원하는 Backend-as-a-Service (Baas) 플랫폼이다. PostgreSQL을 기반으로 하며
등을 제공한다.
PostgreSQL 기반 데이터베이스
왜 PostgreSQL일까?
인증 & 권환 관리 (Authentication)
개인적으로 크게 감탄한 부분이다.
척척학사에선 Kakao 소셜 로그인 방식을 채택해서 사용하고 있는데, 기존 코드를 분석할 때 OAuth 2.0 설정 코드가 거의 없는 것을 발견하고 질문을 했습니다.
"OAuth 2.0으로 소셜 로그인 기능을 개발하신거 같은데 관련 코드는 어디에 있나요?"라는 질문에
"Supabase"가 소셜 로그인 기능을 지원해줍니다.
Spring Security OAuth 2.0으로 소셜 로그인을 구현해본 경험이 있기에 OAuth 플로우 처리, JWT 발급 및 관리가 꽤 까다롭다는 것을 알고 있습니다. 이걸 지원해준다니.. 정말 신세계였습니다.
물론 Spring Security의 장점은 명확합니다. 완전히 커스터마이징이 가능하고 자유로운 확장이 가능합니다. 상황에 따라 선택하면 될 것 같습니다.
스토리지 (Storage)
서버리스 함수 (Edge Functions)
Java용 SDK는 없기 때문에
1. Supabase의 REST API를 활용
방법 중 선택해야 한다.
나의 경우는 Spring Data JPA를 사용할 예정이라 PostgreSQL
을 직접 연결하여 사용하려고 한다.
PostgreSQL 연결을 통해 직접 사용
📌 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
PostgreSQL을 직접 연결하여 JPA를 활용하는 방식을 선택했으므로, 소셜 로그인(OAuth) 부분만 Supabase Auth를 그대로 활용하면 된다.
🎯 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 키로 서명 검증
- 사용자가 인증된 경우, 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());
}
}
}
그러나 추후 소셜 로그인 방식도 모두 백엔드로 이관했다.