728x90

저번에 사용했던 그림으로, Service는 Repository에서 데이터를 받아와서 비즈니스 로직을 다루는 layer이다.

 

Repository Layer를 대충 만들기는 했지만, 그래도 사용은 되기 때문에 이 Repository를 바탕으로 Service를 만들어보자.

 

우선 nest의 convention에 따라 service를 생성한다.

 

service에서는 repository에서 데이터를 받아오기 때문에, service가 repository에게 의존하는 관계가 된다.

 

내부에서 repository를 사용해야 하기 때문에, 생성자를 통해서 repository를 만들어서 가져오자.

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

 

우선 이렇게 만들기는 했지만, 이후에는 절대 이렇게 사용하지 않는다.

messagesRepository를 직접 만들어서 사용하는 것이 아니라, DI를 통해 주입받도록 만들어야 한다.

constructor에서 messagesRepository를 생성하지 않는 것이 목표다.

일단 이렇게 만들고 service layer의 개발에 집중해보자.

 

service에서 만들 메서드는 다음과 같다.

export class MessagesService {

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

    async findOne(id: string){}

    async findAll(id: string){}

    async create(id: string){}

}

내부 코드는 작성하지 않았지만, 이런 의문이 들 수도 있다.

어차피 이거 repository에 있는 내용이랑 똑같은데, 굳이 service를 만들어야 해?

 

하지만 스프링에서도 같은 구조를 사용했었고, 그렇기에 왜 이렇게 사용하는지는 알 수 있을 것이다.

Service Layer에서는 이런 repository의 메서드들 조합해 비즈니스 로직을 만들고, 이런 부분에 대해 공통 관심사인 트랜잭션 처리등의 역할을 하기 때문에 service Layer는 굉장히 중요하다.

 

export class MessagesService {

    constructor(private readonly messagesRepository: MessagesRepository) {
        this.messagesRepository = new MessagesRepository();
    }

    async findOne(id: string){
        return this.messagesRepository.findOne(id);
    }

    async findAll(id: string){
        return this.messagesRepository.findAll();
    }

    async create(content: string){
        return this.messagesRepository.create(content);
    }

}

결국 repository layer의 메서드들을 그대로 사용하는 코드들이지만, 그래도 만들어보도록 하자.

 

그리고 controller에 연결 한 후, http 요청을 보내보자.

controller에 작성한 코드는 간단하기에 생략하겠다.

 

이렇게 POST 요청을 보내니

messages.json 파일에 이렇게 생성이 되고

 

GET으로 해당 데이터를 가져올 수 있었다.

 

마지막으로 여기에 예외의 경우를 추가해보자.

당연히 없는 경우의 id로 조회할 수 있다.

 

이렇게 해당 데이터가 없는 경우에도 404 에러가 아닌, 정상적인 200 코드를 넘기고 있는 것을 볼 수 있다.

 

이런 경우에 NotFoundException을 사용해 예외를 추가해보자.

    @Get('/:id')
    async getMessage(@Param('id') id: string){
        const message = await this.messagesService.findOne(id)

        if(!message){
            throw new NotFoundException('Message not found');
        }

        return message
    }

이렇게 Null인 경우에 NotFoundException을 throw하면 nest에서 해당 예외에 맞는 에러코드로 변경해준다.

 

이렇게 해당 경우에 맞는 에러를 반환해주는 것을 볼 수 있다.

 

이번 글에서는 생성자에서 해당 의존성 클래스들을 생성해서 사용했었다.

다음 글에서는 이 부분을 바꿔보도록 하겠다.

728x90

우선 제목부터 나와있지만 해당 글에서는 데이터베이스를 사용해서 repository를 만드는 것이 아닌, 별도의 파일에 데이터들을 저장하고 그것을 repository로 불러오고 저장할 것이다.

 

우선 repository에 다음과 같은 메서드들을 만들것이다.

export class MessagesRepository {
    async findOne(id: string){
        
    }

    async findAll(){

    }

    async create(message: string){

    }
}

 

우선 구조만 이렇게 만들어두고, messages의 데이터들을 저장하기 위해 루트 디렉토리에 다음과 같은 파일도 생성한다.

이제 read, write로 해당 json 파일에 데이터를 저장할 것이다.

 

이제 repository의 남은 부분을 완성해보자.

 

우선 데이터는 이렇게 저장되어 있을 것이다.

{
	1: {
    	"id": 1,
        "content": "hi"
    }
}

그러면 우리는 1을 검색 할 때, [1]로 가져오면 되는 것이다.

 

이렇게 readFile로 메시지들을 읽어오고, Json으로 파싱한 후 해당 아이디의 값을 가져온다.

    async findOne(id: string){
        const messages = await readFile(`messages.json`, 'utf8');
        const messagesJson = JSON.parse(messages);

        return messagesJson[id];
    }

 

findAll()은 그냥 Json으로 파싱하고, 모든 데이터를 가져오면 된다.

    async findAll(){
        const messages = await readFile(`messages.json`, 'utf8');
        return JSON.parse(messages);
    }

 

마지막으로 가장 어려운 create이다.

 

아이디는 동시성 문제 때문에 우선 random으로 생성한다.

 

그리고 모두 Json으로 불러오고, 해당 Json에 추가하는 값을 넣은 후 다시 파일에 작성하는 방법으로 데이터를 저장한다.

사실 어차피 파일 시스템이라 동시성 문제가 발생하기는 한다...

    async create(content: string){
        const messages = await readFile(`messages.json`, 'utf8');

        const messagesJson = JSON.parse(messages);

        const id = (Math.random() * 9999)

        messagesJson[id] = {id, content};

        await writeFile(`messages.json`, JSON.stringify(messagesJson), 'utf8');
    }

 

이렇게 임시로 repository layer를 만들어보았다.

 

당연히 원래는 데이터베이스로 만들어야 하지만, 임시로 텍스트 파일을 사용했으며 테스트는 service layer까지 만들어보고 진행할 예정이다.

728x90

우선 Post를 통해 요청되는 값 자체를 콘솔로 찍어보자.

값을 이렇게 보내면, 위의 부분이 Header이고 아래부분이 Body이다.

 

다음과 같은 핸들러로 요청을 출력해보니

    @Post()
    async createMessage(@Body() body: any, @Headers() headers: Record<string, string>) {
        console.log('📌 Headers:');
        for (const [key, value] of Object.entries(headers)) {
            console.log(`  ${key}: ${value}`);
        }

        console.log('📌 Body:');
        console.log(JSON.stringify(body, null, 2));
    }

 

그냥 그대로 나오는 것을 볼 수 있다.

그럼 우리는 이제 사용자의 요청에 대하여, 이 헤더와 바디를 통해서 처리를 하도록 할 것이다.

 

그럼 일단 body부터 원하는 데이터로 가져올 수 있도록 하자.

해당 데이터를 이렇게 any 타입이 아닌, 우리가 원하는 값으로 가져오기 위해 DTO를 작성한다.

 

export class CreateMessageDto {
    content: string
}

현재는 가져오는 값이 content 밖에 존재하지 않기 때문에, content 필드 하나만 가지고 있는 DTO를 생성한다.

 

    @Post()
    async createMessage(@Body() body: CreateMessageDto) {
        console.log(`This is content: ${body.content}`);
    }

그리고 body의 타입을 CreateMessageDto로 직접 지정해준 후에 재요청해보자.

 

그러면 이렇게 타입이 적절하게 지정된 것을 볼 수 있다.

 

하지만 이렇게 정상적인 값이 아니라

POST http://localhost:3000/messages
Content-Type: application/json

{
}

이렇게 본문이 비어있거나, 

POST http://localhost:3000/messages
Content-Type: application/json

{
  "content": null
}

null인 값이 있어도 

위와 같이 정상 응답을 주게 된다.

 

우리는 이렇게 잘못된 요청에 대하 BadRequest인 400 응답을 주어야 한다.

 

이렇게 자동으로 체크를 해서 응답을 해줄 수 있도록, validation pipe를 사용해보자.

 

사용자의 요청과 응답이 서버에서 위와 같은 순서로 처리가 되는데, 이 중 가장 먼저 이루어지는 데이터의 유효성 체크이다.

 

현재 CreateMessageDto는 content를 string 타입으로 받고 있다.

그렇기에 content가 string인 것을 확인하고 나서야 요청 처리를 시작해야 한다.

 

class-validator를 사용해보자.

우선 이런 타입에 대한 체크는 모든 핸들러에서 이루어지기 때문에, 전역으로 validation pipe를 추가한다.

 

main.ts에서 app에 다음 속성을 추가하자.

  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true
  }));

이러면 모든 request에 대하여 dto에 타입체크가 시작된다.

 

이제 dto에도 데코레이터를 추가하자.

export class CreateMessageDto {
    @IsString()
    content: string
}

이렇게 string 타입이기 때문에 @IsString() 데코레이터를 추가해준다.

 

그렇게해서 잘못된 요청을 보내보자.

그렇게 요청해보니, 잘못된 요청이라며 400에러가 나오는 것을 볼 수 있었다.

 

근데 여기서 궁금한 부분이 있다.

우리가 만든 TypeScript는 JavaScript로 변환되어 동작하게 되는데, 그럼 이 validation pipe는 어떻게 동작하는 것인가.

 

tsconfig.json을 보면 다음과 같은 부분이 있다.

 

이 설정을 통해서 데코레이터의 정보가 자바스크립트에도 포함이 되게 된다.

변환된 자바스크립트의 dto를 보면 다음과 같이 나와있다.

 

__decorate([
    (0, class_validator_1.IsString)(),
    __metadata("design:type", String)
], CreateMessageDto.prototype, "content", void 0);

데코레이터로 파라미터가 string 타입임을 알리고 있는 것이다.

 

이렇게 자바스크립트에서도 타입에 관하여 동작할 수 있도록 한다.

728x90

우선 명령어를 입력한다.

사실 이 nest 프로젝트를 생성해주는 명령어가 있다고 한다.

 

nest new .

 

이 명령어를 터미널에 입력하면, 현재 폴더에 nest 프로젝트가 생성된다.

 

그러면 모든 nest 프로젝트에 필요한 파일들이 설치된다.

 

여기서 우리는 app module을 사용할 것은 아니기에 main.ts를 제외하고 모두 삭제해도 된다.

 

이제 컨트롤러와 모듈을 추가해야 하는데, 스프링에서는 직접 파일들을 만들면서 연결해주었지만 nest는 터미널 명령어를 사용해 연결한다.

 

  우선 message 모듈을 작성할 것이기에 messages 모듈을 만들어보자.

 

nest g module messages

여기서 g 옵션은 generate이며, messages라는 모듈을 생성하라는 명령어이다.

 

이어서 컨트롤러도 생성해보자.

nest g controller messages

이러면 controller의 테스트 파일과 controller가 생성된다.

 

 

messages 모듈이 3000번 포트에서 동작할 수 있도록, main.ts에 추가해주고

async function bootstrap() {
  const app = await NestFactory.create(MessagesModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

 

message module을 확인해보면, 다음과 같이 컨트롤러가 추가된 것을 볼 수 있다.

 

컨트롤러를 확인해보기 위해, 간단한 Get 메서드를 작성해보고

@Controller('messages')
export class MessagesController {

    @Get()
    listMessages(): string{
        return "hi Seungkyu!!"
    }
}

 

다음과 같이 http 파일을 작성해서 테스트 해보았다.

GET http://localhost:3000/messages

 

일단은 이렇게 정상적인 응답이 오는 것을 보아서는, 아직까지는 성공한 것 같다!

728x90

자 우선 nest를 그냥 컨트롤러만 만들어서 API만 쏴보자.

 

우선 nest를 시작할 폴더에서 terminal에 다음과 같이 입력해준다.

 npm install @nestjs/common@7.6.17 @nestjs/core@7.6.17 @nestjs/platform-express@7.6.17 reflect-metadata@0.1.13 typescript@5.5.

 

이러면 종속성? 들이 추가된다.

 

그리고 src 폴더를 만들고, 거기에 main.ts 파일을 만들어준다.

 

당연히 자바에서도 그렇지는 않았지만, 나중에 옮기기로 하고 일단 main.ts에 모든 코드를 작성해보도록 하자.

 

우선 controller이다.

클라이언트와 서버의 인터페이스의 역할을 한다.

 

import {Controller, Get} from '@nestjs/common';

@Controller()
class SeungkyuController{

    @Get()
    getSeungkyu(){
        return "hi seungkyu"
    }
}

자바에서는 어노테이션이라고 했지만, 여기서는 데코레이터라고 하는 거 같다.

 

이렇게 컨트롤러를 만들고, nest에서는 하나 이상의 모듈이 있어야 한다고 한다.

그리고 그 모듈에 컨트롤러를 추가해야 한다고 한다.

 

그렇기에 이 컨트롤러를 모듈에 추가해보자.

import {Module} from '@nestjs/common';

@Module({
    controllers: [SeungkyuController]
})
class SeungkyuModule{}

아마 각 코드들이 어떻게 동작하는지는 자세히 모르겠지만, 일단 뼈대만 작성해서 동작시켜보고 나중에 알아보도록 하자.

 

마지막으로는 spring의 application.java와 같은 부분이다.

import {NestFactory} from "@nestjs/core";
import {NestExpressApplication} from "@nestjs/platform-express";

async function bootstrap() {
    const app = await NestFactory.create<NestExpressApplication>(AppModule);
    await app.listen(3000);
}

bootstrap();

이렇게 해당 모듈을 등록하고, 동작시켜보자.

 

그럼 다음과 같이 로그가 나온다.

 

우리가 등록했던 3000 포트로 접속해보면

그리고 nest에서는 controller를 SeungkyuController 이렇게 파일을 만드는 것이 아니라

seungkyu.controller.ts

 

이런식으로 파일을 생성한다고 한다.

일단 nest를 동작만 시켜보았다.

앞으로 당분간 nest에 집중해서 제대로 알아보도록 하자.

728x90

기존에 자바로 개발을 했기에, 이런 인터페이스라는 개념은 정말 많이 사용했었다.

다형성, 추상화의 필수 개념으로 각 클래스들에서 공통적인 부분을 뽑아 상위 클래스 혹은 인터페이스 타입으로 각각 다른 동작을 수행할 수 있도록 만드는 방법이다.

이런 개념이 타입스크립트에도 존재한다고 한다.

물론 이 인터페이스를 통해 얼마나 객체 지향적인 개발을 할 수 있을지는 모르겠지만, 그래도 개념을 최대한 사용할 수 있도록 해보자.

 

전에 잠깐 블로그에 사용해서 글을 작성한 적이 있기는 하다.

interface Person{
    name: string;
    age: number;
    email: string;
}

이런 방법으로 선언한다.

 

사람이라는 객체가 가지고 있는 공통적인 특징들을 추출해서 정의한 것이다.

그리고 이러한 인터페이스를 바탕으로 함수를 파라미터로 넘긴다.

function sendEmail(person: Person){
    console.log(`${person.email}에게 메일을 전송했습니다.`);
}

 

만약 이런 함수가 존재한다면, 해당 key와 타입을 멤버로 가지고 있는 객체 혹은 객체 리터럴만 파라미터로 사용이 가능한 것이다.

 

let seungkyu_student = {
    name: "seungkyu",
    age: 27,
    email: "trust1204@gmail.com"
}

이런 객체 리터럴이나

 

아니면 그냥 이렇게 타입으로 인터페이스를 지정해둔

let seungkyu_army: Person = {
    name: "seungkyu",
    age: 25,
    email: "trust1204@gmail.com"
}

이런 객체 리터럴만 사용이 가능한 것이다.

 

여기에서 seungkyu_student라는 친구에 study()라는 함수를 추가해보자.

let seungkyu_student = {
    name: "seungkyu",
    age: 27,
    email: "trust1204@gmail.com",
    study(){
        console.log("승규는 공부 중입니다.")
    }
}

 

이렇게 추가를 하더라도

name: string,

age: number,

email: string

이런 멤버들은 여전히 객체 리터럴 안에 존재한다.

 

그렇기에 sendEmail 함수에 위의 객체 리터럴을 사용하더라도 에러가 발생하지 않는다.

 

이게 자바의 인터페이스와 다른 부분인 것 같다.

자바에서 인터페이스는 안의 멤버가 같더라도 상속을 받은 객체가 아니면 해당 함수들을 사용할 수는 없었다.

 

인터페이스에는 멤버 변수 외에도 함수를 넣을 수도 있다.

interface Person{
    name: string;
    age: number;
    email: string;
    walk(): void
}

이렇게 ()로 함수를 지정하고 반환타입을 지정해주면 된다.

 

이렇게 수정하면 기존에 Person 타입으로 수정해둔 모든 객체 리터럴에도 함수의 생성이 강요된다.

 

그리고 seungkyu_student에도 walk() 함수가 구현되어 있지 않기 때문에, Person 타입으로 볼 수 없어 파라미터로 사용할 수 없다고도 나온다.

 

분명 인터페이스는 좋은 방법이지만, 기존의 자바보다 더욱 자유로운 개념을 가진 것 같다.

최대한 객체지향으로 맞추어 타입스크립트에서의 인터페이스를 사용해보자.

 

 

'Node > TypeScript' 카테고리의 다른 글

TypeScript에서 튜플  (0) 2025.06.29
TypeScript에서 배열  (1) 2025.06.29
TypeScript에서 any 타입  (0) 2025.06.29
TypeScript의 타입 어노테이션 및 타입 추론  (0) 2025.06.29
TypeScript를 쓰는 이유  (0) 2025.06.28
728x90

잘 사용은 하지 않는다고 하지만, 그래도 한 번 알아보도록 하자.

 

let seungkyu = [
    "seungkyu",
    "trust1204@gmail.com",
    24
]

 

이런 변수가 있다고 해보자.

아마 위에서부터 이름, 이메일, 나이가 될 것이다.

 

하지만 그냥 배열로 넣었기에 어떤 위치에 어떤 타입의 값이 오는 것을 강제할 수 없다.

let seungkyu2 = [
    "seungkyu",
    24,
    "trust1204@gmail.com"
]

그렇기에 이런 값으로도 올 수 있는 것이다.

 

이러한 이유로 타입의 위치를 강제하기 위해, []안에 타입의 위치를 지정해준다.

 

let seungkyu1: [string, string, number] = [
    "seungkyu",
    "trust1204@gmail.com",
    24
]

이런 식으로 만들면 number와 string의 위치를 바꿀 수 없게 된다.

 

그냥 만드는 거보다는 유용하긴 하지만, 사실 이렇게 만들어도 seungkyu가 이름이고 trust1204@gmail.com이 이메일이라는 정보는 충분히 주지 못한다.

 

그렇기에 차라리 객체 리터럴 방식을 많이 사용한다고는 한다.

그래도 일단 알아는 두자.

'Node > TypeScript' 카테고리의 다른 글

TypeScript에서 인터페이스  (0) 2025.06.29
TypeScript에서 배열  (1) 2025.06.29
TypeScript에서 any 타입  (0) 2025.06.29
TypeScript의 타입 어노테이션 및 타입 추론  (0) 2025.06.29
TypeScript를 쓰는 이유  (0) 2025.06.28
728x90

자바스크립트에서의 배열은 이렇게 선언했었다.

 

타입이 없기 때문에 그냥 아무 값이나 배열에 추가했었다.

 

타입스크립트에서는 배열에도 어떤 타입의 값들을 넣을지 지정해준다.

let lang: string[] = ["c", "c++"," java"]

 

이런식으로 타입[]을 사용해 해당 타입이 배열임을 알려준다.

그리고 당연히 다른 타입을 값을 넣으려고 하면, 이렇게 에러가 발생한다.

 

만약 한 배열에 string과 number를 같이 넣고 싶다면

let lang: (string | number)[] = ["c", "c++"," java", 1]

이렇게 | 를 사용해서 string 혹은 number가 있다는 것을 알려준다.

하지만 배열이라는게 같은 타입의 객체들을 모으기 위해 사용하는 친구이기에, 이런 식으로 다양한 타입의 배열을 피하도록 하자.

 

이렇게 배열에 타입을 사용하는 이유가 있을까?

이렇게 배열 안의 원소들에 대해 타입을 지정해놔야.

해당 배열의 원소들에 접근할 때, 해당 원소들의 메서드를 가져올 수 있다.

 

그리고 해당 map에서 리턴타입을 빼도

이렇게 만들어지는 배열에 대한 타입 추론을 알아서 할 수 있다.

 

타입스크립트를 사용하는만큼 최대한 타입을 지정해서 사용할 수 있도록 하자.

'Node > TypeScript' 카테고리의 다른 글

TypeScript에서 인터페이스  (0) 2025.06.29
TypeScript에서 튜플  (0) 2025.06.29
TypeScript에서 any 타입  (0) 2025.06.29
TypeScript의 타입 어노테이션 및 타입 추론  (0) 2025.06.29
TypeScript를 쓰는 이유  (0) 2025.06.28

+ Recent posts