Ml/Md 개발자와 문서를 요약해주는 프로젝트를 진행했다.
백엔드 개발자인 나의 경우 Java에서 개발하기를 원하는 상황이였고 Ml/Md 개발자인 친구는 Python에서 프로젝트를 개발해야하는 상황이였기 때문에, 각자 환경에서 개발을 마무리하고 Http 통신을 이용해 데이터만 Json형식으로 건내받기로 결정을 했다.
springboot 버전 2.6.7
JAVA 11
문서를 요약하는 프로그램의 경우 AIHub에 문서요약 텍스트 소개를 활용할 예정이였기 때문에, 해당 프로그램에서 사용한 데이터 셋과 동일하기 Json파일을 만들어 보내줘야할 필요가 있었습니다.
아래와 같은 json 데이터 형식을 Flask로 전송해야할 필요가 있었습니다.
{
"id": "121009",
"abstractive": "시어머니가 아들이 처자를 내버려둔 채 다른...",
"extractive": [5, 0, 6],
"article_original":
[
"시어머니가 아들이 그 처자를...",
"다른 여자로 하여금 자기가...",
"..."
]
}
도메인의 경우 원본과 요약본만 저장해도 된다고 판단했지만, 기존의 샘플데이터의 id값과 다른 유니크한 id값을 생성 및 전달해야 했기 때문에 데이터베이스 상에 기존 샘플 데이터의 id값을 모두 가지고 있어야 했습니다. 이와 같은 이유로 도메인을 json 데이터 셋과 동일하게 세팅했습니다.
@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();
}
}
@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;
}
}
@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;
}
}
데이터를 담아 보내기 위한 Object로 3가지의 클래스를 생성했습니다.
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;
}
}
{
"id": "121009",
"abstractive": "",
"extractive": [],
"article_original": "시어머니가 아들이 그 처자를..."
}
@Getter
@Setter
public class GetArticleRequestDto {
private String articleOriginal;
}
@Getter
@Setter
public class GetArticleResponseDto {
private String articleOriginal;
private String abstractive;
}
데이터를 Json형식의 문자열로 변환해 Python으로 요청하기 위해서 문자열을 Object로 변환하거나 Object를 문자열로 변환을 도와주는 ObjectMapper를 사용했습니다.
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":"이것을 요약해주세요."}
String을 Object로 변환해줍니다.
ObjectMapper test = new Object();
GetAbstractiveDto.response response = test.readValue(responseEntity.getBody(), GetAbstractiveDto.response.class)
들어가기 앞서, Spring Boot 3.0부터 지원하는 RestTemplate을 이용해 HTTP 통신으로 Flask에 요청을 계획했지만, 현재 Flask 서버를 띄우기에는 번거로움이 있어 임의로 Controller에 test를 만들어 SpringBoot에서 SpringBoot로 요청을 보내는 방식으로 구성했습니다.
메서드 | HTTP | 설명 |
---|---|---|
getForObject | GET | HTTP GET 요청 후 결과는 객체로 반환 |
getForEntity | GET | HTTP GET 요청 후 결과는 ResponseEntity로 반환 |
postForLocation | Post | HTTP POST 요청 후 결과는 헤더에 저장된 URL을 반환 |
postForObject | Post | HTTP POST 요청 후 결과는 객체로 반환 |
postForEntity | Post | HTTP POST 요청 후 결과는 ResponseEntity로 반환 |
delete | DELETE | HTTP DELETE 요청 |
headForHeaders | HEADER | HTTP HEAD 요청 후 헤더정보를 반환 |
put | PUT | HTTP PUT 요청 |
patchForObject | PATCH | HTTP PATCH 요청 후 결과는 객체로 반환 |
optionsForAllow | OPTIONS | 지원하는 HTTP 메소드를 조회 |
exchange | Any | 원하는 HTTP 메소드 요청 후 결과는 ResponseEntity로 반환 |
execute | Any | Request/Response의 콜백을 수정 |
@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);
}
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;
}
200 OK
{"id":8,"abstractive":"abstractive","extractive":[1,2,3],"articleOriginal":["article1","article2","article3"]}
Flask(본 글에서는 testController)으로 부터 응답받은 데이터를 데이터베이스에 알맞게 전처리 작업을 거쳐 데이터를 저장하기 위한 메서드입니다.
@PostMapping("/article")
public GetArticleResponseDto getArticles(@RequestBody GetArticleRequestDto requestDto) throws Exception {
GetArticleResponseDto abstractive = articleService.getArticle(requestDto);
return abstractive;
}
@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);
}
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