강한 결합(Tight Coupling)은 클래스와 객체가 서로 의존(Dependency)하고 있는 것이다.
아래에서 배울 느슨한 결합(Loose Coupling)의 반대 개념이라고 생각하면 된다.
DI는 Dependency Injection 을 의마한다. 즉, 의존성 주입을 뜻한다.
IoC는 Inversion of Controll 으로 제어의 역전을 의미한다.
class → section06 폴더 내에 06-07-express-with-OOP-mvc-coupon 폴더를 복사하여 붙여 넣어 더 자세히 알아보자
폴더 이름을 06-08-express-with-DI-IoC 폴더로 변경해준다.
06-08-express-with-DI-IoC → mvc → controller → product.controller.js 파일을 열어준다.
// product.controller.js
import { CashService } from "./services/cash.service.js";
import { ProductService } from "./services/product.service.js";
export class ProductController {
// 이 클래스는 CashService, ProductService 클래스에 의존하고 있음
buyProduct = (req, res) => {
// 1. 가진돈 검증하는 코드 (대략 10줄 => 2줄)
const cashService = new CashService();
const hasMoney = cashService.checkValue(); // true 또는 false 리턴
// 2. 판매여부 검증하는 코드 (대략 10줄 => 2줄)
const productService = new ProductService();
const isSoldout = productService.checkSoldout(); // true 또는 false 리턴
// 3. 상품 구매하는 코드
if (hasMoney && !isSoldout) {
res.send("상품 구매 완료!!");
}
};
refundProduct = (req, res) => {
// 1. 판매여부 검증하는 코드 (대략 10줄 정도)
const productService = new ProductService();
const isSoldout = productService.checkSoldout(); // true 또는 false 리턴
// 2. 상품 환불하는 코드
if (isSoldout) {
res.send("상품 환불 완료!!");
}
};
}
현재 위의 코드는 ProductController가 CashService와 ProductService에 의존하고 있는 상태다.
이런 현상이 ProductController가 CashService와 ProductService에
의존성이 존재한다고 하는 것이다.
다른 말로는강하게 결합되어 있다(Tight Coupling)라고 한다.
06-08-express-with-DI-IoC → mvc → controller → coupon.controller.js 파일을 열어준다.
// coupon.controller.js
import { CashService } from "./services/cash.service.js";
export class CouponController {
buyCoupon = (req, res) => {
// 1. 가진돈 검증하는 코드 (10줄 => 2줄)
const cashService = new CashService();
const hasMoney = cashService.checkValue(); // true 또는 false 리턴
// 2. 쿠폰 구매하는 코드
if (hasMoney) {
res.send("쿠폰 구매 완료!!");
}
};
}
위의 코드는 방금 ProductController와 마찬가지로, CouponController가 CashService에 의존하고 있으므로 CouponController와 CashService가 강하게 결합되어 있는 것이다.
SW에서 결합을 "둘 이상의 객체" 로 해석했을 때,
느슨한 결합(Loose Coupling)이란 "객체들 간에 결합이 되어있긴 하는데 헐겁게 됐다."로 해석할 수 있으며,
다른 클래스를 직접적으로 사용하는 클래스의 의존성을 줄이는 것 이다.
위에서 말한 강한 결합(Tight Coupling)의 반대 개념이라고 생각하면 된다.
이 때, 느슨한 결합을 위해서는 의존성 주입 DI가 필요하다.
이제 느슨한 결합(loose-coupling) 을 위한 의존성주입(DI) 을 해보자
06-08-express-with-DI-IoC → mvc → index.js 를 다음과 같이 수정한다.
// index.js
// const express = require('express') // 옛날방식 => commonjs
import express from "express"; // 요즘방식 => module
import { ProductController } from "./mvc/controllers/product.controller.js";
import { CouponController } from "./mvc/controllers/coupon.controller.js";
import { ProductService } from "./mvc/controllers/services/product.service.js";
const app = express();
// 추가되는 부분
const cashService = new CashService();
// 상품 API
const productController = new ProductController(cashService); // 의존성 주입
app.post("/products/buy", productController.buyProduct);
app.post("/products/refund", productController.refundProduct);
// 쿠폰(상품권) API
const couponController = new CouponController();
app.post("/coupons/buy", couponController.buyCoupon);
app.listen(3000, () => {
console.log("백엔드 API 서버가 켜졌어요!!!");
});
product.controller.js 파일에 있는 new CashService() Controller 에서 하는것이 아니라 밖에서 실행시켜 준다. index.js 파일에서 실행시켜 줄 것이다.
추가 작성한 cashService 를 ProductController() 안에 넣어줌으로써 밖에서 만들어서 안으로 주입하게(Injection) 되는 것이다.
즉, cashService를생성자로 넣어준 것이다.
index.js에 Singleton Pattern(싱글톤 패턴)을 적용했다.
06-08-express-with-DI-IoC → mvc → controller → product.controller.js 파일을 아래와 같이 수정해준다.
// product.controller.js
import { ProductService } from './services/product.service.js'
// import { CashService } from "./services/point.service.js";
export class ProductController {
// 의존성 주입을 받기위한 생성자 코드
constructor(cashService) {
this.cashService = cashService;
}
buyProduct = (req, res) => {
// 1. 가진돈 검증하는 코드(10줄 => 2줄 => 1줄)
// const cashService = new CashService();
const hasMoney = this.cashService.checkValue();
// 판매여부 검증하는 코드(10줄 => 2줄)
const productService = new ProductService()
const isSoldout = productService.checkSoldout();
// 3. 상품 구매하는 코드
if (hasMoney && !isSoldout) {
res.send("상품을 구매합니다.");
}
};
refundProduct = (req, res) => {
// 1. 판매여부 검증하는 코드(10줄 => 2줄 => 1줄)
const productService = new ProductService()
const isSoldout = productService.checkSoldout();
// 2. 상품 환불하는 코드
if (isSoldout) {
res.send("상품을 환불합니다.");
}
};
}
index.js에서 넣어준 cashService를 product.controller.js 파일에서 생성자인 constructor 를 사용하여 moneyService로 받아 줬다.
이렇게 외부에서 받아와서 끝나는 것이 아니라, 반드시 this.으로 사용해서 안으로 넣어주어야 한다.
this.을 사용하게 되면 ProductController class 안의 함수들 선택이 가능해진다.
this 에 관한 것은 모던 자바스크립트 책을 참고
this 참고 자료
https://poiemaweb.com/js-this
다시 말해서 Constructor Inject(생성자 주입)을 사용해서 DI(Dependency Injection) 의존성 주입을 해준 것이다.
그리고 Constructor를 사용해서 DI(Dependency Injection) 를 해주었기 때문에, 이를 제어가 역전되었다고 할 수 있습니다.
이전에 강한 결합(Tight Coupling)상태에는 모듈을 불러와야 했다.(import) 하지만
느슨한 결합(Loose Coupling)은 모듈을 불러오지 않아도 된다.
즉, new CashService() 는 필요 없게 되고, constructor 내부의 this.moneyService 에 cashService 가 저장되어 있기 때문에 가진 돈을 검증하는 코드에서 moneyService 앞에 this. 을 붙여주어야 한다.
이 생성자들을 product.controller.js 에서 받아서 사용하게 되는 것이다.
남은 ProductService 도 바깥에서 안으로 의존성주입을 적용하자.
06-08-express-with-DI-IoC → index.js 파일에서 아래와 동일하게 수정한다.
// index.js
import express from "express";
import { ProductController } from "./mvc/controllers/product.controller.js";
import { CouponController } from "./mvc/controllers/coupon.controller.js";
import { ProductService } from "./mvc/controllers/services/product.service.js";
import { CashService } from "./mvc/controllers/services/cash.service.js";
const app = express();
// 의존을 위해 인스턴스 생성
const productService = new ProductService();
const cashService = new CashService();
// 상품 API
// 의존성을 주입
const productController = new ProductController(cashService, productService);
app.post("/products/buy", productController.buyProduct);
app.post("/products/refund", productController.refundProduct);
// 쿠폰 API
const couponController = new CouponController();
app.post("/coupons/buy", couponController.buyCoupon);
app.listen(3000, () => {
console.log("백엔드 API 서버가 켜졌어요!!!");
});
바깥에서 new ProductService() 를 만들어 준다. 그리고 Productcontroller로 의존성을 주입한다.
06-08-express-with-DI-IoC → mvc → controller → product.controller.js 파일에서 아래와 동일하게 수정해준다.
// product.controller.js
export class ProductController {
constructor(cashService, productService) {
this.cashService = cashService;
this.productService = productService;
}
buyProduct = (req, res) => {
// 1. 가진돈 검증하는 코드(10줄 => 2줄 => 1줄)
// const cashService = new CashService()
const hasMoney = this.cashService.checkValue();
// 2. 판매여부 검증하는 코드(10줄 => 2줄 => 1줄)
// const productService = new ProductService()
const isSoldout = this.productService.checkSoldout();
// 3. 상품 구매하는 코드
if (hasMoney && !isSoldout) {
res.send("상품을 구매합니다.");
}
};
refundProduct = (req, res) => {
// 1. 판매여부 검증하는 코드(10줄 => 2줄 => 1줄)
// const productService = new ProductService()
const isSoldout = this.productService.checkSoldout();
// 2. 상품 환불하는 코드
if (isSoldout) {
res.send("상품을 환불합니다.");
}
};
}
productService가 바깥에서 안으로 의존성 주입되었다.
이제 coupon.controller.js 파일에 new CashServie() 를 의존성 주입을 적용시켜 보자
06-08-express-with-DI-IoC → index.js 파일을 다음과 동일하게 수정해준다.
// index.js
import express from "express";
import { ProductController } from "./mvc/controllers/product.controller.js";
import { CouponController } from "./mvc/controllers/coupon.controller.js";
import { ProductService } from "./mvc/controllers/services/product.service.js";
import { CashService } from "./mvc/controllers/services/cash.service.js";
const app = express();
const productService = new ProductService();
const cashService = new CashService(); // 1. new 한번으로 모든 곳에서 재사용 가능(싱글톤패턴)
// 상품 API
// 의존성을 주입
const productController = new ProductController(cashService, productService);
app.post("/products/buy", productController.buyProduct);
app.post("/products/refund", productController.refundProduct);
// 쿠폰 API
const couponController = new CouponController(cashService);
app.post("/coupons/buy", couponController.buyCoupon);
app.listen(3000, () => {
console.log("백엔드 API 서버가 켜졌어요!!!");
});
06-08-express-with-DI-IoC → mvc → controller → coupon.controller.js 파일에서 아래와 같이 수정한다.
//import { CashService } from './services/cash.service.js'
export class CouponController {
constructor(cashService) {
this.cashService = cashService;
}
buyCoupon = (req, res) => {
// 1. 가진돈 검증하는 코드(10줄 => 2줄 => 1줄)
// const cashService = new CashService()
const hasMoney = this.cashService.checkValue();
// 2. 쿠폰 구매하는 코드(10줄)
if (hasMoney) {
res.send("쿠폰을 구매합니다.");
}
};
}
index.js에서 넣어준 cashService를 coupon.controller.js 파일에서 constructor를 사용하여 moneyService로 받아 주었다.
서버를 실행시켜 확인해 볼 텐데 실행시키기 전에 실제로 구매가 이루어질 것은 아니기에 console.log 를 작성해서 확인해보자
06-08-express-with-DI-IoC → mvc → controller → services → cash.service.js 파일과 product.service.ts 파일에서 아래와 같이 수정한다.
// cash.service.ts
export class CashService {
checkValue = () => {
console.log("현금이 있는지 검사합니다.");
// 1. 가진돈 검증하는 코드 (대략 10줄 정도)
// ...
// ...
// ...
// ...
};
}
// product.service.ts
export class ProductService {
checkSoldout = () => {
console.log("판매가 완료됐는지 검사합니다.");
// 2. 판매여부 검증하는 코드 (대략 10줄 정도)
// ...
// ...
// ...
// ...
};
}
터미널의 위치가 해당 폴더가 아니라면 06-08-express-with-DI-IoC 폴더로 이동하여 node index.js 명령어를 사용하여 실행한다
🚨 이 때, [ERR_MODULE_NOT_FOUND] 에러가 발생한다면
yarn install을 통해서module들을 설치하고 다시 실행시켜준다.

실행이 완료되었다면 postman 을 통해서 확인해보자
크롬 검색창에 postman chrome 을 검색해서 포스트맨으로 들어간 이후
POST 방식을 선택하고 localhost:3000/products/buy 작성하여 Send 버튼을 눌러 요청해준다.

API 를 정상적으로 만든 것이 아니기에 포스트맨으로 요청 시, 무한 Loading이 되다가 멈추게 될 것이니 무한 Loading 되어도 상관하지 않아도 된다.
지금은 터미널 창에 나타난 console.log 를 확인하면 된다.
아래와 같이 나온다면 성공적으로 실행된 것이다.

의존성주입을 사용한 부분들의 로직이 제대로 작동하는지 확인한 것이다.
나머지도 포스트맨을 통해서 확인해보자
앞서 확인한 포스트맨이 아직 Loading 중이라면 Cancel Request 를 눌러줘서 Loading 을 중단시켜준다.
POST 방식을 선택하고 localhost:3000/products/refund 작성하여 Send 버튼을 눌러 요청하면


POST 방식을 선택하고 localhost:3000/coupons/buy 작성하여 Send 버튼을 눌러 요청한다.


모두 성공적으로 작동하는 것을 확인할 수 있다.
비즈니스 로직인 ProductService 를 먼저 선언했다.
이렇게 하면 인스턴스 객체 하나인 new 한 번으로 모든 곳에서 사용 가능하게 된다.
이런 디자인 패턴을 Singleton Pattern(싱글톤 패턴) 이라고 한다.
지금까지 살펴본 바와 같이 의존성주입으로 얻을 수 있는 싱글톤패턴의 장점으로는 2가지가 있다.
DI(Dependency Injection) 의존성주입은 Tight Coupling(강한 결합)을 Loose Coupling(느슨한 결합)으로 전환 시키는 방법이며, 제어의 역전(Inversion of Control)의 기술 중 하나이다.
DI(Dependency Injection) 의존성주입에 총 3가지의 방법이 존재한다,
이 중 Constructor Inject(생성자 주입)이 많은 Design pattern에서 권장된다.
의존성 주입을 한다고 무조건 싱글톤패턴인 것은 아니다. new 를 밖으로 빼주게 되면서 싱글톤패턴이 된 것은 맞지만 의존성 주입이라는 것은 밖에서 안으로 넣어주는 것이기에 같다고는 할 수 없다.
아래와 같이 cashService를 cashService1와 cashService2로 만들어서
똑같은 CashService를 서로 다른 곳에 사용하기 위해서 각각 따로 주입시켜준다면
서로 다른 객체로 사용되기 때문에 의존성 주입은 맞으나 싱글톤 패턴은 아니게 된다.
// index.js
import express from "express";
import { ProductController } from "./mvc/controllers/product.controller.js";
import { CouponController } from "./mvc/controllers/coupon.controller.js";
import { ProductService } from "./mvc/controllers/services/product.service.js";
import { CashService } from "./mvc/controllers/services/cash.service.js";
const app = express();
const productService = new ProductService();
const cashService1 = new Cashservice()
const cashService2 = new Cashservice()
// 상품 API
const productController = new ProductController(cashService1, productService);
app.post("/products/buy", productController.buyProduct);
app.post("/products/refund", productController.refundProduct);
// 쿠폰 API
const couponController = new CouponController(cashService2);
app.post("/coupons/buy", couponController.buyCoupon);
app.listen(3000);
따라서, 의존성주입을 한다는 것이 무조건적으로 싱글톤패턴을 의미하는 것은 아니게 된다.
의존성주입으로 얻을 수 있는 장점 2가지의 과정이 NestJS 에서는 자동으로 이루어지게 된다
따라서, new 를 사용하여 인스턴스를 만들어줘야했던 것들, 의존성주입 등을 NestJS가 해주게 되면서 NestJS 쪽으로 제어가 역전(IoC)되었다.
제어의 역전(Inversion of Control)은 일반적인 디자인 패턴 중 하나다.
일반적으로 개발자가 프로그램의 흐름을 제어하는 주체였다면, IoC의 개념이 나오게 되면서 프레임워크가 dependency를 container화 시켜 생명주기를 관리하게 되었다.
즉, dependency의 제어권이 개발자에서 프레임워크로 넘어가게 되었으며 이를 제어권의 흐름이 변경되었다고 하여
IoC(Inversion of Control)이라고 하게 되었다.
현금으로 구매하고 있던 쿠폰(상품권)을 포인트로 구매하는 API로 바꿔보자
이때, CouponController를 바꾸지 않고 cashService 대신 pointService를 의존성 주입시켜 주면 된다.
먼저, 06-08-express-with-DI-IoC → mvc → controller → services → point.service.js 파일을 아래와 같이 만들어 준다.
// point.service.js
export class PointService {
checkValue = () => {
console.log("포인트가 있는지 검사합니다.")
// 1. 가진포인트 검증하는 코드 (대략 10줄 정도)
// ...
// ...
// ...
// ...
}
}
06-08-express-with-DI-IoC → index.js 파일을 다음과 동일하게 수정해준다.
// index.js
import express from "express";
import { CouponController } from "./mvc/controllers/coupon.controller.js";
import { ProductController } from "./mvc/controllers/product.controller.js";
import { CashService } from "./mvc/controllers/services/cash.service.js";
import { ProductService } from "./mvc/controllers/services/product.service.js";
import { PointService } from "./mvc/controllers/services/point.service.js";
const app = express();
const productService = new ProductService();
const cashService = new CashService(); // 1. new 한번으로 모든 곳에서 재사용 가능(싱글톤패턴)
const pointService = new PointService(); // 2. 쿠폰 구매 방식이 포인트결제로 변경됨(의존성 주입)
// 상품 API
// 기존 의존성이 주입되고 있던 부분
const productController = new ProductController(cashService, productService);
app.post("/products/buy", productController.buyProduct); // 상품 구매하기
app.post("/products/refund", productController.refundProduct); // 상품 환불하기
// 쿠폰(상품권) API
// 의존성이 새로 주입된 부분
const couponController = new CouponController(pointService);
app.post("/coupons/buy", couponController.buyCoupon); // 쿠폰(상품권) 구매하기
app.listen(3000, () => {
console.log("백엔드 API 서버가 켜졌어요!!!");
});
point.service.js 파일을 new PointService()를 통해 pointService에 할당해 주었다.coupon.controller.js 파일에 pointService를 의존성 주입 시켜주기 위해 pointService를 생성자로 넣어 주었다.coupon.controller.js 파일에서 가진 돈 검증을 할 때에는 point 검증을 하게 되는 것이다.node index.js 명령어를 사용하여 서버를 실행시켜 coupons/buy 로 요청을 보냈을때 나타나는 console.log 응답이 바뀌는지 확인해 보자
POST 방식을 선택하고 localhost:3000/coupons/buy 작성하여 Send 버튼을 눌러 요청하면

터미널 창이 아래와 같이 포인트가 있는지 검증하는 것으로 바뀐다면 성공적으로 된 것이다.
