Lv5 - ์์ธ์ฒ๋ฆฌ
์ด์ ๊ณผ์ ์์ ๊ธฐ๋ฅ์ ๋์ํ์ง๋ง, ํ ๊ฐ์ง ํฐ ๋ฌธ์ ๊ฐ ์์๋ค.
์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๋๋ถ๋ถ 400 ๋๋ 500์ผ๋ก๋ง ๋ด๋ ค๊ฐ๋ค.
- ๋ก๊ทธ์ธ ์ ํด๋ 400
- ์กด์ฌํ์ง ์๋ ์ผ์ ๋ 400
- ์ด๋ฉ์ผ ์ค๋ณต๋ 500
- Validation ์คํจ๋ ๊ธฐ๋ณธ ์๋ฌ ํ์ด์ง
์ด ์ํ๋ ๋์์ ํ์ง๋ง API๋ต์ง ์์ ์ํ์๋ค.
๊ทธ๋์ Lv5์์๋ ๋จ์ ๊ธฐ๋ฅ ์ถ๊ฐ๊ฐ ์๋๋ผ API๋ฅผ ์ค๋ฌด์ค๋ฝ๊ฒ ๋ค๋ฌ๋ ์์ ์ ์งํํ๋ค.
๋ชฉํ
- Validation ๊ฐํ
- ์ ์ญ ์์ธ ์ฒ๋ฆฌ ๋์
- HTTP ์ํ ์ฝ๋ ๋ช ํํ ๋ถ๋ฆฌ
- ์๋ฌ ์๋ต ๊ตฌ์กฐ ํต์ผ
Validation ๊ฐํ (@Valid + DTO ์ค์ฌ ์ค๊ณ)
์ DTO์ Validation์ ๋ฃ์๋๊ฐ?
Validation์ Controller๊ฐ ์๋๋ผ DTO์ ๋๋ ๊ฒ ๋ง๋ค.
- ๋ฐ์ดํฐ์ ๊ท์น์ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ ์ ์ํ๋ ๊ฒ ์์ฐ์ค๋ฝ๋ค.
- Entity์๋ ๋น์ฆ๋์ค ๋ก์ง๋ง ๋๋ ๊ฒ์ด ์ข๋ค.
- ์ ์ง๋ณด์์ฑ์ด ์ฌ๋ผ๊ฐ๋ค.
์์ ํ ์ฝ๋
UserRequest
@NotBlank(message = "username์ ํ์์
๋๋ค.")
@Size(max = 4, message = "username์ 4๊ธ์ ์ด๋ด์ฌ์ผ ํฉ๋๋ค.")
private String username;
@Email(message = "email ํ์์ด ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.")
@NotBlank(message = "email์ ํ์์
๋๋ค.")
private String email;
@NotBlank(message = "password๋ ํ์์
๋๋ค.")
@Size(min = 8, message = "password๋ ์ต์ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.")
private String password;
ScheduleRequest
@NotBlank(message = "title์ ํ์์
๋๋ค.")
@Size(max = 10, message = "title์ 10๊ธ์ ์ด๋ด์ฌ์ผ ํฉ๋๋ค.")
private String title;
Controller์ @Valid ์ ์ฉ
@PostMapping
public ResponseEntity<ScheduleResponse> create(
@Valid @RequestBody ScheduleRequest request,
HttpSession session
)
์ฌ๊ธฐ์ ์ค์ํ ๊ฑด:
DTO์๋ง Validation์ ๋ถ์ด๋ฉด ์๋ฌด ์ผ๋ ์ ์ผ์ด๋๋ค.
๋ฐ๋์ @Valid๋ฅผ Controller์์ ๋ถ์ฌ์ผ ๋์ํ๋ค.
์ ์ญ ์์ธ ์ฒ๋ฆฌ ๋์ (@RestControllerAdvice)
์ด์ ๊น์ง๋ ์์ธ๊ฐ ๋ฐ์ํ๋ฉด:
- 500 Internal Server Error
- ๊ธฐ๋ณธ ์๋ฌ ์๋ต
- JSON ๊ตฌ์กฐ๊ฐ ํต์ผ๋์ง ์์
๊ทธ๋์ ์ ์ญ ์์ธ ์ฒ๋ฆฌ ํด๋์ค๋ฅผ ์ถ๊ฐํ๋ค.
exception ํจํค์ง ์ถ๊ฐ
exception
โฃ CustomException
โฃ ErrorResponse
โ GlobalExceptionHandler
ErrorResponse ๊ตฌ์กฐ ํต์ผ
@Getter
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String message;
private final Map<String, String> fieldErrors;
}
๋ชจ๋ ์๋ฌ๋ ์ด ๊ตฌ์กฐ๋ก ๋ด๋ ค๊ฐ๊ฒ ๋ง๋ค์๋ค.
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(...) { ... }
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(...) { ... }
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(...) { ... }
}
์ด์ ์ด๋์ ์์ธ๊ฐ ํฐ์ ธ๋ ์ด ํด๋์ค๊ฐ ์ ๋ถ ๋ฐ์์ ์ฒ๋ฆฌํ๋ค.
CustomException ๋์ (์ํ ์ฝ๋ ๋ถ๋ฆฌ)
์ด์ ์๋:
throw new IllegalArgumentException("๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
-> ์ ๋ถ 400
์ด๊ฑด ๋ช ํํ์ง ์๋ค.
๊ทธ๋์ ์ํ ์ฝ๋๋ฅผ ํฌํจํ ์์ธ ํด๋์ค๋ฅผ ๋ง๋ค์๋ค.
CustomException
public class CustomException extends RuntimeException {
private final HttpStatus status;
public CustomException(HttpStatus status, String message) {
super(message);
this.status = status;
}
}
Service์์ ์ด๋ ๊ฒ ๋ณ๊ฒฝ
๋ก๊ทธ์ธ ํ์
throw new CustomException(HttpStatus.UNAUTHORIZED, "๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค.");
-> 401
์กด์ฌํ์ง ์๋ ์ผ์
.orElseThrow(() -> new CustomException(
HttpStatus.NOT_FOUND,
"ํด๋น ์ผ์ ์ด ์กด์ฌํ์ง ์์ต๋๋ค."
));
-> 404
์ด๋ฉ์ผ ์ค๋ณต
if (userRepository.existsByEmail(request.getEmail())) {
throw new CustomException(HttpStatus.CONFLICT, "์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์
๋๋ค.");
}
-> 409
Postman ์ค์ต
์ ์ ์์ฑ

Validation ์คํจ (400)
{
"title": "12345678901",
"content": "๋ด์ฉ"
}

๋ก๊ทธ์ธ ํ์ง ์๊ณ ์ผ์ ์์ฑ (400)

๋ก๊ทธ์ธ ์ ํจ (401)
POST /api/schedules
-> 401 Unauthorized

๋ก๊ทธ์ธ- ์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ ํ๋ฆผ (401)

์กด์ฌํ์ง ์๋ ์ผ์ (404)
GET /api/schedules/99999
-> 404 Not Found

์ด๋ฉ์ผ ์ค๋ณต (409)
POST /api/users
๊ฐ์ ์ด๋ฉ์ผ 2๋ฒ ์์ฒญ
-> 409 Conflict
Test ๊ณ์ ์์ฑ

"์ด๋ฏธ ์กด์ฌํ๋ ์ด๋ฉ์ผ์ ๋๋ค" 409 ์ฒ๋ฆฌ

ํธ๋ฌ๋ธ์ํ
์ด๋ฉ์ผ ์ค๋ณต ์ 500 ์๋ฌ ๋ฐ์
์ฒ์์๋ ์ด๋ฉ์ผ ์ค๋ณต ์ 500์ด ๋ฐ์ํ๋ค.
์์ธ์:
DB์ UNIQUE ์ ์ฝ์ด ๊ฑธ๋ ค ์์ด์
DataIntegrityViolationException์ด ํฐ์ง ๊ฒ
์ด๊ฑธ ํด๊ฒฐํ๊ธฐ ์ํด:
- UserRepository์ existsByEmail() ์ถ๊ฐ
- ์ ์ฅ ์ ์ ์ฌ์ ์ฐจ๋จ
๊ฒฐ๊ณผ: 500 -> 409 Conflict๋ก ์ ์ ๋ณ๊ฒฝ
400๊ณผ 401์ ์ฐจ์ด ์ดํด
์ฒ์์๋ ๋ก๊ทธ์ธ ์ ํ ์ํ๋ 400์ผ๋ก ์ฒ๋ฆฌํ๋ค.
ํ์ง๋ง
- 400 -> ์์ฒญ ํ์์ด ์๋ชป๋จ
- 401 -> ์ธ์ฆ๋์ง ์์
์ ์ญ ์์ธ ์ฒ๋ฆฌ๊ฐ ์ ํ์ํ์ง ๊นจ๋ฌ์
์์ธ๋ฅผ Controller๋ง๋ค ์ฒ๋ฆฌํ๋ฉด ์ฝ๋๊ฐ ์ง์ ๋ถํด์ง๋ค.
์ ์ญ์ผ๋ก ์ฒ๋ฆฌํ๋:
- ์ฝ๋๊ฐ ๊น๋ํด์ง
- ์๋ฌ ์๋ต ๊ตฌ์กฐ ํต์ผ
- ์ ์ง๋ณด์์ฑ ์ฆ๊ฐ
Lv6 – ๋น๋ฐ๋ฒํธ ์ํธํ ์ ์ฉ (BCrypt)
Lv3์์ password ํ๋๋ฅผ ์ถ๊ฐํ์ง๋ง, ์ฌ์ค ๊ทธ๋๋ ํ๋ฌธ ๊ทธ๋๋ก ์ ์ฅํ๊ณ ์์๋ค.
๊ธฐ๋ฅ์ ๋์ํ์ง๋ง ๋ณด์ ๊ด์ ์์๋ ์น๋ช
์ ์ธ ๊ตฌ์กฐ์๋ค.
์ด๋ฒ Lv6์์๋ ๋น๋ฐ๋ฒํธ๋ฅผ ์ํธํํ์ฌ ์ ์ฅํ๊ณ ,
๋ก๊ทธ์ธ ์์๋ ์ํธํ๋ ๊ฐ๊ณผ ๋น๊ตํ๋๋ก ์์ ํ๋ค.
์ ์ํธํ๊ฐ ํ์ํ๊ฐ?
์ง๊ธ๊น์ง์ ๊ตฌ์กฐ:
new User(
request.getUsername(),
request.getEmail(),
request.getPassword()
);
๊ทธ๋ฆฌ๊ณ ๋ก๊ทธ์ธ ์์๋
if (!user.getPassword().equals(request.getPassword())) {
...
}
์ด ๋ฐฉ์์ ๋ฌธ์ ์ ์
- DB๊ฐ ํธ๋ฆฌ๋ฉด ๋น๋ฐ๋ฒํธ๊ฐ ๊ทธ๋๋ก ๋ ธ์ถ๋จ
- ์ด์ ํ๊ฒฝ์์๋ ์ ๋ ํ์ฉ๋์ง ์๋ ๊ตฌ์กฐ
- equals ๋น๊ต๋ ๋ณด์์ ์ผ๋ก ์ทจ์ฝ
๊ทธ๋์ ๋จ๋ฐฉํฅ ์ํธํ(ํด์) ๋ฐฉ์์ผ๋ก ์ ํํ๋ค.
build.gradle์ BCrypt ์ถ๊ฐ
implementation 'at.favre.lib:bcrypt:0.10.2'
Gradle์ reloadํ ๋ค, ์ํธํ ํด๋์ค๋ฅผ ์ถ๊ฐํ๋ค.
PasswordEncoder ์ง์ ๊ตฌํ
Spring Security๋ฅผ ์ฐ์ง ์๊ณ ,
๊ณผ์ ์๊ตฌ์ฌํญ์ ๋ง๊ฒ ์ง์ PasswordEncoder๋ฅผ ๊ตฌํํ๋ค.
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults()
.hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer()
.verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
- encode() -> ํ์๊ฐ์ ์ ์ํธํ
- matches() -> ๋ก๊ทธ์ธ ์ ๋น๊ต
UserService ์์ (ํ์๊ฐ์ ์ ์ํธํ)
๊ธฐ์กด ์ฝ๋:
request.getPassword()
์์ ์ฝ๋:
String encodedPassword = passwordEncoder.encode(request.getPassword());
User saved = userRepository.save(
new User(
request.getUsername(),
request.getEmail(),
encodedPassword
)
);
์ด์ DB์ ์ ์ฅ๋๋ ๊ฐ์ ๋ค์๊ณผ ๊ฐ์ ํํ๊ฐ ๋๋ค.
$2a$10$g9dS9s8sK...

๋ก๊ทธ์ธ ๋ก์ง ์์
์ด์ :
if (!user.getPassword().equals(request.getPassword())) {
์์ :
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new CustomException(HttpStatus.UNAUTHORIZED,
"์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.");
}
์ด์ ๋ raw password vs encoded password๋ฅผ ์์ ํ๊ฒ ๋น๊ตํ๋ค.
Postman ์ค์ต
ํ์๊ฐ์
{
"username": "lee",
"email": "lee@test.com",
"password": "12345678"
}

DB ํ์ธ -> ์ํธํ๋ ๋ฌธ์์ด ์ ์ฅ๋จ

๋ก๊ทธ์ธ ์ฑ๊ณต
{
"email": "lee@test.com",
"password": "12345678"
}
-> 200 OK

๋ก๊ทธ์ธ ์คํจ (ํ๋ฆฐ ๋น๋ฐ๋ฒํธ)
{
"email": "lee@test.com",
"password": "11111111"
}
-> 401 Unauthorized

ํธ๋ฌ๋ธ์ํ
1) ๊ธฐ์กด ์ ์ ๋ก๊ทธ์ธ ์คํจ
์ํธํ ์ ์ฉ ํ, ๊ธฐ์กด์ ํ๋ฌธ์ผ๋ก ์ ์ฅ๋ ์ ์ ๋ ๋ก๊ทธ์ธ์ ์คํจํ๋ค.
์ด์ ๋ ๊ฐ๋จํ๋ค.
- DB์๋ ํ๋ฌธ
- ๋ก๊ทธ์ธ ๋ก์ง์ ์ํธํ ๋น๊ต
๊ทธ๋์ ํ ์คํธ๋ฅผ ์ํด ์๋ก ํ์๊ฐ์ ์ ์งํํ๋ค.
-> ์ํธํ ์ ์ฉ ์ดํ ์์ฑ๋ ์ ์ ๋ง ์ ์ ๋ก๊ทธ์ธ ๊ฐ๋ฅ
2) equals ๋น๊ต์ ์ํ์ฑ ์ฒด๊ฐ
๋จ์ equals ๋น๊ต๋ ๋ณด์์ ์ผ๋ก ๋งค์ฐ ์ทจ์ฝํ๋ค.
BCrypt๋:
- salt ์๋ ์ ์ฉ
- ๋ฌด์ฐจ๋ณ ๋์ ๊ณต๊ฒฉ ๋ฐฉ์ง
- ๋จ๋ฐฉํฅ ํด์
์ค๋ฌด์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๋ฐฉ์์ด๋ผ๋ ์ ๋ ์ง์ ์ฒด๊ฐํ๋ค.
์ด๋ฒ ๋จ๊ณ์์ ๋๋ ์
Lv5์์ ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ ๋ฆฌํ๋ค๋ฉด,
Lv6์ ๋ณด์์ ์ ๊ฒฝ ์ฐ๋ ๋จ๊ณ์๋ค.
๋จ์ํ ๊ธฐ๋ฅ์ด ๋๋ API๊ฐ ์๋๋ผ, ์ด์ ๊ฐ๋ฅํ API๋ก ๋ฐ์ ํ๋ ๋๋์ด์๋ค.
- ์ํ ์ฝ๋ ๋ถ๋ฆฌ
- ์ ์ญ ์์ธ ์ฒ๋ฆฌ
- ๋น๋ฐ๋ฒํธ ์ํธํ
์ด์ ์ผ ๋น๋ก์ ๋ฐฑ์๋ API๋ต๋ค๋ ๋๋์ด ๋ค์๋ค.