🏗️ 아키텍처

모놀리식

Monolithic

모놀리식 아키텍처는 모든 기능이 하나의 코드베이스와 배포 단위로 통합된 전통적인 소프트웨어 구조입니다. 개발 초기에 단순하고 빠르지만, 규모가 커지면 유지보수와 확장에 어려움이 발생합니다.

📖 상세 설명

모놀리식(Monolithic) 아키텍처는 애플리케이션의 모든 기능(UI, 비즈니스 로직, 데이터 접근)이 하나의 코드베이스에 통합되어 단일 단위로 빌드되고 배포되는 전통적인 소프트웨어 구조입니다.

장단점

장점

  • 개발 초기 단순하고 빠름
  • 로컬에서 전체 실행 용이
  • 트랜잭션 관리 단순
  • 배포가 하나로 끝남
  • 디버깅이 상대적으로 쉬움

단점

  • 코드베이스 커질수록 복잡
  • 작은 변경도 전체 재배포
  • 특정 기능만 스케일 불가
  • 기술 스택 변경 어려움
  • 팀 간 충돌 빈번

모놀리식이 적합한 경우

💻 코드 예제

전형적인 모놀리식 구조

myapp/
├── src/
│   ├── controllers/        # 모든 HTTP 컨트롤러
│   │   ├── UserController.ts
│   │   ├── OrderController.ts
│   │   └── ProductController.ts
│   ├── services/           # 비즈니스 로직 (모두 같은 폴더)
│   │   ├── UserService.ts
│   │   ├── OrderService.ts
│   │   └── ProductService.ts
│   ├── models/             # 데이터베이스 모델 (공유)
│   │   ├── User.ts
│   │   ├── Order.ts
│   │   └── Product.ts
│   └── app.ts              # 단일 진입점
├── package.json
└── Dockerfile              # 하나의 이미지

모놀리식 서비스 간 호출

// services/OrderService.ts
import { UserService } from './UserService';
import { ProductService } from './ProductService';
import { Order } from '../models/Order';

export class OrderService {
  constructor(
    private userService: UserService,     // 직접 의존
    private productService: ProductService  // 직접 의존
  ) {}

  async createOrder(userId: string, productIds: string[]) {
    // 함수 호출로 바로 접근 (네트워크 비용 없음)
    const user = await this.userService.findById(userId);
    const products = await this.productService.findByIds(productIds);

    // 같은 DB 트랜잭션 가능
    const order = new Order({ user, products });
    await order.save();

    return order;
  }
}

문제가 되는 패턴 (Big Ball of Mud)

// 안티패턴: 순환 의존, 명확하지 않은 경계
import { OrderService } from './OrderService';
import { UserService } from './UserService';
import { NotificationService } from './NotificationService';

export class ProductService {
  // 모든 서비스가 서로를 알고 있음
  constructor(
    private orderService: OrderService,
    private userService: UserService,
    private notificationService: NotificationService
  ) {}

  async updatePrice(productId: string, newPrice: number) {
    // 도처에서 다른 서비스 호출
    await this.product.updatePrice(newPrice);

    // 연관된 주문 가격도 업데이트 (경계 침범)
    await this.orderService.recalculateOrdersWithProduct(productId);

    // 관심 있는 사용자에게 알림 (더 복잡해짐)
    const users = await this.userService.findWatchingProduct(productId);
    await this.notificationService.notifyPriceChange(users, productId);
  }
}

💬 현업 대화 예시

👨‍💼 CTO

"배포하는데 30분이나 걸리고, 한 줄 고쳤는데 전체 테스트를 다시 돌려야 해요."

👩‍💻 아키텍트

"모놀리식의 한계가 왔네요. 모듈 경계를 먼저 정리해서 Modular Monolith로 전환하고, 트래픽이 많은 부분부터 점진적으로 마이크로서비스로 분리하는 전략을 추천해요."

👨‍💻 주니어 개발자

"마이크로서비스가 트렌드라고 하던데, 처음부터 마이크로서비스로 시작하면 안 되나요?"

👩‍💻 시니어 개발자

"Monolith First 원칙이 있어요. 도메인을 충분히 이해하기 전에 마이크로서비스로 분리하면 경계를 잘못 나눠서 나중에 더 고생해요. 모놀리식으로 시작해서 도메인이 명확해지면 그때 분리하는 게 낫습니다."

⚠️ 주의사항

Big Ball of Mud: 모놀리식이라도 내부 구조가 없으면 "진흙 덩어리"가 됩니다. 레이어드 아키텍처나 모듈 구조를 유지하세요.

분산 모놀리스: 마이크로서비스처럼 분리했지만 강하게 결합된 상태. 모놀리식의 단점 + 분산 시스템의 복잡성을 모두 가집니다.

팁: 모놀리식은 나쁜 게 아닙니다. Shopify, Stack Overflow 등 대형 서비스도 모놀리식을 잘 운영하고 있어요. 중요한 건 코드 품질과 모듈화입니다.

🔗 관련 용어

📚 더 배우기