문제발생

이전 글에서 config 파일로 리소스 핸들러를 추가해 이미지 렌더링하는데에는 성공했다.

하지만 이전에 만들었던 SeleniumConfig 도 그런데, config 파일을 이렇게 사용하면 아래와 같은 문제가 있었다.

  • 보안에 민감한 값을 포함할 수 있다.
  • OS 별로 다른 변수를 이용해야할 수 있다.

 

문제해결

현재 프로젝트도 윈도우와 맥에서 사용하는 변수값이 달라서 고민이었고, 환경변수를 yml 파일로 분리하기로 했다.

방식은 이 글에서 썼던 @ConfigurationProperties 를 사용하는 방식을 채택했다.

과정을 요약하면 아래와 같다.

 

  1. yml 파일에 환경변수를 설정한다.
  2. @ConfigurationProperties 및 @ConstructorBinding 으로 yml 의 값을 바인딩한다.
  3. Application 클래스에 @EnableConfigurationProperties 로 해당 config 파일을 빈으로 활성화 한다.

1. yml 파일에 환경변수를 설정한다.

application.yml

selenium:
  driver-name: "driver/chromedriver.exe" 
  // 윈도우 기준임. 맥은 driver/chromedriver

resource:
  absolute-path: "file:///C:/Users/midcon/Desktop/my-molu-be/download/"
  // 윈도우 기준임. 맥은 "Users/midcon/Desktop/my-molu-be/download/"

 

2. @ConfigurationProperties 으로 yml 의 값을 바인딩한다.

변경전

WebConfig

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final static String ABS = "\\C:\\Users\\midcon\\Desktop\\my-molu-be\\download\\";

    private final CrawlingService crawlingService;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        Path imageAbsolutePath = Paths.get(crawlingService.DOWNLOAD_DIRECTORY).toAbsolutePath();
        // localhost:8080/images/image.jpg
        registry.addResourceHandler("/download/**")
            .addResourceLocations("file://" + ABS);
    }
}

SeleniumConfig

@Configuration
public class SeleniumConfig {

    public ChromeDriver chromeDriver() {
        System.setProperty("webdriver.chrome.driver", "chormedriver.exe");
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--remote-allow-origins=*");
        ChromeDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3));
        driver.manage().window().maximize();
        return driver;
    }
}

 

변경후

WebConfig

@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "resource")
public class WebConfig implements WebMvcConfigurer {

    private final String absolutePath;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // localhost:8080/images/image.jpg
        registry.addResourceHandler("/download/**")
            .addResourceLocations(absolutePath);
    }
}

SeleniumConfig

@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "selenium")
public class SeleniumConfig {

    private final String driverName;

    public ChromeDriver chromeDriver() {
        System.setProperty("webdriver.chrome.driver", driverName);
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--remote-allow-origins=*");
        ChromeDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3));
        driver.manage().window().maximize();
        return driver;
    }
}

 

 

3. @EnableConfigurationProperties 로 config 파일을 빈으로 등록한다.

MyMoluApplication

@EnableConfigurationProperties(value = {SeleniumConfig.class, WebConfig.class})
@SpringBootApplication
public class MyMoluApplication {

   public static void main(String[] args) {
      SpringApplication.run(MyMoluApplication.class, args);
   }
}

 

이제 윈도우에서와 맥에서 yml 파일 설정만 바꿔주면 나머지 파일들은 모두 깃허브로 관리할 수 있다.

이전처럼 SeleniumConfig 파일을 .gitignore 에 등록할 필요가 없어졌으니 제거하면 된다.

이처럼 민감한 값이나 개발환경에 따라 다른 값을 사용해야할 경우 yml 파일로 환경변수를 관리하면 된다.

문제발생

야심차게 셀레니움으로 크롤링 기능을 만들고 HTML, JS 로 페이지도 만들고 CORS 문제도 해결했다.

테스트로 크롤링이 잘 되는지도 확인했고, 이미지도 잘 저장했고 응답값도 확인했으니 이제 렌더링만 하면 될 줄 알았다.

하지만 어떤 이미지는 보이고, 어떤 이미지는 불러오지 못하는 현상이 발생했다.

 

문제해결

문제 원인을 찾기 위한 여러가지 시도 끝에 안보이던 이미지는 서버 내렸다 다시 올리면 뜨는걸 보았다.

그래서 빌드 이전에 저장된 이미지는 잘 보이는데 빌드 이후 저장한 이미지는 불러오지 못하는거란 생각이 들었다.

이를 통해 스프링 동작 원리를 간단히 찾아보았다.

  • 스프링에서 정적 리소스는 빌드 타임에 결정된다.
  • 이미지 파일은 기본적으로 resource/static 을 root 로 인식하고 불러온다.
  • static 폴더에 빌드 이후 런타임에 동적으로 추가된 리소스는 기본적으로 인식하지 않는다.

 

따라서 리소스 핸들러를 적용하여 static 폴더 외부에서 이미지를 불러오도록 설정해줘야한다.

아래처럼 WebMvcConfigurer 에 리소스 핸들러를 추가해주면 된다.

WebConfig

경로 변수들을 yml 로 따로 빼서 환경변수 설정을 하여 사용하면 더 좋다.

@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private final static String ABS = "\\C:\\Users\\midcon\\Desktop\\my-molu-be\\download\\";

    private final CrawlingService crawlingService;

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        Path imageAbsolutePath = Paths.get(crawlingService.DOWNLOAD_DIRECTORY).toAbsolutePath();
        // localhost:8080/images/image.jpg
        registry.addResourceHandler("/download/**")
            .addResourceLocations("file://" + ABS);
    }
}

 

 

문제발생

yml 파일로 환경변수를 분리하려고 했다.

@Configuration
@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "selenium")
public class SeleniumConfig {

    private final String driverName;

    public ChromeDriver chromeDriver() {
        System.setProperty("webdriver.chrome.driver", driverName);
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--remote-allow-origins=*");
        ChromeDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3));
        driver.manage().window().maximize();
        return driver;
    }
}

@ConfigurationProperties 로 yml 값을 불러오고 @ConstructorBinding 으로 값도 바인딩 했다.

이제 Application 클래스로 가서 @EnableConfigurationProperties 로 config 파일만 설정해주면 되겠지 싶었다.

하지만 Application 클래스의 설정을 해주니 아래와 같은 에러창이 나를 맞아주었다.

 

 

문제해결

@Configuration 때문에 발생한 에러였다.

 Application 클래스로의 @EnableConfigurationProperties config 파일의 빈 등록을 해주기 때문에

@Configuration 을 달면 위와 같은 오류가 발생하는 것이었다.

그러므로 아래처럼 @Configuration 을 빼주면 정상 동작한다.

@ConstructorBinding
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "selenium")
public class SeleniumConfig {

    private final String driverName;

    public ChromeDriver chromeDriver() {
        System.setProperty("webdriver.chrome.driver", driverName);
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--remote-allow-origins=*");
        ChromeDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3));
        driver.manage().window().maximize();
        return driver;
    }
}

 

 

문제발생

야심차게 백엔드 서버에서 크롤링 기능을 만들고 화면에 렌더링하려고 fetch 함수를 써서 데이터를 받아오려 했다.

그래서 백엔드 서버를 띄우고 localhost:8080 에 fetch 함수로 요청을 보냈다.

하지만 요청을 보내면 200으로 응답은 받는데 자꾸 응답 body가 빈값으로 오는 현상이 발생했다.

 

문제해결

원인은 CORS 때문이었다. CORS의 개념은 이 글을 참고하자.

해결 방법은 아래의 두가지가 생각이 났다.

  1. 백엔드 서버에서 origin 관련 설정을 설정해준다.
  2. 백엔드 서버 자체에서 HTML 을 렌더링해서 origin 이 애초에 같도록 한다.

스프링으로 HTML, JS 을 사용해본적이 없어서 경험해보고 싶었기 때문에 이 중 두번째 방법을 선택했다.

예상대로 origin이 같으니 응답 body에 데이터가 제대로 넘어왔다.

프론트를 만져보니 팀 프로젝트때는 대충 설정하고 넘겼던 CORS 라는 개념을 직접 사용해야해서

설정에 대해 더 공부하고, 이해하고 알아보는 좋은 기회가 됐다.

하지만 웹서버와 WAS 를 분리하는 시점이 오면 첫 번째도 해봐야 할 시점이 오겠지.

그땐 조금 더 많은 경험을 할 수 있을것 같다.

픽시브 짤 다운로더

referer 헤더의 벽을 넘지 못하고 픽시브 짤 다운로더로 노선을 변경했다.

그래도 이미지 url 을 이용해서 다운로드 하니까 이전에 했던게 헛된건 아니라고 할 수 있겠다.

기존의 이미지 url 긁어오는 로직에서 다운로드 정도만 추가했다.

대략적인 과정은 아래와 같다.

 

  1. 셀레니움을 이용하여 이미지 url을 크롤링 한다.
  2. 크롤링해온 이미지 url 을 이용하여 referer 헤더 설정 후 이미지 다운로드 한다.
  3. 픽시브 몰루 일러스트 탭의 1페이지 60장을 저장한다.
  4. 저장한 이미지 파일 이름을 리턴 값으로 반환한다.
  5. 이미지 저장은 try with resources 를 이용하므로 따로 스트림을 닫을 필요는 없다.

CrawlingService

테스트는 이미지 url 만 긁어오는것과 거의 달라진게 없으므로 따로 올리지는 않겠다.

이미지 다운로드 경로는 src/main/resources/static/download 로 했다.

@RequiredArgsConstructor
@Service
public class CrawlingService {

    // 짤 크롤링
    private static final String URL_ILLUSTRATION = "https://www.pixiv.net/tags/%E3%83%96%E3%83%AB%E3%83%BC%E3%82%A2%E3%83%BC%E3%82%AB%E3%82%A4%E3%83%96/illustrations";
    private static final String IMAGE_BOX_ILLUSTRATION = "#root > div.charcoal-token > div > div:nth-child(4) > div > div > div.sc-15n9ncy-0.jORshO > section > div.sc-l7cibp-0.juyBTC > div:nth-child(1) > ul > li";
    private static final String DOWNLOAD_PATH = "src/main/resources/static/download/";


    public List<String> downloadImages() {
        ChromeDriver driver = SeleniumConfig.chromeDriver();
        driver.get(URL_ILLUSTRATION);

        autoScroll(driver);

        List<WebElement> imageBox = driver.findElements(By.cssSelector(IMAGE_BOX_ILLUSTRATION));
        List<String> images = imageBox.stream()
                .map(o -> o.findElement(By.cssSelector("img.sc-rp5asc-10.erYaF")).getAttribute("src"))
                .map(o -> {
                    String fileName = o.substring(o.lastIndexOf('/') + 1);
                    downloadImageWithReferer(o, DOWNLOAD_PATH + fileName);
                    return fileName;
                })
                .collect(Collectors.toList());

        driver.quit();
        return images;
    }

    private static void autoScroll(ChromeDriver driver) {
        Long height = (Long) driver.executeScript("return document.body.scrollHeight");
        Long scroll = 500l;
        while (scroll <= height) {
            driver.executeScript("window.scrollTo(0," + scroll + ")");
            scroll += 500l;
        }
    }

    private static void downloadImageWithReferer(String imageUrl, String destinationFile) {
        try {
            URL url = new URL(imageUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            // Referer 헤더 설정
            connection.setRequestProperty("Referer", "https://www.pixiv.net/");

            File folder = new File(DOWNLOAD_PATH);
            if (!folder.exists()) {
                folder.mkdir();
            }
            try (InputStream in = connection.getInputStream();
                 FileOutputStream out = new FileOutputStream(destinationFile)) {
                byte[] buffer = new byte[1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer, 0, 1024)) != -1) {
                    out.write(buffer, 0, bytesRead);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 


다음 목표

다음 목표는 다운로드 받은 이미지를 렌더링하는 페이지 추가하는 것이다.

사실 이미지 다운로더로 노선 변경한것도 이미지 렌더링 페이지를 만들다가 난관에 봉착해서였다.

다시 원래 목표로 돌아가서 HTML, CSS, JS 도 써보면서 뭔가 보이는 페이지가 보이면 흥미도 더 붙고 좋을것 같다.

초기 계획

픽시브에서 주는 이미지url 을 긁어와서 나만의 몰루 사이트에 렌더링하고 싶었다.

픽시브 썸네일 이미지 url 60장을 긁어오기는 성공했고, 이제 렌더링만 하면 되겠지 생각했다.

하지만 렌더링 과정에서 문제가 생겼다.

 

픽시브의 nginx 설정으로 인한 문제에 봉착

픽시브에서는 nginx에서 지정된 referer 이외엔 리소스 접근을 차단하는 설정을 하고 있다.

내가 접근하려는 리소스는 이미지인데, 이미지 url 을 알아도 지정된 referer 설정 때문에 접근할 수 없는 것이다.

따라서 referer 헤더가 https://www.pixiv.net/가 아니면 nginx 에서 접근을 차단하여 아래처럼 403 에러가 뜬다.

 

사실 이건 처음 구상할때 알고 있었는데 분명 postman 으로 referer 헤더만 설정해두면 되길래 어려울거 없다고 생각했다.

하지만 이게 생각처럼 쉽게 해결되는 문제가 아니었다.

브라우저에서는 임의로 referer 를 설정할 수 없다.

 

이놈의 referer 헤더를 설정하려고 별짓을 다 해보고 챗GPT를 혹사시키기도 해봤지만 얻은 결론이었다.

결국 따로 이미지 저장할 필요 없이 픽시브에서 주는 이미지 주소를 날로 먹으려 했는데 노선을 변경해야할듯.

아무래도 이미지를 저장해서 렌더링하는 방향으로 잡아야 할것 같다.

스크롤 자동화

이전 글에서 목표로 삼았던 스크롤 자동화를 구현했다.

이제 수동으로 스크롤을 내리지 않아도 크롤링 메서드를 호출하면 알아서 긁어온다.

이전 글이랑 크게 다를건 없고 자동 스크롤만 추가됐다. 테스트도 동일한 코드.

과정은 간단하게 설명하면 다음과 같다.

 

  1. 셀레니움을 이용하여 크롬 드라이버를 동작시킨다. (SeleniumConfig에서 관련 설정 초기화)
  2. 크롬 드라이버에 픽시브 몰루짤 검색 url 을 입력한다.
  3. 셀레니움 기능으로 페이지 높이를 계산하는 자바 스크립트를 실행한다.
  4. 계산한 페이지 높이로 조금씩 스크롤을 내린다. (한번에 너무 많이 내리면 오류 생길듯)
  5. 픽시브 몰루짤 일러스트 탭이 뜨고 썸네일 이미지 url 만 긁어온다 (60장.)

CrawlingService

@RequiredArgsConstructor
@Service
public class CrawlingService {

    // 짤 크롤링
    private static final String URL_ILLUSTRATION = "https://www.pixiv.net/tags/%E3%83%96%E3%83%AB%E3%83%BC%E3%82%A2%E3%83%BC%E3%82%AB%E3%82%A4%E3%83%96/illustrations";
    private static final String IMAGE_BOX_ILLUSTRATION = "#root > div.charcoal-token > div > div:nth-child(4) > div > div > div.sc-15n9ncy-0.jORshO > section > div.sc-l7cibp-0.juyBTC > div:nth-child(1) > ul > li";

    public List<String> getImages() {
        ChromeDriver driver = SeleniumConfig.chromeDriver();
        driver.get(URL_ILLUSTRATION);

        // 자동 스크롤 부분
        Long height = (Long) driver.executeScript("return document.body.scrollHeight");
        Long scroll = 500l;
        while (scroll <= height) {
            driver.executeScript("window.scrollTo(0," + scroll + ")");
            scroll += 500l;
        }

        List<WebElement> imageBox = driver.findElements(By.cssSelector(IMAGE_BOX_ILLUSTRATION));
        List<String> images = imageBox.stream()
            .map(o -> o.findElement(By.cssSelector("img.sc-rp5asc-10.erYaF")).getAttribute("src"))
            .collect(Collectors.toList());

        driver.quit();
        return images;
    }
}

CrawlingServiceTest

@SpringBootTest
class CrawlingServiceTest {

    @Autowired
    private CrawlingService crawlingService;

    @DisplayName("픽시브 이미지를 크롤링하면 60장의 이미지 url을 긁어온다.")
    @Test
    void CrawlingServiceTest() throws InterruptedException {
        // given
        List<String> result = crawlingService.getImages();
        // expected
        assertThat(result.size()).isEqualTo(60);
    }

}

 


다음 목표

다음 목표는 60장의 짤을 렌더링하는것이다.

프론트엔드도 필요한만큼 공부해야 할거고, 각종 문제도 해결해야 할테니 갈 길이 멀겠지만 화이팅!

픽시브 짤 크롤링

셀레니움을 이용하여 픽시브 짤 크롤링 기능을 구현해보았다.

사실 크롤링이라 해야할지 스크래핑이라 해야할지 모르겠지만 아무튼...

이미지를 저장하는건 아니고 픽시브에서 일러스트 탭에서 블루아카이브 태그로 검색한 결과의 썸네일 리스트만 긁어온다.

픽시브는 동적 페이지라서 스크롤을 어느 정도까지 내려야 모든 이미지를 로딩한다.

하지만 이건 아직 미완성 버전이라 크롬 드라이버를 수동으로 스크롤해서 내려줘야 60장을 온전히 긁어온다.

크롤링 과정은 간단하게 설명하면 다음과 같다.

 

  1. 셀레니움을 이용하여 크롬 드라이버를 동작시킨다. (SeleniumConfig에서 관련 설정 초기화)
  2. 크롬 드라이버에 픽시브 몰루짤 검색 url 을 입력한다.
  3. 픽시브 몰루짤 일러스트 탭이 뜨고 썸네일 이미지 url 만 긁어온다 (60장.)

SeleniumConfig

윈도우 기준 config 이다. 맥은 조금 다르게 설정 해줘야 한다.

@Configuration
public class SeleniumConfig {

    public static ChromeDriver chromeDriver() {
        System.setProperty("webdriver.chrome.driver", "driver/chromedriver.exe");
        ChromeOptions options = new ChromeOptions();
        options.addArguments("--remote-allow-origins=*");
        ChromeDriver driver = new ChromeDriver(options);
        driver.manage().timeouts().implicitlyWait(Duration.ofMinutes(3));
        driver.manage().window().maximize();
        return driver;
    }
}

CrawlingService

@RequiredArgsConstructor
@Service
public class CrawlingService {

    // 짤 크롤링
    private static final String URL_ILLUSTRATION = "https://www.pixiv.net/tags/%E3%83%96%E3%83%AB%E3%83%BC%E3%82%A2%E3%83%BC%E3%82%AB%E3%82%A4%E3%83%96/illustrations";
    private static final String IMAGE_BOX_ILLUSTRATION = "#root > div.charcoal-token > div > div:nth-child(4) > div > div > div.sc-15n9ncy-0.jORshO > section > div.sc-l7cibp-0.juyBTC > div:nth-child(1) > ul > li";

    public List<String> getImages() {
        ChromeDriver driver = SeleniumConfig.chromeDriver();
        driver.get(URL_ILLUSTRATION);

        List<WebElement> imageBox = driver.findElements(By.cssSelector(IMAGE_BOX_ILLUSTRATION));
        List<String> images = imageBox.stream()
            .map(o -> o.findElement(By.cssSelector("img.sc-rp5asc-10.erYaF")).getAttribute("src"))
            .collect(Collectors.toList());

        driver.quit();
        return images;
    }
}

CrawlingServiceTest

테스트 코드는 간단하게 60장 다 긁어 오는지만 확인하는 테스트만 작성해봤다.

더 수정할 부분도 있을듯?

@SpringBootTest
class CrawlingServiceTest {

    @Autowired
    private CrawlingService crawlingService;

    @DisplayName("픽시브 이미지를 크롤링하면 60장의 이미지 url을 긁어온다.")
    @Test
    void CrawlingServiceTest() throws InterruptedException {
        // given
        List<String> result = crawlingService.getImages();
        // expected
        assertThat(result.size()).isEqualTo(60);
    }

}

 


다음 목표

다음 목표는 수동 스크롤할 필요 없이 자동으로 스크롤 하여 이미지를 긁어오는 기능을 구현하는것이다.

+ Recent posts