์ด์ง€๋ฏผ์˜ ๊ฐœ๋ฐœ ๋…ธํŠธ

๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์„ ๊ณต๋ถ€ํ•˜๋ฉฐ Java์™€ Spring์„ ์ค‘์‹ฌ์œผ๋กœ ํ•™์Šต ๊ธฐ๋ก์„ ์ •๋ฆฌํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.

๐Ÿ”ฅ ๋‚ด์ผ๋ฐฐ์›€์บ ํ”„/๐Ÿ“Œ ๊ณผ์ œ

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„ Spring 3๊ธฐ] CH3 ์ˆ™๋ จ Spring ์ผ์ • ๊ด€๋ฆฌ ์•ฑ Develop - ๋„์ „ ๊ธฐ๋Šฅ

jiiimni 2026. 3. 10. 12:28

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๋‹ต๋‹ค๋Š” ๋А๋‚Œ์ด ๋“ค์—ˆ๋‹ค.