UsersService로 이해한 사용자 생성과 조회 로직
NestJS에서 UsersService가 사용자 생성, 비밀번호 암호화, 게스트 조회, 이메일 조회, CRUD 로직을 처리하는 흐름을 정리한 글
이전 글에서는 UsersModule과 UsersController를 기준으로 사용자 API 요청이 어디로 들어오는지 정리했다.
Controller는 프론트에서 들어온 요청을 받고, 필요한 값을 꺼낸 뒤 Service로 넘기는 역할을 했다.
이번 글에서는 그 다음 단계인 UsersService를 정리해보려고 한다.
전체 흐름은 다음과 같다.
프론트 요청
↓
UsersController
↓
UsersService
↓
PrismaService
↓
DB
이번 글의 핵심은 이거다.
Controller는 요청을 받는 곳이고,
Service는 실제 로직을 처리하는 곳이다.
운영 중인 서비스 코드이기 때문에 실제 코드를 그대로 공개하지 않고, 핵심 흐름과 개념 중심으로 정리했다.
1. UsersService는 사용자 관련 실제 로직을 처리한다
UsersService는 사용자와 관련된 실제 처리 로직을 담당한다.
예를 들면 다음과 같은 기능들이 있다.
회원 생성
비밀번호 암호화
게스트 유저 조회
이메일로 유저 조회
유저 목록 조회
유저 단건 조회
유저 수정
유저 삭제
이전 글에서 UsersController는 이런 식으로 Service를 호출했다.
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
즉, Controller는 요청을 받고 UsersService.create()를 호출한다.
그리고 실제로 비밀번호를 암호화하고, role을 결정하고, DB에 저장하는 일은 UsersService가 담당한다.
정리하면 다음과 같다.
UsersController
→ 요청을 받는다.
UsersService
→ 사용자 관련 실제 로직을 처리한다.
PrismaService
→ DB에 접근한다.
2. @Injectable()은 주입 가능한 서비스라는 표시다
UsersService는 NestJS에서 관리하는 Service다.
구조를 단순화하면 다음과 같다.
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
}
여기서 @Injectable()은 NestJS에게 다음과 같이 알려주는 역할을 한다.
이 클래스는 NestJS가 직접 생성하고,
필요한 곳에 주입해줄 수 있는 클래스다.
그래서 UsersController에서 직접 new UsersService()를 하지 않고, 생성자를 통해 주입받을 수 있었다.
constructor(private readonly usersService: UsersService) {}
이게 NestJS의 의존성 주입, 즉 DI다.
직접 객체를 만들지 않고,
NestJS가 필요한 객체를 생성해서 넣어준다.
3. PrismaService를 주입받아 DB에 접근한다
UsersService는 DB 작업을 해야 하기 때문에 PrismaService를 주입받는다.
constructor(private readonly prisma: PrismaService) {}
이 코드는 다음과 같이 이해할 수 있다.
UsersService는 DB 작업을 위해 PrismaService가 필요하다.
NestJS야, PrismaService 객체를 여기에 넣어줘.
이렇게 주입받은 prisma를 통해 User 테이블에 접근할 수 있다.
this.prisma.user.create()
this.prisma.user.findFirst()
this.prisma.user.findUnique()
this.prisma.user.findMany()
this.prisma.user.update()
this.prisma.user.delete()
이전 글에서 정리한 것처럼 PrismaService는 PrismaClient를 상속하고 있기 때문에, Prisma의 DB 접근 메서드를 사용할 수 있다.
4. 유저 생성 로직의 전체 흐름
유저 생성 로직은 create() 메서드에서 처리된다.
구조를 단순화하면 다음과 같다.
async create(createUserDto: CreateUserDto) {
const { password, ...userData } = createUserDto;
const hashedPassword = password
? await bcrypt.hash(password, 10)
: null;
const role = createUserDto.provider === 'GUEST'
? Role.GUEST
: Role.USER;
return this.prisma.user.create({
data: {
...userData,
password: hashedPassword,
role,
},
});
}
이 메서드는 대략 다음 순서로 동작한다.
createUserDto를 받는다.
↓
password만 따로 분리한다.
↓
password가 있으면 bcrypt로 암호화한다.
↓
provider 값에 따라 role을 결정한다.
↓
Prisma로 User 테이블에 저장한다.
↓
생성된 user를 반환한다.
한 문장으로 정리하면 다음과 같다.
UsersService.create()는 유저 정보를 받아 비밀번호를 안전하게 암호화한 뒤 DB에 저장하는 메서드다.
5. async / await을 사용하는 이유
create() 메서드 앞에는 async가 붙어 있다.
async create(createUserDto: CreateUserDto) {
이유는 메서드 안에서 시간이 걸리는 작업을 하기 때문이다.
대표적으로 두 가지가 있다.
await bcrypt.hash(password, 10)
await this.prisma.user.create(...)
비밀번호를 해시하는 작업도 시간이 걸리고, DB에 저장하는 작업도 시간이 걸린다.
서버가 DB에 요청을 보내고 응답을 기다려야 하기 때문이다.
Node.js 서버
↓ 요청
PostgreSQL DB
↓ 응답
Node.js 서버
이런 작업을 비동기 작업이라고 한다.
await은 쉽게 말하면 다음과 같다.
이 작업이 끝날 때까지 기다렸다가 다음 줄로 넘어가라.
그래서 DB 작업이나 암호화처럼 시간이 걸리는 작업에는 async / await을 사용한다.
6. 비밀번호를 따로 분리하는 이유
유저 생성 로직에서 가장 먼저 하는 일은 password를 분리하는 것이다.
const { password, ...userData } = createUserDto;
처음 보면 문법이 조금 낯설 수 있다.
이건 JavaScript의 구조 분해 할당과 rest 문법이다.
예를 들어 요청 데이터가 이렇게 들어왔다고 해보자.
const createUserDto = {
email: 'test@example.com',
password: 'password1234',
name: '사용자',
phoneNumber: '01012345678',
provider: 'LOCAL',
};
아래 코드가 실행되면:
const { password, ...userData } = createUserDto;
결과는 이렇게 나뉜다.
password = 'password1234'
그리고 userData에는 password를 제외한 나머지 값이 들어간다.
userData = {
email: 'test@example.com',
name: '사용자',
phoneNumber: '01012345678',
provider: 'LOCAL',
}
이렇게 나누는 이유는 비밀번호를 그대로 저장하면 안 되기 때문이다.
비밀번호는 따로 꺼내서 암호화한 뒤 저장해야 한다.
7. 비밀번호는 원본 그대로 저장하면 안 된다
사용자가 비밀번호를 입력했다고 해보자.
password1234
이 값을 DB에 그대로 저장하면 매우 위험하다.
password = password1234
만약 DB가 유출되면 사용자의 실제 비밀번호가 그대로 노출된다.
그래서 비밀번호는 반드시 해시해서 저장해야 한다.
이 프로젝트에서는 bcrypt를 사용했다.
const hashedPassword = password
? await bcrypt.hash(password, 10)
: null;
이 코드는 다음과 같이 이해할 수 있다.
password가 있으면 bcrypt로 해시한다.
password가 없으면 null로 저장한다.
해시된 비밀번호는 대략 이런 형태가 된다.
$2b$10$...
중요한 점은 DB에는 사용자가 입력한 원본 비밀번호가 아니라, 해시된 값이 저장되어야 한다는 것이다.
8. password가 없을 수도 있는 이유
코드를 보면 password가 없으면 null을 저장하도록 되어 있다.
const hashedPassword = password
? await bcrypt.hash(password, 10)
: null;
처음에는 유저를 생성하는데 비밀번호가 없을 수 있다는 점이 이상하게 느껴질 수 있다.
하지만 서비스 구조에 따라 password가 없는 유저가 있을 수 있다.
예를 들면 다음과 같다.
일반 이메일 회원가입 → password 있음
게스트 사용자 → password 없을 수 있음
소셜 로그인 사용자 → password 없을 수 있음
즉, 모든 유저가 반드시 로컬 비밀번호를 가지는 것은 아니다.
그래서 password가 optional한 구조라면, Service에서도 password가 없을 때를 처리해야 한다.
9. bcrypt.hash(password, 10)에서 10의 의미
bcrypt 해시 코드에는 숫자 10이 들어간다.
await bcrypt.hash(password, 10)
여기서 10은 salt rounds라고 부른다.
쉽게 말하면 다음과 같다.
비밀번호를 얼마나 강하게 섞어서 해시할지 정하는 값
숫자가 높을수록 해시 계산이 더 오래 걸리고, 공격자가 비밀번호를 추측하기도 더 어려워진다.
하지만 너무 높으면 로그인이나 회원가입 속도가 느려질 수 있다.
그래서 보안성과 성능 사이에서 적절한 값을 선택해야 한다.
이 프로젝트에서는 기본적으로 많이 사용하는 값인 10을 사용했다.
10. role 결정 로직
유저를 생성할 때 role도 결정한다.
const role = createUserDto.provider === 'GUEST'
? Role.GUEST
: Role.USER;
이 코드는 다음과 같은 뜻이다.
provider가 GUEST이면 role은 GUEST
그 외에는 role은 USER
조금 풀어 쓰면 다음과 같다.
let role;
if (createUserDto.provider === 'GUEST') {
role = Role.GUEST;
} else {
role = Role.USER;
}
여기서 사용된 문법은 삼항 연산자다.
조건 ? 참일 때 값 : 거짓일 때 값
즉, 게스트로 들어온 사용자는 GUEST 권한을 가지고, 그 외의 방식으로 생성된 사용자는 기본적으로 USER 권한을 갖게 된다.
11. provider와 role의 차이
여기서 provider와 role을 구분하는 것이 중요했다.
둘 다 사용자와 관련된 값이라 처음에는 헷갈릴 수 있다.
내가 이해한 차이는 다음과 같다.
provider
→ 사용자가 어떤 방식으로 들어왔는가
role
→ 사용자가 어떤 권한을 가지는가
예를 들어 provider는 이런 값이 될 수 있다.
LOCAL
GUEST
GOOGLE
KAKAO
반면 role은 권한에 가깝다.
GUEST
USER
ADMIN
즉, provider는 가입 또는 진입 방식이고, role은 권한이다.
현재 로직은 단순하게 다음처럼 처리하고 있다.
게스트로 들어왔으면 GUEST 권한
그 외 방식이면 USER 권한
나중에 관리자 권한이나 소셜 로그인 정책이 더 복잡해지면, role 결정 로직도 더 명확하게 분리할 필요가 있을 수 있다.
12. Prisma로 유저 생성하기
마지막으로 Prisma를 사용해서 DB에 유저를 생성한다.
return this.prisma.user.create({
data: {
...userData,
password: hashedPassword,
role,
},
});
여기서 this.prisma.user.create()는 User 테이블에 새 데이터를 생성하는 Prisma 메서드다.
SQL로 비유하면 대략 이런 느낌이다.
INSERT INTO "User" (...) VALUES (...);
하지만 Prisma를 사용하면 TypeScript 코드로 작성할 수 있다.
data 안에는 DB에 저장할 값이 들어간다.
data: {
...userData,
password: hashedPassword,
role,
}
여기서 ...userData는 userData 객체 안의 값을 펼쳐 넣는다는 뜻이다.
예를 들어 userData가 다음과 같다면:
const userData = {
email: 'test@example.com',
name: '사용자',
provider: 'LOCAL',
};
아래처럼 펼쳐진다.
{
email: 'test@example.com',
name: '사용자',
provider: 'LOCAL',
password: hashedPassword,
role: Role.USER,
}
즉, 요청 데이터 중 password를 제외한 값은 그대로 사용하고, password는 해시된 값으로 바꿔서 저장하는 구조다.
13. 생성 결과를 그대로 반환할 때 주의할 점
create()는 생성된 user 객체를 반환한다.
return this.prisma.user.create(...)
이때 한 가지 주의할 점이 있다.
생성된 user 객체에 password 필드가 포함될 수 있다면, 응답으로 그대로 내려보내면 안 된다.
비밀번호는 해시된 값이라 하더라도 외부 응답에 포함하지 않는 것이 좋다.
그래서 운영 서비스에서는 다음 중 하나를 고려해야 한다.
응답 DTO를 따로 만든다.
password 필드를 제외하고 반환한다.
Interceptor나 serializer를 사용해 민감한 필드를 제거한다.
이번 글에서는 Service 흐름 이해에 집중하지만, 실제 운영 API에서는 응답 데이터에 민감한 값이 포함되지 않도록 반드시 확인해야 한다.
14. 게스트 유저 조회 로직
게스트 유저 조회는 findGuest() 메서드에서 처리한다.
async findGuest(name: string, phoneNumber: string) {
return this.prisma.user.findFirst({
where: {
name,
phoneNumber,
provider: 'GUEST',
},
include: {
results: true,
},
});
}
이 메서드는 이름과 전화번호를 기준으로 게스트 유저를 찾는다.
조건은 다음과 같다.
name이 전달받은 name과 같고
phoneNumber가 전달받은 phoneNumber와 같고
provider가 GUEST인 유저
즉, 일반 회원이 아니라 게스트 사용자를 찾기 위한 로직이다.
Controller에서는 다음과 같은 요청과 연결될 수 있다.
GET /users/guest/lookup?name=사용자&phoneNumber=01012345678
이 요청에서 query string으로 받은 name, phoneNumber가 Service로 전달되고, Service는 DB에서 조건에 맞는 게스트 유저를 찾는다.
15. findFirst()를 사용하는 이유
게스트 유저 조회에서는 findFirst()를 사용한다.
this.prisma.user.findFirst({
where: {
name,
phoneNumber,
provider: 'GUEST',
},
});
Prisma에는 findUnique()도 있는데, 여기서는 findFirst()를 사용했다.
둘의 차이는 다음과 같이 이해했다.
findUnique
→ unique 조건으로 딱 하나를 찾을 때 사용한다.
findFirst
→ 일반 조건으로 검색해서 조건에 맞는 첫 번째 데이터를 가져올 때 사용한다.
예를 들어 email이 unique로 설정되어 있다면 findUnique()를 사용할 수 있다.
this.prisma.user.findUnique({
where: { email },
});
하지만 name + phoneNumber + provider 조합이 DB에서 unique로 선언되어 있지 않다면 findUnique()를 사용할 수 없다.
그래서 여기서는 조건에 맞는 첫 번째 유저를 가져오는 findFirst()를 사용했다.
16. include로 관련 결과까지 함께 가져오기
게스트 유저 조회에는 include가 들어간다.
include: {
results: true,
}
이 코드는 유저 정보뿐만 아니라, 유저와 연결된 결과 데이터도 함께 가져오겠다는 뜻이다.
예를 들어 User와 MatchingResult가 관계로 연결되어 있다면, user만 조회할 때는 기본적으로 유저 정보만 가져올 수 있다.
하지만 include를 사용하면 연결된 결과도 함께 가져올 수 있다.
User
↓
results
즉, 게스트 사용자가 이전에 받은 추천 결과를 다시 조회하는 흐름이라면, 유저 정보와 함께 결과 목록까지 가져오는 것이 필요할 수 있다.
다만 실제 운영 서비스에서는 이름과 전화번호만으로 결과를 조회하는 방식이 충분히 안전한지 고민해야 한다.
개인정보와 관련된 조회 API는 다음을 함께 고려해야 한다.
조회 조건이 충분히 안전한가?
다른 사람의 결과를 추측해서 조회할 수 있지는 않은가?
응답 데이터에 민감한 정보가 포함되지는 않는가?
추가 인증이나 확인 절차가 필요한가?
17. 이메일로 유저 찾기
findByEmail()은 이메일로 유저를 찾는 메서드다.
async findByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
});
}
이 메서드는 로그인 흐름에서 중요하다.
로그인 과정은 보통 다음과 같이 진행된다.
사용자가 이메일과 비밀번호 입력
↓
DB에서 해당 이메일을 가진 유저 조회
↓
유저가 없으면 로그인 실패
↓
유저가 있으면 비밀번호 비교
↓
비밀번호가 맞으면 JWT 발급
여기서 “DB에서 해당 이메일을 가진 유저 조회”를 담당하는 것이 findByEmail()이다.
findUnique()를 사용하려면 email이 unique 필드로 설정되어 있어야 한다.
email은 한 사용자에게만 연결되어야 한다.
그래야 같은 이메일로 여러 사용자가 생성되는 문제를 막을 수 있다.
18. JWT 인증 흐름과의 연결
findByEmail()은 직접 JWT를 발급하지는 않는다.
하지만 로그인 과정에서 사용자를 찾는 역할을 하기 때문에 인증 흐름과 연결된다.
로그인이 성공하면 보통 서버는 JWT를 발급한다.
그리고 이후 보호된 API에 접근할 때 클라이언트는 다음과 같은 헤더를 보낸다.
Authorization: Bearer JWT_TOKEN
JWT 인증 전략에서는 이 Bearer 토큰을 꺼내고, 토큰의 payload를 검증한 뒤 요청 객체에 사용자 정보를 붙일 수 있다.
예를 들어 payload에서 다음 정보를 꺼낼 수 있다.
userId
email
role
즉, 흐름을 크게 보면 다음과 같다.
findByEmail()
↓
로그인 대상 유저 조회
↓
비밀번호 검증
↓
JWT 발급
↓
보호된 API 요청 시 JWT 검증
↓
req.user에 사용자 정보 저장
UsersService는 이 중에서 “사용자를 찾는 역할”을 담당하고, JWT 발급과 검증은 Auth 쪽에서 담당한다.
이렇게 역할을 나누면 사용자 관리 로직과 인증 로직을 분리해서 이해할 수 있다.
19. 유저 목록 조회
findAll()은 유저 목록을 조회하는 메서드다.
async findAll() {
return this.prisma.user.findMany();
}
findMany()는 조건에 맞는 여러 데이터를 가져오는 Prisma 메서드다.
조건 없이 사용하면 User 테이블의 여러 데이터를 조회한다.
SQL로 비유하면 다음과 비슷하다.
SELECT * FROM "User";
다만 운영 서비스에서 유저 목록 조회는 매우 조심해야 한다.
유저 목록에는 개인정보가 포함될 수 있기 때문이다.
그래서 실제 운영 환경에서는 보통 다음을 고려해야 한다.
관리자만 접근 가능하게 할 것
응답에서 password, phoneNumber 같은 민감한 필드는 제외할 것
페이지네이션을 적용할 것
필요한 필드만 select할 것
즉, findAll()은 기능 자체로는 단순하지만, 운영 API로 제공할 때는 인증과 권한, 응답 범위를 신중하게 설계해야 한다.
20. id로 유저 단건 조회
findOne()은 id로 유저 하나를 찾는 메서드다.
async findOne(id: string) {
return this.prisma.user.findUnique({
where: { id },
});
}
Controller에서는 다음 요청과 연결된다.
GET /users/:id
예를 들어 요청이 다음과 같다면:
GET /users/abc123
abc123이 id가 된다.
Service는 이 id를 사용해서 DB에서 유저를 조회한다.
where: { id }
이 역시 운영 환경에서는 응답 데이터에 주의해야 한다.
유저 단건 조회 결과에 password 같은 민감한 필드가 포함되지 않도록 해야 한다.
또한 일반 사용자가 다른 사용자의 id를 입력해 조회할 수 없도록 권한 정책도 필요할 수 있다.
21. 유저 수정 로직
update()는 유저 정보를 수정하는 메서드다.
async update(id: string, updateUserDto: UpdateUserDto) {
return this.prisma.user.update({
where: { id },
data: updateUserDto,
});
}
흐름은 다음과 같다.
PATCH /users/:id 요청
↓
Controller에서 id와 body 추출
↓
UsersService.update(id, updateUserDto) 호출
↓
Prisma로 해당 유저 수정
where는 어떤 유저를 수정할지 정한다.
where: { id }
data는 어떤 값으로 수정할지 정한다.
data: updateUserDto
예를 들어 updateUserDto가 다음과 같다면:
{
name: '수정된 이름'
}
해당 유저의 name이 수정된다.
22. update에서 주의할 점
현재 구조에서는 updateUserDto에 password가 포함될 경우 그대로 저장될 가능성이 있다.
data: updateUserDto
create()에서는 password를 bcrypt로 해시한 뒤 저장했다.
하지만 update()에서 password를 처리하지 않는다면, 비밀번호 변경 요청이 들어왔을 때 원본 비밀번호가 저장될 위험이 있다.
그래서 나중에 비밀번호 변경 기능을 구현한다면 update 로직에서 password를 별도로 처리해야 한다.
예를 들면 다음과 같은 흐름이 필요하다.
updateUserDto에 password가 있는지 확인
↓
password가 있으면 bcrypt.hash() 실행
↓
해시된 password로 data 재구성
↓
DB 업데이트
이 부분은 실제 운영 서비스에서 반드시 확인해야 하는 중요한 포인트라고 느꼈다.
23. 유저 삭제 로직
remove()는 유저를 삭제하는 메서드다.
async remove(id: string) {
return this.prisma.user.delete({
where: { id },
});
}
Controller에서는 다음 요청과 연결된다.
DELETE /users/:id
이 메서드는 id가 일치하는 유저를 삭제한다.
SQL로 비유하면 다음과 비슷하다.
DELETE FROM "User"
WHERE id = 'abc123';
Prisma의 delete()는 삭제된 데이터를 반환할 수 있다.
다만 운영 서비스에서 유저 삭제는 신중하게 다뤄야 한다.
정말 DB에서 삭제할 것인지, 아니면 비활성화 처리할 것인지도 정책적으로 결정해야 한다.
예를 들어 다음과 같은 방식이 있을 수 있다.
Hard delete
→ DB에서 실제로 삭제한다.
Soft delete
→ deletedAt 같은 값을 기록하고, 실제 데이터는 남겨둔다.
또한 누가 삭제할 수 있는지도 중요하다.
본인만 삭제할 수 있는가?
관리자만 삭제할 수 있는가?
게스트 유저는 어떻게 처리할 것인가?
이번 글에서는 로직 흐름을 이해하는 데 집중하지만, 운영 서비스에서는 삭제 정책과 권한 검사를 함께 고려해야 한다.
24. Prisma CRUD 메서드 정리
이번 UsersService에서 사용된 Prisma 메서드들을 정리하면 다음과 같다.
create()
→ 새 데이터 생성
findFirst()
→ 조건에 맞는 첫 번째 데이터 조회
findUnique()
→ unique 조건으로 하나 조회
findMany()
→ 여러 데이터 조회
update()
→ 데이터 수정
delete()
→ 데이터 삭제
이걸 흔히 CRUD라고 부른다.
Create → 생성
Read → 조회
Update → 수정
Delete → 삭제
즉, UsersService는 User 테이블에 대한 기본 CRUD를 담당하는 서비스라고 볼 수 있다.
25. 메서드별 역할 정리
이번 글에서 본 UsersService의 메서드별 역할은 다음과 같다.
create()
→ 유저 생성, 비밀번호 암호화, role 결정
findGuest()
→ 이름 + 전화번호 + GUEST provider로 게스트 유저와 결과 조회
findByEmail()
→ 이메일로 유저 조회, 로그인에서 사용됨
findAll()
→ 유저 목록 조회
findOne()
→ id로 유저 하나 조회
update()
→ id로 유저 수정
remove()
→ id로 유저 삭제
이렇게 정리하고 나니 UsersService가 단순히 DB 메서드를 호출하는 파일이 아니라, 사용자와 관련된 정책과 흐름이 모이는 계층이라는 것을 이해할 수 있었다.
정리
이번 글에서는 UsersService를 기준으로 사용자 생성과 조회 로직을 정리했다.
가장 중요한 흐름은 다음과 같았다.
UsersController가 요청을 받는다.
↓
UsersService 메서드를 호출한다.
↓
UsersService가 필요한 로직을 처리한다.
↓
PrismaService를 통해 DB에 접근한다.
↓
결과를 반환한다.
특히 유저 생성 로직에서 중요한 부분은 비밀번호 처리였다.
password만 따로 분리한다.
↓
bcrypt로 해시한다.
↓
해시된 password를 DB에 저장한다.
비밀번호는 절대 원본 그대로 저장하면 안 된다.
또한 provider와 role의 차이도 정리할 수 있었다.
provider → 사용자가 어떤 방식으로 들어왔는가
role → 사용자가 어떤 권한을 가지는가
이번 글을 통해 Service는 단순히 Controller에서 호출되는 함수 모음이 아니라, 실제 비즈니스 로직과 데이터 처리 정책이 들어가는 계층이라는 것을 이해하게 됐다.
다만 운영 서비스에서는 현재 구조에서 추가로 고민해야 할 부분도 있었다.
유저 생성 응답에서 password 필드를 제거하고 있는가?
유저 목록 조회는 관리자만 접근 가능한가?
게스트 결과 조회가 개인정보 측면에서 안전한가?
비밀번호 수정 시에도 bcrypt 해시가 적용되는가?
유저 삭제는 hard delete가 맞는가, soft delete가 필요한가?