파이썬 처리가 필요한 이유
현재 졸업프로젝트로 백엔드를 담당하고 있고, 스프링을 사용하기로 결정했다. 우리의 비즈니스 로직 중 STT와 키워드 추출이 필요한데, 이는 파이썬 쪽에서 처리가 필요하다. 처음에는 다음과 같이 처리를 하였다.
1. 파이썬 파일 실행 (ProcessBuilder, Process)
자바에서 ProcessBuilder를 사용하면, 컴퓨터의 python을 실행시킬 수 있다. 코드는 다음과 같다.
package site.atkproject.sttservice.util;
import lombok.NoArgsConstructor;
import java.io.*;
@NoArgsConstructor
public abstract class PythonFileManager {
protected String homePath = System.getProperty("user.home");
protected String pythonPath;
protected String finalPath;
protected String appPath;
public PythonFileManager(PythonFileName pythonFileName, String appPath) {
this.pythonPath = pythonFileName.getPath();
this.appPath = appPath;
this.finalPath = homePath + pythonPath;
}
public String getResult(ProcessBuilder processBuilder) {
try {
processBuilder.redirectErrorStream(true);
Process process = processBuilder.directory(new File(homePath + appPath)).start();
OutputStream stdin = process.getOutputStream();
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(stdin));
bw.flush();
bw.close();
String result = getPythonStream(process.getInputStream());
// boolean exitCode = process.waitFor(20, TimeUnit.SECONDS);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IllegalArgumentException("키워드 생성 도중 오류가 발생했습니다.");
}
return result;
} catch (Exception e) {
e.printStackTrace();
throw new IllegalArgumentException("키워드 생성 도중 오류가 발생했습니다.");
}
}
public String getPythonStream(InputStream inputStream) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
while (true) {
String line = br.readLine();
if (line == null) {
break;
}
sb.append(line).append("\n");
}
return sb.toString();
}
}
- 맞게 설계한건진 모르겠지만 일단 추상 클래스로 만들어 두어 공통화된 부분을 두고, STT와 Keyword 클래스를 만들어 각기 다른 처리를 해주었다.
- 홈 디렉터리는 테스트 컴퓨터들마다 다르기에 System.getProperty("user.home")을 통해 사용자의 홈 디렉터리 경로를 지정할 수 있다. 파이썬 프로그램의 위치는 "home/app/stt or keyword/파일 이런 형태고, 최종 경로는 각 클래스에서 지정한다.
- 이후 Process.builder(File 객체).start()를 통해 Process를 받고 이를 실행시키면 된다.
- getPythonStream()을 통해 결과를 받아 저장해주면 된다. 참.. 파이썬으로는 파일 읽기가 쉬웠는데 자바는 너무 번거로운거 같다.
public class SttPythonFile extends PythonFileManager implements SttManager {
public SttPythonFile() {
super(PythonFileName.STT, "/app/stt");
}
public String getSTT(String fileName) {
int INDEXING = 7;
String result = getResult(new ProcessBuilder(homePath + PythonFileName.PYTHON.getPath(), finalPath, fileName));
System.out.println(result);
int i = result.indexOf("result=") + INDEXING;
result = result.substring(i);
return result;
}
public static void main(String[] args) {
SttPythonFile pythonSTT = new SttPythonFile();
long start = System.currentTimeMillis();
pythonSTT.getSTT("seokju/testaudio.wav");
System.out.println((System.currentTimeMillis() - start)/1000);
}
}
STT를 처리하는 파이썬 호출 클래스다. 생성자를 통해 경로를 정해주고, getResult시 ProcessBuilder를 만들어 넣어주었다. 그리고 조금 처리해서 서비스에 돌려주고, 디비에 반영해주었다. 속도도 나름 나쁘지 않다 생각한다. 그래도 서버로 바꿔보려는 시도를 한 이유는 다음과 같다.
- 모델을 통해 처리가 필요한 데이터는 시간이 걸린다. python 파일.py 방식으로 하면, 새로 실행되다보니 학습이 필요한 경우에는 처음부터 올리기 때문에 시간이 걸리게 되었다.
- 두 번째는 나의 궁금증이다. 지금까지 스프링은 서버 역할만 했기에 클라이언트에서 온 요청만 처리하였다. 하지만 현대 웹 생태계에서는 내 서버가 클라이언트가 될 수도 있는 것이다. 그리고 그 통신을 api 요청 형태로 처리를 해주는 것인데, 생각해보니 한 번도 안해본 것이다. 그래서 이번기회에 써보았다!
WebClient 사용
기존 스프링에서는 RestTemplate을 이용했는데, 이것은 이제 Deprecated 된다하고 Webclient를 쓴다고 한다. 그래서 다음과 같이 의존성을 추가해주면 된다. WebFlux가 무엇인지는 공부해서 다른 포스팅에 정리해보겠다. 지금은 사용에 집중해보겠다.
implementation 'org.springframework.boot:spring-boot-starter-webflux'
그리고 기존 파이썬 파일을 실행하는 방식도 일단 두고 싶어서 어떻게 할까 고민하다가 인터페이스를 만들어 구현체를 나누어 주었다.
public interface Keyword {
String getKeyword(String content);
default String[] separateWords(String content) {
String result = getKeyword(content);
return result.split(",");
}
}
공통화된 부분은 default 메서드로 해두었다.
이제 파이썬으로 통신하는 클래스를 만들어주어야 하는데, 그 전에 파이썬 서버인 Fast API 간단 설정을 해보겠다.
FastApi 설정
FastApi는 Django와 Flask같은 파이썬 기반 웹 프레임워크고 Flask보다 성능이 더 좋아 각광받고 있다 한다. 딥하게 다룰건 아니라 나는 main 파일에 다 넣어줬다. 설치법은 구글링을 하면 금방 나온다.
from typing import Optional
from atk_stt import *
from atk_keyword import get_keywords
from atk_make_dict import make_dict
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Keyword(BaseModel):
content: str
class KeywordOut(BaseModel):
result: list
class KeywordList(BaseModel):
keywords: list
@app.get("/items/{file_name}")
async def read_item(file_name: str):
trans = get_stt(f"seokju/{file_name}.wav")
return {"trans": trans}
@app.get("/api/stt")
async def read_item(username: str, filename: str):
final_filename = f"{username}/{filename}"
print(final_filename)
trans = get_stt(final_filename)
return {"result": trans}
@app.post("/api/keyword")
async def extract_keyword(keyword: Keyword):
print(keyword.content)
result = get_keywords(keyword.content)
return {
"result": result
}
@app.post("/api/keyword-info")
async def make_keyword_info(keywords: KeywordList):
print(keywords)
make_dict(keywords.keywords)
@app.get과 post를 통해 HTTP Method를 구분하고, 모델을 바로 받을 수 있다는게 스프링과 유사한 듯 하다.
이제 다시 돌아와 자바에서 클래스를 만들어 주었다.
FastAPI 클래스
package site.atkproject.sttservice.util;
import lombok.Data;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.Objects;
@Component
public class FastApi {
private static final String url = "http://localhost:8000";
public WebClient webClient = WebClient.create(url);
public String getStt() {
long start = System.currentTimeMillis();
String block = webClient.get().uri("/items/testaudio").retrieve().bodyToMono(String.class).block();
System.out.println((System.currentTimeMillis()-start)/1000);
return block;
}
public String getKeyword() {
Keyword keyword = new Keyword();
keyword.content = "hello bye hi may i help you";
Mono<Result> resultMono = webClient.post().uri("/api/keyword").body(Mono.just(keyword), Keyword.class).retrieve()
.bodyToMono(Result.class);
return Objects.requireNonNull(resultMono.block()).result;
}
public static void main(String[] args) {
FastApi fastApi = new FastApi();
String keyword = fastApi.getKeyword();
System.out.println(keyword);
}
@Data
static class Keyword {
private String content;
}
@Data
static class Result {
private String result;
}
}
WebClient를 하나 만들어주고 필요할 때마다 불러서 사용해주면 된다. 반환 타입으로 Mono와 Flux를 받는데, Mono는 0~1개의 응답을 받을 때 쓰고, Flux는 그 이상을 받을 때 쓴다는데 어떤 의민지는 나중에 더 다루어봐야겠다. request, response용 dto는 이 내부에서만 쓸거 같아 따로 클래스로 빼지 않고 내부클래스로 구성했다. fastApi용 클래스를 만들었으니 구현체에 적용해보겠다.
구현체
@RequiredArgsConstructor
@Primary
@Component
public class SttClient implements SttManager {
private final FastApi fastApi;
@Override
public String getSTT(String fileName) {
String[] split = fileName.split("/");
String username = split[0];
String filename = split[1];
Mono<Response> responseMono = fastApi.webClient.get().
uri(uriBuilder -> uriBuilder
.path("/api/stt")
.queryParam("username", username)
.queryParam("filename", filename)
.build())
.retrieve()
.bodyToMono(Response.class);
return Objects.requireNonNull(responseMono.block()).result;
}
@Data
static class Response {
private String result;
}
}
인터페이스를 implement 했으니 getSTT 메서드를 구현해주면 된다. 파일이름(seokju/test.wav)을 가지고 쿼리 파라미터를 만들어 줄것이기에 다음과 같이 split을 통해 나누고 queryParam의 인자들로 넣어주었다. 이런식으로 활용하면 스프링과 파이썬 서버와 통신하여 파이썬에서 처리해줘야 할 로직들을 넘겨줄 수 있다. 이런게 api통신임을 조금은 알게 되는 학습이었다.
'Spring > mvc' 카테고리의 다른 글
[Spring Thymeleaf] Layout 만들기 (0) | 2022.05.08 |
---|---|
[Spring] Thymeleaf 정리 (0) | 2022.03.16 |
댓글