본문 바로가기

TIL

Bulk Insertion과 ORM 최적화

기존에 location과 place를 create_or_get할 때, 인스턴스 하나당 개별적으로 요청을 보내서 확인하는 작업을 진행했다.

시간이 되면 이를 개선하여 한 번의 요청으로 모든 데이터를 확인하고 생성하는 로직으로 변경하려고 생각하고 있었는데 place 와 location 관계를 다시 복구 하면서 같이 리팩토링하게 되었다.

location은 비교적 간단히 처리할 수 있었지만, place 모델은 내부에 place_type 모델과의 관계가 있었기 때문에 생각보다 시간이 걸렸다. 솔직히 시간좀 잡아 먹었다.

그래서 처음에는 하다가 그냥 또 빠르게 바꿀까 해서 add_all을 생각했지만, 이건 사실상 기존에 하던 로직이랑 다를바가 없어서 스스로를 다독여 유혹에 빠지지 않고 작업을 계속 진행 했다.

 

처음 설계 시, 단일 create 기능에서는 type을 문자열로 받아 인스턴스로 변환하는 로직이 포함되어 있었다. 이를 bulk_insert 로직에도 적용하려면 여러 단계의 확인과 변환 작업이 필요했다.

먼저, 새롭게 추가될 place_type이 기존에 있는 값인지를 확인하고, 기존에 없는 경우만 데이터베이스에 추가해야 했다.

이 과정에서 bulk_insert_mappings를 사용하려 했지만, place.place_type에 직접 추가하는 방식은 문제가 있었다. 일반적인 db.add(place) 방식에서는 리스트 형식으로 할당해주고 저렇게 해주면 자동으로 관계가 연결되었지만, bulk_insert_mappings에서는 place_type을 명시적으로 저장해야 했다. 그리고 또한 관계 테이블도 인스턴스도 명시적으로 생성해야 했다.

이후, bulk_insert_mappings로 Place types 값을 추가한 후, 해당 객체들이 id를 가질 것이라고 예상해서 끝났다 생각했는데. 보니까 기존 새로운 place type 인스턴스 리스들에 Id 가 없더라. 생각해보니까 기존 add 방식들도 db를 업데이트 하고도 refresh 를 해줬는데 이건 따로 처리를 안해줬으니까 없는건 당연한거같다 . 그리고 bulk_insert_ 종류들은 ORM 세션의 관리 대상이 아니여서 바로 refresh를 사용할 수 없다고 한다.

그래서 뭐 어떻게 하겠냐 그냥 id를 얻기 위해 별도의 쿼리를 보내기로 했다.  

일부 코드( PlaceType 부분만)

...
# 밑에 조회 쿼리가 더 추가됨  
added_place_types = (
            db.query(PlaceType)
            .filter(
                PlaceType.type_name.in_(
                    [place_type.type_name for place_type in new_types]
                )
            )
            .all()
        )
        
combined_types_dict = {
            item.type_name: item for item in existing_types + added_place_types
        }

        db.bulk_insert_mappings(Place, place_list)

        association_data = []
        for place in place_list:
            for place_type in place["place_types"]:
                association_data.append(
                    {
                        "place_id": place["place_id"],
                        "place_type_id": combined_types_dict[place_type].id,
                    }
                )
                
...

이렇게 처리하고 위에서 말한 관계 테이블들은 core 형태의 table로 구현이 되어있어서 orm의 bulk_insert_mappings는 못쓰고 대신에 execute(relation_table.insert(), data) 이런방식으로 추가해줬다. 

 

결과적으로, 대량의 데이터를 한 번에 저장하기 위해 총 세 번의 데이터베이스 요청을 수행하게되었는데, 처음에는 방금 언급한  place_type의 id들을 가져오기 위한 요청이 아쉬웠지만, 이전의 각 인스턴스별로 체크하고 생성하는 로직에 비교하면 훨씬 효율적이라고 생각되었다.

bulk_insert_mappings는 기존 ORM의 일부 기능을 생략하여 최적화되어 있다고한다. 이 특성 때문에 어떤 부분이 다른지 세세하게 확인하고 작업을 진행해야 했는데, 급하게 작업을 진행하려다 보니 예상보다 더 많은 시간이 소요되었다.

'TIL' 카테고리의 다른 글

결합과 추상화  (0) 2023.09.30
API 요청 모듈 계층 분리 과정  (0) 2023.09.21
TDD 3부  (0) 2022.06.12
TDD 2부  (0) 2022.06.01
객체지향의 사실과 오해 3~4장  (1) 2022.03.31