큰 꿈은 파편이 크다!!⚡️

시스템 디자인 인터뷰 준비 (1) - 개념 정리 본문

기타 CS

시스템 디자인 인터뷰 준비 (1) - 개념 정리

wood.forest 2025. 1. 5. 19:17

어느 정도 연차가 쌓여서 피할 수 없는 시스템 디자인 인터뷰를 공부해보다가, 실전 예제와 함께 정리해보려 한다.

이 시리즈는 이렇게 계획했다.

1) 시스템 디자인 인터뷰에 필요한 개념들을 확실히 함 <- NOW!

2) 디자인 패턴 (LLD 할때 자주 쓰임)

3) 시스템 디자인 시 각 컴포넌트의 역할 (HLD)

4) 예제 및 각 컴포넌트에 적용한 내용 뜯어보기

 

 

 

1. High-Level System Design (HLD) vs Low-Level System Design (LLD)

1) High-Level System Design (HLD)

시스템이 "무엇”을 해야 하는지 정의하고 주요 구성 요소와 이들 간의 상호작용을 설계한다. 설계 내용에는 전체 시스템 아키텍처, 데이터 흐름, 외부 인터페이스 등이 포함된다.

 

설계 내용 ex:

  • 데이터베이스 유형 결정 (SQL vs NoSQL)
  • API 게이트웨이 구조 설계
  • 주요 모듈 분할 및 상호작용 정의

면접질문 예시:

  • 차량 공유 서비스를 설계하라
  • 음식 배달 서비스를 설계하라

 

 

2) Low-Level System Design (LLD)

시스템이 "어떻게" 동작할지를 정의하고 시스템 내부 구성 요소의 세부적인 동작을 설계한다. 클래스 다이어그램, 메서드 설계 등의 세부 로직이 포함되며, HLD의 세부내용을 상세화한다.

 

설계 내용 ex:

  • 데이터베이스 테이블 설계 및 관계 정의
  • API 엔드포인트 세부 구현

면접질문 예시:

  • 자판기를 설계하라
  • 엘리베이터를 설계하라

 

 

 

2. SOLID 디자인 원칙

SOLID 원칙은 객체 지향 설계에서 높은 응집도와 낮은 결합도를 유지하기 위한 설계 가이드라인이다. 원칙들을 유념하면서 디자인하고, 설계 후 이 원칙을 만족하는지 검증해본다.

 

1) S: 단일 책임 원칙 (Single Responsibility Principle)

하나의 클래스는 하나의 책임을 가진다.

ex. 사용자 관리 클래스는 사용자 인증과 데이터 저장을 동시에 처리하지 않고, 각각 인증 클래스와 저장 클래스를 만들어 역할을 분리한다

 

2) O: 개방-폐쇄 원칙 (Open-Closed Principle)

클래스는 기존 코드를 수정하지 않고도 시스템을 확장할 수 있도록 설계한다.

ex. 새로운 기능 추가 시 기존 클래스를 수정하지 않고, 상속이나 인터페이스를 통해 구현한다

 

적용 시나리오: 보고서를 생성하는 시스템이 있다. 처음에는 PDF 보고서만 생성하다가, 이후 Excel 보고서를 추가해야 하는 상황이다.

OCP를 위반한 설계

  • 새로운 포맷(예: Word 보고서)을 추가할 때마다 generate 메서드를 수정해야 함
  • 기존 코드 수정은 새로운 버그를 유발할 가능성이 있음
class ReportGenerator {
  generate(format) {
    if (format === "PDF") {
      console.log("Generating PDF report...");
    } else if (format === "Excel") {
      console.log("Generating Excel report...");
    }
  }
}

// 사용 예
const reportGenerator = new ReportGenerator();
reportGenerator.generate("PDF");  // Output: Generating PDF report...
reportGenerator.generate("Excel"); // Output: Generating Excel report...

 

OCP를 준수한 설계

  • 새로운 포맷이 추가될 때 기존 코드를 수정하지 않아도 됨
  • 기존 generate_report 함수와 클래스들은 수정 없이 재사용 가능
  • 추상화를 사용하여 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있도록 설계
// 추상화: Report 클래스
class Report {
  generate() {
    throw new Error("Method 'generate()' must be implemented.");
  }
}

// PDF 보고서 클래스
class PDFReport extends Report {
  generate() {
    console.log("Generating PDF report...");
  }
}

// Excel 보고서 클래스
class ExcelReport extends Report {
  generate() {
    console.log("Generating Excel report...");
  }
}

// 클라이언트 코드
class ReportGenerator {
  generateReport(reportInstance) {
    if (!(reportInstance instanceof Report)) {
      throw new Error("Invalid report type. Must implement the Report interface.");
    }
    reportInstance.generate();
  }
}

// 사용 예
const reportGenerator = new ReportGenerator();

const pdfReport = new PDFReport();
const excelReport = new ExcelReport();

reportGenerator.generateReport(pdfReport); // Output: Generating PDF report...
reportGenerator.generateReport(excelReport); // Output: Generating Excel report...

 

3) L: 리스코프 치환 원칙 (Liskov Substitution Principle)

상위 클래스의 객체를 하위 클래스로 교체해도 프로그램이 정상적으로 작동해야 한다.

ex. Rectangle 클래스를 상속한 Square 클래스가 상위 클래스의 기능을 모두 제대로 수행해야 한다

 

4) I: 인터페이스 분리 원칙 (Interface Segregation Principle)

인터페이스에 불필요한 메서드를 포함하지 않는다.

ex. 동물 인터페이스에서 모든 동물이 날지 않으므로, Flyable과 Walkable 인터페이스를 별도로 나눈다

// 공통 Animal 인터페이스
class Animal {
  eat() {
    console.log("This animal is eating.");
  }
}

// Flyable 인터페이스
class Flyable {
  fly() {
    console.log("This animal is flying.");
  }
}

// Walkable 인터페이스
class Walkable {
  walk() {
    console.log("This animal is walking.");
  }
}

// Bird 클래스: Animal + Flyable
class Bird extends Animal {
  constructor() {
    super();
    this.flyable = new Flyable();
  }

  fly() {
    this.flyable.fly();
  }
}

// Dog 클래스: Animal + Walkable
class Dog extends Animal {
  constructor() {
    super();
    this.walkable = new Walkable();
  }

  walk() {
    this.walkable.walk();
  }
}

 

5) D: 의존 역전 원칙 (Dependency Inversion Principle)

고수준 모듈(High-Level Module)과 저수준 모듈(Low-Level Module) 모두 추상화(ex.인터페이스)에 의존한다.

ex. 데이터베이스를 사용하는 코드가 특정 데이터베이스 구현체가 아니라 인터페이스에 의존하도록 설계한다

 

적용 시나리오: 어떤 주문내역을 db에 저장해야 하는 상황

DIP를 위반한 설계

  • 고수준 모듈(OrderService)이 특정 저수준 모듈(SQLDatabase)에 강하게 결합되어 있음.
  • 데이터 저장 방식을 변경하려면 OrderService를 수정해야 함.
  • 테스트 시 다른 데이터베이스를 사용하는 경우 교체가 어렵고 유연성이 낮음.
// 저수준 모듈
 class SQLDatabase {
   save(data) {
     console.log("Saving data in SQL database...");
   }
 }
 
 // 고수준 모듈
 class OrderService {
   constructor() {
     this.database = new SQLDatabase(); // 특정 구현체에 의존
   }
 
   saveOrder(order) {
     this.database.save(order);
   }
 }
 
 // 사용 예
 const orderService = new OrderService();
 orderService.saveOrder("Order 1");

 

DIP를 준수한 설계

  • 고수준 모듈은 추상화(인터페이스)에 의존하고, 저수준 모듈이 해당 인터페이스를 구현하도록 설계.
// 추상화(인터페이스 역할)
 class Database {
   save(data) {
     throw new Error("Method 'save()' must be implemented.");
   }
 }
 
 // 저수준 모듈 - SQL Database
 class SQLDatabase extends Database {
   save(data) {
     console.log("Saving data in SQL database...");
   }
 }
 
 // 저수준 모듈 - NoSQL Database
 class NoSQLDatabase extends Database {
   save(data) {
     console.log("Saving data in NoSQL database...");
   }
 }
 
 // 고수준 모듈
 class OrderService {
   constructor(database) {
     this.database = database; // 추상화에 의존
   }
 
   saveOrder(order) {
     this.database.save(order);
   }
 }
 
 // 사용 예
 const sqlDatabase = new SQLDatabase();
 const nosqlDatabase = new NoSQLDatabase();
 
 // SQL 데이터베이스 사용
 const orderServiceSQL = new OrderService(sqlDatabase);
 orderServiceSQL.saveOrder("Order 1"); // Output: Saving data in SQL database...
 
 // NoSQL 데이터베이스 사용
 const orderServiceNoSQL = new OrderService(nosqlDatabase);
 orderServiceNoSQL.saveOrder("Order 2"); // Output: Saving data in NoSQL database...
반응형