ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 셀레니움 내부구조 (Selenium Internals)
    SE General 2021. 3. 29. 21:48
    반응형

    이 글에서는 자동화 웹 테스트나 웹 스크래핑을 진행해보셨다면, 한 번쯤은 들어보셨을 셀레니움(Selenium)의 내부구조를 살펴보려 합니다. 

     

    먼저 넓은 범위의 아키텍쳐를 살펴보고, 실제 주요 로직이 어떻게 동작하는지 코드 레벨까지 살펴보도록 하겠습니다:

     

    • 셀레니움이란?
    • 셀레니움 아키텍쳐
    • 파이썬 Selenium 요청의 flow

     


     

    셀레니움이란?

    셀레니움의 시초는 2004년 Thought Works에서 Jason Huggins가 내부 도구로 개발한 JavaScriptTestRunner입니다. 그러한 JavaScriptTestRunner는 Selenium Core가 되어 현재의 Selenium을 이루고 있습니다.

     

    발전하던 Selenium에 남아있던 문제 중 한 가지는 로컬 환경에서의 "Same Origin Policy"였습니다. 이것을 극복하기 위해, Proxy를 중간에 두어 브라우져가 요청이 같은 도메인에서 발생하는 것으로 인식하도록 구현한 것이 바로 Selenium Remote Control (RC)입니다. 

     

    오픈소스 이후, 이어서 Selnium IDE, Grid, WebDriver가 추가되었습니다. Selenium 3를 기준으로 아래와 같은 컴포넌트로 이루어져 있습니다:

     

    • Selenium Core: 코어 라이브러리로 애플리케이션 자동화 기능을 제공합니다. 클라이언트는 다양한 언어(Java, JavaScript, C#, Python, Ruby, PHP 등)가 셀레니움을 사용할 수 있도록 기능을 제공합니다.
    • Selenium WebDriver / Remote Control: WebDriver는 RC보다 더 구조화되고 다양한 브라우저에서의 자동화된 테스트 기능을 제공합니다. Selenium 3에서는 RC가 완전히 제거되고 WebDriver로 대체되었습니다.
    • Selenium IDE: 프로그래머가 아닌 사람이 웹 애플리케이션을 테스트하고 다양한 Checks을 사용할 수 있는 GUI 도구입니다.
    • Selenium Grid: 그리드(허브와 노드) 형태로 parallel 테스트 실행 기능을 제공하여 수많은 테스트를 짧은 시간 안에 실행할 수 있도록 지원합니다. 

    파이썬으로 셀레니움을 사용하고 있다면, Python Selenium Client를 통해서 WebDriver를 제어하여 동작하게 됩니다. 아래에서는 상세한 아키텍쳐와 관련 코드를 살펴보겠습니다.

     

    셀레니움 아키텍쳐

    셀레니움 아키텍쳐의 주요 컴포넌트는 아래와 같이 4가지로 구성됩니다:

     

    Selenium Architecture - Image from Author inspired by [2]

     

    1. Selenium Client / Core 라이브러리

     

    2. HTTP 상의 JSOON wire protocol

    이것은 RC 또는 WebDriver를 통한 웹 애플리케이션 자동화를 가능하게 합니다. 셀레니움에서 JSON은 WebDriver 구현체를 연결하는 요소가 됩니다. WebDriver는 W3C 스탠다드 원격 컨트롤 인터페이스로 end 애플리케이션에 있는 에이전트와 웹 요소들을 컨트롤하도록 프로그램을 실행하는 것을 도와줍니다. JSON wire 프로토콜을 통해서 WebDriver는 플랫폼과 언어에 상관없는 간단한 모델의 프로토콜을 제공합니다. 

     

    셀레니움은 JSON을 통해 클라이언트와 서버 간에 정보를 주고 받습니다. JSON wire 프로토콜은 REST API를 사용하여 비동기적으로 통신합니다. 각 브라우저 드라이버는 HTTP 서버 구현체를 통해 셀레니움 클라이언트 라이브러리와의 요청 / 응답을 처리합니다. 그러한 요청 / 응답은 브라우저 드라이버를 통해 브라우저의 버튼 클릭, 텍스트 박스에 텍스트 삽입, 웹 페이지 컨텐츠 추출 등과 같은 액션을 발생시키고 값을 전달합니다. 

     

    3. Browser WebDrivers

    RC에는 적용되지 않지만, 웹 애플리케이션 자동화와 크로스 브라우저 테스팅의 기반이 됩니다. 위에서 언급된 것과 같이 end 브라우저와의 통신을 위해 사용되며, 브라우저 구현에 대한 부분을 encapsulate하게 합니다. 그렇기에 프로그래머는 브라우저와 관련한 부분을 상관하지 않고, 브라우저 개발자가 제공한 드라이버를 사용하여 통신할 수 있습니다. 

     

    셀레니움 오픈소스 커뮤니티는 다양한 브라우저에 대한 브라우저 드라이버를 관리하고 있습니다. 

     

    4. Browser

    라이브러리, 프로토콜, 드라이버를 통해 테스트 중인 웹 애플리케이션이 제어되는 곳입니다.

     

    파이썬 Selenium 요청의 flow

    이 부분에서는 일반적으로 사용하는 파이썬 Selenium 클라이언트 및 WebDriver에서의 요청이 내부적으로 어떻게 처리되는지 살펴보도록 하겠습니다. 살펴보니 파이썬으로 Selenium WebDriver까지 모두 구현되어 있었습니다. 

     

    driver = webdriver.Chrome('/Users/kadencho/chromedriver')
    
    try:
        driver.get('https://finance.naver.com/')
    
        trs = driver.find_elements_by_xpath("//tbody[@id='_topItems1']/tr")
    
    # ...

     

    위는 크롬 웹드라이버를 사용해 사이트에 접근에서 특정 element 객체를 얻는 코드의 일부입니다. 

     

    파이썬 클라이언트의 Chrome WebDriver는 다음과 같습니다:

     

    class WebDriver(ChromiumDriver):
    	# ...
        
        def __init__(self, executable_path="chromedriver", port=DEFAULT_PORT,
                     options: Options = None, service_args=None,
                     desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
                     chrome_options=None, service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
             # ...
             
    
    class ChromiumDriver(RemoteWebDriver): # 아래의 WebDriver as RemoteWebDriver
        # ...
        
        def __init__(self, browser_name, vendor_prefix,
                     port=DEFAULT_PORT, options: BaseOptions = None, service_args=None,
                     desired_capabilities=None, service_log_path=DEFAULT_SERVICE_LOG_PATH,
                     service: Service = None, keep_alive=DEFAULT_KEEP_ALIVE):
                     
             # ...
             
             
    class WebDriver(BaseWebDriver):
        # ...
        
        def find_element(self, by=By.ID, value=None) -> WebElement:
            # ...
            if by == By.ID:
                by = By.CSS_SELECTOR
                value = '[id="%s"]' % value
            elif by == By.TAG_NAME:
                by = By.CSS_SELECTOR
            elif by == By.CLASS_NAME:
                by = By.CSS_SELECTOR
                value = ".%s" % value
            elif by == By.NAME:
                by = By.CSS_SELECTOR
                value = '[name="%s"]' % value
    
            return self.execute(Command.FIND_ELEMENT, {
                'using': by,
                'value': value})['value']
                
        # ...
        
        def find_element_by_xpath(self, xpath) -> WebElement:
            # ...
    
            warnings.warn("find_element_by_* commands are deprecated. Please use find_element() instead")
            return self.find_element(by=By.XPATH, value=xpath)

     

    Reference

    [1] www.selenium.dev/

    [2] Science of Selenium: Master Web UI Automation and Create Your Own Test Automation Framework

    [3] github.com/SeleniumHQ/selenium

    [4] https://aosabook.org/en/v1/selenium.html

    반응형
Kaden Sungbin Cho