GitHubGitHub
← 홈으로 돌아가기
backend·

UsersModuleUsersController로 이해한 사용자 API 요청 흐름

NestJS에서 UsersModule과 UsersController를 통해 사용자 API 요청이 들어오고 Service로 전달되는 흐름을 정리한 글

UsersModule과 UsersController로 이해한 사용자 API 요청 흐름

이번 글에서는 사용자 관련 API 요청이 어디로 들어오는지 살펴보려고 한다.

전체 흐름은 대략 다음과 같다.

프론트 요청
UsersController
UsersService
PrismaService
DB

이번 글에서는 아직 UsersService 내부의 DB 로직까지 깊게 들어가지 않고, 먼저 UsersModuleUsersController를 중심으로 정리한다.

운영 중인 서비스 코드이기 때문에 실제 코드를 그대로 공개하지 않고, 핵심 구조만 단순화해서 작성했다.


흐름 정리

사용자 관련 기능은 UsersModule 안에 묶여 있다.

그리고 프론트에서 /users 관련 API 요청을 보내면, 그 요청은 먼저 UsersController로 들어온다.

AppModule
UsersModule
UsersController
UsersService

즉, UsersModule은 사용자 기능을 NestJS 애플리케이션에 등록하는 역할을 하고,
UsersController는 사용자 관련 HTTP 요청을 받는 입구 역할을 한다.


1. UsersModule은 사용자 기능을 묶는 모듈이다

먼저 UsersModule의 구조를 단순화하면 다음과 같다.

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

NestJS에서 모듈은 기능 단위를 묶는 중요한 단위다.

사용자 관련 기능에는 다음과 같은 것들이 있을 수 있다.

회원 생성
회원 목록 조회
회원 단건 조회
회원 수정
회원 삭제
게스트 유저 조회
이메일로 유저 찾기

2. controllers는 요청을 받을 Controller를 등록한다

UsersModule 안에는 controllers가 있다.

controllers: [UsersController]

이 설정은 NestJS에게 다음과 같이 알려준다.

사용자 관련 요청은 UsersController가 받을 것이다.

즉, UsersController 안에 정의된 @Get, @Post, @Patch, @Delete 같은 API들이 서버에 등록된다.

예를 들어 UsersController에 다음 코드가 있다면:

@Get()
findAll() {
  return this.usersService.findAll();
}

이 메서드는 사용자 목록 조회 API로 등록될 수 있다.


3. providers는 Service를 NestJS가 관리하게 한다

UsersModule에는 providers도 있다.

providers: [UsersService]

이 설정은 NestJS에게 다음과 같이 알려준다.

UsersService 객체를 NestJS가 직접 생성하고 관리해줘.

그래서 Controller에서 직접 new UsersService()를 하지 않는다.

const usersService = new UsersService();

대신 Controller의 생성자에서 주입받는다.

constructor(private readonly usersService: UsersService) {}

그러면 NestJS가 UsersService 객체를 만들어서 UsersController에 넣어준다.

이게 NestJS의 의존성 주입, 즉 DI다.

Controller가 직접 Service를 만들지 않는다.
NestJS가 필요한 Service를 생성해서 넣어준다.

이 구조 덕분에 Controller는 요청을 받고 Service를 호출하는 역할에 집중할 수 있다.


4. exports는 다른 모듈에서도 사용할 수 있게 공개한다

UsersModule에는 exports도 있다.

exports: [UsersService]

이 설정은 UsersService를 다른 모듈에서도 사용할 수 있게 공개한다는 의미다.

예를 들어 로그인 기능을 담당하는 AuthService에서는 사용자를 이메일로 찾아야 할 수 있다.

사용자가 이메일과 비밀번호 입력
이메일을 가진 유저가 있는지 확인
비밀번호 비교
JWT 발급

여기서 “이메일로 유저 찾기”는 사용자 기능에 가깝다.

그래서 AuthServiceUsersService의 기능을 사용할 수 있도록 UsersModule에서 UsersService를 export한다.

정리하면 UsersModule의 역할은 다음과 같다.

UsersController를 등록한다.
UsersService를 Provider로 등록한다.
UsersService를 다른 모듈에서도 사용할 수 있게 공개한다.

5. UsersController는 사용자 API 요청의 입구다

이제 UsersController를 보자.

Controller는 프론트에서 들어온 HTTP 요청을 받는 입구다.

예를 들어 프론트에서 다음 요청을 보낸다고 해보자.

POST /users

이 요청은 UsersController 안의 @Post() 메서드로 들어간다.

Controller의 중요한 역할은 다음과 같다.

1. 요청을 받는다.
2. Param, Query, Body에서 필요한 값을 꺼낸다.
3. DTO를 통해 요청 데이터 형태를 맞춘다.
4. 실제 처리는 Service에 넘긴다.

중요한 점은 Controller가 직접 DB 작업을 하지 않는다는 것이다.

Controller → 요청을 받는다
Service    → 실제 로직을 처리한다
Prisma     → DB에 접근한다

이렇게 역할을 나누면 코드가 훨씬 읽기 쉬워진다.


6. @Controller('users')는 기본 주소를 정한다

UsersController에는 다음과 같은 데코레이터가 붙는다.

@Controller('users')
export class UsersController {}

여기서 'users'는 이 Controller의 기본 경로를 의미한다.

즉, 이 Controller 안에 있는 API들은 기본적으로 /users로 시작한다.

예를 들어 다음과 같이 정리할 수 있다.

@Get()        → GET /users
@Post()       → POST /users
@Get(':id')   → GET /users/:id
@Patch(':id') → PATCH /users/:id
@Delete(':id')→ DELETE /users/:id

처음에는 @Get()만 보면 주소가 /처럼 보일 수 있다.

하지만 Controller 위에 @Controller('users')가 붙어 있기 때문에, 실제 주소는 /users가 된다.


7. 생성자에서 UsersService를 주입받는다

Controller 안에서는 UsersService를 생성자로 주입받는다.

constructor(private readonly usersService: UsersService) {}

이 코드는 다음과 같이 이해할 수 있다.

UsersController는 UsersService가 필요하다.
NestJS야, UsersService 객체를 여기에 넣어줘.

여기서 private readonly도 의미가 있다.

private  → 이 클래스 안에서만 사용한다.
readonly → 한 번 주입받은 뒤 다른 값으로 바꾸지 않는다.

이렇게 주입받은 usersService는 Controller 메서드 안에서 사용할 수 있다.

this.usersService.create(...)
this.usersService.findAll()
this.usersService.findOne(...)

즉, Controller는 요청을 받은 뒤 Service 메서드를 호출하는 구조로 동작한다.


8. 유저 생성 요청 흐름

유저 생성 API의 구조를 단순화하면 다음과 같다.

@Post()
create(@Body() createUserDto: CreateUserDto) {
  return this.usersService.create(createUserDto);
}

이 코드는 다음 요청을 처리한다.

POST /users

프론트에서 요청 body를 보낸다고 해보자.

{
  "email": "test@example.com",
  "password": "password1234",
  "name": "사용자",
  "phoneNumber": "01012345678"
}

이때 @Body()는 요청 본문을 꺼내서 createUserDto에 넣어준다.

@Body() createUserDto: CreateUserDto

그리고 Controller는 이 데이터를 직접 DB에 저장하지 않고 Service로 넘긴다.

return this.usersService.create(createUserDto);

전체 흐름은 다음과 같다.

POST /users 요청
UsersController.create()
요청 body를 CreateUserDto 형태로 받음
UsersService.create(createUserDto) 호출
Service에서 실제 생성 로직 처리

이 구조를 보면서 Controller는 “요청을 받는 입구”이고, Service는 “실제 처리를 담당하는 곳”이라는 역할 분리가 조금 더 명확해졌다.


9. DTO는 요청 데이터의 규칙이다

유저 생성 요청에서는 CreateUserDto가 사용된다.

create(@Body() createUserDto: CreateUserDto)

DTO는 쉽게 말하면 프론트에서 서버로 보내는 데이터의 규칙이다.

예를 들어 유저 생성 요청에는 이메일, 비밀번호, 이름, 전화번호 같은 값이 들어올 수 있다.

{
  "email": "test@example.com",
  "password": "password1234",
  "name": "사용자"
}

이 값들이 어떤 형식이어야 하는지 DTO에서 정의할 수 있다.

이전에 main.ts에서 전역으로 ValidationPipe를 설정했다.

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

그래서 DTO에 정의되지 않은 필드가 요청 body에 들어오면 400 에러가 발생할 수 있다.

DTO는 단순한 타입 선언이 아니라, 프론트와 백엔드 사이의 데이터 약속이다.


10. 게스트 유저 조회 요청 흐름

게스트 유저 조회 API는 query string을 사용한다.

구조를 단순화하면 다음과 같다.

@Get('guest/lookup')
findGuest(
  @Query('name') name: string,
  @Query('phoneNumber') phoneNumber: string,
) {
  return this.usersService.findGuest(name, phoneNumber);
}

이 코드는 다음과 같은 요청을 처리한다.

GET /users/guest/lookup?name=사용자&phoneNumber=01012345678

여기서 ? 뒤에 붙는 값을 query string이라고 한다.

name=사용자
phoneNumber=01012345678

NestJS에서는 @Query()를 사용해서 이 값을 꺼낸다.

@Query('name') name: string
@Query('phoneNumber') phoneNumber: string

그리고 꺼낸 값을 Service에 넘긴다.

return this.usersService.findGuest(name, phoneNumber);

전체 흐름은 다음과 같다.

GET /users/guest/lookup 요청
Query에서 name, phoneNumber 추출
UsersService.findGuest(name, phoneNumber) 호출
Service에서 게스트 유저 조회 로직 처리

이 API는 비회원 또는 게스트 사용자가 본인의 결과를 다시 조회하는 흐름과 연결될 수 있다.

다만 실제 운영 서비스에서는 이름과 전화번호만으로 조회하는 방식이 충분히 안전한지 별도로 검토해야 한다.
개인정보와 관련된 API는 조회 조건, 인증 방식, 응답 데이터 범위를 더 신중하게 다뤄야 한다.


11. JWT 인증이 필요한 요청

유저 목록 조회 API에는 JWT Guard가 적용되어 있다. 다만 운영 서비스에서는 단순히 로그인 여부만 확인하는 것보다, 관리자 권한이 있는지도 함께 확인해야 할 수 있다.

@UseGuards(JwtAuthGuard)
@Get()
findAll(@Request() req) {
  return this.usersService.findAll();
}

@UseGuards(JwtAuthGuard)는 이 API를 실행하기 전에 JWT 인증 검사를 하겠다는 의미다.

Guard는 쉽게 말하면 문지기 역할을 한다.

요청 들어옴
Guard가 먼저 검사
통과하면 Controller 실행
실패하면 401 Unauthorized

JWT 인증이 필요한 요청은 보통 Authorization 헤더에 토큰을 담아 보낸다.

Authorization: Bearer JWT_TOKEN

인증에 성공하면 요청 객체에 사용자 정보가 담길 수 있다.

@Request() req

그리고 Guard를 통과한 뒤에는 req.user를 통해 인증된 사용자 정보를 확인할 수 있다.

개발 중에는 console.log(req.user)로 확인할 수 있지만, 운영 코드에서는 불필요한 로그나 사용자 정보 로그는 제거하는 것이 좋다.

특히 사용자 정보, 토큰, 개인정보가 로그에 남지 않도록 주의해야 한다.


12. 유저 단건 조회 요청 흐름

유저 한 명을 조회할 때는 URL 경로에 id가 들어간다.

@Get(':id')
findOne(@Param('id') id: string) {
  return this.usersService.findOne(id);
}

이 코드는 다음 요청을 처리한다.

GET /users/:id

예를 들어 실제 요청이 다음과 같다면:

GET /users/abc123

abc123id가 된다.

NestJS에서는 @Param()으로 URL 경로에 들어간 값을 꺼낸다.

@Param('id') id: string

전체 흐름은 다음과 같다.

GET /users/:id 요청
URL에서 id 추출
UsersService.findOne(id) 호출
Service에서 해당 유저 조회

이런 구조를 통해 Controller는 id를 꺼내고, 실제 조회는 Service에 맡긴다.


13. 유저 수정 요청 흐름

유저 수정 API는 PATCH 메서드를 사용한다.

@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
  return this.usersService.update(id, updateUserDto);
}

이 코드는 다음 요청을 처리한다.

PATCH /users/:id

수정 요청에서는 보통 두 가지 값이 필요하다.

1. 어떤 유저를 수정할지 나타내는 id
2. 어떤 값을 수정할지 담은 body

예를 들어 요청 body가 다음과 같을 수 있다.

{
  "name": "수정된 이름"
}

Controller에서는 @Param()으로 id를 꺼내고, @Body()로 수정할 데이터를 꺼낸다.

@Param('id') id: string
@Body() updateUserDto: UpdateUserDto

그리고 Service로 넘긴다.

return this.usersService.update(id, updateUserDto);

전체 흐름은 다음과 같다.

PATCH /users/:id 요청
URL에서 id 추출
body에서 수정할 데이터 추출
UsersService.update(id, updateUserDto) 호출
Service에서 해당 유저 수정

UpdateUserDto는 수정 요청에서 사용할 DTO다.
생성 요청과 달리 수정은 일부 필드만 바꿀 수 있기 때문에, 보통 생성 DTO보다 optional한 구조를 가진다.


14. 유저 삭제 요청 흐름

유저 삭제 API는 DELETE 메서드를 사용한다.

@Delete(':id')
remove(@Param('id') id: string) {
  return this.usersService.remove(id);
}

이 코드는 다음 요청을 처리한다.

DELETE /users/:id

삭제할 유저의 id를 URL에서 꺼낸다.

@Param('id') id: string

그리고 Service에 삭제 처리를 맡긴다.

return this.usersService.remove(id);

전체 흐름은 다음과 같다.

DELETE /users/:id 요청
URL에서 id 추출
UsersService.remove(id) 호출
Service에서 해당 유저 삭제

다만 실제 운영 서비스에서는 유저 삭제 API에 권한 검사가 반드시 필요하다.

예를 들어 사용자가 자기 자신만 삭제할 수 있는지, 관리자인지, 또는 삭제가 아니라 비활성화 처리를 해야 하는지 같은 정책을 고려해야 한다.


15. Param, Query, Body, Request 구분하기

이번 Controller를 보면서 가장 중요하게 느낀 것 중 하나는 요청 데이터를 어디서 꺼내는지 구분하는 것이었다.

초보자 입장에서는 Param, Query, Body, Request가 헷갈릴 수 있다.


Param

Param은 URL 경로에 들어가는 값이다.

GET /users/abc123

여기서 abc123이 Param이다.

NestJS에서는 다음처럼 꺼낸다.

@Param('id') id: string

Query

Query는 URL에서 ? 뒤에 붙는 값이다.

GET /users/guest/lookup?name=사용자&phoneNumber=01012345678

여기서 name, phoneNumber가 Query 값이다.

NestJS에서는 다음처럼 꺼낸다.

@Query('name') name: string
@Query('phoneNumber') phoneNumber: string

Body

Body는 요청 본문에 담긴 데이터다.

POST /users

예를 들어 JSON body는 다음과 같을 수 있다.

{
  "email": "test@example.com",
  "password": "password1234",
  "name": "사용자"
}

NestJS에서는 다음처럼 꺼낸다.

@Body() createUserDto: CreateUserDto

Request

Request는 요청 객체 전체를 가져올 때 사용한다.

@Request() req

요청 객체에는 body, query, param, header 등 요청과 관련된 여러 정보가 들어 있다.

또한 JWT Guard를 통과한 뒤에는 인증된 사용자 정보가 req.user에 들어갈 수 있다.

req.user

다만 운영 코드에서는 요청 객체 전체를 다루거나 로그로 남길 때 개인정보가 포함되지 않도록 주의해야 한다.


16. API 흐름 정리

이번 글에서 본 사용자 API 흐름을 정리하면 다음과 같다.

POST   /users              → 유저 생성
GET    /users/guest/lookup → 게스트 유저 조회
GET    /users              → 전체 유저 조회, JWT 인증 또는 권한 검사 필요
GET    /users/:id          → 유저 단건 조회
PATCH  /users/:id          → 유저 수정
DELETE /users/:id          → 유저 삭제

단, 운영 서비스에서는 단순히 API가 동작하는 것만으로 충분하지 않다.

특히 사용자 정보와 관련된 API는 인증과 권한 정책을 함께 고려해야 한다.

누가 조회할 수 있는가?
누가 수정할 수 있는가?
누가 삭제할 수 있는가?
응답에서 어떤 사용자 정보를 내려줘도 되는가?
로그에 개인정보가 남지는 않는가?

이번 글에서는 Controller의 요청 흐름을 중심으로 보고, 이런 권한 정책은 이후 인증/인가 글에서 더 깊게 정리해보려고 한다.


정리

이번 글에서는 UsersModuleUsersController를 기준으로 사용자 API 요청이 어디로 들어오는지 정리했다.

UsersModule은 사용자 기능을 하나로 묶는 모듈이다.

UsersController 등록
UsersService 등록
UsersService 외부 공개

UsersController는 사용자 관련 HTTP 요청을 받는 입구다.

Controller는 요청에서 필요한 값을 꺼낸 뒤, 실제 처리는 Service에 맡긴다.

프론트 요청
UsersController
UsersService
PrismaService
DB

특히 이번 글에서는 Param, Query, Body, Request의 차이를 정리할 수 있었다.

Param   → URL 경로에 들어가는 값
Query   → URL의 ? 뒤에 붙는 값
Body    → 요청 본문에 담긴 JSON 데이터
Request → 요청 객체 전체

Controller는 요청을 받고 Service로 넘기는 입구에 가깝다.

Controller가 모든 일을 직접 처리하지 않고 Service에 역할을 넘기면, 코드 구조가 더 명확해진다.

Controller → 요청과 응답 흐름 담당
Service    → 실제 비즈니스 로직 담당
Prisma     → DB 접근 담당
UsersModule과 UsersController로 이해한 사용자 API 요청 흐름 | OnlyMinkk Blog