aioresponses에 대한 본격적인 내용은 서론 아래에 있습니다

 

목차

- @aioresponses() 데코레이터 사용

- Context Manager를 활용한 aioresponses()

- 동일 URL 반복 호출

- 특정 URL만 실제 api로의 요청을 허용하는 방법

 


 

 

 

파이썬 서버 테스트 과정에서 공부한 외부 api mock out 방법에 대해 기록하고자 한다

 

 

현 회사에서 최근 서버 개발을 위한 unit test를 작성하는 방법에 대해 새롭게 익히고 있다.

기존에는 postman을 통해 e2e test를 중심으로 개발하였는데, e2e test는 사용자 입장에서의 테스트이기 때문에 개발 디버깅용으로는 비효율적인 방식이었다. 

 

일반적으로 서버가 MVC 방식으로 모듈화 되어있다고 했을 때, 각 모듈이 어떻게 동작하는지를 먼저 테스트 해야한다.

이때 사용하는 것이 unit test(단위 테스트)다. 

파이썬 unit test 툴로는 가장 유명한 것이 Pytest와 Unitttest 두 가지가 있는데, 필자는 pytest를 중심으로 공부하고 있다.

 

 

최근 크롤링 등의 이유로 외부 api를 많이 호출하면서 단위 테스트 과정에서 문제가 되었다.

 

예를 들어 크롤링해야하는 웹페이지는 로그인을 해야지만 접근할 수 있다면 테스트를 할 때 authorization 헤더에 매번 올바른 토큰값을 넣어줘야 한다.

이를 위해 주기적으로 로그인을 한 후 토큰값을 가져와 헤더에 넣어주는 절차를 거쳐야하는데, 비용 측면에서 큰 낭비가 될 것이다.

 

이 때 사용할 수 있는 방법은 외부 페이지를 mocking 하는 것이다.

mock은 서로 의존성이 강한 객체가 있을 때 가짜 객체를 만들어 사용하는 것을 말한다

 

 

그렇다면 외부 api를 호출할 때 어떻게 mocking한 페이지를 가져올까? 이 때 사용하는 것이 aioresponses 라이브러리다

 

 

 

 


 

 

 

 

aioresponses는  aiohttp request를 테스트할 때 사용할 수 있는 helper다.

 

requests 모듈을 테스트할 때 사용하는 툴은 httpretty, responses, requests-mock 등 많이 제공되고 있지만 aiohttp와 같이 비동기 방식이 적용되면 테스트 난이도가 올라가게 된다. 이때 사용할 수 있는것이 aioresponses다

 

aioresponses는 aiohttp session을 통해 외부 api로 request를 보낼때, mocking한 가상의 response를 대신 받을 수 있도록 한다.

 

aioresponses를 사용하는 방법에 대해 알아보자

 

 

 


 

@aioresponses() 데코레이터 사용

 

다음 코드를 보자

import aiohttp
import asyncio
from aioresponses import aioresponses

TEST_URL = "https://test.co.kr"

@aioresponses()
async def test(mocked):
    # 가상의 응답 설정
    mocked.get(TEST_URL, status=200, payload=dict(test="Success"))
    
    # 외부 api 호출
    session = aiohttp.ClientSession()
    async with session.get(TEST_URL) as response:
        print(await response.json())
    await session.close()


if __name__ == '__main__':
    asyncio.run(test())

 

결과

{'test': 'Success'}

 

 

@aioresponses() 데코레이터를 단 함수는 첫번째 인자로 aioresponses 객체를 받는다.

이 객체를 사용하면 어떤 api 호출에 어떤 응답을 줄 것인지 지정할 수 있다.

 

mocked 변수에 받은 aioreponses 객체를 사용하는 코드를 살펴보자

 

mocked.get(TEST_URL, status=200, payload=dict(test="Success"))

 

aioresponses에는 get, post, put, delete 등 http method를 지원한다.

인자로는 호출하고자 하는 api url를 전달해야하며, status, payload, body 등 다양한 응답을 지정할 수 있다.

 

payload의 인자로는 dict, str, list 등을 사용할 수 있지만 body의 경우 str만 전달이 가능하다

만약 json을 응답으로 전달한다면 payload가 좋을 것이고, html을 전달한다면 body가 나은 선택일 수 있다.

 

이외에 headers, exception, status 등도 직접 설정이 가능하다. 

 

 

 


 

Context Manager를 활용한 aioresponses

 

위 코드에서 데코레이터를 활용한 방식을 소개했다면 이번에는 context manager를 사용한 방식을 소개하려 한다

 

여기서 context manager란 with 구문을 사용하여 특정 자원을 정확한 시간동안 사용하고 종료할 수 있도록 하는 것을 말한다.

즉, with 구문 아래에 들여쓰기가 되어있는 코드에만 할당한 자원을 사용할 수 있도록 하는 것을 말한다.

위 코드에서 이미 aiohttp session을 사용하는데 활용했기 때문에 여기서는 간단하게만 설명하였다

 

 

다음 코드를 살펴보자

 

TEST_URL = "https://test.co.kr"

async def test():
    # context manager를 사용한 aioresponses
    with aioresponses() as mocked:
        mocked.get(TEST_URL, status=200, payload=dict(test="Success"))

        session = aiohttp.ClientSession()
        async with session.get(TEST_URL) as response:
            print(await response.json())
        await session.close()

 

결과

{'test': 'Success'}

 

위 코드를 보면 aioresponses 메서드에서 해당 객체를 mocked 변수에 받아 사용하고 있다.

아래에 코드는 데코레이터를 사용한 코드와 동일하다.

 

이 방법은 외부 api를 사용하는 부분이 전체 코드의 일부분이고, 복잡한 코드로 인해 에러가 우려되거나 메모리 누수(resource leak) 우려가 있는 경우 사용할 수 있다.

데코레이터는 함수 전체가 끝날 때까지 aioresponses 객체를 계속 유지한다.

때문에 외부 api 호출이 주요 기능인 경우가 아니라면 위와 같이 context manager를 사용해서 일정 시간동안만 리소스를 사용하고 안전하게 닫는 것이 더 좋은 방법일 수 있다.

 

 

 


 

동일 URL 반복 호출

 

이렇게 설정한 가상의 응답은 기본적으로 1회만 사용이 가능하다. 

아래의 예시 코드를 보자

 

TEST_URL = "https://test.co.kr"

@aioresponses()
async def test(mocked):
    mocked.get(TEST_URL, status=404)	# 첫번째 응답
    mocked.get(TEST_URL, status=200)	# 두번째 응답

    session = aiohttp.ClientSession()
    async with session.get(TEST_URL) as response:
        print(response.status)
    async with session.get(TEST_URL) as response:
        print(response.status)
    await session.close()

 

결과

404
200

 

위 코드에서는 같은 url에 대해 mocking된 응답을 2개 만든 뒤 같은 url을 2번 호출하였다.

결과는 보다시피 첫번째 요청은 첫번째 응답을, 두번째 요청은 두번째 응답을 받았다.

 

만약 같은 url에 대해 다양한 응답을 받고 싶다면 위와 같은 방법을 사용하면 된다.

 

 

 

그렇다면 만약 같은 url을 여러 번 호출하고 계속 같은 응답을 받고 싶다면 어떻게 하면 될까?

이때 사용할 수 있는 파라미터가 repeat 이다. 

 

가령 url은 같지만 body를 바꿔가며 여러개의 응답을 전부 받아야하는 경우가 있을 수 있는데  repeat=False (default)로 되어있는 경우 Connection refused 에러가 발생하게 된다. 

내가 mocking 한 응답을 재사용한다는 의미로 repeat=True를 해주어야만 반복적인 요청이 가능하다.

 

예시 코드를 보자

 

TEST_URL = "https://test.co.kr"

@aioresponses()
async def test(mocked):
    mocked.get(TEST_URL, status=200, repeat=True)	# repeat 인자 설정

    session = aiohttp.ClientSession()
    async with session.get(TEST_URL) as response:
        print(response.status)
    async with session.get(TEST_URL) as response:
        print(response.status)
    await session.close()

 

결과

200
200

 

 

 


 

특정 URL만 실제 api로의 요청을 허용하는 방법

 

aioresponses는 외부 api를 호출하는 모든 요청을 통제하고 응답이 mocking된 URL 요청만 허용한다.

만약 mocking되지 않은 URL을 임의로 호출하면 aioresponses는 해당 요청을 차단하고 에러를 보낸다.

 

아래의 코드를 보자

 

REAL_URL = "https://real.url.com"

@aioresponses()
async def test(mocked):
    session = aiohttp.ClientSession()
    # mocking하지 않은 외부 api 호출
    async with session.get(REAL_URL) as response:	
        print(await response.json())
    await session.close()

 

결과

aiohttp.client_exceptions.ClientConnectionError: Connection refused: GET https://real.url.com

 

위와 같이 mocking되지 않은 api를 호출하면 해당 요청은 Connection refused 되었다는 에러메세지를 볼 수 있다.

 

 

경우에 따라, 일부 api는 mocking된 응답을 받고 일부는 외부에서 실제 데이터를 가져와야 할 수 있다.

그런 경우에 사용할 수 있는 것이 passthrough 파라미터다.

 

aioresponses 함수를 호출할 때, passthrought 파라미터에 특정 url 리스트를 인자로 전달하면 해당 url은 외부로 요청할 수 있도록 허용한다. 

 

아래의 코드를 보자

 

TEST_URL = "https://test.co.kr"
REAL_URL = "https://real.url.com"

# https://real.url.com은 예시를 위한 임의의 URL입니다.
@aioresponses(passthrough=[REAL_URL])
async def test(mocked):
    mocked.get(TEST_URL, status=200, payload=dict(test="Success"))

    session = aiohttp.ClientSession()
    # mocking된 요청
    async with session.get(TEST_URL) as response:
        print(await response.json())
        
    # 실제 외부 api로의 요청
    async with session.get(REAL_URL) as response:
        print(await response.json())
        
    await session.close()

 

결과

{'test': 'Success'}
{'real_url': 'Connected'}

 

위 코드는 mocking 된 TEST_URL과 외부로 실제 요청을 보낼 REAL_URL 두개에 각각 요청을 보내고 있다.

TEST_URL은 가상의 응답을 받아왔고, REAL_URL은 외부 서버에서 응답을 받아온 것을 확인할 수 있다.

 

 


 

 

본 포스트는 제가 공부하는 과정에서 작성하였습니다. 정정사항 댓글은 언제나 환영합니다!

반응형

윈도우를 사용하는 개발자들 중 wsl을 통해 리눅스를 설치해서 사용하는 경우가 있습니다.

간혹 윈도우에서 리눅스로 파일을 전달해야하는 경우가 있는데 2가지 방법을 쓸 수 있습니다.

 

첫째는 공유폴더를 만드는 방법이고

두번째는 scp(Secure copy protocol) 명령어를 이용하는 방법인데

 

개인적으로 공유폴더를 만드는 방법은 방화벽 설정과 유저 설정 등등 윈도우에서는 상당히 번거로웠습니다.

따라서 scp를 이용해 특정 파일 몇 개를 직접 전송하는 방법을 사용해보려 합니다.

 

참고 : https://baekh-93.tistory.com/50

 

필자는 윈도우 10과 리눅스 Ubuntu를 사용하였습니다.

 

 


 

 

우선 파일을 전송하기에 앞서 리눅스에서 몇가지 설정이 필요합니다.

 

ssh부터 설치해봅시다.

 

sudo apt-get install openssh-server

 

그리고 sshd-config에 두군데를 수정합니다.

에디터로 저는 vim을 사용하지만 nano 등 본인이 편하신 걸로 사용하시면 됩니다.

 

sudo vim /etc/ssh/sshd_config

 

기나긴 설정값들 사이에 두 부분을 다음과 같이 수정합니다.

 

#Port 22
-> Port 22
(주석을 해제합니다)
PasswordAuthentication no
-> PasswordAuthentication yes

 

첫 번째는 22번 포트를 여는 설정입니다. 이 부분을 수정해야 윈도우에서 22번 포트로 접근할 수 있습니다.

PasswordAuthentication은 리눅스 서버에 접속할 때 root 유저의 비밀번호를 사용하도록 허용하는 설정입니다.

기본값은 no이며 설정이 꺼져있으면 공개키를 사용해야합니다.

로컬에서만 돌릴 예정이므로 편하게 비밀번호를 사용합시다.

 

설정을 변경하였다면 저장합시다.

vim의 경우 :w! 을 먼저 입력해 readonly였던 파일을 수정 가능하게 변경해야합니다. !를 사용할 것이기 때문에 위에서 sudo를 사용한 것입니다.

 

설정을 변경했으니 ssh를 재실행합니다.

 

sudo /etc/init.d/ssh restart

 

22번 포트가 열렸는지 확인하고 싶다면 다음 명령어를 입력합니다.

 

netstat -ntl

 

위 사진을 확인하면 잘 열린 것을 볼 수 있습니다.

 

 

파일을 전송할 차례입니다. 윈도우 Powershell로 넘어옵시다.

명령어는 다음과 같습니다.

 

scp [전송할파일경로] [리눅스서버계정ID]@[리눅스서버계정IP]:[전송받을경로]

(예시)
scp .\test.txt root@10.0.0.1:\home\user01\share

 

여기서 확인해야할 사항은 서버계정 아이디와 IP입니다.

계정 아이디@IP는 우분투 터미널에 초록색 글자로 아이디@IP: 이렇게 쓰여있는걸 참고하셔도 됩니다.

 

@ 오른쪽 IP가 DESKTOP- 등으로 되어있다면 이를 입력했을 때 scp에서 접속이 어려운 경우가 있습니다.(저의 경우가 그랬네요)

 

이 경우 다음 명령어로 ip를 직접 확인해야합니다.

 

ifconfig

 

빨간색으로 가려준 부분에 적인 숫자가 ip입니다. 255.255.255.255 형식으로 적혀있습니다.

 

scp 명령어를 입력하고 password를 입력하면 전송이 완료됩니다.

만약 Are you sure you want to continue connecting (yes/no/[fingerprint])? 문구가 뜨면 yes를 입력해줍시다. 그 다음 password를 칠 수 있습니다.

명령어에 설정한 리눅스 해당 폴더에 가서 확인하시면 됩니다.

반응형

locust를 실행할 때 user 클래스에게 특정 값을 초기에 전달해야하는 경우가 있다.

예를 들면, 특정 도메인으로 요청을 보내고 싶을 때가 있고 아닐 때가 있는데

이 분기를 실행할 때 user 클래스로 True/False를 전달해서 결정할 수 있도록 하는 것이다.

 

 

이번 포스팅은 locust 실행 시 커스텀 인수를 전달하는 2가지 방법에 대해 정리하고자 한다.

 

  1. command line 명령어를 커스터마이징 하기
  2. 환경 변수 설정하기

 


 

 

1. command line 명령어 커스터마이징 하기

 

 

locust에는 디폴트로 설정된 커맨드라인 명령어가 있다. 여기에 내가 커스터마이징 한 명령어를 추가하는 방법을 사용한다

 

이 방법은 locust의 events 데코레이션을 활용하게 된다.

 

 

from locust import events

@events.init_command_line_parser.add_listener	#데코레이터 추가
def set_command_line(parser):
    #명령어 추가
    parser.add_argument("--test", type=bool, default=False, help="let's test customizing")

 

 

코드를 한 줄씩 살펴보면 먼저 events 데코레이션에서 커맨드라인 parser의 add_listener를 사용한다.

 

그리고 parser를 파라미터로 전달받아 argument를 추가한다.

 

add_argument 내부 파라미터는 여러가지가 있는데, 차례로 명령어, 입력타입, 디폴트값, 그리고 설명을 위 코드에 넣었다.

이외의 유용한 파라미터들은 공식문서를 통해 파악하면 된다.

 

위 함수는 user클래스 밖에 선언하면 된다.

 

재밌는 점은, 위와 같이 커맨드라인 명령어를 추가하고 locust -h 를 누르면 디폴트 명령어들과 함께 내가 추가한 명령어까지 콘솔에 나온다는 것이다. help는 이때 출력되는 명령어 설명에 들어가는 부분이다.

 

 

 

위와 같이 명령어를 추가했다면 user 클래스에서 사용해보자.

 

 

class TestUser(HttpUser):
    if self.environment.parsed_options.test == True:	#명령어 입력값 가져오기
    	self.client.get("/test")

 

 

사용은 간단하게 environment에 포함된 parsed_options에서 내가 추가한 명령어의 값을 가져오면 된다.

필자는 --test 라고 작성했으니 사용도 parsed_options.test로 사용하면 되는 것이다.

 

 

 

이제 이 명령어를 콘솔에서 사용해보자

 

 

locust --test True

 

 

 

 

2.  환경 변수 설정하기

 

따로 명령어를 추가하고 사용하는 것이 번거롭다면 단순히 환경변수를 사용하는 방법도 있다.

 

환경변수의 장점은 어떤 코드에서든 입력값에 접근할 수 있다는 점이다.

필자는 TaskSet에서 입력값을 가져와야 했는데 추가한 명령어로 접근하는 경우 user class에서 값을 가져와 TaskSet으로 전달하는 번거로운 과정을 거쳐야 했다. TaskSet은 environment에 접근할 수 없기 때문이다.

 

그러나 환경변수를 사용하면 TaskSet이든 일반 함수든 자유롭게 입력값에 접근할 수 있다.

 

 

환경변수는 따로 추가하거나 설정해줄 필요 없이 콘솔에서 다음과 같이 실행하면 된다.

 

 

<linux/mac>
TEST=True locust ...

<Window>
SET TEST=True
locust ...

 

 

운영체제에 따라 달라지긴 하지만 위처럼 간단하게 환경변수를 추가할 수 있다.

 

이렇게 추가한 변수를 사용해보자

 

 

import os
from locust import TaskSet, task

class TestTaskSet(TaskSet):
	
    @task
    def test_task(self):
    	if os.environ['TEST'] == "True":		# 환경변수 값 가져오기
        	self.client.get("/test")

 

 

사용은 os에서 환경변수를 가져오는 방식과 동일하다.

단, 환경변수는 타입이 str이므로 필요하면 타입을 변경하는 방식으로 사용하면 된다.

반응형

Locust로 테스트 시나리오를 작성하다보면 복잡한 코드를 구조적으로 구성해야하는 순간이 온다.

TaskSet은 테스트 코드를 계층적으로 구조화 할 때 유용하게 사용할 수 있다.

 

 


 

1. TaskSet

 

 

TaskSet은 User 클래스와 유사하게 사용할 수 있다.

단, User 클래스가 아니기 때문에 User 클래스 대신 사용하면 안된다.

 

 

from locust import HttpUser, task, TaskSet

class TaskSetTest(TaskSet):
    @task
    def test1(self):
    	self.client.get("/hello")
       
    @task(2)
    def test2(self):
    	self.client.get("/world")

 

 

위 코드를 보면 부모클래스로 HttpUser 대신 TaskSet을 상속받고 있다.

그 외에 나머지는 HttpUser를 사용하는 것과 동일하게 작성되어 있는걸 볼 수 있다.

 

이렇게 작성한 taskSet 클래스는 직접적으로 locust에서 사용할 수는 없다.

이 taskSet은 아래 User 클래스를 상속받은 클래스에서 사용할 것이다.

 

 

class UserClassTest(HttpUser):
    host = "https://localhost:8000"
    tasks = [TaskSetTest]

 

 

locust에서는 HttpUser를 상속받은 UserClassTest를 사용할 것이다.

 

미리 작성해둔 taskSet은 이 클래스에서 tasks = [] 형식으로 주입해 사용한다.

리스트 내부에 taskSet 클래스를 넣으면 클래스 내에 선언한 여러개의 task들이 리스트 안으로 들어간다.

그리고 locust를 실행하면 이 task들을 weight에 맞게 무작위로 선택하여 수행한다.

 

tasks 리스트 안에는 여러개의 TaskSet을 넣을 수 있다.

그리고 task를 추가하는 개념이기 때문에 아래에 UserClassTest만의 task를 만들 수 있다.

 

 

 

 

2. SequentialTaskSet

 

 

그렇다면 SequentialTaskSet은 무엇일까?

 

 SequentialTaskSet은 TaskSet에서 특징이 하나 추가되었다.

SequentialTaskSet을 부모클래스로 상속받고 task를 여러개 만들었다면, 위에서부터 순서대로 수행한다.

 

 

class SequentialTest(SequentialTaskSet):
    @task
    def test1(self):
    	self.client.get("/num1")
    
    @task
    def test2(self):
    	self.client.get("/num2")

    @task
    def test3(self):
    	self.client.get("/num3")

 

 

위와 같은 클래스가 있다고 했을 때, 이 클래스를 User 클래스에 넣어 수행하면

/num1 -> /num2 -> /num3 -> /num1 -> /num2 ...

이렇게 순서대로 수행하게 될 것이다. 

반응형

1.  Locust란?

 

" Python 스크립트 언어로 구현하는 서버 부하테스트 툴 "

 

즉, 내가 만든 서버에 수백, 수천의 동시사용자가 들어왔을 때 어떤 일이 벌어지는지를 테스트할 수 있는 툴이다.

파이썬 언어로 테스트 시나리오를 간편하게 작성할 수 있고, 결과를 Locust 웹으로 확인할 수 있다.

 

 

 

이 튜토리얼은 '서버 테스트'이기 때문에 서버가 이미 만들어져 있고 정상적으로 동작한다는 것을 가정합니다.

 

url로 접속할 것이기 때문에 서버 언어는 python이 아니어도 관계 없다.

필자의 서버는 localhost로 실행하였다

 

 


 

2.  설치

 

우선 파이썬 프로젝트를 하나 준비하자.

서버가 fastapi처럼 python3 언어를 사용한다면 서버 안에 설치해도 관계없다.

 

해당 파이썬 프로젝트 루트 폴더에서 다음 명령을 입력한다.

 

pip install locust

 

잘 설치가 되었는지 확인하자.

 

locust -V

locust 2.8.6

 

locust 버전 확인을 했을 때 위와 같이 잘 뜨면 설치가 완료된 것이다.

 

 

 

 


 

3.  locustfile.py 작성하기

 

 

locustfile.py는 locust를 실행했을 때 별다른 옵션을 주지 않으면 자동으로 실행시켜주는 파일이다.

우선, 다음과 같이 작성해보자

 

from locust import HttpUser, task

class ServerTest(HttpUser):
	@task
    def api_test(self):
    	self.client.get("/hello")
        self.client.get("/world")

 

이제 코드를 한 줄씩 살펴보자.

 

locust에서 알아야 할 가장 중요한 개념은 2개가 있다.

User class Task다.

 

 

class ServerTest(HttpUser):

 

User class는 한 명의 유저를 나타낸다.

Locust는 각각의 유저를 생성하기 위해 해당 클래스의 인스턴스를 생성한다.

즉, 유저가 만약 3명이라면 작성된 User class의 인스턴스를 3개 생성하게 되는 것이다.

이 클래스 안에는  request를 보내고, response를 받아 print를 하는 등의 테스트 스크립트가 작성된다.

 

HttpUser는  Http 프로토콜을 사용하는 User class로 가장 보편적으로 사용되는 클래스이다.

HttpUser를 내가 만든 클래스에서 상속받으면 클래스가 완성된다.

 

 

@task
def api_test(self):

 

Task는 유저가 실행하는 함수이다.

User class는 green thread(실제 스레드를 모방한 가짜 스레드)를 통해 실행된다.

task가 여러개 있을 때, 유저는 무작위로 task를 선택하여 실행하고 다음 task를 다시 선택해 실행하는 방식으로 테스트가 진행된다.

 

@task 는 task를 추가하는 가장 간단한 방법이다. 함수 위에 선언해주면 된다.

 

 

self.client.get("/hello")
self.client.get("/world")

 

client는 파이썬 라이브러리인 requests.Sessions의 서브클래스인 HttpSession의 인스턴스이다.

 HttpSession이기 때문에 Http의 모든 메서드를 사용할 수 있다.(get, post, delete...)

requests.Session과 동일하게 활용하면 된다.

url은 호스트 부분을 제외한 나머지 부분을 적어주면 된다. (http://localhost:8000/hello 라고 했을 때 /hello 만 작성한다.)

 

 


 

4.  실행하기

 

실행은 터미널에 locust라고만 작성해주면 된다.

(서버는 이미 실행중이어야 한다)

 

locust

 

locust를 실행하면 링크가 하나 뜨게된다.

"http://localhost:8089"로 접속하면 locust에서 제공하는 테스트 웹 인터페이스에 접속할 수 있다.

(localhost는 0.0.0.0과 같은 의미이다)

 

 

접속해보면 위와 같은 화면이 뜬다. 하나씩 살펴보자

 

Number of users는 총 동시사용자 수를 의미한다.

즉, 만약 내가 1000이라고 작성하면 테스트에는 총 1000명의 동시사용자가 서버에 접속하게 된다.

 

Spawn rate는 1초 당 추가로 접속할 사용자의 수이다.

만약 총 동시사용자가 1000명이고 초당 100명으로 설정했다면 1초에 100명, 2초에 200명... 이렇게 총 동시사용자수에 도달하기까지 10초가 걸릴 것이다.

 

Host는 내 서버의 host url이다. http://localhost:8000 과 같이 작성해주면 된다.

호스트를 잘못 작성하면 에러가 뜨니 제대로 작성했는지 확인하자.

 

 

 

접속한 후 화면을 살펴보자

 

 

아까 get으로  task에 작성한 url이 뜨는 것을 볼 수 있다.

Median, Average, Min 등은 각 요청에 대한 응답 중간값, 평균, 최솟값 등을 의미한다.

 

위에 툴바에 edit 버튼을 누르면 실행 중간에라도 총 동시사용자 수나 초당 사용자 수를 변경할 수 있다.

 

 

차트에서는 이렇게 테스트 현황을 실시간으로 확인할 수 있다.

이때, 사용자는 아까 설정한 초당 추가사용자수만큼 증가하고 있는 것이다. 

 

첫번째 차트는 RPS(current Requests Per Seconds)와 초당 Failures를 보여준다. 

두번째 차트는 Median Response Time과 95% percentile를 보여준다.

캡쳐하지는 않았지만 스크롤 아래에 세번째 차트도 있는데, 증가하는 유저수 추이를 보여준다.

 

탭에는 발생한 이슈를 보여주는 Failures, 가중치를 둔 task들간의 current rate 등도 있다.

 

 


 

 

본 포스트는 제가 공부하는 과정에서 작성하였습니다. 정정사항 댓글은 언제나 환영합니다!

반응형

결론부터 말하자면 필자는 구글 코딩테스트를 보는 환경에 적응하지 못했다.

프로그래머스, 앨리스 코딩 등 한국 코딩테스트 환경에는 적응되어있는 상태였다.

그러나 Google's Online Challenge 환경은 개인적으로 한국 코테 환경보다 더 제약도 많고 상당히 불편했다.

 

GOCC 코테 환경 관련 후기가 많이 없어서 이런 환경에 대비하지 못했던 것이 억울한 나머지

이렇게 후기를 좀 남겨보고자 한다.

 

문제는 총 2문제였고 90분 안에 풀면 된다.

 

후기에는 어떤 문제가 나왔는지는 설명되어 있지 않으니 참고하시기 바랍니다.

 

 

 


 

 

우선 코딩테스트 안내 메일에는 GOCC 테스트에 응시할 수 있는 ID와 passkey를 보내준다.

테스트에 접속하는 부분은 메일을 받으면 다 설명되어 있으므로 달리 이야기할 것은 없다.

 

테스트 제한사항 중 포인트는 총 3가지다.

 

 

  1. 코딩테스트 페이지에서 벗어나는 순간 강제 로그아웃이 된다.
  2. 외부 IDE나 웹사이트 등에서 코드 복사 붙여넣기가 불가능하다.
  3. 제공되는 테스트 케이스 전부 사용가능한 것은 아니다.

 

 

1. 페이지를 벗어난다는게 무슨 의미냐..

테스트에 접속한 순간부터 테스트를 종료할 때까지 메모장, 다른 웹사이트, 다른 프로그램 등 테스트 웹사이트를 벗어나 다른 창으로 옮겨가는 그 어떤 것도 할 수 없다.

 

필자는 문제를 한글로 번역해 메모하고자 메모장을 켰고, 테스트 웹사이트에서 사이트를 벗어나면 강제 로그아웃이 된다는 경고가 떴다.

경고를 한 번 받고 또 시도하면 그때는 정말 강제 로그아웃이 되나 싶었다. 아쉽게도 겁이 나서 다시 시도해보지 못했다.

 

2. 테스트에 들어가기 전 안내문을 보면 외부에서 가져오는 복사 붙여넣기는 불가능하며 테스트 사이트 내 IDE에서의 복붙은 가능하다고 나와있다

 

3. 코드를 작성하고 나면 Test & Compile이라고 테스트 코드를 돌려볼 수 있는 버튼이 있다. 한국 코테에도 다 있는 그런 기능이다.

 

 

 

1번 문제에 올라와있는 테스트케이스는 총 3개였는데 첫번째 테케는 디폴트로 설정되어있지만 두번째 세번째는 사용하고 싶으면 본인이 직접 custom test case로 작성해야했다.

 

문제 본문 드래그는 안되고, 테스트 케이스 위에 복사 버튼을 눌러 복붙을 시도하려해도 애초에 복사 자체가 불가능한 것 같았다.

 

더불어 테스트케이스 전체를 보여주지도 않는다.

테스트케이스는 가로 스크롤만 가능하고 세로 스크롤이 안되는데 테스트가 10개 이상씩 되면 일부는 잘리게 된다. 테케 전체를 보고자 View more 버튼을 눌러도 팝업창이 열리려다 바로 꺼져버린다.

 

결국 준비된 테케 자체를 전부 볼 수도 없다는 것이다.

 

2, 3번째 테케는 submit을 하면 프로그램에서 제출용 테스트케이스로 사용하는데... 애초에 코드 디버깅용으로 주는 테스트케이스가 아니라 submit 테스트케이스를 미리보기 형식으로 보여주는 건가 싶기도 하다.

 

 

 

 

이거 외에 필자가 개인적으로 당황했던 부분은

 

  • Java 14의 경우, IDE가 main함수를 제대로 인식하지 못하는 에러가 있다.

 

필자는 복붙 하나 없이 코드를 작성해 초안을 완성했는데도 class에 main 함수가 없다는 에러를 받았다. main함수가 있는데도! 이건 실제 테스트 전 연습 테스트에서도 겪은 에러였고, IDE 내에서 복붙하는 것 때문에 그런가 싶어서 실제 테스트에서는 처음부터 끝까지 타이핑으로 작성했음에도 에러가 발생했다. 

결국 코드 초기화를 하고 디폴트 코드 상태에서 main을 인식하는지 확인한 후에야 정상적인 진행이 가능했다.

 

혹시 모르니 다른 언어를 사용하더라도 디폴트 코드가 정상동작하는지 꼭 Test & Compile을 하고 코드 작성을 시작하길 바란다.

 

 

 

구글 코딩테스트를 경험하고 깨달은 것은 IDE에 의존없이, 에러 없이, 짧은 시간 안에 바로 정답으로 접근하는 연습을 해야 한다는 것이다.

디버깅도 불가능하고 짧은 시간 안에 테스트케이스 여러개를 만들기도 조금 어려울 수 있을 것 같다.

문제 난이도는 그렇게 높지는 않았지만, 문제를 '맞추는' 거에 더해서 '쉽게' 맞출 수 있는 정도의 실력은 되어야 하지 않나 싶었다.

반응형

아래는 패키징을 WAR로 작업한 스프링부트 서버를 도커에 올리는 과정이다. 

개인적인 기록을 위한 것으로 간단하게 명령어만 서술하려 한다.

 

- 필자는 Window10 버전을 사용한다.

- 도커가 설치되어있다고 가정한다.

- Gradle을 사용하여 빌드한다

- Dockerfile을 사용하여 도커에 올린다

 

참고 : https://zzang9ha.tistory.com/360

 

 


1. Dockerfile 작성

프로젝트 루트 폴더에 Dockerfile을 생성한다.

 

파일 내부에는 아래와 같이 작성한다

 

FROM openjdk:17-jdk-alpine
ARG JAR_FILE=build/libs/*.war
COPY ${JAR_FILE} app.war
ENTRYPOINT ["java","-jar","app.war"]

 

필자는 java 17버전을 사용하고 있어서 jdk:17로 baseimage를 불러온다. 

ARG는 변수를 선언하는 명령어이다. 빌드를 하면 build/libs/ 폴더에 .war 형태로 저장된다. 이 파일들을 변수에 저장한다.

app.war 파일에 빌드된 war 파일들을 저장한다.

ENTRYPOINT는 도커를 실행할 때 실행하는 스크립트이다.

찾아보니, java -jar 형태로도 war 파일을 실행할 수 있었다.

 

2. 프로젝트 빌드

 

gradlew build -x test

 

-x test 는 test를 생략한다는 명령이다.

빌드를 진행하면 build/libs 폴더가 생성되며 그 안에 war 파일 2개가 저장된다.

 

 

3. 도커 이미지 생성

 

docker build --build-arg DEPENDENCY=build/dependency -t 이미지이름 .

 

이미지이름은 dockerTest처럼 원하는 이름으로 지정하면 된다.

마지막에 점(.) 은 필수이므로 잊지 말 것.

 

이미지가 잘 생성됐는지 확인해보고 싶으면 다음 명령어를 입력하면 된다.

 

docker images

 

4. 도커 컨테이너 실행

이제 생성된 도커 이미지로 도커 컨테이너를 실행한다. 

 

docker run -d -p 8080:8080 이미지이름

-d : 백그라운드(데몬)으로 컨테이너를 실행하라는 명령어

-p : 포트를 지정, 연결해주는 명령어. 외부의 8080포트와 컨테이너 내부 8080 포트를 연결한다.

 

컨테이너를 실행하면 컨테이너 ID 하나만 띄우고 터미널 실행이 끝나는데, 제대로 돌아가고 있는지를 확인하려면 다음 명령어를 입력한다.

 

docker ps

 

위 명령어를 입력하면 현재 실행중인 모든 컨테이너를 띄운다.

실행을 시켰는데도 아무것도 뜨지 않는다면, 무언가 문제가 생겨 컨테이너가 종료된 것이다.

종료된 컨테이너까지 모두 보려면 마지막에 -a 를 붙이면 된다.

 

docker ps -a

 

종료된 컨테이너의 경우 STATUS에 Exited라고 뜰 것이다.

오류를 확인하려면 로그를 봐야하니 다음 명령어를 입력해보자.

 

docker logs 컨테이너ID

 

ps -a 명령어로 확인한 컨테이너 ID를 logs 옆에 작성하면 로그가 뜬다.

이 로그에서 문제가 뭔지를 파악하면 된다.

반응형

이번 게시물에서는 response에서 원하는 데이터를 추출하는 방식을 소개한다.

 

Scrapy 튜토리얼1 - Scrapy를 선택한 이유

Scrapy 튜토리얼2 - 네이버 검색 결과 크롤링(크롤러 설정)


 

1. 데이터 추출

 

위 조건을 다 맞춰 spider를 만들었다면 parse함수에서 받아온 response로 원하는 값을 가져오자.

 

 

크롤링을 하기 위해서는 기본적으로 내가 원하는 데이터가 들어있는 태그를 파악해야한다.

F12키를 눌러 개발자 도구를 열고 왼쪽 위에 커서모양 버튼을 누르면 내가 원하는 데이터의 태그를 쉽게 찾을 수 있다.

 

 

scrapy는 class명과 id명, xpath 등을 활용할 수 있는데 간단히 class명으로 데이터를 받아온다.

 

response.css() 함수를 사용하면 class명과 id명을 이용해 데이터를 받아올 수 있다.

클래스는 .클래스명 으로 앞에 점을 하나 붙여야 하고 id는 #아이디명 으로 #을 붙여줘야 동작한다.

 

그 뒤에는 get()함수나 getall() 혹은 extract() 등을 사용해야 비로소 데이터를 추출해온다.

get() 함수는 해당하는 여러 태그 중 1개만 추출하고, getall()이나 extract()는 해당하는 태그가 여러개일 경우 리스트로 반환한다.

getall()과 extract()는 큰 차이는 없지만 getall()은 무조건 리스트로 반환하는 반면 extract() 결과 개수에 따라 리스트 혹은 str로 반환된다. 

 

selector 함수에 대한 더 자세한 사항은 공식문서를 참고하자.

 

 

 

위 코드를 실행한 결과는 다음과 같다.

<a href="https://blog.naver.com/kizaki56?Redirect=Log&amp;logNo=222665532164" class="api_txt_lines total_tit" target="_blank" onclick="return goOtherCR(this, 'a=blg*a.iblg&amp;r=1&amp;i=90000003_0000000000000033D7E66304&amp;u='+urlencode(this.href))"><mark>강남</mark> 한우 <mark>맛집</mark> 장위동유성집 특선 메뉴 추천</a>

 

 

 

2. 텍스트 추출

 

scrapy도 beautifulSoup의 get_text()함수처럼 텍스트만 추출해오는 함수가 존재한다.

이번에는 결과에서 텍스트만 추출해보자

 

    def parse(self, response):
        result = response.css('.api_txt_lines::text').get()
        print(result)

css 선택자에서 텍스트만 추출하도록 하려면 class명 뒤에 ::text 를 붙이면 된다.

 

 

추출된 결과를 보자

 

한우

 

 

분명 제목 전체를 가져오기를 원했는데 결과가 조금 이상하다. 그 이유는 html파일 구조를 보면 알 수 있다.

 

 

::text는 하위태그 내의 text까지는 접근할 수 없고, 텍스트 중 첫번째 텍스트만 가져오는 단점이 있다.

하위 태그까지 포괄한 모든 텍스트를 가져오고자 한다면 xpath를 사용해야한다.

 

 

    def parse(self, response):
        result = response.css('.api_txt_lines').xpath('string(.)').get()
        print(result)

 

xpath는 함수 xpath()를 사용하여 접근하면 된다.

일반적인 사용 예시는 'response.xpath('//*[@id="sp_blog_1"]/div/div/a').get()' 처럼 활용하면 된다.

 

여기서 주목해야할 것은 string()이다.

xpath 링크를 string()으로 감싸면 현재 태그가 감싸고 있는 모든 텍스트를 전부 반환하는 상당히 편리한 함수이다. 

 

한가지 더 설명하자면, string() 함수 안에 있는 .은 현재 위치를 의미한다. css로 접근한 뒤 xpath로 재접근을 하는 것이기 때문에 css로 접근한 현재위치인 a태그에서 모든 텍스트를 가져오는 것을 뜻한다. 만약 그 안쪽으로 더 접근하고자 한다면 .xpath('./div') 등으로 활용할 수 있다. 

 

 

결과를 살펴보자

 

강남 한우 맛집 장위동유성집 특선 메뉴 추천

 

원하는 결과를 잘 추출한 것을 볼 수 있다.

반응형

이번 게시글에서는 Scrapy를 파이썬 스크립트에서 어떻게 호출하고 사용하는지에 대해 소개하려 한다.

 

Scrapy는 프레임워크이기 때문에 쉘에서 사용하는 것이 일반적이지만, 필자는 서버에서 사용해야 하기 때문에 파이썬 스크립트에서 활용하였다.

 

튜토리얼은 네이버 블로그 검색 결과를 가져오는 방법을 소개한다.

 

 

Scrapy 튜토리얼1 - Scrapy를 선택한 이유

Scrapy 튜토리얼3 - 네이버 검색 결과 크롤링(데이터 추출)


 

1. 설치

 

우선 Scrapy를 사용하기 위해 파이썬 프로젝트에 설치해보자

pip install Scrapy

 

 

 

2. Spider 클래스 생성

 

설치가 완료되었다면 우선 Spider라는 것을 만들어야 한다. 용어가 조금 생소한데, 거미줄을 쳐서 html에서 원하는 정보만 걸러 가져온다.. 라는 발상에서 나온 용어이지 않나 싶다.

 

Scrapy는 이 Spider라는 클래스를 호출해 크롤링을 실행한다.

필자는 크롤링을 실제로 진행하는 함수들을 모아둔 클래스 정도로 이해했다.

 

import scrapy

class NaverSpider(scrapy.Spider):
    name = "naver"

    # 처음 크롤링을 시작하는 함수
    def start_requests(self):
    	# '강남 맛집'으로 검색한 네이버 블로그 결과 링크
        url = "https://search.naver.com/search.naver?query=강남 맛집&nso=&where=blog&sm=tab_opt"

        yield scrapy.Request(url=url, callback=self.parse)

    # 크롤링 결과를 받는 callback함수
    def parse(self, response):
    	# 해당 코드는 다음 게시물에서 설명합니다
        result = response.css('.api_txt_lines').get()
        print(result)

 

spider 클래스를 만드는 것에는 몇 가지 제약 조건이 있다.

 

 

 

1. 클래스는 scrapy.Spider를 상속받아야 한다.

 

클래스명은 어떤 것이든 관계 없다.

 

 

2. 클래스 멤버변수 name을 생성한다.

 

Scrapy가 spider를 사용하여 크롤링을 시작할 때, spider를 구분하기 위해 name 멤버변수를 사용한다. 

name은 말 그대로 spider를 식별할 수 있는 이름으로 어떤 이름을 사용하든 상관없다.

 

 

3. start_requests(self)를 생성한다.

 

Scrapy는 이 start_requests 함수를 가장 먼저 호출한다. 함수명은 변경하면 안된다.

함수 안에는 반드시 'yield scrapy.Request(url=링크, callback=콜백함수)'가 선언되어 있어야한다.

return을 쓰면 TypeError: 'Request' object is not iterable  라는 에러가 발생한다.

(yield은 return과 달리 여러번 결과를 반환할 수 있다.)

 

yield로 인해 scrapy는 해당 링크로 요청을 보내고 콜백함수를 호출해 응답을 사용할 수 있도록 한다.

for문을 통해 여러개의 링크로 request를 보낼수도 있다.

 

#예시코드

class NaverSpider(scrapy.Spider):
    name = "naver"

    def start_requests(self):
        urls = [링크1, 링크2, 링크3]

        for url in urls :
        	yield scrapy.Request(url=url, callback=self.parse)

 

 

4. scrapy.Request의 콜백함수를 만든다.

 

콜백함수의 함수명은 어떤것이든 관계 없다. 그러나 매개변수로 response는 반드시 들어가있어야 한다.

scrapy가 콜백함수로 response를 보내주기 때문이다.

 

 

 

 

3. CrawlerProcess 호출

 

spider를 만들었다면 이제 scrapy에서 spider를 호출해야한다.

 

파이썬 스크립트에서 scrapy를 사용하는 방법은 2가지이다.

 

  1.  CrawlerRunner()
  2.  CrawlerProcess()

 

두 함수의 차이는 호출 방식도 있지만 메인 스레드가 아닌 다른 스레드에서 호출이 가능하냐 아니냐의 차이이다.

 

CrawlerRunner는 메인 스레드가 아닌 서브스레드, 서브 프로세스에서도 동작이 가능하다.

CrawlerProcess는 오직 메인 스레드에서만 동작이 가능하다.

CrawlerRunner여러개의 spider를 동시에 실행할 수 있다는 장점도 있다.

 

두 함수의 호출 방식을 살펴보자.

 

from scrapy.crawler import CrawlerRunner

if __name__ == '__main__':

    configure_logging({'LOG_FORMAT': '%(levelname)s: %(message)s'})
    runner = CrawlerRunner()
    runner.crawl(NaverSpider)  # 이 부분에 자신이 정의한 spider 클래스를 넣는다
    # runner.crawl(DaumSpider)   # 여러개의 spider를 동시에 실행할 수도 있다.
    crawler = runner.join()
    crawler.addBoth(lambda _: reactor.stop())
    reactor.run()  # the script will block here until the crawling is finished

 

위 코드는 공식문서 튜토리얼 코드 그대로이다. 필자도 특별히 이 부분에 대해 건드린 것은 없다.

reactor.run()이 실행되면 크롤링이 모두 끝날때까지 다음 코드로 이동하지 않고 기다려준다.

 

단 주의해야할 사항은 reactor는 한번 실행되면 해당 프로세스가 끝날때까지 재실행이 불가능하다.

 

 

from scrapy.crawler import CrawlerProcess

if __name__ == '__main__' :
    process = CrawlerProcess()
    process.crawl(NaverSpider)  # 이 부분에 자신이 정의한 spider 클래스를 넣는다
    process.start()

 

CrawlerProcess는 호출 방식이 더 심플하다.

reactor와 달리 여러번 호출이 가능하고, process.start() 역시 크롤링이 다 끝날때까지 기다려준다. 

 

 

다음 포스트에서는 크롤링하여 받아온 response에서 데이터를 추출하는 작업을 진행하도록 하겠다

반응형

이번 프로젝트에서는 서버에서 블로그 본문 데이터를 크롤링해 온 다음 이를 분석하여 클라이언트에 반환하는 기능을 구현했다. 이 과정에서 Scrapy를 공부하고 사용한 내용을 기록하고자 한다.

 

Scrapy 튜토리얼2 - 네이버 검색결과 크롤링(크롤러 설정)

Scrapy 튜토리얼3 - 네이버 검색결과 크롤링(데이터 추출)


 

웹 크롤링은 대표적으로 BeautifulSoup와 Selenium을 사용한다. 배우기 쉽고, 한국어 자료도 방대한 장점이 있다. 

 

다만 내가 프로젝트에서 두 라이브러리 대신 Scrapy를 사용한 이유는 3가지가 있다.

 

 

 

Webdriver를 호출하는 Selenium의 속도저하 문제

 

Selenium은 실제 웹페이지를 띄우고, 렌더링되는 시간을 기다려야 크롤링이 가능하다. 지도나 무한 스크롤같은 동적인 페이지를 크롤링해야 한다면 Selenium을 사용하는 것이 맞다.

그러나 나는 정적인 페이지를 크롤링하고, BeautifulSoup이나 Scrapy와 비교했을때 훨씬 많은 시간을 소요해 선택하지 않았다.

 

 

 

동기적으로 동작하는 BeautifulSoup

 

BeautifulSoup와 Scrapy는 둘 다 request를 보냈을 때 response를 그대로 받아온다는 점에서 동일하다.

이 둘의 차이점은 BeautifulSoup은 동기적으로, Scrapy는 비동기적으로 동작한다는 것이다.

쉽게 말해 BeautifulSoup은 페이지 크롤링하는 동안 어떤 코드도 수행할 수 없어 대기해야 한다. 그러나 Scrapy는 페이지를 크롤링하는 동안 다른 동작을 수행할 수 있다. 

 

따라서 시간적으로 봤을때 BeautifulSoup는 Scrapy보다 더 많은 시간을 소요한다. BeautifulSoup와 multiprocessing을 함께 사용하면 상당히 빨라지지만 프로젝트 특성상 이후에도 많은 멀티스레딩을 동원하기 때문에 추가적으로 멀티프로세스를 호출하는 것이 오버헤드 측면에서 부담스러웠다.

 

 

 

Scrapy의 확장성 및 편의

 

BeautifulSoup가 단순히 html에서 정보를 가져오는데 그친다면 Scrapy는 확장성 측면에서 상당히 유연하다. middleware를 사용자가 직접 손볼 수 있고 데이터를 다운로드 하거나 하는 등의 여러 확장이 자유롭다.

 

더불어, 필자는 블로그에서 본문데이터를 가져오는 작업을 진행했는데 BeautifulSoup보다는 Scrapy가 하위 태그에서 텍스트만 골라 가져오는 것이 더 편리했다.(이것은 개인적인 의견일 수도 있다.)

 

 

 

한국에서는 Scrapy를 잘 사용하지 않는지 한국어 자료보다는 영어자료가 훨씬 많았다. 다행스러운 점은 스택오버플로우 등 영어권에는 Scrapy 자료가 훨씬 방대하다.

 

 

이러저러한 이유로 결론은 Scrapy를 선택했다는 것이다.

Scrapy가 BeautifulSoup와 Selenium에 비해서 배우기 어렵고 한국어 자료도 많이 없는 단점은 있다.

그러나 공식문서 튜토리얼을 따라가며 배우다보면 대략적으로 어떻게 동작하는지 이해하는 데에는 오래걸리지 않는다.

 

 

다음 포스트에서 본격적으로 Scrapy를 어떻게 사용하는지에 대해 소개하도록 하겠다.

반응형

+ Recent posts