반응형

프로젝트에서 DeepL API를 사용해 PDF를 번역하는 기능이 추가되었다.

https://developers.deepl.com/docs

 

Introduction | DeepL API Documentation

Learn more about the DeepL API's capabilities and common use cases.

developers.deepl.com

 

우선 DeepL의 공식문서이다.

처음에는 늘 그렇듯 curl을 사용해 파일을 보내고, 응답을 받을 것이라고 생각했지만 자바쪽으로 지원해주는 라이브러리가 있었다.

 

 

자바 라이브러리의 주소는 다음과 같다.

https://github.com/DeepLcom/deepl-java

 

GitHub - DeepLcom/deepl-java: Official Java library for the DeepL language translation API.

Official Java library for the DeepL language translation API. - DeepLcom/deepl-java

github.com

 

요즘에는 자료가 많이 없는 기능들을 사용하다보니, 이렇게 깃허브에 직접 찾아들어가 사용방법을 찾아보는 일이 많아졌다.

 

implementation("com.deepl.api:deepl-java:1.9.0")

 

현재 기준으로 가장 최신 버전인 해당 라이브러리를 추가해준다.

 

처음에는 해당 문서처럼 Translator를 생성하려고 했지만, 해당 기능은 Deprecated 되었다고 한다.

(그러면 문서 좀 수정해주지...)

 

지금은 DeepLClient 클래스를 생성하고, 해당 생성자에 DeepL의 API 키를 넘겨주면 된다.

 

DeepLClient(deepLKey)

 

해당 클래스에서도 translateDocument라는 함수를 사용할 것이다.

 

해당 함수를 오버로딩하고 있는 함수들은 다음과 같다.

    public DocumentStatus translateDocument(File inputFile, File outputFile, @Nullable String sourceLang, String targetLang, @Nullable DocumentTranslationOptions options) throws DocumentTranslationException, IOException {
        try {
            if (outputFile.exists()) {
                throw new IOException("File already exists at output path");
            } else {
                InputStream inputStream = new FileInputStream(inputFile);

                DocumentStatus var8;
                try {
                    OutputStream outputStream = new FileOutputStream(outputFile);

                    try {
                        var8 = this.translateDocument(inputStream, inputFile.getName(), outputStream, sourceLang, targetLang, options);
                    } catch (Throwable var12) {
                        try {
                            outputStream.close();
                        } catch (Throwable var11) {
                            var12.addSuppressed(var11);
                        }

                        throw var12;
                    }

                    outputStream.close();
                } catch (Throwable var13) {
                    try {
                        inputStream.close();
                    } catch (Throwable var10) {
                        var13.addSuppressed(var10);
                    }

                    throw var13;
                }

                inputStream.close();
                return var8;
            }
        } catch (Exception exception) {
            outputFile.delete();
            throw exception;
        }
    }

    public DocumentStatus translateDocument(File inputFile, File outputFile, @Nullable String sourceLang, String targetLang) throws DocumentTranslationException, IOException {
        return this.translateDocument((File)inputFile, (File)outputFile, (String)sourceLang, targetLang, (DocumentTranslationOptions)null);
    }

    public DocumentStatus translateDocument(InputStream inputStream, String fileName, OutputStream outputStream, @Nullable String sourceLang, String targetLang, @Nullable DocumentTranslationOptions options) throws DocumentTranslationException {
        DocumentHandle handle = null;

        try {
            handle = this.translateDocumentUpload(inputStream, fileName, sourceLang, targetLang, options);
            DocumentStatus status = this.translateDocumentWaitUntilDone(handle);
            this.translateDocumentDownload(handle, outputStream);
            return status;
        } catch (Exception exception) {
            throw new DocumentTranslationException("Error occurred during document translation: " + exception.getMessage(), exception, handle);
        }
    }

    public DocumentStatus translateDocument(InputStream inputFile, String fileName, OutputStream outputFile, @Nullable String sourceLang, String targetLang) throws DocumentTranslationException {
        return this.translateDocument(inputFile, fileName, outputFile, sourceLang, targetLang, (DocumentTranslationOptions)null);
    }

 

File을 넘기는 함수가 아닌 inpuStream을 넘기는 함수를 사용할 것이며, 사용 가능한 언어의 종류는 DeepL의 공식문서에 나와있다.

 

여기서 sourceLang은 Null이 가능하다.

현재 문서에 대한 정보를 주지 않아도, 번역을 해보니 sourceLang을 지정해 줄 때와 똑같은 결과가 나왔었다.

 

해당 함수를 사용하면 byteOutputStream이 나오게 된다.

해당 byteOutputStream을 byteArray로 바꾸어서 controller에서 응답해주면 된다.

@Service
class DeepLManager(
    @Value("\${deepL.key}")
    private val deepLKey: String
) {

    fun translateDocument(inputStream: InputStream, fileName: String, targetLang: LanguageEnum): ByteArray {
        ByteArrayOutputStream().use{
            byteArrayOutputStream ->
            DeepLClient(deepLKey).translateDocument(
                inputStream,
                fileName,
                byteArrayOutputStream,
                null,
                targetLang.targetLang,
            )
            return byteArrayOutputStream.toByteArray()
        }
    }

}

 

해당 서비스는 DeepL로 부터 번역된 ByteArray를 가져오는 서비스이고

그냥 응답받은 byteArrayOutputStream에서 toByteArray만 호출하면 ByteArray로 변환된다.

 

이렇게 응답받은 ByteArray을

return translateConnector.createTranslate(
            docsId, createTranslateReq
        ).map{
            ByteArrayOutputStream().use{

            }
            ResponseEntity.ok()
                .contentType(
                    MediaType.APPLICATION_OCTET_STREAM
                )
                .headers{
                    header ->
                    header.contentDisposition = ContentDisposition.builder("pdf")
                        .filename("${docsId}-${createTranslateReq.targetLang.name}.pdf")
                        .build()
                }
                .body(it)
        }

 

이런 식으로 controller에서 응답해주면 된다.

이렇게 하면 지정해준 파일의 이름으로 번역된 파일을 다운받을 수 있다.

 

+ 근데 이게 생각보다 돈이 많이 나오는 거 같다.

이렇게 만들고 몇번 요청을 한 후, PM한테 금액을 확인해달라고 부탁하니 벌써 5만원이 나왔다고 한다...

다들 조심해서 사용하는 게 좋을 것 같다.......

'틔움랩' 카테고리의 다른 글

MockBean deprecated와 대체  (0) 2025.01.28
Github action을 통한 Spring CI/CD 설정  (0) 2025.01.25
반응형

전날에 작성해 놓은 테스트를 추가하기 위해, 아침부터 테스트 코드를 작성하던 중

@MockBean이 적용되지 않는 문제가 발생했다.

 

아예 사용할 수 없도록 되어버린 것 같고, 대체가 어떻게 되었을까 찾아보다가 아래와 같은 글을 발견했다.

 

https://stackoverflow.com/questions/79243535/what-is-the-replacement-for-the-deprecated-mockbeans-in-springboot-3-4-0

 

What is the replacement for the deprecated @MockBeans in SpringBoot 3.4.0?

We defined an annotation for our tests to avoid to declare Mockito Mocks for all the test classes. @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @MockBeans(...

stackoverflow.com

 

@MockitoBean

이것으로 대체가 되었다고 한다.

 

 

이렇게 하니 적용이 되는 것을 볼 수 있다.

 

다른 프로젝트에서는 되던 것이, 버전이 바뀌었다고 되지 않아서 깜짝 놀랐었다...

반응형

프로젝트를 시작할 때마다, 가장 먼저하는 CI/CD 설정이다.

기존에는 대부분 Jenkins를 통해 CI/CD 설정을 했었지만, 이번 프로젝트는 별도의 서버를 구성할 여유가 없어 Github에서 제공하는 Github action을 통해 설정을 해보려고 한다.

 

우선 Github의 리포지토리에서 actions 탭을 클릭한다.

그러면 이런 식으로 페이지가 나올텐데, 여기에 gradle을 검색한다.

 

그리고 이 action을 configure해준다.

 

이거 클릭하면 코드의 .github 폴더에 yml 파일이 생길텐데, 해당 파일을 수정해주면 된다.

 

크게 build와 deploy로 나뉜다.

build는 Gradle을 바탕으로 spring을 빌드하는 부분이고, deploy는 빌드된 jar파일을 서버로 전송하여 배포하는 부분이다.

 

name: CI/CD for develop branch

on:
  push:
    branches: [ "develop" ]
  pull_request:
    branches: [ "develop" ]
  workflow_dispatch:
    

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582

    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
      working-directory: ./WAS

    - name: Build with Gradle Wrapper
      run: ./gradlew build
      working-directory: ./WAS
      
    - name: Scp to EC2
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{secrets.SSH_USER}}
        key: ${{secrets.SSH_PRIVATE_KEY}}
        source: |
          WAS/build/libs/{빌드된 파일 이름}.war
        target: was/
      
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.4
        with:
          ssh-private-key: ${{secrets.SSH_PRIVATE_KEY}}
          
      - name: Deploy Docker Container
        run: |
          ssh -o StrictHostKeyChecking=no ${{secrets.SSH_USER}}@${{secrets.SERVER_IP}} << 'EOF'
          cd was
          docker stop {컨테이너 이름} && docker rm {컨테이너 이름} && docker rmi {이미지 이름}:latest
          docker build -t {이미지 이름} .
          docker-compose up -d
          EOF

 

우선 작성한 파일은 다음과 같다.

 

step에서 하나씩 뜯어보도록 하자.

  • build
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582

이 부분은 자바와 gradle을 준비하는 부분이다.

큰 이상이 없다면 아마 고정적으로 사용할 것 같다.

 

    - name: Grant execute permission for gradlew
      run: chmod +x ./gradlew
      working-directory: ./WAS

    - name: Build with Gradle Wrapper
      run: ./gradlew build
      working-directory: ./WAS

이 부분은 빌드하는 부분이다.

gradlew에 실행 권한을 부여하고, ./gradlew build를 통해 스프링부트를 빌드한다.

만약 테스트와 관련된 명령어가 있다면, 이곳에 추가하면 된다.

 

아래를 보면 working-directory가 설정되어 있는 것을 볼 수 있는데, 만약 빌드하는 폴더가 루트 폴더가 아니라면 해당 명령어를 통해 폴더를 지정해주어야 한다.

 

    - name: Scp to EC2
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.SERVER_IP }}
        username: ${{secrets.SSH_USER}}
        key: ${{secrets.SSH_PRIVATE_KEY}}
        source: |
          WAS/build/libs/{빌드된 파일}.war
        target: was/

이제 빌드된 jar(war) 파일을 서버로 전송하는 부분이다.

당연히 서버의 ip, 사용자 이름, pem 키가 필요하다.

이런 부분은 yml 파일에 작성할 수 없기 때문에, 해당 Repository의 설정에 환경변수로 추가해주도록 하자.

 

pem키를 환경변수로 추가할 때는 위, 아래 boundary까지 그냥 추가해주면 된다.

source는 빌드된 파일(결과물)이고, target은 해당 서버에서 어떤 폴더에 저장할 것인지를 적어주면 된다.

 

이렇게 작성하면 빌드 후, 파일이 서버에 저장되게 된다.

그럼 이제 해당 파일을 통해 배포를 설정해주어야 한다.

 

 

  • deploy

해당 jar 파일을 docker container를 통해 실행해보자.

 

    needs: build

needs에 build를 추가해서, 해당 작업이 build 이후에 이루어질 수 있도록 해준다.

 

 

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.5.4
        with:
          ssh-private-key: ${{secrets.SSH_PRIVATE_KEY}}

서버에서 사용할 pem키를 등록해준다.

 

      - name: Deploy Docker Container
        run: |
          ssh -o StrictHostKeyChecking=no ${{secrets.SSH_USER}}@${{secrets.SERVER_IP}} << 'EOF'
          cd was
          docker stop {컨테이너 이름} && docker rm {컨테이너 이름} && docker rmi {이미지 이름}:latest
          docker build -t {이미지 이름} .
          docker-compose up -d
          EOF

 

그리고는 이제 원격 서버에 접속해 해당 파일을 바탕으로 도커를 빌드한다.

원격 서버에서 추가적으로 실행하고 싶은 부분이 있다면, 저 안에 추가해주면 된다.

 

이제 작성이 모두 완료되었으니, 실행해보도록 하자.

위의 on에 workflow_dispatch:를 지정해주면 직접 실행 할 수도 있다.

 

 

이렇게 초록색으로 체크표시가 된다면 성공한 것이다.

서버에서도 배포가 완료된 것을 확인 할 수 있었다.

'틔움랩' 카테고리의 다른 글

Spring에서 DeepL API를 사용해 문서를 번역하기  (0) 2025.02.27
MockBean deprecated와 대체  (0) 2025.01.28

+ Recent posts