code review 그리고 리뷰의 목적. 과거와 현재.

~ 2007:
 예전에 15년도 더 된 직장에서의 코드 리뷰는 주로 overflow , 문자열 배열이 포함된 구조체 등 에서 java 로 변환되던 마지막 시점이었다. 코드리뷰는 그 이후로는 그렇게 깊게 algorithm 을 봐야하는 경우는 없었던 것 같다.  HW RAS 와 battery table 을 보거나 audio sound 만 보면 되는 정도였다.
2007 ~ 2010:
아직 smartphone 이 본격화 되기 전이라서 real time OS embedded SW 가 chipset licensee 에게만 주어지는 상황이었고, 항상 유념해야 할 data type, 메모리관리, lockup , reset 에 유의해야 할 mutex, semaphore 에 대한 이해가 기본적으로 필요한 SW 직군으로 부서 복귀를 하게 되어 정신없이 디버깅을 하던 시절에는 로그를 찍을 때 조차도 printf 와 snprintf 를 구분해야 하지 않나라는 정도 강박관념이 있었던 때에는 시간차 메모리 공격을 J-TAG 디버거로 해야 했고, 고객 단말을 회수해서 재현안되는 실제 보드에 납땜을 해서 ELF 로 현지 출장 디버깅도 했었던 2008년의 기억도 있었다.
요즘은 그 기억은 저 멀리 아득하고, 아마도 특수 산업이나 kernel 을 다뤄야 하는 closed 프러덕이거나, framework SK 자체를 개발하는 HW 회사, 칩셋 회사, 플랫폼 회사 정도 만이 남아 있을 듯 하다.   
2010 ~2021:
한동안 TAM, wireless standard 관련된 서비스 TPM 을 했고, 본격적으로 cloud 에 관심이 생긴 시절에는 이런 저런 demo 연습을 하면서 쉬던 시절이있는데, 
2021 ~:
다시 복귀한 일종의 feature engineering , data mining 을 하다보니 확실히 주변과 소통하는 코드 리뷰에서는 mutable, immutable 이 확실해 지니까 대부분은 메모리 한계에 대한 부담감은 현저히 줄고, 성능과 모델의 정확도와 안정성에 대한 부분이 더 중요해 졌다. NPE 의 부담도 없는 Kotlin 도 직접 변경하고 빌드할 경우도 2년도 더 되었고, 매일 사용해야 하는 코드도 모두 python 이나 SQL 뿐이기 때문에, 오히려 data warehouse , UDTF 에서의 사용성 확인을 Snowflake 애들과 할 때 빼고는 별로 코드 리뷰라고 할 것도 없다.
2025 ~:
요즘 DevOps 가 바빠서, 어쩔수 없이 직접 내가 비정형 데이터를 다루되 pipeline 을 거치지 않고 manual 로 다루고 있다. 이 와중에 오랜 만에 코드 및 query 리뷰가 필요한 경우가 많아지고 있다. 실험 model 을 돌리고 WEB Hook 을 걸어보는 것은 내년에나 해 볼 듯 하고, 당장은 초기 값들을 처리하면서 proof of concept 을 하는 과정이다 보니, 하나하나 확인해 보고 직관적으로 맞는 스토리인지 분석해야 하는 시간이 더 많아졌다. 다른 미팅에도 들어가 보면, 뒷단은 일반 모델로 찾고 연결하려 해도, 결국 경력있는 사람들은 custom 으로 일일이 봐줘야 critical 한 부분을 해결할 수 밖에 없다. 1BP 오류라도 최소화 하고자 하기 때문에 나같이 미들필드에서 골대 앞에까지만 넘겨주는 사람은 대부분 문제는 없는데, 마지막 insight 를 뽑는 경우에는 어쨌든 대충 믿고 맡길수가 없다. 한 번 더 예전방식으로 리뷰를 해야 한다.  
그러던 와중에 간만에 생각과 많이 다른 결과값을 dashboard 에서 보게 되어 어제 간만에 data scientist 와 대화를 했고, 왜 내가 전달해 준 코드를 쓰지 않고 자기 방식으로 변경했는지에 대해 건설적인 코드 리뷰를 하게 되었다. 우리가 코드를 다루다보면, 항상 익숙하고 자기가 자신있는 방식을 도입하곤 한다. 그러면 조심해야 할 부분이 다루는 data의 context, 즉 근본적인 그 데이터의 속성과 우리가 바라는 최종 결과값과 왜 그 결과값을 원했는지의 목적사이에 그 어딘가에 갭을 발견하게 된다. 
결론적으로, data scientist 가 domain 에 대한 이해, 다루고 있는 raw data 에 대한 확실한 이해를 하지 않는다면, 이는 쇼를 위한 쇼, 실제 사용 가치가 없는 물건을 그럴 듯 하게 포장한 연금술사의 개떡같은 껍데기일 수 밖에 없다는 결론이 났고, data scientist가 최종적으로 내 코드를 그대로 쓰기로 결정했다.  
Delivery 하는 data 가 실제 어떤 input data 들이 초기에 들어 왔었는지, pre-process 를 어느정도로 해서, 어떻게 넘겨야 performance 가 보장되는지, 또 왜 모두 imputation 하지 말아야 하는지, 등등에 대해 정말 건설적인 대화를 간만에 하게 되었다. 
오늘 아내가 차려준 맛있는 식사를  하면서 들은 생각, data 의 정확도와 신뢰도, 이건 요리도 마찬가지가 아닐까? 내가 산 식재료로, 내가 요리하는 조리도구, 내가 어느정도로 어떤 온도로 상황에 따라 다른 경우, 그리고 이 요리가 어느때 어느 시점에 식탁에 올려지는지도 참 중요하다.
상반기에 돌린 모델의 spaceX 의 위치와 data 품질에 대한 분석결과도 약간 빛이 바랬고, 식기 전에 드세요라는 말을 하는 것은 꼭 지켜야 할 부분이다.
참고로 예전의 기억으로 정리한 주의해야 할 사항들은 아래와 같다 0 ~ 4. 요즘은 보지 않는 것들....
그리고 요즘 보는 것들 5. ~ 9.
0. Type cast 와 연산자 overloading, overriding 


1. Buffer Overflow가 발생하기 쉬운 데이터 타입
  • 주요 데이터 타입: 문자열 (char 배열 또는 char)*
    • 왜 이 타입인가?
      • 문자열은 끝을 나타내는 NULL 문자('\0')로 종료되지만, 입력 함수가 입력 길이를 제한하지 않으면 배열의 크기를 초과해 인접 메모리를 덮어쓸 수 있습니다. 이는 스택 오버플로나 보안 취약점(예: 코드 주입 공격)을 초래합니다.
      • 예시 데이터 타입:
        • char buffer[SIZE]; (고정 크기 배열)
        • char* str; (동적 할당 문자열, 하지만 입력 시 크기 관리 필요)
      • 관련 함수들 (버퍼 오버플로 위험이 높은 것들):
        • gets(char* s): stdin에서 문자열 입력. 가장 위험 – 개행 문자까지 읽지만 길이 제한 없음. (권장되지 않음, 대신 fgets 사용)
        • scanf("%s", char* s): 문자열 입력 시 공백 전까지 읽음. 길이 제한 없어 오버플로 발생 가능. (대안: scanf("%Ns", s)로 길이 제한, N은 버퍼 크기-1)
        • fgets(char* s, int size, FILE* stream): 안전하지만, size를 잘못 지정하면 여전히 위험.
        • strcpy(char* dest, const char* src): 문자열 복사. src 길이가 dest 크기 초과 시 오버플로. (대안: strncpy)
        • strcat(char* dest, const char* src): 문자열 연결. 비슷한 이유로 위험. (대안: strncat)
    • 다른 데이터 타입과의 차이:
      • 숫자 타입 (int, float, double 등): scanf("%d", &i)처럼 입력 시 고정 크기(4바이트 등)이므로 오버플로가 덜 발생. 하지만 잘못된 입력(예: 너무 큰 숫자)은 오버플로나 잘못된 값으로 이어질 수 있음 (e.g., int 범위 초과 시 signed integer overflow, 하지만 이는 버퍼 오버플로와 다름).
      • 포인터 타입 (%p): 메모리 주소 입력 시 위험하지만, 문자열만큼 흔하지 않음.
      • 배열 타입 (non-char): e.g., int 배열 입력 시 루프나 개별 scanf 사용, 하지만 문자열처럼 연속 입력되지 않음.
  • 기타 주의할 데이터 타입:
    • 배열 기반 구조체: 문자열 멤버가 포함된 경우 (e.g., struct에 char name[20];).
    • *wchar_t (wide characters)**: 유니코드 문자열 입력 시 비슷한 문제 (wscanf, wcscpy 등).
2. Buffer Overflow 방지 팁 (C/C++)
  • 안전한 대안 사용: fgets 대신 gets, strncpy 대신 strcpy.
  • 길이 지정: scanf에서 %Ns 사용 (N=버퍼 크기-1).
  • 동적 할당: malloc으로 충분한 크기 할당 후 입력.
  • 컴파일러 옵션: Stack protector 활성화 (e.g., gcc -fstack-protector).
  • 예시 코드 (위험한 경우):
    C
    #include <stdio.h>
    int main() {
        char buf[10];
        gets(buf);  // 입력이 10자 초과 시 오버플로
        printf("%s\n", buf);
        return 0;
    }
  • 안전한 버전:
    C
    #include <stdio.h>
    int main() {
        char buf[10];
        fgets(buf, sizeof(buf), stdin);  // 크기 제한
        printf("%s", buf);
        return 0;
    }
3. Python과 Java에서의 유의점

Python 
  • 왜 버퍼 오버플로가 거의 없나?
    • 문자열(str)은 **immutable(변경 불가)**하고, 동적 할당됩니다. 입력 함수가 메모리를 자동으로 확장하므로 고정 버퍼 개념이 없음.
    • CPython 구현에서 메모리 관리는 가비지 컬렉터(GC)가 처리. 입력 크기가 커도 런타임이 메모리 할당을 관리.
    • 표준 입력 함수: input() 또는 sys.stdin.read() – 입력을 문자열로 읽어 안전하게 저장. 길이 제한 없음 (메모리 한계까지).
  • 유의점:
    • 메모리 소모: 매우 큰 입력(예: GB 단위 파일 읽기) 시 OutOfMemoryError 발생 가능. 해결: sys.stdin.readline()로 라인 단위 읽기, 또는 chunk 단위 처리 (e.g., for line in sys.stdin:).
    • 보안: 사용자 입력 시 SQL 인젝션이나 명령어 주입 방지 (e.g., input()으로 받은 걸 os.system()에 넣지 말기). 대신 parametrized query나 subprocess 사용.
    • 성능: 큰 데이터 입력 시 느려질 수 있음. 유의: buffered IO 사용 (e.g., io.BufferedReader).
    • 데이터 타입 관련: list나 bytes 같은 mutable 타입에서 큰 데이터 추가 시 메모리 증가, but 오버플로 아님. bytes는 고정 크기 가능하지만, 입력 함수가 자동 처리.
    • 예시: data = input() – 아무리 길어도 안전 (메모리 한계 제외).
 Java 

  • 왜 버퍼 오버플로가 거의 없나?
    • String은 immutable하고, JVM이 메모리 자동 관리 (GC). 고정 크기 char 배열이 아닌 동적 문자열.
    • 입력 클래스 (Scanner, BufferedReader): 내부적으로 버퍼링되지만, 오버플로 방지 메커니즘 있음 (e.g., Scanner.nextLine()은 라인 끝까지 읽지만 메모리 자동 확장).
    • 저수준 배열 (char[]) 사용 시 직접 관리해야 하지만, 표준 IO에서는 String으로 변환되어 안전.
  • 유의점:
    • 메모리 한계: 큰 입력 시 OutOfMemoryError. 해결: BufferedReader로 chunk 읽기 (e.g., readLine() 루프).
    • 인코딩 문제: UTF-8 등 입력 시 잘못된 인코딩으로 데이터 손실. 유의: InputStreamReader에 charset 지정.
    • 보안: 입력 검증 필수 (e.g., Scanner로 숫자 입력 시 InputMismatchException 처리). 명령어 실행 시 ProcessBuilder 사용.
    • 성능: 큰 파일 입력 시 Scanner 대신 BufferedReader 사용 (더 빠름).
    • 데이터 타입 관련: ArrayList<String>처럼 컬렉션 사용 시 동적 성장, but 매우 큰 입력은 힙 오버플로 유발 가능.
    • 예시:
      Java
      import java.util.Scanner;
      public class Main {
          public static void main(String[] args) {
              Scanner sc = new Scanner(System.in);
              String input = sc.nextLine();  // 안전하게 읽음
              System.out.println(input);
          }
      }
  • 현대 Android 개발에서 ViewBinding + Kotlin + Safe Args만 써도 Java 시절에 80~90%를 차지하던 NPE는 거의 없음 개발자가 진짜 신경 써야하는 부분은,

    • 외부 API 응답 (JSON 파싱)
    • Java로 작성된 아주 오래된 서드파티 라이브러리




4. Python에서 자주 쓰이는 null 안전 기법들
Python
# 1. 간단한 체크
if user.name is not None:

# 2. 기본값 제공 (Kotlin의 elvis 연산자와 동일)
name = user.name if user.name is not None else "Anonymous"

# 3. getattr (Java의 optional.orElse 같은 느낌)
name = getattr(user, "name", "Anonymous")

# 4. 최신 Python (3.8+) 에서 typing으로 정적 체크 가능
from typing import Optional

def greet(user: Optional[User]) -> str:
    name = user.name if user else "Guest"   # mypy, pyright 등으로 체크 가능

null로 인한 크래시 가능성개발자가 반드시 신경 써야 하나? Java: 매우 높음 (무조건 신경 써야 함, NPE = 1등 크래시 원인) Kotlin:극히 낮음 (Android 거의 안 써도 됨, null safety + ViewBinding) Python: 중간~높음 (항상 수동으로 체크하거나 타입 써야 함. 정적 체크는 선택 사항)

5. 초기 Preprocess 단계: 입력 데이터의 구조 검증

  • 주의할 점: Nested array가 varchar로 저장되어 있으면(예: ['[1,2,[3,4]]', '[timestamp1, value1]']), 파싱 전에 구조 불일치(길이 다름, 중첩 깊이 불규칙, malformed string)가 발생할 수 있음. Flatten 전에 전체 데이터셋을 스캔하지 않으면, 900줄 쿼리 중간에 런타임 에러(예: 배열 인덱스 out-of-bounds)로 전체 실패.
    • Timestamp가 문자열로 섞여 있으면(ISO8601 vs. epoch), 파싱 실패로 invalidity가 폭발.
  • 피하는 방법:
    • 첫 CTE에서 COUNT(*)와 ARRAY_LENGTH()로 배열 크기/깊이 통계 수집 (e.g., WITH stats AS (SELECT AVG(ARRAY_LENGTH(col)) FROM table)).
    • UDTF로 커스텀 파서 만들기: 입력 배열을 JSON_EXTRACT_PATH나 REGEXP_SUBSTR로 flatten 전에 validate (e.g., IF(ARRAY_LENGTH(nested) > 0, PARSE_JSON(col), NULL)).
  • 실무 팁: Preprocess를 별도 쿼리로 분리 (e.g., CREATE TEMP VIEW preprocessed AS ...). 900줄 쿼리 시작 전에 이 뷰를 참조하면 디버깅 쉬움. Invalid row 비율이 10% 초과 시, imputation 전에 데이터 샘플링(예: TABLESAMPLE SYSTEM 1)으로 테스트.
6. Data Invalidity 체크: CTE 내 점진적 유효성 검사
  • 주의할 점: Flatten 후 관계성(예: parent-child array 간 링크)을 이해하려면, invalid 데이터(빈 배열, null timestamp, 비숫자 값)가 쿼리 중간에 누적되어 JOIN 실패나 잘못된 RANK 계산 유발. 특히 timestamp가 문자열이면, TO_TIMESTAMP() 실패로 NULL 폭증 → imputation이 과도하게 적용됨.
    • Nested 배열에서 "마지막 값" 추출 시 RANK() DESC가 invalid timestamp 때문에 순서 왜곡 (e.g., '2023-13-01' 같은 잘못된 날짜).
  • 피하는 방법:
    • CTE 체인을 계층화: WITH raw AS (...), validated AS (SELECT * FROM raw WHERE ARRAY_LENGTH(nested) > 0 AND col REGEXP '^[0-9]' -- 숫자/타임스탬프 패턴 체크), ....
    • 각 CTE 끝에 CASE WHEN으로 invalid 플래그 추가 (e.g., invalid_flag = CASE WHEN timestamp IS NULL THEN 1 ELSE 0 END), 그리고 HAVING SUM(invalid_flag) < threshold로 필터링.
    • UDTF 활용: 커스텀 함수로 배열 내 timestamp 유효성 검사 (e.g., CREATE FUNCTION validate_ts(ts VARCHAR) RETURNS BOOLEAN AS $$ SELECT TRY_TO_TIMESTAMP(ts) IS NOT NULL $$).
  • 실무 팁: CTE 10개마다 중간 결과 저장 (e.g., CREATE TEMP TABLE mid_cte AS SELECT * FROM validated). Invalidity 비율 로그: SELECT COUNT(*) FILTER (WHERE invalid_flag=1) / COUNT(*) FROM validated. 목표: invalid < 5% 유지, 아니면 upstream(데이터 소스) 수정 요구.
7. Imputation(결측치 처리): 적절성 확인과 과적합 방지
  • 주의할 점: Nested 배열 flatten 후(예: UNNEST로 배열 풀기), 결측(빈 슬롯, null timestamp)이 많으면 imputation(평균/중앙/forward-fill)이 관계성을 왜곡. 예: timestamp 결측 시 이전 값으로 채우면 "마지막 값" RANK가 부정확해짐. 900줄 쿼리에서 imputation 로직이 중복되면 성능 저하(스캔 반복).
    • Varchar 기반이니, 문자열 imputation(예: 'N/A' → '0')이 숫자 변환 후 실패할 수 있음.
  • 피하는 방법:
    • CTE 내 conditional imputation: imputed_value = COALESCE(value, LAG(value) OVER (PARTITION BY id ORDER BY ts) -- forward-fill).
    • UDTF로 복잡한 imputation: CREATE FUNCTION impute_array(arr ARRAY) RETURNS ARRAY AS $$ ... $$ (e.g., 배열 내 median 계산).
    • Post-imputation 검증 CTE: post_imp AS (SELECT *, ABS(imputed - original) / original AS deviation FROM imputed HAVING deviation < 0.1).
  • 실무 팁: Imputation 전에 sensitivity 테스트: 3가지 방법(median, mean, drop)으로 샘플 쿼리 돌려 결과 비교 (e.g., 상관계수 계산). 긴 쿼리에서 QUALIFY ROW_NUMBER() OVER (...) = 1로 중복 imputation 피함. 문서화: 주석으로 "Imputation: forward-fill on ts, threshold=0.05" 추가.

8. Flatten 과정: 관계성 유지와 Combine/Rank 처리

  • 주의할 점: Nested 배열 flatten 시(예: LATERAL FLATTEN(nested) in Snowflake), parent-child 관계(예: outer array의 index가 inner의 key)가 끊어지면, combine(예: JOIN on array_index) 실패. Timestamp로 ORDER BY 하면 invalid 값 때문에 RANK DESC가 "마지막 값"을 잘못 뽑음 (e.g., NULL이 최상위).
    • 900줄 중 flatten 부분이 200줄 넘으면, 중간 결과가 메모리 초과(특히 대용량 데이터).
  • 피하는 방법:
    • Flatten CTE: flattened AS (SELECT id, outer_idx, inner_array FROM raw LATERAL FLATTEN(input => nested) AS t(outer_idx, inner_array)), 그리고 UNNEST(inner_array) 체인.
    • 관계 유지: array_index 컬럼 추가 (e.g., ROW_NUMBER() OVER (PARTITION BY id)). Combine 시 JOIN ON parent.id = child.parent_id AND parent.outer_idx = child.inner_idx.
    • Rank 처리: WITH ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC) AS rn FROM flattened WHERE ts IS NOT NULL) SELECT * FROM ranked WHERE rn=1.
  • 실무 팁: Flatten을 UDTF로 캡슐화 (e.g., flatten_nested(arr VARCHAR) RETURNS TABLE(...)). 성능: CLUSTER BY id나 인덱스 활용. 테스트: 작은 서브셋(100 rows)으로 flatten 결과 샘플 출력해 관계 확인.
9. 전체 쿼리 관리: 900줄+의 가독성/성능/디버깅
  • 주의할 점: CTE가 50개+ 되면, 중간 invalidity 누적으로 최종 결과 왜곡. UDTF 호출이 많으면 컴파일 시간 길어짐. Timestamp timezone 미처리 시 RANK가 지역별 다름.
  • 피하는 방법:
    • 모듈화: CTE를 논리 그룹으로 나누고, -- Section: Validation 주석. UDTF는 5개 이내로 제한.
    • 성능: EXPLAIN으로 스캔 확인, 불필요 JOIN 제거. Imputation 후 APPROX_PERCENTILE로 대략 계산.
    • 디버깅: 각 CTE 끝에 LIMIT 10으로 테스트 쿼리 생성 (e.g., SELECT * FROM cte5 LIMIT 10).
  • 실무 팁: 버전 관리: Git에 쿼리 저장, diff로 변경 추적. 도구: dbt나 SQLFluff로 포맷팅. 결과 분석 전, "sanity check" CTE 추가 (e.g., SUM(imputed) vs. original 비교).

요약: TOP 5 실수 피하기

  1. Preprocess 무시: 항상 통계 CTE부터 → 구조 파악.
  2. Invalidity 누적: 각 CTE 끝에 플래그 + 필터 → 5% 룰.
  3. Imputation 과도: deviation 체크 + sensitivity 테스트 → 왜곡 방지.
  4. Flatten 관계 끊김: index 컬럼 필수 + LATERAL UNNEST 체인.
  5. 긴 쿼리 블랙박스: 모듈화 + 중간 테이블 → 디버깅 10배 쉬움.

댓글

이 블로그의 인기 게시물

WACC 분석 CEG vs VST

2025 QT and 2026 전망.

TCPC vs GBDC