Spring ์ฌํ ์ฃผ์ฐจ ๊ฐ์ธ ๊ณผ์ ๋ฅผ ์งํํ๋ค. ์ฝ๋ ๋ฆฌํฉํ ๋ง, N+1 ๋ฌธ์ ํด๊ฒฐ, API ๋ก๊น , ํ ์คํธ ์ฝ๋ ์์ฑ ๋ฑ์ ๊ณผ์ ๋ฅผ ์ํํ๋ค. ๊ทธ ๊ณผ์ ์์์ ํธ๋ฌ๋ธ์ํ ๊ณผ ๊ณ ๋ฏผํ๋ ์ , ๋ฐฐ์ด ์ ๋ฑ์ ๊ธฐ๋กํ๊ณ ์ ํ๋ค.
์ฒ์ ํ๋ก์ ํธ๋ฅผ ๋ฐ์ ์คํํ๋ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋์๋ค.
์๋ฌ ๋ฉ์ธ์ง๊ฐ ๊ธธ๊ธด ํ์ง๋ง ์ ์ฝ์ด๋ณด๋ฉด JWT ์ํฌ๋ฆฟ ํค์ ๋ํ ๋ฌธ์ ์ธ ๊ฒ์ ์ง์ํ ์ ์๋ค. application.properties
ํ์ผ์ ํค๋ฅผ ๋ฃ์ผ๋ ค๊ณ ํ๋๋ฐ, ์์ resourse
ํด๋๊ฐ ์์๋ค. ๋ฐ๋ผ์ ๋จผ์ ํค๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค.
java.lang.IllegalArgumentException: Could not resolve placeholder 'jwt.secret.key' in value "${jwt.secret.key}"
org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
Unsatisfied dependency expressed through constructor parameter 0:
Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
ํฐ๋ฏธ๋์ ์๋์ ๊ฐ์ด ์ ๋ ฅํด ํค๋ฅผ ๋ง๋ค์๋ค. ํค๋ฅผ ์ฒ์ ๋ง๋ค์ด ๋ณด์ง๋ง ๊ฒ์ํ๋ ๋ง๋๋ ๋ฐฉ๋ฒ์ด ๋ฐ๋ก ๋์๋ค. ํฐ๋ฏธ๋๋ก ๋ง๋๋ ๋ฐฉ๋ฒ ์ธ์๋ ํค๋ฅผ ๋ง๋๋ ์ฌ์ดํธ๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ๋ ์์๋ค.
openssl rand -hex 64
๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ๊ฐ์ด ์์ฃผ ๊ธด ์ํฌ๋ฆฟ ํค๊ฐ ๋์๋ค.
22b8c9ae9a1b397ef0c8a2d142f91d1d00b7c0dc0dae01d2999d197f111f3e01117c15a4477f63732fa2aa6fe8fbf313874b681fa31cef4049aa50707b7f8156
๊ทธ๋ฆฌ๊ณ resourse ํด๋๋ฅผ ์์ฑ ํ, application.properties
ํ์ผ์ ๋ง๋ค๊ณ ๋ค์๊ณผ ๊ฐ์ด ํค๋ฅผ ๋ฃ์๋ค.
jwt.secret.key="JWT_์ํฌ๋ฆฟ_ํค"
์ฌ๊ธฐ์ ์ค์ํ ์ ์ JwtUtil
ํด๋์ค์์${jwt.secret.key}
๋ก ์ ์๋ค๋ฉด, jwt.secret.key
๋ก ๋๊ฐ์ด ์ ์ด์ผ ํ๋ค๋ ์ ์ด๋ค.
๊ทธ๋ฐ๋ฐ ๋ ์๋ฌ๊ฐ ๋ฌ๋ค. ์์ผ๊น? ์ ์ดํด๋ณด๋ ์๋ก์ด ์๋ฌ๊ฐ ๋ฑ์ฅํ๋ค.
java.lang.IllegalArgumentException: Illegal base64 character 22
ํด๋น ์๋ฌ์ ๋ํ ๋ด์ฉ์ ๊ฒ์ํ๋ค๊ฐ JWT๋ฅผ ์ฌ์ฉํ ๋ค๋ฅธ ์ฌ๋์ ์คํฌ๋ฆฐ์ท์ ๋ณด๊ณ ๋์ ์ค์๋ฅผ ์์์ฐจ๋ ธ๋ค. ๋ฐ๋ก ์๋ฐ์ดํ๋ก ํค๋ฅผ ๊ฐ์ผ ๊ฒ์ด๋ค.
์ด๋ ๊ฒ ์์ฑํ์ง ๋ง๊ณ ,
jwt.secret.key="JWT_์ํฌ๋ฆฟ_ํค"
์ด๋ ๊ฒ ์๋ฐ์ดํ ์์ด ์์ฑํด์ผ ํ๋ค. ์ ๋๋ก ๊ณ ์น ํ ์คํํ๋ ์ ์์ ์ผ๋ก ์๋ํ๋ค.
jwt.secret.key=JWT_์ํฌ๋ฆฟ_ํค
+) ์ถ๊ฐ ๊ณต๋ถ ๐
์ฒ์์๋ ์ํฌ๋ฆฟ ํค ๊ฐ์ ๋ฐ๊พผ ๊ฒ ๋๋ฌธ์ ์ ์ ์๋ํ๋ ์ค ์์์ง๋ง ์๋์๋ค. ํค ๊ฐ์ด ๋ฌธ์ ๋ ์๋์์ง๋ง, ๋ด๊ฐ ์์์ ๋ง๋ ํค ๊ฐ์ ํ์ฌ ํ๋ก์ ํธ์์ ์ฌ์ฉํ๊ณ ์๋ ์๊ณ ๋ฆฌ์ฆ์ ์ ํฉํ์ง ์๋ค๋ ๊ฒ์ ์๊ฒ ๋์๋ค.
ํ์ฌ ํ๋ก์ ํธ์์ ์ ์ฉํ ์๊ณ ๋ฆฌ์ฆ์ HS256
์๊ณ ๋ฆฌ์ฆ์ด๋ค.
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
์ฌ์ฉํ๋ ์๊ณ ๋ฆฌ์ฆ์ ๋ฐ๋ผ ํค๋ฅผ ๋ค๋ฅด๊ฒ ์์ฑํด์ผ ํ๋ค. ๋ด๊ฐ ์์์ ํค๋ฅผ ์์ฑํ๊ธฐ ์ํด ์ฌ์ฉํ openssl rand -hex 64
๋ HS512(512๋นํธ) ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ ๋ ์ ํฉํ๋ค.
๋ฐ๋ผ์ ๋ค์๊ณผ ๊ฐ์ด ์กฐ๊ธ ๋ค๋ฅธ ๋ช ๋ น์ด๋ฅผ gitbash์ ์น๋ ๋ ์งง์ ํค๊ฐ ๋์๋ค.
openssl rand -base64 32
๊ฒฐ๊ณผ๋ฅผ ๋ณด๋ ํ์คํ ์์์ ์ป์ ํค์ ๋ค๋ฅด๋ค.
7yD9Da2chXAX5qCI2WrIzdiIifDZI8WPtcqJ+rjUJRA=
๐ก-hex 64
vs -base64 32
์ฐจ์ด
1) -hex 64
(HEX)
byte[] keyBytes = new BigInteger(secretKey, 16).toByteArray();
2) -base64 32
(Base64)
Base64.getDecoder().decode(secretKey)
๋ฅผ ํตํด ๋ฐ๋ก ๋ฐ์ดํธ ๋ฐฐ์ด๋ก ๋ณํ ๊ฐ๋ฅKeys.hmacShaKeyFor(Base64.getDecoder().decode(secretKey))
์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฌ์ฉ ๊ฐ๋ฅ๊ทธ๋ฐ๋ฐ, openssl rand -hex 64
๋ฅผ ์ด์ฉํด ์์ฑํ ํค๋ ์ ์์ ์ผ๋ก ๋์๊ฐ์ ์๋ฌธ์ด ๋ค์๋ค. HS256 ์๊ณ ๋ฆฌ์ฆ์ ์ ํฉํ ํค๋ openssl rand -base64 32
๋ก ์์ฑํ ํค๋ผ๊ณ ํ๋๋ฐ, ์ ์ ์์ ์ผ๋ก ์๋ํ๋ ๊ฑธ๊น? ์ด ๋ถ๋ถ์ GPT์๊ฒ ๋ฌผ์ด๋ดค๋ค.๐ค
๊ทธ ์ด์ ๋ Spring Security ๋ฐ JJWT ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์ ๋ด๋ถ์ ์ผ๋ก HEX ๋ฌธ์์ด์ ๋ฐ์ดํธ ๋ฐฐ์ด๋ก ๋ณํํ์ฌ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ด๋ผ๊ณ ํ๋ค.
JJWT์์ Keys.hmacShaKeyFor()
๋ฉ์๋๋ ๋ค์๊ณผ ๊ฐ์ด ๋์ํ๋ค. byte[] keyBytes
๋ฅผ ์
๋ ฅ ๋ฐ์ SecretKey
๊ฐ์ฒด๋ก ๋ณํํ๋๋ฐ, ์ฌ๊ธฐ์ ์
๋ ฅ๊ฐ์ด 256๋นํธ๋ณด๋ค ๊ธธ๋ฉด ์๋์ผ๋ก ์ ์ ํ ๊ธธ์ด๋ก ์๋ผ์ ์ฌ์ฉํ๋ค.
public static SecretKey hmacShaKeyFor(byte[] keyBytes) {
return new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
}
๋ฐ๋ผ์, ํค๊ฐ ๊ธธ์ด๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์๋๋ค. ํ์ง๋ง ๋ถํ์ํ๊ฒ ๊ธด ๊ฐ์ ์ฌ์ฉํ๊ธฐ๋ณด๋จ Base64
ํค๋ฅผ ์ฌ์ฉํด ๋ณํ ๊ณผ์ ์ ์์ ๋ ๊ฒ์ด ๋ ํจ์จ์ ์ผ ๊ฒ์ด๋ค.
์ฝ๋ ๊ฐ์ ๋ฌธ์ ์ค ์๋ ์ฝ๋ ๋ถ๋ถ์ ํด๋น API์ ์์ฒญ DTO์์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๊ฐ์ ํ๋ ๋ฌธ์ ๊ฐ ์์๋ค.
if (userChangePasswordRequest.getNewPassword().length() < 8 ||
!userChangePasswordRequest.getNewPassword().matches(".*\\d.*") ||
!userChangePasswordRequest.getNewPassword().matches(".*[A-Z].*")) {
throw new InvalidRequestException("์ ๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํ๊ณ , ์ซ์์ ๋๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.");
}
๊ทธ๋์ ๊ณผ์ ์์ ํ๋ ๋ด์ฉ์ด์ด์ ๋ฐ๋ก @size
์ @Pattern
์ ์ฌ์ฉํด ๋ค์๊ณผ ๊ฐ์ด ์์ฑํ๋ค. ๊ทธ๋ฐ๋ฐ @Pattern
์ ํ๋๋ก ํฉ์น๋๊ฒ ๋์ ๊ฒ ๊ฐ์๋ค. ํฉ์น์ง ์์ผ๋ฉด ์ ๋น๋ฐ๋ฒํธ๋ ์ซ์์ ๋๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.
๋ผ๋ ๋ฉ์ธ์ง๋ ์ด๋์ ๋ฃ์ด์ผ ํ ์ง ์ ๋งคํ๋ค.
@NotBlank
@Size(min = 8, message = "์ ๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.")
@Pattern(regexp = ".*\\d.*")
@Pattern(regexp = ".*[A-Z].*")
private String newPassword;
๊ทธ๋์ ์ ๊ทํํ์์ ๊ฒ์ํด ์์ ํ ๋ค์๊ณผ ๊ฐ์ด ํ ๋ฒ์ ๋ฃ๊ณ ๋ฉ์ธ์ง๋ ๊ฐ์ด ๋ฃ์๋ค.
@NotBlank
@Size(min = 8, message = "์ ๋น๋ฐ๋ฒํธ๋ 8์ ์ด์์ด์ด์ผ ํฉ๋๋ค.")
@Pattern(regexp = "^(?=.*\\d)(?=.*[A-Z]).*$", message = "์ ๋น๋ฐ๋ฒํธ๋ ์ซ์์ ๋๋ฌธ์๋ฅผ ํฌํจํด์ผ ํฉ๋๋ค.")
private String newPassword;
์ด ๋ฌธ์ ์์ ๋ด๊ฐ ๊ณ ๋ฏผํ ์ ์ @Pattern
์ ํ๋์ ์ ๊ท ํํ์์ผ๋ก ์ ๊ธฐ vs ๋ฐ๋ก ๋๋ ์ ์ ๊ธฐ ์๋ค.
์ ๊ฐ์ ์๊ฐ์ ํ๋ค๊ฐ ํํฐ๋๊ป ์ฐพ์๊ฐ ์ฌ์ญค๋ดค๋ค. ์ฐ์ ๊ฒฐ๋ก ๋ถํฐ ๋งํ์๋ฉด, ๊ฐ๋ฐ์๋ง๋ค ๋ค๋ฅด์ง๋ง ์ง๊ธ ์ํฉ(๋น๋ฐ๋ฒํธ์ ์ซ์์ ๋๋ฌธ์๊ฐ ํฌํจ๋์ด์ผ ํ๋ ์ํฉ)์ ํน์ดํ ์กฐ๊ฑด์ด ์๋๊ธฐ ๋๋ฌธ์ ํ๋๋ก ํฉ์ณ๋ ์ข์ ๊ฒ ๊ฐ๋ค๊ณ ๋ง์ํด์ฃผ์ จ๋ค.
๊ฐ์์ ์คํ์ผ์ ๋ฐ๋ผ ๋ค๋ฅด์ง๋ง, ๋ค์๊ณผ ๊ฐ์ ์ํฉ์ ์ ์ํด์ฃผ์ ์ ์ดํด๊ฐ ์ ๋๋ค.
๐ญ ํ๋์ ๋ฐฉ๋ฒ์ ์ ๋ต์ผ๋ก ๊ณ ์ํ๊ธฐ ๋ณด๋ค๋, ์ํฉ์ ๋ง์ถฐ์ ์ ์ฐํ๊ฒ ์ฝ๋ ์คํ์ผ์ ์ ์ฉํ๋ ๊ฒ๋ ๊ฐ๋ฐ์์ ์ญ๋์ด๋ผ๋ ์๊ฐ์ด ๋ค์๋ค.
ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋๋ฐ ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
์ฐพ์๋ณด๋ mockito-core ๋ฒ์ ์ด 1.x์ผ ๋ ์์๋ Strictness(ํ ์คํธ์ฝ๋์ ์๊ฒฉ์ฑ)์ ๊ท์ ํ๊ธฐ ์ํด ์๊ธด ์๋ฌ๋ผ๊ณ ํ๋ค. ๋ธ๋ก๊ทธ๋ค์ ์ญ ๋ณด๋ ํด๊ฒฐ ๋ฐฉ๋ฒ์ 3๊ฐ์ง ์ ๋๊ฐ ์์๋ค.
๋ญ๊ฐ๋ฅผ ์ถ๊ฐํ๊ฑฐ๋ ๋ฒ์ ์ ๋ด๋ฆฌ๋ ๊ฒ์ ์กฐ๊ธ ๋๋ ค์์(...) ํ์์๋ Stubbing์ ์ ๊ฑฐํ๋ ๋ฐฉ๋ฒ์ ์ ํํ๋ค. ํ์ง๋ง ์ค์ํ ์ ์ Stubbing์ด ์ ํํ ๋ฌด์์ธ์ง ๋ชจ๋ฅธ๋ค๋ ์ ์ด๋ค.๐คจ
findById(todoId)
๋ฅผ ํธ์ถํ๋ฉด ํญ์ Optional.of(todo)
๊ฐ ๋ฐํ๋๋ค. ์ฌ๊ธฐ์ todoRepository
๋ ์ค์ DB๋ฅผ ์กฐํํ๋ ๊ฒ์ด ์๋๋ผ ๋ฏธ๋ฆฌ ์ง์ ํ ๊ฐ์ ๋ฐํํ๋ ๊ฐ์ง(Mock) ๊ฐ์ฒด๋ก ๋์ํ๋ค. ์ค์ DB์ ์ ๊ทผํ์ง ์๊ณ ํ
์คํธํ๋ฉด ๋ ๋น ๋ฅด๊ณ ๋
๋ฆฝ์ ์ธ ํ
์คํธ๊ฐ ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ Stubbing์ ์ฌ์ฉํ๋ค. given(todoRepository.findById(todoId)).willReturn(Optional.of(todo));
์ด์ ๋ค์ ์๋ฌ ์ฝ๋๋ฅผ ์ดํด๋ณด๋ฉด, UnnecessaryStubbingException
, ์ฆ ํ์ํ์ง ์์ Stubbing์ด ์กด์ฌํ๋ค๋ ๋ป์ธ ๊ฒ ๊ฐ๋ค. ํ์ํ์ง ์์ Stubbing์ด๋ผ๋ ๊ฒ์ ์ค์ ๋๊ธ ๋ชฉ๋ก์ ์กฐํํ๋ CommentService
ํด๋์ค์ getComments()
๋ฉ์๋์์ ์ฌ์ฉ๋์ง ์๋ ๋ถ๋ถ์ ๋งํ๋ ๊ฒ์ด๋ค.
์๋ฌ๊ฐ ๋ ํ
์คํธ ์ฝ๋์ getComments()
๋ฉ์๋๋ฅผ ๋ณด๋ฉด ์๋น์ค ์ฝ๋์์๋ ํธ์ถํ์ง ์์ง๋ง, ํ
์คํธ ์ฝ๋ ๋ด์์๋ ํธ์ถํ๊ณ ์๋ ๋ถ๋ถ์ด ์๋ค.
@Test
void comment_๋ชฉ๋ก_์กฐํ์_์ฑ๊ณตํ๋ค() {
// given
long userId = 1L;
long todoId = 1L;
User user = new User("user1@example.com", "password", UserRole.USER);
ReflectionTestUtils.setField(user, "id", userId);
Todo todo = new Todo("Title", "Contents", "Sunny", user);
ReflectionTestUtils.setField(todo, "id", todoId);
Comment comment = new Comment("comment", user, todo);
List<Comment> commentList = List.of(comment);
given(todoRepository.findById(todoId)).willReturn(Optional.of(todo));
given(commentRepository.findByTodoIdWithUser(todoId)).willReturn(commentList);
// when
List<CommentResponse> commentResponses = commentService.getComments(todoId);
// then
assertEquals(1, commentResponses.size());
assertEquals(comment.getId(), commentResponses.get(0).getId());
assertEquals(comment.getUser().getEmail(), commentResponses.get(0).getUser().getEmail());
}
@Transactional(readOnly = true)
public List<CommentResponse> getComments(long todoId) {
List<Comment> commentList = commentRepository.findByTodoIdWithUser(todoId);
return commentList.stream()
.map(comment -> new CommentResponse(
comment.getId(),
comment.getContents(),
new UserResponse(comment.getUser().getId(), comment.getUser().getEmail())
))
.collect(Collectors.toList());
}
๋ฐ๋ก ttodoRepository.findById(todoId)
๋ฅผ ํธ์ถํ๊ณ ์๋ ๋ถ๋ถ์ด๋ค. ์ค์ ์๋น์ค ๋ก์ง์์๋ ํธ์ถํ๊ณ ์์ง ์๋ค. ๋ฐ๋ผ์ ๋ค์ given()
์ ์ ๊ฑฐํ๋ค.
given(todoRepository.findById(todoId)).willReturn(Optional.of(todo)); // ์ ๊ฑฐ
๊ทธ๋ฆฌ๊ณ ๋์ ๋ค์ ์คํํ๋ ํ
์คํธ ์ฝ๋๊ฐ ์ ์๋ํ๋ ๊ฒ์ ๋ณผ ์ ์์๋ค. ํ์ํ ๊ฒ ๊ฐ๋ค๊ณ ๋ฌด์์ ๋์ด์ฐ๋ค๊ฐ ์ด๋ฐ ์๋ฌ๋ฅผ ๋ง์ฃผํ๋ ์๋น์ค ๋ก์ง์ ์ ์ดํด๋ณด๊ณ ํ
์คํธ ์ฝ๋๋ฅผ ์ง์ผํ๋ค๋ ๊ฒ์ ๊นจ๋ฌ์๋ค.
๋ค์ getTodos()
๋ฉ์๋์์ ๋ฐ์ํ๊ณ ์๋ N+1 ๋ฌธ์ ๋ฅผ @EntityGraph
๋ฅผ ์ฌ์ฉํด ํด๊ฒฐํด์ผ ํ๋ค.
public Page<TodoResponse> getTodos(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
์ ์ฝ๋์์ ํ ์ผ์ ๋ชจ๋ ๋ถ๋ฌ์จ ํ, ๊ทธ ์๋ ์ ์ ๋ฅผ ๋ ๋ค์ ๋ถ๋ฌ์ค๋ ๊ณผ์ ์์ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊ฐ๋ฅ์ฑ์ด ๋์๋ณด์ธ๋ค.
Todo
์ํฐํฐ์ ์ผ๋ถ๋ฅผ ์ดํด๋ณด๋ฉด User
์ @ManyToOne
๋ค๋์ผ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋งบ๊ณ ์๊ณ , ์ง์ฐ ๋ก๋ฉ(FetchType.LAZY
)์ผ๋ก ์ค์ ๋์ด ์๋ค. ์ง์ฐ ๋ก๋ฉ์ผ๋ก ์ค์ ํ๋ฉด Todo
๋ฅผ ์กฐํํ ๋ User
๋ฅผ ๋ฐ๋ก ๊ฐ์ ธ์ค๋ ๊ฒ์ด ์๋๋ผ ํ๋ก์ ๊ฐ์ฒด๋ก ์๋ค๊ฐ ์ค์ ์ฌ์ฉ๋ ๋ SELECT ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ ๊ฒ์ด๋ค.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
๋จผ์ findAllByOrderByModifiedAtDesc()
๋ฉ์๋๋ฅผ ํตํด ๋ค์๊ณผ ๊ฐ์ด ๋ชจ๋ todo๋ฅผ ๋ถ๋ฌ์ค๊ณ ์๋ค.
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
์ด๋ ๋ค์๊ณผ ๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๊ฒ์ด๋ค.
SELECT * FROM todo ORDER BY modified_at DESC LIMIT ?, ?
๊ทธ๋ฆฌ๊ณ ๋ค์ ์ฝ๋์์ todo.getUser()
๋ฅผ ํ๋ฉด User
์ ๋ํ SELECT ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๋ค. ์ฆ todo๊ฐ 100๊ฐ๋ผ๋ฉด ์์์ 1๊ฐ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๊ณ , ์๋์์ 100๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ ๋ฐ์ํ๊ฒ ๋๋ ๊ฒ์ด๋ค.
todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()), // N+1 ๋ฌธ์ ๋ฐ์ ๊ฐ๋ฅ
todo.getCreatedAt(),
todo.getModifiedAt()
));
Repository์์๋ ์ด๋ฏธ ๋ค์๊ณผ ๊ฐ์ด fetch join
์ ์ฌ์ฉํด N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ค.
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
์ฌ๊ธฐ์ @EntityGraph
๋ผ๋ ์ด๋
ธํ
์ด์
์ ํ์ฉํด ํด๊ฒฐํด๋ณด์. @EntityGraph(attributePaths = {"user"})
์ ๊ฐ์ด ์ ์ด์ฃผ๋ฉด User
๋ ํจ๊ป ์กฐํํ๋๋ก ์ค์ ํ ์ ์๋ค.
@EntityGraph(attributePaths = {"user"})
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
ํ์๋ถ๊ป์ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ์ง ์์์ ๋์ ๊ฒฐ๊ณผ์ ํด๊ฒฐํ์ ๋์ ๊ฒฐ๊ณผ๋ฅผ ์ง์ ๋ณด์ฌ์ฃผ์ จ๋ค. ๊ทธ๋์ ๋๋ ๋ฐ๋ผ์ ํ ์คํธ ํด๋ณด๋ ํจ์ฌ ์ดํด๊ฐ ์๋๋ค. ๋ฏธ๋ฆฌ ์ ์ 2๋ช ์ ์์ฑํ๊ณ , ์ผ์ ๋ ๊ฐ ์ ์ ๋ง๋ค 1๊ฐ์ฉ ๋ง๋ค์ด ํ ์คํธ ํ๋๋ ๋ค์๊ณผ ๊ฐ์ ๊ฒฐ๊ณผ๊ฐ ๋์๋ค.
@EntityGraph ์ ์ฉ ์ | @EntityGraph ์ ์ฉ ํ |
---|---|
![]() | ![]() |
N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ ์๋ ์ผ์ ์ ์กฐํํ๊ณ , ๊ฐ ์ ์ ๋ง๋ค ํ ๋ฒ์ฉ ๋ ์กฐํํ๊ณ ์๋ค. ์ ์ ์ ์(2๋ช )๋งํผ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๊ฒ์ด๋ค. N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ํ์๋ ์ ์ ๋ฅผ ์ถ๊ฐ๋ก ์กฐํํ๋ ๊ฒ ์์ด ์ฟผ๋ฆฌ๊ฐ ํ ๋ฒ๋ง ์คํ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค.