1. 회원가입 (Registration) 흐름
회원가입은 새로운 사용자의 정보를 데이터베이스에 안전하게 저장하는 과정이다.
회원가입 실행 간단 흐름도
1. 클라이언트 → 서버:
POST /register (email, password, name, ...)
2. Controller:
- 요청 데이터 추출
- userService.register 호출
3. Service:
- 이메일 중복 확인 (userRepository.findUserByEmail)
- 비밀번호 해싱 (bcrypt.hash)
- 새로운 사용자 정보 저장 요청 (userRepository.createUser)
4. Repository:
- DB에 `INSERT` 쿼리 실행
- 생성된 사용자 ID 반환
5. Controller:
- 201 Created 상태 코드와 함께 성공 메시지 응답
1단계: 클라이언트 요청 및 데이터 검증 (Controller)
사용자가 이메일, 비밀번호 등의 정보를 입력하여 /register 엔드포인트로 POST 요청을 보낸다. Controller는 이 요청을 받아 서비스 계층으로 전달한다.
// user.controller.js
exports.register = async (req, res, next) => {
try {
// 1. 서비스 계층에 사용자 등록 로직을 위임한다.
const newUser = await userService.register(req.body);
// 2. 성공 시, 201 Created 상태와 함께 응답한다.
res.status(201).json({ message: "User registered successfully", userId: newUser.id });
} catch (err) {
next(err); // 에러 발생 시 중앙 에러 핸들러로 전달
}
};
2단계: 비즈니스 로직 처리 (Service)
Service 계층에서는 회원가입의 핵심 로직이 처리된다.
// user.service.js
exports.register = async ({ email, password, name, address, phone_number }) => {
// 1. 이메일 중복 확인 (Repository 호출)
const existingUser = await userRepository.findUserByEmail(email);
if (existingUser) {
throw new CustomError(USER_ALREADY_EXISTS.statusCode, USER_ALREADY_EXISTS.message);
}
// 2. 비밀번호를 bcrypt로 해싱한다. (saltRounds=10)
const hashedPassword = await bcrypt.hash(password, 10);
// 3. 해싱된 비밀번호와 함께 사용자 정보를 DB에 저장 요청한다.
const userId = await userRepository.createUser({
email,
hashedPassword, // 평문 비밀번호가 아닌 해시 값을 전달
name,
address,
phone_number,
});
return { id: userId };
};
가장 중요한 부분은 사용자의 비밀번호를 절대 평문으로 저장하지 않고, bcrypt.hash를 통해 안전하게 암호화된 상태로 변환하는 것이다.
3단계: 데이터베이스 저장 (Repository)
Repository 계층은 Service로부터 받은 데이터를 실제 데이터베이스에 저장(INSERT)하는 역할을 담당한다.
// user.repository.js
exports.createUser = async ({ email, hashedPassword, name, address, phone_number }) => {
const result = await dbPool.query(
"INSERT INTO users (email, password, name, address, phone_number) VALUES (?, ?, ?, ?, ?)",
[email, hashedPassword, name, address, phone_number]
);
// 생성된 사용자의 ID를 반환한다.
return result[0].insertId;
};
4단계: 회원가입 API 결과

2. 로그인 (Login) 흐름
로그인은 저장된 사용자 정보를 바탕으로 신원을 확인하고, 인증 상태를 유지할 수 있는 토큰을 발급하는 과정이다.
로그인 실행 간단 흐름도
1. 클라이언트 → 서버:
POST /login (email, password)
2. Controller:
- 요청 데이터 추출, userService.login 호출
3. Service:
- 사용자 조회 (userRepository.findUserByEmail)
- 비밀번호 검증 (bcrypt.compare)
- 인증 성공 시 Controller로 사용자 정보 반환
4. Controller:
- Access/Refresh Token 생성
- Refresh Token DB 저장 요청 (userRepository.saveRefreshToken)
- Refresh Token을 httpOnly 쿠키에 설정
- Access Token을 JSON으로 클라이언트에 응답
5. 클라이언트:
- Access Token으로 API 요청, 만료 시 Refresh Token으로 재발급
1단계: 클라이언트 요청 및 인증 위임 (Controller)
사용자가 이메일과 비밀번호로 /login 엔드포인트에 POST 요청을 보낸다.
// user.controller.js
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
// 1. 서비스 계층에 인증 처리를 위임한다.
const user = await userService.login(email, password);
// ... (인증 성공 후 토큰 발급 로직)
} catch (err) {
next(err);
}
};
2단계: 사용자 인증 (Service & Repository)
Service는 Repository를 통해 사용자를 조회하고, bcrypt.compare를 통해 비밀번호의 일치 여부를 검증한다.
// user.repository.js
exports.findUserByEmail = async (email) => {
const result = await dbPool.query(
"SELECT * FROM users WHERE email = ? AND deleted_at IS NULL",
[email]
);
return result[0][0]; // MariaDB 결과 형식에 맞춰 실제 데이터 행 추출
};
// user.service.js
exports.login = async (email, password) => {
const user = await userRepository.findUserByEmail(email);
if (!user) { // 사용자 존재 여부 확인
throw new CustomError(INVALID_CREDENTIALS.statusCode, INVALID_CREDENTIALS.message);
}
// bcrypt.compare는 입력된 평문과 DB의 해시를 비교한다.
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) { // 비밀번호 불일치 시 에러
throw new CustomError(INVALID_CREDENTIALS.statusCode, INVALID_CREDENTIALS.message);
}
// 보안을 위해 비밀번호를 제외한 정보만 반환
const { password: _, ...userWithoutPassword } = user;
return userWithoutPassword;
};
3단계: JWT 발급 및 저장 (Controller & Repository)
인증에 성공하면 Controller는 Access Token과 Refresh Token을 생성한다. Refresh Token은 탈취되더라도 서버에서 무효화할 수 있도록 데이터베이스에 저장한다.
// user.controller.js (login 함수 내부)
const payload = { id: user.id, email: user.email };
const accessToken = generateAccessToken(payload);
const refreshToken = generateRefreshToken(payload);
// 서비스 계층을 통해 Repository의 저장 함수를 호출
await userService.saveRefreshToken(user.id, refreshToken);
// user.repository.js
exports.saveRefreshToken = async (userId, token) => {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
const sql = `
INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE token = VALUES(token), expires_at = VALUES(expires_at)`;
await dbPool.query(sql, [userId, token, expiresAt]);
};
4단계: 최종 응답 (Controller)
마지막으로 Controller는 클라이언트에게 토큰을 전달한다. Access Token은 API 요청 시 사용되며, Refresh Token은 안전하게 쿠키에 저장된다.
// user.controller.js (login 함수 마지막)
// Refresh Token은 httpOnly 쿠키에 담아 XSS 공격을 방지한다.
res.cookie("refreshToken", refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "Strict",
});
// Access Token은 JSON 본문에 담아 전달한다.
res.status(200).json({ message: "Login successful", accessToken });
5단계: 로그인 API 결과

결론
- 회원가입 시에는
bcrypt를 통해 비밀번호를 안전하게 해싱하여 저장하는 것이 핵심이다. - 로그인 시에는
bcrypt.compare로 비밀번호를 검증하고,JWT(Access/Refresh Token)를 발급하여 인증 상태를 관리한다. - 계층형 아키텍처(
Controller,Service,Repository)를 통해 각 모듈의 역할을 명확히 분리함으로써 코드의 유지보수성과 확장성을 높일 수 있다.
이러한 흐름을 통해 안전하고 효율적인 사용자 인증 시스템을 구축할 수 있다.
'Programmers' 카테고리의 다른 글
| [과제] 도서 전체 및 상세 조회 API 구현 흐름, 결과 (0) | 2025.10.20 |
|---|---|
| [27일차]Node.js 프로젝트 구조, 암호화 인증 시스템 (0) | 2025.10.17 |
| [26일차]Express.js 프로젝트 구조의 진화와 Nest.js와의 구조 비교 (0) | 2025.10.16 |
| [25일차]프로젝트 설계 후기, 방법론 정리 (0) | 2025.10.15 |
| [24일차]API 설계 보고서 (0) | 2025.10.14 |