Testing & Code Quality

[코드 냄새 #2] 라자냐 코드, 파일 일곱 개를 고치는 이유

계층을 반듯하게 나눴는데도 필드 하나 추가에 파일 일곱 개를 고쳐야 하는 코드가 라자냐 코드입니다. 층이 늘어나는 이유와 비용이 발생하는 지점, 층이 필요한 경우와 걷어내는 법을 정리했습니다.

7분 읽기
[코드 냄새 #2] 라자냐 코드, 파일 일곱 개를 고치는 이유 대표 이미지

앞 편에서 본 스파게티 코드는 흐름이 얽힌 코드였습니다. 그럼 흐름을 완벽하게 정리하면 좋은 코드가 될까요.

이전 글 코드 냄새 #1에서 이어지는 내용입니다.

꼭 그렇지는 않습니다.

계층을 반듯하게 나누고, 각 계층이 아래층만 호출하도록 규칙을 세우고, 인터페이스로 경계를 그었는데도 손대기 싫은 코드가 됩니다.

문자열 필드 하나를 화면에 띄우려고 파일 일곱 개를 고쳐야 하는 코드. 이걸 라자냐 코드(Lasagna Code)라고 부릅니다(C2 원문).

층이 예쁘게 쌓인 코드

이름 그대로입니다. 라자냐는 면과 소스를 켜켜이 쌓아 만듭니다.

단면이 반듯하고 층이 또렷하죠. 문제는 그 층을 하나만 꺼낼 수가 없다는 겁니다.

전형적인 모습을 보겠습니다. 서버가 내려주는 사용자 정보에 nickname 필드가 추가됐고 화면에 표시하면 되는 일입니다.

1. UserDTO            — 서버 응답 구조체에 필드 추가
2. UserMapper         — DTO를 도메인 모델로 변환하는 코드에 한 줄
3. User               — 도메인 모델에 프로퍼티 추가
4. UserRepository     — 프로토콜 시그니처가 바뀌면 여기도
5. FetchUserUseCase   — 통과만 하는데 타입이 걸려서 수정
6. UserViewModel      — 화면용 모델로 또 변환
7. ProfileView        — 드디어 표시

DTO(Data Transfer Object, 데이터 전송 객체)에서 시작해 화면까지 오는 동안 같은 문자열이 세 번 다른 타입에 담깁니다.

일곱 파일 중 실제로 판단을 내리는 곳은 몇 군데일까요. 대개 한두 곳입니다.

나머지는 값을 그대로 옆으로 넘기는 코드입니다.

이런 계층을 통과 계층(pass-through layer)이라고 합니다.

아키텍처 패턴 문헌에서는 요청이 아무 처리 없이 층만 통과하는 상태를 싱크홀 안티패턴이라고도 부릅니다. 라자냐 코드의 핵심 증상이고요.

왜 이렇게 되나

나쁜 의도로 이렇게 만드는 사람은 없습니다. 오히려 정반대입니다.

층이 늘어나는 이유는 대부분 좋은 조언을 문맥 없이 적용했기 때문입니다.

“레이어를 분리하라”를 규칙으로 받아들인 경우. 클린 아키텍처 도식을 보고 원의 개수만큼 폴더를 만듭니다.

그 도식은 계층이 몇 개여야 한다는 규정이 아닙니다. 의존성이 안쪽으로만 향해야 한다는 원칙을 그린 것이죠(Presentation-Domain-Data Layering).

그런데 폴더 구조로 옮겨지면서 의미가 바뀝니다.

“구현이 아니라 추상에 의존하라”를 전부에 적용한 경우. 구현체가 하나뿐인 프로토콜이 잔뜩 생깁니다.

UserRepository 프로토콜과 UserRepositoryImpl 하나. 이 프로토콜이 하는 일은 코드 점프를 한 번 더 만드는 것뿐입니다.

테스트에서 가짜 구현을 끼워 넣을 목적이라면 값어치를 하지만 그런 계획이 없다면 층만 하나 늘어난 셈입니다.

“나중에 바꿀 수 있게”를 미리 준비한 경우. 데이터베이스를 바꿀지도 모르니 추상화하고 서버가 바뀔지도 모르니 한 겹 더 감쌉니다.

YAGNI(You Aren’t Gonna Need It)가 경고하는 지점이 정확히 여기입니다.

팀이 커지면서 층으로 나눈 경우. 콘웨이의 법칙이 말하듯 조직의 소통 구조는 아키텍처에 그대로 새겨집니다.

팀이 넷이면 계층도 넷이 되기 쉽고 그 경계는 기술적 필요보다 사람의 경계를 따릅니다.

필드 하나를 추가할 때 DTO부터 화면까지 거치는 7개 계층 수정 경로 다이어그램
빨간 칸이 아무것도 결정하지 않는 통과 계층입니다

비용은 어디서 나오나

층이 많으면 뭐가 나쁜지 구체적으로 짚어 보겠습니다.

변경 비용이 층 수에 비례합니다. 필드 하나에 일곱 파일. 이 자체보다 무서운 건 개발자가 이걸 피하려 든다는 점입니다.

정석대로 층을 통과시키는 대신 ViewModel에서 API를 직접 부르는 지름길이 생깁니다.

규칙이 지키기 번거로우면 규칙을 우회하는 코드가 생기고 결국 층은 층대로 남고 흐름은 스파게티가 됩니다. 라자냐가 스파게티를 낳는 겁니다.

읽는 데 드는 비용이 큽니다. 어떤 값이 어디서 왔는지 알려면 층을 따라 내려가야 합니다.

각 층은 짧고 명확하지만 일곱 번 점프한 뒤에는 처음 질문이 뭐였는지 잊습니다.

디버깅이 느려집니다. 스택 트레이스가 길어지고 어느 층에서 값이 잘못됐는지 찾으려면 층마다 중단점을 걸어야 합니다.

빌드가 느려집니다. 계층을 모듈로 나눴다면 특히 그렇습니다. 아래층을 고칠 때마다 위층이 전부 다시 빌드됩니다.

층이 필요한 경우와 아닌 경우

그렇다고 층을 없애라는 뜻은 아닙니다. 판단 기준이 필요합니다.

한 가지 질문이 대체로 잘 통합니다.

“이 층에서 무언가를 결정하는가, 아니면 넘기기만 하는가.”

결정한다는 건 변환, 검증, 분기, 조합 중 하나를 한다는 뜻입니다. 서버 날짜 문자열을 Date로 바꾸는 매퍼는 결정을 합니다.

캐시가 있으면 캐시를, 없으면 네트워크를 쓰는 저장소도 결정을 합니다. 반면 파라미터를 그대로 받아 그대로 넘기는 UseCase는 아무것도 결정하지 않습니다.

또 하나의 기준은 변경 이유입니다. 서버 응답 형식이 바뀌는 것과 화면 표시 규칙이 바뀌는 것은 서로 다른 이유입니다.

그래서 DTO와 화면 모델을 나눌 값어치가 있습니다. 반대로 도메인 모델과 화면 모델이 항상 같이 바뀐다면, 둘로 나눠 둘 이유가 없습니다.

표로 추리면 이렇습니다.

상황 층을 두는 게 맞나
외부 응답 형식과 내부 모델이 따로 움직인다 맞습니다
구현체를 갈아끼울 계획이 실제로 있다 맞습니다
테스트에서 가짜 구현이 필요하다 맞습니다
규모가 커서 팀 경계를 코드에 새겨야 한다 조건부로 맞습니다
구현체가 하나뿐이고 앞으로도 하나다 아닙니다
값을 받아 그대로 넘기기만 한다 아닙니다
도식에 그 층이 있어서 만들었다 아닙니다
값을 변환하는 계층과 그대로 통과시키는 통과 계층을 비교한 이미지
변환하면 계층이고, 그대로 넘기면 군더더기입니다

층을 걷어내는 법

이미 쌓인 라자냐를 다루는 순서는 대략 이렇습니다.

통과 계층부터 찾습니다. 메서드 본문이 한 줄이고 그 한 줄이 다른 객체의 같은 이름 메서드 호출이라면 후보입니다.

프로젝트 전체에서 이 패턴을 검색하면 목록이 금방 나옵니다.

구현체 수를 셉니다. 프로토콜마다 구현이 몇 개인지 봅니다. 하나뿐이고 테스트에서도 안 쓴다면 프로토콜을 지우고 구체 타입을 직접 씁니다.

이때 성능을 근거로 들지는 않는 게 좋습니다. any UserRepository 같은 존재 타입으로 부르면 witness table을 거치는 건 맞습니다.

다만 프로토콜을 제네릭 제약으로 쓰면 특수화되면서 그 비용이 사라집니다. 반대로 final이 아닌 클래스는 구체 타입으로 불러도 기본이 동적 디스패치입니다.

게다가 네트워크나 데이터베이스를 오가는 저장소 호출에서 이 차이는 실측에 잡히지 않습니다.

프로토콜을 지우는 이유는 성능이 아니라 점프 횟수입니다.

타입을 합칩니다. 필드가 똑같고 항상 같이 바뀌는 DTO와 도메인 모델이라면 하나로 씁니다.

Codable을 도메인 모델에 직접 붙이는 걸 오염이라고 보는 시각도 있습니다. 다만 그 오염이 언제 문제가 되는지부터 따져 보는 게 순서입니다.

한 층 안에서 접습니다. 층을 지우는 게 부담스러우면 계층은 두되 파일을 합칠 수 있습니다.

프로토콜과 구현을 한 파일에 두는 것만으로도 점프 횟수가 줄어듭니다.

정리하면

  • 라자냐 코드는 계층이 지나치게 많아 작은 변경에도 여러 파일을 고쳐야 하는 코드입니다.
  • 대개 나쁜 코드를 짜서가 아니라 좋은 조언을 문맥 없이 적용해서 생깁니다.
  • 핵심 증상은 통과 계층입니다. 아무것도 결정하지 않고 값을 옆으로 넘기기만 하는 층이죠.
  • 판단 기준은 두 가지입니다. 이 층이 결정을 하는가, 그리고 이 층은 다른 층과 다른 이유로 바뀌는가.
  • 규칙을 지키기 번거로우면 우회로가 생깁니다. 층이 과하면 결국 스파게티가 함께 자랍니다.

다음 편은 층이 아니라 조각이 문제인 경우입니다. 클래스는 하나같이 작고 잘 캡슐화돼 있는데 전체 흐름을 아무도 설명하지 못하는 코드, 라비올리 코드를 봅니다.

출처 및 확인 기준

  • Lasagna Code — C2 Wiki · 저자 원문 · 확인 2026-08-17 · 근거: 라자냐 코드 은유와 과도한 계층의 문제
  • Presentation-Domain-Data Layering — Martin Fowler · 저자 원문 · 확인 2026-08-17 · 근거: 계층 분리의 목적과 변경 경계

이어서 읽기