낙서장이자 오답 노트이자 컨닝 페이퍼
패스트캠퍼스 환급챌린지 13일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기 본문
패스트캠퍼스 환급챌린지 13일차 : 코드팩토리의 백엔드 아카데미 : 한 번에 끝내는 NestJS 패키지 - 기초부터 MSA까지 강의 후기
NangIn 2025. 4. 13. 21:25본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
강의 내용 정리
Advanced Options & 통계 쿼리 이론
기본 연산자
- Equal: 같은 값을 찾을 때 사용
-
const users = await userRepository.find({ where: { age: Equal(25) }, })
-
- Not: 아닌 값을 찾을 때 사용
-
const users = await userRepository.find({ where: { age: Not(25) }, })
-
- LessThan, LessThanOrEqual: 작거나, 작거나 같은 값
-
const users = await userRepository.find({ where: { age: LessThan(30) }, }) const users = await userRepository.find({ where: { age: LessThanOrEqual(30) }, })
-
- MoreThan, MoreThanOrEqual: 크거나, 크거나 같은 값
-
const users = await userRepository.find({ where: { age: MoreThan(30) }, }) const users = await userRepository.find({ where: { age: MoreThanOrEqual(30) }, })
-
- Between: 사이 값을 찾을 때 사용
-
const users = await userRepository.find({ where: { age: Between(20, 30) }, })
-
문자열 연산자
- Like: 문자열 매치되는 값 찾을 때 사용(대소문자 구분)
- %는 찾으려는 값 아님 → 어떤 값이 이 이후에는 들어가도 된다는 뜻
-
const users = await userRepository.find({ where: { firstName: Like('Co%') }, })
- ILike: 문자열 매치되는 값 찾을 때 사용(대소문자 구분X)
-
const users = await userRepository.find({ where: { firstName: ILike('co%') }, })
-
목록 비교
- In: 리스트에 매칭되는 값을 찾음
-
const users = await userRepository.find({ where: { age: In([20, 25, 30]) }, })
-
배열 연산자 (비추천) → SQL에서 배열 쓰는 대신 테이블 관계를 만드는 걸 권장
- ArrayContains: 완전히 같은 배열
- ArrayContainedBy: 필터 값이 배열에 모두 포함될 경우
- ArrayOverlap: 배열 값 중 겹치는 게 있을 경우
-
ArrayContains(['Admin']) // 배열 전체 일치 ArrayContainedBy(['Admin', 'User']) // 배열이 이 값 안에 포함 ArrayOverlap(['Admin', 'Guest']) // 배열 값 일부라도 겹침
-
기타 조건
- IsNull(): Null인 값을 필터링할 때 사용
-
const users = await userRepository.find({ where: { profile: IsNull() }, })
-
조합 조건
- Or: OR 로직 연산할 때 사용 → 여러개 중 하나라도 맞으면 찾음
-
const loadedPosts = await dataSource.getRepository(Post).findBy({ title: Or(Equal("About #2"), ILike("About%")), })
-
- And: AND 로직 연산할 때 사용 → 여러개 다 일치해야 찾음
-
const loadedPosts = await dataSource.getRepository(Post).findBy({ title: And(Not(Equal("About #2")), ILike("%About%")), })
-
- Not(Equal(x)): Not(Equal(2)) → != 2
복합 옵션 조합 예시
-
const users = await userRepository.find({ where: [ { isActive: true, age: MoreThan(25) }, { isActive: "John", age: LessThan(50) }, ], order: { firstName: 'ASC' }, relations: ["profile"], select: ['firstName', 'lastName'], skip: 0, take: 10, cache: true, loadRelationIds: true, loadEagerRelations: false, withDeleted: false, });
통계 관련 기능 (Aggregate Functions)
- count(): 개수 반환
- repo.count({ where: { isActive: true } })
- sum(): 총합 반환
- repo.sum('age', { firstName: 'John' })
- average(): 평균
- repo.average('age', { firstName: 'John' })
- min() / max(): 최소/최대값
- repo.min('age', { firstName: 'John' })
- repo.max('age', { firstName: 'John' })
Advanced Options & 통계 쿼리 적용
title 기반 필터 기능 구현
- 기본 조건 분기 처리
-
getManyMovies(title?: string) { if (!title) { return this.movieRepository.find(); } return this.movieRepository.find({ where: { title: Like(`%${title}%`), }, }); }
- title 값이 없으면 전체 영화 리스트 반환
- title이 존재하면 해당 키워드가 포함된 영화만 반환
-
- LIKE 쿼리 적용 방식
- %${title}% 사용: 문자열 어디에든 해당 키워드가 포함되어야 통과
- % 위치에 따라 필터링 결과가 달라짐
- %반지의 제왕%: 제목 중간에도 포함되면 통과 (ex: "호빗과 반지의 제왕")
- 반지의 제왕%: 앞이 정확히 일치해야 함 (ex: "반지의 제왕 1", "반지의 제왕 2")
- %반지의 제왕: 뒤가 정확히 일치해야 함
- 예시 테스트
- "반지의 제왕" 검색 시 1~3편 모두 출력
- "호빗과 반지의 제왕" 추가했을 때 %반지의 제왕% 조건이면 포함됨
통계 기능: Count & FindAndCount
- findAndCount() 사용
-
return this.movieRepository.findAndCount({ where: { title: Like(`%${title}%`), }, }); - 반환값: [영화 리스트, 해당 조건의 총 개수]
- 필터링 + 카운트 동시 수행 가능
- ex) "반지의 제왕" 포함 영화 리스트 + 개수(4)
-
- count() 사용
-
return [ await this.movieRepository.find(), await this.movieRepository.count(), ]; - 전체 또는 조건에 해당하는 row 수만 반환
- where 조건 없이 전체 영화 수 반환
- where 조건 지정 시 특정 장르, 키워드 등 필터링 가능
-
- 실전 중요성
- 실무에서는 페이징 처리에 count가 반드시 필요
- 예: 전체 200만 개 중 20개만 보여줘야 할 때 → count로 전체 개수 전달
- 실무에서는 페이징 처리에 count가 반드시 필요
Entity Embedding & Entity Inheritance 이론
엔티티 임베딩 (Entity Embedding)
- 개념 요약
- 독립된 테이블로 만들지 않는 클래스를 다른 엔티티에 포함시키는 방식
- 재사용 가능한 필드 묶음을 한 클래스에 정의한 뒤, 다른 엔티티에서 import 하여 사용 → @Column(() => Name)으로 사용
- @Entity()는 붙이지 않지만, 내부 프로퍼티에는 @Column() 등을 사용 가능
class Name { @Column() first: string; @Column() last: string; } @Entity() class User { @PrimaryGeneratedColumn() id: number; @Column(() => Name) name: Name; @Column() isActive: boolean; } @Entity() class Employee { @PrimaryGeneratedColumn() id: number; @Column() salary: number; @Column(() => Name) name: Name; } - 테이블 결과
- User 테이블 → name_first, name_last 컬럼 생성
- Employee 테이블 → name_first, name_last 컬럼 생성
- 장점
- 중복 필드를 깔끔하게 재사용 가능
- 유지보수 시 필드 수정이 편함
엔티티 상속 (Entity Inheritance) → 강사님은 이 방식 더 선호
- Abstract Class 기반 상속
- 중복되는 필드를 상위 클래스에 정의 후, 하위 엔티티에서 상속
- 상위 클래스는 @Entity() 안 붙이고, 하위 클래스에만 붙이면 됨
abstract class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() description: string; } @Entity() class Photo extends Content { @Column() size: string; } @Entity() class Post extends Content { @Column() viewCount: number; } - 테이블 결과
- Photo → id, title, description, size
- Post → id, title, description, viewCount
- 이름이 원래대로 나옴
- 장점
- OOP스럽게 공통 필드를 묶어 관리 가능
- 실질적으로 테이블은 각각 따로 생성됨 (자식마다 독립)
싱글 테이블 상속 (Single Table Inheritance) → 유용한지 모르겠다고 함
- 개념 요약
- 상속 구조를 하나의 테이블에 통합
- @Entity()는 부모 클래스에만 적용하고, 자식 클래스에는 @ChildEntity() 사용
- @TableInheritance()로 상속 전략 정의
@Entity() @TableInheritance({ column: { type: 'varchar', name: 'type' } }) class Content { @PrimaryGeneratedColumn() id: number; @Column() title: string; @Column() description: string; } @ChildEntity() class Photo extends Content { @Column({ nullable: true }) size: string; } @ChildEntity() class Post extends Content { @Column({ nullable: true }) viewCount: number; } - 테이블 결과
- 하나의 테이블 content 생성
- 모든 컬럼 포함: id, title, description, size, viewCount, type
- 자식 클래스 전용 컬럼은 nullable 처리 필수
- → size는 photo에만 존재
- type 컬럼의 역할
- 각 row가 어떤 자식 클래스(Photo vs Post)에서 생성된 건지 구분하는 식별자 역할
- 장단점장점 단점
테이블 하나로 처리 가능 → 조인 비용 없음 자식 클래스가 많아질수록 nullable 컬럼 증가 관리 편함 구조적으로 느슨하고 유연성 떨어짐
마무리 요약
- Entity Embedding: 공통 필드들을 하나의 클래스에 묶어서 다른 엔티티에서 포함
- Entity Inheritance: 추상 클래스처럼 상속 구조로 필드 재사용
- Single Table Inheritance: 하나의 테이블에 모든 자식 정보를 담는 전략
- 각 방식은 상황에 따라 장단점이 다르며, 실무에서는 주로 Entity Inheritance와 Embedding 방식이 더 선호되는 편
학습 후기
TypeORM의 고급 쿼리 옵션과 통계 기능, 그리고 Entity Embedding과 Inheritance 개념에 대해 학습하면서 단순한 CRUD를 넘어서 보다 정교한 데이터 처리와 구조 설계가 가능하다는 점을 실감할 수 있었습니다. 특히 Like, ILike, Between, MoreThan, In 등 다양한 연산자들을 조합해 조건에 맞는 데이터를 유연하게 조회할 수 있다는 점은 실무에서 복잡한 검색 조건을 구현할 때 매우 유용하겠다는 생각이 들었습니다. 또한 findAndCount()는 데이터를 가져오면서 동시에 페이징 처리를 위한 전체 개수까지 함께 반환해주는 점에서, 실제 프론트엔드와 연동되는 API 구현 시 효율적인 설계를 가능하게 해준다고 느꼈습니다.
Entity Embedding과 Inheritance는 코드 재사용성과 응답 구조 면에서 뚜렷한 차이를 보여주었습니다. Embedding은 중복되는 필드를 한 객체로 묶어 구성할 수 있어 유지보수 측면에서 강력한 이점이 있지만, JSON 응답 구조가 중첩되어 생기는 불편함이 있을 수 있습니다. 반면 Inheritance는 OOP적 사고를 반영한 방식으로, 중복 필드를 상속받아 깔끔하게 처리할 수 있었고, 응답 또한 평탄한 구조로 제공되기 때문에 실전에서 더 자주 활용할 수 있겠다는 생각이 들었습니다. 특히 BaseEntity를 추상 클래스로 만들어 중복 메타데이터를 관리하는 방식은 NestJS + TypeORM 조합에서 자주 보게 될 패턴이라 생각합니다.
마지막으로 Single Table Inheritance는 테이블이 하나로 합쳐지는 장점이 있긴 했지만, 자식 엔티티가 많아질수록 테이블 구조가 복잡해지고, nullable 필드가 과도하게 많아지는 구조적 단점이 크게 느껴졌습니다. 따라서 프로젝트 규모와 복잡도에 따라 신중하게 선택해야 할 전략이라는 생각이 들었습니다. 전반적으로 고급 쿼리와 구조 설계를 배우면서 단순히 기능 구현을 넘어서, 데이터 구조 자체를 설계하고 유지하기 위한 여러 전략들을 체득할 수 있었던 강의였습니다.
학습 인증샷



