험프리 12월 14일, 2021에 포스트됨 공유하기 12월 14일, 2021에 포스트됨 이글에서는 RAD 서버의 REST API 엔드포인트 구현 시 비지니스 로직을 적용하는 과정을 실습을 통해 이해할 수 있습니다. 비지니스 로직을 적용해 REST API 구현하기 RAD 서버의 REST API 개발 시 리소스와 엔드포인트 구현 규칙 글에서 RAD 서버 패키지의 구조와 리소스, 엔드포인트를 구현하는 규칙을 알아봤다. 이 글에서는 REST API 엔드포인트 메소드에 비지니스 로직등을 포함해 개발하는 과정을 실습을 통해 익혀본다. 실습의 주제는 도서정보를 제공하는 REST API를 개발이며, 실습에서 사용하는 데이터베이스는 도서대여 프로그램 만들기에서 사용한 데이터베이스를 사용한다. 다음 링크에서 먼저 데이터베이스 파일을 다운로드 받고 시작하기 바란다.(다른 DBMS 사용도 가능하며 create_script를 참고해 테이블 생성 후 진행) https://github.com/devgear/BOOKRENTAL/tree/master/DB 이글은 실습에 용이해 데이터베이스의 데이터를 REST API로 제공하지만, 데이터베이스 외에 다른 서비스(클라우드, 외부 서비스 연동 등)의 데이터를 제공하도록 비지니스 로직을 마음껏 구현할 수 있다는 것을 명심하기 바란다. 이 글을 참고해 필요한 데이터소스의 데이터를 REST API로 제공해보기 바란다. 참고로, 단순히 데이터베이스의 데이터를 REST API로 제공하는 효과적인 방안은 다음 글에서 참고할 수 있다. 데이터셋 데이터를 REST API로 바로 서비스할 수 있는TEMSDataSetResource 컴포넌트 안내 이글의 목적은 여러분이 직접 엔드포인트 메소드에 비지니스 로직을 구현하는 방법을 익히고, 필요한 경우 활용할 수 있게 함이다. (단순 데이터 제공만 하는 REST API 구현은 TEMSDataSetResource 컴포넌트를 이용하는 것이 편리하다.) [실습] 도서정보 제공 REST API 구현하기 (RAD 서버 개발환경이 설정되지 않은 경우 RAD 서버 개발환경 설정 글에서 개발환경 설정 후 진행해야 한다.) 프로젝트 생성 RAD 서버 패키지 프로젝트를 생성한다. 델파이 : File > New > Other | Delphi > RAD Server > RAD Server Package 리소스 이름은 "books"로 지정한다. 엔드포인트 선택 화면에서 Sample EndPoints를 모두 선택한다. 프로젝트가 생성되면 다음과 같은 코드가 생성된다. 코드를 간단히 설명하면, [ResourceName('books')] 애트리뷰트를 지정해 클래스가 books라는 이름의 리소스로 지정하고, 엔드포인트 메소드를 구현할 것이라 정의한다. 리소스 클래스의 공개 영역(public, published)에 선언된 메소드는 엔드포인트 메소드 후보이며, HTTP 메소드로 메소드 이름이 시작되므로 엔드포인트 메소드로 사용된다.(Get과 GetItem은 books에 GET 요청 시 실행될 수 있다.) [ResourceSuffix('{item}')] 애트리뷰트를 지정해 엔드포인트의 경로를 지정하고, 중괄호로 매개변수를 지정할 수 있다. (GetItem은 'GET /books/100' 등의 리소스 뒤에 경로가 추가된 URI로 요청 시 호출되며, item이라는 이름으로 매개변수가 지정되었다.) 프로젝트 저장(디렉토리 구성) RAD 서버 패키지는 향후 서버에 배포해야 하므로, 패키지와 DB 또는 파일등 참조 리소스의 경로 설정이 중요하다. 먼저 임의의 경로에 다음과 같이 디렉토리를 생성한다.(아래 구조는 예시이므로 원하는 구조로 설정해도 무방하다.) 도서대여 프로그램 데이터베이스 파일을 DB 디렉토리에 저장한다. 위에서 생성한 프로젝트를 Source 디렉토리에 저장한다. bin 디렉토리에 패키지 파일이 생성되도록 프로젝트 옵션을 설정한다. Package output directory : ../bin 데이터베이스 연결 및 DB 경로 설정 BookResource 데이터모듈에 아래와 같이 TFDConnection과 TFDQuery 컴포넌트를 추가하고 이름을 변경한다. conBookRental(TFDConnection) 컴포넌트를 더블클릭해 연결설정 후 Test 버튼을 클릭 해 연결을 확인한다. Driver ID : IB Database : 앞에서 생성한 DB 경로 하위의 파일 선택 User_Name : sysdba Password : masterkey CharacterSet : UTF8(한글 사용을 위함) 위에서 데이터베이스 파일 경로를 절대경로로 설정했다. 실행 시 데이터베이스 파일 경로를 상대경로로 설정해야 한다. TFDConnection의 OnBeforeConnect 이벤트 핸들러에 다음 코드를 추가한다.(데이터베이스 연결 전 코드가 실행된다.) uses 절에 System.IOUtils를 추가한다.(TPath 및 TFile 헬퍼 사용 위함) procedure TBooksResource1.conBookRentalBeforeConnect(Sender: TObject); var Path: string; begin Path := TPath.GetFullPath('..\DB\BOOKRENTAL.IB'); if not TFile.Exists(Path) then raise Exception.Create('Not found database.'); conBookRental.Params.Values['Database'] := Path; end; 구현에 필요한 준비가 완료되었다. 이제 본격적으로 구현해보자. Get / GetItem 구현 리소스에 GET 메소드를 요청하면, Get 또는 GetItem(접미사 있는 경우)이 호출된다. REST에서 GET은 조회 역할을 하며 각 엔드포인트는 다음 역할을 구현해야 한다. Get - 리소스의 목록 정보 조회 GetItem - 리소스의 특정 항목 상세 정보 조회 Get - 리소스 목록 Get 엔드포인트 메소드는 리소스의 목록 정보를 제공해야 한다. 실습에서는 도서정보의 목록을 제공하도록 구현한다. REST API에서 응답 데이터 포맷은 일반적으로 JSON을 사용한다. 하지만 JSON이 표준은 아니므로 필요에 따라 XML 또는 줄바꿈, 쉼표 구분 포맷등을 사용해도 된다. 다음은 GET 요청시의 응답 데이터 샘플이다. GET /books { "books": { "total":6, "book": [ { "BOOK_SEQ":15, "BOOK_TITLE":"델파이 Begin...End", "BOOK_AUTHOR":"김원경" }, { "BOOK_SEQ":16, "BOOK_TITLE":"한 번에 개발하는 안드로이드 iOS앱 with 델파이. 1편", "BOOK_AUTHOR":"김원경 , 김현수, 오상현" } ] } } 위 데이터 구조는 작성자가 개인적으로 사용하는 구조이다. 서비스에 따라 JSON 데이터의 구조는 가지각색이다. 위에서 주의할 내용은 books.book 속성은 JSON 배열([])이며, 배열의 요소타입은 JSON 객체({})인 것을 유의해야 한다. GET은 정보의 목록을 제공해야 하므로 특정 속성(위의 books.book)은 JSON 객체의 배열 구조로 제공해야 한다. 목록에서는 상세설명, 이미지와 같은 길이가 길거나 용량이큰 데이터 항목을 포함하지 않는것이 성능상 좋다. Get 엔드포인트 메소드는 아래와 같이 구현한다. procedure TBooksResource1.Get(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_LIST ='SELECT BOOK_SEQ, BOOK_TITLE, BOOK_AUTHOR, BOOK_PRICE FROM BOOK'; var Writer: TJsonObjectWriter; begin qryBook.Close; qryBook.SQL.Text := SQL_LIST; qryBook.Open; Writer := TJsonObjectWriter.Create; try Writer.WriteStartObject; // start resource Writer.WritePropertyName('books'); Writer.WriteStartObject; // start item Writer.WritePropertyName('total'); Writer.WriteValue(qryBook.RecordCount); Writer.WritePropertyName('book'); Writer.WriteStartArray; qryBook.First; while not qryBook.Eof do begin Writer.WriteStartObject; Writer.WritePropertyName('BOOK_SEQ'); Writer.WriteValue(qryBook.FieldByName('BOOK_SEQ').AsInteger); Writer.WritePropertyName('BOOK_TITLE'); Writer.WriteValue(qryBook.FieldByName('BOOK_TITLE').AsString); Writer.WritePropertyName('BOOK_AUTHOR'); Writer.WriteValue(qryBook.FieldByName('BOOK_AUTHOR').AsString); Writer.WritePropertyName('BOOK_PRICE'); Writer.WriteValue(qryBook.FieldByName('BOOK_PRICE').AsString); Writer.WriteEndObject; qryBook.Next; end; Writer.WriteEndArray; Writer.WriteEndObject; // end item Writer.WriteEndObject; // end resource AResponse.Body.SetValue(Writer.JSON as TJSONValue, True); except Writer.Free; raise; end; end; 도서 정보를 조회해 JSON 데이터로 작성해 응답한다. 목록에 표시할 항목만 조회했다. JSON 데이터 작성은 TJsonObjectWriter 클래스를 이용했다.(델파이에서 JSON을 다루는 내용은 델파이에서 JSON 데이터 다루기와 델파이에서 JSON을 다룰 때 당신이 몰랐던 것들을 참고하기 바란다.) 응답의 바디에 JSONValue를 설정한다. 위 내용을 구현했다면, 프로젝트를 실행하고 웹브라우저에서 "http://localhost:8080/books" 실행해 JSON 문자열이 표시되는 것을 확인한다. GetItem - 리소스 상세 GetItem 엔드포인트 메소드에서는 특정 도서의 상세 정보를 제공하도록 구현한다. GET /books/{item} { "book": { "BOOK_SEQ":15, "BOOK_TITLE":"델파이 Begin...End", "BOOK_ISBN":"9788996251613", "BOOK_AUTHOR":"김원경", "BOOK_PRICE":"28000", "BOOK_LINK":"http://www.kyobobook.co.kr/product/detailViewKor.laf?ejkGb=KOR&mallGb=KOR&barcode=9788996251613&orderClick=LAG&Kc=SETLBkserp1_5", "BOOK_DESCRIPTION":"델파이의 시작부터 끝까지 파헤치다! ... (생략)" } } 상세 정보의 경우 이미지와 상세설명등을 포함해도 된다. 다음과 같이 구현한다. procedure TBooksResource1.GetItem(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_ITEM_INFO = 'SELECT ' + 'BOOK_SEQ, BOOK_TITLE, BOOK_ISBN, BOOK_AUTHOR, BOOK_PRICE, ' + 'BOOK_LINK, BOOK_DESCRIPTION ' + 'FROM BOOK WHERE BOOK_SEQ = :BOOK_SEQ'; var BOOK_SEQ: string; Writer: TJsonObjectWriter; begin BOOK_SEQ := ARequest.Params.Values['item']; // Sample code qryBook.Close; qryBook.SQL.Text := SQL_ITEM_INFO; qryBook.ParamByName('BOOK_SEQ').AsString := BOOK_SEQ; qryBook.Open; if qryBook.RecordCount = 0 then AResponse.RaiseNotFound('Not found', '''' + BOOK_SEQ + ''' is not found'); Writer := TJsonObjectWriter.Create; try Writer.WriteStartObject; // start resource Writer.WritePropertyName('book'); Writer.WriteStartObject; // start item Writer.WritePropertyName('BOOK_SEQ'); Writer.WriteValue(qryBook.FieldByName('BOOK_SEQ').AsInteger); Writer.WritePropertyName('BOOK_TITLE'); Writer.WriteValue(qryBook.FieldByName('BOOK_TITLE').AsString); Writer.WritePropertyName('BOOK_ISBN'); Writer.WriteValue(qryBook.FieldByName('BOOK_ISBN').AsString); Writer.WritePropertyName('BOOK_AUTHOR'); Writer.WriteValue(qryBook.FieldByName('BOOK_AUTHOR').AsString); Writer.WritePropertyName('BOOK_PRICE'); Writer.WriteValue(qryBook.FieldByName('BOOK_PRICE').AsString); Writer.WritePropertyName('BOOK_LINK'); Writer.WriteValue(qryBook.FieldByName('BOOK_LINK').AsString); Writer.WritePropertyName('BOOK_DESCRIPTION'); Writer.WriteValue(qryBook.FieldByName('BOOK_DESCRIPTION').AsString); Writer.WriteEndObject; // end item Writer.WriteEndObject; // end resource AResponse.Body.SetValue(Writer.JSON as TJSONValue, True); except Writer.Free; raise; end; end; 특정 항목의 도서정보를 조회해 JSON 데이터로 응답한다. 도서에 대한 전반적인 정보를 포함한다.(이미지는 별도로 제공한다.) 구현을 마치고 프로젝트를 실행한다. 웹브라우저에 "http://localhost:8080/books/{item}"({item}은 도서목록의 BOOK_SEQ로 치환해야 한다.) 실행 후 결과를 확인한다. Post / PutItem / DeleteItem 구현 리소스를 생성, 수정, 삭제 기능을 구현한다. 생성과 수정시에는 데이터를 Custom Body에 JSON 데이터를 수신하도록 구현한다. Post - 리소스 생성 Post 엔드포인트 메소드는 Custom Body에 담긴 JSON 데이터로 도서정보를 생성해야 한다. POST /books { "book": { "BOOK_TITLE":"테스트", "BOOK_ISBN":"1234567890123", "BOOK_AUTHOR":"홍길동", "BOOK_PRICE":"10000", "BOOK_LINK":"", "BOOK_DESCRIPTION":"12345." } } 다음과 같이 구현한다. procedure TBooksResource1.Post(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_ITEM_INSERT = 'INSERT INTO BOOK(BOOK_TITLE, BOOK_ISBN, BOOK_AUTHOR, BOOK_PRICE, BOOK_LINK, BOOK_DESCRIPTION)' + ' VALUES (:BOOK_TITLE, :BOOK_ISBN, :BOOK_AUTHOR, :BOOK_PRICE, :BOOK_LINK, :BOOK_DESCRIPTION)'; var Title, Author, Price, ISBN, Link, Desc: string; Json: TJSONValue; begin JSON := ARequest.Body.GetValue; Title := JSON.GetValue<string>('book.BOOK_TITLE'); ISBN := JSON.GetValue<string>('book.BOOK_ISBN'); Author := JSON.GetValue<string>('book.BOOK_AUTHOR'); Price := JSON.GetValue<string>('book.BOOK_PRICE'); Link := JSON.GetValue<string>('book.BOOK_LINK'); Desc := JSON.GetValue<string>('book.BOOK_DESCRIPTION'); qryBook.Close; qryBook.SQL.Text := SQL_ITEM_INSERT; qryBook.ParamByName('BOOK_TITLE').AsString := Title; qryBook.ParamByName('BOOK_ISBN').AsString := ISBN; qryBook.ParamByName('BOOK_AUTHOR').AsString := Author; qryBook.ParamByName('BOOK_PRICE').AsString := Price; qryBook.ParamByName('BOOK_LINK').AsString := Link; qryBook.ParamByName('BOOK_DESCRIPTION').AsString := Desc; qryBook.ExecSQL; end; Body에서 JSON 객체 취득 후, 항목별 데이터 취득해 INSERT 문을 호출한다. Body에서 JSON 객체 취득(ARequest.Body.GetValue) 필요한 정보는 JSONValue.GetValue<T>로 취득(T는 원하는 데이터 타입 선언, string, integer 등) 응답을 구현하지 않은 경우 상태코드 200으로 응답된다. 구현을 마치고, 프로젝트를 실행한다. GET 메소드 테스트는 웹브라우저에서 가능하지만, POST, PUT, DELETE 메소드는 REST 디버거를 이용해 테스트한다. IDE에서 Tools > REST Debugger 메뉴 실행 후 아래 정보를 입력해 테스트 한다. Method : POST URL : http://localhost:8080/books Content-Type : application/json Custom body : 상단의 JSON 문자열 입력(method를 POST, PUT 선택 시 입력 가능) Send Request 버튼을 누르고, 하단 Response 영역의 응답코드 200 확인 PutItem - 리소스 수정 PutItem 엔드포인트 메소드는 특정 도서정보를 수정하도록 구현해야 한다. 도서의 Id는 리소스 접미사를 이용해 파악한다. 도서의 정보는 Cutom Body에 담긴 JSON 데이터를 이용한다. PUT /books/{item} { "book": { "BOOK_TITLE":"테스트 수정", "BOOK_ISBN":"1234567890123", "BOOK_AUTHOR":"홍길동", "BOOK_PRICE":"10000", "BOOK_LINK":"", "BOOK_DESCRIPTION":"12345." } } 다음과 같이 구현한다. procedure TBooksResource1.PutItem(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_ITEM_UPDATE = 'UPDATE BOOK SET ' + ' BOOK_TITLE = :BOOK_TITLE,' + ' BOOK_ISBN = :BOOK_ISBN, ' + ' BOOK_AUTHOR = :BOOK_AUTHOR,' + ' BOOK_PRICE = :BOOK_PRICE,' + ' BOOK_LINK = :BOOK_LINK, ' + ' BOOK_DESCRIPTION = :BOOK_DESCRIPTION ' + ' WHERE ' + ' BOOK_SEQ = :BOOK_SEQ'; var BOOK_SEQ: string; Title, Author, Price, ISBN, Link, Desc: string; Json: TJSONValue; begin BOOK_SEQ := ARequest.Params.Values['item']; JSON := ARequest.Body.GetValue; Title := JSON.GetValue<string>('book.BOOK_TITLE'); ISBN := JSON.GetValue<string>('book.BOOK_ISBN'); Author := JSON.GetValue<string>('book.BOOK_AUTHOR'); Price := JSON.GetValue<string>('book.BOOK_PRICE'); Link := JSON.GetValue<string>('book.BOOK_LINK'); Desc := JSON.GetValue<string>('book.BOOK_DESCRIPTION'); qryBook.Close; qryBook.SQL.Text := SQL_ITEM_UPDATE; qryBook.ParamByName('BOOK_TITLE').AsString := Title; qryBook.ParamByName('BOOK_ISBN').AsString := ISBN; qryBook.ParamByName('BOOK_AUTHOR').AsString := Author; qryBook.ParamByName('BOOK_PRICE').AsString := Price; qryBook.ParamByName('BOOK_LINK').AsString := Link; qryBook.ParamByName('BOOK_DESCRIPTION').AsString := Desc; qryBook.ParamByName('BOOK_SEQ').AsString := BOOK_SEQ; qryBook.ExecSQL; if qryBook.RowsAffected = 0 then AResponse.RaiseNotFound('Not found', '''' + BOOK_SEQ + ''' is not found'); end; id는 리소스 접미사로, 데이터는 Body의 JSON 객체로 취득 후 UPDATE문으로 데이터를 수정한다. 만약 업데이트한 데이터가 없다면, 404에러를 발생(RaiseNotFound)한다. 구현을 마치고, REST 디버거에서 아래와 같이 테스트 한다. Method : PUT URL : http://localhost:8080/books/{item} Content-Type : application/json Custom body : 상단의 JSON 문자열 입력 Send Request 버튼을 누르고, 하단 Response 영역의 응답코드 200 확인 DeleteItem - 리소스 삭제 DeleteItem 엔드포인트 메소드는 특정 도서정보를 삭제하도록 구현해야 한다.도서의 Id는 리소스 접미사로 파악한다. DELETE /books/{item} procedure TBooksResource1.DeleteItem(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_ITEM_DELETE ='DELETE FROM BOOK WHERE BOOK_SEQ = :BOOK_SEQ'; var BOOK_SEQ: string; begin BOOK_SEQ := ARequest.Params.Values['item']; qryBook.Close; qryBook.SQL.Text := SQL_ITEM_DELETE; qryBook.ParamByName('BOOK_SEQ').AsString := BOOK_SEQ; qryBook.ExecSQL; if qryBook.RowsAffected = 0 then AResponse.RaiseNotFound('Not found', '''' + BOOK_SEQ + ''' is not found'); end; 삭제시 데이터는 필요하지 않으며, Id로 데이터를 삭제한다. 구현 완료 후 프로젝트를 실행한다. REST 디버거로 다음과 같이 테스트 한다. 위 테스트 후 데이터를 확인한다. "Get /books"로 목록을 확인하거나, DB를 확인한다. GetItemPhoto - 이미지 제공 도서정보의 이미지를 제공하는 엔드포인트 메소드를 추가하고, 이미지를 제공하도록 추가 구현한다. 다음 코드를 선언부에 추가한다. [ResourceSuffix('{item}/photo')] procedure GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); ResourceSuffix로 경로를 지정한다.(GET /books/{item}/photo 호출 시 GetItemPhoto 메소드 실행) GET /books/{items}/photo procedure TBooksResource1.GetItemPhoto(const AContext: TEndpointContext; const ARequest: TEndpointRequest; const AResponse: TEndpointResponse); const SQL_ITEM_IMAGE ='SELECT BOOK_IMAGE FROM BOOK WHERE BOOK_SEQ = :BOOK_SEQ'; var BOOK_SEQ: string; Stream: TMemoryStream; begin BOOK_SEQ := ARequest.Params.Values['item']; Stream := TMemoryStream.Create; try qryBook.Close; qryBook.SQL.Text := SQL_ITEM_IMAGE; qryBook.ParamByName('BOOK_SEQ').AsString := BOOK_SEQ; qryBook.Open; if qryBook.RecordCount = 0 then AResponse.RaiseNotFound('Not found', '''' + BOOK_SEQ + ''' is not found'); TBlobField(qryBook.FieldByName('BOOK_IMAGE')).SaveToStream(Stream); if Stream.Size = 0 then AResponse.RaiseNotFound('Not found', '''' + BOOK_SEQ + ''' is not found'); Stream.Position := 0; AResponse.Body.SetStream(Stream, 'image/jpeg', True); except Stream.Free; raise; end; end; 리소스 접미사로 취득한 Id의 이미지 데이터 조회 후 Stream을 읽어 바로 Body에 Stream을 출력한다. 필요한 항목(BOOK_IMAGE)만 조회 한다. 조회 결과가 없으면 404 오류를 발생(RaiseNotFound) Blob 필드의 데이터를 Stream으로 추출 Body에 바로 Stream을 출력한다. Content-Type을 image/jpeg으로 지정한다. 구현 후 프로젝트를 실행하고, 웹브라우저에서 "http://localhost:8080/books/{item}/photo" 실행해 결과를 확인한다. 여러분이 직접 엔드포인트 메소드에 비지니스 로직을 구현하는 방법을 살펴보았다. 이글에서는 실습에 용이해 데이터베이스의 데이터를 REST API로 제공하지만, 데이터베이스 외에 다른 서비스(클라우드, 외부 서비스 연동 등)의 데이터를 제공하도록 비지니스 로직을 마음껏 구현할 수 있다는 것을 명심하기 바란다. 이 글을 참고해 필요한 데이터소스의 데이터를 REST API로 제공해보기 바란다. 참고로, 단순히 데이터베이스의 데이터를 REST API로 제공하는 효과적인 방안은 다음 글에서 참고하기 바란다. 데이터셋 데이터를 REST API로 바로 서비스할 수 있는TEMSDataSetResource 컴포넌트 안내 인용하기 이 댓글 링크 다른 사이트에 공유하기 더 많은 공유 선택 사항
Kori 12월 22일, 2021에 포스트됨 공유하기 12월 22일, 2021에 포스트됨 REST API에 대해서 정리가 잘되어 있는 글이 Redhat에 있어서 공유합니다. REST와 RESTful API에 대해 깔끔 명확하게 이해할 수 있습니다: https://www.redhat.com/ko/topics/api/what-is-a-rest-api 인용하기 이 댓글 링크 다른 사이트에 공유하기 더 많은 공유 선택 사항
Recommended Posts
이 토의에 참여하세요
지금 바로 의견을 남길 수 있습니다. 그리고 나서 가입해도 됩니다. 이미 회원이라면, 지금 로그인하고 본인 계정으로 의견을 남기세요.