ํ๋ก์ ํธ ๋ชฉํ ์์ฝ
- 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์ผ๋ก ๋ณ๊ฒฝํ ์ผ์ด์ค)