SOLID 원칙은 애자일 소프트웨어 개발 선언을 따르기 위한 방법이며, SOLID 원칙을 지킴으로써, 더 유연하고 유지보수가 용이한 코드를 작성할 수 있다.
이 주제로 팀원들과 스터디를 진행했는데, 개인적으로 글로 원칙을 암기하는 것보다 SOLID 원칙 각각에 대응하는 코드를 작성하는 것이 원칙을 이해하는 데 훨씬 유익했다.
SOLID 원칙을 타입스크립트, NestJS 코드로 익혀보자!
// before
class User {
name: string;
password: string;
email: string;
grade: string;
constructor(name: string, password: string, email: string) {
this.name = name;
this.password = password;
this.email = email;
this.grade = "BASIC";
}
introduce(): string {
return `hello! my name is ${name}!`
}
setUserPassword(password: string): void {
this.password = password;
}
setGrade(grade: string) {
this.grade = grade;
}
// ...
}
//after
enum Grade {
BASIC, VIP
}
class User {
name: string;
password: string;
email: string;
grade: Grade;
constructor(name: string, password: string, email: string) {
this.name = name;
this.password = password;
this.email = email;
this.grade = Grade.BASIC;
}
// ...
}
class UserService {
introduce(user: User): string {
return `hello! my name is ${user.name}!`
}
setUserPassword(user: User, password: string): void {
user.password = password;
}
setGrade(user: User, grade: Grade) {
user.grade = grade;
}
//...
}
// before
class Rectangle {
private width: number;
private height: number;
public shape: string = "rectangle";
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public area(): number {
return this.width * this.height;
}
}
class Circle {
private radius: number;
public shape: string = "circle";
constructor(radius: number) {
this.radius = radius;
}
public area(): number {
return Math.PI * this.radius ** 2;
}
}
class AreaCalculator {
private shapes: Circle [] | Rectangle[];
constructor(shapes: Circle[] | Rectangle[]) {
this.shapes = shapes;
}
public sum(): number {
let area = 0;
for (const shape of this.shapes) {
area += shape.area();
}
return area;
}
}
// after
interface Shape {
area(): number;
}
class Rectangle implements Shape {
private width: number;
private height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public area(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
private radius: number;
constructor(radius: number) {
this.radius = radius;
}
public area(): number {
return Math.PI * this.radius ** 2;
}
}
class AreaCalculator {
private shapes: Shape[];
constructor(shapes: Shape[]) {
this.shapes = shapes;
}
public sum(): number {
let area = 0;
for (const shape of this.shapes) {
area += shape.area();
}
return area;
}
}
// before
class Animal {
public move(): void {
console.log("Animal can move");
}
}
class Cat extends Animal {
public move(): void {
console.log("newnewnewnew 🐈");
}
}
class Fish extends Animal {
public move(): void {
console.log("꿈틀꿈틀 🐟");
}
}
// after
class Animal {
public move(): void {
console.log("Animal can move");
}
}
class Cat extends Animal {
public move(): void {
console.log("Cats can walk and run 🐈");
}
}
class Fish extends Animal {
public move(): void {
console.log("Fish can swim 🐟");
}
}
// before
interface Movable {
startEngine(): void;
drive(): void;
fly(): void;
}
class Car implements Movable {
public startEngine(): void {
console.log("Engine started for car 🚗🚗🚗");
}
public drive(): void {
console.log("Car is driving 🚗");
}
fly(): void {
}
}
class Plane implements Movable {
public startEngine(): void {
console.log("Engine started for plane 🛬");
}
public fly(): void {
console.log("Plane is flying 🛫");
}
drive(): void {
}
}
// after
interface Driveable {
startEngine(): void;
drive(): void;
}
interface Flyable {
startEngine(): void;
fly(): void;
}
class Car implements Driveable {
public startEngine(): void {
console.log("Engine started for car 🚗🚗🚗");
}
public drive(): void {
console.log("Car is driving 🚗");
}
}
class Plane implements Flyable {
public startEngine(): void {
console.log("Engine started for plane 🛬");
}
public fly(): void {
console.log("Plane is flying 🛫");
}
}
의존관계 역전 원칙을 준수하면
"극단적으로 2개의 저수준 모듈이 있고 그중 하나에 의존하는 10만 개의 고수준 모듈이 있을 때, 저수준 모듈 변경시 10만 번의 코드 변경이 일어나는 것을 IOC 컨테이너에서 모듈을 갈아 끼움으로써 한번의 코드 변경으로 해결할 수 있다"
Nest에서 의존관계 역전 원칙을 어떻게 준수할 수 있을까?
음... 적절한 예시가 떠오르지 않지만,
app.service가 getHello() 메서를 가진 인터페이스이고 app.controller가 getHello를 이용하는 고수준 모듈이라고 생각해보자.
우리는 잘 배운('Well-Educated') 개발자다. controller가 추상화(인터페이스)인 app.service에만 의존하면서 IOC 컨테이너인 app.module에서 그 구현체를 주입받게 하고 싶다.
예를 들면, getHello()가 환영 인사를 한국어로 건낼 수도, 영어로 건낼 수도, 일본어로 건낼 수도 있게 말이다!
단계별 코드를 보면서 의존관계 역전 원칙을 준수한 코드를 완성해보자.
// getHello()라는 인사 메서드를 가진 인터페이스 AppService
export interface AppService {
getHello();
}
// 인터페이스인 AppService를 구현해 한국말로 인사하는 getHello()를 가진 클래스 AppKoService
import { AppService } from './app.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppKoService implements AppService {
getHello(): string {
return '안녕하세요!';
}
}
// 인터페이스인 AppService를 구현해 영어로 인사하는 getHello()를 가진 클래스 AppEnService
import { AppService } from './app.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppEnService implements AppService {
getHello(): string {
return 'Hello World!';
}
}
// 인터페이스인 AppService를 구현해 일본어로 인사하는 getHello()를 가진 클래스 AppJpService
import { AppService } from './app.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppJpService implements AppService {
getHello(): string {
return 'イラッシャイマセ!';
}
}
Nest의 공식 문서 클래스 프로바이더와 클래스 사용에 따르면, provide에 토큰 값(AppService)을 지정 해주면 해당 토큰을 주입받는 고수준 모듈에서 useClass에 명시된 객체를 주입받는다고 설명되어 있다.
provide의 값으로 들어갈 토큰 value는 고수준 모듈이 의존하고 있는 추상화(AppService)에 일치해야 한다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppKoService } from './app.ko.service';
@Module({
imports: [],
controllers: [AppController],
providers: [
{
// provide: "제공할 추상화 인터페이스명"
provide: 'AppService',
// useClass: "제공할 추상화 인터페이스의 구현체"
useClass: AppKoService,
},
],
})
export class AppModule {}
이제 고수준 모듈에서 토큰 값을 사용해보자!
@Inject 데코레이터에 주입을 원하는 토큰 value를 넣어 AppService 타입인 변수 appService가 모듈에 명시된 useClass인 구현체를 주입받을 수 있게 만들면 끝이다.
이제 AppController는 구현체가 뭔지 모르는 상태에서도 의존성을 주입받아 다양한 언어의 인사를 '컨트롤러 내부 코드 수정 없이' 수행할 수 있게 되었다!
import { Controller, Get, Inject } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {
constructor(@Inject("AppService") private readonly appService: AppService) {
}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
한국어 인사 구현체 주입
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: 'AppService',
useClass: AppKoService,
},
],
})
export class AppModule {}
영어 인사 구현체 주입
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: 'AppService',
useClass: AppEnService,
},
],
})
export class AppModule {}
일본어 인사 구현체 주입
@Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: 'AppService',
useClass: AppJpService,
},
],
})
export class AppModule {}