Closed veluxer62 closed 3 years ago
graphene_sqlalchemy
라이브러리는 페이지네이션을 수행할때 불필요한 count 쿼리를 수행합니다. (hasNext
를 호출 하지 않을 때도 말이죠) 아래 코드는 graphene_sqlalchemy
라이브러리의 resolve_connection
함수 구현코드입니다.
@classmethod
def resolve_connection(cls, connection_type, model, info, args, resolved):
if resolved is None:
resolved = cls.get_query(model, info, **args)
if isinstance(resolved, Query):
_len = resolved.count() # <---------- 여기서 카운트 쿼리를 무조건 실행함
else:
_len = len(resolved)
connection = connection_from_list_slice(
resolved,
args,
slice_start=0,
list_length=_len,
list_slice_length=_len,
connection_type=connection_type,
pageinfo_type=PageInfo,
edge_type=connection_type.Edge,
)
connection.iterable = resolved
connection.length = _len
return connection
왜 전체 건수가 필요한지 보니 graphene
라이브러리가 connection_from_list_slice
함수에서 slice_end
를 만들기 위해서는 전체 건수가 필요해서 그런 것입니다.
def connection_from_list_slice(list_slice, args=None, connection_type=None,
edge_type=None, pageinfo_type=None,
slice_start=0, list_length=0, list_slice_length=None):
"""
Given a slice (subset) of an array, returns a connection object for use in
GraphQL.
This function is similar to `connectionFromArray`, but is intended for use
cases where you know the cardinality of the connection, consider it too large
to materialize the entire array, and instead wish pass in a slice of the
total result large enough to cover the range specified in `args`.
"""
connection_type = connection_type or Connection
edge_type = edge_type or Edge
pageinfo_type = pageinfo_type or PageInfo
args = args or {}
before = args.get('before')
after = args.get('after')
first = args.get('first')
last = args.get('last')
if list_slice_length is None:
list_slice_length = len(list_slice)
slice_end = slice_start + list_slice_length # <----------------- 여기
before_offset = get_offset_with_default(before, list_length)
after_offset = get_offset_with_default(after, -1)
start_offset = max(
slice_start - 1,
after_offset,
-1
) + 1
end_offset = min(
slice_end,
before_offset,
list_length
)
if isinstance(first, int):
end_offset = min(
end_offset,
start_offset + first
)
if isinstance(last, int):
start_offset = max(
start_offset,
end_offset - last
)
# If supplied slice is too large, trim it down before mapping over it.
_slice = list_slice[
max(start_offset - slice_start, 0):
list_slice_length - (slice_end - end_offset)
]
edges = [
edge_type(
node=node,
cursor=offset_to_cursor(start_offset + i)
)
for i, node in enumerate(_slice)
]
first_edge_cursor = edges[0].cursor if edges else None
last_edge_cursor = edges[-1].cursor if edges else None
lower_bound = after_offset + 1 if after else 0
upper_bound = before_offset if before else list_length
return connection_type(
edges=edges,
page_info=pageinfo_type(
start_cursor=first_edge_cursor,
end_cursor=last_edge_cursor,
has_previous_page=isinstance(last, int) and start_offset > lower_bound,
has_next_page=isinstance(first, int) and end_offset < upper_bound
)
)
앞으로 점차적으로는 해당 라이브러리를 사용하지 않고 Query와 Model을 분리하면서 직접 페이지네이션을 구현하려고 합니다. 그렇다면 위 라이브러리로 인해 불필요한 count 쿼리를 수행하지 않을 수 있구요, 페이지네이션은 아래 코드와 같이 전체 건수가 없이도 슬라이싱을 할 수 있습니다.
session.query(VendorReview).offset(paging.offset).limit(paging.limit).all()
# 또는
session.query(VendorReview)[paging.offset:paging.limit]
GraphQL의 표준으로 잠깐 얘기를 돌려보면 GraphQL Pagenation을 보면 페이지네이션을 위한 매개변수로 first
와 offset
, after
를 사용하는 것을 볼 수 있습니다. 하지만 graphene
라이브러리에서 사용하는 매개변수는 before
, after
, first
, last
로 보입니다. 표준과 다른 이유는 제가 GraphQL의 히스토리를 다 알지 못해서 파악하지는 못했는데 버전의 문제지 않을까 라는 조심스러운 추측을 해봅니다.
다시 본론으로 돌아와서 현재 서버에서 제공하고 있는 페이지네이션은 모호함과 함께 표준을 지키지 못하고 있다고 보입니다. 모호함은 첫번째 페이지임에도 불구하고 {first: 10, last: 10}
와 같은 형태로 호출 하고 있다는 것입니다. 표준 대로라면 첫 페이지는 {first: 10}
또는 {first: 10, offset: 0}
으로 표현됨이 맞다고 보입니다. (커서 기반이라면 {first: 10, after: '<마지막 커서>'}
가 되겠네요)
그래서 표준에 맞게 스펙을 바꾸는게 어떨까 하는 제안을 해봅니다. 물론 신규로 만드는 기능부터 적용하고 시간이 될때 리펙토링을 통해 기존 페이지네이션도 고쳐가는 방법으로 하는 전략을 채택하면 좋을 것 같네요. 이슈로는 하위 호환과 일관성인데요. 하위 호환은 기존 기능의 변경 시 협의를 해야 할 것으로 보이구요, 일관성은 기존에 표준에 맞지 않는 구현을 제대로 바꾼다는 측면에서 어느정도 희생을 해야하지 않을까라는 생각을 조심스레 해봅니다.
webflux timeout 설정
webclient에서 응답 미디어 타입이 text/plain
인 경우 dto로 매핑해주기 위해서 codec을 사용하면 된다. 시간되면 한번 활용방법에 대해 적어봐야겠다.
@Bean
fun korailClient(korailProperties: KorailProperties): WebClient {
return WebClient.builder()
.baseUrl(korailProperties.host)
.clientConnector(
ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(korailProperties.timeout))
)
)
.codecs {
it.defaultCodecs()
.jackson2JsonDecoder(Jackson2JsonDecoder(jacksonObjectMapper(), MimeType.valueOf("text/plain")))
}
.build()
}
jacoco gitaction 뱃지 제너레이터
https://github.com/marketplace/actions/jacoco-badge-generator
Jira 세팅을 이것저것 해보고 요구사항에 대한 계층도 만들어 보고 하고 있는데 참 어느것 하나 만족스럽지 못하네Face with open mouth and cold sweat 너무 복잡하게 생각하는게 아닌가 싶기도 하고
sub task는 말 그대로 하위 작업이다. story의 task 용도로 사용하면 나중에 번다운 차트나 번업 차트 등 리포트를 생성할때 estimate값을 불러오지 못한다
component는 서브 프로젝트 용도로 사용할 수 있는데 epic을 그루핑 해주는 용도로 사용하기에 나쁘지 않다. 하지만 상위 티켓으로 사용되는 것이 아니기에 불편함이 있다.
결국 티켓의 계층은 epic과 story(task, backlog 등) 두 계층으로만 나눌 수 있다. 이슈링크를 통해 계층을 표현할 순 있지만 결국 링크일 뿐이라 유연함은 있지만 가시적이지 않다.
BDD 테스트 도구
jacoco gradle plugin document
https://docs.gradle.org/current/userguide/jacoco_plugin.html
git case sensitive issue
git mv
명령어를 사용하면 된다
python에서 배열의 index를 함께 조회할때 사용하기 좋은 함수 enumerate
https://realpython.com/python-enumerate/#using-pythons-enumerate
kotlin 에서 padding 문자열 넣을때 사용하면 좋은 함수
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/pad-start.html
https://www.coupang.com/vp/products/4945355563?itemId=6522590198&vendorItemId=73817074279&src=1191000&spec=10999999&addtag=400&ctag=4945355563&lptag=CFM91783024&itime=20210702140500&pageType=PRODUCT&pageValue=4945355563&wPcid=16225971019382016078617&wRef=&wTime=20210702140500&redirect=landing&isAddedCart=
키보드 사고싶다