백엔드 도구들, FE로 치면 무엇일까요?

이 글은 직무 통합 과정에서 백엔드 실무를 맡게 된 프론트엔드 개발자가 실제 작업을 부딪히며 배운 것들을 주제 단위로 정리하는 시리즈입니다. 낯선 개념을 이미 아는 개념에 대응시키는 방식으로 씁니다.
첫 PR을 올리기까지 코드보다 도구와 싸운 시간이 길었습니다. 커밋하면 처음 보는 린터가 막아서고, 빌드 파일은 어디를 고쳐야 하는지 모르겠고, 테스트를 돌리려니 Docker를 켜라고 합니다. 그런데 하나씩 정체를 알고 보니 전부 프론트엔드에서 매일 쓰던 것들의 다른 이름이었습니다 — 몇 개의 예외만 빼고. 그 예외가 오히려 백엔드라는 직무의 정체를 알려줬습니다.
백엔드의 도구들은 FE의 무엇에 대응하고, 대응물이 없는 도구는 왜 없을까요?
이 글에서 다루는 내용
시리즈의 마지막 글입니다. 지금까지의 글이 개념의 대응이었다면 이번에는 도구의 대응입니다 — 코드 품질(ktlint·detekt), 빌드와 구조(Gradle 멀티모듈, 템플릿 레포, 도메인별 레포 분리), 테스트(Testcontainers)를 FE 대응물로 정리하고, 대응물이 없는 도구들이 왜 없는지를 짚습니다. 끝에는 시리즈 전체의 FE ↔ BE 대응표를 한 장으로 모았습니다.
- 1. 코드 품질 — ktlint와 detekt
- 2. 빌드와 구조 — Gradle, 템플릿, 레포 경계
- 3. 테스트 — 진짜 DB를 띄우는 Testcontainers
- 4. 대응물이 없는 도구들
- 시리즈 한 장 요약 — FE ↔ BE 대응표
- References
1. 코드 품질 — ktlint와 detekt
커밋을 막아섰던 린터의 정체는 두 가지였습니다.
| 도구 | 하는 일 | FE 대응물 |
|---|---|---|
| ktlint | 포맷 검사와 자동 수정 | Prettier |
| detekt | 코드 스멜·복잡도 정적 분석 (긴 메서드, 복잡한 조건식 등) | ESLint |
역할 분담까지 똑같아서 설명이 거의 필요 없었습니다. 흥미로웠던 것은 baseline이라는 장치입니다. detekt를 기존 코드베이스에 도입하면 위반이 수백 개씩 나오는데, 그걸 다 고치는 대신 현재 위반 목록을 스냅샷으로 얼려두고 새로운 위반만 막습니다. 레거시 코드 곳곳의 eslint-disable 주석을 파일 하나에 모아둔 것과 같은 개념입니다.
여기서 한 가지 데인 것 — baseline 항목은 코드 위치가 아니라 코드 내용을 서명으로 기억합니다. 그래서 얼려둔 코드를 조금만 고쳐도 서명이 어긋나 "새 위반"으로 살아납니다. 처음엔 당황했는데 곱씹으니 설계 의도였습니다. 그 코드를 건드린 사람이 그 빚도 갚아라. 다만 갚는 방법은 구분해야 합니다 — 남의 빚이 깨어난 것이면 baseline을 갱신할 수도 있지만, 내가 새로 만든 위반은 baseline에 얼리는 게 아니라 리팩터링으로 없애는 것이 맞습니다.
Recap
ktlint는 Prettier, detekt는 ESLint에 대응하고, baseline은 레거시 위반을 얼려두는 eslint-disable 모음입니다. baseline은 코드 내용을 서명으로 기억하므로 건드리면 빚이 깨어나며, 내가 새로 만든 위반은 얼리는 게 아니라 갚는 것이 맞습니다.
2. 빌드와 구조 — Gradle, 템플릿, 레포 경계
구조를 만드는 도구들은 이 시리즈에서 이미 여러 번 등장했습니다. 이름표만 정리하면 이렇게 됩니다.
| 도구/관습 | 하는 일 | FE 대응물 |
|---|---|---|
Gradle 멀티모듈 (settings.gradle) | 모듈 선언·의존 방향·빌드 오케스트레이션 | pnpm workspace + Turborepo |
| 스켈레톤 템플릿 레포 | 새 레포의 출발점 — 모든 레포가 같은 구조를 따름 | create-app 계열 보일러플레이트 |
| 도메인별 레포 분리 | 도메인 = 레포. 담당과 맥락의 분리 | 모노레포 패키지 경계, 마이크로프론트엔드 |
settings.gradle이 모듈들을 선언하고 모듈 간 의존이 빌드 설정에 명시된다는 것 — 1편과 2편에서 본 "빌드가 강제하는 의존 방향"의 물리적 실체가 이 파일들입니다. workspace 루트의 pnpm-workspace.yaml을 처음 열어봤을 때의 감각으로 읽으면 정확히 맞습니다.
템플릿 레포는 규모의 산물입니다. 도메인마다 레포가 늘어나는 구조에서는 "새 레포를 어떻게 시작하는가"가 반복 문제가 되고, 그 답이 모두가 따르는 출발 템플릿입니다. 모든 레포가 같은 구조라는 것은 처음 온 사람에게 큰 선물이기도 합니다 — 하나를 익히면 열을 읽을 수 있습니다.
Recap
Gradle 멀티모듈은 pnpm workspace + Turborepo, 템플릿 레포는 create-app 보일러플레이트, 도메인별 레포 분리는 모노레포 패키지 경계·마이크로프론트엔드에 대응합니다. 시리즈 내내 말한 "빌드가 강제하는 경계"의 물리적 실체가 이 빌드 설정 파일들입니다.
3. 테스트 — 진짜 DB를 띄우는 Testcontainers
테스트를 돌리려니 Docker를 켜라고 했던 이유가 이것입니다. Testcontainers는 테스트가 시작될 때 진짜 MySQL 같은 것을 Docker 컨테이너로 띄우고 끝나면 걷어 갑니다. 인메모리 가짜 DB로는 못 잡는 것들 — 실제 SQL 방언, 제약 조건, 트랜잭션 동작 — 을 실물로 검증하는 것입니다.
프론트엔드 대응물은 정확합니다 — jsdom으로 브라우저를 흉내 내는 대신 Playwright로 진짜 브라우저를 띄우는 통합 테스트. "모킹의 편함과 실물의 정직함" 사이에서 실물 쪽에 선 선택이라는 점, 대신 로컬 환경 요구사항(Docker)이 생긴다는 비용까지 같은 구조입니다.
1편에서 헥사고날 구조의 일상적 이점이 테스트 가능성이라고 했는데, 그 문장과 이 도구는 역할이 나뉩니다 — 코어의 업무 규칙은 DB 없이 빠르게 검증하고(구조가 주는 것), 어댑터와 SQL은 Testcontainers로 실물 검증합니다(도구가 주는 것). 순수한 안쪽은 가볍게, 바깥 경계는 정직하게.
Recap
Testcontainers는 테스트마다 진짜 DB를 Docker로 띄우는 도구로, jsdom 대신 Playwright로 진짜 브라우저를 띄우는 선택에 대응합니다. 코어는 구조 덕분에 DB 없이 검증하고 어댑터는 이 도구로 실물 검증하는 — 안쪽은 가볍게 바깥은 정직하게 — 역할 분담이 됩니다.
4. 대응물이 없는 도구들
정직하게 대응물이 없는 칸이 두 개 있습니다.
- 마이그레이션 도구 (Liquibase 등) — 8편에서 본 대로, 프론트엔드에는 "살아남아서 움직여야 하는 저장된 데이터"가 없기 때문입니다. (IndexedDB의 버전·업그레이드 콜백이 예외적인 친척이지만, 기기 하나의 데이터를 다룬다는 점에서 무게가 다릅니다.)
- JPA / Entity (ORM) — 객체와 테이블 사이의 매핑 계층입니다. 굳이 찾으면 API 응답 정규화 계층이 비슷한 자리에 있지만, 영속성이라는 상대가 없으니 본질적으로는 대응물이 없다고 보는 게 정확합니다.
돌아보면 이 "없음"이 백엔드라는 직무의 정체를 가장 잘 설명해 줍니다. 온보딩 내내 새로 배운 것 대부분은 사실 이미 아는 개념의 다른 이름이었습니다 — 낯선 것은 개념이 아니라 어휘였습니다. 그리고 진짜로 새로웠던 소수는 전부 한 지점으로 수렴했습니다. 살아 있는 데이터. 데이터가 남고, 움직여야 하고, 그 이력에 답해야 한다는 것. 프론트엔드와 백엔드를 가르는 선은 언어도 프레임워크도 아니고 그 책임이었습니다.
Recap
마이그레이션 도구와 ORM은 FE 대응물이 없고, 그 이유는 하나로 수렴합니다 — 프론트엔드에는 살아남아 움직여야 하는 데이터가 없습니다. 온보딩에서 낯선 것 대부분은 어휘였고, 진짜 새로운 것은 살아 있는 데이터에 대한 책임이었습니다.
시리즈 한 장 요약 — FE ↔ BE 대응표
아홉 편에 걸쳐 쌓아온 대응표의 전체판입니다.
| BE 개념 | FE에서 가장 가까운 것 | 대응의 핵심 | 다룬 글 |
|---|---|---|---|
| 도메인 모델 | React 코어 / 헤드리스 로직 | 순수한 중심, 바깥을 모름 | 1편 |
| 어댑터 | 렌더러 (react-dom 등) | 갈아끼울 수 있는 바깥 부품 | 1편 |
| 포트 (인터페이스) | 스토리지 인터페이스 계약 | 계약은 안쪽이 소유한다 | 1편 |
| 인바운드 어댑터 | 이벤트 리스너 | 세상이 나를 부른다 | 2편 |
| 아웃바운드 어댑터 | fetch / API 클라이언트 | 내가 세상을 부른다 | 2편 |
| 의존성 규칙 | 모노레포 import 방향 | 덜 바뀌는 쪽으로만 의존 | 2편 |
| 의존성 역전 (DIP) | props 콜백 | 안쪽이 선언, 바깥이 구현 | 2편 |
| 도메인 서비스 | 순수 도메인 util | 프레임워크를 모르는 업무 규칙 | 3편 |
| 애플리케이션 서비스 | 오케스트레이션 훅 | 조율만, 로직은 위임 | 3편 |
| 인가의 자리 | 라우트 가드 | 모델 밖, 흐름의 관문에서 | 3편 |
| 인터널 API | BFF의 서버 간 조립 | 클라 대신 서버가 합친다 | 4편 |
| 모델 ↔ Response 정합 | 리소스 REST vs 뷰모델 논쟁 | 같은 축의 서버판 | 5편 |
| DTO 레이어링 | 응답 타입·뷰모델·폼 상태 어댑터 | 경계마다 자기 표현 | 5편 |
| 필드 마스크 / permit | 폼의 dirty fields | 의도를 값이 아니라 목록에 | 6편 |
| 컬럼 vs JSON 컬럼 | atom 쪼개기 vs 상태 객체 | 그 단위로 일하는 쪽이 누구인가 | 7편 |
| 마이그레이션 | (대응물 없음) | 살아 있는 데이터를 옮기는 일 | 8편 |
| ktlint / detekt | Prettier / ESLint | baseline은 얼려둔 빚 | 이 글 |
| Gradle 멀티모듈 | pnpm workspace + Turborepo | 빌드가 강제하는 경계 | 이 글 |
| Testcontainers | Playwright 통합 테스트 | 흉내 대신 실물 | 이 글 |
긴 시리즈를 함께 읽어주셔서 감사합니다. 언젠가 반대 방향의 온보딩 — 백엔드 개발자를 위한 프론트엔드 개념 지도 — 을 쓰는 분이 있다면, 이 표가 거울처럼 쓰일 수 있기를 바랍니다.
References
도구 공식 문서
- ktlint — An anti-bikeshedding Kotlin linter
- detekt — Static code analysis for Kotlin
- Gradle — Structuring Projects with Gradle (multi-project builds)
- Testcontainers
