티스토리 뷰

학교 공지사항 알림 어플, 롯데시네마 클론 코딩, 공연 동행 구인 웹 서비스의 백엔드로 참여하며
크롤링 및 Open API 호출을 해보았습니다.

각각의 프로젝트에서 어떤 방법을 사용했는지

그리고 적용한 HTTP 통신 방법의 장단점에 대해 정리해보고자 합니다✏️

 


1. URLConnection

특징

  • Java 자체적으로 HTTP 통신을 진행하는 클래스
  • URLConnection은 JDK에 내장된 클래스이므로 추가 라이브러리 설치가 필요 없다.
  • 타임아웃 설정 불가능 , 쿠키 제어 불가능❌

 
 학교 홈페이지 공지사항 크롤링하면서, 간단한 HTTP 요청이라 외부 라이브러리 없이 URLConnection을 사용해보았습니다. 홈페이지 별로, html 구조가 달라서 공통 로직만 PknuCrawling 추상 클래스에 두고 상속받도록 구현했습니다.

public abstract class PknuCrawling {

    // ...

    public Connection getConnection(String pageUrl) {
        return Jsoup.connect(pageUrl)
                .header("Content-Type", "application/json;charset=UTF-8")
                .userAgent(USER_AGENT)
                .method(Connection.Method.GET)
                .ignoreContentType(true);
    }

    public Long getLatestNoticeNum() {
        return noticeRepository.findLatestPageNum();
    }

    public Notice saveNotice(String title, String content, String link, String image,
            Long noticeNum, Homepage homepage) {
        return noticeRepository.save(
                Notice.createWithPageNum(title, content, link, image, noticeNum, homepage));
    }

    public List<Keyword> keywordFiltering(List<Notice> notices) {
        List<Keyword> keywords = new ArrayList<>();
        for (Notice notice : notices) {
            keywords = filtering.filterKeywords(notice.getTitle(),
                    notice.getContent());
            noticeKeywordService.createNoticeKeyword(notice, keywords);
        }

        return keywords;
    }

    public abstract void crawling();
}

 

@Component
public class PknuCeCrawling extends PknuCrawling {

    // ...

    @Override
    @Scheduled(fixedDelay = 3600000)
    public void crawling() {
        try {
            Homepage pknuCeHomepage = findHomepage();
            List<Notice> newNotices = new ArrayList<>();
            if (homepageUrl.contains("https://")) {
                setSSL();
            }

            // 공지사항 리스트
            Elements notices = getConnection(homepageUrl).get()
                    .select(".a_bdCont .a_brdList tbody tr:not(.noti)");
            for (Element notice : notices) {
                // 번호, 제목, 링크 가져오기
                String link = homepageUrl + notice.select("td.bdlTitle a").attr("href");
                Long noticeNum = Long.valueOf(notice.select("td.bdlNum.noti").text());
                if (noticeNum <= getLatestNoticeNum()) {
                    continue;
                }

                // 상세페이지 내용 크롤링
                Document doc = getConnection(link).get();
                String title = notice.select("td.bdlTitle a").text();
                String text = doc.select("td.bdvEdit").text();
                String content = text.length() > 30 ? text.substring(0, 30) + "..." : text;

                // 이미지
                Element element = doc.select("td.bdvEdit").first();
                String image = element.select("img").attr("src");
                if (image.isEmpty()) {
                    image = noticeDefaultImage;
                }

                // 공지사항 저장
                newNotices.add(saveNotice(title, content, link, image, noticeNum, pknuCeHomepage));
            }

            // 공지사항에서 키워드 필터링 -> 키워드, 홈페이지 기반 알림
            List<Keyword> keywords = keywordFiltering(newNotices);
            sendKeywordNotification(keywords, newNotices);
            sendHomepageNotification(pknuCeHomepage, newNotices);
        } catch (Exception exception) {
            throw new RuntimeException("크롤링에 실패했습니다.");
        }
    }
}

 


2. Apache HttpClient

특징

  • Apache 기반으로 HTTP 통신을 진행하게 하는 클래스
  • 다양한 API를 제공하여 URLConnection을 사용하는 방식보다 코드가 간결하다.
  • 타임아웃 설정 가능⭕, 쿠키 제어 가능⭕
  • HttpURLConnection보다 무겁다.

 

📌HttpURLConnection vs HttpClient

HttpClient의 경우 HttpURLConnection과 비교하면 상대적으로 무겁기에,
성능이 우선시 된다면 HttpURLConnection을 사용하는 것이 좋고,
좀 더 간결한 코드와 다양한 API를 필요로 한다면 HttpClient를 사용하는 것이 좋지 않을까 싶습니다!

 
HttpURLConnection과 많이 비교되는 HttpClient를 이용하여 영화 오픈 API를 호출해보았습니다.
 
🎞️Kobis Open API
https://www.kobis.or.kr/kobisopenapi/homepg/main/main.do

 

영화진흥위원회 오픈API

OPEN API 서비스 영화진흥위원회 영화관입장권통합전산망에서 제공하는 오픈API 서비스로 더욱 풍요롭고 편안한 영화 서비스를 즐겨보세요.

www.kobis.or.kr

 

구현 내용

  • 현재 상영중인 영화 목록 반환 API 구현
    • 현재 시간 기준으로 상영 중인 영화 스케줄을 추출하여 현재 상영중인 영확 목록을 가져옴.
  • 영화 등록 API 구현
    • Top 10 영화 등록 API
    • 영화 코드를 통한 특정 영화 등록 API

 

전체적인 코드 흐름

1. HttpClient 설정
HttpClientBuilder를 사용하여 CloseableHttpClient 인스턴스를 생성합니다.

2. HTTP GET 요청 생성 및 전송
URIBuilder를 사용하여 URI를 생성하고 HTTP GET 요청을 전송하고 응답을 JSON 형식으로 받습니다.

3. Json 데이터 필터링을 통한 영화 정보 가져오기
 

@Component
public class KobisOpenApiUtil {

    private final MovieRepository movieRepository;
    private final CloseableHttpClient httpClient;
    @Value("${spring.kobis.url.base}")
    private String baseUrl;
    @Value("${spring.kobis.url.boxoffice-top10}")
    private String boxOfficeTop10Url;
    @Value("${spring.kobis.url.movie-info}")
    private String movieInfoUrl;
    @Value("${spring.kobis.key}")
    private String key;

    public KobisOpenApiUtil(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
        httpClient = HttpClientBuilder.create().build();
    }

    @Transactional
    public void saveBoxOfficeTop10ByTargetDate(String targetDate) {
        String boxOfficeTop10BaseUrl = baseUrl + boxOfficeTop10Url;
        try {
            // HTTP GET 요청 생성
            URI requestBoxOfficeTop10Uri = new URIBuilder(boxOfficeTop10BaseUrl)
                    .addParameter("key", key)
                    .addParameter("targetDt", targetDate)
                    .addParameter("weekGb", String.valueOf(0))
                    .build();

            // 요청 전송 및 응답 수신
            JsonNode rootNode = executeHttpRequestAndGetJsonResponse(requestBoxOfficeTop10Uri);

            // 데이터 필터링
            JsonNode weeklyBoxOfficeList = rootNode.path("boxOfficeResult")
                    .path("weeklyBoxOfficeList");
            for (JsonNode movieNode : weeklyBoxOfficeList) {
                Long audienceCount = movieNode.path("audiCnt").asLong();
                String code = movieNode.path("movieCd").asText();

                Movie movie = getMovieInfoByMovieCodeAndSave(code);
                movie.updateAudienceCount(audienceCount);
            }
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Movie getMovieInfoByMovieCodeAndSave(String movieCode) {
        String movieInfoBaseUrl = baseUrl + movieInfoUrl;
        Optional<Movie> movie = movieRepository.findByCode(Long.valueOf(movieCode));
        if (movie.isPresent()) {
            return movie.get();
        } else {
            try {
                // HTTP GET 요청 생성
                URI requestMovieInfoUri = new URIBuilder(movieInfoBaseUrl)
                        .addParameter("key", key)
                        .addParameter("movieCd", movieCode)
                        .build();

                // 요청 전송 및 응답 수신
                JsonNode rootNode = executeHttpRequestAndGetJsonResponse(requestMovieInfoUri);

                // 데이터 필터링
                Long code;
                String title, genre, director, nation;

                JsonNode movieInfoJsonNode = rootNode.path("movieInfoResult").path("movieInfo");
                code = movieInfoJsonNode.path("movieCd").asLong();
                title = movieInfoJsonNode.path("movieNm").asText();
                JsonNode directorsNode = movieInfoJsonNode.path("directors");
                director =
                        directorsNode.size() >= 1 ? directorsNode.get(0).path("peopleNm").asText()
                                : null;
                JsonNode genresNode = movieInfoJsonNode.path("genres");
                genre = genresNode.size() >= 1 ? genresNode.get(0).path("genreNm").asText() : null;
                JsonNode nationsNode = movieInfoJsonNode.path("nations");
                nation = nationsNode.size() >= 1 ? nationsNode.get(0).path("nationNm").asText()
                        : null;
                // TODO: 포스터 별도 크롤링 필요

                return movieRepository.save(Movie.builder()
                        .title(title)
                        .director(director)
                        .genre(genre)
                        .nation(nation)
                        .code(code)
                        .build());
            } catch (URISyntaxException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        throw new RuntimeException();
    }

    private JsonNode executeHttpRequestAndGetJsonResponse(URI uri) throws IOException {
        HttpGet httpGet = new HttpGet(uri);
        HttpResponse response = httpClient.execute(httpGet);
        BufferedReader reader = new BufferedReader(
                new InputStreamReader(response.getEntity().getContent()));
        StringBuilder result = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            result.append(line);
        }
        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode rootNode = objectMapper.readTree(result.toString());

        return rootNode;
    }
}

 


 

3. RestClient

 스프링 프레임워크는 RestTemplate, WebClient와 같은 HTTP 클라이언트를 지원하고 있습니다. 매번 스프링부트로 프로젝트를 진행해왔는데 활용해보지 않았더라구요..ㅎㅎ 공연 동행 구인 웹 서비스에서 같은 백엔드 팀원이 공연 Open API를 호출하며 Rest Client를 적용해주셨습니다👍

 RestClient는 RestTemplate과 WebClient를 보완하여 등장했습니다.

1️⃣ RestTemplate

Spring 3.0부터 지원하는 Spring의 HTTP 통신 템플릿으로, 
Blocking I/O 기반의 동기방식을 사용하여, 호출 후 응답을 받을 때까지 기다린다는 특징이 있습니다.
원하는 HTTP 클라이언트 라이브러리의 팩토리 클래스를 RestTemplate에 설정하여 사용할 수 있습니다.
(HttpURLConnection, Apache HttpComponents, OkHttp 3, Netty  등)
 

2️⃣ WebClient

스프링 5에서 도입된 비동기적이고 논블로킹 HTTP 클라이언트로,
빌더 패턴을 사용하여 직관적이고 유연하게 요청을 구성할 수 있습니다.
WebClient는 기본적으로 Reactor Netty를 사용하지만, ClientHttpConnector 인터페이스를 통해 다양한 HTTP 클라이언트 라이브러리로 유연하게 교체할 수 있습니다.

 

RestTemplate과 WebClient의 공통점과 차이점

공통점

  • 다양한 HTTP 메서드를 사용한 HTTP 요청 전송:
  • 스프링 프레임워크와 잘 통합된다.
  • 요청 헤더, URL 파라미터, 요청 본문 등을 설정할 수 있는 유연한 API를 제공
  • JSON, XML, 문자열 등 다양한 형식의 요청 및 응답을 처리 가능

차이점

동기 vs 비동기

  • RestTemplate: 동기식으로 작동, 요청을 보내면 응답이 올 때까지 현재 스레드를 블로킹합니다. 많은 수의 동시 요청을 처리하려면 더 많은 스레드가 필요하며, 이는 메모리와 리소스 소비가 증가하게 됩니다.
  • WebClient: 비동기식으로 작동, 요청을 보내고 응답을 기다리는 동안 현재 스레드를 블로킹하지 않고, 리액티브 타입(Mono, Flux)을 사용하여 응답을 처리합니다. 더 적은 수의 스레드로 더 많은 동시 요청을 효율적으로 처리할 수 있으며, 높은 확장성을 제공합니다.

사용 패턴

  • RestTemplate: 전통적인 객체 지향 방식의 API를 사용하여 HTTP 요청을 보냅니다. 주로 메서드 체이닝이 아닌 단순 호출 방식입니다.
  • WebClient: 빌더 패턴을 사용하여 직관적이고 유연하게 요청을 구성할 수 있습니다.

 

RestClient의 등장 배경

 RestTemplate은 수많은 메서드가 오버로딩되어 기능을 사용하는데 혼란을 줄 수 있으며, 비동기 호출이 불가능하다는 한계있습니다. 이후 등장한 WebClient는 동기식 처리와 비동기식 처리 모두 지원하며, 체이닝을 통한 fluent API를 제공하지만 Spring MVC 환경에서 사용하기 위해 WebFlux를 추가로 의존해야 한다는 단점이 있습니다. 이러한 단점들을 보완하여 Spring 6.1에서 RestClient 등장하였습니다.🥁
 

3️⃣ RestClient

 RestClient는 RestTemplate의 인프라와 함께 WebClient의 fluent API를 제공하여 코드의 가독성을 높입니다. 
 
🎫RestClient로 Kopis 오픈 API로부터 공연 데이터를 가져와보았습니다.
https://www.kopis.or.kr/por/main/main.do

 

공연예술통합전산망

예술경영지원센터 운영, 공연 예매 정보 집계 및 DB, 예매상황판, 공연통계 등 제공.

www.kopis.or.kr:443

 

 공연 Open API 호출 위한 restClient, httpInterface 설정 후, 비즈니스 로직을 구현해주셨습니다.
 

HttpInterface

 스프링의 HttpInterface는 HTTP 요청을 위한 서비스를 자바 인터페이스와 어노테이션으로 정의할 수 있도록 도와줍니다. 인터페이스에 KOPIS API와 통신하기 위한 HTTP 요청 메소드를 정의하면 됩니다. Spring의 HTTP 인터페이스 프록시 기능을 사용하여 구현됩니다.

RestClientConfig

KOPIS API와 통신하기 위한  RestClient를 설정하고 KopisHttpInterface의 구현체를 빈을 스프링 컨텍스트에 등록합니다.

 
👉Spring을 사용하여 KOPIS API와 쉽게 통신할 수 있도록 인터페이스와 클라이언트 구성을 설정하면 이제 HTTP 요청을 직접 작성하지 않고도 필요한 데이터를 쉽게 조회할 수 있습니다.
 

공연 Open API 호출 비즈니스 로직

공연 목록 조회, 공연 상세 조회를 구현해주면 됩니다.


 
 플젝하면서 사용해본 여러 HTTP 통신 방법의 코드를 다시 살펴보면서 각각의 특징들을 정리해보았습니다. 처음에는 무작정 URLConnection만 사용했었는데 이외에도 방법이 다양한 만큼, 플젝 상황에 맞는 방법을 선택해서 사용하는게 중요한 것 같습니다. 스프링을 사용한다면, 스프링 6.1에서 새롭게 등장한 RestClient를 적용해봐도 좋을 것 같습니다👍⭐