학교/캡스톤디자인과창업프로젝트

[Spring] 네이버 CLOVA OCR API 연결

ChaSso 2024. 5. 21. 13:49

네이버 클로바 OCR

  프론트엔드에서는 보안이 보장된 HTTPS를 통해 네이버 클로바 OCR API로 요청을 보내지 못하기 때문에 HTTPS로 보안 접속이 가능한 백엔드에서 CLOVA OCR API로 요청을 보내도록 구현해야 했다. 백엔드는 스프링 프레임워크에서 구현했다.

 

  이 글에서는 CLOVA OCR 도메인 등록 이후 백엔드에서 OCR API에 요청을 보내고 반환 받은 텍스트 인식 결과를 이용하는 기능을 구현하기 위해 백엔드와 OCR API를 연결하는 방법을 작성하려 한다. 여기서는 CLOVA OCR의 일반 모델이 아닌 영수증 특화 모델을 이용했다. (모델에 따라 버전이 상이하기 때문에 참고해야 하는 API 예제 코드가 다르다.)

 

코드 작성

  우선 application.yml 파일에 OCR API 관련된 사항을 추가해준다. Naver cloud platform에서 CLOVA OCR의 Domain에 들어가보면 연결하고자 하는 도메인의 오른편에 'API Gateway 연동' 버튼이 있다. 버튼을 눌러보면 Secret Key와 APIGW Invoke URL를 조회할 수 있다. {APIGW Invoke URL}과 {secretKey} 자리에 복사한 각각의 값을 넣어 application.yml에 다음 코드를 추가한다.

naver:
  service:
    url: {APIGW Invoke URL}
    secretKey: {secretKey}

 

 

  다음과 같이 Controller단에서 작성한 코드로 구현된 기능에서 CLOVA API를 사용하려한다. CLOVA API를 사용하는 Service단의 클래스 명은 ClovaOcrApi로 작성했다. Controller에서 프론트로부터 받아온 MultipartFile 타입의 영수증 이미지를 Service단에 있는 ClovaOcrApi로 전달한다.

@PostMapping(value = "/ingredients/text", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(value = HttpStatus.CREATED)
public List<OcrResponseDto> getOcrIngredientList(@RequestPart(value = "image") MultipartFile multipartFile) throws IOException {
    Member member = memberService.getLoginMember();
    return clovaOcrApi.getOcrList(member, multipartFile);
}

 

 

  Service단인 ClovaOcrApi 클래스에 private String으로 apiUrl, secretKey를 선언하고 위에서 작성했던 yml 파일의 url, secretKey 값을 @Value를 이용해 할당한다. 

@Slf4j
@Service
@Component
@RequiredArgsConstructor
public class ClovaOcrApi {
    @Value("${naver.service.url}")
    private String apiUrl;
    @Value("${naver.service.secretKey}")
    private String secretKey;

 

 

  다음과 같이 OCR API의 응답을 반환받는 메서드 getResult를 Service단인 ClovaOcrApi 클래스 내에 작성했다. 공식 문서 CLOVA OCR Document API 예제의 코드를 참고하여 작성했다. (영수증 모델을 이용할 것이기 때문에 CLOVA OCR Custom API가 아닌 CLOVA OCR Document API의 예제를 참고해야 한다.)  공식 문서에는 getResult 메서드를 따로 두지 않고 다음 코드도 모두 OCRGeneralAPIDemo 클래스 내에 작성되어 있다. 해당 문서에는 API에 multipart/form-data 형식으로 요청을 보내는 방식과 application/json 형식으로 요청을 방식이 나와있는데 application/json 방식으로 작성해보려 한다.

public StringBuffer getResult(MultipartFile multipartFile) {
    try {
        URL url = new URL(apiUrl);
        HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setUseCaches(false);
        con.setDoInput(true);
        con.setDoOutput(true);
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json; charset=utf-8");
        con.setRequestProperty("X-OCR-SECRET", secretKey);

        JSONObject json = new JSONObject();
        json.put("version", "V2");
        json.put("requestId", UUID.randomUUID().toString());
        json.put("timestamp", System.currentTimeMillis());
        JSONObject image = new JSONObject();
        image.put("format", "png");

        String imageByte = encode(multipartFile);
        image.put("data", imageByte);
        image.put("name", "demo");
        JSONArray images = new JSONArray();
        images.add(image);
        json.put("images", images);
        String postParams = json.toString();

        DataOutputStream wr = new DataOutputStream(con.getOutputStream());
        wr.writeBytes(postParams);
        wr.flush();
        wr.close();

        int responseCode = con.getResponseCode();
        BufferedReader br;
        if (responseCode == 200) {
            br = new BufferedReader(new InputStreamReader(con.getInputStream()));
        } else {
            br = new BufferedReader(new InputStreamReader(con.getErrorStream()));
        }
        String inputLine;
        StringBuffer response = new StringBuffer();
        while ((inputLine = br.readLine()) != null) {
            response.append(inputLine);
        }
        br.close();
        return response;
    } catch (Exception e) {
        System.out.println(e);
        StringBuffer a = null;
        a.append(e);
        return a;
    }
}

  getReult의 흐름을 정리해보자면, yml 파일에 입력한 apiUrl을 이용해 connection을 맺고 영수증 사진인 image를 포함한 데이터를 담은 json 객체를 만들어 요청을 보낸다. 그 후 OutputStream으로 결과값을 받고 StringBuffer 타입으로 변환해 return 한다.

 

  예제 코드를 수정한 부분을 살펴보려 한다. 우선 예제 코드에서는 file의 위치를 FileInputStream에 넣어 읽어와서 byte 타입으로 변환하도록 구현되어 있는데, 위 코드는 프론트로부터 받아온 multipartFile을 encode 메서드로 바로 byte 타입으로 변환하여 json 객체의 image "data" 파트에 넣어준다. ClovaOcrApi 클래스에 추가로 작성한 encode 메서드의 코드는 다음과 같다. MultipartFile 타입의 데이터를 Base64 인코딩을 하여 byte 타입으로 변환하는 메서드다.

public String encode(MultipartFile file) throws IOException {
    try {
        byte[] fileBytes = file.getBytes();
        return Base64.getEncoder().encodeToString(fileBytes);
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

 

  결과값이 잘 나오는지 확인하기 위해서 return response; 코드 윗줄에 System.out.println(response); 를 추가해 콘솔에서 결과값을 확인하며 개발을 진행했다.

 

API 테스트

  일단 Controller단의 메서드가 file 타입으로 image를 받고 빈 재료 리스트를 반환하도록 작성하고 콘솔창에서 OCR API의 결과값이 어떻게 나오는지부터 포스트맨 테스트를 통해 확인해봤다. Http Method는 POST로 설정했다.

 

image01.png
Postman 테스트

  image01.png 파일을 key 값이 'image'인 value에 할당하여 form-data 형식인 Body에 넣고 요청을 보냈다.

 

 

  그 결과, 위와 같이 response가 반환된 것을 확인할 수 있었다. response에는 가게 정보, 구매한 상품들, 총 금액 등의 데이터가 포함되어 있다. 공식문서에서 응답 바디에 대한 정보뿐만 아니라 Receipt 응답 예시가 나와있으니 참고하면 도움이 될 것 같다.

 

  위와 같은 결과를 json 형식으로 가독성 좋게 들여쓰기를 해보면 다음과 같다. 구현할 기능에서 필요로 하는 값들을 여기서 추출해서 이용하면 된다.

{
    "version":"V2",
    "requestId":"3868ec76-edce-4c99-be32-c8b24e493673",
    "timestamp":1716270345483,
    "images":[
        {
            "receipt":{
                "meta":{
                    "estimatedLanguage":"ko"
                },
                "result":{
                    "storeInfo":{
                        "name":{
                            "text":"퇴촌농협하나로마트",
                            "formatted":{
                                "value":"퇴촌농협하나로마트"
                            },
                            "keyText":"",
                            "confidenceScore":0.30847,
                            "boundingPolys":[
                                {
                                    "vertices":[
                                        {
                                            "x":101.0,
                                            "y":65.0
                                        },
                                        {
                                            "x":234.0,
                                            "y":66.0
                                        },
                                        {
                                            "x":234.0,
                                            "y":88.0
                                        },
                                        {
                                            "x":101.0,
                                            "y":87.0
                                        }
                                    ]
                                }
                            ],
                            "maskingPolys":[]
                        },
                        "bizNum":{
                            "text":"126-82-01059",
                            "formatted":{
                                "value":"126-82-01059"
                            },
                            "keyText":"사업자번호",
                            "confidenceScore":0.91475
                            "boundingPolys":[{"vertices":[{"x":180.0,"y":119.0},{"x":272.0,"y":118.0},{"x":272.0,"y":133.0},{"x":180.0,"y":134.0}]}],
                            "maskingPolys":[]
                        },
                        "addresses":[
                            {
                                "text":"경기도 광주시 퇴촌면 광동로 52번길 7",
                                "formatted":{"value":"경기도 광주시 퇴촌면 광동로 52번길 7"},
                                "keyText":"주소",
                                "confidenceScore":0.94222,
                                "boundingPolys":[{"vertices":[{"x":139.0,"y":85.0},{"x":185.0,"y":85.0},{"x":185.0,"y":104.0},{"x":139.0,"y":104.0}]},{"vertices":[{"x":188.0,"y":85.0},{"x":234.0,"y":84.0},{"x":234.0,"y":104.0},{"x":188.0,"y":104.0}]},{"vertices":[{"x":237.0,"y":82.0},{"x":285.0,"y":81.0},{"x":286.0,"y":103.0},{"x":237.0,"y":104.0}]},{"vertices":[{"x":288.0,"y":80.0},{"x":326.0,"y":77.0},{"x":328.0,"y":100.0},{"x":290.0,"y":102.0}]},{"vertices":[{"x":339.0,"y":76.0},{"x":390.0,"y":73.0},{"x":392.0,"y":96.0},{"x":341.0,"y":99.0}]},{"vertices":[{"x":395.0,"y":76.0},{"x":406.0,"y":76.0},{"x":406.0,"y":91.0},{"x":395.0,"y":91.0}]}],
                                "maskingPolys":[]
                            }
                        ],
                        "tel":[
                            {
                                "text":"031-768-7707",
                                "formatted":{"value":"0317687707"},
                                "keyText":"전화",
                                "confidenceScore":0.91801,
                                "boundingPolys":[
                                    {
                                        "vertices":[
                                            {"x":303.0,"y":100.0},{"x":399.0,"y":95.0},{"x":400.0,"y":112.0},{"x":303.0,"y":117.0}
                                        ]
                                    }
                                ]
                            }
                        ]
                    },
                    
                    ...