소프트웨어 디자인, 설계

디자인 원리

  • 아키텍쳐 원리랑 디자인 원리랑 경계가 약간 모호한 부분이 있는데요. 디자인이 아키첵쳐보다 전체적인 큰 그림에서 시스템의 방향성을 잡아준다고 생각합니다. 예로들면 단일책임 원칙을 따라야 한다. 라는 큰 맥락을 잡아주고요.

    아키텍쳐 설계는 실제로 서비스를 확장성있게 구축해야 한다. 이런 느낌이고요. 무튼 디자인 원리들을 기술하겠습니다.

    객체지향, 함수형 프로그래밍 패러다임 두개 다 예시를 들겠습니다. 그리고 YAGNI(You Ain’t Gonna Need It)원칙은 DRY(Don't Repeat Yourself)원칙과 비슷한 부분이 많아서 제외하겠습니다.

    마지막으로 해당 원칙들을 항상 저도 지키지는 못합니다. 급하게 빨리 만들어야 할 때에는 많은 고민을 하지 못하기 때문이에요. 일은 계획대로 흘러가지 않은경우가 대부분이고 예측하지 못한 버그가 생길때도 많아요. 그리고 이론과 현실은 항상 다른법이니깐요. 그래도 알고 있으면 훨씬 제품을 완성도 있게 만들 수 있다고 생각합니다. 시작하겠습니다.


1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

  • 정의 : 한 클래스는 하나의 책임만 가져야 하며, 이를 변경하는 이유는 오직 하나여야 합니다. 즉, 하나의 클래스나 모듈은 하나의 기능만 담당해야 합니다.
  • 이점 : 유지보수가 쉬워지고, 코드의 변경이 한 부분에만 영향을 미치도록 제한됩니다.

클래스형

// 나쁜 예: 하나의 클래스가 여러 책임을 가짐
class User {
  constructor(name, email) {
    this.name = name
    this.email = email
  }

  getUserInfo() {
    return `Name: ${this.name}, Email: ${this.email}`
  }

  saveToDatabase() {
    console.log('Saving user to database...')
  }
}

// 좋은 예: 각 클래스가 하나의 책임만 가짐
class User {
  constructor(name, email) {
    this.name = name
    this.email = email
  }

  getUserInfo() {
    return `Name: ${this.name}, Email: ${this.email}`
  }
}

class UserRepository {
  save(user) {
    console.log('Saving user to database...')
  }
}

const user = new User('Alice', 'alice@example.com')
const userRepository = new UserRepository()

console.log(user.getUserInfo()) // Name: Alice, Email: alice@example.com
userRepository.save(user) // Saving user to database...

함수형

// 나쁜 예: 하나의 함수가 여러 책임을 가짐
function handleUser(user) {
  console.log(`Name: ${user.name}, Email: ${user.email}`)
  console.log('Saving user to database...')
}

// 좋은 예: 각 함수가 하나의 책임만 가짐
function formatUserInfo(user) {
  return `Name: ${user.name}, Email: ${user.email}`
}

function saveUserToDatabase(user) {
  console.log('Saving user to database...')
}

const user = { name: 'Alice', email: 'alice@example.com' }
console.log(formatUserInfo(user))
saveUserToDatabase(user)

2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

  • 정의 : 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 수정에는 닫혀 있어야 합니다. 즉, 기존 코드를 변경하지 않고 새로운 기능을 추가할 수 있어야 합니다.
  • 이점 : 코드가 수정될 때 발생할 수 있는 오류를 줄이고, 새로운 요구사항에 유연하게 대처할 수 있습니다.

클래스형

// 나쁜 예: 새로운 계산 로직을 추가하려면 기존 코드를 수정해야 함
class Calculator {
  calculate(operation, a, b) {
    if (operation === 'add') {
      return a + b
    } else if (operation === 'subtract') {
      return a - b
    }
    // 새로운 연산을 추가하려면 이곳에 수정이 필요함
  }
}

// 좋은 예: 새로운 연산을 추가할 때 기존 코드를 수정할 필요 없음
class Operation {
  execute(a, b) {
    throw new Error('This method should be overridden')
  }
}

class AddOperation extends Operation {
  execute(a, b) {
    return a + b
  }
}

class SubtractOperation extends Operation {
  execute(a, b) {
    return a - b
  }
}

class Calculator {
  calculate(operation, a, b) {
    return operation.execute(a, b)
  }
}

const calculator = new Calculator()
console.log(calculator.calculate(new AddOperation(), 5, 3)) // 8
console.log(calculator.calculate(new SubtractOperation(), 5, 3)) // 2

함수형

// 나쁜 예: 새로운 계산 로직을 추가하려면 기존 코드를 수정해야 함
function calculate(operation, a, b) {
  if (operation === 'add') {
    return a + b
  } else if (operation === 'subtract') {
    return a - b
  }
  // 새로운 연산을 추가하려면 이곳에 수정이 필요함
}

// 좋은 예: 새로운 연산을 추가할 때 기존 코드를 수정할 필요 없음
const operations = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b, // 새로운 연산을 추가해도 calculate 함수는 수정되지 않음
}

function calculate(operation, a, b) {
  return operations[operation](a, b)
}

console.log(calculate('add', 5, 3)) // 8
console.log(calculate('multiply', 5, 3)) // 15

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

  • 정의 : 프로그램의 객체는 그 하위 타입의 객체로 대체할 수 있어야 하며, 이로 인해 프로그램의 기능이 변경되지 않아야 합니다.
  • 이점 : 상속 구조에서의 일관성을 유지하고, 다형성(polymorphism)을 통한 코드 재사용성을 높입니다.

클래스형

// 나쁜 예: 리스코프 치환 원칙 위반
class Rectangle {
  constructor(width, height) {
    this.width = width
    this.height = height
  }

  setWidth(width) {
    this.width = width
  }

  setHeight(height) {
    this.height = height
  }

  getArea() {
    return this.width * this.height
  }
}

class Square extends Rectangle {
  setWidth(width) {
    this.width = width
    this.height = width // 정사각형은 높이와 너비가 같아야 함
  }

  setHeight(height) {
    this.width = height
    this.height = height
  }
}

function printArea(rectangle) {
  rectangle.setWidth(5)
  rectangle.setHeight(4)
  console.log(rectangle.getArea())
}

const rectangle = new Rectangle(2, 3)
printArea(rectangle) // 20

const square = new Square(2, 2)
printArea(square) // 16 (예상과 다른 결과)

//좋은 예
class Shape {
  getArea() {
    throw new Error('This method should be overridden')
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super()
    this.width = width
    this.height = height
  }

  getArea() {
    return this.width * this.height
  }
}

class Square extends Shape {
  constructor(side) {
    super()
    this.side = side
  }

  getArea() {
    return this.side * this.side
  }
}

function printArea(shape) {
  console.log(shape.getArea())
}

const rectangle = new Rectangle(5, 4)
const square = new Square(5)

printArea(rectangle) // 20
printArea(square) // 25

함수형

// 나쁜 예: 리스코프 치환 원칙 위반
function getRectangleArea(rectangle) {
  return rectangle.width * rectangle.height
}

const rectangle = { width: 5, height: 4 }
const square = { width: 4, height: 4 }

console.log(getRectangleArea(rectangle)) // 20
console.log(getRectangleArea(square)) // 16

// 좋은 예: 각 도형의 종류에 따라 별도의 함수를 사용
function getRectangleArea(rectangle) {
  return rectangle.width * rectangle.height
}

function getSquareArea(square) {
  return square.side * square.side
}

const square2 = { side: 4 }

console.log(getRectangleArea(rectangle)) // 20
console.log(getSquareArea(square2)) // 16

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

  • 정의 : 인터페이스는 특정 클라이언트를 위한 메서드들로 분리되어야 하며, 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
  • 이점 : 인터페이스의 복잡성을 줄이고, 클라이언트와의 결합도를 낮출 수 있습니다.

클래스형

// 나쁜 예: 단일 클래스가 너무 많은 기능을 포함하고 있음
class MultiFunctionDevice {
  print(document) {
    console.log('Printing document...')
  }

  scan(document) {
    console.log('Scanning document...')
  }

  fax(document) {
    console.log('Faxing document...')
  }
}

class OldPrinter extends MultiFunctionDevice {
  scan(document) {
    throw new Error('Scan not supported!')
  }

  fax(document) {
    throw new Error('Fax not supported!')
  }
}

// 좋은 예: 인터페이스를 분리하여 필요 없는 기능에 의존하지 않도록 함
class Printer {
  print(document) {
    console.log('Printing document...')
  }
}

class Scanner {
  scan(document) {
    console.log('Scanning document...')
  }
}

class FaxMachine {
  fax(document) {
    console.log('Faxing document...')
  }
}

const printer = new Printer()
printer.print('My Document') // Printing document...

함수형

// 나쁜 예: 하나의 함수가 너무 많은 기능을 포함하고 있음
function multiFunctionDevice(document, action) {
  if (action === 'print') {
    console.log('Printing document...')
  } else if (action === 'scan') {
    console.log('Scanning document...')
  } else if (action === 'fax') {
    console.log('Faxing document...')
  }
}

// 좋은 예: 필요한 기능만 수행하는 개별 함수 사용
function printDocument(document) {
  console.log('Printing document...')
}

function scanDocument(document) {
  console.log('Scanning document...')
}

function faxDocument(document) {
  console.log('Faxing document...')
}

const document = { content: 'Hello, world!' }

printDocument(document) // Printing document...
scanDocument(document) // Scanning document...

5. 의존성 역전 원칙 (Dependency Inversion Principle, DIP)

  • 정의 : 고수준 모듈(정책 결정 계층)은 저수준 모듈(세부 사항 처리 계층)에 의존해서는 안 됩니다. 둘 다 추상화된 인터페이스에 의존해야 하며, 구체적인 구현이 아닌 추상화에 의존해야 합니다.
  • 이점 : 모듈 간 결합도를 낮추어 변경에 유연하고, 재사용 가능한 코드를 작성할 수 있습니다.

클래스형

// 나쁜 예: 고수준 모듈이 저수준 모듈에 의존
class MySQLDatabase {
  connect() {
    console.log('Connecting to MySQL database...')
  }
}

class UserService {
  constructor() {
    this.db = new MySQLDatabase()
  }

  getUser() {
    this.db.connect()
    console.log('Fetching user...')
  }
}

const userService = new UserService()
userService.getUser()

// 좋은 예: 고수준 모듈과 저수준 모듈이 추상화된 인터페이스에 의존
class Database {
  connect() {
    throw new Error('This method should be overridden')
  }
}

class MySQLDatabase extends Database {
  connect() {
    console.log('Connecting to MySQL database...')
  }
}

class MongoDBDatabase extends Database {
  connect() {
    console.log('Connecting to MongoDB database...')
  }
}

class UserService {
  constructor(db) {
    this.db = db
  }

  getUser() {
    this.db.connect()
    console.log('Fetching user...')
  }
}

const mysqlDatabase = new MySQLDatabase()
const mongoDatabase = new MongoDBDatabase()

const userService1 = new UserService(mysqlDatabase)
const userService2 = new UserService(mongoDatabase)

userService1.getUser() // Connecting to MySQL database... Fetching user...
userService2.getUser() // Connecting to MongoDB database... Fetching user...

함수형

// 나쁜 예: 고수준 모듈이 저수준 모듈에 의존
function userService() {
  function connectToMySQL() {
    console.log('Connecting to MySQL database...')
  }

  connectToMySQL()
  console.log('Fetching user...')
}

userService()

// 좋은 예: 고수준 모듈과 저수준 모듈이 추상화된 인터페이스에 의존
function createUserService(databaseConnection) {
  return function () {
    databaseConnection.connect()
    console.log('Fetching user...')
  }
}

function connectToMySQL() {
  return {
    connect: () => console.log('Connecting to MySQL database...'),
  }
}

function connectToMongoDB() {
  return {
    connect: () => console.log('Connecting to MongoDB database...'),
  }
}

const mySQLService = createUserService(connectToMySQL())
const mongoDBService = createUserService(connectToMongoDB())

mySQLService() // Connecting to MySQL database... Fetching user...
mongoDBService() // Connecting to MongoDB database... Fetching user...

6. 모듈화 (Modularity)

  • 정의 : 시스템을 여러 독립적인 모듈로 나누어 설계합니다. 각 모듈은 고유한 기능을 가지며, 다른 모듈과 최소한의 인터페이스만 공유합니다.
  • 이점 : 코드의 이해와 유지보수가 용이해지고, 모듈 단위로 테스트 및 재사용이 가능합니다.

클래스형

// 나쁜 예: 모든 코드가 한 클래스에 모여 있어 모듈화되지 않음
class ShoppingCart {
  constructor() {
    this.items = []
  }

  addItem(item) {
    this.items.push(item)
  }

  calculateTotal() {
    let total = 0
    for (let item of this.items) {
      total += item.price * item.quantity
    }
    return total
  }

  formatCurrency(value) {
    return `$${value.toFixed(2)}`
  }

  printTotal() {
    const total = this.calculateTotal()
    console.log(this.formatCurrency(total))
  }
}

const cart = new ShoppingCart()
cart.addItem({ name: 'Apple', price: 0.5, quantity: 10 })
cart.addItem({ name: 'Orange', price: 0.75, quantity: 5 })
cart.printTotal() // $10.75

// 좋은 예: 모듈로 분리하여 코드 관리 및 재사용성 향상
class CartCalculator {
  calculateTotal(items) {
    let total = 0
    for (let item of items) {
      total += item.price * item.quantity
    }
    return total
  }
}

class CurrencyFormatter {
  formatCurrency(value) {
    return `$${value.toFixed(2)}`
  }
}

class ShoppingCart {
  constructor(calculator, formatter) {
    this.items = []
    this.calculator = calculator
    this.formatter = formatter
  }

  addItem(item) {
    this.items.push(item)
  }

  printTotal() {
    const total = this.calculator.calculateTotal(this.items)
    console.log(this.formatter.formatCurrency(total))
  }
}

const calculator = new CartCalculator()
const formatter = new CurrencyFormatter()

const cart = new ShoppingCart(calculator, formatter)
cart.addItem({ name: 'Apple', price: 0.5, quantity: 10 })
cart.addItem({ name: 'Orange', price: 0.75, quantity: 5 })
cart.printTotal() // $10.75

함수형

// 나쁜 예: 모든 코드가 한 파일에 모여 있어 모듈화를 하지 않음
function calculateTotal(items) {
  let total = 0
  for (let item of items) {
    total += item.price * item.quantity
  }
  return total
}

function formatCurrency(value) {
  return `$${value.toFixed(2)}`
}

const items = [
  { name: 'Apple', price: 0.5, quantity: 10 },
  { name: 'Orange', price: 0.75, quantity: 5 },
]

const total = calculateTotal(items)
console.log(formatCurrency(total))

// 좋은 예: 모듈로 분리하여 코드 관리 및 재사용성 향상
// file: calculate.js
export function calculateTotal(items) {
  let total = 0
  for (let item of items) {
    total += item.price * item.quantity
  }
  return total
}

// file: format.js
export function formatCurrency(value) {
  return `$${value.toFixed(2)}`
}

// file: main.js
import { calculateTotal } from './calculate.js'
import { formatCurrency } from './format.js'

const items = [
  { name: 'Apple', price: 0.5, quantity: 10 },
  { name: 'Orange', price: 0.75, quantity: 5 },
]

const total = calculateTotal(items)
console.log(formatCurrency(total))

7. 레이어드 아키텍처 (Layered Architecture)

  • 정의 : 소프트웨어 시스템을 여러 계층으로 나누어 설계합니다. 일반적으로 프레젠테이션 계층, 비즈니스 로직 계층, 데이터 접근 계층 등으로 구분됩니다.
  • 이점 : 각 계층은 다른 계층의 세부 사항에 의존하지 않으므로, 계층 간의 변경이 독립적으로 이루어질 수 있습니다.

클래스형

// 나쁜 예: 모든 로직이 한 곳에 혼합되어 있음
class UserApp {
  getUser(id) {
    // 데이터 접근 로직
    const users = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]
    const user = users.find((user) => user.id === id)

    // 비즈니스 로직
    if (user) {
      return `User: ${user.name}`
    } else {
      return 'User not found'
    }
  }
}

const app = new UserApp()
console.log(app.getUser(1)) // User: Alice

// 좋은 예: 레이어드 아키텍처를 적용
class UserRepository {
  getUserData(id) {
    const users = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ]
    return users.find((user) => user.id === id)
  }
}

class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository
  }

  getUser(id) {
    const user = this.userRepository.getUserData(id)
    if (user) {
      return `User: ${user.name}`
    } else {
      return 'User not found'
    }
  }
}

class UserApp {
  constructor(userService) {
    this.userService = userService
  }

  displayUser(id) {
    console.log(this.userService.getUser(id))
  }
}

const userRepository = new UserRepository()
const userService = new UserService(userRepository)
const app = new UserApp(userService)

app.displayUser(1) // User: Alice

함수형

// 나쁜 예: 모든 로직이 한 곳에 혼합되어 있음
function getUser(id) {
  // 데이터 접근 로직
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ]
  const user = users.find((user) => user.id === id)

  // 비즈니스 로직
  if (user) {
    return `User: ${user.name}`
  } else {
    return 'User not found'
  }
}

console.log(getUser(1)) // User: Alice

// 좋은 예: 레이어드 아키텍처를 적용
// file: dataLayer.js
export function getUserData(id) {
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ]
  return users.find((user) => user.id === id)
}

// file: businessLayer.js
import { getUserData } from './dataLayer.js'

export function getUser(id) {
  const user = getUserData(id)
  if (user) {
    return `User: ${user.name}`
  } else {
    return 'User not found'
  }
}

// file: presentationLayer.js
import { getUser } from './businessLayer.js'

console.log(getUser(1)) // User: Alice

8. DRY 원칙 (Don't Repeat Yourself)

  • 정의 : 동일한 코드나 로직을 반복하지 않고, 중복을 최소화하여 코드의 일관성을 유지합니다.
  • 이점 : 코드의 유지보수를 쉽게 하고, 수정 시 중복된 부분을 모두 변경해야 하는 번거로움을 없앱니다.

클래스형

// 나쁜 예: 중복된 코드가 여러 곳에 존재
class User {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`
  }

  printUserGreeting() {
    const fullName = `${this.firstName} ${this.lastName}` // 중복 코드
    console.log(`Hello, ${fullName}`)
  }

  printUserFarewell() {
    const fullName = `${this.firstName} ${this.lastName}` // 중복 코드
    console.log(`Goodbye, ${fullName}`)
  }
}

const user = new User('Alice', 'Johnson')
user.printUserGreeting() // Hello, Alice Johnson
user.printUserFarewell() // Goodbye, Alice Johnson

// 좋은 예: 중복 코드를 제거하고 재사용
class User {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`
  }

  printUserGreeting() {
    console.log(`Hello, ${this.getFullName()}`)
  }

  printUserFarewell() {
    console.log(`Goodbye, ${this.getFullName()}`)
  }
}

const user = new User('Alice', 'Johnson')
user.printUserGreeting() // Hello, Alice Johnson
user.printUserFarewell() // Goodbye, Alice Johnson

함수형

// 나쁜 예: 중복된 코드가 여러 곳에 존재
function getUserFullName(user) {
  return `${user.firstName} ${user.lastName}`
}

function printUserGreeting(user) {
  const fullName = `${user.firstName} ${user.lastName}` // 중복 코드
  console.log(`Hello, ${fullName}`)
}

function printUserFarewell(user) {
  const fullName = `${user.firstName} ${user.lastName}` // 중복 코드
  console.log(`Goodbye, ${fullName}`)
}

// 좋은 예: 중복 코드를 제거하고 재사용
function getUserFullName(user) {
  return `${user.firstName} ${user.lastName}`
}

function printUserGreeting(user) {
  const fullName = getUserFullName(user)
  console.log(`Hello, ${fullName}`)
}

function printUserFarewell(user) {
  const fullName = getUserFullName(user)
  console.log(`Goodbye, ${fullName}`)
}

const user = { firstName: 'Alice', lastName: 'Johnson' }
printUserGreeting(user) // Hello, Alice Johnson
printUserFarewell(user) // Goodbye, Alice Johnson

9. KISS 원칙 (Keep It Simple, Stupid)

  • 정의 : 코드를 가능한 한 단순하고 명확하게 유지합니다. 복잡성을 줄이는 것이 목표입니다.
  • 이점 : 코드의 가독성을 높이고, 디버깅과 유지보수가 용이해집니다.

클래스형

// 나쁜 예: 불필요하게 복잡한 코드
class NumberUtility {
  isPositiveNumber(number) {
    if (typeof number === 'number') {
      if (number > 0) {
        return true
      } else {
        return false
      }
    } else {
      return false
    }
  }
}

// 좋은 예: 단순하고 명확한 코드
class NumberUtility {
  isPositiveNumber(number) {
    return typeof number === 'number' && number > 0
  }
}

const utility = new NumberUtility()
console.log(utility.isPositiveNumber(5)) // true
console.log(utility.isPositiveNumber(-5)) // false

함수형

// 나쁜 예: 불필요하게 복잡한 코드
function isPositiveNumber(number) {
  if (typeof number === 'number') {
    if (number > 0) {
      return true
    } else {
      return false
    }
  } else {
    return false
  }
}

// 좋은 예: 단순하고 명확한 코드
function isPositiveNumber(number) {
  return typeof number === 'number' && number > 0
}

console.log(isPositiveNumber(5)) // true
console.log(isPositiveNumber(-5)) // false

10. SOLID 원칙 통합

  • 정의 : 앞에서 설명한 SRP, OCP, LSP, ISP, DIP 원칙의 첫 글자를 따서 만든 약어로, 객체 지향 설계의 5대 원칙을 의미합니다.
  • 이점 : SOLID 원칙을 따르면 코드의 유연성, 확장성, 유지보수성이 크게 향상됩니다.

클래스형

// SRP, OCP: 계산 로직과 사용자 서비스 로직을 분리하고 확장 가능하게 설계
class Operation {
  execute(a, b) {
    throw new Error('This method should be overridden')
  }
}

class AddOperation extends Operation {
  execute(a, b) {
    return a + b
  }
}

class SubtractOperation extends Operation {
  execute(a, b) {
    return a - b
  }
}

class Calculator {
  calculate(operation, a, b) {
    return operation.execute(a, b)
  }
}

// DIP: 데이터베이스와 사용자 서비스의 의존성을 역전
class Database {
  connect() {
    throw new Error('This method should be overridden')
  }
}

class MySQLDatabase extends Database {
  connect() {
    console.log('Connecting to MySQL database...')
  }
}

class UserService {
  constructor(db) {
    this.db = db
  }

  getUser() {
    this.db.connect()
    console.log('Fetching user...')
  }
}

const calculator = new Calculator()
console.log(calculator.calculate(new AddOperation(), 5, 3)) // 8

const mysqlDatabase = new MySQLDatabase()
const userService = new UserService(mysqlDatabase)
userService.getUser() // Connecting to MySQL database... Fetching user...

함수형

// SOLID 원칙을 종합한 예시
// file: calculate.js (OCP, SRP)
export function calculate(operation, a, b) {
  const operations = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
  }
  return operations[operation](a, b)
}

// file: userService.js (DIP)
export function createUserService(databaseConnection) {
  return function () {
    databaseConnection.connect()
    console.log('Fetching user...')
  }
}

// file: main.js (ISP)
import { calculate } from './calculate.js'
import { createUserService } from './userService.js'

function connectToMySQL() {
  return {
    connect: () => console.log('Connecting to MySQL database...'),
  }
}

const mySQLService = createUserService(connectToMySQL())
mySQLService()

console.log(calculate('add', 5, 3)) // 8
Previous
nextAuth(세션, 토큰)