코드 기반 테스트 자동화

POM을 넘어서: Screenplay 패턴으로 확장하는 테스트 자동화

snatchdream 2025. 4. 4. 10:44

Screenplay 패턴이란? (기본 개념 및 구조)

Screenplay 패턴은 사용자  관점에서 테스트 코드를 구성하는
액터(Actor) 중심의 설계 패턴입니다.

전통적인 Page Object 모델(POM)이
웹 페이지를 클래스와 메서드로 표현하는 페이지 중심 접근이라면,
Screenplay 패턴은 배우(Actor)가 행동한다”는 스토리텔링 개념을 도입합니다​

 

주요 요소는 다음과 같습니다:

    • Actor (배우):
      테스트에서 행동하는 주체로, 사람이나 사용자를 모사합니다.
      예를 들어 고객, 관리자 등이 Actor가 될 수 있습니다.
      Actor는 행동에 필요한 능력(Ability)을 부여받아 사용합니다​.
    • Ability (능력):
      Actor가 사용할 수 있는 기능이나 도구를 의미합니다.
      예를 들어 웹 브라우저 사용 능력(예: Selenium WebDriver),
      API 호출 능력 등이 있습니다.

      Selenium으로 웹을 테스트할 경우 Actor에게
      “웹 브라우저 다루기” 능력을 부여해 WebDriver를 사용할 수 있게 합니다​.
    • Task (작업):
      Actor가 수행하는 고수준 행위입니다.
      하나의 Task는 하나의 사용 시나리오 단계를 나타내며,
      여러 Action으로 구성될 수도 있습니다.


      예를 들어 “상품 검색하기”, “장바구니에 상품 추가” 등이 Task가 될 수 있습니다.

      Task는 값을 반환하지 않고 행동만 수행합니다​.
    • Action/Interaction (세부 동작):
      Task를 구성하는 세부 동작으로,실제로 브라우저에서 하는
      구체적인 조작입니다.


      클릭하기
      , 텍스트 입력하기 등으로,
      Screenplay 패턴에서는
      이런 동작들을 재사용 가능한 액션 클래스로 구현합니다​.

      작은 Action들을 조합하여 상위 Task를 만들 수 있습니다​

    • Question (질의):
      Actor가 시스템의 상태를 확인하는 행위입니다.
      값을 반환하는 상호작용으로, 예를 들어 “페이지 제목 가져오기”,
      “장바구니에 담긴 상품 개수 확인” 등이 Question이 될 수 있습니다.

      Actor는 Question을 통해 애플리케이션의 상태를 조회하고,

      이를 검증(assertion)에 활용합니다​.

 

이러한 구성 요소를 조합하면 마치 시나리오 속 등장인물이
행동하듯이 테스트를 작성할 수 있습니다.

Screenplay 패턴의 코드 흐름은 자연어에 가깝고 읽기 쉽게 만들어주는데,

예를 들어:

# Actor 생성 및 능력 부여 (예: 웹 브라우저 제어 능력)
actor = Actor("사용자")
actor.can_use(browser=webdriver.Chrome())  # Chrome WebDriver 능력 부여

# Actor가 일련의 Task를 수행하는 모습
actor.attempts_to(OpenHomePage(), SearchProduct("노트북"), AddToCart("노트북 모델명"))
result = actor.asks_for(CartItemCount())  # Question을 통해 장바구니 아이템 수 조회
assert result == 1  # 검증: 장바구니에 상품이 1개 있어야 함


위 예시에서 Actor는 Selenium WebDriver를
사용할 수 있는 능력을 갖추었고,

attempts_to 메서드를 통해 여러 Task를 순차적으로 수행합니다.

마지막에 asks_for를 이용해 Question을 질의하고,
그 결과로 반환된 값을 통해 테스트 검증을 합니다.

이처럼 Screenplay 패턴은
행위 주체(Actor)와 행동(Task/Action), 질의(Question)를

명확히 구분함으로써
테스트 코드를 사람의 시나리오 서술처럼 표현합니다


Page Object 패턴 vs Screenplay 패턴 (차이점 비교)

Page Object 모델(POM)과 Screenplay 패턴은
테스트 자동화 설계 철학에서 큰 차이를 보입니다.

 

아래 표는 두 패턴의 주요 차이를 정리한 것입니다:

클릭해서 확대가 가능합니다.

▲ 표: Page Object Model과 Screenplay 패턴 비교


위 비교처럼, Screenplay 패턴은 Page Object 패턴의 단점을 보완하고
객체지향 설계 원칙(SOLID)을 적극 적용한 대안입니다​

 

특히 Screenplay는 구성 요소별 단일 책임을 강조하여,
Page Object의 비대화와 취약성을 개선합니다.

Screenplay 도입으로 Page Object 클래스는
locator와 URL 등 최소한의 역할만 갖게 되고,
Actor와 Task들이 실제 상호작용을 담당하므로 구성의 유연성이 높아집니다​

 

하지만 학습 난이도와 초기 구현 부담은 고려해야 합니다.
협업하는 동료가 Screenplay 패턴에 익숙하지 않다면
오히려 복잡하게 느껴질 수 있으므로,

프로젝트 규모작업자 역량에 따라 두 패턴 중 선택하는 것이 좋습니다​

 

예를 들어, 빠른 프로토타입이나 소규모 프로젝트에서는
익숙한 POM으로도 충분하며,
장기적으로 확장될 큰 프로젝트나
복잡한 도메인
(예: e커머스 플랫폼)에는 Screenplay 패턴이 유용합니다.


Python 활용 예제: e커머스 시나리오 테스트

이제 Python과 Selenium을 활용한 e커머스 웹사이트 테스트
Screenplay 패턴으로 구현하는 예제를 살펴보겠습니다.

시나리오는 “사용자가 상품을 검색하여 장바구니에 담고,
장바구니에 해당 상품이 있는지 확인”하는 흐름입니다.

 

먼저 페이지 요소를 정의하는 클래스들을 작성합니다.

Screenplay에서는 페이지 객체가 방대한 메서드를 갖지 않도록,
각 페이지별 요소 식별자(locator)와 필요한 상수만 클래스 변수로 선언합니다
(UI 구조를 모델링할 뿐, 행동 로직은 포함하지 않음)​

# 페이지 요소 정의 (Page Object 역할 최소화)
from selenium.webdriver.common.by import By

class HomePage:
    URL = "https://example-ecommerce.com"
    SEARCH_BOX = (By.CSS_SELECTOR, "input.search-field")       # 검색어 입력창
    SEARCH_BUTTON = (By.CSS_SELECTOR, "button.search-submit")  # 검색 버튼

class ResultsPage:
    FIRST_RESULT = (By.CSS_SELECTOR, ".product-list .item a")  # 첫 번째 상품 링크

class ProductPage:
    ADD_TO_CART = (By.ID, "btn-add-cart")                      # '장바구니에 담기' 버튼

class CartPage:
    CART_ITEMS = (By.CSS_SELECTOR, ".cart-items .item")        # 장바구니 상품 리스트 아이템
 

각 페이지 클래스는 해당 페이지의
URL이나 요소 Locator만을 포함하고 있으며,

동작 메서드는 전혀 없습니다.

이제 Screenplay 패턴의 핵심인
TaskQuestion 클래스를 구현합니다.


Task와 Question은 Actor가 수행할
구체적인 동작을 캡슐화한 것으로, 재사용 가능하게 설계됩니다:

from screenplay.pattern import Actor  # Screenplay 패턴 지원 라이브러리 (가정)
from selenium.webdriver.common.keys import Keys

# Task: 홈페이지 열기
class OpenHomePage(Task):
    def perform_as(self, actor: Actor):
        browser = actor.using("browser")                  # Actor가 보유한 browser 능력(WebDriver) 사용
        browser.get(HomePage.URL)                         # 홈페이지 열기

# Task: 상품 검색하기
class SearchProduct(Task):
    def __init__(self, keyword: str):
        self.keyword = keyword
    def perform_as(self, actor: Actor):
        browser = actor.using("browser")
        search_box = browser.find_element(*HomePage.SEARCH_BOX)
        search_box.send_keys(self.keyword)
        browser.find_element(*HomePage.SEARCH_BUTTON).click()  # 검색 수행

# Task: 검색 결과에서 첫 상품 선택하기
class SelectFirstResult(Task):
    def perform_as(self, actor: Actor):
        browser = actor.using("browser")
        first_item = browser.find_element(*ResultsPage.FIRST_RESULT)
        first_item.click()                                    # 첫 번째 상품 페이지로 이동

# Task: 상품을 장바구니에 추가하기
class AddToCart(Task):
    def perform_as(self, actor: Actor):
        browser = actor.using("browser")
        browser.find_element(*ProductPage.ADD_TO_CART).click() # 상품 페이지에서 '장바구니 담기' 클릭

# Question: 장바구니에 담긴 상품 개수 묻기
class CartItemCount(Question[int]):
    def request_as(self, actor: Actor) -> int:
        browser = actor.using("browser")
        items = browser.find_elements(*CartPage.CART_ITEMS)
        return len(items)


위 코드에서
OpenHomePage, SearchProduct, SelectFirstResult, AddToCart는

Task 클래스로 정의되었습니다.

각 Task는 perform_as(self, actor) 메서드를 통해
해당 Actor와 상호작용하며,

Selenium WebDriver를 사용해 실제 브라우저 동작을 수행합니다.

예컨대 SearchProduct Task는 전달된 키워드로 검색창에
입력하고 검색 버튼을 누르는 역할을 합니다.

Task들은 반환값이 없으며, 필요한 경우
다음 단계의 Task나 Question이 이어서 실행됩니다​

 

CartItemCount는 Question 클래스로,
request_as(self, actor) 메서드에서

현재 장바구니에 담긴 상품 요소들을 찾아 개수를 반환합니다.

이런 Question은
나중에 Actor의 asks_for()를 통해 호출되며,
테스트 검증에 사용됩니다​

 

이제 실제 테스트 시나리오를 작성해보겠습니다.
Actor 생성Ability 설정,
그리고 Task/Question의 사용 흐름은 다음과 같습니다:

from selenium import webdriver
import pytest

# Pytest 픽스처로 WebDriver 초기화 (테스트 시작 시 브라우저 열기)
@pytest.fixture
def browser():
    driver = webdriver.Chrome()            # Chrome 브라우저 드라이버 생성
    yield driver
    driver.quit()                          # 테스트 종료 후 브라우저 종료

# Pytest 픽스처로 Actor 생성 및 Selenium 사용 능력 부여
@pytest.fixture
def actor(browser):
    actor = Actor("쇼핑 사용자")           # Actor 생성 (이름은 '쇼핑 사용자')
    actor.can_use(browser=browser)         # Selenium WebDriver 능력을 Actor에 부여
    return actor

# 테스트 함수: 상품 검색부터 장바구니 확인까지
def test_add_to_cart_flow(actor: Actor):
    # 1. 홈페이지 열기
    actor.attempts_to(OpenHomePage())
    # 2. 상품 검색
    actor.attempts_to(SearchProduct("노트북"))
    # 3. 검색 결과에서 첫 상품 클릭
    actor.attempts_to(SelectFirstResult())
    # 4. 상품 페이지에서 '장바구니 담기' 클릭
    actor.attempts_to(AddToCart())
    # 5. (검증) 장바구니에 상품이 추가되었는지 확인
    item_count = actor.asks_for(CartItemCount())
    assert item_count == 1, "장바구니에 상품이 정확히 1개 담겨야 합니다."

위 테스트 시나리오는 Screenplay 패턴의 문법을 그대로 따릅니다.

actor.can_use(browser=browser)를 통해
Actor에게 Selenium 브라우저 제어 능력을 부여한 후,

actor.attempts_to(...)로 일련의 Task들을 수행하게 합니다​

 

각 Task는 내부적으로 actor.using("browser")를 통해
WebDriver 객체에 접근하여 Selenium 명령을 실행합니다.

마지막으로 actor.asks_for(CartItemCount())로
Question을 질의하여 장바구니

아이템 개수를 얻고, assert로 기대 결과를 검증합니다.


이러한 구조를 통해 테스트 코드는
“사용자가 홈페이지를 열고, 상품을 검색해서,
첫 상품을 장바구니에 담고, 장바구니에 상품이 있는지 확인한다”

는 시나리오를 그대로 표현합니다.

코드의 의도가 명확하게 드러나며,
세부 Selenium 조작(예: find_element, click 등)은
Task/Question 내부로 캡슐화되어

테스트 본문은 시나리오 서술에 집중하게 됩니다​


Selenium WebDriver와의 통합 방식 (Screenplay 구조)

Screenplay 패턴을 Selenium과 함께 사용할 때는
WebDriver 관리와 Actor의 연결이 핵심입니다.

일반적인 통합 방식은 다음과 같습니다:

  1. WebDriver 초기화
    테스트 시작 시 Selenium WebDriver (예: ChromeDriver)를 생성합니다.

    Pytest를 사용한다면 @pytest.fixture로 WebDriver 객체를 설정할 수 있습니다.

  2. Actor 생성 및 능력 부여:
    Actor를 만들고, WebDriver를 다룰 수 있는

    Ability를 Actor에게 추가합니다.

    Serenity BDD(Java)에서는 BrowseTheWeb.with(driver)와 같이

    Actor에 능력을 부여하는데, Python에서도 유사하게
    actor.can_use(browser=driver) 형태로
    Actor가
    WebDriver를 활용하도록 설정합니다​.


    이렇게 하면 Actor는 내부적으로 WebDriver 인스턴스를 가지고 있게 되며,

    Screenplay의 Task/Action들은 Actor로부터 이 browser 능력
    꺼내어(actor.using("browser"))
    Selenium 동작을 수행합니다​

  3. 페이지 요소 모델링:
    앞서 예제의 HomePage, ResultsPage처럼
    페이지별 locator를 정의합니다.
    이는 선택사항이지만, UI 요소 정의를 중앙 관리함으로써 locator 변경 시
    한 곳만 수정하면 되는 이점이 있습니다​.

    Screenplay 패턴에서는 이러한 페이지 클래스에
    상호작용 메서드는 넣지 않음에 유의합니다​

  4. Task/Action 구현:
    테스트에서 자주 쓰이는 동작들을 Task나 Action 클래스로 구현합니다.

    Selenium의 find_element, click 등의 호출을 해당 클래스의
    perform_as 메서드 내에서 실행하도록 합니다.


    예를 들어 Click 같은 간단한 Action부터 Login, AddItemToCart 같은
    복합적인 Task까지 필요에 따라 구현합니다.

    이때 여러 페이지에 걸친 시나리오도 하나의 Task로 만들 수 있는데,

    예를 들어 로그인 Task는 로그인 페이지와 홈 페이지를 모두 거칠 수 있습니다.


  5. Scenario 작성 (Actor 사용):
    실제 테스트에서는 Actor 객체를 사용하여 actor.attempts_to()로
    시나리오 흐름대로 Task들을 호출합니다.
    그런 다음 actor.should(see_that(...)) 또는 actor.asks_for()를 통해
    결과를 조회하고 검증합니다.

    BDD 스타일을 원한다면 Given/When/Then 메서드를 활용해
    가독성을 높일 수도 있습니다 (Serenity Python 구현체가 있다면)​.

 

통합 구조의 핵심은 Actor ↔ WebDriver 연결입니다.

Actor가 WebDriver를 능력으로 소유하기 때문에,
테스트 어디에서든 Actor만 있으면 브라우저 조작이 가능합니다.

특히 Screenplay 패턴은 멀티스레드나 병렬 시나리오에서도
독립적인 Actor로 각각 별도의 브라우저를 제어할 수 있어,
여러 사용자 시나리오를 동시 모사하기에도 적합합니다​

 


예를 들어 두 명의 Actor를 생성하고
각자 다른 능력(브라우저 세션)을 주면,

하나의 테스트에서
두 명의 사용자가 상호작용하는 시나리오
(e.g. 채팅 또는 거래)도 구현할 수 있습니다.


또한, 테스트가 종료될 때는
Actor가 가진 WebDriver 세션을
적절히 종료(driver.quit())하여
자원을 해제하는 것을 잊지 않아야 합니다.


Pytest라면 위 예제처럼 fixture의 yield 뒤에 quit을 호출하거나,
테스트 프레임워크의 teardown 단계에 WebDriver 종료를 넣으면 됩니다.


Screenplay 패턴의 장단점

마지막으로, Screenplay 패턴을 실무에서 활용할 때의
장점과 단점
을 정리합니다.

특히 복잡한 시나리오가 많은 e커머스 분야를 예로 들어 설명합니다.

Screenplay 패턴의 장점

    • 유지보수성과 확장성:
      Screenplay는 SOLID 원칙 중
      SRP(단일 책임)와 OCP(개방-폐쇄)를
      잘 지키도록 유도하여, 테스트 코드가 커져도 구조가 견고합니다​​


      한번 작성한 Task나 Action은 요구사항이 변하지 않는 한
      거의 수정하지 않고도 재사용되며,
      새로운 시나리오를 추가할 때도
      기존 코드를 건드릴 일이 적습니다​


      예를 들어 “쿠폰 적용 후 결제하기” 같은
      새로운 시나리오를 추가해도, 기존 “상품 검색”, “장바구니 담기”
      Task를 조합하면 되므로 빠르게 테스트를 작성할 수 있습니다.
    • 가독성 (비즈니스 도메인 표현력):
      테스트 코드가 사용자 행동의 서술에 가깝게 표현됩니다.
      이는 코드 리뷰나 협업 시 큰 이점인데, 비개발자도 시나리오를 이해하기 쉽습니다​.

      e커머스 예를 들면, *“사용자가 로그인하고 상품을 검색해서 장바구니에 담는다”*는
      시나리오가 코드에도 user.attempts_to(Login(), Search(...), AddToCart(...))
      형태로 드러나 이해하기 쉽습니다.

      QA와 개발자 간에 공통 언어로 소통하기에 용이하며,
      BDD의 시나리오 문서와 테스트 구현 간 격차도 줄어듭니다.
    • 재사용을 통한 생산성 향상:
      공통되는 사용자 행동을 Task/Action으로 만들어 두면,
      이후 테스트 작성 시 조합하는 것만으로 시나리오를
      구축할 수 있어 속도가 빨라집니다​.

      예를 들어 로그인, 로그아웃, 결제하기 등의 시나리오 조각을 라이브러리화해두면,
      새로운 테스트는 이들 퍼즐을 맞추는 작업으로 완료됩니다.
      이는 팀 전체의 테스트 자동화 생산성을 끌어올립니다.
    • 멀티-유저 시나리오 대응:
      Actor 개념 덕분에 여러 사용자가 등장하는 복잡한 시나리오를
      자연스럽게 구현할 수 있습니다.

      예를 들어 판매자와 구매자의 상호작용이 모두 필요한
      주문 처리 시나리오도, 두 Actor를 생성해 각자의 Task를 수행하게
      함으로써 한 테스트 흐름에서 표현할 수 있습니다.

      서로 다른 브라우저 세션이나 권한으로 동작하는 것도
      Actor의 능력 조합으로 처리가 가능하므로,
      시나리오 확장에 유연합니다​.

 

Screenplay 패턴의 단점 (주의사항)

    • 초기 학습과 설계 부담:
      Screenplay 패턴을 처음 접하는 팀이라면 러닝 커브가 있습니다​.
      Actor, Task, Question 등 개념을 이해하고
      코드 구조를 잡는 데에 초기 투자가 필요합니다.

      소규모 팀이나 자동화 경험이 적은 조직에서는 자칫 복잡하게 느껴질 수 있습니다.
      또한 잘못 구현하면 오히려 코드 구조가 산만해질 위험도 있습니다.

      따라서 팀 내 핵심 구성원들이 패턴을 충분히 습득하고
      가이드라인을 정한 후 도입하는 것이 좋습니다
      (예: 코드 템플릿, 네이밍 규칙 공유 등).
  • 과도한 추상화 가능성:
    패턴에 익숙해지면 모든 것을 세분화하고
    추상화하고픈 유혹이 있습니다.

    하지만 지나치게 미세한 단위로 Task/Action을 나누면
    오히려 코드량이 늘고 이해가 어려워질 수 있습니다.

    적절한 추상화 수준
    을 유지하는 것이 중요합니다.
    예를 들어, 너무 단순한 클릭 하나까지
    별도 Action 클래스로 만들기보다는 특정 맥락에서만 쓰이는 클릭은
    해당 Task 내에서 구현하는 식의 균형 잡기가 필요합니다.

  • 디버깅의 복잡성:
    Screenplay는 다층 구조로 이루어지다 보니,
    문제가 발생했을 때 추적이 번거로울 수 있습니다.

    어느 Task에서 실패했는지 로그를 잘 남기도록 설계해야 합니다.
    다행히 Screenplay 패턴을 지원하는 프레임워크나 라이브러리들은
    액터의 행동을 로그로 기록하는 기능이 있어 도움이 됩니다.

    예컨대 Serenity BDD의 경우 Actor의 모든 시도(attempt)와 질문(ask)이
    로그나 리포트에 남아 디버깅에 활용됩니다.

    Python 환경에서도 Actor에 로거(logger)를 설정해두면
    유사한 효과를 얻을 수 있습니다​.

 

Screenplay 패턴은 초기에 투자해야 할 학습과 구현 작업이 있지만,
복잡한 시나리오가 많은 테스트 자동화에선 장기적으로 큰 이득을 줄것으로 보입니다.

 

특히 다양한 사용자 흐름을 다루는 e커머스 테스트에서는 Screenplay 패턴으로
체계를 잡으면 테스트 케이스 추가나 변경 시에 변화에 강한 구조를 유지할 수 있습니다.
팀원들이 역할별로 Task를 개발하고 공유함으로써 협업 효율도 높아집니다.


마지막으로, 패턴은 도구일 뿐이므로 상황에 맞게 응용하는 지혜가 필요합니다.
프로젝트의 성격과 팀의 숙련도를 고려하여 Screenplay 패턴을 도입하고,
필요하면 Page Object 모델과 혼용하는 것도 현실적인 전략일 수 있습니다.

목표는 테스트 코드의 가독성과 유지보수성 향상이므로,
이를 위해 Screenplay의 원칙을 얼마나 적용할지 유연하게 결정하면 됩니다.
기존에 POM에 익숙한 QA 엔지니어들도 Screenplay 패턴의 이점을 경험하면
“페이지가 아니라 사용자의 경험 동선”
을 코드로 옮기는 새로운 관점에 익숙해질 것으로 보입니다.


참고 내용 :

Screenplay 패턴은

John Ferguson Smart 등이 Serenity BDD 프레임워크를 통해
대중화에 많은 영향력을 끼쳤습니다.



오늘은 Screenplay 패턴에 대해 알아보았습니다.

저는 현재 현업에서 POM(Page Object Model)을 기반으로 한
테스트 자동화 아키텍처를 활용하고 있습니다.

최근 리서치 차원에서 Screenplay 패턴을 학습했는데,
POM 디자인 패턴만으로는 명확하게 구현하기 어려웠던 복잡한 상호작용 및
엣지 케이스 처리에 많은 도움이 될 것으로 기대하고 있습니다.


이 글이 테스트 자동화 개선에 고민하시는

모든 분들에게 작은 도움이나마 되었으면 좋겠습니다.

 

그럼 오늘 하루도 좋은 하루되세요 :)