문제 보완하기 (3) - 타입스크립트 Repository, DTO, generic

1

NestJS에서 작업을 하면서 개념을 어렴풋이 알고만 있고 기능 구현에만 급급했던 부분이 있다. Repositorydto generic 타입이 대표적이다.

각 부분에 대해서 조금 더 구체적으로 질문을 작성하면 아래와 같다.

  1. Repository - 어떻게 typeorm이나 db와 매핑이 되는 걸까 (typeorm 작동원리)
  2. dto - 타입스크립트에서는 classinterface로도 입력/출력값의 타입(형태)을 지정해줄 수 있는데 dto는 왜 필요할까?
  3. generic 타입을 사용해도 되는 경우는 어떤 경우가 있을까

이제 각 질문들에 대한 답변을 중심으로 타입스크립트에서 헷갈린 내용을 정리하고자 한다.

Repository

개념

john ahn님의 nest강의를 중심으로 공부를 했기 때문에 강사님이 알려주신 내용을 중심으로 내가 이해한 내용을 정리하면 다음과 같다.

entity

레포지토리를 다루기 전에 우선 entity의 개념부터 다룰 필요가 있다. entity는 쉽게 생각해서 RDBMS에서 테이블에 해당하는 개념이다.

이렇게 별도의 entity파일을 만들어주는 이유는 typeorm에서 orm으로 작업을 할 시 해당하는 테이블이 존재해야만 CRUD를 진행할 수 있기 때문이다. 만약 엔티티가 없다면 직접 Raw Query로 CREATE TABE .....을 치면서 테이블을 정의해줘야 한다.

이렇게 테이블을 정의해주어야지만 각 테이블에 대한 Repository를 생성할 수 있다. 테이블에 접근하여 CRUD등을 하는 로직을 작성할 수 있는데 데이터베이스에 접근하는 로직을 한 파일에 만들어서 한번에 관리를 한다. Repository를 사용하는 패턴은 TypeormData-mapper방식을 이용한 것이다.

매핑원리

사실 내가 Repository개념을 헷갈렸던 이유는 아래 두가지 mapping방식을 헷갈렸기 때문이었다. 어떤 경우에는 DB 삽입할 때 new User 인스턴스를 만들어서 삽입을 하고 어떤 경우에는 Repository<User> 에 삽입을 진행했기 때문이다. 이번 기회에 정리를 하고 가야겠다. 잘 정리해 주신 분의 블로그를 참고해서 기록하려고 한다.
공식깃허브 typeorm- Active Record vs Data-mapper

Active Record

모델의 직접 접근 -> CRUD

이 방식은 entity를 정의하고 해당 데이터 테이블에 접근을 할 때 인스턴스를 만들어서 접근하는 방식이다. ( 인스턴스- 실제 - Active?라는 의미인듯) 따라서 entity를 정의하고 나서 해당하는 Class의 인스턴스를 만들어주어야 한다.

모델을 BaseEntity를 상속을 받아서 만들어준다. (.find(), .findOne(), .save(),Remove()사용 가능하도록)

  • Create- Update- Delete의 경우에는 하나의 instance를 클래스에 저장하고 변경하는 개념이니 instance 객체인 user를 사용한다.

  • Read의 경우에는 전체 클래스에서 조건에 맞는 경우를 찾아야 하니 User 클래스에서 찾는다.

User 클래스(모델) 자체로 바로 접근을 하는 것을 확인할 수 있다.

만일 custom한 데이터베이스 접근 함수를 만들고 싶다면 (eg. findByName) User 클래스를 정의할 때 static 함수(클래스의 메서드로만 호출 가능 / 인스턴스 메서드 호출 불가능 )로 같이 정의해주어야 한다.

Data Mapper

모델의 직접 접근하는 대신에 Repository 통해서 -> CRUD

별도의 상속없이 User 클래스를 만들어준다. @Entity로 테이블로 만들어지는 것은 알 수 있다.

그리고 만들어진 User class타입의 Repository를 상속하는UserRepository Class를 별도로 만들어준다.
(Repository 상속을 통해 .find(), .findOne(), .save(),Remove()사용 가능)

커스텀한 함수는 이 안에서 구현이 가능하고 사용할 때는 UserRepository의 메서드로 사용을 하면 된다.

  • Create- Update- Delete의 경우에는 userRepository를 통해서 삽입, 변경, 삭제를 한다.

  • Read의 경우에도 userRepository를 통해서 조회를 한다는 것을 알 수 있다.

User 클래스(모델) 로 접근을 하는 것이 아니라 모두 Repository를 통해서 접근을 하고 있다.

두 패턴의 장단점

공식문서에도 각 패턴별로 차이가 있다고 이야기하고 있다.

우선은 Active Record 같은 경우 빠르게 작성을 할 수 있기 때문에 규모가 작은 서비스에서 간단히 사용을 할 때 사용하면 좋다고 한다.

반면 레포지토리는 유지보수에 용이하기 때문에 규모가 큰 애플리케이션에서 사용하는 것이 좋다고 한다. 조금 더 자세한 내용은 아래 Repository 패턴을 사용하는 이유에 대해서 서술한다.

Repository 패턴

그러면 Repository 패턴을 사용하는 이유는 무엇일까? 기존 서비스 파일에서 데이터 가져오고 가공하는 과정을 거쳐도 되지만 이렇게 되면 중복의 문제가 발생한다.

서비스 로직 내부에서 중복

예를 들어 게시글 테이블에서 정보를 read 하는 로직을 한 서비스 메서드에서 여러 번 사용한다고 가정을 했을 때 해당 서비스를 작성할 때마다 다시 db에 접근해서 찾아오는 로직을 작성한다면 중복이 많이 발생할 것이다. 코딩에서 중복이란 human error를 야기하기 때문에 피할 수 있으면 피하는 것이 좋다.
그래서 데이터베이스에 접근해서 Find, Insert , Delete 등을 하는 액션만을 모아서 모듈화해서 관리한다.

다른 서비스 로직에서 참조 가능

만일 유저관련한 서비스 중에서 유저에 맞는 게시글을 찾는 기능을 만든다 했을 때 게시글 관련하여 만들어진 Repository에서 조회하는 함수만을 import해서 가져오면 별도로 로직을 다시 작성하지 않더라도 데이터베이스에서 정보를 가져올 수 있다. (모듈화의 장점!)

사용 데이터베이스가 바뀌는 경우 빠르게 변환 + 여러개 데이터베이스 연결

예를 들어 사용하던 데이터베이스가 Mysql에서 sqlite로 바뀌게 되는 경우 모듈화가 안되어있다면 매번 데이터베이스 정보를 바꿔주고 각 서비스 별로 접근 로직도 바꿔줘야 할 것이다. 실제로 튜토리얼 영상에서는 postgre로 진행을 했지만 과제 요구사항에서 in memory database를 요구해서 sqlite로 바꿔서 진행을 해보았다. 단순히 db 연결정보만 바꾸어주었더니 기존 로직이 그대로 작동했다. (놀라워라! )

만일 Enityt와 Repository로 정의가 되어있다면 해당 데이터베이스에 맞는 부분만 수정을 해주면 빠르게 데이터베이스 환경을 변경할 수 있는 것이다.

데이터베이스가 여러개 (메인 데이터베이스 : RDBMS, 로그 기록: NoSQL)인 경우 해당하는 Repository를 각각 만들어준 다음 필요한 부분에 함수를 import해서 사용하면 된다.

Repository는 typeorm에서 제공하는 Repository 클래스를 상속해서 만들 수 있다.

createBoard함수로 다른 모듈에서 board 테이블에 접근해서 데이터를 insert하는 로직을 사용할 수 있다.

서비스에서 해당 BoardRepository의 메서드인 createBoard를 사용하면 데이터를 삽입할 수 있다.


Data Transfer Object

개념

dt

사용 이유

class, interface 차이점


Generic type

제대로 공부를 하고 나니 기존에 알고 있던 내용과는 오히려 반대였었다. 기존에 타입을 지정해주는 것인 줄 알았는데 실제로는 동적 할당이 가능하도록 하는 것이었다. 어떻게 작동하는지 대략적으로 정리했다.

참고 문서 : https://joshua1988.github.io/ts/guide/generics.html
https://poiemaweb.com/typescript-generic

개념

위 참고문서의 표현을 빌리면 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다고 한다. 즉 입력되는 파라미터의 타입에 따라 타입이 결정되는 것이다.

타입스크립트에서는 타입을 미리 지정해주거나 any를 사용하는 방법으로 타입에러를 방지할 수 있다. 다만 타입을 항상 미리 지정해줄 수 없을 수도 있고 any로 지정했을 경우 Type 검사를 하지 않는다. 이럴 경우 동적으로 타입을 할당하는 방법이 Generic이다.

다양한 타입에 적용이 가능하기 때문에 재사용이 가능하다!

함수에 대해 string타입을 지정을 해주었고 입력값이 string인 경우 별 문제 없이 출력이 되는 것을 알 수 있다.

stringnumber로 바꾸었을때는 string이 number타입에 할당이 불가능하다고 나온다.

입력값을 number를 입력하고 number 타입과 비교하는 과정을 동일한 logText 함수로 진행이 가능하다. 즉 입력값에 따른 타입 체크가 동적으로 가능한 것이다.


주의해야 할 점은 입력값에 따라 타입이 지정되기 때문에 다음과 같은 경우 에러가 나온다.

.length메서드는 배열에서 count를 해주는 함수인데 위 제너릭함수에서는 배열이 입력된다는 보장이 없기 때문에 에러가 나오게 된다.

배열이 아니기 때문에 발생한 문제기 때문에 어떠한 타입이 들어가든 배열안에 담기도록 generic을 수정을 해주면 위와 같이 에러가 사라진 것을 볼 수 있다.

직접 타입 지정 vs generic 통해서 지정

직접 타입을 지정하는 경우는 입력되는 데이터 타입이 정확한 경우에 사용한다. DTO를 직접 만들어 지정하는 경우가 이에 해당된다. 반면에 generic은 입력되는 값의 타입이 여러가지가 될 수 있는 경우에 사용된다.

Repository<Class> 는 직접 만든 클래스를 입력해도 해당 클래스의 타입을 반환하도록 하는 generic함수를 이용한 것이다.

profile
기록을 통해 한 걸음씩 성장ing!

0개의 댓글