1๏ธโฃ JPA (Java Persistence API)
๊ฐ์
JPA๋ ์๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ์์ ORM (Object-Relational Mapping)์ ๊ตฌํํ๊ธฐ ์ํ ํ์ค API๋ก, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฐ์ฒด ๊ฐ์ ๋งคํ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. JPA๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ์ํํ ์ ์์ผ๋ฉฐ, SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉํ ์ ์์ต๋๋ค. ์ด์ ํจ๊ป `Hibernate`๋ JPA์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๊ตฌํ์ฒด๋ก, JPA์ ๊ธฐ๋ฅ์ ๋์ฑ ๊ฐํํ๊ณ ๋ค์ํ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธ์ํฐํฐ ๋งคํ(Entity Mapping)
JPA๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ ์ด๋ธ์ ์๋ฐ ํด๋์ค(์ํฐํฐ)์ ๋งคํํฉ๋๋ค. ์๋ฅผ ๋ค์ด, `@Entity` ์ ๋ํ ์ด์ ์ ์ฌ์ฉํ์ฌ ํด๋์ค๋ฅผ ์ํฐํฐ๋ก ์ ์ธํ๊ณ , `@Id`, `@Column` ๋ฑ์ ์ ๋ํ ์ด์ ์ผ๋ก ๊ฐ ํ๋๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปฌ๋ผ์ ๋งคํํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ์ฒด์งํฅ์ ์ธ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
}
โ๏ธ์ฟผ๋ฆฌ ๋ฉ์๋ (Query Method)
JPA๋ ๋ฉ์๋ ์ด๋ฆ์ ๊ธฐ๋ฐํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์์ฑํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์กฐํํ ์ ์์ต๋๋ค.
* ๊ณต์๋ฌธ์ ์ฐธ์กฐ
public interface UserRepository extends JpaRepository<User, Long> {
// Spring Data JPA๊ฐ ์ ๊ณตํ๋ ์ฟผ๋ฆฌ ๋ฉ์๋ ๊ธฐ๋ฅ์ ์ด์ฉํ์ฌ, ๋ฉ์๋ ์ด๋ฆ์ ๊ธฐ๋ฐํ์ฌ ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑ
List<User> findByName(String name);
}
โ๏ธ์ฐ๊ด ๊ด๊ณ ๋งคํ
JPA๋ ์ํฐํฐ ๊ฐ์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ์ค์ ํ ์ ์๋ ๋ค์ํ ์ ๋ํ ์ด์ ์ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด, `@ManyToOne`, `@OneToMany`, ๋ฑ์ ์ฌ์ฉํ์ฌ ์ํฐํฐ ๊ฐ์ ๋ค๋์ผ, ์ผ๋๋ค, ๋ค๋๋ค ๊ด๊ณ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค. ์ด๋ฌํ ๊ด๊ณ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ธ๋ ํค์ ์ผ์นํ๋ฉฐ, ๊ฐ์ฒด ๊ฐ์ ์ฐธ์กฐ ๊ด๊ณ๋ก ํํ๋ฉ๋๋ค. JPA์์๋ ์ด๋ฌํ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก `JOIN` ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ด๋ จ๋ ์ํฐํฐ๋ฅผ ํจ๊ป ์กฐํํ ์ ์์ต๋๋ค.
@Entity
public class User {
@OneToMany(mappedBy = "user") // ์ฌ์ฉ์(User)์์ ์ผ๋๋ค ๊ด๊ณ๋ฅผ ์ ์ํ๋ฉฐ, 'user' ํ๋๋ก ๋งคํโจ
private List<Post> posts; // ์ฌ์ฉ์๊ฐ ์์ฑํ ๊ฒ์๋ฌผ ๋ชฉ๋ก
}
@Entity
public class Post {
@ManyToOne // ๊ฒ์๋ฌผ(Post)์์ ๋ค๋์ผ ๊ด๊ณ๋ฅผ ์ ์ํ๋ฉฐ, ๊ฒ์๋ฌผ์ด ์ํ ์ฌ์ฉ์(User)๋ฅผ ์ฐธ์กฐโจ
private User user; // ๊ฒ์๋ฌผ์ด ์ํ ์ฌ์ฉ์
}
โ๏ธํจ์น ์ ๋ต(Fetch Strategy)
JPA์์๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ๋ก๋ํ ๋ `Eager`์ `Lazy` ๋ ๊ฐ์ง ํจ์น ์ ๋ต์ ์ ํํ ์ ์์ต๋๋ค. Eager ํจ์น๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์ฆ์ ๋ก๋ํ๊ณ , Lazy ํจ์น๋ ์ค์ ๋ก ํด๋น ๋ฐ์ดํฐ๊ฐ ํ์ํ ๋ ๋ก๋ํฉ๋๋ค.
@Entity
public class User {
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
// FetchType ๊ธฐ๋ณธ๊ฐ: Lazy - ์ฌ์ฉ์๊ฐ ์กฐํ๋ ๋ ๊ด๋ จ๋ ๊ฒ์๋ฌผ ๋ชฉ๋ก์ด ํ์ํ ๋๋ง ๋ก๋๋จโจ
private List<Post> posts;
}
@Entity
public class Post {
@ManyToOne(fetch = FetchType.EAGER)
// FetchType ๊ธฐ๋ณธ๊ฐ: Eager - ๊ฒ์๋ฌผ์ด ์กฐํ๋ ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ฆ์ ๋ก๋๋จโจ
private User user;
}
โ๏ธCustomRepository ๊ตฌํ
JPA๋ฅผ ์ฌ์ฉํ๋ค ๋ณด๋ฉด ๊ธฐ๋ณธ ๊ธฐ๋ฅ ์ธ์ ์ถ๊ฐ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ๋ณต์กํ ์ฟผ๋ฆฌ๊ฐ ํ์ํ ๋๊ฐ ์์ต๋๋ค. ์ด๋ด ๋ `CustomRepository`๋ฅผ ํตํด ์ปค์คํ ๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ๋ง๋ค์ด ํ์ํ ๊ธฐ๋ฅ์ ํ์ฅํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ธฐ๋ณธ CRUD ๊ธฐ๋ฅ ์ธ์ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ํน์ ์๊ตฌ์ฌํญ์ ๋ง๋ ๋ฐ์ดํฐ ์กฐํ ๋ฐ ์ฒ๋ฆฌ ๋ก์ง์ ๊ตฌํํ ์ ์์ผ๋ฉฐ, ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.
โ๏ธEntityManager
JPA์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ๊ด๋ฆฌํ๋ ํต์ฌ ์ธํฐํ์ด์ค๊ฐ `EntityManager`์ ๋๋ค. ์ด ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ํฐํฐ์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ๊ณ , JPQL ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ฉฐ, ํธ๋์ญ์ ์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
2๏ธโฃ JPQL (Java Persistence Query Language)
๊ฐ์
JPQL์ JPA์์ ์ ๊ณตํ๋ ๊ฐ์ฒด ์งํฅ ์ฟผ๋ฆฌ ์ธ์ด๋ก, SQL๊ณผ ์ ์ฌํ์ง๋ง ํ ์ด๋ธ ๋์ ์ํฐํฐ ํด๋์ค์ ๊ทธ ์์ฑ์ ๋์์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํฉ๋๋ค.
JPQL ์ฟผ๋ฆฌ ์์ฑ ๋ฐฉ๋ฒ
โ๏ธ์ ๋ ธํ ์ด์ ๋ฐฉ์
์ด ๋ฐฉ๋ฒ์ `@Query` ์ ๋ ธํ ์ด์ ์ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ๋ฉ์๋ ์์ ์ ์ํ๋ ๋ฐฉ์์ ๋๋ค. Spring Data JPA์์ ์ฃผ๋ก ์ฌ์ฉ๋๋ฉฐ, ๋ฉ์๋์ ์ ์ธ๋ถ์ ์ฟผ๋ฆฌ๋ฅผ ๋ช ์ํจ์ผ๋ก์จ, ๋ณด๋ค ๊ฐ๊ฒฐํ๊ณ ์ง๊ด์ ์ธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
public interface PostRepository extends CrudRepository<Post, Long> {
// @Query ์ ๋ํ
์ด์
์ ์ฌ์ฉํ์ฌ ๋ฉ์๋์ ์ง์ JPQL ์ฟผ๋ฆฌ ์ ์โจ
@Query("SELECT p FROM Post p WHERE p.user.id = :userId")
List<Post> findByUserId(@Param("userId") Long userId);
}
โ๏ธEntityManager ๋ฐฉ์
์ด ๋ฐฉ๋ฒ์ `EntityManager`๋ฅผ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ๋์ ์ผ๋ก ์์ฑํ๊ณ ์คํํ๋ ๋ฐฉ์์ ๋๋ค. ๋ณด๋ค ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ํ์ํ ๋ ์ ์ฉํฉ๋๋ค. JPQL ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์์ด๋ก ์ ์ํ๊ณ , `createQuery()` ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์คํํฉ๋๋ค.
public class PostService {
// JPA์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ๊ด๋ฆฌํ๋ EntityManagerโจ
@PersistenceContext
private EntityManager entityManager;
public void updatePostContent(Long postId, String newContent) {
// ๊ฒ์๋ฌผ ID์ ๋ฐ๋ผ ๋ด์ฉ์ ์
๋ฐ์ดํธํ๋ JPQL ์ฟผ๋ฆฌ
String sql = "UPDATE Post p SET p.content = :content WHERE p.id = :postId";
Query query = entityManager.createQuery(sql); // EntityManager๋ฅผ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ ์์ฑโจ
query.setParameter("content", newContent);
query.setParameter("postId", postId);
int result = query.executeUpdate(); // ์ฟผ๋ฆฌ ์คํ
}
}
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธFetch ์กฐ์ธ
Fetch ์กฐ์ธ์ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ํจ๊ป ๋ก๋ํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐ ๋์์ ์ค๋๋ค. ์ด ๋ฐฉ๋ฒ์ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๊ฐ์ ธ์ฌ ์ ์๋๋ก ํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค. ๊ทธ๋ฌ๋ Fetch ์กฐ์ธ์ ์ฌ์ฉํ ๋, ์ค๋ณต๋ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก `DISTINCT` ํค์๋๋ฅผ ์ถ๊ฐํ์ฌ ์ค๋ณต๋ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๊ฒฐ๊ณผ ๋ฆฌ์คํธ์์ ๊ฐ์ ์ํฐํฐ๊ฐ ์ฌ๋ฌ ๋ฒ ๋ํ๋๋ ๊ฒ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
public class PostRepositoryCustomImpl implements PostRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
public List<Post> fetchPostsWithUsers() {
// Fetch ์กฐ์ธ์ ์ฌ์ฉํ์ฌ Post์ User๋ฅผ ํจ๊ป ๋ก๋ํ๊ณ ์ค๋ณต๋ ๊ฒ์๋ฌผ ์ ๊ฑฐโจ
String jpql = "SELECT DISTINCT p FROM Post p JOIN FETCH p.user";
List<Post> posts = entityManager.createQuery(jpql, Post.class)
.getResultList();
return posts; // ์ฐ๊ด๋ User ๋ฐ์ดํฐ์ ํจ๊ป ๋ก๋๋ Post ๋ฆฌ์คํธ ๋ฐํ
}
}
โ๏ธ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ
`JPQL` ๋์ SQL์ ์ง์ ์ฌ์ฉํ๊ณ ์ถ์ ๋ ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ SQL ๋ฌธ๋ฒ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ผ๋ก, ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํน์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์์ต๋๋ค.
public interface PostRepository extends CrudRepository<Post, Long> {
// @Query ์ ๋ํ
์ด์
์ ์ฌ์ฉํ์ฌ ๋ฉ์๋์ ์ง์ ๋ค์ดํฐ๋ธ SQL ์ฟผ๋ฆฌ ์ ์โจ
@Query(value = "SELECT * FROM post WHERE user_id = :userId", nativeQuery = true)
List<Post> findByUserId(@Param("userId") Long userId);
}
3๏ธโฃ QueryDSL (Query Domain-Specific Language)
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธํ์ ์์ ์ฑ
QueryDSL์ ์ฟผ๋ฆฌ ๋ฌธ๋ฒ์ ์๋ฐ ์ฝ๋๋ก ํํํ๋ฏ๋ก, ์ปดํ์ผ ํ์์ ์ฟผ๋ฆฌ์ ์ค๋ฅ๋ฅผ ๋ฏธ๋ฆฌ ํ์ธํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ฐํ์ ์ค๋ฅ๋ฅผ ์ค์ด๊ณ , ์ฝ๋์ ์์ ์ฑ์ ๋์ ๋๋ค.
โ๏ธ๋์ ์ฟผ๋ฆฌ ์์ฑ
๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ์ฌ์ฉ์ ์๊ตฌ ์ฌํญ์ ๋ฐ๋ผ ์กฐ๊ฑด์ ๋์ ์ผ๋ก ์ถ๊ฐํ๊ฑฐ๋ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค. `BooleanBuilder` ์ ๊ฐ์ ๊ธฐ๋ฅ์ ํ์ฉํ๋ฉด ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฝ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
BooleanBuilder builder = new BooleanBuilder();
if (name != null) {
builder.and(user.name.eq(name)); // name ์กฐ๊ฑด ์ถ๊ฐโจ
}
if (otherCondition != null) {
builder.or(user.status.eq(otherCondition)); // ๋ค๋ฅธ ์กฐ๊ฑด์ OR ์ฌ์ฉโจ
}
// ์ต์ข
์ฟผ๋ฆฌ ์คํ
List<User> users = queryFactory.selectFrom(user)
.where(builder) // ๋์ ์ผ๋ก ์์ฑ๋ ์กฐ๊ฑด์ ์ฌ์ฉโจ
.fetch();
โ๏ธ์กฐ์ธ (JOIN)
QueryDSL์์๋ ์กฐ์ธ ์ฐ์ฐ์ ํตํด ์ฌ๋ฌ ์ํฐํฐ๋ฅผ ์ฐ๊ด ์ง์ด ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ์์ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์กฐํํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
QPost post = QPost.post;
QUser user = QUser.user;
// INNER JOIN ์์: ์์ฑ์๊ฐ John์ธ ํฌ์คํธ ์กฐํ
List<Post> innerJoinPosts = queryFactory
.selectFrom(post)
.join(post.user, user) // INNER JOINโจ
.where(user.name.eq("John"))
.fetch();
// LEFT JOIN ์์: ๋ชจ๋ ํฌ์คํธ์ ๊ทธ ์์ฑ์๋ฅผ ์กฐํํ๋, ์์ฑ์๊ฐ ์๋ ํฌ์คํธ๋ ํฌํจ
List<Post> leftJoinPosts = queryFactory
.selectFrom(post)
.leftJoin(post.user, user) // LEFT JOINโจ
.fetch();
// ON ์ ์ ๋ช
์ํ๋ LEFT JOIN ์์
List<Post> leftJoinWithOn = queryFactory
.selectFrom(post)
.leftJoin(post.user, user).on(user.age.gt(18)) // 18์ธ ์ด์์ธ ์ฌ์ฉ์๋งโจ
.fetch();
๐ก ์์ฝ
`JPA`, `JPQL`, `QueryDSL`์ ๊ฐ๊ฐ์ ๊ฐ์ ์ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, ์ด๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ํ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ํจ์จ์ฑ์ ๊ทน๋ํํ ์ ์์ต๋๋ค. `JPA`๋ ๊ฐ์ฒด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ๋งคํ์ ๋ด๋นํ๊ณ , `JPQL`์ ๊ฐ์ฒด์งํฅ์ ์ธ ์ฟผ๋ฆฌ ์์ฑ์ ์ง์ํฉ๋๋ค. `QueryDSL`์ ๋์ ์ฟผ๋ฆฌ ์์ฑ๊ณผ ํ์ ์์ ์ฑ์ ๋ณด์ฅํ์ฌ ๋์ฑ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด ์ธ ๊ฐ์ง ๋๊ตฌ๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์ ์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํจ์จ์ ์ผ๋ก ์ํธ์์ฉํ ์ ์์ผ๋ฉฐ, ์ ์ง๋ณด์์ ํ์ฅ์ฑ ์ธก๋ฉด์์๋ ์ ๋ฆฌํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ถ ์ ์์ต๋๋ค.
'โJava' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
1๏ธโฃ JPA (Java Persistence API)
๊ฐ์
JPA๋ ์๋ฐ ์ ํ๋ฆฌ์ผ์ด์
์์ ORM (Object-Relational Mapping)์ ๊ตฌํํ๊ธฐ ์ํ ํ์ค API๋ก, ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๊ฐ์ฒด ๊ฐ์ ๋งคํ์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํฉ๋๋ค. JPA๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฐ์ฒด์งํฅ ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ์ํํ ์ ์์ผ๋ฉฐ, SQL ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํธ์์ฉํ ์ ์์ต๋๋ค. ์ด์ ํจ๊ป Hibernate
๋ JPA์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ๊ตฌํ์ฒด๋ก, JPA์ ๊ธฐ๋ฅ์ ๋์ฑ ๊ฐํํ๊ณ ๋ค์ํ ์ถ๊ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธ์ํฐํฐ ๋งคํ(Entity Mapping)
JPA๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํ
์ด๋ธ์ ์๋ฐ ํด๋์ค(์ํฐํฐ)์ ๋งคํํฉ๋๋ค. ์๋ฅผ ๋ค์ด, @Entity
์ ๋ํ
์ด์
์ ์ฌ์ฉํ์ฌ ํด๋์ค๋ฅผ ์ํฐํฐ๋ก ์ ์ธํ๊ณ , @Id
, @Column
๋ฑ์ ์ ๋ํ
์ด์
์ผ๋ก ๊ฐ ํ๋๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปฌ๋ผ์ ๋งคํํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ฐ์ฒด์งํฅ์ ์ธ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; }
โ๏ธ์ฟผ๋ฆฌ ๋ฉ์๋ (Query Method)
JPA๋ ๋ฉ์๋ ์ด๋ฆ์ ๊ธฐ๋ฐํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์๋์ผ๋ก ์์ฑํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด๋ฅผ ํตํด ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ์กฐํํ ์ ์์ต๋๋ค.
* ๊ณต์๋ฌธ์ ์ฐธ์กฐ
public interface UserRepository extends JpaRepository<User, Long> { // Spring Data JPA๊ฐ ์ ๊ณตํ๋ ์ฟผ๋ฆฌ ๋ฉ์๋ ๊ธฐ๋ฅ์ ์ด์ฉํ์ฌ, ๋ฉ์๋ ์ด๋ฆ์ ๊ธฐ๋ฐํ์ฌ ์๋์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑ List<User> findByName(String name); }
โ๏ธ์ฐ๊ด ๊ด๊ณ ๋งคํ
JPA๋ ์ํฐํฐ ๊ฐ์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ์ค์ ํ ์ ์๋ ๋ค์ํ ์ ๋ํ
์ด์
์ ์ ๊ณตํฉ๋๋ค. ์๋ฅผ ๋ค์ด, @ManyToOne
, @OneToMany
, ๋ฑ์ ์ฌ์ฉํ์ฌ ์ํฐํฐ ๊ฐ์ ๋ค๋์ผ, ์ผ๋๋ค, ๋ค๋๋ค ๊ด๊ณ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค. ์ด๋ฌํ ๊ด๊ณ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ธ๋ ํค์ ์ผ์นํ๋ฉฐ, ๊ฐ์ฒด ๊ฐ์ ์ฐธ์กฐ ๊ด๊ณ๋ก ํํ๋ฉ๋๋ค. JPA์์๋ ์ด๋ฌํ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก JOIN
์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ๊ด๋ จ๋ ์ํฐํฐ๋ฅผ ํจ๊ป ์กฐํํ ์ ์์ต๋๋ค.
@Entity public class User { @OneToMany(mappedBy = "user") // ์ฌ์ฉ์(User)์์ ์ผ๋๋ค ๊ด๊ณ๋ฅผ ์ ์ํ๋ฉฐ, 'user' ํ๋๋ก ๋งคํโจ private List<Post> posts; // ์ฌ์ฉ์๊ฐ ์์ฑํ ๊ฒ์๋ฌผ ๋ชฉ๋ก } @Entity public class Post { @ManyToOne // ๊ฒ์๋ฌผ(Post)์์ ๋ค๋์ผ ๊ด๊ณ๋ฅผ ์ ์ํ๋ฉฐ, ๊ฒ์๋ฌผ์ด ์ํ ์ฌ์ฉ์(User)๋ฅผ ์ฐธ์กฐโจ private User user; // ๊ฒ์๋ฌผ์ด ์ํ ์ฌ์ฉ์ }
โ๏ธํจ์น ์ ๋ต(Fetch Strategy)
JPA์์๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ๋ก๋ํ ๋ Eager
์ Lazy
๋ ๊ฐ์ง ํจ์น ์ ๋ต์ ์ ํํ ์ ์์ต๋๋ค. Eager ํจ์น๋ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ์ฆ์ ๋ก๋ํ๊ณ , Lazy ํจ์น๋ ์ค์ ๋ก ํด๋น ๋ฐ์ดํฐ๊ฐ ํ์ํ ๋ ๋ก๋ํฉ๋๋ค.
@Entity public class User { @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) // FetchType ๊ธฐ๋ณธ๊ฐ: Lazy - ์ฌ์ฉ์๊ฐ ์กฐํ๋ ๋ ๊ด๋ จ๋ ๊ฒ์๋ฌผ ๋ชฉ๋ก์ด ํ์ํ ๋๋ง ๋ก๋๋จโจ private List<Post> posts; } @Entity public class Post { @ManyToOne(fetch = FetchType.EAGER) // FetchType ๊ธฐ๋ณธ๊ฐ: Eager - ๊ฒ์๋ฌผ์ด ์กฐํ๋ ๋ ์ฌ์ฉ์ ์ ๋ณด๊ฐ ์ฆ์ ๋ก๋๋จโจ private User user; }
โ๏ธCustomRepository ๊ตฌํ
JPA๋ฅผ ์ฌ์ฉํ๋ค ๋ณด๋ฉด ๊ธฐ๋ณธ ๊ธฐ๋ฅ ์ธ์ ์ถ๊ฐ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ๋ณต์กํ ์ฟผ๋ฆฌ๊ฐ ํ์ํ ๋๊ฐ ์์ต๋๋ค. ์ด๋ด ๋ CustomRepository
๋ฅผ ํตํด ์ปค์คํ
๋ฆฌํฌ์งํ ๋ฆฌ๋ฅผ ๋ง๋ค์ด ํ์ํ ๊ธฐ๋ฅ์ ํ์ฅํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ธฐ๋ณธ CRUD ๊ธฐ๋ฅ ์ธ์ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ํน์ ์๊ตฌ์ฌํญ์ ๋ง๋ ๋ฐ์ดํฐ ์กฐํ ๋ฐ ์ฒ๋ฆฌ ๋ก์ง์ ๊ตฌํํ ์ ์์ผ๋ฉฐ, ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค.
โ๏ธEntityManager
JPA์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ๊ด๋ฆฌํ๋ ํต์ฌ ์ธํฐํ์ด์ค๊ฐ EntityManager
์
๋๋ค. ์ด ์ธํฐํ์ด์ค๋ฅผ ํตํด ์ํฐํฐ์ ์๋ช
์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ๊ณ , JPQL ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ฉฐ, ํธ๋์ญ์
์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
2๏ธโฃ JPQL (Java Persistence Query Language)
๊ฐ์
JPQL์ JPA์์ ์ ๊ณตํ๋ ๊ฐ์ฒด ์งํฅ ์ฟผ๋ฆฌ ์ธ์ด๋ก, SQL๊ณผ ์ ์ฌํ์ง๋ง ํ ์ด๋ธ ๋์ ์ํฐํฐ ํด๋์ค์ ๊ทธ ์์ฑ์ ๋์์ผ๋ก ์ฟผ๋ฆฌ๋ฅผ ์์ฑํฉ๋๋ค.
JPQL ์ฟผ๋ฆฌ ์์ฑ ๋ฐฉ๋ฒ
โ๏ธ์ ๋ ธํ ์ด์ ๋ฐฉ์
์ด ๋ฐฉ๋ฒ์ @Query
์ ๋
ธํ
์ด์
์ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ๋ฉ์๋ ์์ ์ ์ํ๋ ๋ฐฉ์์
๋๋ค. Spring Data JPA์์ ์ฃผ๋ก ์ฌ์ฉ๋๋ฉฐ, ๋ฉ์๋์ ์ ์ธ๋ถ์ ์ฟผ๋ฆฌ๋ฅผ ๋ช
์ํจ์ผ๋ก์จ, ๋ณด๋ค ๊ฐ๊ฒฐํ๊ณ ์ง๊ด์ ์ธ ์ฝ๋๋ฅผ ์์ฑํ ์ ์์ต๋๋ค.
public interface PostRepository extends CrudRepository<Post, Long> { // @Query ์ ๋ํ
์ด์
์ ์ฌ์ฉํ์ฌ ๋ฉ์๋์ ์ง์ JPQL ์ฟผ๋ฆฌ ์ ์โจ @Query("SELECT p FROM Post p WHERE p.user.id = :userId") List<Post> findByUserId(@Param("userId") Long userId); }
โ๏ธEntityManager ๋ฐฉ์
์ด ๋ฐฉ๋ฒ์ EntityManager
๋ฅผ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ๋์ ์ผ๋ก ์์ฑํ๊ณ ์คํํ๋ ๋ฐฉ์์
๋๋ค. ๋ณด๋ค ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๋์ ์ฟผ๋ฆฌ ์์ฑ์ด ํ์ํ ๋ ์ ์ฉํฉ๋๋ค. JPQL ์ฟผ๋ฆฌ๋ฅผ ๋ฌธ์์ด๋ก ์ ์ํ๊ณ , createQuery()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์คํํฉ๋๋ค.
public class PostService { // JPA์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ๊ด๋ฆฌํ๋ EntityManagerโจ @PersistenceContext private EntityManager entityManager; public void updatePostContent(Long postId, String newContent) { // ๊ฒ์๋ฌผ ID์ ๋ฐ๋ผ ๋ด์ฉ์ ์
๋ฐ์ดํธํ๋ JPQL ์ฟผ๋ฆฌ String sql = "UPDATE Post p SET p.content = :content WHERE p.id = :postId"; Query query = entityManager.createQuery(sql); // EntityManager๋ฅผ ์ฌ์ฉํ์ฌ JPQL ์ฟผ๋ฆฌ ์์ฑโจ query.setParameter("content", newContent); query.setParameter("postId", postId); int result = query.executeUpdate(); // ์ฟผ๋ฆฌ ์คํ } }
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธFetch ์กฐ์ธ
Fetch ์กฐ์ธ์ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ํจ๊ป ๋ก๋ํ์ฌ N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐ ๋์์ ์ค๋๋ค. ์ด ๋ฐฉ๋ฒ์ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๊ฐ์ ธ์ฌ ์ ์๋๋ก ํ์ฌ ์ฑ๋ฅ์ ํฅ์์ํต๋๋ค. ๊ทธ๋ฌ๋ Fetch ์กฐ์ธ์ ์ฌ์ฉํ ๋, ์ค๋ณต๋ ๊ฒฐ๊ณผ๊ฐ ๋ฐ์ํ ์ ์์ผ๋ฏ๋ก DISTINCT
ํค์๋๋ฅผ ์ถ๊ฐํ์ฌ ์ค๋ณต๋ ์ํฐํฐ๋ฅผ ์ ๊ฑฐํ๋ ๊ฒ์ด ์ข์ต๋๋ค. ์ด๋ ๊ฒ ํ๋ฉด ๊ฒฐ๊ณผ ๋ฆฌ์คํธ์์ ๊ฐ์ ์ํฐํฐ๊ฐ ์ฌ๋ฌ ๋ฒ ๋ํ๋๋ ๊ฒ์ ๋ฐฉ์งํ ์ ์์ต๋๋ค.
public class PostRepositoryCustomImpl implements PostRepositoryCustom { @PersistenceContext private EntityManager entityManager; public List<Post> fetchPostsWithUsers() { // Fetch ์กฐ์ธ์ ์ฌ์ฉํ์ฌ Post์ User๋ฅผ ํจ๊ป ๋ก๋ํ๊ณ ์ค๋ณต๋ ๊ฒ์๋ฌผ ์ ๊ฑฐโจ String jpql = "SELECT DISTINCT p FROM Post p JOIN FETCH p.user"; List<Post> posts = entityManager.createQuery(jpql, Post.class) .getResultList(); return posts; // ์ฐ๊ด๋ User ๋ฐ์ดํฐ์ ํจ๊ป ๋ก๋๋ Post ๋ฆฌ์คํธ ๋ฐํ } }
โ๏ธ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ
JPQL
๋์ SQL์ ์ง์ ์ฌ์ฉํ๊ณ ์ถ์ ๋ ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ฅผ ์ฌ์ฉํ ์ ์์ต๋๋ค. ๋ค์ดํฐ๋ธ ์ฟผ๋ฆฌ๋ ํน์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ SQL ๋ฌธ๋ฒ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ผ๋ก, ๋ณต์กํ ์ฟผ๋ฆฌ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ํน์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์์ต๋๋ค.
public interface PostRepository extends CrudRepository<Post, Long> { // @Query ์ ๋ํ
์ด์
์ ์ฌ์ฉํ์ฌ ๋ฉ์๋์ ์ง์ ๋ค์ดํฐ๋ธ SQL ์ฟผ๋ฆฌ ์ ์โจ @Query(value = "SELECT * FROM post WHERE user_id = :userId", nativeQuery = true) List<Post> findByUserId(@Param("userId") Long userId); }
3๏ธโฃ QueryDSL (Query Domain-Specific Language)
์ฃผ์ ๊ธฐ๋ฅ ๋ฐ ๊ฐ๋
โ๏ธํ์ ์์ ์ฑ
QueryDSL์ ์ฟผ๋ฆฌ ๋ฌธ๋ฒ์ ์๋ฐ ์ฝ๋๋ก ํํํ๋ฏ๋ก, ์ปดํ์ผ ํ์์ ์ฟผ๋ฆฌ์ ์ค๋ฅ๋ฅผ ๋ฏธ๋ฆฌ ํ์ธํ ์ ์์ต๋๋ค. ์ด๋ฅผ ํตํด ๋ฐํ์ ์ค๋ฅ๋ฅผ ์ค์ด๊ณ , ์ฝ๋์ ์์ ์ฑ์ ๋์ ๋๋ค.
โ๏ธ๋์ ์ฟผ๋ฆฌ ์์ฑ
๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ด๋ ์ฌ์ฉ์ ์๊ตฌ ์ฌํญ์ ๋ฐ๋ผ ์กฐ๊ฑด์ ๋์ ์ผ๋ก ์ถ๊ฐํ๊ฑฐ๋ ๋ณ๊ฒฝํ ์ ์์ต๋๋ค. BooleanBuilder
์ ๊ฐ์ ๊ธฐ๋ฅ์ ํ์ฉํ๋ฉด ๋์ ์ฟผ๋ฆฌ๋ฅผ ์์ฝ๊ฒ ์์ฑํ ์ ์์ต๋๋ค.
BooleanBuilder builder = new BooleanBuilder(); if (name != null) { builder.and(user.name.eq(name)); // name ์กฐ๊ฑด ์ถ๊ฐโจ } if (otherCondition != null) { builder.or(user.status.eq(otherCondition)); // ๋ค๋ฅธ ์กฐ๊ฑด์ OR ์ฌ์ฉโจ } // ์ต์ข
์ฟผ๋ฆฌ ์คํ List<User> users = queryFactory.selectFrom(user) .where(builder) // ๋์ ์ผ๋ก ์์ฑ๋ ์กฐ๊ฑด์ ์ฌ์ฉโจ .fetch();
โ๏ธ์กฐ์ธ (JOIN)
QueryDSL์์๋ ์กฐ์ธ ์ฐ์ฐ์ ํตํด ์ฌ๋ฌ ์ํฐํฐ๋ฅผ ์ฐ๊ด ์ง์ด ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์์ต๋๋ค. ์ด ๋ฐฉ์์ ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ์์ ์ฐ๊ด๋ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์กฐํํ๋ ๋ฐ ์ ์ฉํฉ๋๋ค.
QPost post = QPost.post; QUser user = QUser.user; // INNER JOIN ์์: ์์ฑ์๊ฐ John์ธ ํฌ์คํธ ์กฐํ List<Post> innerJoinPosts = queryFactory .selectFrom(post) .join(post.user, user) // INNER JOINโจ .where(user.name.eq("John")) .fetch(); // LEFT JOIN ์์: ๋ชจ๋ ํฌ์คํธ์ ๊ทธ ์์ฑ์๋ฅผ ์กฐํํ๋, ์์ฑ์๊ฐ ์๋ ํฌ์คํธ๋ ํฌํจ List<Post> leftJoinPosts = queryFactory .selectFrom(post) .leftJoin(post.user, user) // LEFT JOINโจ .fetch(); // ON ์ ์ ๋ช
์ํ๋ LEFT JOIN ์์ List<Post> leftJoinWithOn = queryFactory .selectFrom(post) .leftJoin(post.user, user).on(user.age.gt(18)) // 18์ธ ์ด์์ธ ์ฌ์ฉ์๋งโจ .fetch();
๐ก ์์ฝ
JPA
, JPQL
, QueryDSL
์ ๊ฐ๊ฐ์ ๊ฐ์ ์ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ, ์ด๋ฅผ ์ ๊ธฐ์ ์ผ๋ก ํ์ฉํ๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์
์ ํจ์จ์ฑ์ ๊ทน๋ํํ ์ ์์ต๋๋ค. JPA
๋ ๊ฐ์ฒด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ๋งคํ์ ๋ด๋นํ๊ณ , JPQL
์ ๊ฐ์ฒด์งํฅ์ ์ธ ์ฟผ๋ฆฌ ์์ฑ์ ์ง์ํฉ๋๋ค. QueryDSL
์ ๋์ ์ฟผ๋ฆฌ ์์ฑ๊ณผ ํ์
์์ ์ฑ์ ๋ณด์ฅํ์ฌ ๋์ฑ ๊ฐ๋ ฅํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฒ๋ฆฌ ๋ฅ๋ ฅ์ ์ ๊ณตํฉ๋๋ค. ์ด ์ธ ๊ฐ์ง ๋๊ตฌ๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋๊ท๋ชจ ์ ํ๋ฆฌ์ผ์ด์
์์๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํจ์จ์ ์ผ๋ก ์ํธ์์ฉํ ์ ์์ผ๋ฉฐ, ์ ์ง๋ณด์์ ํ์ฅ์ฑ ์ธก๋ฉด์์๋ ์ ๋ฆฌํ ๊ตฌ์กฐ๋ฅผ ๊ฐ์ถ ์ ์์ต๋๋ค.