SpringBoot 외부 Api 통신

김광훈·2022년 5월 21일
4

문서요약프로젝트

목록 보기
1/3

외부 Api 통신 _ 문서요약프로젝트

개요

Ml/Md 개발자와 문서를 요약해주는 프로젝트를 진행했다.
백엔드 개발자인 나의 경우 Java에서 개발하기를 원하는 상황이였고 Ml/Md 개발자인 친구는 Python에서 프로젝트를 개발해야하는 상황이였기 때문에, 각자 환경에서 개발을 마무리하고 Http 통신을 이용해 데이터만 Json형식으로 건내받기로 결정을 했다.

개발환경

springboot 버전 2.6.7
JAVA 11

Json 데이터 셋

문서를 요약하는 프로그램의 경우 AIHub에 문서요약 텍스트 소개를 활용할 예정이였기 때문에, 해당 프로그램에서 사용한 데이터 셋과 동일하기 Json파일을 만들어 보내줘야할 필요가 있었습니다.

아래와 같은 json 데이터 형식을 Flask로 전송해야할 필요가 있었습니다.

{
  "id": "121009",
  "abstractive": "시어머니가 아들이 처자를 내버려둔 채 다른...",
  "extractive": [5, 0, 6],
  "article_original":
  	[
      "시어머니가 아들이 그 처자를...",
      "다른 여자로 하여금 자기가...",
      "..."
    ]
}

도메인

도메인의 경우 원본과 요약본만 저장해도 된다고 판단했지만, 기존의 샘플데이터의 id값과 다른 유니크한 id값을 생성 및 전달해야 했기 때문에 데이터베이스 상에 기존 샘플 데이터의 id값을 모두 가지고 있어야 했습니다. 이와 같은 이유로 도메인을 json 데이터 셋과 동일하게 세팅했습니다.

Article

@Setter
@Getter
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity
public class Article extends Timestamped {

    // ID를 자동으로 생성 및 증가시켜줍니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    @Column
    private String abstractive;

    @Column(nullable = false, columnDefinition = "text")
    private String articleOriginal;

    @OneToMany(mappedBy = "article")
    private List<Extractive> extractive;

    @OneToMany(mappedBy = "article")
    private List<ArticleOriginalList> articleOriginalList;

    public Article(GetArticleRequestDto requestDto) {
        this.articleOriginal = requestDto.getArticleOriginal();
    }
}

ArticleOriginalList

@Setter
@Getter
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity
public class ArticleOriginalList {

    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long idx;

    @Column(nullable = true)
    private String articleOriginalSentence;

    @ManyToOne
    @JoinColumn(name = "article_idx", nullable = false)
    private Article article;

    public ArticleOriginalList(String articleOriginalSentence, Article article) {
        this.articleOriginalSentence = articleOriginalSentence;
        this.article = article;
    }
}

Extractive

@Setter
@Getter
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity
public class Extractive {

    // ID를 자동으로 생성 및 증가시켜줍니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    @Column
    private int extractiveNum;

    @ManyToOne
    @JoinColumn(name="article_idx", nullable = false)
    private Article article;

    public Extractive(int extractiveNum, Article article) {
        this.extractiveNum = extractiveNum;
        this.article = article;
    }
}
  • 참고 : 데이터베이스 효율 및 쿼리 요청에 따라 언제든지 변경될 수 있습니다.

DTO

데이터를 담아 보내기 위한 Object로 3가지의 클래스를 생성했습니다.

GetAbstractiveDto

Flask로 Request를 보내고 Response를 받아오기 위한 Object입니다.

@Getter
@Setter
public class GetAbstractiveDto {

    @Setter
    @Getter
    public static class request {
        private Long id;
        private String abstractive;
        private List<Integer> extractive;
        private String articleOriginal;
    }

    @Setter
    @Getter
    public static class response {
        private Long id;
        private String abstractive;
        private List<Integer> extractive;
        private List<String> articleOriginal;
    }
}
  • Request와 Response가 다른 이유는 json데이터 형식중에article_original을 보면 리스트 형식으로 이루어져 있는데, 자연어 처리를 통해 문서를 의미있는 문장으로 나누어 리스트에 저장해놓은 것입니다. 이와 같은 동작을 진행하는 메서드는 Python 프로그램에 존재하기 때문에 요청의 경우 해당 문서를 그대로 보내는 역할만 수행하면 됩니다.
  • 요청하는 입장에서는 아래와 같이 데이터를 보내기로 약속했습니다. 이와 같은 약속은 환경이나 프로그램 상황에 따라 변경될 수 있을 것으로 예상됩니다.
    {
     "id": "121009",
     "abstractive": "",
     "extractive": [],
     "article_original": "시어머니가 아들이 그 처자를..."
    }

GetArticleRequestDto

@Getter
@Setter
public class GetArticleRequestDto {
    private String articleOriginal;
}

GetArticleResponseDto

@Getter
@Setter
public class GetArticleResponseDto {
    private String articleOriginal;
    private String abstractive;
}
  • GetArticleRequestDto와 GetArticleResponseDto는 클라이언트와 서버 사이에 데이터를 옮기기 위한 Dto입니다.

ObjectMapper

데이터를 Json형식의 문자열로 변환해 Python으로 요청하기 위해서 문자열을 Object로 변환하거나 Object를 문자열로 변환을 도와주는 ObjectMapper를 사용했습니다.

WriteValueAsString()

Object를 String으로 변환해줍니다.

GetAbstractiveDto.request abstractiveDto = new GetAbstractiveDto.request();
List<Integer> extractive = new ArrayList<>();
abstractiveDto.setId(8L);
abstractiveDto.setAbstractive("");
abstractiveDto.setExtractive(extractive);
abstractiveDto.setarticleOriginal("이것을 요약해주세요.");

ObjectMapper test = new Object();
test.writeValueAsString()

// 결과 : {"id":8,"abstractive":"","extractive":[],"articleOriginal":"이것을 요약해주세요."}

readValue(string, class)

String을 Object로 변환해줍니다.

ObjectMapper test = new Object();
GetAbstractiveDto.response response = test.readValue(responseEntity.getBody(), GetAbstractiveDto.response.class)

RestTemplate을 이용한 API 호출

들어가기 앞서, Spring Boot 3.0부터 지원하는 RestTemplate을 이용해 HTTP 통신으로 Flask에 요청을 계획했지만, 현재 Flask 서버를 띄우기에는 번거로움이 있어 임의로 Controller에 test를 만들어 SpringBoot에서 SpringBoot로 요청을 보내는 방식으로 구성했습니다.

RestTemplate 동작 원리

  1. 애플리케이션이 RestTemplate을 생성하고 URI, HTTP 메서드 등을 헤더에 담아서 요청합니다.
  2. RestTemplate는 HttpMessageConverter를 사용하여 RequestEntity를 요청메시지로 변환합니다.
  3. RestTemplate는 ClientHttpRequestFactory로 부터 ClientHttpRequest를 가져온후 요청을 보냅니다.
  4. ClientHttpRequest는 요청메세지를 만들어 HTTP 프로토콜을 통해 서버와 통신합니다.
  5. RestTemplate은 ResponseErrorHandler로 오류를 확인합니다.
  6. ResponseErrorHandler는 오류가 있다면 ClientHttpResponse에서 응답 데이터를 가져와서 처리합니다.
  7. RestTemplate은 HttpMessageConverter를 이용해서 응답메세지를 Java Object(Response Type)로 변환합니다.
  8. 어플리케이션에 반환합니다.

RestTemplate 메서드

메서드HTTP설명
getForObjectGETHTTP GET 요청 후 결과는 객체로 반환
getForEntityGETHTTP GET 요청 후 결과는 ResponseEntity로 반환
postForLocationPostHTTP POST 요청 후 결과는 헤더에 저장된 URL을 반환
postForObjectPostHTTP POST 요청 후 결과는 객체로 반환
postForEntityPostHTTP POST 요청 후 결과는 ResponseEntity로 반환
deleteDELETEHTTP DELETE 요청
headForHeadersHEADERHTTP HEAD 요청 후 헤더정보를 반환
putPUTHTTP PUT 요청
patchForObjectPATCHHTTP PATCH 요청 후 결과는 객체로 반환
optionsForAllowOPTIONS지원하는 HTTP 메소드를 조회
exchangeAny원하는 HTTP 메소드 요청 후 결과는 ResponseEntity로 반환
executeAnyRequest/Response의 콜백을 수정

TEST Controller

    @PostMapping("/test")
    public ResponseEntity<GetAbstractiveDto.response> testApi(@RequestBody GetAbstractiveDto.request abstractiveDto) {
        GetAbstractiveDto.response response = new GetAbstractiveDto.response();
        response.setId(abstractiveDto.getId());
        response.setAbstractive("abstractive");
        List<Integer> numList = new ArrayList();
        numList.add(1);
        numList.add(2);
        numList.add(3);
        response.setExtractive(numList);
        List<String> articleList = new ArrayList<>();
        articleList.add("article1");
        articleList.add("article2");
        articleList.add("article3");
        response.setArticleOriginal(articleList);
        return new ResponseEntity<>(response, HttpStatus.OK);
    }
  • API요청을 받기 위한 TestController입니다. POST 메서드로 넘어온 HTTP요청을 받아 임의로 작성된 데이터를 Dto에 담아 반환해줍니다.

SpringBoot Service - getAbstractive()

SpringBoot의 HTTP API요청을 위한 메서드로 요청 Request에 데이터를 담아 Flask(본 글에서는 Test Controller)으로 요청을 보냅니다.

public GetAbstractiveDto.response getAbstractive(GetAbstractiveDto.request abstractiveDto) throws Exception {
        //헤더 설정
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8")));

        // Object Mapper를 통한 JSON 바인딩
        List<Integer> extractive = new ArrayList<>();
        abstractiveDto.setExtractive(extractive);
        abstractiveDto.setAbstractive("");
        String params2 = objectMapper.writeValueAsString(abstractiveDto);

        // HttpEntity에 헤더 및 params 설정
        HttpEntity entity = new HttpEntity(params2, httpHeaders);

        // RestTemplate의 exchange 메소드를 통해 URL에 HttpEntity와 함께 요청
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<String> responseEntity = restTemplate.exchange("http://localhost:8080/test", HttpMethod.POST,
                entity, String.class);

        // 요청 후 응답 확인
        System.out.println(responseEntity.getStatusCode());
        System.out.println(responseEntity.getBody());

        // String to Object
        GetAbstractiveDto.response response = objectMapper.readValue(
                responseEntity.getBody(), GetAbstractiveDto.response.class
        );
        return response;
    }
  • Http 방식으로 API를 요청하기 때문에, HttpHeaders를 이용해 헤더에 ContentType을 설정해줍니다.
  • body에 json을 담기 위해 Object를 Json형식의 String으로 변환해줍니다.
  • HttpEntity를 설정해줍니다.
  • RestTemplate의 exchange를 이용해 원하는 HTTP 메서드 요청하고 결과는 ResponseEntity로 반환합니다.
  • 응답받은 responseEntity를 Object로 다시 변환해줍니다.

응답결과

200 OK
{"id":8,"abstractive":"abstractive","extractive":[1,2,3],"articleOriginal":["article1","article2","article3"]}

SpringBoot Service - getArticle()

Flask(본 글에서는 testController)으로 부터 응답받은 데이터를 데이터베이스에 알맞게 전처리 작업을 거쳐 데이터를 저장하기 위한 메서드입니다.

Controller

@PostMapping("/article")
    public GetArticleResponseDto getArticles(@RequestBody GetArticleRequestDto requestDto) throws Exception {
        GetArticleResponseDto abstractive = articleService.getArticle(requestDto);

        return abstractive;
    }

Service

    @Transactional
    public GetArticleResponseDto getArticle(GetArticleRequestDto requestDto) throws Exception {
        // 1. article 저장
        Article article = setArticle(requestDto);

        // 2. Java To Python HTTP 요청
        GetAbstractiveDto.request abstractiveDto = new GetAbstractiveDto.request();
        abstractiveDto.setArticleOriginal(requestDto.getArticleOriginal());
        abstractiveDto.setId(article.getId());
        GetAbstractiveDto.response result = getAbstractive(abstractiveDto);

        // 3. article 업데이트
        Article updateArticle = articleRepository.findById(article.getId()).orElseThrow(
                () -> new NullPointerException("해당 글이 존재하지 않습니다.")
        );
        updateArticle.setAbstractive(result.getAbstractive());
        Article responseResult = articleRepository.save(updateArticle);
        for (String originalSentence : result.getArticleOriginal()) {
            ArticleOriginalList articleOriginal = new ArticleOriginalList(originalSentence, updateArticle);
            articleOriginalListRepository.save(articleOriginal);
        }
        for (int num : result.getExtractive()) {
            Extractive sentenceNum = new Extractive(num, updateArticle);
            extractiveRepository.save(sentenceNum);
        }

        // 4. 요약 데이터 반환
        GetArticleResponseDto responseDto = new GetArticleResponseDto();
        responseDto.setArticleOriginal(responseResult.getArticleOriginal());
        responseDto.setAbstractive(responseResult.getAbstractive());
        return responseDto;
    }
    
    public Article setArticle(GetArticleRequestDto requestDto) {
        Article article = new Article(requestDto);
        return articleRepository.save(article);
    }
  1. 어플리케이션으로부터 넘어온 원본 문서를 데이터베이스에 저장하고 유니크한 id값을 얻습니다.
  2. 1번으로 얻은 id값과 원본 문서 데이터를 이용해 요청 request를 만들고, 앞서 만든 http 요청 메서드를 실행시킵니다.
  3. python(본 글에서는 testController)로부터 받은 response를 알맞은 필드에 업데이트합니다.
  4. 이후 요약된 데이터는 json형식으로 클라이언트에 반환합니다.

결과


트러블슈팅

cannot be null

Article을 join하는 도메인의 경우(ArticleOriginalList, Extractive) nullable이 false로 설정했음에도 불구하고 생성자에 article을 넣어주지 않아서 해당 에러가 발생했습니다.
해당 문제를 해결하기 위해서 생성자에 Join하는 컬럼에 대한 Object를 함께 추가함으로서 해결했습니다.

    @ManyToOne
    @JoinColumn(name = "article_idx", nullable = false)
    private Article article;

    public ArticleOriginalList(String articleOriginalSentence, Article article) {
        this.articleOriginalSentence = articleOriginalSentence;
        this.article = article;
    }

긴 문자열 저장 에러 발생

데이터베이스에 원본 문서필드에 긴 문자열을 저장하려고 할 때 긴 문자열을 저장할 수 없다는 에러가 발생했습니다.
해당 문제는 Column의 String 기본 데이터 형식의 문제로 Column상에 데이터 형식을 정의해줌으로서 해결했습니다.

    @Column(nullable = false, columnDefinition = "text")
    private String articleOriginal;

참조

AIHub - 문서요약 텍스트 소개
jackson ObjectMapper 정리 _ RyanGomdoriPooh
[WEB] RestTemplate을 이용하여 API 호출하기 _ 프로그래민
spring boot data jpa 생성표의 긴 텍스트 필드 _ Intrepidgeeks

profile
잘 부탁드려요

0개의 댓글