ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 21. MongoDB를 꼭 써야해?
    스프링개발자/201 - 일반 2020. 10. 19. 02:34

    1. 개요

    우리 팀은 vehicle route optimization을 제공하는 팀이다. 

    고객마다 중요하게 생각하는 parameter들이 다르기 때문에 

    이 값들을 db에 configuration, objective 등의 형태로 SQL을 통해 저장한다.

     

    최근 NoSQL(MongoDB)를 사용하기 시작했는데,

    schema에 딱히 얽매이지 않고 데이터 구조가 나중에 바뀌더라도,

    코드의 큰 변화 없이 그냥 저장하면 되기 때문에, 고객 metrics를 저장하기 위해 사용하고 있다.

     

    이와중에, 고객이 보낸 입력 데이터와 우리의 결과(return) 값들을 저장하는 logging framework를 우리가 직접 몽고로 만들기로 했다. (Splunk가 고객 입력데이터중 몇몇 필드를 masking 하는 문제가 있어서이다).

     

    이 글은 그 와중에 발견한 우리코드의 문제점과, 해결과정 및 해결책에 대한 내용이다.

    2. 우리 코드의 문제점

    @Slf4j
    @Data
    @AllArgsConstructor @NoArgsConstructor
    @Builder
    @Document(collection = "tenantMetrics")
    public class TenantMetricsContainer implements Serializable {
    
        @Id
        @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
        private String tenantId;
        
        // ...
    
        @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
        private HashMap<UUID, RequestMetricsContainer> requests;
    
    
    }

    보다시피 parent-child relationship이 정의되어 있다.

    부모인 TenantMetricsContainer가 다량의(one-to-many) RequestMetricsContainer를 포함하는 구조이다.

    왜 map구조를 썼는지는 이해할 수 없지만, 아마 NoSQL라서 뭔가 이점이 있다고 판단했던거 같다.

     

    문제점은 이렇다, 부모 container의 repository를 통해서 2개의 자식이 있는 json request를 save하면, 자식 2개가 저장된다. 이후에 자식1개가 있는 request를 추가로 save하면, 기존의 2개가 날아가고, 새로운 1개만 저장이 된다.

     

    3. 임시 해결책

    그래서 우리 코드에서는 service단에서 custom save를 구현하여서, 새로운 1개를 더하기 위해서, 기존의 자식들을 불러오고, 추가하고, 그 결과값을 parent repo를 통해 다시 덮어씌우고 있었다.

     public void saveTenantRideMetrics(SaveTenantMetricsRequest request) {
            Optional<TenantMetricsContainer> tenantMetrics = this.tenantMetricsRepository.findById(request.getTenantId());
            // ...
            if (tenantMetrics.isPresent()) {
                TenantMetricsContainer tM = tenantMetrics.get();
                tM.setTenantName(request.getTenantName());
                // ...
                HashMap<UUID, RequestMetricsContainer> requests = tM.getRequests();
                requests.put(UUID.fromString(request.getRequestId()), RequestMetricsContainer
                        .builder()
                        // ...
                        .build());
                tM.setRequests(requests);
                this.tenantMetricsRepository.save(tM);
                log.info("Tenant metric saved");
            } else {
                HashMap<UUID, RequestMetricsContainer> tenantRequests = new HashMap<>();
                tenantRequests.put(UUID.fromString(request.getRequestId()), RequestMetricsContainer
                        .builder()
                        // ...
                        .build());
                TenantMetricsContainer newTenantMetrics = TenantMetricsContainer
                        .builder()
                        // ...
                        .build();
                this.tenantMetricsRepository.save(newTenantMetrics);
                log.info("New Tenant metric saved");
            }
        }

    코드가 복잡하고, 재사용성도 없다.

     

    4. 근본 해결책; NoSQL vs SQL

    근본적인 문제가 뭐였을까?

    부모 데이터구조에 변경사항이 있으면, 자식에게까지 그대로 상태변화를 적용하는 것을 Cascade라고 한다.

    예를들어, 부모 데이터를 지우면, 자식 container 데이터까지 지우는 식이다.

    SQL에서는 이런 관계가 잘 정의되었지만, NoSQL에서는 지양하는 방법이다.

     

    NoSQL의 탄생 목적은 이렇다.

    많은 데이터량 속에서 schema 문제 없이 빠른 시간안에 데이터를 write하고 싶다

    분산 환경에서 동작하기에 유리하다(scale up)

     

    그렇기 때문에 Spring Data MongoDB에서는 관계성을 지원하지 않는다.

    부모, 자식 두가지 Document에 관계를 인위적으로 부여할 수 있(DBRef, Reflection)지만, 그럴거면 SQL를 쓰자.

     

    5. 결론

    기존에 쓰던 TenantMetrics는, 숫자들만 추적하는 것으로 트래픽과 확장성을 걱정 할 필요가 없고, Metrics의 구조또한 바뀔 염려가 거의 없다. SQL로 쓰도록 한다.

    이번에 도입하는 LogMetrics같은 경우에는, 고객이 보낸 Request와 우리의 반응 Response모두를 저장해야하고, 데이터구조의 mapping도 expensive하다. 부모-자식 구조의 LogContainer가 아닌, request의 고유 id값에 따라서 document 1개만 있는 구조의 container로 NoSQL를 쓰도록 한다. 

     

    부모-자식 구조가 있는 NoSQL의 나쁜 예제

    @Document(collection = "systemLog")
    public class SystemLogContainer implements Serializable {
    
        @Id @Field("tenantId")
        private String tenantId;
    
        private List<SystemLogRequestContainer> requests;
    
    }
    @Document(collection = "systemLogRequest")
    public class SystemLogRequestContainer implements Serializable {
    
        private String tenantId;
        @Id
        private String scheduleRequestId;
        private ZonedDateTime timestamp;
        private String request;
        private String response;
        // ...
    
    }

     

    이 두개를 합친 NoSQL의 좋은 예

    @Document(collection = "systemLogCombined")
    public class SystemLogCombinedContainer implements Serializable {
    
        @Id
        private String requestId;
        
        private String tenantId;
        private ZonedDateTime timestamp;
        private String request;
        private String response;
    
    }

     

    상황에 맞는 데이터구조를 고르자.

    NoSQL를 쓸 때에는 Document는 한개만 쓰고, 절대 두개를 관계성을 부여해서 쓰지 말자

Designed by Tistory.