0. ๋ค์ด๊ฐ๋ฉฐ
DB์์ ํต์ ๊ณผ์ ์์ ๋ถํ๋ฅผ ์ค์ด๋ ๋ฐฉ๋ฒ์ผ๋ก ์ธ๋ฑ์ค, ์ง์ฐ๋ก๋ฉ, ์บ์ฑ ๋ฑ ์ฌ๋ฌ ๋ฐฉ๋ฒ์ ํ์ฉํด๋ณผ ์ ์๋ค.
์ค๋์ ๊ธฐ์กด์ ์ ์๊ฐํด๋ณด์ง ๋ชปํ๋ ์ํฐํฐ ์กฐํ ๋ฐฉ๋ฒ์ ๋งํด๋ณด๋ ค๊ณ ํ๋ค.
ํ์์ JPA๋ฅผ ์ด์ฉํด์ ์ํฐํฐ๋ฅผ ์กฐํํ๊ณ ํด๋ผ์ด์ธํธ ์์ฒญ์ ์๋ตํ ๋, ๋ณดํต์ ๊ฒฝ์ฐ์๋ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ์๋ค.
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById(Long id);
}
@Builder
@Getter
public class UserResponseDto {
Long id;
String email;
String name;
public static UserResponseDto from(User user) {
return UserResponseDto.builder()
.id(user.getId())
.name(user.getName())
.email(user.getEmail())
.build();
}
}
ํ์ํ Entity๋ฅผ ๋จผ์ ์ ์ฒด๋ก ์กฐํํ๊ณ , ํ์ํ DTO๋ก ๋ณํํ๋ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ๋ค.
public UserResponseDto getUser(Long id) {
User user = userRepository.findById(id).orElseThrow();
return UserResponseDto.from(user);
}
๋ฌด๋ถ๋ณํ Entity ์ ์ฒด ์กฐํ๋ ๊ณง ์ฑ๋ฅ์ ํ๋ก ์ด์ด์ง ์ ์๋ค.
์ค์ ๋ก ์ธํ๋ฐ์์ ๋น์ทํ ๋ฐฉ์์ ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ๋ก ์ธํด ์๋น์ค ์ฅ์ ๊ฐ ์ด์ด์ง ์ ์ด ์์๋ค.
์ค์ ๋ก ์ฌ์ฉํ์ง ์๋ ๋์ฉ๋ ์ปฌ๋ผ์ ์ ๊ฑฐํ๋ฉด์ ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ๋ฅผ ํด๊ฒฐํ๋ค.
1. ์ ์ฒด ์ปฌ๋ผ ์กฐํ vs ํน์ ์ปฌ๋ผ ์กฐํ
1-1. ๋น์ฉ ๊ณ์ฐ
๋ง์ฝ Post ํ ์ด๋ธ์ด ์ฌ๋ฌ ์ปฌ๋ผ์ ๊ฐ๊ณ ์๋ค๊ณ ๊ฐ์ ํ ๋,
SELECT * FROM post WHERE id >=1 AND id <=100;
SELECT title, summary, created_at FROM post WHERE id >=1 AND id <=100;
๋ ์ฟผ๋ฆฌ์ค ์ด๋ค ์ฟผ๋ฆฌ๊ฐ ๋น์ฉ์ด ์ ๊ฒ ๋ค๊น?
์ฟผ๋ฆฌ์ ๋น์ฉ์ ๊ณ์ฐํด์ฃผ๋ MySQL explain์ ์ฌ์ฉํด๋ณด๋ฉด,
์ ์ฒด ์ปฌ๋ผ ์กฐํ
ํน์ ์ปฌ๋ผ ์กฐํ
๋๋๊ฒ๋ ๋ ์ฟผ๋ฆฌ์ ์ด ๋น์ฉ(cost)์ 10.3์ผ๋ก ๊ฐ๋ค.
๊ทธ ์ด์ ๋ MySQL ๋ฐ์ดํฐ ์ ์ฅ ๋ฐฉ์์ ์๋๋ฐ,
MySQL์ ๋ฐ์ดํฐ๋ฅผ ํ์ด์ง ๋จ์๋ก ์ ์ฅํ๋ค.
๋ฐ๋ผ์ ํน์ ํ์ ์ฝ์ ๋๋, ๋ช ๊ฐ์ ์ปฌ๋ผ์ ์ฝ๋ ์๊ด์์ด ํ์ด์ง ์ ์ฒด๋ฅผ ๋ฉ๋ชจ๋ฆฌ๋ก ๊ฐ์ ธ์์ผ ํ๋ค.
MySQL Explain์ ๋น์ฉ ๊ณ์ฐ ์์ ๋ค์๊ณผ ๊ฐ๋ค.
cost = (๋ฐ์ดํฐ ํ์ด์ง ์ × ํ์ด์ง ์ฝ๊ธฐ ๋น์ฉ) + (ํ ์ × ํ ์ฒ๋ฆฌ ๋น์ฉ)
๋ ์ฟผ๋ฆฌ ๋ชจ๋ ๊ฐ์ ์์ ํ์ด์ง๋ฅผ ์ฝ๊ธฐ ๋๋ฌธ์, ๊ฒฐ๊ตญ ๋น์ฉ ๊ณ์ฐ ๊ฒฐ๊ณผ๊ฐ ๊ฐ๊ฒ ๋์จ๋ค.
๊ทธ๋ ๋ค๋ฉด ๋น์ฉ์ด ๊ฐ์ผ๋๊น, DB ์ ์ฅ์์ ์ฐจ์ด๊ฐ ์๋๊ฒ ์๋๊ฐ?
1-2. ์คํ ์๊ฐ
๋น๋ก ๋น์ฉ์ ๊ฐ์ ์ ์์ง๋ง, ์ค์ ์คํ ์๊ฐ์ ๋ค๋ฅผ ์ ์๋ค.
์ ์ฒด ์กฐํ๋ ํน์ ์ปฌ๋ผ ์กฐํ๋ณด๋ค ํด๋ผ์ด์ธํธ์๊ฒ ๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํด์ผ ํ๋ฏ๋ก, ๋คํธ์ํฌ ๋์ญํญ์ ๋ ๋ง์ด ์ฌ์ฉํ๊ฒ ๋๋ค.
์ค์ ๋ฐ์ดํฐ ์ ์ก๋์ ํ๋กํ์ผ๋ง์ ์ด์ฉํด์ ๋น๊ตํด ๋ณด๊ฒ ๋ค.
# ํ๋กํ์ผ๋ง ํ์ฑํ
SET profiling = 1;
# ์ ์ฒด ์ปฌ๋ผ ์กฐํ
SELECT * FROM post WHERE id >=1 AND id <=100;
# ํน์ ์ปฌ๋ผ๋ง ์กฐํ
SELECT title, summary, created_at FROM post WHERE id >=1 AND id <=100;
# ํ๋กํ์ผ๋ง ๊ฒฐ๊ณผ ํ์ธ
SHOW PROFILES;
์ ์ฒด ์ปฌ๋ผ ์กฐํ
ํน์ ์ปฌ๋ผ ์กฐํ
์ ์ฒด ์ปฌ๋ผ : 0.001626
ํน์ ์ปฌ๋ผ : 0.000568
๋ฏธ๋ฌํ์ง๋ง ๋ถ๋ช ํ ์ฐจ์ด๊ฐ ์๊ณ , ์ด๋ ๋ฐํ ์ปฌ๋ผ์ด ๋ง์์ง์๋ก ์ฐจ์ด๊ฐ ๋ถ๋ช ํด์ง ๊ฒ์ด๋ค.
๋ํ ๋คํธ์ํฌ ์ฌ์ฉ๋ ์ด์ธ์๋, ๋ฐ์ดํฐ๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋์ด์์ (ORM์ ์ฌ์ฉํ ๊ฒฝ์ฐ) ๊ฐ์ฒด์ ๋งคํํ๋ ์๊ฐ์ด ์ฆ๊ฐํ๊ณ ,
๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ์ฒ๋ฆฌํด์ผ ํ๋ฏ๋ก ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋์ด ์ฆ๊ฐํ๋ค.
2. JPA์ DTO ๋ฐํ
๊ทธ๋ ๋ค๋ฉด, JPA์์ ์ด๋ป๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ํ๋ DTO๋ก ๋ฐํ๋ฐ์ ์ ์์๊น?
๋ค์๊ณผ ๊ฐ์ Post ์ํฐํฐ๋ฅผ ๊ฐ์ง๊ณ ์๋ค๊ณ ๊ฐ์ ํด ๋ณด๊ฒ ๋ค.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "post")
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Column(length = 10000)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column(length = 1000)
private String summary;
private LocalDateTime createdAt;
@Column(length = 500)
private String tags;
private int viewCount;
@Column(length = 2000)
private String metaDescription;
private String category;
private boolean isPublished;
private LocalDateTime lastModifiedAt;
private String thumbnailUrl;
}
์๋ฅผ ๋ค์ด, ๊ฒ์๊ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์์ ๋ชจ๋ ์ ๋ณด๊ฐ ํ์ํ์ง ์๋ค.
๋๋ ์ฌ๊ธฐ์ ๊ฒ์๊ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์๋์ ๊ฐ์ด ์ถ์ถํ๋ ค๊ณ ํ๋ค.
@Getter
@AllArgsConstructor
public class PostResponseDto {
public Long id;
public String title;
public String summary;
}
๊ฐ์ฅ ์ผ์ฐจ์ ์ธ ๋ฐฉ๋ฒ์ผ๋ก ์ง์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํด์ ๋ค์๊ณผ ๊ฐ์ด ์ฌ์ฉํ ์ ์๊ฒ ๋ค.
public interface PostRepository extends JpaRepository<Post, Long> {
@Query("SELECT new com.example.testproject.PostResponseDto(p.id, p.title, p.summary) FROM Post p")
List<PostResponseDto> findAllBy();
}
public List<PostResponseDto> getPosts() {
return postRepository.findAllBy();
}
์ด๋ ๊ฒ ๊ตฌ์ฑํด๋๋ฉด ํ์ํ ์ปฌ๋ผ๋ง ์กฐํํ๋ ์ฟผ๋ฆฌ๊ฐ ์คํ๋๊ฒ ๋๋ค.
๊ธฐ์กด ์คํ ์ฟผ๋ฆฌ
๊ฐ์ ํ ์ฟผ๋ฆฌ
ํ์ง๋ง ์ฌ๊ธฐ์ ๋ ๊ฐํธํ ๋ฐฉ๋ฒ์ด ์๋๋ฐ, ๋ฐ๋ก JPA์ ๋ณํ ๊ธฐ๋ฅ์ ํ์ฉํ๋ ๊ฒ์ด๋ค.
์ฌ๊ธฐ์ Repository ๋ฉ์๋๋ง ๋ค์๊ณผ ๊ฐ์ด ๋ฐ๊พธ๊ณ ์คํํด๋ณด๊ฒ ๋ค.
public interface PostRepository extends JpaRepository<Post, Long> {
List<PostResponseDto> findAllBy();
}
์คํ๋ ์ฟผ๋ฆฌ
ํ์ํ ์ปฌ๋ผ์ ๋ช ์ํด์ฃผ์ง ์์๋๋ฐ๋, ์ ๊ณผ ๊ฐ์ด ํ์ํ ์ปฌ๋ผ๋ง ๊ณจ๋ผ์ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
์ด๋ JPA ๋ด๋ถ์์ ๋ฆฌํด ํ์ (ํด๋์ค)์ ๋ถ์ํ๊ณ ๊ฒฐ๊ณผ์ ์ผ๋ก ์์์ ์ง์ ์์ฑํ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํด์ฃผ๊ธฐ ๋๋ฌธ์ด๋ค.
๊ฐ์ ๋ฐฉ์์ผ๋ก ๋จ๊ฑด ์กฐํ๋ ์ฌ์ฉํ ์ ์๋ค.
public interface UserRepository extends JpaRepository<User, Long> {
UserResponseDto findDtoById(Long id);
}
์ฃผ์์ฌํญ
1. ์ด๋ฅผ ์ํด์ ๋ฐ๋์ ์์ฑ์๋ฅผ ๋ช ์ํด ๋์ด์ผ ํ๋ค.(@AllArgsConstructor)
๊ทธ๋ ์ง ์์ผ๋ฉด JPA์์ ๋ค์๊ณผ ๊ฐ์ ์์ธ๋ฅผ ๋์ง ๊ฒ์ด๋ค.
org.hibernate.query.QueryTypeMismatchException: Specified result type [com.example.testproject.UserResponseDto] did not match Query selection type [com.example.testproject.User] - multiple selections: use Tuple or array
2. DTO์ ํ๋๋ช ๊ณผ ์ปฌ๋ผ๋ช ์ด ์ผ์นํด์ผ ํ๋ค.
3. JPA๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ๋ฉ์๋๋ค์ ๋ฆฌํด ํ์ ์ ์ํฐํฐ์ด๊ธฐ ๋๋ฌธ์,
๊ธฐ๋ณธ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ๋ค๋ฅธ ์ด๋ฆ์ ์ฌ์ฉํด์ผ ์ถฉ๋์ ํผํ ์ ์๋ค.
3. ์๋๋ฐฉ์
๊ทธ๋ ๋ค๋ฉด, DTO๋ก ๋ฐํ๋๋ ๊ณผ์ ์ด ์ด๋ป๊ฒ ์ด๋ฃจ์ด์ง๋๊ฑธ๊น?
Spring Data JPA ๋ฒ์ 3.2.4 ๊ธฐ์ค
3-1. ์ฟผ๋ฆฌ ์์ฑ
๋จผ์ PostRepository์ ๋ฉ์๋๊ฐ ํธ์ถ๋๋ฉด, Spring Data JPA์ QueryExecutorMethodInterceptor ํด๋์ค๊ฐ ์ด๋ฅผ ๊ฐ๋ก์ฑ๋ค.
ํด๋น ํด๋์ค๋ ๋ฉ์๋ ํธ์ถ์ ๋ถ์ํ๊ณ ์ ์ ํ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๋ ์ญํ ์ ํ๋ค.
org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor
์ด ๊ณผ์ ์์ JpaQueryMethod๋ฅผ ํตํด ๋ฉ์๋ ๋ฐํ ํ์ ์ ์ฌ์ฉํ๋ค.
org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery
protected Query createJpaQuery(String queryString, Sort sort, @Nullable Pageable pageable,
ReturnedType returnedType) {
EntityManager em = getEntityManager();
//์์ฑ์๊ฐ ์๋์ง ๊ฒ์ฌ
if (this.query.hasConstructorExpression() || this.query.isDefaultProjection()) {
return em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable));
}
//์ฝ์ด์ผ ํ๋ ํด๋์ค(return type)์ ๊ฐ์ ธ์ด
Class<?> typeToRead = getTypeToRead(returnedType);
return typeToRead == null
? em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable))
: em.createQuery(potentiallyRewriteQuery(queryString, sort, pageable), typeToRead); //->return type๊ณผ ๊ฐ์ด ์ฟผ๋ฆฌ๋ฅผ ์์ฑ
}
typeToRead(๋ฆฌํด ํ์ )์ ๊ฐ์ ธ์ค๊ณ , EntityManager์ ๊ฐ์ด ๋ฐํ ํ์ ์ ๋๊ธฐ๋ฉฐ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ๊ฒ ๋๋ค.
3-2. ์คํ ํ ๋ณํ
JpaQueryExecution๋ด์์ ์ฟผ๋ฆฌ๊ฐ ์คํ๋ ๋ค์, requiredType๊ณผ ๋น๊ตํ์ฌ ๋ณํ์ ์๋ํ๋ค.
org.springframework.data.jpa.repository.query.JpaQueryExecution
@Nullable
public Object execute(AbstractJpaQuery query, JpaParametersParameterAccessor accessor) {
//...
//์ฟผ๋ฆฌ ์คํ
try {
result = doExecute(query, accessor);
} catch (NoResultException e) {
return null;
}
//...
//ConversionService๋ฅผ ํตํด ๋ณํ์ ์๋
return CONVERSION_SERVICE.canConvert(result.getClass(), requiredType) //
? CONVERSION_SERVICE.convert(result, requiredType) //
: result;
}
์ค๋์ MySQL์ ์ ์ฅ ๋ฐฉ์๊ณผ ํจ๊ป JPA์ ์ด์ฉํ์ฌ DTO๋ก ๋ฐํํ๋ ๋ฐฉ๋ฒ์ ์์๋ณด์๋ค.
์์์ ์๊ฐํ๋ ์ธํ๋ฐ์ ์ฅ์ ์ด์๋ ๊ทธ๋ฌ๋ฏ, ์ฌ๋ฌ ์์ ์ฑ๋ฅ ์ด์๋ค์ด ๋ชจ์ฌ์ ์ฅ์ ๋ฅผ ์ผ์ผํฌ ์ ์๋ค๋ ๊ฒ์ ๋ค์ ํ ๋ฒ ๋๋ผ๊ฒ ๋์๋ค.
์์ ์์๋ค๋ ๋์น์ง ๋ง๊ณ ์ด๋ค ์ด์๊ฐ ์์ ์ ์๋์ง ๋ณด๋ ๋ฅ๋ ฅ์ ๊ธธ๋ฌ๋ณด์!!