영상 스트리밍 #1. 기본 파일 업로드

Bobby·2023년 2월 24일
3

streaming

목록 보기
1/9

🎥 multipart/form-data

  • multipart/form-data 타입은 이름에서 알 수 있듯 form-data의 여러 파트(multipart)를 전송 한다.

basic.html

...

<form method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="text" name="desc">
    <input type="submit"/>
</form>

...
  • 이처럼 form 데이터 안에 file 타입도 있고 text 타입도 있다. 이렇게 여러가지 타입의 데이터들을 전송 하기 위한 타입이다.
  • 그냥 여러 타입을 보내기만 한다면 받아서 처리할 때 file 타입 인지 text 타입인지 구별 할 수 없기 때문에 multipart/form-data 를 바디에 담아 보낼 때 정해진 규칙이 있다.

  • 요청 헤더에 다음과 같이 multipart/form-data 타입으로 보낸다.
  • 여기서의 boundary 값은 여러 타입의 데이터들을 구분 짓는 구분자 역할을 한다.

  • 이렇게 boundary 값을 기준으로 타입이 뭔지, 그 안에 값은 뭔지 담아서 전송하기 때문에 서버에서 받아서 처리 할 때 타입에 맞도록 디코딩 할 수 있다.
  • 맨 마지막 줄의 boundary 값 뒤에 -- 값은 종료를 알리는 값이다.

🎥 파일 전송 - 서버

  • 스프링부트 2.7.8 버전을 사용
  • 스프링 웹과 타임리프를 사용했다
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
  • 스프링에서 multipart/form-data 를 처리하기 위해StandardServletMultipartResolver 가 동작한다.
  • StandardServletMultipartResolverMultipartFile 객체에 담아 준다.

컨트롤러

BasicController

@Controller
@RequiredArgsConstructor
public class BasicController {

    private final BasicService basicService;
	
    // view
    @GetMapping("/basic")
    public String basic() {
        return "basic";
    }

	// 파일 업로드
    @ResponseBody
    @PostMapping("/basic")
    public String saveFile(@RequestParam("file") MultipartFile file,
                           @RequestParam("desc") String description) {
        basicService.saveFile(file);
        return "업로드 성공!! - 파일 이름: " + file.getOriginalFilename() + ", 파일 설명: " + description;
    }

}

서비스

  • MultipartFile 객체는 메모리에 값을 저장하고 있다.
  • 따라서 이미지를 메모리에서 내 로컬 저장소로 저장해야 영구적으로 보관할 수 있다.

BasicService

@Service
public class BasicService {
    public void saveFile(MultipartFile file) {

        if (!file.isEmpty()) {
        	// 파일을 저장할 path
            // project root path 밑에 video 디렉토리에 저장
            Path filepath = Paths.get("video", file.getOriginalFilename());
			
            // 해당 path 에 파일의 스트림 데이터를 저장
            try (OutputStream os = Files.newOutputStream(filepath)) {
                os.write(file.getBytes());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

🎥 파일 전송 - 클라이언트

  • 자바스크립트의 fetch 함수를 이용해 비동기 호출을 한다.
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>basic file upload</title>
</head>
<body>
  
<form method="post" enctype="multipart/form-data">
    <input type="file" name="file">
    <input type="text" name="desc">
    <input type="submit"/>
</form>
  
<div id="result"></div>
  
</body>
  
<script>
    const formElement = document.querySelector('form');
    const resultElement = document.querySelector('div');
	
    formElement.addEventListener('submit', async (event) => {
        event.preventDefault();

        const formData = new FormData(formElement);

        const response = await fetch('/basic', {
            method: 'POST',
            body: formData
        });

        if (response.ok) {
            const result = await response.text();
            resultElement.textContent = result;
        } else {
            throw new Error(`${response.status} ${response.statusText}`);
        }
    });
</script>
</html>

🎥 실행

  • video 폴더 아래 저장이 됐다.

🎥 문제점

  • 영상 스트리밍을 하기위해 동영상을 전송해보자.
  • 대용량의 영상을 전송하기 위해 설정 변경

application.yml

spring:
  servlet:
    multipart:
      max-file-size: -1
      max-request-size: -1  
  • 4기가 정도의 영상을 업로드 한다.
  • OutOfMemory 에러가 발생했다.

앱에 할당된 힙메모리 사이즈

@Test
void heap_memory() {
    long gb = 1024L * 1024L * 1024L;
    long max = Runtime.getRuntime().maxMemory();
    System.out.println((double) max / gb);
}

  • 현재 할당된 힙메모리 사이즈가 약 0.5GB이다.
  • 위에서 언급했듯 스프링은 MultipartFile 객체에 파일 데이터를 담는데 해당 파일 데이터를 메모리에 저장하기 때문에 저장할 파일을 4GB인데 힙메모리 공간이 0.5GB 밖에 되지 않기 때문에 OOM이 발생했다.

다음 글에서는 이 문제를 해결해 보자.


코드

profile
물흐르듯 개발하다 대박나기

0개의 댓글