3.SW개발/AI코딩과실무전략

8편. AI 코드 속 나쁜 습관 – 자동화된 안티 패턴의 실태

쿼드큐브 2025. 11. 9. 14:55
반응형
반응형

8편. AI 코드 속 나쁜 습관 – 자동화된 안티 패턴의 실태

 

📚 목차
1. 돌아가기만 하는 코드 vs 실무에 맞는 코드
2. AI가 자주 만들어 내는 실무 비추 패턴
3. 오픈소스에서 따라 배운 나쁜 습관들
4. 실무에서 문제가 되는 AI 코드 사례
5. 왜 이런 코드가 만들어지는가? - 학습 데이터의 문제
✔ 마무리 - 실무 개발자가 AI 코드에 대해 가져야 할 기준

 

AI가 망치는 베스트 프랙티스를 표현한 삽화 이미지
AI가 망치는 베스트 프랙티스를 표현한 삽화 이미지

코드 자동화가 빠르게 일상화되면서, 개발자는 더 많은 일을 더 짧은 시간 안에 처리할 수 있게 되었습니다.

하지만 눈에 띄지 않게 번지고 있는 문제가 하나 있습니다. 바로, AI가 그럴듯한 코드 속에 ‘나쁜 습관’까지 함께 자동화하고 있다는 사실입니다.

 

전역변수 남용, 중복 코드, 의미 없는 변수명, 레거시 문법 등… 분명 실무에서는 지양해야 할 패턴들이 AI가 짜주는 코드 속에 무심코 들어가고 있습니다. 그 이유는 단순합니다. AI는 ‘좋은 코드’를 만드는 것이 아니라, ‘많이 쓰인 코드’를 흉내 내는 것이기 때문입니다.

 

이 글에서는 AI 코드가 자주 담고 있는 대표적인 비추 패턴들을 짚고, 그 근본 원인이 어디서 비롯되었는지 살펴봅니다. 그리고 개발자로서 우리가 어떤 태도와 기준을 가져야 하는지 실무 관점에서 풀어보려 합니다.

 

1. 돌아가기만 하는 코드 vs. 실무에 맞는 코드

AI가 생성하는 코드는 대부분 “정답처럼 보이는 코드”입니다. 문법적으로 맞고 실행도 잘 됩니다. 하지만 실무에서 중요한 건 단순히 작동하는 코드가 아닙니다.

 

좋은 코드는 다음을 만족해야 합니다:

🔹 시간이 지나도 유지보수가 쉽고

🔹 팀원들이 쉽게 이해할 수 있으며

🔹 요구사항이 바뀌었을 때 유연하게 대응할 수 있어야 합니다


예제를 보겠습니다.

def calc(op, a, b):
    if op == '+':
        return a + b
    if op == '-':
        return a - b
    if op == '*':
        return a * b
    if op == '/':
        return a / b

이 코드는 요청한 기능을 수행하며, 문법적으로도 문제없습니다. 그러나 다음과 같은 근본적인 한계를 가지고 있습니다

🔹연산 분기가 하드코딩되어 있어 연산자 추가 시 코드 수정이 불가피합니다.
🔹b = 0일 때의 예외 상황 처리가 없습니다.
🔹op에 잘못된 입력이 들어올 경우 사용자에게 적절한 피드백이 없습니다.
🔹모든 로직이 하나의 함수에 밀집되어 있어 테스트와 재사용이 어렵습니다.

이러한 코드는 “돌아는 가지만” 실무에서는 유지보수 리스크가 큰 코드로 간주됩니다.

반면 실무에서 바람직한 구조는 다음과 같습니다:

class Calculator:
    def __init__(self):
        self.operations = {
            '+': lambda a, b: a + b,
            '-': lambda a, b: a - b,
            '*': lambda a, b: a * b,
            '/': self.safe_divide
        }

    def safe_divide(self, a, b):
        if b == 0:
            raise ValueError("0으로 나눌 수 없습니다.")
        return a / b

    def calculate(self, op, a, b):
        if op not in self.operations:
            raise ValueError(f"지원하지 않는 연산자입니다: {op}")
        return self.operations[op](a, b)

이 코드는 구조적으로 다음과 같은 실무적인 장점을 갖습니다:
🔹연산 로직이 딕셔너리 기반으로 추상화되어 있어 연산 추가가 쉽고, 기존 코드를 건드리지 않아도 됩니다.
🔹예외 처리가 포함되어 있어 오류 발생 시 안정적으로 대응합니다.
🔹클래스 기반 설계로 기능이 분리되어 있어 테스트 작성이 쉬우며, 코드 재사용성과 확장성이 높습니다.
🔹의도가 명확하게 드러나 가독성이 좋고, 협업 환경에 적합합니다.

 

📌 좋은 코드는 무엇이 다른가?

‘좋은 코드’는 단순히 정답이 아니라 시간과 협업을 견디는 코드입니다. 다음은 실무 개발자들이 중요하게 여기는 좋은 코드의 기준입니다:

기준 설명
가독성 코드만 보고도 기능과 의도를 쉽게 이해할 수 있어야 함
유지보수성 기능 수정이나 확장이 쉽게 가능한 구조여야 함
재사용성 중복을 줄이고 범용적으로 활용 가능한 구성
예외 대응 실패 시 상황에 맞는 에러 처리 및 안내가 되어야 함
테스트 가능성 단위 테스트 작성이 쉽고, 자동화가 가능한 형태

AI는 요청한 기능을 빠르게 구현해 주는 훌륭한 도구이지만, 생성된 코드가 “좋은 코드인지 아닌지”는 스스로 판단하지 못합니다.
‘잘 작동하는 코드’와 ‘잘 만들어진 코드’는 분명히 다릅니다.

좋은 코드를 만들기 위해서는 여전히 개발자의 설계 역량과 비판적 판단이 필수적입니다.


AI는 초안을 만들고, 개발자는 그것을 ‘좋은 코드’로 다듬는 전문가가 되어야 합니다.
그것이 AI 시대에도 살아남는 개발자의 조건입니다.

 

2. AI가 자주 만들어내는 실무 비추 패턴

AI가 만들어주는 코드는 실행 가능성에 초점을 맞추다 보니, 정석적이지 않거나 장기적으로 유지보수가 어려운 코드 패턴을 그대로 담는 경우가 많습니다.

여기서는 실무에서 피해야 할 대표적인 비추 패턴들을 살펴보겠습니다.

 

🔷 전역 변수(global variable) 남용

counter = 0

def increase():
    global counter
    counter += 1

전역 변수는 여러 함수 또는 모듈에서 동시에 접근하거나 변경될 수 있어 의도하지 않은 사이드 이펙트가 발생하기 쉽습니다.

특히 규모가 커질수록 디버깅과 테스트가 어려워지고, 상태 관리가 복잡해집니다.


개선안

class Counter:
    def __init__(self):
        self.count = 0

    def increase(self):
        self.count += 1

상태는 가급적 클래스 내부 변수로 캡슐화하거나 함수 인자로 넘겨주어야 합니다.

 

🔷 중복된 DB 연결/쿼리 코드

def get_user_name():
    conn = sqlite3.connect("db.sqlite")
    cur = conn.cursor()
    cur.execute("SELECT name FROM users")
    return cur.fetchall()

def get_user_email():
    conn = sqlite3.connect("db.sqlite")
    cur = conn.cursor()
    cur.execute("SELECT email FROM users")
    return cur.fetchall()

같은 로직이 반복되면 변경이 발생했을 때 모든 위치를 수정해야 하며, 실수로 하나만 빠뜨려도 버그로 이어질 수 있습니다.

이런 중복은 코드 유지보수성과 일관성을 떨어뜨립니다.

 

개선안

def get_user_field(field):
    conn = sqlite3.connect("db.sqlite")
    cur = conn.cursor()
    cur.execute(f"SELECT {field} FROM users")
    return cur.fetchall()

중복된 코드를 함수로 추출하여 재사용 가능한 모듈로 분리해야 합니다.

 

🔷 의미 없는 변수명 사용

def calc(x, y, z):
    return x + y * z

변수명이 x, y, z처럼 추상적이면 코드의 목적과 의미를 파악하기 어렵습니다.

특히 협업 상황에서는 다른 개발자가 코드를 읽는 데 시간과 오해가 발생할 수 있습니다.

 

개선안

def calc_total_price(quantity, price_per_unit, discount_rate):
    return quantity + price_per_unit * discount_rate

의미 있는 변수명을 사용하여 코드 자체가 설명서처럼 읽히도록 작성해야 합니다.

 

🔷 매직 넘버(magic number) 하드코딩

if user_age > 65:
    return "Senior"

코드 내에서 숫자나 문자열을 직접 사용하는 경우, 해당 값의 의미를 이해하기 어렵고, 다른 곳에서 수정해야 할 때 일관되게 관리하기 어렵습니다.

이른바 "매직 넘버"는 유지보수에 큰 걸림돌이 됩니다.

 

개선안

SENIOR_AGE_THRESHOLD = 65

if user_age > SENIOR_AGE_THRESHOLD:
    return "Senior"

해당 값을 상수로 정의하고, 이름을 통해 의미를 명시적으로 표현해야 합니다

이렇게 하면 가독성이 향상되고, 기준 변경 시 수정도 훨씬 간편해집니다.

 

이러한 비추 패턴들은 AI가 학습한 코드에서 자주 등장하며, 실제로 실무에서도 초보 개발자들이 흔히 저지르는 실수이기도 합니다.

따라서 AI가 제공한 코드를 무비판적으로 사용할 것이 아니라, 설계 원칙과 코드 품질 관점에서 반드시 점검하고 리팩토링하는 노력이 필요합니다.

 

3. 오픈소스에서 따라 배운 나쁜 습관들

AI는 대규모 언어 모델의 특성상 GitHub, Stack Overflow, 블로그 등에서 수집된 방대한 코드 데이터를 바탕으로 학습합니다.

그러나 오픈소스라고 해서 항상 모범적인 코드만 존재하는 것은 아닙니다.

오히려 실제 데이터에는 다음과 같은 비정형적이고 비실용적인 코드들도 대량으로 포함되어 있습니다.

 

🔷 1) 실습용 예제 코드: 보안과 설계는 생략된다

튜토리얼이나 블로그에 자주 등장하는 실습용 코드는 초보자에게 개념을 전달하는 데는 유용하지만, 실제 서비스 수준에서는 매우 위험한 코딩 방식입니다.


예를 들어 아래는 로그인 로직을 설명하기 위한 흔한 예제입니다

# 단순한 로그인 예제
def login(user_input):
    if user_input == "admin123":
        return "Access granted"
    return "Access denied"

🔹 비밀번호가 코드에 하드코딩되어 있음
🔹 사용자 입력에 대한 검증 로직이 없음
🔹 보안 없이 문자열 비교만으로 인증 처리

실습 목적이라면 충분할 수 있지만, 이런 코드가 AI의 학습 데이터로 다수 포함되면 AI는 이를 ‘표준’처럼 학습하게 됩니다.

 

실전에서는 이렇게 써야 합니다

from werkzeug.security import check_password_hash

def login(username, password):
    user = db.get_user(username)
    if user and check_password_hash(user.password_hash, password):
        return True
    return False

이 코드는 다음과 같은 보안 요소를 포함하고 있습니다:

🔹사용자 입력에 대한 검증 절차
🔹비밀번호에 대한 해시 기반 검증
🔹사용자 정보를 조회하는 데이터베이스 연동

 

🔷 2) 레거시 코드: 낡은 문법과 API가 여전히 생성된다

AI가 사용하는 학습 데이터는 수년간 축적된 오픈소스 코드입니다.
이 중 상당수는 Python 2 시절의 문법이나, 이미 사용 중단된(deprecated) API를 여전히 포함하고 있습니다.


예를 들어, 아래는 AI가 아직도 종종 생성하는 코드입니다

# Python 2 스타일의 print 문법
print "Hello, world"

 

혹은 다음과 같은 deprecated API 사용 예시도 있습니다

import imp
imp.reload(my_module)

imp 모듈은 Python 3.4 이후 폐지되었으며 importlib을 사용해야 합니다.

 

현대적이고 유지보수 가능한 코드 예

from importlib import reload
reload(my_module)

이러한 차이는 사소해 보일 수 있지만, 신규 개발자에게 잘못된 스타일을 학습시키고, 실무 환경에서는 경고 또는 에러를 발생시킬 수 있습니다.

 

🔷 3) 논문 구현 코드: "결과만 나오는 코드"

AI 알고리즘 관련 GitHub 저장소를 살펴보면, 다음과 같은 특징을 가진 코드들이 많습니다:

🔹함수 이름이 test1(), run_exp()처럼 의미가 없음
🔹변수명이 x, x1, x2, x3처럼 일관되지 않음
🔹하드코딩된 경로, 파라미터, 파일 이름
🔹전역 변수 사용
🔹예외 처리 거의 없음

def train():
    a = np.load('data.npy')
    b = model(a)
    np.save('output.npy', b)

이러한 코드는 논문 결과 재현에는 충분할 수 있지만, 재사용성과 유지보수성은 전혀 고려되어 있지 않습니다.

그런데도 AI는 이를 '실제 운영 코드'처럼 학습할 수 있습니다.

 

실전에서는 다음과 같은 구성이 필요합니다.

🔹명확한 함수 분리
🔹파라미터화
🔹로깅/예외 처리
🔹폴더 구조 정리

 

🔷 4) 개인 실험 코드: 설계 원칙 없는 코드도 학습된다

많은 GitHub 저장소는 개인 실험, 사이드 프로젝트, 과제 등을 위해 작성된 코드입니다.

이들 코드의 공통적인 특징은 다음과 같습니다:

🔹함수 하나에 수십 줄이 몰려 있음
🔹MVC나 레이어드 구조 없이 파일 하나에 모든 로직 포함
🔹if __name__ == '__main__': 블록 없이 실행 흐름이 섞여 있음
🔹전역 import, 하드코딩 다수

import requests

url = "https://my-api.com/data"
r = requests.get(url)
print(r.text)

이런 코드가 쌓이고, AI가 “사용률이 높다”고 판단하면, 설계 원칙 없는 코드가 반복 생성되는 구조가 됩니다.

 

📌 개발자 실천 팁

🔹AI에게 코드를 요청할 때 "최신 문법", "보안 고려", "리팩토링된 구조" 등의 조건을 명시하세요.
🔹AI가 만든 코드는 반드시 리뷰하고 리팩토링하는 습관을 들이세요.
🔹black, flake8, mypy, bandit 같은 도구로 자동 분석을 병행하세요.
🔹자주 쓰는 템플릿/패턴은 직접 작성해 AI에게 학습시키거나 피드백을 주는 것도 방법입니다.

반응형

 

4. 실무에서 문제가 되는 AI 코드 사례

AI가 생성한 코드를 그대로 도입했다가 실무에서 문제가 될 수 있는 사례는 생각보다 많습니다.

특히 글로벌 상태 관리나 중복 코드 작성처럼, 언뜻 보기엔 문제가 없어 보이지만 실전에서 예기치 않은 버그나 유지보수 비용 증가로 이어지는 경우가 많습니다.

 

✔️ 사례 1 – 전역 변수 사용으로 인한 상태 공유 버그

GPT가 생성한 Flask API 예제 중 일부는 다음과 같이 전역 리스트를 사용해 사용자를 추가하도록 구성되어 있었습니다:

users = []

@app.route('/add', methods=['POST'])
def add_user():
    users.append(request.json['name'])

이 코드는 단순한 로직 테스트용으로는 충분히 작동하지만, 실무 환경에서는 다음과 같은 문제가 발생합니다.

🔹전역 변수(users)가 모든 요청 간에 공유됨
🔹서버가 리로딩되기 전까지 사용자 데이터가 계속 메모리에 유지됨
🔹여러 테스트 케이스에서 서로의 데이터를 침범하는 현상 발생

 

결과적으로

테스트 환경에서 예기치 않게 이전 사용자의 데이터가 유지되며, 사용자 데이터 간 충돌이 발생합니다.

프로덕션 환경에서는 서버 인스턴스 수에 따라 동작이 달라지는 불안정한 상태 공유 문제로 이어질 수 있습니다.

 

개선 프롬프트 예시

"Flask API에서 사용자 목록을 전역 변수 대신 세션이나 데이터베이스를 활용하여 안전하게 관리하는 코드를 작성해 줘.
테스트 환경에서도 요청 간 상태가 공유되지 않도록 구현해 줘."

 

✔️ 사례 2 – 코드 중복으로 인한 유지보수 폭증

GPT를 활용해 “각 부서별 보고서 생성 함수”를 작성한 한 팀은, 아래와 같이 각 보고서 유형별로 함수를 반복 생성했습니다:

def generate_sales_report():
    # ...

def generate_finance_report():
    # ...

def generate_hr_report():
    # ...

코드는 처음엔 빠르게 작성되고 동작도 잘했지만, 공통 양식 변경 요청이 들어오면서 문제가 본격화됐습니다.
🔹총 20개 이상의 보고서 함수가 동일한 구조를 갖고 있었음
🔹양식 변경 사항을 전부 수작업으로 수정해야 했고,
🔹변경 누락, 포맷 오류 등 부작용도 동반됨

 

결과적으로

단순한 구조였지만 리팩토링 없이 AI가 제시한 코드를 그대로 도입한 탓에, 단일 변경 요청에 수일이 소요되고 전체 코드의 일관성도 크게 훼손됩니다.

 

개선 프롬프트 예시

"다양한 부서 보고서를 생성하는 공통 함수 하나를 작성해 줘.
부서 이름과 데이터를 매개변수로 받아서, 재사용 가능하고 유지보수하기 쉬운 구조로 작성해 줘."

 

이처럼 AI가 제시한 ‘그럴듯한 구조’는 실제 운영 환경에 맞춰 검증되지 않으면 문제의 원인이 되기도 합니다.

개발자는 코드를 받아들이기 전에 반드시, “이 구조는 확장 가능한가?”, “변경 가능성이 있는가?”, “테스트와 협업에 적합한가?”를 함께 고려해야 합니다.

 

📌 AI에게 좋은 코드 구조를 요청하는 전략

🔹 “간단하게 만들어줘” 대신 → “확장성과 재사용성을 고려해 작성해 줘”

🔹 “돌아가기만 하면 돼” 대신 → “보안/테스트/유지보수 가능한 구조로 만들어줘”

🔹 함수 명세 제공 → 어떤 입력, 어떤 출력, 어떤 조건을 명확히 전달

🔹 요구사항 명확히 → 전역 변수 사용 여부, 하드코딩 금지, 중복 제거 등 조건 포함

 

5. 왜 이런 코드가 만들어지는가 – 학습 데이터의 문제

AI는 윤리, 설계 원칙, 베스트 프랙티스를 스스로 이해하거나 판단하지 못합니다.

AI가 학습하는 것은 코드가 왜 그렇게 작성되었는가가 아니라, 어떤 코드가 자주 등장하는가입니다.


즉, AI가 생성하는 코드는 “좋은 코드”라기보다 “자주 등장한 코드 패턴”의 결과물일 가능성이 높습니다.
그렇기 때문에, 실무 기준에서 부적절하거나 위험한 코드조차 빈도가 높다는 이유로 학습되고, 결과적으로 AI는 그릇된 패턴을 “정답”처럼 제시할 수 있습니다.


이로 인해 개발자는 다음과 같은 인식을 갖고 접근해야 합니다:

 

🔹AI 코드 품질은?

학습에 사용된 코드의 품질에 직접적으로 의존합니다. 데이터가 좋지 않으면 결과물도 신뢰하기 어렵습니다.

🔹AI가 스스로 개선하는가?

불가능합니다. 외부에서 명확한 기준과 검증 절차를 제공해야만 개선이 가능합니다.

🔹개발자의 역할은?

AI가 생성한 코드를 선별하고 필터링하는 감시자 역할을 해야 합니다.

단순히 받아들이는 것이 아니라, 그 코드가 실무에서 적절한지 판단하고 조정할 책임이 있습니다.

 

결국, AI의 코딩 능력은 ‘코드를 얼마나 많이 학습했느냐’보다 ‘어떤 코드를 학습했느냐’에 달려 있습니다.

그리고 그 코드의 품질을 판단하고, 필요한 수정을 가하는 것은 전적으로 개발자의 몫입니다.

 

✔ 마무리 - 실무 개발자가 AI 코드에 대해 가져야 할 기준

AI는 자동화된 초안 생성기일 뿐, 완성된 결과물이 아닙니다.
‘AI가 만들었으니 괜찮겠지’라는 생각은 실무에서 가장 위험한 태도입니다.

실제 현업에서는 다음과 같은 기준이 필요합니다:
🔹 이 구조는 확장 가능한가?
🔹 변경 요청이 왔을 때 쉽게 수정 가능한가?
🔹 다른 사람이 봐도 이해하기 쉬운가?
🔹 보안, 예외 처리, 테스트 관점이 반영되어 있는가?

AI 시대의 개발자는,

코드 생성기 앞에 있는 ‘마지막 품질 게이트’입니다.
AI가 생성한 코드를 단순히 받아들이는 것이 아니라, 실무 환경에 적합하도록 판단하고 개선하는 전문가적 태도가 필수입니다.

 

※ 게시된 글 및 이미지 중 일부는 AI 도구의 도움을 받아 생성되거나 다듬어졌습니다.

 

반응형
반응형