이 글에서는 REST API 기반 파일 업로드와 다운로드 구현방안을 설명합니다.

REST 서버와 REST 클라이언트를 이용해 기능을 구현했습니다.

REST 기반 파일 업로드와 다운로드 구현

 

REST API 구현 시 파일을 제공해야하는 경우가 있습니다. 파일 업로드 시 기존의 데이터와 함께 파일을 업로드할 수도 있고, 별도의 파일 전용 엔드포인트를 추가해 구현할 수 있습니다. 이 두가지 방법 모두에 대해 설명합니다.

 

이 글에 앞서 다음 내용을 이해하고 있어야 합니다. 미리 선행 학습이 필요합니다.

 

이 글에서는 다음 내용을 다룹니다.

  • 파일 엔드포인트 추가 구성
  • 파일 업로드 구현 방안
    • 서버 측 구현
    • 클라이언트 측 구현
  • 파일 다운로드 구현 방안
    • 서버 측 구현
    • 클라이언트 측 구현

 

파일 엔드포인트 추가 구성

파일을 제공하는 기능을 추가하기 위해서는, 1) 기존 엔드포인트에서 파일 항목을 추가하는 방법과 2) 별도의 엔드포인트를 추가하는 방법으로 구현할 수 있습니다.

 

이 글에서는 별도의 엔드포인트를 추가해 파일 업로드와 다운로드 기능을 구현하는 방법을 설명합니다. 

 

저는 images라는 리소스이름으로 RAD 서버 패키지 프로젝트를 생성했습니다.

1, File > New > Other

2, RAD Server > RAD Server Package

3, Create package with resource > Next

4, Resource name: images, File type: Data Module > Next

5, 모든 항목 선택 해제 > Finish

 

다음과 같이 엔드포인트를 추가합니다.(선언부에 추가 후 Ctrl + Shift + C를 눌러 구현부를 자동 생성할 수 있습니다.)

1
2
3
4
5
6
7
8
9
type
  [ResourceName('images')]
  TImagesResource1 = class(TDataModule)
  published
    [ResourceSuffix('{item}/photo')]
    procedure GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
    [ResourceSuffix('{item}/photo')]
    procedure PostItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
  end;

엔드포인트는 ResourceSuffix(리소스 접미사)와 메소드로 구성됩니다.

 

메소드는 Get, Post, Put, Delete 접두사로 시작해야 합니다. 리소스 호출 시 HTTP 메소드와 접두사가 매핑되어 메소드가 호출 됩니다.

즉, GET으로 요청된 경우 Get*으로 시작된 메소드가, POST로 요청한 경우 Post*로 시작된 메소드가 실행됩니다.

 

ResourceSuffix에 지정된 항목과 매핑된 요청이 온 경우 메소드가 실행됩니다.

중괄호({})로 감싼 부분은 파라미터화된 항목으로 구현부에서 ARequest.Params.Values['item'] 등의 코드로 그부분의 내용을 취득할 수 있습니다.

 

예를 들어

GET http://localhost:8080/images/1/photo 호출 시 GetItemPhoto 메소드가 호출되고

POST http://localhost:8080/images/1/photo 호출 시 PostItemPhoto 메소드가 호출됩니다.

ARequest.Params.Values['item']은 1을 반환합니다.

 

파일 업로드 구현 방안

서버(RAS Server) 측 구현

 

파일 업로드는 PostItemPhoto 메소드에서 구현합니다.

 

주의할 점은, 파일 전송을 위해서는 multipart/form-data 인코딩 타입으로 데이터가 전달됩니다.

현재(2020년 08월)에 RAD 서버에서 multipart/form-data 타입의 데이터를 처리하는 기능이 구현되어있지 않은 것으로 파악되어, 직접 데이터를 분석해 필요한 데이터를 사용해야 합니다.

 

저는 다음과 같은 DecodeParamsAndStream 함수를 이용해 데이터를 분석했습니다.

(Indy에서 재공하는 TIdMessageDecoderMIME 객체를 이용했습니다.)

1
2
3
4
5
6
7
8
9
10
11
12
type
  TStreamParams = class(TDictionary<string, tstream="">)
  private
    function GetStream(const Name: string): TStream;
    procedure SetStream(const Name: string; const Value: TStream);
  public
    property Streams[const Name: string]: TStream read GetStream write SetStream;
    destructor Destroy; override;
  end;
 
procedure DecodeParamsAndStream(AStream: TStream; AContentType: string;
  Params: TStrings; StreamParams: TStreamParams);</string,>

파라메터로는 

  • AStream : Body의 전체 스트림
  • AContentType : 요청의 컨텐트 타입, boundary 포함
  • Params : 문자열 형식의 데이터(파라미터)
  • StreamParams : Stream 형식의 데이터(파라미터), TStreamParams 객체는 <string, TStream> 쌍의 딕셔너리 사용
 
컨텐트 타입은 다음과 같이 데이터의 인코딩 타입과 boundary값이 포함됩니다.
multipart/form-data; boundary=---------Embt-Boundary--493D921E3683D69B
 
REST Client에서 파일 업로드 요청한 데이터(Body의 스트림)의 내용은 아래 그림과 같습니다.

 


파라미터 데이터들은 컨텐트타입의 boundary 값을 앞뒤에 두어 구분합니다.

주의할 점은 문자열 형식의 파라미터 데이터의 경우 Content-Type이 누락되어 있습니다.(분석 시 누락된 경우에 대해 예외처리가 필요합니다.)

위 데이터를 분석한 내용은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
uses
  System.IOUtils,
  IdGlobalProtocols, IdMessageCoder, IdMessageCoderMIME;
 
procedure DecodeParamsAndStream(AStream: TStream; AContentType: string;
  Params: TStrings; StreamParams: TStreamParams);
var
  Boundary: string;
  Decoder, NewDecoder: TIdMessageDecoderMIME;
  MsgEnd: Boolean;
 
  FieldName: string;
  StringStream: TStringStream;
  MemoryStream: TMemoryStream;
begin
  Boundary := ExtractHeaderSubItem(AContentType, 'boundary', QuoteHTTP);
 
  Decoder := TIdMessageDecoderMIME.Create(nil);
  try
    MsgEnd := False;
    repeat
      Decoder.MIMEBoundary := Boundary;
      Decoder.SourceStream := AStream;
      Decoder.FreeSourceStream := False;
 
      Decoder.ReadHeader;
      
  // Content-Type이 없는 경우 mcptAttachment로 인식해 기본값 설정         // RESTClient에서 MultiPart 전송 시 일반 파라메터의 경우 Content-Type 누락해 전송 함
      if Decoder.Headers.Values['Content-Type'] = '' then
      begin
        Decoder.Headers.Values['Content-Type'] := 'text/plain';
        Decoder.CheckAndSetType(Decoder.Headers.Values['Content-Type'], Decoder.Headers.Values['Content-Disposition']);
      end;
      case Decoder.PartType of
        mcptText:
          begin
            FieldName := ExtractHeaderSubItem(Decoder.Headers.Values['Content-Disposition'], 'name', QuoteMIME);
            StringStream := TStringStream.Create;
            try
              NewDecoder := Decoder.ReadBody(StringStream, MsgEnd) as TIdMessageDecoderMIME;
              try
                Params.Values[FieldName] := StringStream.DataString.Trim;
              finally
                Decoder.Free;
                Decoder := NewDecoder;
              end;
            finally
              StringStream.Free;
            end;
          end;
        mcptAttachment:
          begin
            var HL: string := Decoder.Headers.Values['Content-Disposition'];
            FieldName := ExtractHeaderSubItem(Decoder.Headers.Values['Content-Disposition'], 'name', QuoteMIME);
            MemoryStream := TMemoryStream.Create;
            NewDecoder := Decoder.ReadBody(MemoryStream, MsgEnd) as TIdMessageDecoderMIME;
            try
              StreamParams.Streams[FieldName] := MemoryStream;
            finally
              Decoder.Free;
              Decoder := NewDecoder;
            end;
          end;
        mcptIgnore:
          begin
            FreeAndNil(Decoder);
            Decoder := TIdMessageDecoderMIME.Create(nil);
            TIdMessageDecoderMIME(Decoder).MIMEBoundary := Boundary;
          end;
        mcptEOF:
          begin
            FreeAndNil(Decoder);
            MsgEnd := True;
          end;
      end;
 
    until (Decoder = nil) or MsgEnd;
  finally
    Decoder.Free;
  end;
end;

 

위의 코드가 좀 길고 생소할 수 있습니다. 

중요한 부분은 디코더로 분석한 데이터의 Decoder.PartType이 mcptText(텍스트)인 경우 Params 항목에 데이터를 추가하고, mcptAttachment(첨부 파일)인 경우 StreamParams 항목에 데이터를 추가했습니다.

 

위 함수를 사용한 PostItemPhoto 메소드의 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const
  ROOT_PATH = 'D:\Projects\DelphiDemos\OpenAPI\RESTUpload\Server';
 
procedure TImagesResource1.PostItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
var
  LItem: string;
 
  Stream: TStream;
  ContentType: string;
 
  Params: TStringList;
  StreamParams: TStreamParams;
  Path, RawPath: string;
begin
  LItem := ARequest.Params.Values['item'];
 
  Params := TStringList.Create;
  StreamParams := TStreamParams.Create;
 
  Stream := ARequest.Body.GetStream;
  ContentType := ARequest.Headers.GetValue('Content-Type');
    // e.g. multipart/form-data; boundary=--------070120105641002
 
  Path := TPath.Combine(ROOT_PATH, 'images');
  RawPath := TPath.Combine(Path, 'raw_' + LItem + '.jpg');
  Path := TPath.Combine(Path, LItem + '.jpg');
 
  // Save raw data
  TMemoryStream(Stream).SaveToFile(RawPath);
 
  // Decode parameters and streams from body stream.
  DecodeParamsAndStream(Stream, ContentType, Params, StreamParams);
 
  // Using parameters as a below
  if Params.Values['user_id'] = '123' then
  begin
  end;
 
  // Save photo parameter to a file.
  if Assigned(StreamParams.Streams['photo']) then
    TMemoryStream(StreamParams.Streams['photo']).SaveToFile(Path);
 
  StreamParams.Free;
  Params.Free;
end;

참고로, 파일 저장 방식은 파일로 저장하는 방식과 Blob 필드에 저장하는 방식이 있으며, 이 예제에서는 지정경로(ROOT_PATH) 하위 images 디렉토리에 {item}항목 이름으로 저장했습니다.

 

파일로 저장시의 주의점은 RAD 서버의 로컬 디스크로 저장 시 서버를 병렬화(여러대 구성) 시 접근이 제한됩니다. 네트워크 경로 또는 공유 파일 시스템을 이용해야 합니다.(또는 파일 공유 솔루션 등을 이용할 수도 있습니다.)

 

Blob 필드로 저장 시 주의점은 데이터와 별도의 테이블에 Blob 필드를 구성하는 것을 추천드립니다. 성능 측면과 향후 데이터 관리(백업 등) 시 유리할 수 있습니다.

 

클라이언트(REST Client) 측 구현

REST 클라이언트를 이용해 파일 업로드 구현은 데이터 전송과 크게 차이나지 않습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
procedure TForm1.Button1Click(Sender: TObject);
var
  Filepath: string;
  Stream: TFileStream;
begin
  if not OpenDialog1.Execute then
    Exit;
 
  Filepath := OpenDialog1.FileName;
  Stream := TFileStream.Create(Filepath, fmOpenRead);
 
  RESTClient1.BaseURL := 'http://localhost:8080';
  RESTRequest2.Method := rmPOST;
  RESTRequest2.Resource := 'images/{item}/photo';
  RESTRequest2.Params.ParameterByName('item').Value := Edit1.Text;
 
  RESTRequest2.Params.AddItem('user_id', '123');
  RESTRequest2.Params.AddItem('photo', Stream, pkFILE, [], ctAPPLICATION_OCTET_STREAM);
  RESTRequest2.Execute;
 
  Stream.Free;
end;

파일을 추가할 경우, TStream 이용하므로, TStream을 상속받는 객체들(TFileStream, TMemoryStream 등)을 이용할 수 있습니다.

 

스트림을 파라미터로 추가하는 코드는 다음과 같습니다.

1
RESTRequest2.Params.AddItem('photo', Stream, pkFILE, [], ctAPPLICATION_OCTET_STREAM);

 

파라미터 종류를 pkFILE로 지정시 내부적으로 multipart/form-data 인코딩 타입으로 데이터가 전송되며, 컨텐트타입을 ctAPPLICATION_OCTET_STREAM으로 지정해야 합니다.

파일 다운로드 구현 방안

서버(RAD Server) 측 구현

파일 다운로드는 GetItemPhoto 메소드에서 구현합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
procedure TImagesResource1.GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse);
var
  LItem: string;
 
  Path: string;
  Stream: TStream;
begin
  LItem := ARequest.Params.Values['item'];
 
  Path := TPath.Combine(ROOT_PATH, 'images');
  Path := TPath.Combine(Path, LItem + '.jpg');
 
  if not TFile.Exists(Path) then
    AResponse.RaiseNotFound('Not found', '''' + LItem + ''' is not found');
 
  Stream := TFileStream.Create(Path, fmOpenRead);
  AResponse.Body.SetStream(Stream, 'image/jpeg', True);
end;
요청한 {item} 항목의 파일을 스트림(TFileStream)으로 읽어 그대로 추력합니다.
주의할 점은 마지막 줄의 SetStream의 마지막 파라미터(AOwnerValue)를 True로 지정해야 Stream 객체가 해제됩니다.(직접 해제 시 메모리 참조 오류가 발생합니다.) False로 지정 시 객체를 복사하므로, 직접 해제해도 됩니다.
 

클라이언트(REST Client) 측 구현

VCL Form Application에서 REST Client로 이미지를 다운로드 후 이미지(TImage)에 표시하는 코드는 다음과 같습니다.
procedure TForm1.Button2Click(Sender: TObject);
var
  WICImage: TWICImage;
  Stream: TMemoryStream;
begin
  RESTClient1.BaseURL := 'http://localhost:8080';
  RESTRequest1.Method := rmGET;
  RESTRequest1.Resource := 'images/{item}/photo';
  RESTRequest1.Params.ParameterByName('item').Value := Edit1.Text;
  RESTRequest1.ExecuteAsync(procedure
    begin
      if RESTResponse1.StatusCode = 404 then
        Exit;
      Stream := TMemoryStream.Create;
      Stream.WriteData(RESTResponse1.RawBytes, RESTResponse1.ContentLength);
      WICImage := TWICImage.Create;
      WICImage.LoadFromStream(Stream);
      Image1.Picture.Assign(WICImage);
      WICImage.Free;
      Stream.Free;
    end);
end;
 
이미지 등의 용량이 큰 요청의 경우 비동기로 요청하는 것이 좋습니다.
다양한 이미지 포맷을 지원하기 위해 TWICImage 컴포넌트를 사용했습니다.
 
완성된 프로젝트 샘플코드
번호 제목 글쓴이 날짜 조회 수
공지 [DelphiCon 요약] 코드사이트 로깅 실전 활용 기법 (Real-world CodeSite Logging Techniques) 관리자 2021.01.19 15417
공지 [UX Summit 요약] 오른쪽 클릭은 옳다 (Right Click is Right) 관리자 2020.11.16 13960
공지 [10.4 시드니] What's NEW! 신기능 자세히 보기 관리자 2020.05.27 16496
공지 RAD스튜디오(델파이,C++빌더) - 고객 사례 목록 관리자 2018.10.23 22048
공지 [데브기어 컨설팅] 모바일 앱 & 업그레이드 마이그레이션 [1] 관리자 2017.02.06 23267
공지 [전체 목록] 이 달의 기술자료 & 기술레터 관리자 2017.02.06 18921
공지 RAD스튜디오(델파이, C++빌더) - 시작하기 [1] 관리자 2015.06.30 39245
공지 RAD스튜디오(델파이,C++빌더) - 모바일 앱 개발 사례 (2020년 11월 업데이트 됨) 험프리 2014.01.16 174696
654 XML MAPPER 업데이트 관리자 2020.10.21 280
653 [개발팁] 오래된 레거시 델파이, C++ 애플리케이션을 최신 스타일의 초고속 앱으로 마이그레이션하기 관리자 2020.10.15 337
652 [10.4 시드니][업데이트 1] 새로운 IDE 기능들 관리자 2020.10.08 335
651 [엠바카데로 UX SUMMIT] 눈여겨 봐야할 컨텐츠들! 관리자 2020.10.06 290
650 이 달의 기술자료 - 2020년 10월 file 험프리 2020.09.24 357
649 [발표자료] 델파이 개발자가 REST 서비스를 활용할 때 가장 많이 하는 질문 TOP 5 file 관리자 2020.09.18 1177
648 [10.4 시드니][업데이트 1] 새로운 TLightweightMREW 레코드 관리자 2020.09.10 411
647 [10.4 시드니][업데이트 1] What's NEW! 신기능 자세히 보기 [2] 관리자 2020.09.03 979
» [REST API] REST 기반 파일 업로드와 다운로드 구현하기 험프리 2020.08.31 83041
645 [개발환경] 델파이, RAD스튜디오 에서 Git과 Github를 사용해야하는 이유와 꼭 필요한 만큼 사용법 관리자 2020.08.27 1286
644 이 달의 기술자료 - 2020년 09월 file 험프리 2020.08.27 260
643 델파이/C++빌더에 TTcpClient, TTcpServer, TUdpSocket 등록하기 [1] file 험프리 2020.08.26 621
642 [고객 사례- 델파이, 예술, 창작] 아트젠, 컴퓨터로 완성하는 미술 작품 관리자 2020.08.25 394
641 2020 년 8 월 GM 업데이트 관리자 2020.08.24 485
640 이 달의 기술자료 - 2020년 08월 험프리 2020.07.30 363
639 [10.4 시드니][패치] RAD스튜디오 10.4 '세 번째 패치(Patch 3)': VCL그리드, C++Win64 디버깅, C++ 안드로이드 예외처리 핸들링 및 리소스 등 관리자 2020.07.29 686
638 [생산성] RAD 스튜디오, 델파이, C++빌더 단축키 표 관리자 2020.07.28 412
637 [발표자료] VCL 애플리케이션 확장하기 with RAD 서버 관리자 2020.07.22 389
636 [발표자료] 20170623 최신OS와 멀티플랫폼 개발 전략 with RAD Studio 관리자 2020.07.22 263
635 델파이 개발자 관점에서 본 C++ 빌더 file 김원경 2020.07.20 1182