전통적인 Linters가 중요한 버그를 놓치는 이유 (그리고 AI가 할 수 있는 일)

발행: (2025년 12월 15일 오후 11:49 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

방어 계층

현대 소프트웨어 개발에는 버그를 탐지하는 여러 계층이 존재합니다:

계층 1: 린터 (ESLint, Pylint, RuboCop)
잡아내는 것: 구문 오류, 스타일 위반, 간단한 패턴
놓치는 것: 논리 오류, 보안 취약점, 성능 문제

계층 2: 타입 검사기 (TypeScript, Flow, mypy)
잡아내는 것: 타입 불일치, 정의되지 않은 변수
놓치는 것: 런타임 오류, 비즈니스 로직 버그

계층 3: 단위 테스트
잡아내는 것: 회귀, 깨진 기능
놓치는 것: 엣지 케이스, 통합 문제

계층 4: 코드 리뷰
잡아내는 것: 아키텍처 문제, 설계 이슈
놓치는 것: 미묘한 버그(인간은 실수할 수 있음)

하지만 다음과 같은 격차가 있습니다: 구문적으로는 올바르고, 타입도 안전하며, 테스트를 통과하고, 인간 리뷰어에게도 정상으로 보이는 버그.

사각지대

예시 1: await 누락

async function getUsers() {
  const response = await fetch('/api/users')
  const users = response.json() // BUG: Missing await
  return users
}
  • ESLint: ✅ 오류 없음
  • TypeScript: ✅ 오류 없음 (responseany인 경우)
  • 테스트: ✅ 통과 가능(테스트가 데이터 타입을 확인하지 않으면)
  • 코드 리뷰: ❌ 놓치기 쉬움

결과: users는 배열이 아니라 Promise입니다. users.lengthusers.map()을 기대하는 코드는 실패합니다.

예시 2: SQL 인젝션

app.get('/user', (req, res) => {
  const query = `SELECT * FROM users WHERE email = '${req.query.email}'`
  db.execute(query)
})
  • ESLint: ✅ 오류 없음
  • TypeScript: ✅ 오류 없음
  • 테스트: ✅ 통과(테스트가 안전한 입력만 사용)
  • 코드 리뷰: ❌ 보안에 집중하지 않으면 놓칠 수 있음

결과: 치명적인 보안 취약점. 공격자는 임의의 SQL을 실행할 수 있습니다:

GET /user?email=' OR '1'='1
GET /user?email='; DROP TABLE users; --

예시 3: 메모리 누수

function setupWebSocket() {
  const ws = new WebSocket('wss://api.example.com')
  ws.on('message', handleMessage)
  return ws
}

setInterval(() => {
  setupWebSocket()
}, 5000)
  • ESLint: ✅ 오류 없음
  • TypeScript: ✅ 오류 없음
  • 테스트: ✅ 통과(짧은 테스트 환경)
  • 코드 리뷰: ❌ 놓칠 수 있음

결과: 5초마다 새 WebSocket을 만들지만 이전 연결은 닫히지 않음. 1시간 후: 720개의 열린 연결 → 메모리 부족으로 크래시.

예시 4: 레이스 컨디션

async function processItems(items) {
  items.forEach(async item => {
    await saveToDatabase(item)
  })
  console.log('All items processed!')
}
  • ESLint: ✅ 오류 없음
  • TypeScript: ✅ 오류 없음
  • 테스트: ❌ 간헐적으로 실패할 수 있음(레이스 컨디션)
  • 코드 리뷰: ❌ 괜찮아 보임

결과: forEach는 async 콜백을 기다리지 않음. console.log가 즉시 실행돼 실제 저장이 이루어지기 전에 로그가 출력되고, 프로세스가 일찍 종료되면 데이터 손실이 발생할 수 있습니다.

전통적인 도구가 이를 놓치는 이유

패턴 매칭 한계

린터는 추상 구문 트리(AST)와 단순 패턴 매칭을 사용합니다:

IF 코드가 패턴 X와 일치하면
THEN 오류 Y를 표시

이는 구문 오류에는 잘 작동하지만, 코드가 무엇을 하는지 이해해야 하는 의미론적 오류에는 실패합니다.
예: 린터는 var x = x + 1(선언 전에 변수 사용)을 잡을 수 있지만 const users = response.json()(await 누락)은 잡지 못합니다. 두 경우 모두 구문적으로는 유효하기 때문입니다.

컨텍스트 이해 부족

전통적인 도구는 코드를 독립적으로 분석합니다. 다음을 알지 못합니다:

  • 함수가 수행해야 할 목적
  • 런타임에 변수에 어떤 값이 들어갈 수 있는지
  • 코드베이스의 서로 다른 부분이 어떻게 상호작용하는지
  • 일반적인 보안 취약점
  • 성능에 미치는 영향

예: 린터는 query = "SELECT * FROM users WHERE id = " + userId를 정상적인 문자열 연결로 본다. 사용자 입력을 SQL에 직접 연결하면 인젝션 위험이 있다는 사실을 인식하지 못합니다.

언어별 도구의 한계

각 린터는 하나의 언어에만 초점을 맞춥니다(ESLint → JavaScript, Pylint → Python, RuboCop → Ruby 등). 이로 인해:

  • 언어별 별도 도구 필요
  • 규칙 집합과 설정이 언어마다 다름
  • 다중 언어 코드베이스에서 결과가 일관되지 않음
  • 유지보수 부담 증가

AI 접근법

GPT‑4와 같은 대형 언어 모델(LLM)은 다른 전략을 제공합니다:

컨텍스트 인식 분석

패턴 매칭 대신 LLM은 코드를 의미론적으로 추론합니다. 다음을 유추할 수 있습니다:

  • 함수의 의도된 동작
  • 잠재적인 런타임 실패
  • 보안 영향
  • 성능 특성
  • 언어별 베스트 프랙티스

예시: GPT‑4는 const users = response.json()을 보고 response.json()이 비동기임을 이해하고, await이 없으면 변수는 Promise가 된다는 것을 파악합니다.

다중 언어 지원

하나의 LLM은 학습된 모든 언어의 코드를 분석할 수 있어 언어별 린터를 별도로 사용할 필요가 없습니다.

설명형 출력

LLM은 단순히 문제를 표시하는 것이 아니라 다음을 설명합니다:

  • 버그인가
  • 무엇이 문제를 일으킬 수 있는가
  • 어떻게 올바르게 고칠 수 있는가
  • 언제 해당 패턴이 적절한가
  • 어디서 더 배울 수 있는가

이 덕분에 피드백이 교정뿐 아니라 교육적인 가치도 가집니다.

실제 사례

사례 1: Async/Await 버그

입력

async function fetchUserData(userId) {
  const response = await fetch(`/api/users/${userId}`)
  const user = response.json()
  return user.name
}

AI 분석

  • 문제: response.json() 앞에 await가 없음.

  • 잘못된 점: response.json()Promise를 반환하므로 user는 파싱된 데이터가 아니라 Promise가 됩니다.

  • 결과: user.namePromise에서 .name을 읽으려 하여 undefined 혹은 Cannot read property 'name' of undefined와 같은 런타임 오류가 발생합니다.

  • 수정:

    const user = await response.json()
  • 이유: JSON 파싱은 CPU 집약적일 수 있어 비동기로 처리하면 메인 스레드가 차단되지 않습니다.

  • 베스트 프랙티스: .json(), .text(), .blob()Promise를 반환하는 메서드는 항상 await를 사용합니다.

사례 2: 보안 취약점 (Python)

입력

def get_user_by_email(email):
    query = f"SELECT * FROM users WHERE email = '{email}'"
    return database.execute(query)

AI 분석

  • 문제: SQL 인젝션 취약점.

  • 잘못된 점: 사용자 제공 email이 SQL 문자열에 직접 삽입됨.

  • 공격 시나리오:

    email = "' OR '1'='1"
    # 쿼리: SELECT * FROM users WHERE email = '' OR '1'='1'

    모든 사용자를 반환해 인증을 우회할 수 있음.

  • 더 심각한 공격:

    email = "'; DROP TABLE users; --"
    # 쿼리: SELECT * FROM users WHERE email = ''; DROP TABLE users; --

    users 테이블 전체가 삭제됨.

  • 중요성: SQL 인젝션은 OWASP Top‑10에 포함된 심각한 취약점으로, 데이터 유출·손실·전체 시스템 장악으로 이어질 수 있습니다.

  • 수정 (파라미터화된 쿼리):

    def get_user_by_email(email):
        query = "SELECT * FROM users WHERE email = %s"
        return database.execute(query, (email,))
  • 왜 안전한가: 데이터베이스가 파라미터를 데이터로 처리해 특수 문자를 자동으로 이스케이프합니다.

  • 절대 사용 금지: f‑string, % 포맷, 문자열 연결을 통한 SQL 생성. 대신 파라미터화된 쿼리나 ORM(User.objects.filter(email=email))을 사용하세요.

사례 3: 성능 문제 (JavaScript)

입력

function getActiveUsers() {
  const users = fetch('/api/active')
    .then(res => res.json())
    .then(data => data.filter(u => u.active))
  return users
}

AI 분석

  • 문제: 함수가 Promise를 반환하지만 호출자는 배열이라고 착각할 수 있음.

  • 잘못된 점: await이나 적절한 처리 없이 반환하면, 이후 코드가 즉시 users를 순회하려 할 때 오류가 발생하거나 예기치 않은 동작을 보일 수 있음.

  • 수정: 함수 자체를 async로 만들고 await을 사용하거나, 반환값이 Promise임을 명확히 문서화합니다.

    async function getActiveUsers() {
      const res = await fetch('/api/active')
      const data = await res.json()
      return data.filter(u => u.active)
    }
  • 성능 메모: await을 사용하면 체인형 .then()보다 가독성이 높아지고 오류 처리가 간단해집니다.


AI 기반의 컨텍스트 인식 분석을 활용하면 전통적인 린터, 타입 검사기, 테스트, 인간 리뷰를 통과하는 버그들을 잡아낼 수 있습니다. 이는 궁극적으로 더 신뢰성 높고, 안전하며, 성능 좋은 소프트웨어를 제공하는 데 기여합니다.

Back to Blog

관련 글

더 보기 »