DS와 MLE가 함께 일하는 법

DS와 MLE가 함께 일하는 법

김경윤 · 토스뱅크 ML Engineer
2026년 8월 3일

안녕하세요, 토스뱅크 Product Foundation ML Service Team, ML Engineer 김경윤입니다.

1년 전, 이런 일이 있었어요. DS(Data Scientist)가 노트북에서 모델을 다 만들었습니다. 정확도도 잘 나오고, 노트북에서는 완벽하게 돌았죠. 그래서 그 코드를 서빙으로 옮기려고 받았는데, 실행이 안 됐어요. 모델이 쓰는 라이브러리랑 설정 파일이 여기저기 흩어져 있어서 "이 파일도 필요해요, 저것도요"를 며칠 동안 주고받았습니다.

문제는 이게 한 번으로 끝나지 않았다는 겁니다. 새 모델을 올릴 때마다 비슷한 일이 반복됐어요.

ML 모델 하나가 서비스에 올라가려면, 모델을 만들고, 그 모델을 서빙하는 과정을 거치는데요. 보통 이 작업을 두 사람이 나눠서 합니다. 모델을 만드는 DS와, 그 모델을 서비스로 굳히는 MLE(ML Engineer)예요. 그런데 진짜 어려운 건 모델 자체가 아니라, 이 두 사람이 일을 어떻게 나누느냐였어요. 어디까지가 DS 몫이고, 어디부터가 MLE 몫일까요? 누가 어디까지 할지가 흐릿하면, 같은 모델을 두고 협업이 매끄럽지 않습니다.

저희 팀은 1년 동안 이 일 나누는 방식을 세 번 다시 다듬었습니다. 오늘은 그 과정을 하나씩 정리해 볼게요.

왜 두 직군의 협업이 매끄럽지 않았을까요?

토스뱅크는 다양한 분야에서 ML 서비스를 제공하고 있어요. 신분증 OCR 및 사후검증(관련글), 상담 음성을 텍스트로 바꾸는 STT, 문서를 읽는 OCR, 아이 얼굴 생성까지요. 서비스는 계속 늘어나는데, 새 모델을 올릴 때마다 DS와 MLE의 협업 과정이 매끄럽지 않았어요.

문제의 뿌리는 하나였어요. DS가 모델링 과정에서 작성했던 코드와, MLE가 비즈니스 로직과 함께 서빙에 알맞게 재작성했던 코드 사이에 명확한 약속이 없었던 거예요. 이 약속을 어떻게 잡았느냐에 따라 협업의 질이 완전히 달라졌어요.

그 과정을 세 단계(Phase)로 나눠서 볼 수 있어요. 각 단계에서 DS와 MLE가 일을 어디서 나눴는지를 같이 살펴볼게요.

Phase 0: DS가 전달하면, MLE가 처음부터 다시

처음엔 일을 나누는 지점이 사실상 사람과 사람 사이에 있었어요.

DS는 주피터 노트북에서 모델을 학습하고, 추론(Inference, 학습된 모델로 예측을 뽑는 과정) 코드까지 노트북에 다 적어요. 그 노트북을 MLE에게 통째로 전달하면, MLE가 그걸 보고 서빙 코드를 처음부터 다시 작성했어요. 손에서 손으로 전달하는 방식이었죠.

앞에서 말한 ‘실행이 안 되던’ 일도 바로 여기서 나왔어요.

그때 이런 대화가 오갔어요 - 받는 쪽에서 "이 파일도 필요해요, 저 라이브러리 소스도 주세요"를 하나하나 다시 요청했어요. - 결국 "구동이 확인된 버전 그대로 통째로 전달해달라"는 말이 오갔죠.

노트북에서 도는 것과, 다른 사람 손에서 그대로 재현되는 것은 완전히 다른 문제였습니다. 이 ‘전달받아 맞추기’에만 많은 시간이 들었어요.

일을 나누는 기준이 코드에 있지 않고 사람 사이에 있으니, 협업이 매끄럽지 않았습니다.

초기에는 역할을 딱 나누니까 이 방식이 오히려 빨랐습니다. 그런데 모델이 5개, 6개로 늘면서 이 방식으로는 감당이 안되기 시작했어요.

Phase 1: 일을 .py 파일에서 나누기

그래서 일 나누는 지점을 사람 사이에서 파일로 내렸습니다. DS가 노트북에 다 적지 말고, 핵심 추론 로직만 .py 파일로 따로 빼는 거예요.

노트북은 학습과 실험에만 집중하고, 실제 ML 로직은 .py 파일에 담았습니다. 이 파일은 MLE 리뷰와 CI(코드를 자동으로 검증하는 파이프라인)를 꼭 통과하게 했죠. 확실히 나아졌어요. 소통 비용이 줄고, DS가 의도한 모델이 그대로 유지됐습니다.

하지만 이 방법도 최선의 정답은 아니었어요.

실제로 이런 일이 있었어요 - 한 모델의 노트북 코드에는 공용 추론 라이브러리의 전역 설정을 바꾸는 한 줄이 들어 있었어요. 노트북에서는 그 모델 하나만 도니까 아무 문제가 없었죠. - 그런데 서빙은 달라요. 여러 모델이 한 프로세스 안에 함께 올라가요. 이 한 줄을 그대로 옮기면, 같은 라이브러리를 쓰는 다른 서빙 모델까지 영향을 받을 수 있었어요.

파일로 나눈 것은 맞았습니다. 하지만 그 파일이 ‘어떤 모양이어야 하는지’는 정하지 않아서, 문제가 남아 있었죠.

파일의 모양을 약속으로 정해두지 않으면, 결국 옮길 때마다 여전히 시간이 소요된다는 것을 깨달았어요.

Phase 2: 일을 '약속(인터페이스)'으로 나누기

그래서 마지막으로, 일 나누는 기준을 인터페이스로 바꿨습니다. 인터페이스는 "이 코드는 이런 모양이어야 한다"는 약속이에요. 이 약속을 코드로 정해두면 DS와 MLE의 역할 구분이 말이 아니라 계약이 됩니다.

핵심은 commons-ml-model이라는 패키지인데요. 여기에는 ‘모델이라면 이런 모양이어야 한다’는 추상화 클래스가 들어 있어요. 추상화 클래스에는 전처리, 추론, 후처리 메서드가 있습니다.

이제 협업 방식이 확 달라집니다.

DS는 전처리, 추론, 후처리 등 인터페이스의 구현체를 작성해 하나의 패키지로 만들고, MLE는 그 패키지를 설치해 서비스에 올립니다. 코드를 손으로 옮기지 않고, 라이브러리처럼 pip install로 가져다 쓰는 거예요.

추상화 클래스의 인터페이스 3개가 곧 ‘계약서’

이게 협업 관점에서 가장 중요한 부분입니다. 추상화 클래스는 전처리, 추론, 후처리의 겉을 감싸고, DS는 그 안의 인터페이스를 구현해요. 그리고 감싼 부분에서 로그, 메트릭, 추적을 자동으로 남깁니다.

class BaseInference:
    def run(self, input):
        trace_id = start_trace()          # 요청 추적 시작
        timer = start_timer()
        log.info("추론 시작", trace_id=trace_id)
        
        # DS가 구현하는 인터페이스들
        pre = self.pre_process(input)
        out = self.inference(pre)
        post = self.post_process(out)
        
        record_metric("inference_seconds", timer.elapsed())
        log.info("추론 완료", trace_id=trace_id, elapsed=timer.elapsed())
        return post

이 코드가 바로 DS와 MLE의 계약서예요.

인터페이스(pre_process, inference, post_process)는 DS 몫이라, 모델이 어떻게 동작하는지에만 집중하면 됩니다. 그 이외의 로그, 메트릭, 추적은 MLE 몫인데, 추상화 클래스가 자동으로 처리해주기 때문에 DS는 신경 쓸 필요가 없어요.

누가 무엇을 할지가 코드에 정해지니, 협업이 이렇게 달라졌어요.

DS는 "로그를 어떻게 남기지?"를 고민하지 않아도 되고, MLE는 "이 모델은 함수 이름이 뭐지?"를 물어볼 필요가 없어요. 새 관측 항목을 넣고 싶을 때도 추상화 클래스 한 곳만 고치면 모든 모델에 한 번에 적용됩니다.

실제로 이런 변화가 있었어요 - 예전에는 모델 하나를 서빙으로 옮기는 데 며칠씩 걸렸어요. - 지금은 DS가 패키지를 올리면, MLE는 pip install 한 줄로 가져다 씁니다. - 실제로 새로 만든 서비스 하나는 첫 커밋을 찍은 날에 배포까지 마쳤어요. 앞에서 며칠씩 소요되던 ’전달받아 맞추기’가 사라진 순간입니다.

같은 레포에서 함께 리뷰하기

역할을 코드로 정했으면, 이제 어디서 함께 작업하느냐가 남습니다. 저희는 DS가 만든 모델 패키지들을 한 레포에서 함께 관리하기로 했어요.

이렇게 여러 패키지를 한 저장소에서 관리하는 방식을 모노레포(Mono Repository)라고 합니다. uv라는 도구의 워크스페이스 기능으로 묶여 있어요. DS와 MLE가 함께 일하기에 이 구조가 좋았습니다.

물론 대가도 있어요.

공통 패키지를 건드리면 그 위 모델이 모두 영향권에 들기 때문에 더 신중해야 하고, 모델이 쌓일수록 레포가 무거워져 빌드도 느려졌어요. 그래서 패키지 매니저를 poetry에서 uv로 바꿔 빌드 속도를 3~5배 개선했습니다.

AI가 작성한 코드, 스타일까지 통일하기

여기까지가 ‘역할 나누기’를 맞추는 이야기였습니다. 그런데 최근에 새로운 종류의 협업 문제가 생겼어요.

요즘은 DS도 MLE도 AI로 코드를 작성해요. 예전에는 사람이 직접 작성하니까 팀 안에서 코드 스타일이 어느 정도 자연스럽게 수렴했어요. 그런데 각자 AI로 작성하니까, 같은 기능인데 코드 모양이 제각각이 됐습니다.

누구는 예외 처리를 이렇게, 누구는 저렇게 하고, 누구는 고정된 값을 문자열로 쓰고 누구는 enum으로 쓰다 보니, 리뷰할 때마다 "이건 우리 팀 방식이 아닌데"가 반복됐어요.

역할 나누기는 인터페이스로 맞췄지만, 코드의 결까지 맞춰지진 않았던 것입니다. 그래서 팀 코딩 컨벤션을 AI가 따르도록 하는 규칙 모음, pfmls-stylepack을 도입했어요.

Hook을 활용해서 네이밍, 예외 처리 방식, enum 쓰는 기준 같은 팀 컨벤션을 AI가 코드를 작성할 때부터 따르게 했고, 규칙을 적용한 자리에는 "여기 이 규칙 때문에 이렇게 작성했어요"라는 표시를 남겨 리뷰어가 왜 이렇게 작성했는지 바로 알 수 있게 했습니다.

협업을 이렇게 두 층위로 나눠서 보게 됐습니다.

하나는 구조 통일로, 인터페이스로 '무엇을 어디까지 하나'를 맞추는 것이고, 다른 하나는 스타일 통일로, pfmls-stylepack으로 "코드를 어떤 방식으로 작성하나"를 맞추는 것입니다.

둘 다 맞춰야 리뷰가 진짜 매끄러워집니다. 인터페이스만 같고 코드 결이 제각각이면, 리뷰에서 또 어긋나거든요.

협업하며 배운 것들

아래 내용은 DS와 MLE가 일을 나누는 방식을 세 번 다듬으면서 배운 것들입니다.

1️⃣ 일을 어디서 나눌지가 제일 어려워요

전체적인 구조를 너무 엄격하게 만들면, DS가 커스터마이징 하기 어려울 수 있습니다. 반대로 너무 느슨하게 만들면, 다시 인터페이스가 제각각이 돼요. 이 사이의 적절한 지점을 찾는게 중요합니다.

2️⃣ 러닝 커브가 있을 수 있어요

전체적으로 약속을 이해해야 모델 패키지를 만들 수 있습니다. 그래서 가이드 문서를 초기에 바로 만들고, 첫 모델은 DS와 MLE가 함께 페어로 작업했어요. 이후에는 기존 패키지가 좋은 참고 코드가 됐습니다. 그걸 참고하니 DS가 더 적은 러닝 커브로 작업할 수 있었어요.

3️⃣ 공통 라이브러리를 공유하는 구조는 양날의 검이에요

공통 라이브러리를 한 번 업데이트하면 같은 모노레포에 있는 다른 패키지들에 영향을 줍니다. 하지만 뒤집으면, 공통 로직을 한 곳만 고쳐도 모든 패키지에 반영된다는 뜻이죠.

4️⃣ 최근에는 사람 뿐만 아니라 AI와의 협업 방식도 중요해요

인터페이스를 맞추는 것과 코드 스타일을 맞추는 건 다른 문제입니다. AI로 작성하는 시대에는 스타일을 맞추는 협업이 더 중요해졌어요.

마무리

1년 전에는 DS가 노트북을 통째로 전달하고, MLE가 그걸 처음부터 다시 작성했습니다. 지금은 DS가 정해진 인터페이스를 구현해 패키지를 올리면, MLE가 설치해서 서비스에 올립니다. 나머지는 추상화 클래스가 알아서 처리하죠.

그리고 AI로 코드를 작성하게 되면서, 역할 나누기를 넘어 코드 스타일까지 맞추는 게 새로운 협업 과제가 됐습니다.

재미있는 건, 협업 방식이 진화할수록 DS와 MLE의 역할이 더 섞이는 동시에 더 뚜렷해진다는 거예요. 하는 일은 섞이는데, 누가 책임지는지는 더 또렷해져요.

이렇게 DS와 MLE가 매끄럽게 함께 일하는 방식을 같이 고민하고 싶으시다면, 토스뱅크에 합류해 주세요.

감사합니다.

뉴스레터가 발행되면
이메일로 알려드릴게요
구독하기
DS와 MLE가 함께 일하는 법