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

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

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

[๋‚ด์ผ๋ฐฐ์›€์บ ํ”„ Spring 3๊ธฐ] CH 3 ์‹ฌํ™” Spring ์ฝ”๋“œ ๊ฐœ์„  ๊ณผ์ œ - ํ•„์ˆ˜ ๊ธฐ๋Šฅ

jiiimni 2026. 3. 10. 12:41

ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ ์š”์•ฝ

  • Lv0: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰์„ ์œ„ํ•œ JWT ํ‚ค/DB ์„ค์ •
  • Lv1: @Auth AuthUser ํŒŒ๋ผ๋ฏธํ„ฐ ๋ฐ”์ธ๋”ฉ์„ ์œ„ํ•œ ArgumentResolver ๋“ฑ๋ก
  • Lv2: ํšŒ์›๊ฐ€์ž…/๋กœ๊ทธ์ธ ๋ฐ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ ๋กœ์ง ๋ฆฌํŒฉํ† ๋ง(๊ฐ€๋…์„ฑ/์ฑ…์ž„ ๋ถ„๋ฆฌ)
  • Lv3: N+1 ํ•ด๊ฒฐ (fetch join -> EntityGraph๋กœ ๊ต์ฒด)
  • Lv4: ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • + ์„œ๋น„์Šค ๋กœ์ง ์ˆ˜์ •์œผ๋กœ ํ…Œ์ŠคํŠธ ์‹คํŒจ ์›์ธ ์ œ๊ฑฐ

Lv0. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ (JWT ํ‚ค / DB ์„ค์ •)

๋ฌธ์ œ ์ƒํ™ฉ

์ฒ˜์Œ ํ”„๋กœ์ ํŠธ ์‹คํ–‰์„ ์œ„ํ•ด application.yml์— JWT secret, datasource ์„ค์ •์ด ํ•„์š”ํ–ˆ๋‹ค.

์˜ˆ์‹œ:

jwt:
  secret:
    key: (base64 key)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/expert
    username: root
    password: 12345678
    driver-class-name: com.mysql.cj.jdbc.Driver

jpa:
  hibernate:
    ddl-auto: create

์‹คํ–‰ ๋กœ๊ทธ์—์„œ Tomcat 8080, Hibernate, HikariCP ๋ชจ๋‘ ์ •์ƒ ๊ธฐ๋™๋˜๋ฉด Lv0์€ ํ†ต๊ณผ๋กœ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 1)

์ฒ˜์Œ์—๋Š” application-local.yml์— DB ๋น„๋ฐ€๋ฒˆํ˜ธ/JWT ํ‚ค๋ฅผ ๊ทธ๋Œ€๋กœ ๋„ฃ๊ณ  ์ปค๋ฐ‹ํ•ด๋ฒ„๋ ธ๋‹ค.
๊ทธ ๊ฒฐ๊ณผ GitHub ์ปค๋ฐ‹ diff์—์„œ ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋˜๋Š” ๊ฑธ ํ™•์ธํ–ˆ๋‹ค.

๋‚ด๊ฐ€ ์„ ํƒํ•œ ๋ฐฉ์‹ (๋…ธ์ถœ ๋ฐฉ์ง€)

  • ๋ฏผ๊ฐ ์ •๋ณด๋Š” ๋กœ์ปฌ ์ „์šฉ ํŒŒ์ผ(application-local.yml) ๋กœ ๊ด€๋ฆฌ
  • ๊นƒ์—๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ placeholder๋งŒ ๋‚จ๊น€
  • ๊ทธ๋ฆฌ๊ณ  ์ด๋ฏธ ์˜ฌ๋ผ๊ฐ„ ํžˆ์Šคํ† ๋ฆฌ๋Š” git filter-repo๋กœ ์ •๋ฆฌ

application.yml์€ ์ด๋ ‡๊ฒŒ ๋ฐ”๊พธ์—ˆ๋‹ค:

jwt:
  secret:
    key: ${JWT_SECRET_KEY}

spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

๊ทธ๋ฆฌ๊ณ  application-local.yml์€ .gitignore์— ์ถ”๊ฐ€ํ•ด์„œ ์ถ”์  ์ œ์™ธ.

ํžˆ์Šคํ† ๋ฆฌ์—์„œ ํŒŒ์ผ ์™„์ „ ์ œ๊ฑฐ

์ด๋ฏธ ์ปค๋ฐ‹ ํžˆ์Šคํ† ๋ฆฌ์— ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋‚จ์œผ๋ฉด “ํŒŒ์ผ์„ ์‚ญ์ œํ–ˆ๋”๋ผ๋„” ๊ณผ๊ฑฐ ์ปค๋ฐ‹์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
๊ทธ๋ž˜์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ํžˆ์Šคํ† ๋ฆฌ์—์„œ ์ œ๊ฑฐํ–ˆ๋‹ค.

git filter-repo --force --path src/main/resources/application-local.yml --invert-paths

์ด ๊ณผ์ •์—์„œ origin remote๊ฐ€ ์ž๋™ ์ œ๊ฑฐ๋˜๋Š” ์ด์Šˆ๊ฐ€ ์žˆ์—ˆ๊ณ , remote๋ฅผ ๋‹ค์‹œ ์žก์•„์•ผ push๊ฐ€ ๊ฐ€๋Šฅํ–ˆ๋‹ค.

(์ด๊ฑด ๋‹ค์Œ์— ๋‹ค์‹œ ๊ฐ™์€ ์‹ค์ˆ˜ ์•ˆ ํ•˜๋ ค๊ณ  ๊ผญ ๊ธฐ๋กํ•ด๋‘”๋‹ค.)


Lv1. ArgumentResolver ๋“ฑ๋ก (AuthUser ์ฃผ์ž…)

์š”๊ตฌ์‚ฌํ•ญ

์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ด๋ ‡๊ฒŒ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค:

@PostMapping("/todos")
public ResponseEntity<TodoSaveResponse> saveTodo(
        @Auth AuthUser authUser,
        @Valid @RequestBody TodoSaveRequest todoSaveRequest
) { ... }

์ฆ‰, @Auth + AuthUser ์กฐํ•ฉ์œผ๋กœ ๋“ค์–ด์˜ค๋ฉด
ํ•„ํ„ฐ์—์„œ ๋„ฃ์–ด๋‘” request attribute๋ฅผ ๊บผ๋‚ด์„œ AuthUser๋กœ ๋งŒ๋“ค์–ด ์ฃผ์ž…ํ•ด์•ผ ํ•œ๋‹ค.


ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… 2) Resolver ๋งŒ๋“ค์—ˆ๋Š”๋ฐ ์™œ ๋™์ž‘์„ ์•ˆ ํ•˜์ง€?

์›์ธ

HandlerMethodArgumentResolver๋Š” ๊ตฌํ˜„๋งŒ ํ•˜๋ฉด ์ž๋™์œผ๋กœ ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
๋ฐ˜๋“œ์‹œ WebMvcConfigurer์—์„œ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค.

๋“ฑ๋ก์„ ๋นผ๋จน์œผ๋ฉด ์ปจํŠธ๋กค๋Ÿฌ์—์„œ @Auth AuthUser๋ฅผ ๋ฐ›์„ ๋•Œ ๋ฐ”์ธ๋”ฉ์ด ์•ˆ ๋˜๊ฑฐ๋‚˜,
์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.


ํ•ด๊ฒฐ: WebConfig์— Resolver ๋“ฑ๋ก

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers){
        resolvers.add(new AuthUserArgumentResolver());
    }
}

AuthUserArgumentResolver ๊ตฌํ˜„ ์˜๋„

public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
        boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);

        // @Auth ์–ด๋…ธํ…Œ์ด์…˜๊ณผ AuthUser ํƒ€์ž…์ด ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ
        if (hasAuthAnnotation != isAuthUserType) {
            throw new AuthException("@Auth์™€ AuthUser ํƒ€์ž…์€ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
        }

        return hasAuthAnnotation;
    }

    @Override
    public Object resolveArgument(...) {
        HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();

        Long userId = (Long) request.getAttribute("userId");
        String email = (String) request.getAttribute("email");
        UserRole userRole = UserRole.of((String) request.getAttribute("userRole"));

        return new AuthUser(userId, email, userRole);
    }
}

๋‚ด๊ฐ€ ์ด๋ ‡๊ฒŒ ์ง  ์ด์œ 

  • supportsParameter์—์„œ @Auth์™€ AuthUser๋ฅผ ๊ฐ•์ œ ๋ฌถ์Œ ์ฒ˜๋ฆฌ
    -> ์ปจํŠธ๋กค๋Ÿฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ์‹ค์ˆ˜๋กœ @Auth String email ๊ฐ™์€ ํ˜•ํƒœ๋กœ ์“ฐ๋ฉด ์ฆ‰์‹œ ์˜ˆ์™ธ๋กœ ์•Œ๋ ค์คŒ
  • resolveArgument๋Š” JwtFilter์—์„œ ์ €์žฅํ•œ attribute๋ฅผ ๊ทธ๋Œ€๋กœ ๊บผ๋‚ด ์ธ์ฆ ์ •๋ณด ์ „๋‹ฌ ์ฑ…์ž„์„ ํ•œ ๊ณณ์— ๋ชจ์Œ

Lv2. ์ฝ”๋“œ ๊ฐœ์„  (๋ฆฌํŒฉํ† ๋ง)

1) Early Return ์ ์šฉ (Signup ๋กœ์ง ๊ฐœ์„ )

๋ฌธ์ œ

๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” ์กฐ๊ฑด๋ฌธ์ด ์ค‘์ฒฉ๋˜๋Š” ๊ตฌ์กฐ์˜€๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋ฉ”์ผ ์ค‘๋ณต ์ฒดํฌ ๊ฐ™์€ ๋กœ์ง์—์„œ if ๋ธ”๋ก ์•ˆ์— ๋กœ์ง์ด ๊ณ„์† ๋“ค์–ด๊ฐ€๋Š” ํ˜•ํƒœ์˜€๋‹ค.

์ด ๋ฐฉ์‹์€ ์ฝ”๋“œ๊ฐ€ ๊ธธ์–ด์งˆ์ˆ˜๋ก ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

  • ์กฐ๊ฑด๋ฌธ์ด ๊นŠ์–ด์ง (Nested if)
  • ํ•ต์‹ฌ ๋กœ์ง ํŒŒ์•…์ด ์–ด๋ ค์›€
  • ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง

ํ•ด๊ฒฐ: Early Return ์ ์šฉ

Early Return์€ ์กฐ๊ฑด์ด ๋งŒ์กฑ๋˜์ง€ ์•Š์œผ๋ฉด ๋ฐ”๋กœ return ๋˜๋Š” ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” ๋ฐฉ์‹์ด๋‹ค.

if (userRepository.existsByEmail(signupRequest.getEmail())) {
    throw new InvalidRequestException("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค.");
}

๊ทธ ๋‹ค์Œ์— ์ •์ƒ ๋กœ์ง์„ ๋ฐ”๋กœ ์ง„ํ–‰ํ•œ๋‹ค.

User savedUser = userRepository.save(
        new User(
                signupRequest.getEmail(),
                encodedPassword,
                signupRequest.getUserName(),
                userRole
        )
);

Early Return์„ ์ ์šฉํ•œ ์ด์œ 

Early Return์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ ์žฅ์ ์ด ์žˆ๋‹ค.

  • ์ฝ”๋“œ ๊นŠ์ด๊ฐ€ ์ค„์–ด๋“ ๋‹ค
  • ์ •์ƒ ํ๋ฆ„์ด ๋” ์ž˜ ๋ณด์ธ๋‹ค
  • ์˜ˆ์™ธ ์ผ€์ด์Šค๋ฅผ ๋จผ์ € ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค

์ฆ‰,

"์˜ˆ์™ธ ์ƒํ™ฉ ๋จผ์ € ์ฒ˜๋ฆฌ -> ์ •์ƒ ๋กœ์ง ์‹คํ–‰"

์ด๋ผ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค.

์ด ํŒจํ„ด์€ Spring ์„œ๋น„์Šค ๋กœ์ง์—์„œ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.


2) WeatherClient ๋ถˆํ•„์š”ํ•œ if-else ์ œ๊ฑฐ

WeatherClient ์ฝ”๋“œ์—์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ตฌ์กฐ๊ฐ€ ์žˆ์—ˆ๋‹ค.

if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
    throw new ServerException(...);
} else {
    if (weatherArray == null || weatherArray.length == 0) {
        throw new ServerException(...);
    }
}

์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๋Š”

  • ๋ถˆํ•„์š”ํ•œ else ๋ธ”๋ก
  • ์กฐ๊ฑด๋ฌธ ์ค‘์ฒฉ
  • ๊ฐ€๋…์„ฑ ์ €ํ•˜

์˜€๋‹ค.


๊ฐœ์„  ๋ฐฉ๋ฒ•

๋ถˆํ•„์š”ํ•œ else๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ์กฐ๊ฑด์„ ๋ถ„๋ฆฌํ–ˆ๋‹ค.

if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
    throw new ServerException("๋‚ ์”จ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
}

if (weatherArray == null || weatherArray.length == 0) {
    throw new ServerException("๋‚ ์”จ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.");
}

์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•œ ์ด์œ 

๋ถˆํ•„์š”ํ•œ else๋Š” ์ฝ”๋“œ ๊ฐ€๋…์„ฑ์„ ๋–จ์–ด๋œจ๋ฆฐ๋‹ค.

์กฐ๊ฑด์ด ์‹คํŒจํ•˜๋ฉด ๋ฐ”๋กœ ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๊ณ 
์ •์ƒ ํ๋ฆ„์„ ์ด์–ด๊ฐ€๋Š” ๋ฐฉ์‹์ด ๋” ์ž์—ฐ์Šค๋Ÿฝ๋‹ค.

์ด ์—ญ์‹œ Early Fail / Guard Clause ํŒจํ„ด์ด๋ผ๊ณ  ํ•œ๋‹ค.


3) ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ ๋กœ์ง์„ DTO Validation์œผ๋กœ ์ด๋™

๊ธฐ์กด ์ฝ”๋“œ์—์„œ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ์„ ์„œ๋น„์Šค ๋ ˆ์ด์–ด์—์„œ ์ง์ ‘ ์ˆ˜ํ–‰ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.

if (userChangePasswordRequest.getNewPassword().length() < 8 ||
        !userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
        !userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
    throw new InvalidRequestException("์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•˜๊ณ , ์ˆซ์ž์™€ ๋Œ€๋ฌธ์ž๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.");
}

์ด ๋ฐฉ์‹์€ ๋‹ค์Œ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

  • ์„œ๋น„์Šค ๋กœ์ง์ด ๊ธธ์–ด์ง
  • ๊ฒ€์ฆ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ์„œ๋น„์Šค์— ํฉ์–ด์งˆ ์ˆ˜ ์žˆ์Œ
  • ์žฌ์‚ฌ์šฉ์„ฑ์ด ๋–จ์–ด์ง

ํ•ด๊ฒฐ: DTO Validation ์‚ฌ์šฉ

DTO์— Validation์„ ์ ์šฉํ–ˆ๋‹ค.

@NotBlank
private String newPassword;

๊ทธ๋ฆฌ๊ณ  Controller์—์„œ

@Valid

๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ๊ฒ€์ฆํ•˜๋„๋ก ํ–ˆ๋‹ค.


Validation์„ DTO๋กœ ์˜ฎ๊ธด ์ด์œ 

Spring์—์„œ๋Š” ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ์„ DTO์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ธ ํŒจํ„ด์ด๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด

  • ์„œ๋น„์Šค ๋กœ์ง์ด ๋‹จ์ˆœํ•ด์ง
  • ์ฑ…์ž„ ๋ถ„๋ฆฌ ๊ฐ€๋Šฅ
  • ์ฝ”๋“œ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • ์œ ์ง€๋ณด์ˆ˜์„ฑ ์ฆ๊ฐ€

Lv2 ์ฝ”๋“œ ๊ฐœ์„ ์„ ํ†ตํ•ด ์–ป์€ ๊ฒƒ

์ด๋ฒˆ ๋ฆฌํŒฉํ† ๋ง์„ ํ†ตํ•ด ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ๋„˜์–ด์„œ

  • ๊ฐ€๋…์„ฑ ์žˆ๋Š” ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐฉ๋ฒ•
  • ์„œ๋น„์Šค ๋กœ์ง์„ ๋‹จ์ˆœํ•˜๊ฒŒ ์œ ์ง€ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • Spring Validation์˜ ํ™œ์šฉ ๋ฐฉ์‹

์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

ํŠนํžˆ,

"๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ฒ€์ฆ ๋กœ์ง์„ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ"

์ด ์„œ๋น„์Šค ๋ ˆ์ด์–ด ์„ค๊ณ„์—์„œ ๋งค์šฐ ์ค‘์š”ํ•˜๋‹ค๋Š” ๊ฒƒ์„ ๋А๊ผˆ๋‹ค.


Lv3. N+1 ํ•ด๊ฒฐ (fetch join -> EntityGraph๋กœ ๋ณ€๊ฒฝ)

๋ฌธ์ œ ์‹œ๋‚˜๋ฆฌ์˜ค

getTodos()์—์„œ Todo ๋ชฉ๋ก ์กฐํšŒ ํ›„ todo.getUser()๋ฅผ ์ ‘๊ทผํ•œ๋‹ค.

Todo.user๋Š” LAZY์ด๊ธฐ ๋•Œ๋ฌธ์—,
Todo N๊ฐœ ์กฐํšŒ ํ›„ user๋ฅผ ์ ‘๊ทผํ•˜๋Š” ์ˆœ๊ฐ„ N๋ฒˆ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค -> N+1.

๊ธฐ์กด ์ฝ”๋“œ๋Š” JPQL fetch join์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค:

public interface TodoRepository extends JpaRepository<Todo, Long> {

    @Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
    Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

    @Query("SELECT t FROM Todo t " +
            "LEFT JOIN FETCH t.user " +
            "WHERE t.id = :todoId")
    Optional<Todo> findByIdWithUser(@Param("todoId") Long todoId);

    int countById(Long todoId);
}

์š”๊ตฌ์‚ฌํ•ญ: EntityGraph๋กœ ๋™์ผ ๋™์ž‘ ๊ตฌํ˜„

EntityGraph ๋ฐฉ์‹ ์˜ˆ์‹œ (์ตœ์ข… ๋ชฉํ‘œ ํ˜•ํƒœ)

@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);

๊ทธ๋ฆฌ๊ณ  ๋‹จ๊ฑด ์กฐํšŒ๋„ ๋™์ผํ•˜๊ฒŒ:

@EntityGraph(attributePaths = {"user"})
Optional<Todo> findById(Long todoId);

์ฃผ์˜: @EntityGraph๋Š” “์—ฐ๊ด€ ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•จ๊ป˜ ๋กœ๋”ฉ”ํ•˜๋„๋ก ํžŒํŠธ๋ฅผ ์ฃผ๋Š” ๋ฐฉ์‹์ด๊ณ ,
fetch join์ฒ˜๋Ÿผ ์ง์ ‘ JPQL์„ ์“ฐ์ง€ ์•Š์•„๋„ N+1์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.


Lv4. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ˆ˜์ • + ์„œ๋น„์Šค ๋กœ์ง ๋ณด์™„

Lv4๋Š” “์ฝ”๋“œ๊ฐ€ ๋งž๋Š”๋ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ํ‹€๋ฆผ / ํ…Œ์ŠคํŠธ๋Š” ๋งž๋Š”๋ฐ ๋กœ์ง์ด ๋ฐ”๋€Œ์–ด์„œ ๊นจ์ง” ๊ฐ™์€ ์ƒํ™ฉ์„ ์ง์ ‘ ๋‹ค๋ฃจ๊ฒŒ ํ–ˆ๋‹ค.


(1) PasswordEncoderTest ์ˆ˜์ •

๋ฌธ์ œ

matches() ๋ฉ”์„œ๋“œ ํŒŒ๋ผ๋ฏธํ„ฐ ์ˆœ์„œ๋ฅผ ์ž˜๋ชป ๋„ฃ์œผ๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค.

์ •์ƒ์€:

boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);

encoded๋ฅผ raw์ฒ˜๋Ÿผ ๋„ฃ์œผ๋ฉด ๋‹น์—ฐํžˆ ๋น„๊ต๊ฐ€ ์•ˆ ๋งž๋Š”๋‹ค.


(2) ManagerServiceTest: Todo ์—†์„ ๋•Œ NPE๊ฐ€ ์•„๋‹ˆ๋ผ InvalidRequestException

๋‚ด๊ฐ€ ๊ณ ์นœ ๋ฐฉํ–ฅ

  • ํ…Œ์ŠคํŠธ ๋ฉ”์„œ๋“œ๋ช…๋ถ€ํ„ฐ ์ปจํ…์ŠคํŠธ์— ๋งž๊ฒŒ ์ˆ˜์ •
    (NPE๊ฐ€ ์•„๋‹ˆ๋ผ InvalidRequestException์„ ๊ธฐ๋Œ€ํ•ด์•ผ ํ•จ)
  • ์˜ˆ์™ธ ํƒ€์ž… + ๋ฉ”์‹œ์ง€๋ฅผ ๋™์‹œ์— ๊ฒ€์ฆํ•˜๋„๋ก assertThatThrownBy๋ฅผ ์‚ฌ์šฉ
assertThatThrownBy(() -> managerService.getManagers(todoId))
    .isInstanceOf(InvalidRequestException.class)
    .hasMessage("Todo not found");

์™œ assertThatThrownBy๋ฅผ ์ผ๋‚˜?

  • ์˜ˆ์™ธ ๊ฒ€์ฆ์„ ์ฒด์ด๋‹์œผ๋กœ ํ•œ ๋ฒˆ์— ํ‘œํ˜„ ๊ฐ€๋Šฅ -> ๊ฐ€๋…์„ฑ ์˜ฌ๋ผ๊ฐ
  • assertThrows๋Š” ๋ฉ”์‹œ์ง€ ๊ฒ€์ฆ๊นŒ์ง€ ํ•˜๋ ค๋ฉด ์˜ˆ์™ธ๋ฅผ ๋ณ€์ˆ˜๋กœ ๋ฐ›์•„์„œ ์ถ”๊ฐ€ ๊ฒ€์ฆํ•ด์•ผ ํ•จ -> ์˜๋„๊ฐ€ ๊ธธ์–ด์ง


(3) CommentServiceTest: Todo ์—†์„ ๋•Œ ๋˜์ง€๋Š” ์˜ˆ์™ธ๊ฐ€ ๋‹ค๋ฅด๋‹ค

ํ…Œ์ŠคํŠธ๋Š” ServerException์„ ๊ธฐ๋Œ€ํ–ˆ์ง€๋งŒ, ์‹ค์ œ ์ปจํ…์ŠคํŠธ(์„œ๋น„์Šค ๊ตฌํ˜„)๋Š” InvalidRequestException์„ ๋˜์งˆ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค.

์ฆ‰, ํ…Œ์ŠคํŠธ์˜ ๊ธฐ๋Œ€๊ฐ’(์˜ˆ์™ธ ํƒ€์ž…/๋ฉ”์‹œ์ง€)์„ ์„œ๋น„์Šค ์ปจ๋ฒค์…˜๊ณผ ๋งž์ถฐ์•ผ ํ…Œ์ŠคํŠธ๊ฐ€ ์˜๋ฏธ๋ฅผ ๊ฐ€์ง„๋‹ค.


(4) ํ•ต์‹ฌ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…: todo.user๊ฐ€ null์ด๋ฉด NPE -> ์„œ๋น„์Šค ๋กœ์ง ์ˆ˜์ •

์ฆ์ƒ

ํ…Œ์ŠคํŠธ์—์„œ ์ผ๋ถ€๋Ÿฌ Todo.user = null๋กœ ๋งŒ๋“ค์—ˆ๋”๋‹ˆ, ์„œ๋น„์Šค ๋กœ์ง์—์„œ ์•„๋ž˜๊ฐ€ ํ„ฐ์กŒ๋‹ค:

todo.getUser().getId()

ํ•ด๊ฒฐ

๊ถŒํ•œ ์ฒดํฌ ์ „์— null ๋ฐฉ์–ด ๋กœ์ง์„ ๋„ฃ์—ˆ๋‹ค.

if (todo.getUser() == null) {
    throw new InvalidRequestException("์ผ์ •์„ ์ƒ์„ฑํ•œ ์œ ์ €๋งŒ ๋‹ด๋‹น์ž๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.");
}

๋‚ด๊ฐ€ ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•œ ์ด์œ 

  • user๊ฐ€ null์ด๋ฉด ๊ถŒํ•œ ๊ฒ€์ฆ ์ž์ฒด๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•จ (๋น„๊ตํ•  ๋Œ€์ƒ์ด ์—†์Œ)
  • “NPE(๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜)” ๋Œ€์‹  “์˜๋„ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ์˜ˆ์™ธ”๋กœ ๋ฐ”๊พธ๋ฉด
    • ํ…Œ์ŠคํŠธ๋„ ์•ˆ์ •ํ™”๋˜๊ณ 
    • API ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ๋„ ์—๋Ÿฌ ์˜๋ฏธ๊ฐ€ ๋ช…ํ™•ํ•ด์ง„๋‹ค


ํšŒ๊ณ : ์ด๋ฒˆ ๊ณผ์ œ๋กœ ์–ป์€ ๊ฒƒ

  • ArgumentResolver๋Š” ๋งŒ๋“ค๊ธฐ๋ณด๋‹ค ๋“ฑ๋ก ๋ˆ„๋ฝ์ด ๋” ํ”ํ•œ ์‹ค์ˆ˜๋ผ๋Š” ์ 
  • ํ…Œ์ŠคํŠธ๋Š” “์„ฑ๊ณต์‹œํ‚ค๋Š” ๊ฒƒ”๋ณด๋‹ค ์™œ ์‹คํŒจํ–ˆ๋Š”์ง€ ์›์ธ์„ ๋ถ„ํ•ดํ•˜๋Š” ๊ณผ์ •์ด ํ›จ์”ฌ ํ•™์Šต์ด ๋œ๋‹ค๋Š” ์ 
  • ์˜ˆ์™ธ ์ฒ˜๋ฆฌ์˜ ๋ชฉ์ ์€ “์—๋Ÿฌ๋ฅผ ๋ง‰๋Š” ๊ฒƒ”์ด ์•„๋‹ˆ๋ผ ์˜๋ฏธ ์žˆ๋Š” ์‹คํŒจ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด๋ผ๋Š” ์ 
    (NPE -> InvalidRequestException์œผ๋กœ ๋ณ€๊ฒฝํ•œ ์ผ€์ด์Šค)

ํ”„๋กœ์ ํŠธ github ๋งํฌ

CH3 ํ”„๋กœ์ ํŠธ github ๋งํฌ