[Springboot] Supabase Storage에 원본 이미지 업로드하기

유자·2026년 2월 28일

multi-modal-fraud-project

목록 보기
8/16

관계형 데이터베이스(PostgreSQL)에 이미지 파일의 이진 데이터를 직접 넣는 것은 성능상 매우 비효율적입니다. 따라서 실제 파일은 클라우드 스토리지인 Supabase Storage의 버킷에 업로드하고, 데이터베이스에는 해당 이미지를 렌더링할 수 있는 접속 URL만 텍스트로 저장하는 아키텍처를 구성하겠습니다.

0. 환경변수 및 @Value 세팅

본격적인 로직을 작성하기 전에, 보안을 위해 스토리지 주소와 API Key를 컨트롤러에 직접 하드코딩하지 않도록 설정해야 합니다. application.properties에 뼈대를 작성하고, 실제 중요 키값은 시스템 환경 변수로 주입받습니다.

supabase.storage.url=https://[본인프로젝트ID].supabase.co/storage/v1/object/[버킷이름]/
supabase.api.key=${SUPABASE_API_KEY}

그 후, 컨트롤러 클래스 상단에서 Spring이 제공하는 @Value 어노테이션(org.springframework.beans.factory.annotation.Value)을 사용하여 이 설정값들을 자바 변수로 안전하게 불러옵니다.

@RestController
public class ImageUploadController {

    @Value("${supabase.storage.url}")
    private String supabaseUrl;

    @Value("${supabase.api.key}")
    private String supabaseKey;
    
    @PostMapping("/upload-images")
    public String uploadImage(@RequestParam("file") MultipartFile file) {

1. 안전한 통신을 위한 try-catch 뼈대 만들기

파일의 byte 데이터를 읽어오거나 외부 네트워크(Supabase)와 통신하는 과정에서는 언제든 IOException 등의 예외가 발생할 수 있습니다. 자바에서는 이러한 예외 처리를 강제하므로, 먼저 try-catch 뼈대부터 만들어 줍니다. 이 뼈대 안에서 순차적으로 업로드 로직을 조립할 것입니다.

        try {
            // 이곳에 2, 3, 4, 5번의 세부 로직이 순서대로 들어갑니다
        } catch (Exception e) {
            e.printStackTrace();
            return "스토리지 업로드 중 에러 발생";
        }

2. 고유명(UUID) 생성과 최종 목적지 설정

try 블록 안에서 가장 먼저 할 일은 파일명 중복 방지입니다. 클라우드 스토리지는 동일한 경로에 같은 이름의 파일이 들어오면 기존 데이터를 덮어씌웁니다. 따라서 자바의 UUID(범용 고유 식별자)를 활용해 절대 겹치지 않는 무작위 문자열을 원본 파일명 앞에 붙여줍니다.

이때 UUID.randomUUID()는 문자열(String)이 아닌 UUID 객체를 반환하므로, 뒤에 오는 파일명과 결합하려면 반드시 .toString() 메서드를 명시적으로 호출해야 합니다.


String savedFileName = java.util.UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
// API 규격에 맞게 버킷 주소와 파일명을 결합하여 최종 업로드 엔드포인트 생성
String finalSupabaseUrl = supabaseUrl + savedFileName;

최종 목적지 URL을 supabaseUrl + savedFileName으로 결합하는 이유는 Supabase REST API의 파일 업로드 규격 때문입니다. Supabase API는 리소스를 생성할 때 단순히 '버킷 주소'까지만 POST 요청을 보내는 것이 아니라, 버킷주소/저장될_최종_파일명 형식으로 엔드포인트를 명확히 지정해야 해당 경로에 파일이 생성됩니다.

3. HTTP 통신을 위한 인증 헤더 세팅

목적지가 정해졌으니, Supabase API 서버와 통신하기 위한 HTTP 헤더를 구성합니다. HttpHeaders 객체를 생성하여 다음 핵심 정보들을 세팅합니다.

  • Authorization, apikey: 환경변수로 주입받은 API Key(supabaseKey)를 세팅합니다.
  • Content-Type: 클라이언트가 넘겨준 MultipartFile 객체에서 파일의 포맷(getContentType())을 추출해 명시합니다. 이때 값이 null로 넘어올 경우 발생할 수 있는 NullPointerException을 방어하기 위해 자바의 Objects.requireNonNull()을 사용하여 명시적으로 검증해 줍니다.
            // 3. 인증 키와 데이터 타입을 담은 HTTP 헤더 생성
            HttpHeaders supabaseHeaders = new HttpHeaders();
            supabaseHeaders.set("Authorization", "Bearer " + supabaseKey);
            supabaseHeaders.set("apikey", supabaseKey);
            // IDE 권장 방식인 Objects.requireNonNull을 활용해 널(null) 안전성 확보
            supabaseHeaders.setContentType(MediaType.valueOf(java.util.Objects.requireNonNull(file.getContentType())));

4. 데이터 바디(Body) 추출 및 HttpEntity 조립

가장 중요한 파일 데이터를 포장할 차례입니다. 앞서 다른 서버(ex. 파이썬 AI 서버)와 통신할 때는 MultiValueMap을 활용하여 multipart/form-data 규격으로 전송했습니다.

하지만 Supabase의 단일 파일 업로드 API는 이러한 폼(Form) 형태를 지원하지 않습니다. API 문서를 확인해 보면, 파라미터나 키값 없이 HTTP Request Body 전체를 순수한 파일의 이진 데이터(Binary Data)로만 채워서 전송하도록 설계되어 있습니다.

따라서 전송용 데이터 상자인 MultiValueMap을 쓰거나 file.getResource()를 호출하는 대신, file.getBytes() 메서드를 호출하여 파일의 순수한 바이트 배열(byte[]) 데이터만을 곧바로 추출해야 합니다. 그리고 이를 앞서 구성한 인증 헤더와 결합하여 전송용 객체인 HttpEntity로 포장합니다.

HttpEntity<byte[]> supabaseEntity = new HttpEntity<>(file.getBytes(), supabaseHeaders);

5. RestTemplate으로 POST 요청 전송 및 결과 확인

헤더와 바디의 조립이 끝났으므로, 이를 Supabase 엔드포인트로 전송합니다.

이때 Spring의 동기식 HTTP 통신 템플릿인 RestTemplate을 사용합니다. 단순히 POST 요청의 결과값만 받아오는 postForObject 대신, exchange 메서드를 사용했습니다.

외부 클라우드 API(Supabase)와 통신할 때는 인증 실패(401)나 권한 오류(403) 등 다양한 예외 상황이 발생할 수 있습니다. exchange 메서드를 사용하면 단순한 응답 본문(Body)뿐만 아니라, HTTP 상태 코드(Status Code)와 응답 헤더까지 포함된 ResponseEntity를 반환받을 수 있어 훨씬 안정적이고 상세한 예외 처리가 가능해집니다.

exchange 메서드에는 다음 4가지 파라미터를 순서대로 조립하여 던집니다.
1. 목적지 URL (finalSupabaseUrl): 앞서 만든 최종 엔드포인트 문자열
2. HTTP 메서드 (HttpMethod.POST): 리소스를 생성하므로 POST 방식 지정
3. 요청 데이터 (supabaseEntity): 방금 조립한 헤더(인증키)와 바디(byte 배열)의 묶음
4. 응답 타입 (String.class): Supabase 서버가 돌려줄 응답을 어떤 자바 타입으로 받을지 지정

RestTemplate restTemplate = new RestTemplate();
org.springframework.http.ResponseEntity<String> supabaseResponse = restTemplate.exchange(
              finalSupabaseUrl,
              org.springframework.http.HttpMethod.POST,
              supabaseEntity,
              String.class
);

// 통신 결과 검증을 위해 서버가 반환한 실제 응답 본문 출력
System.out.println("Supabase 응답 결과: " + supabaseResponse.getBody());

0개의 댓글