ORM, DI


이 글은 추가/수정 될 예정입니다

마지막으로 수정된 날짜 2020.12.29

ORM

서버 개발의 주된 패러다임은 객체지향 프로그래밍이다. 프로그램을 이루는 요소를 객체(Class)로 추상화하는 것을 말한다. 많은 경우에 서버는 영속성을 가진 데이터베이스(RDB, NoSQL)에 저장한다. 하지만 데이터베이스는 객체를 모른다. 관계형 데이터베이스의 경우 테이블과 스키마, 비 관계형 데이터베이스의 경우 문서, Key-value 쌍 등으로 데이터를 정의한다. 여기서 ORM은 개발자가 객체-기반 사고방식을 데이터베이스에 접근해 로직을 설계할 때도 지속될 수 있도록 도와주는 역할을 한다.

아래의 첫번째 사진은 관계형 데이터베이스의 Memo 테이블이고, 이어서 나오는 코드는 Memo 테이블을 클래스로 구현하고 스키마를 Memo 내부 속성으로 정의하고 있다.

memo table

// sequelize-typescript @Table export class Memo extends Model<Memo> { @Column title: string; @Column contents: string; @Default(() => false) @Column secret: Boolean; @CreatedAt @Column createdAt!: Date; @UpdatedAt @Column updatedAt!: Date; @ForeignKey(() => User) @Column userId: number; }

ORM 사용의 장점

가장 큰 장점으로 객체지향 방식을 데이터를 다루는 코드를 작성할 때에도 이어갈 수 있다. 서버의 가장 주된 기능은 데이터베이스와 통신해 데이터 베이스를 생성, 조회, 수정, 삭제 하는 것이다. 그러므로 서버 코드의 상당 부분은 데이터베이스에 의존할 수 밖에 없다.

ORM을 사용하지 않으면 SQL Query를 사용해 데이터베이스를 제어해야 한다. 이 경우 작은 실수부터, 매번 ERD를 참고 해야하기 때문에 번거로울 수 있다. ORM을 사용하게 되면 테이블과 내부 스키마까지 매핑 되어있는 (친숙한)객체를 사용할 수 있어 용이하다.

또한 데이터베이스에는 존재하지 않지만 OOP에는 존재하는 상속을 사용해 보다 유지보수에 용이한 코드를 작성할 수 있게 되고, 경우에 따라 한개의 데이터베이스 테이블을 두개의 객체로 나누는 등 유동적인 설계가 가능하다. (Granularity)

ORM 사용의 단점

프로젝트의 복잡성이 증가하는 경우 ORM 사용의 난이도 자체가 올라갈 수 있다고 한다. n+1 문제로 대표되는 성능 이슈가 대다수로 보인다. 필요 이상의 raw query를 요청해 성능 저하를 방지하기 위해 실제로 변환되는 raw query를 살펴보며 코드를 수정하는 등 주의가 필요하다.

Dependency injection

의존하고있는 A 객체를 내부에서 생성하는 것이 아닌, 외부에서 생성한 A 객체를 내부에 주입해 사용하는 것을 말한다. 이 때 주입하는 객체의 관리(중복 생성 방지)를 위해 nest.js 의 경우 singleton 패턴으로 프레임워크 단에서 관리하는데(Provider), 이를 IoC(inversion of Control) Container 라고 한다. 장점은 개발자가 클래스의 생성, 소멸등에 신경쓰지 않아도 되게 해준다.

Dependency Inversion Principle

의존관계 역전 원칙 이란 모듈 간에 의존관계를 맺을 때 구체적인 클래스 보다 추상 클래스 / interface 등 추상화에 의존하는 것을 말한다. 아래 nest.js 로 구현된 memo 컨트롤러를 통해 조금 더 이해해보자.

memo.controller.ts

import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard'; import { CreateMemoDto } from './dto/create-memo.dto'; import { MemoService } from './memos.service'; @Controller() export class MemoController { // MemoService는 @injectable 데코레이터가 사용되어 Nest의 IoC에 의해 관리된다. constructor(private memoService: MemoService) {} @UseGuards(JwtAuthGuard) @Get('memos') findAll(@Req() req) { const { userId } = req.user; return this.memoService.findAll(userId); } @UseGuards(JwtAuthGuard) @Post('memo') create(@Req() req, @Body() payload: CreateMemoDto) { const { userId } = req.user; return this.memoService.create(userId, payload); } }

MemoController는 MemoService의 메소드를 사용하고 있는데, 내부에서 memoService를 직접 생성하지 않고, constructor에 MemoService (interface) 의존 관계를 명시했다.

이 방법의 장점으로 모듈간의 느슨한 결합(인터페이스 사용)이 가능하게 되어, 컴파일 타임에 모듈이 결합되지 않고, 실행될 때 결합된다. 따라서 테스트에도 용이하다.

반복의 느린 變化
oowgnoj github
© 2022, by oowgnoj