JiwonDev

Spring Data JPA

by JiwonDev

๊ธฐ์–ต์•ˆ๋‚  ๋•Œ ์‰ฝ๊ฒŒ ์ฐพ์œผ๋ ค๊ณ  ํ•œ ๊ธ€์— ๋‹ค ์ ์—ˆ์Šต๋‹ˆ๋‹ค.

* ์Šคํฌ๋กค ์••๋ฐ• ์ฃผ์˜ *

 

์Šคํ”„๋ง ์„ค์ •์„ ์‰ฝ๊ฒŒ ํ•ด์ฃผ๋Š” Spring Boot ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ๋“ฏ์ด

์Šคํ”„๋ง์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” Spring Data ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ๋‹ค.

Spring Data์— ํฌํ•จ๋œ ์ฃผ์š” ๋ชจ๋“ˆ๋“ค. ์—ฌ๊ธฐ ์•ˆ๋‚˜์˜จ ๋‹ค๋ฅธ ๊ธฐ๋Šฅ๋„ ๋งŽ๋‹ค.

 

๊ทธ์ค‘ ์Šคํ”„๋ง์—์„œ JPA๋ฅผ ์ถ”์ƒํ™”ํ•˜์—ฌ, ์ •๋ง ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋งŒ๋“  Spring Data JPA๋ฅผ ์•Œ์•„๋ณด์ž.

 

 

๐Ÿ’ญ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋‚˜์š”

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

์ฐธ๊ณ ๋กœ spring-boot-data-jpa์—๋Š” ์•„๋ž˜์˜ ํŒจํ‚ค์ง€๋“ค์„ ํฌํ•จํ•˜๊ณ  ์žˆ๋‹ค.

๋”๋ณด๊ธฐ

ํŒจํ‚ค์ง€ ๋‚ด๋ถ€์—์„œ Spring Core๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์œผ๋ฏ€๋กœ, gradle์ด spring core ์˜์กด์„ฑ๊นŒ์ง€ ๋ถˆ๋Ÿฌ์˜จ๋‹ค.

  • Jakarta EE(๊ตฌ Java EE)์˜ persistence, transaction ๋ชจ๋“ˆ
  • hibernate-core (JPA ๊ตฌํ˜„์ฒด, ํ•˜์ด๋ฒ„๋„ค์ดํŠธ)
  • spring data-jpa
  • spring aspectJ
  • spring-boot-starter aop
  • spring-boot-starter jdbc

 

์š”์ฆ˜์€ ์Šคํ”„๋ง๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, @SpringBootApplication ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ํŠน๋ณ„ํžˆ ๋”ฐ๋กœ ํ•ด์ค„ ์„ค์ •์€ ์—†๋‹ค.

๋ฌผ๋ก  public class Application ์ด ์ตœ์ƒ์œ„ ํŒจํ‚ค์ง€๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ์ฐพ์ง€ ๋ชปํ•œ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋งŒ ์ง์ ‘ ์ง€์ •ํ•ด์ฃผ์ž.

package mypackage.myapp; // ์ด ๋ชจ๋“ˆ์˜ ํ•˜์œ„ ํŒจํ‚ค์ง€๋ฅผ ๋ชจ๋‘ ์ ์šฉ์‹œํ‚จ๋‹ค.

@SpringBootApplication
public class DataJpaApplication { 
    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }
}

 

๋งŒ์•ฝ ์Šคํ”„๋ง ๋ถ€ํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด์š”?

๋”๋ณด๊ธฐ

@EnableJpaRepositories( ์Šค์บ” ๊ฒฝ๋กœ )๋ฅผ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ๋ณดํ†ต ํ”„๋กœ์ ํŠธ ์ตœ์ƒ๋‹จ์— ์žˆ๋Š” ์„ค์ • ๊ฐ์ฒด์— @Configuration ์™€ ํ•จ๊ป˜ ์ž‘์„ฑํ•œ๋‹ค.

@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}

 

 

@Entity์˜ ๊ฒฝ์šฐ ์Šคํ”„๋ง๊ณผ ์—ฐ๊ด€์—†์ด ๊ทธ๋ƒฅ JPA ๋ช…์„ธ(javax.persistence.Entity)์ด๊ธฐ์— ์Šคํ”„๋ง ์˜์กด์„ฑ์ด ์—†๋”๋ผ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธดํ•˜๋‹ค. ์˜›๋‚  Hybernate ์‹œ์ ˆ์—๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค์ •์„ ํ•ด์ฃผ์—ˆ๋‹ค.

<property name="packagesToScan">myNamespace.*</property>

์ด ์„ค์ • ๋˜ํ•œ @SpringbootApplication ์— ์žˆ๋Š” @EnableAutoConfiguraion์ด ์ž๋™์œผ๋กœ ํ•ด์ค€๋‹ค.
๋งŒ์•ฝ @Entity๊ฐ€ ๋‹ค๋ฅธ ๋ชจ๋“ˆ(๋˜๋Š” ์™ธ๋ถ€ JAR)์— ์žˆ์–ด ์Šค์บ”์ด ์•ˆ๋˜๋Š” ๊ฒฝ์šฐ, ์•„๋ž˜์™€ ๊ฐ™์ด ์ง์ ‘ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

@EntityScan ์œผ๋กœ ์Šค์บ”ํ•  ๋Œ€์ƒ์„ ์ถ”๊ฐ€๋กœ ์ง€์ •ํ•œ๋‹ค.&amp;amp;amp;amp;amp;amp;amp;nbsp;

 

๊ฐ„ํ˜น @ComponetScan๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋Š” ์‚ฌ๋žŒ์ด ์žˆ๋Š”๋ฐ, ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์€ ๋ง ๊ทธ๋Œ€๋กœ @Component๊ฐ€ ๋‹ฌ๋ฆฐ ํด๋ž˜์Šค๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. @EntityScan์€ Entity๋ฅผ ์ฐพ๋Š” ๋ฒ”์œ„๋ฅผ ํ™•์žฅํ•  ๋ฟ, ์Šคํ”„๋ง ๋นˆ๊ณผ๋Š” ์•„๋ฌด๋Ÿฐ ์—ฐ๊ด€์ด ์—†๋‹ค. ๋นˆ์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.

// ๋‘˜ ๋‹ค ์„ค์ •ํ•ด์ค„ ๋•Œ๋„ ์ด๋ ‡๊ฒŒ ๋”ฐ๋กœ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์ผ๋ฐ˜์ ์ด๋‹ค.
@ComponentScan("org.example.base")
@EntityScan("org.example.base.entities")
public class MyConfig {

}

 

Spring Data JPA๋Š” ์—ฌ๊ธฐ์— ์ถ”๊ฐ€์ ์œผ๋กœ @Repository๊ฐ€ ๋ถ™์–ด์žˆ๋Š” ํด๋ž˜์Šค๋ฅผ ์ฐพ์•„ ํ”„๋ก์‹œ๋กœ ๋Œ€์ฒดํ•ด์ค€๋‹ค.

@Repository // org.springframework.data.repository.Repository

 

 

SpringDataJpa๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด EntityManagerFactory๋Š” ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ์— ์ƒ์„ฑ๋˜์–ด ์žˆ๋‹ค.

์‚ฌ์šฉํ•  ์ผ์€ ์—†๊ฒ ์ง€๋งŒ, ๋งŒ์•ฝ ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ์— ์žˆ๋Š” EntityMangerFactory์—์„œ EntityManager๋ฅผ ๋ฐ›๊ณ ์‹ถ๋‹ค๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ ์ฃผ์ž…๋ฐ›์œผ๋ฉด ๋œ๋‹ค.

@PersistenceContext // ์ตœ์‹  ๋ฒ„์ „ ์Šคํ”„๋ง์—์„œ๋Š” @Autowired๋กœ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค.
EntityManager entityManager;
// EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("emf name");
// EntityManager entityManager = entityManagerFactory.createEntityManager();
๊นŒ๋จน์—ˆ์„๊นŒ๋ด ์ถ”๊ฐ€์„ค๋ช…. ์ฐธ๊ณ ๋กœ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์—์„œ๋Š” SessionManager, SessionFactory๋ผ๊ณ  ๋ถˆ๋ ธ๋‹ค.

 

 

 

๊ทธ๋ฆฌ๊ณ  ๊ณต์šฉ ์ธํ„ฐํŽ˜์ด์Šค์ธ JpaRepository๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด, ์•„๋ฌด๋Ÿฐ ์ถ”๊ฐ€ ์ž‘์—…์—†์ด ์•„๋ž˜์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

// ์ด๊ฒŒ ๋. ์ƒ๋žตํ•œ๊ฒŒ ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ๋‘๋ฉด ๋์ด๋‹ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
}

 


๐Ÿ’ญ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฐฉ๋ฒ•

๋‹ค๋งŒ, interface JpaRepository<Entity, ID>๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค์˜ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“คํ•„์š”๋„, @Repository๋ฅผ ๋ถ™์ผ ํ•„์š”๋„ ์—†๋‹ค.

// ์ด๊ฒŒ ๋. ์ƒ๋žตํ•œ๊ฒŒ ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ๋‘๋ฉด ๋์ด๋‹ค.
// SpringDataJpa์—์„œ ๋งŒ๋“  ๊ธฐ๋ณธ Jpa ๊ตฌํ˜„์ฒด๊ฐ€ ์Šคํ”„๋ง ์ปจํ…Œ์ด๋„ˆ์— ์ƒ์„ฑ๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
๋ณต์žกํ•œ ์กฐํšŒ๋„ ์•„๋‹ˆ๊ณ  ๋ฉ”์„œ๋“œ ์ด๋ฆ„๋งŒ ๋ณด๋ฉด ๋”ฑ ๋ฌด์Šจ ์ฟผ๋ฆฌ์ธ์ง€ ๋ณด์ด๋Š”๋ฐ..๊ทธ๋ƒฅ ์•Œ์•„์„œ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ์•ˆ๋˜๋‚˜?

โžก ์ด๊ฒŒ ๋ฐ”๋กœ SpringDataJpa์˜ ๊ธฐ๋Šฅ์ด๋‹ค.

JpaRepository ์ธํ„ฐํŽ˜์ด์Šค์—๋Š” ๋น„์ฆˆ๋‹ˆ์Šค์— ์ž์ฃผ์“ฐ๋Š” ์ฝ”๋“œ (save, findById, delete..)๊ฐ€ ๋Œ€๋ถ€๋ถ„ ํฌํ•จ๋˜์–ด์žˆ๋‹ค.

๊ทธ ์™ธ ํŠน์ • ํ•„๋“œ๋ช…์ด ๋“ค์–ด๊ฐ€๋Š” ๊ฒฝ์šฐ(findByEmail ๋“ฑ)๋งŒ ๋”ฐ๋กœ ์ถ”๊ฐ€๋กœ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

๋ง์ด ๋Œ€๋ถ€๋ถ„์ด์ง€, JPA๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ๋ฉ”์„œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. 

๋ฌผ๋ก  ์กฐ๊ฑด์ด ์—ฌ๋Ÿฌ๊ฐœ๊ฑฐ๋‚˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ณต์žกํ•ด์ง€๋ฉด QureyDSL๋“ฑ์œผ๋กœ JPQL์„ ์ง์ ‘ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜, ๊ทธ๋ƒฅ JPA๋ฅผ ์•ˆ์“ฐ๋Š” ๊ฒฝ์šฐ๋„ ๋งŽ๋‹ค.

repository.save(new Member("username", 10));
repository.save(new Member("username", 20));

// ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๊ทœ์น™๋งŒ ์ž˜ ์ง€ํ‚จ๋‹ค๋ฉด, ์ด๋Ÿฐ ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.
List<Member> result = repository.findByUsernameAndAgeGreaterThan("username", 15);

assertThat(result.size()).isEqualTo(1);
Member member = result.get(0);
assertThat(member.getUsername()).isEuqalTo("username");
assertThat(member.getAge()).isEqualTo(20);

๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๊ทœ์น™ (Count, Between, In๋“ฑ ์™ ๋งŒํ•œ๊ฑด ๋‹ค ์ง€์›ํ•œ๋‹ค)_

์ฒ˜์Œ์—๋Š” T findOne(ID)๋„ ์ œ๊ณตํ–ˆ์œผ๋‚˜, ์ตœ์‹  ๋ฒ„์ „์—๋Š” Optional&amp;amp;amp;amp;amp;amp;amp;lt;T&amp;amp;amp;amp;amp;amp;amp;gt; findById(ID)๋งŒ ๋‚จ๊ฒŒ๋˜์—ˆ๋‹ค.&amp;amp;amp;amp;amp;amp;amp;nbsp;
์ฐธ๊ณ ๋กœ find(...)By ์‚ฌ์ด๋Š” ๋ฉ”์„œ๋“œ ์ฃผ์„(์„ค๋ช…)์„ ์ ๋Š” ์ž๋ฆฌ์ด๋‹ค.

 

๋ฌธ์ž์—ด LIKE ๋ฅผ Spring JPA ๋ฉ”์„œ๋“œ๋„ค์ด๋ฐ์œผ๋กœ ๋งŒ๋“ค๊ธฐ

public interface BookRepository extends JpaRepository<Book, Long>{

  // select ... like %:title%
  List<Book> findBooksByTitleContaining(String title);
  List<Book> findBooksByTitleContains(String title);
  List<Book> findBooksByTitleIsContaining(String title); 
  
  // select ... like :title
  List<Book> findBooksByTitleLike(String title);
  
  // select ... like :title%
  List<Book> findBooksByTitleStartingWith(String title);
  
  // select ... like %:title
  List<Book> findBooksByTitleEndingWith(String title);
  
}

 

๐Ÿ’ญ ์ด๊ฒŒ ์–ด๋–ป๊ฒŒ ๊ฐ€๋Šฅํ•œ๊ฑฐ์ฃ ? -> SimpleJpaRepository

SpringDataJPA๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ class SimpleJpaRepository๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ์ปจํ…Œ์ด๋„ˆ์— ๋“ฑ๋ก์‹œํ‚จ๋‹ค.
์ฆ‰ interface JpaRepository ์ธํ„ฐํŽ˜์ด์Šค์— ์•Œ๋งž์€ SimpleJpaRepository๊ฐ€ ์ปจํ…Œ์ด๋„ˆ์— ์˜ํ•ด ์ฃผ์ž…๋œ๋‹ค.

  • [ JDBC๋‚˜ JPA์— ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ ]๋ฅผ [ ์Šคํ”„๋ง์ด ์ œ๊ณตํ•˜๋Š” ์˜ˆ์™ธ ]๋กœ ํฌ์žฅ์‹œ์ผœ์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ์ธํ„ฐํŽ˜์ด์Šค์˜ ์ œ๋„ค๋ฆญ (Entity, ID) ํƒ€์ž…์„ ๋ฐ”ํƒ•์œผ๋กœ JPA ์ฟผ๋ฆฌ๋ฌธ(em.persist ๋“ฑ)์„ ์ƒ์„ฑํ•œ๋‹ค.
  • ํŠน์ • ํ•„๋“œ๋ฅผ ์ด์šฉํ•œ ์ฝ”๋“œ( findByUsername, findByEmail ) ๋˜ํ•œ ๋ฉ”์„œ๋“œ ๋ช…์„ ๋ถ„์„ํ•˜์—ฌ JPQL ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

@Repository ๊ฐ€ ๋ถ™์–ด์žˆ์œผ๋ฏ€๋กœ, ์ด๋Š” ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋œ๋‹ค.

์‹ค์ œ๋กœ SimpleJpaRepository๊ฐ€ ์“ฐ๋Š” QureyUtils ๋ฅผ ์‚ดํŽด๋ณด๋ฉด, ์ฟผ๋ฆฌ๋ฌธ์— ๋Œ€ํ•œ ์ •๊ทœํ‘œํ˜„์‹์ด ์ •์˜๋˜์–ด ์žˆ๋‹ค.

public abstract class QueryUtils {

    public static final String COUNT_QUERY_STRING = "select count(%s) from %s x";
    public static final String DELETE_ALL_QUERY_STRING = "delete from %s x";
    public static final String DELETE_ALL_QUERY_BY_ID_STRING = "delete from %s x where %s in :ids";
    private static final String IDENTIFIER = "[._$[\\P{Z}&&\\P{Cc}&&\\P{Cf}&&\\P{Punct}]]+";
    static final String COLON_NO_DOUBLE_COLON = "(?<![:\\\\]):";
    static final String IDENTIFIER_GROUP = String.format("(%s)", IDENTIFIER);
    ... // ์ƒ๋žต
}

 

๐Ÿ“‘ ํŠธ๋žœ์žญ์…˜์€ ์–ด๋–ป๊ฒŒํ•˜์ฃ ? ์—†์–ด๋„ ๋™์ž‘ํ•˜๋˜๋ฐ?

์šฐ์„ , JPA์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ๋งŒ ๋™์ž‘ํ•œ๋‹ค. (์—†์œผ๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.)

public static void main(String[] args) {
    /* ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์‹œ์ž‘ํ•  ๋•Œ Factory ์ƒ์„ฑํ•œ๋‹ค. */
    EntityManagerFactory factory = Persistence.createEntityManagerFactory("hello");

    // ๐Ÿ’ญ 1. ์Šค๋ ˆ๋“œ์—์„œ ์‚ฌ์šฉํ•  EntityManager ๋ฐ›์•„์˜จ๋‹ค. ํ•œ ํŠธ๋žœ์žญ์…˜์— ํ•œ EntityManager๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค.
    EntityManager entityManager = factory.createEntityManager();

    // ๐Ÿ’ญ 2. JPA๋ฅผ ์ด์šฉํ•œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์€ ํŠธ๋žœ์žญ์…˜ ์•ˆ์—์„œ๋งŒ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋‹ค.
    EntityTransaction tx = entityManager.getTransaction();
    try {
        Member member = new Member();
        member.setId(2L);
        member.setName("Kim");

        entityManager.persist(member);

        tx.commit();
    } catch (Exception e) {
        tx.rollback();
    } finally {
        // ๐Ÿ’ญ 3. ์‚ฌ์šฉ์ด ๋๋‚œ EntityManager ๋ฐ˜๋‚ฉํ•œ๋‹ค.
        entityManager.close();
    }

    factory.close(); /* ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ข…๋ฃŒํ•  ๋•Œ ํ•„์š”์—†์–ด์ง„ Factory ๋ฐ˜ํ™˜ํ•œ๋‹ค. */
}

SimpleJpaRepository ์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด, ํด๋ž˜์Šค ์ž์ฒด์— @Transactional์ด ๊ฑธ๋ ค์žˆ๋‹ค.

์ปค๋งจ๋“œ์„ฑ ๋ฉ”์„œ๋“œ์—๋งŒ ์ด๋ ‡๊ฒŒ ์ถ”๊ฐ€๋กœ ์„ค์ •ํ•œ๋‹ค.

 

์„œ๋น„์Šค ๊ณ„์ธต์—์„œ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด Repository์—์„œ ํŠธ๋žœ์žญ์…˜์ด ์‹œ์ž‘ํ•œ๋‹ค.

(* propagation.Required ์˜ต์…˜์ด ๊ธฐ๋ณธ๊ฐ’์ด๊ธฐ ๋•Œ๋ฌธ. ์ด๋Š” ํŠธ๋žœ์žญ์…˜์ด ์žˆ๋‹ค๋ฉด ๊ทธ๋Œ€๋กœ ์“ฐ๊ณ , ์—†์œผ๋ฉด ์ƒˆ๋กœ ์‹œ์ž‘ํ•œ๋‹ค. )

 

 

 

Spring Data ํ”„๋กœ์ ํŠธ์˜ ์„œ๋ธŒ ํ”„๋กœ์ ํŠธ๊ฐ€ Spring Data JPA์ด๋‹ค.

๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ณตํ†ต๋ถ€๋ถ„์„ ๋ฝ‘์•„ ์ƒ์† ๊ด€๊ณ„๋ฅผ ๋งŒ๋“ค์–ด๋’€๋Š”๋ฐ, ์ž์„ธํ•˜๊ฒŒ ์•Œ ํ•„์š”๋Š” ์—†๊ธดํ•˜๋‹ค.

Repository&amp;amp;amp;amp;amp;amp;amp;lt;T, ID&amp;amp;amp;amp;amp;amp;amp;gt;๋Š” ์Šคํ”„๋ง ์Šค์บ”์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ผ๋ฟ, ์ฝ”๋“œ๋Š” ๋น„์–ด์žˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์ถ”์ƒํ™”๋ฅผ ์ž˜ ์‹œ์ผœ๋†”์„œ ์‚ฌ์šฉ์„ ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์„ ๋ฟ, ๋‚ด๋ถ€์ฝ”๋“œ๋Š” JPA๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ตฌํ˜„ํ•œ๋‹ค.

SimpleJpaRepository์˜ ์ฝ”๋“œ๋ฅผ ๋œฏ์–ด๋ณด๋ฉด entityManager๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ถ€๋ถ„์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

JPA๋„ ๊ฒฐ๊ตญ Java์˜ JDBC๋ฅผ ์ด์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ด์šฉํ•จ์„ ์žŠ์ง€๋ง์ž.

๋ญ๋“ ์ง€ ์•Œ์•„์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ๋Š” ๋งˆ๋ฒ•์˜ ๋„๊ตฌ๊ฐ€ ์•„๋‹ˆ๋‹ค.

 

@Transactional ์˜ ๋™์ž‘์›๋ฆฌ, ํŠธ๋žœ์žญ์…˜ ๋งค๋‹ˆ์ €

# JDBC์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํŠธ๋žœ์žญ์…˜ ์ž๋ฐ”์˜ JDBC์—์„œ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ํ•œ๊ฐ€์ง€ ๋ฐ–์— ์—†์Šต๋‹ˆ๋‹ค. // ์ปค๋„ฅ์…˜ ํ’€์—์„œ DB์ปค๋„ฅ์…˜์„ ๋ฐ›์•„์™”๋‹ค๊ณ  ๊ฐ€์ • Connection connection = dataSource.getConnect

jiwondev.tistory.com

 


๐Ÿ’ญ ์Šคํ”„๋ง์—์„œ์˜ NameQurey

์ด ๊ธฐ๋Šฅ์€ JPA์˜ @NameQurey๋ฅผ ํ™œ์šฉํ•œ๋‹ค. (์ง์ ‘ ์‚ฌ์šฉํ•  ์ผ์€ ์—†์œผ๋‹ˆ ์„ค๋ช…๋งŒ ๋“ฃ๊ณ ๊ฐ€์ž)

๋”๋ณด๊ธฐ

์ด๋Š” ์—”ํ‹ฐํ‹ฐ์— ํŠน์ • ์ฟผ๋ฆฌ๋ฅผ ํ• ๋‹นํ•ด๋†“๊ณ , ํ•„์š”ํ•  ๋•Œ ๊บผ๋‚ด์„œ ์“ธ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username" // :username ๋Š” ํŒŒ๋ผ๋ฉ”ํƒ€
)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    ...
}
@PersistenceContext
EntityManager em;

public List<Member> findByUsername(String username) {
	// createNameQurey()๋ฅผ ํ†ตํ•ด์„œ ๊บผ๋‚ด ์“ธ ์ˆ˜ ์žˆ๋‹ค.
    return em.createNamedQuery("Member.findByUsername", Member.class)
        .setParameter("username", "์ด๋ฆ„")
        .getResultList();
}

 

๋ฌผ๋ก  ์•„๋ž˜์™€ ๊ฐ™์ด ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด๋„ ๋˜‘๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.

์ด๋Š” ์žฌ์‚ฌ์šฉ์„ฑ ์ธก๋ฉด์—๋„ ์ข‹์ง€๋งŒ, NameQurey๋Š” ๋นŒ๋“œํ•˜๋Š” ์‹œ์ ์— ์ฟผ๋ฆฌ์˜ ์œ ํšจ์„ฑ์„ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์žฅ์ ๋„ ์žˆ๋‹ค.

public List<Member> findByUsername(String username) {
    // ์ด๋ ‡๊ฒŒ ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•ด๋„ ๋˜์ง€๋งŒ, ์ด๋Š” ์‹คํ–‰ํ•˜๋Š” ์‹œ์ (๋Ÿฐํƒ€์ž„)์— ์ฟผ๋ฆฌ ์œ ํšจ์„ฑ์„ ํŒ๋ณ„ํ•œ๋‹ค.
    return em.createQuery("select m from Member m where m.username = :username")
        .setParameter("username", "์ด๋ฆ„")
        .getResultList();
}
@NameQurey๋Š” ๋นŒ๋“œํƒ€์ž„์— JPQL ์ฟผ๋ฆฌ๋ฌธ์˜ ์˜ค๋ฅ˜๋ฅผ ์ฐพ์•„๋‚ด์ง€๋งŒ, ๊ทธ๋ƒฅ ์ƒ์„ฑํ•˜๋ฉด ์‹คํ–‰ ์‹œ์ ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

 

 

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA์—์„œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด @Query๋ฅผ ์ด์šฉํ•ด์„œ NameQurey๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Query(name = "Member.findByUsername")
    List<Member> findByUsername(@Param("username") String username);

}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@NamedQuery(
    name = "Member.findByUsername",
    query="select m from Member m where m.username = :username" // :username ๋Š” ํŒŒ๋ผ๋ฉ”ํƒ€
)
public class Member {
...
}

 

์žฌ๋ฐŒ๋Š”์ ์€ @Qurey๋ฅผ ์ƒ๋žตํ•˜๋”๋ผ๋„ SpringDataJPA๋Š” NameQurey๋ฅผ ๋จผ์ € ์ฐพ์•„๋ณด๊ณ , ์—†์„ ๋•Œ ๊ธฐ๋ณธ ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.

๋‹ค๋งŒ ์ด๋Ÿฐ ๋ฐฉ๋ฒ•์„ ๊ถŒ์žฅํ•˜์ง€๋„ ์•Š๊ณ , ์• ์ดˆ์— @NameQurey ์ž์ฒด๋„ ์‹ค๋ฌด์—์„œ๋Š” ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋‹ˆ ๋„˜์–ด๊ฐ€๋„๋ก ํ•˜์ž.

 

 


๐Ÿ’ญ @Query(...)

 @NameQuery๋Š” ์‹ค๋ฌด์—์„œ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , ๋ณดํ†ต์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์— ์ฟผ๋ฆฌ๋ฅผ ์ง์ ‘ ์ž‘์„ฑํ•œ๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    
    @Query("select m from Member m where m.username = :username")
    List<Member> findByUsername(@Param("username") String username); // username ์œผ๋กœ ์กฐํšŒ

    @Qurey("select m from Member m where m.username in :names")
    List<Member> findByNames(@Param("names") List<String> names); // ์ปฌ๋ ‰์…˜์œผ๋กœ in ์กฐํšŒ
    
    // @Query๋ฅผ ์“ฐ๋ฉด ์ด๋ ‡๊ฒŒ ๊ธด ์ด๋ฆ„์˜ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.
    List<Member> findByUsernameAndAgeGraterThanAnd...(...);
}

 

 

์ด๋Š” @NameQuery์™€ ๋˜‘๊ฐ™์ด ์ž‘๋™ํ•œ๋‹ค. ์•ฑ ์‹คํ–‰์‹œ์ ์— ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๋ฅผ ๊ฒ€์ฆํ•œ๋‹ค.

๋งŒ์•ฝ Entity๊ฐ€ ์•„๋‹ˆ๋ผ DTO๋กœ ์กฐํšŒํ•˜๊ณ  ์‹ถ์„ ๋•Œ์—๋„, ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    
    @Query("select new mypackage.dto.MemberDto(m.id, m.username, t.name) from Member.m join m.team t")
    List<MemberDto> findMemberDto();
}
@Data
public final class MemberDto {

    private Long id;
    private String username;
    private String teamName;

	// JPQL ์ฟผ๋ฆฌ์— ์ƒ์„ฑ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ, ์ƒ์„ฑ์ž๋Š” ํ•„์ˆ˜.
    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }
}

 

์‹ฌ์ง€์–ด ์•„๋ž˜์™€ ๊ฐ™์ด ์ œ๋„ค๋ฆญ๋„ ์ง€์›ํ•œ๋‹ค.

์ฐธ๊ณ ๋กœ DTO ๋˜ํ•œ @Qurey๋ฅผ ์ƒ๋žตํ•˜๋”๋ผ๋„ ๋˜‘๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค. ๋‹น์—ฐํžˆ ์ƒ์„ฑ์ž ํŒŒ๋ผ๋ฉ”ํƒ€๋Š” ๋งž์ถฐ์ค˜์•ผํ•จ.

 

 

๋งˆ์ง€๋ง‰์œผ๋กœ ๋™์ ์ฟผ๋ฆฌ๋Š” ๋ฌธ์ž์—ด๋กœ ์ž…๋ ฅํ•˜๊ธฐ์—๋Š” ๋„ˆ๋ฌด ๊นŒ๋‹ค๋กญ๋‹ค. ๊ทธ๋ž˜์„œ ๋ณดํ†ต ์ฟผ๋ฆฌ๋นŒ๋”(QueryDSL๋“ฑ)์„ ํ™œ์šฉํ•œ๋‹ค.

์‚ฌ์‹ค ์œ„์—์„œ ์–ธ๊ธ‰ํ•œ ๋ฐฉ๋ฒ•๋“ค๋„ QueryDSL์„ ์‚ฌ์šฉํ•˜๋ฉด ํ›จ์”ฌ ๊ฐ„ํŽธํ•˜๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ธดํ•˜๋‹ค.

๋™์ ์ฟผ๋ฆฌ๊ฐ€..๋ญ์˜€์ฃ ?

๋”๋ณด๊ธฐ

์ž‘์„ฑํ•œ ์ฟผ๋ฆฌ๊ฐ€ ์–ด๋– ํ•œ ์ƒํ™ฉ์—์„œ๋„ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ์ฟผ๋ฆฌ๊ฐ€ ์ •์ ์ฟผ๋ฆฌ๊ฐ€ ๋  ๊ฒƒ์ด๊ณ  ์ž…๋ ฅ๊ฐ’์ด๋‚˜ ํŠน์ • ์ƒํ™ฉ์— ๋”ฐ๋ผ ์ฟผ๋ฆฌ๋ฌธ์ด ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋™์ ์ฟผ๋ฆฌ์ด๋‹ค.

๋™์ ์ฟผ๋ฆฌ

  • ์ฝ”๋“œ ์‹คํ–‰ ์‹œ์ ์— SQL ์ฟผ๋ฆฌ๋ฌธ์ด ๋™์ ์œผ๋กœ ๊ตฌ์„ฑ๋˜๊ณ  ์‹คํ–‰๋˜๋Š” ์ฟผ๋ฆฌ
  • Stored Procedure๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ํ•„์š”์— ๋”ฐ๋ผ ๋™์  ์ฟผ๋ฆฌ๋กœ ์ž‘์„ฑ
  • ๋™์  ์ฟผ๋ฆฌ ์‹คํ–‰ SQL ๋ฌธ์ž์—ด์— ๊ฐ’์ด ๋ฐ”๋€Œ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์–ด ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•˜๊ฒŒ ๋˜๋ฉด SP๊ฐ€ ์ƒˆ๋กœ ์บ์‹ฑ๋˜์–ด ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋–จ์–ดํŠธ๋ฆผ
  • ๋™์ ์ฟผ๋ฆฌ๋Š” EXEC()ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ SP_EXECUTESQL์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹คํ–‰ํ•œ๋‹ค.

์ •์ ์ฟผ๋ฆฌ

  • ์ผ๋ฐ˜์ ์ธ ์ฟผ๋ฆฌ
  • ๋ณ€์ˆ˜์— ๋ฌธ์ž์—ด์„ ๋Œ€์ž…ํ•˜์—ฌ ์ฟผ๋ฆฌ๋ฌธ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹Œ ์ผ๋ฐ˜์ ์œผ๋กœ ์ž‘์„ฑ๋œ SQL ์ฟผ๋ฆฌ
  • ์ •์  ์ฟผ๋ฆฌ๋กœ ์ž‘์„ฑ๋œ Stored Procedure๋Š” ์ƒˆ๋กœ ์บ์‹ฑ๋˜์ง€ ์•Š์•„ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋–จ์–ดํŠธ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
CREATE PROCEDURE TestSP 
	-- Add the parameters for the stored procedure here
    @input       NVARCHAR(50) =null,
    @output     INT                 =0   OUT
AS
-- sp_executesql์„ ์‹คํ–‰ํ•  ๋ณ€์ˆ˜ ์„ ์–ธ
DECLARE @Sql      NVARCHAR(MAX) = ''
DECLARE @Where  NVARCHAR(MAX) = ''
DECLARE @Param  NVARCHAR(MAX) = ''


-- ์ž…๋ ฅ ์ด๋ฆ„์ด ๋“ค์–ด์˜ค๋ฉด WHERE์ ˆ ์ถ”๊ฐ€
IF ISNULL(@input,'') <> '' BEGIN
SET @Where = @Where + N'
    WHERE NAME = @input '
END


BEGIN
	-- SET NOCOUNT ON added to prevent extra result sets from
	-- interfering with SELECT statements.
	SET NOCOUNT ON;

SET @Sql = @Sql + N'
    SELECT @output = AGE
    FROM   dbo.testTable '

SET @Sql = @Sql + @Where

SET @Param = N'
    @input  NVARCHAR(50),
    @output INT OUT '

EXEC SP_EXECUTESQL @Sql
                  ,@Param
                  ,@input  = @input
                  ,@output = @output

 

 


๐Ÿ’ญ ๋ฐ˜ํ™˜ ํƒ€์ž…

์ฐธ๊ณ ๋กœ SpringDataJpa๋Š” ๋‹ค์–‘ํ•œ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์ง€์›ํ•œ๋‹ค.

๋ณดํ†ต์€ ์•„๋ž˜์˜ 3๊ฐ€์ง€. ์ปฌ๋ ‰์…˜, ๊ฐ์ฒด, Optional<๊ฐ์ฒด> ๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•œ๋‹ค.

List<Member> findByUsername(String name); //์ปฌ๋ ‰์…˜
Member findByUsername(String name); //๋‹จ๊ฑด
Optional<Member> findByUsername(String name); //๋‹จ๊ฑด Optional

์ปฌ๋ ‰์…˜์€ ๊ฐ’์ด ์—†์–ด๋„ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€๋Š” ์•Š๊ณ , ๋นˆ ์ปฌ๋ ‰์…˜์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๋‹จ๊ฑด ์กฐํšŒ์ธ๋ฐ ๋งŒ์•ฝ ๊ฒฐ๊ณผ๊ฐ€ 2๊ฑด ์ด์ƒ์ด๋ฉด ์˜ˆ์™ธ(javax.NonUniqueResultException)์ด ๋ฐœ์ƒํ•œ๋‹ค.

๋‹จ๊ฑด ์กฐํšŒ์‹œ ๊ฒฐ๊ณผ๊ฐ€ ์—†์œผ๋ฉด null์ด๋‚˜ Optional.Empty๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๋‹จ๊ฑด ์กฐํšŒ์˜ ๋‚ด๋ถ€ ๋™์ž‘์€ JPA์˜ Query.getSingleResult()๋ฅผ ํ˜ธ์ถœํ•˜๋Š”๋ฐ, ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” `javax.NoResultException` ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. ๋‹จ๊ฑด ์กฐํšŒ์‹œ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ๊ฐ€ ์—†๋Š”๊ฑด ์˜ˆ์™ธ ์ƒํ™ฉ์ด ์•„๋‹ˆ๋ผ, ์ถฉ๋ถ„ํžˆ ์žˆ์„ ์ˆ˜ ์žˆ๋Š” ์ƒํ™ฉ์ด๋‹ค. ๊ทธ๋ž˜์„œ  ๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ ๋งค์šฐ ๋ฒˆ๊ฑฐ๋กœ์šด๋ฐ, ์Šคํ”„๋ง์—์„œ๋Š” ์ด ์˜ˆ์™ธ๋ฅผ ์žก์•„ null๋กœ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•˜๊ณ ์žˆ๋‹ค.

 

์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š”๊ฒŒ ์œ„์— 3๊ฐ€์ง€๋ผ๋Š”๊ฑฐ์ง€, ๊ฑฐ์˜ ๋Œ€๋ถ€๋ถ„์˜ ๋ฐ˜ํ™˜ํƒ€์ž…์„ SpringDataJPA์—์„œ๋Š” ์ œ๊ณตํ•˜๊ณ  ์žˆ๋‹ค.

void, ๊ธฐ๋ณธํƒ€์ž…(int, long), Mono<T>, Flux<T>, Maybe<T>, Future<T>, Stream<T>, Iterator<T>, Map<K,V>..

 

 


๐Ÿ’ญ ํŽ˜์ด์ง•

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ํŽ˜์ด์ง•์€ ์–ธ์ œ๋‚˜ ๊ณจ์น˜์•„ํ”ˆ ๋ฐ˜๋ณต ์ž‘์—…์ด์—ˆ๋‹ค. ๋งค๋ฒˆ ๊ตฌ๊ธ€๋ง์„ ํ†ตํ•ด ์ตœ์ ํ™”๋œ ํŽ˜์ด์ง• SQL์„ ์ฐพ์•„์•ผํ–ˆ๋‹ค.

์ˆœ์ˆ˜ JPA๋Š” ํŽ˜์ด์ง•์„ ๊ฐ„๋‹จํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์คฌ์—ˆ๋‹ค.

๋”๋ณด๊ธฐ
public List<Member> findByPage(int age, int offset, int limit) {
    // ๋“ฑ๋ก๋œ DBMS ์— ์ตœ์ ํ™”๋œ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
    return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
        .setParameter("age", age)
        .setFirstResult(offset) // ์‹œ์ž‘์œ„์น˜
        .setMaxResults(limit) // ์ตœ๋Œ€๊ฐœ์ˆ˜
        .getResultList();
}

public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age ", Long.class)
        .setParameter("age", age)
        .getSingleResult();
}
์ƒ์„ฑ๋œ ์ฟผ๋ฆฌ๋ฌธ
๋‹ค๋ฅธ DBMS์œผ๋กœ ๋ฐ”๊พผ ๊ฒฝ์šฐ. ํ•ด๋‹น DBMS์— ์ตœ์ ํ™”๋œ ํŽ˜์ด์ง• ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋œ๋‹ค.

 

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA์—์„œ๋Š” ํŽ˜์ด์ง• ์ฟผ๋ฆฌ๋ฅผ Sort ์™€ Pageable ์ธํ„ฐํŽ˜์ด์Šค๋กœ ํ‘œ์ค€ํ™” ์‹œ์ผฐ๋‹ค.

// Page, Slice, Sort, Pageable ๋“ฑ์€ org.sptringframework.data.domain.* ์— ์กด์žฌํ•œ๋‹ค.

Page<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์‚ฌ์šฉ
Slice<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์‚ฌ์šฉ์•ˆํ•จ
List<Member> findByUsername(String name, Pageable pageable); //count ์ฟผ๋ฆฌ ์‚ฌ์šฉ์•ˆํ•จ
List<Member> findByUsername(String name, Sort sort); // ์ •๋ ฌ

๋˜ํ•œ Page์™€ Slice๋ผ๋Š” ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์ œ๊ณตํ•ด์ค€๋‹ค.

๋ฐ˜ํ™˜ ํƒ€์ž…์„ Page๋‚˜ Slice๋กœ ๋ฐ›๋Š”๋‹ค๋ฉด ์ถ”๊ฐ€์ ์œผ๋กœ ํ•„์š”ํ•œ ๊ฐ’๋“ค(totalCount ๋“ฑ)์„ ์ฟผ๋ฆฌ๋กœ ๋‚ ๋ฆฐ๋‹ค.

  • ์ปฌ๋ ‰์…˜(List)๋กœ ๋ฐ›์œผ๋ฉด ๊ทธ๋ƒฅ ๊ฒฐ๊ณผ๋งŒ ์ปฌ๋ ‰์…˜์— ๋ฐ›์•„์˜จ๋‹ค.
  • Page ๋Š” Slice๋ฅผ ์ƒ์†๋ฐ›์•„ ๋งŒ๋“  ๊ฐ์ฒด๋กœ, ๊ฒฐ๊ณผ์— count ์ฟผ๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ํฌํ•จํ•œ๋‹ค.
  • Slice ๋Š” ๊ฒฐ๊ณผ์— count ์ฟผ๋ฆฌ ์—†์ด ๋‹ค์ŒํŽ˜์ด์ง€๋งŒ ํ™•์ธํ•œ๋‹ค. (limit+1 ์กฐํšŒ, ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”์—†๋Š” ๊ฒฝ์šฐ. ๋”๋ณด๊ธฐ๋ฒ„ํŠผ)
@Test
public void page() throws Exception {
    //given
    repoistory.save(new Member("member1", 10L)); // Member(name, code)
    repoistory.save(new Member("member2", 10L));
    repoistory.save(new Member("member3", 10L));
    repoistory.save(new Member("member4", 10L));
    repoistory.save(new Member("member5", 10L));

    //when
    PageRequest pageRequest = PageRequest.of(// page, size, Sort ๊ตฌํ˜„์ฒด
        0, 3, Sort.by(Direction.DESC, "username"));
    Page<Member> page = repoistory.findByCode(10L, pageRequest);

    //then
    List<Member> content = page.getContent(); //์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ
    assertThat(content.size()).isEqualTo(3); //์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ ์ˆ˜
    assertThat(page.getTotalElements()).isEqualTo(5); //์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜
    assertThat(page.getNumber()).isEqualTo(0); //ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
    assertThat(page.getTotalPages()).isEqualTo(2); //์ „์ฒด ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ
    assertThat(page.isFirst()).isTrue(); //์ฒซ๋ฒˆ์งธ ํ•ญ๋ชฉ์ธ๊ฐ€?
    assertThat(page.hasNext()).isTrue(); //๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ์žˆ๋Š”๊ฐ€?
}

 

Pageable์„ ์‚ฌ์šฉํ•  ๋–„์—๋Š” org.springframework.data.domain์— ์žˆ๋Š” PageRequest๋ฅผ ๊ตฌํ˜„์ฒด๋กœ ์‚ฌ์šฉํ•œ๋‹ค. 

PageRequest.of( offset, limit )๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๋œ๋‹ค. ํ•„์š”ํ•˜๋‹ค๋ฉด 3๋ฒˆ์งธ ์ธ์ž๋กœ Sort ๋ฅผ ์ด์šฉํ•ด ์ •๋ ฌ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

//when
PageRequest pageRequest = PageRequest.of( 0, 3, Sort.by(Direction.DESC, "username") );
Page<Member> page = repoistory.findByCode(10L, pageRequest);

์ด๋ฅผ ์ด์šฉํ•ด Page<Entity>๋ฅผ ์‰ฝ๊ฒŒ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

๋งŒ์•ฝ ์ •๋ ฌ ๊ธฐ์ค€์ด ๋„ˆ๋ฌด ๋ณต์žกํ•ด์„œ, Sort๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๊นŒ๋‹ค๋กญ๋‹ค๋ฉด ๊ทธ๋ƒฅ @Query(...)๋ฅผ ์ด์šฉํ•ด์„œ ์ง์ ‘ ์ž‘์„ฑํ•˜๋Š”๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค.

 

 

์ฐธ๊ณ ๋กœ Page ๋Š” Slice๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„๋˜์–ด์žˆ๋Š”๋ฐ, ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

public interface Page<T> extends Slice<T> {
    int getTotalPages(); //์ „์ฒด ํŽ˜์ด์ง€ ์ˆ˜
    long getTotalElements(); //์ „์ฒด ๋ฐ์ดํ„ฐ ์ˆ˜
    <U> Page<U> map(Function<? super T, ? extends U> converter); //๋ณ€ํ™˜๊ธฐ
}
public interface Slice<T> extends Streamable<T> {

    int getNumber(); //ํ˜„์žฌ ํŽ˜์ด์ง€
    int getSize(); //ํŽ˜์ด์ง€ ํฌ๊ธฐ
    int getNumberOfElements(); //ํ˜„์žฌ ํŽ˜์ด์ง€์— ๋‚˜์˜ฌ ๋ฐ์ดํ„ฐ ์ˆ˜
    List<T> getContent(); //์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ
    boolean hasContent(); //์กฐํšŒ๋œ ๋ฐ์ดํ„ฐ ์กด์žฌ ์—ฌ๋ถ€
    Sort getSort(); //์ •๋ ฌ ์ •๋ณด
    
    boolean isFirst(); //ํ˜„์žฌ ํŽ˜์ด์ง€๊ฐ€ ์ฒซ ํŽ˜์ด์ง€ ์ธ์ง€ ์—ฌ๋ถ€
    boolean isLast(); //ํ˜„์žฌ ํŽ˜์ด์ง€๊ฐ€ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€ ์ธ์ง€ ์—ฌ๋ถ€
    boolean hasNext(); //๋‹ค์Œ ํŽ˜์ด์ง€ ์—ฌ๋ถ€
    boolean hasPrevious(); //์ด์ „ ํŽ˜์ด์ง€ ์—ฌ๋ถ€

    Pageable getPageable(); //ํŽ˜์ด์ง€ ์š”์ฒญ ์ •๋ณด
    Pageable nextPageable(); //๋‹ค์Œ ํŽ˜์ด์ง€ ๊ฐ์ฒด
    Pageable previousPageable();//์ด์ „ ํŽ˜์ด์ง€ ๊ฐ์ฒด

    <U> Slice<U> map(Function<? super T, ? extends U> converter); //๋ณ€ํ™˜๊ธฐ
}

 

Slice๋Š” count ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ์ง€ ์•Š๊ณ , ์ตœ๋Œ€ ๊ฐœ์ˆ˜(limit)๋ฅผ ๋‹ค ๊ฐ€์ ธ์™”๋‹ค๋ฉด ๋‹ค์ŒํŽ˜์ด์ง€๊ฐ€ ์žˆ๋Š” ๊ฑธ๋กœ ํŒ๋‹จํ•œ๋‹ค.

ํ…Œ์ด๋ธ”์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์•„์„œ totalCount๋ฅผ ์„ธ๋Š” ์ฟผ๋ฆฌ๊ฐ€ ๋ถ€๋‹ด์Šค๋Ÿฝ๋‹ค๋ฉด, Slice๋ฅผ ๋Œ€์‹  ์‚ฌ์šฉํ•ด์„œ ์ฟผ๋ฆฌ๋ฅผ ์ตœ์ ํ™” ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํ…Œ์ด๋ธ” ๋ ˆ์ฝ”๋“œ๊ฐ€ ์˜ค์ง€๊ฒŒ๋งŽ์•„์„œ, Count๊ฐ€ ๋ถ€๋‹ด์Šค๋Ÿฝ๋‹ค๋ฉด Slice๋„ ๊ณ ๋ คํ•ด๋ณด๋ผ

 

Page์™€ Slice์— ์žˆ๋Š” map ๋ฉ”์„œ๋“œ๋Š” ๋‹ค๋ฅธ ๊ฐ์ฒด๋กœ ๋งคํ•‘ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

์•„๋ž˜์™€ ๊ฐ™์ด ๋žŒ๋‹คํ•จ์ˆ˜๋ฅผ ์ด์šฉํ•ด์„œ DTO์— ์‰ฝ๊ฒŒ ๋งคํ•‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

PageRequest pageRequest = PageRequest.of( 0, 3 );
Page<Member> page = repository.findByAge(age, pageReuqest);
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername()));

 

 


๐Ÿ’ญ @Modify, ๋ฒŒํฌ์„ฑ ์ˆ˜์ • ์ฟผ๋ฆฌ

์ˆœ์ˆ˜ JPA์˜ ๋ฒŒํฌ์„ฑ ์ฟผ๋ฆฌ(executeUpdate)๋ฅผ ์ดํ•ดํ•ด์•ผ ์Šคํ”„๋ง์˜ @Modify๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋‹ค.

๋”๋ณด๊ธฐ

JPA๋Š” ์ปค๋ฐ‹ ์‹œ์ ์— Entity์˜ ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•ด์„œ ์ ์ ˆํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

ํ•˜์ง€๋งŒ ์ž‘์—…์„ ํ•˜๋‹ค๋ณด๋ฉด ๋ฒŒํฌ์„ฑ ์ฟผ๋ฆฌ(ex ์žฌ๊ณ ๊ฐ€ 10๊ฐœ ๋ฏธ๋งŒ์ธ ์ƒํ’ˆ ์ „์ฒด์˜ ๊ฐ€๊ฒฉ์„ 20% ์ƒ์Šน) ์ž‘์—…์„ ํ•  ๋•Œ๋„ ์žˆ๋‹ค.

 

SQL๋กœ๋Š” ๊ฐ„๋‹จํ•œ ์ž‘์—…์ด์ง€๋งŒ JPA ๋ณ€๊ฒฝ๊ฐ์ง€๋กœ๋Š” ๋ฆฌ์†Œ์Šค๋„ ๋งŽ์ด ์žก์•„๋จน๊ณ , ์ฟผ๋ฆฌ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฐœ์ˆ˜๋งŒํผ ๋‚˜๊ฐˆ ์ˆ˜ ์žˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ˆœ์ˆ˜ JPA์—์„œ๋„ ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ์œ„ํ•ด executeUpdate() ๋ผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ด์คฌ์—ˆ๋‹ค.

  • ํ•ด๋‹น ๋ฉ”์„œ๋“œ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ง์ ‘ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฐ๋‹ค.
    = ๋ฒŒํฌ์—ฐ์‚ฐ์€ ๋ณ€๊ฒฝ๊ฐ์ง€๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์‚ฌ์šฉํ•˜๋ฉด ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์™€ Database์˜ ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง„๋‹ค.
  • ๊ทธ๋ž˜์„œ ๋ฒŒํฌ์—ฐ์‚ฐ ์ดํ›„์— ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ์ดˆ๊ธฐํ™”(em.clear)ํ•˜๊ณ  ๋‹ค๋ฅธ ์ž‘์—…์„ ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.
    ๊ฐ€๋Šฅํ•˜๋ฉด ๋ฒŒํฌ์—ฐ์‚ฐ์„ ๋จผ์ € ํ•˜๊ณ  em.clear()๋ฅผ ํ•˜๋„๋ก ํ•˜์ž.
    ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ์™„์ „ํžˆ ๋น„์›Œ๋ฒ„๋ฆฌ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ๋‹ค์‹œ ์กฐํšŒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ •ํ•ฉ์„ฑ์ด ๊นจ์ง€์ง€์•Š๋Š”๋‹ค.

์ฐธ๊ณ ๋กœ JPA ๋Š” Update, Delete ์ฟผ๋ฆฌ๋งŒ ์ง€์›ํ•˜๊ณ , ํ•˜์ด๋ฒ„๋„ค์ดํŠธ์—์„œ๋Š” select (insert into select๋ฌธ)๊นŒ์ง€ ์ถ”๊ฐ€ ์ง€์›ํ•œ๋‹ค.

// Update ๋ฒŒํฌ์—ฐ์‚ฐ
String query = "update Product p "+
    "set p.price = p.price * 1.1 " +
    "where p.stockAmount < :stockAmount";

int resultCount = em.createQuery(query)
                    .setParameter("stockAmount", 10)
                    .executeUpdate();
// Delete ๋ฒŒํฌ์—ฐ์‚ฐ. (Select ๋ฒŒํฌ์—ฐ์‚ฐ์€ JPA ์ŠคํŽ™์—๋Š” ์—†๊ณ , ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋งŒ ์ง€์›ํ•œ๋‹ค.)
int deletedCount = em.createQuery("DELETE FROM Country").executeUpdate();

์ฐธ๊ณ ๋กœ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋งŒ ์ง€์›ํ•˜๋Š” ๋ฒŒํฌ์„ฑ insert into select๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฐ์น˜ ์ฟผ๋ฆฌ๋ฅผ ๋งํ•œ๋‹ค.

// Batch
INSERT INTO table1 (col1, col2) VALUES
(val11, val12),
(val21, val22),
(val31, val32);

 

SpringDataJpa์—์„œ๋Š” ๋ฒŒํฌ์„ฑ ์ฟผ๋ฆฌ๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์ด ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // @Modifying ์ด ๋ถ™์–ด์žˆ์œผ๋ฉด executeUpdate()๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
    // ์—†์œผ๋ฉด getResultList(), getSingleResult() ํ˜ธ์ถœ => ์ฟผ๋ฆฌ ์˜ˆ์™ธ(not supported DML) ๋ฐœ์ƒ.
    @Modifying
    @Query("update Member m set m.age = m.age + 1 where m.age >= :age")
    int bulkAgePlus(@Param("age") int age);


}

์ฐธ๊ณ ๋กœ ๋ฒŒํฌ์„ฑ ์ˆ˜์ •, ์‚ญ์ œ์ฟผ๋ฆฌ์— @Modifying์„ ๋ถ™์ด์ง€์•Š์œผ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations

 

๋ฒŒํฌ์—ฐ์‚ฐ์€ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์‹คํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Database์™€ ์ปจํ…์ŠคํŠธ๊ฐ„์˜ ์ •ํ•ฉ์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ๋‹ค.

  • ๊ทธ๋ž˜์„œ ๋ฒŒํฌ ์—ฐ์‚ฐ ์ดํ›„์—๋Š” entityManager.clear()๋ฅผ ์‹คํ–‰ํ•ด์„œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋น„์›Œ๋ฒ„๋ฆฌ๊ณ , DB์—์„œ ์กฐํšŒํ•˜๋ผ๊ณ  ํ–ˆ๋‹ค.

์ด๋Š” @Modifying(clearAutomatically = true)๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ค‘์š”ํ•œ ๊ฑด ์ด ์†์„ฑ์˜ ๊ธฐ๋ณธ๊ฐ’์€ false์ด๋‹ค.

flushAutomatically ๋ผ๋Š” ์˜ต์…˜๋„ ์žˆ๋Š”๋ฐ, ๊ฐ€๋Šฅํ•˜๋ฉด ์ด๊ฑธ ์“ฐ๊ธฐ๋ณด๋‹ค๋Š” ํ•œ ํŠธ๋žœ์žญ์…˜์—์„œ ๋ฒŒํฌ ์—ฐ์‚ฐ์„ ์ œ์ผ ๋จผ์ € ํ•˜๋Š”๊ฒŒ ์ข‹๋‹ค.

 @Modifying(clearAutomatically = true) // ํ•ด๋‹น ์ฟผ๋ฆฌ๋ฌธ ์‹คํ–‰ ์ดํ›„, entityManager.clear() ์‹คํ–‰
 @Modifiying(flushAutomatically = true) // ํ•ด๋‹น ์ฟผ๋ฆฌ๋ฌธ ์‹คํ–‰ ์ด์ „, entityManager.flush() ์‹คํ–‰

 

 

์‚ฌ์‹ค ์ด ๋ถ€๋ถ„์€ ๋ฒŒํฌ์—ฐ์‚ฐ ๋ฟ ์•„๋‹ˆ๋ผ, JDBC ๋‚˜ MyBatis๊ฐ™์€ ์ƒ ์ฟผ๋ฆฌ๋ฅผ JPA์™€ ํ•จ๊ป˜ ์“ธ๋•Œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€์ด๋‹ค.

๊ฐ™์€ ์ด์œ ๋กœ ์Œฉ ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฌ๊ธฐ์ „ flush(๊ฐ€๋Šฅํ•˜๋ฉด ์Œฉ ์ฟผ๋ฆฌ๋ฅผ ์ฒ˜์Œ์œผ๋กœ), ๋‚ ๋ฆฌ๊ณ  ๋‚˜์„œ clear๋ฅผ ํ•ด์ฃผ๋Š”๊ฒŒ ์ข‹๋‹ค.

 


๐Ÿ’ญ@EntityGraph

์ˆœ์ˆ˜ JPA์—์„œ๋Š” ์—ฐ๊ด€๊ด€๊ณ„์ธ ๊ฐ์ฒด๋ฅผ Lazyํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค. ์ด ๋•Œ 1+N ๋ฌธ์ œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด join fetch(ํŒจ์น˜์กฐ์ธ)์„ ์ ์ ˆํ•˜๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

๋”๋ณด๊ธฐ

์—ฐ๊ด€๊ด€๊ณ„์ธ ๊ฐ์ฒด๋ฅผ Lazy๋กœ ์„ค์ •ํ•ด๋†“์œผ๋ฉด, ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ๊นŒ์ง€ ์‹ค์ œ ์ฟผ๋ฆฌ๋ฌธ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.
null ์—ญํ• ์„ ํ•˜๋Š” ํ”„๋ก์‹œ๊ฐ€ ์‚ฝ์ž…๋˜์–ด ์žˆ๋‹ค๊ฐ€, ํ”„๋ก์‹œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์— ์ƒˆ๋กœ์šด ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

์ด๊ฒŒ OneToMany ๊ด€๊ณ„ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค. 1๊ฐœ๋ฅผ ์กฐํšŒํ–ˆ๋Š”๋ฐ ๊ฑฐ๊ธฐ์— ์—ฐ๊ด€๋œ 100๊ฐœ์˜ ๊ฐ์ฒด๊ฐ€ ์กฐํšŒ๋˜๋Š”๋ฐ, JPA ํŠน์„ฑ์ƒ ํ•ด๋‹น ์ฟผ๋ฆฌ๊ฐ€ 100๋ฒˆ ๋‚˜๊ฐ€๊ธฐ ๋•Œ๋ฌธ.

 

๋งŒ์•ฝ member.team.order ๊ฐ™์ด ์—ฐ๊ด€๊ด€๊ณ„์— ๋˜ ๋‹ค๋ฅธ ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์žˆ๋‹ค๋ฉด? ๊ณฑํ•ด์ ธ์„œ ๋ช‡๋งŒ๊ฐœ์˜ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ• ์ง€๋„ ๋ชจ๋ฅธ๋‹ค. ์ด๋ฅผ 1+N ๋ฌธ์ œ๋ผ๊ณ  ๋ถ€๋ฅด๊ณ , ์ด๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ JPA์—์„œ๋Š” join fetch, ํŒจ์น˜์กฐ์ธ์ด๋ผ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

  • select ... from ... join fetch (๊ฐ€์ ธ์˜ฌ ์—ฐ๊ด€๊ด€๊ณ„ ํ•„๋“œ)
  • select m from Member m join fetch m.team
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class).getResultList();
    
// ํŒจ์น˜ ์กฐ์ธ์œผ๋กœ ํšŒ์›์„ ๊ฐ€์ ธ์˜ฌ๋•Œ (์—ฐ๊ด€๋œ ํŒ€ ๊ฐ์ฒด๋ฅผ inner join์œผ๋กœ) ๊ฐ€์ ธ์™€์„œ ์ง€์—ฐ ๋กœ๋”ฉ X
for (Member member : members) {
    log.info("username = " + member.getUsername() + ", " +
        "teamName = " + member.getTeam().name());
}
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();

 

์ฐธ๊ณ ๋กœ join fetch๊ฐ€ ์•„๋‹ˆ๋ผ ๊ทธ๋ƒฅ join๋งŒ ์‚ฌ์šฉํ•ด์„œ๋Š” 1+N๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์ง€ ๋ชปํ•œ๋‹ค.

join ๋งŒ ์‚ฌ์šฉํ•˜๋ฉด ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•œ๋‹ค. ์ฆ‰ DB ์กฐ์ธ ์ฟผ๋ฆฌ์—๋งŒ ์‚ฌ์šฉ๋  ๋ฟ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์—ฐ๊ด€๋œ ๊ฐ์ฒด(Team)์„ ๊ฐ€์ ธ์˜ค์ง€๋Š” ์•Š๋Š”๋‹ค. ๊ฒฐ๊ตญ ์‚ฌ์šฉํ•˜๋Š” ์‹œ์ ์—๋Š” ์ถ”๊ฐ€์ ์ธ ์ฟผ๋ฆฌ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

์‚ฌ์‹ค์ƒ join fetch (ํŒจ์น˜์กฐ์ธ)์˜ ๊ฐ„ํŽธ๋ฒ„์ „์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

 


@EntityGraph๋Š” ์‚ฌ์‹ค์ƒ join fetch (ํŒจ์น˜์กฐ์ธ)์˜ ๊ฐ„ํŽธ๋ฒ„์ „์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋ฉด ๋œ๋‹ค.

JPQL ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์ง€๋งŒ, ๋‚ด๋ถ€ ๊ตฌํ˜„ ์ฝ”๋“œ์˜ ๋™์ž‘์€ ํŒจ์น˜์กฐ์ธ๊ณผ ๊ฐ™๋‹ค.

 

ํ•˜๋‚˜ ๋‹ค๋ฅธ์ ์€ LEFT OUTER JOIN (์•„๋ž˜ ์ฝ”๋“œ์—์„œ Member๋ฅผ ๊ธฐ์ค€์œผ๋กœ LEFT ์กฐ์ธ)์„ ์‚ฌ์šฉํ•œ๋‹ค.

//๊ณตํ†ต ๋ฉ”์„œ๋“œ ์˜ค๋ฒ„๋ผ์ด๋“œ
@Override
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
List<Member> findAll();

//JPQL + ์—”ํ‹ฐํ‹ฐ ๊ทธ๋ž˜ํ”„
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

//๋ฉ”์„œ๋“œ ์ด๋ฆ„์œผ๋กœ ์ฟผ๋ฆฌ์—์„œ ํŠนํžˆ ํŽธ๋ฆฌํ•˜๋‹ค.
@EntityGraph(attributePaths = {"team"}) // left join fetch m.team
List<Member> findByUsername(String username)
@Query("select m from Member m left join fetch m.team") // ํ•ด๋‹น ๊ธฐ๋Šฅ๊ณผ ๋™์ผํ•˜๋‹ค.
List<Member> findMemberFetchJoin();

 

์ž˜ ์‚ฌ์šฉํ•˜์ง€๋Š” ์•Š์ง€๋งŒ, @NameQuery ์ฒ˜๋Ÿผ @NamedEntityGraph๋ผ๋Š” ๊ธฐ๋Šฅ๋„ ์ œ๊ณตํ•ด์ฃผ๊ธด ํ•œ๋‹ค.

@Entity
@NameEntityGraph(name="Member.all", attributeNodes = @NamedAttributeNode("team"))
public class Member{
...
}
@EntityGraph("Member.all")
List<Member> findByUsername(@Param("username") String username);

 

 

๐Ÿ’ญ JPA Hint & Lock

@QueryHints๋Š” JPA ๊ตฌํ˜„์ฒด์— ์ œ๊ณตํ•˜๋Š” ํžŒํŠธ์ด๋‹ค. ๋งˆ์น˜ DB์— SQL ํžŒํŠธ๋ฅผ ์ œ๊ณตํ•ด์„œ ํŠœ๋‹ํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ.

@QueryHints( value = @QueryHint(name = "org.hibernate.readOnly", value ="true") )
Member findReadOnlyByUsername(String username);

 

๋™์ž‘์„ ๊ฐ•์ œํ•˜์ง€๋Š” ์•Š๊ณ , ์ตœ์ ํ™”์— ๋„์›€์„ ์ค€๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด JPA๋Š” ๋ณ€๊ฒฝ๊ฐ์ง€๋ฅผ ์ด์šฉํ•˜์—ฌ Update ์ฟผ๋ฆฌ๋ฌธ์„ ๋ฐ˜์˜์‹œํ‚จ๋‹ค. ์ฆ‰ ๋ณ€๊ฒฝํ•˜๋“  ๋ง๋“  ์ตœ์ดˆ ์กฐํšŒ์‹œ์˜ Entity๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ฐ€, ๋‚˜์ค‘์— ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ์šฉ์œผ๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

 

๋งŒ์•ฝ ์œ„์™€ ๊ฐ™์ด ํ•ด๋‹น ์กฐํšŒ ๊ฑด์ด `readOnly`์ž„์„ ์•Œ๋ ค์ค€๋‹ค๋ฉด ๋ณ€๊ฒฝ๊ฐ์ง€๋ฅผ ๋”ฐ๋กœ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋‹ค๋งŒ ๋Œ€๋ถ€๋ถ„ JPA ์ฟผ๋ฆฌ ์ตœ์ ํ™”๋Š” ๋ณ„ ํšจ๊ณผ๊ฐ€ ์—†์–ด์„œ ๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋Œ€๋ถ€๋ถ„ ๋งŒ๋“ค์–ด์ง„ SQL์˜ ์ฟผ๋ฆฌ๋ฌธ์˜ ๋ฌธ์ œ์ด๋‹ค.

 

 

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—๋Š” ๋น„๊ด€์  Lock, ๋‚™๊ด€์  Lock ์„ ์ง€์ •ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์žˆ๋Š”๋ฐ, JPA๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ œ๊ณตํ•œ๋‹ค.

์ด๋Š” @Lock(...)์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Lock(LockModeType.PESSIMISTIC_WRITE) // ๋˜๋Š” LockModeType.OPTIMISTIC_~~~
List<Member> findByUsername(String name);

 

๋น„๊ด€์ , ๋‚™๊ด€์  lock์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊นŠ๊ฒŒ ๊ณต๋ถ€ํ•˜๋‹ค๋ณด๋ฉด ์•Œ ์ˆ˜์žˆ๋Š” ๋‚ด์šฉ์ด๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ์„ค๋ช…ํ•˜์ž๋ฉด

  • ๋น„๊ด€์ (perssimistic lock) : ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ๋‚œ๋‹ค ๊ฐ€์ •ํ•œ๋‹ค. ์‚ฌ์šฉ์ „ ๋ฝ์„ ๊ฑธ๊ณ , ์ˆ˜์ •ํ•˜๋Š” ์‹œ์ ์— ์ถฉ๋Œ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.
    DB๊ฐ€ ์ œ๊ณตํ•˜๋Š” Lock ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด Select for update ๋“ฑ์ด ์žˆ๋‹ค.
  • ๋‚™๊ด€์ (optimisitc lock) : ํŠธ๋žœ์žญ์…˜ ์ถฉ๋Œ์ด ๋‚˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. ๋ฝ์„ ๊ฑธ์ง€์•Š๊ณ  ์‚ฌ์šฉํ•œ๋‹ค.
    ์‰ฝ๊ฒŒ ๋งํ•ด Lock์„ ๊ฑธ์ง€ ์•Š๋Š”๋‹ค. ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ version ๊ฐ™์€ ๊ฐ’์„ ์ด์šฉํ•ด์„œ ์ถฉ๋Œ์„ ๊ฐ์ง€ํ•œ๋‹ค. ์ปค๋ฐ‹์‹œ์ ์— ์ถฉ๋Œ์ด ๊ฐ์ง€(=version์ด ๋‹ค๋ฆ„)๋œ๋‹ค๋ฉด ๋กค๋ฐฑํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

์„ฑ๋Šฅ์€ ๋‹น์—ฐํžˆ ํŠธ๋žœ์žญ์…˜์— ๋ฝ์„ ๊ฑธ์ง€์•Š๋Š” ๋‚™๊ด€์  ๋ฝ์ด ์ข‹์ง€๋งŒ, ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ ๋ณต๊ตฌ๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€์ ์ธ ์ž‘์—…์„ ํ•ด์ค˜์•ผํ•œ๋‹ค. (๋ณด์ƒ ํŠธ๋žœ์žญ์…˜ ๋“ฑ)

 

๋ฝ์€ DBMS๋งˆ๋‹ค ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์กฐ๊ธˆ์”ฉ ๋‹ค๋ฅด๋‹ค. ๋‚™๊ด€์  ๋ฝ์˜ ๊ฒฝ์šฐ ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์—์„œ๋งŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

์ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๊ณต๋ถ€ํ•˜๋‹ค๋ณด๋ฉด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์•Œ๊ฒŒ๋˜๋Š” ๋‚ด์šฉ์ด๋‹ˆ, ์ง€๊ธˆ์€ ๊ฐ„๋‹จํ•˜๊ฒŒ๋งŒ ์•Œ๊ณ  ๋„˜์–ด๊ฐ€๋„๋ก ํ•˜์ž.

 

 

๐Ÿ’ญ ์‚ฌ์šฉ์ž ์ •์˜ Repository ๊ตฌํ˜„

SpringDataJpa์˜ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋„ˆ๋ฌด ํŽธํ•˜์ง€๋งŒ, ํŠน์ • ๊ตฌํ˜„๋ถ€๋ถ„์„ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค. (์กฐํšŒ ์ฟผ๋ฆฌ๋“ฑ)

  • ์Šคํ”„๋ง JDBC Template, MyBatis๋“ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ
  • QueryDSL๋“ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ

์ด ๋•Œ JpaRepository<Entity, ID> ์˜ ๊ตฌํ˜„์ฒด๋ฅผ ์ง์ ‘ ๋งŒ๋“ค์–ด์ค˜์•ผํ•˜๋Š”๋ฐ, ๋ฉ”์„œ๋“œ๊ฐ€ ์›Œ๋‚™ ๋งŽ์•„์„œ ์‚ฌ์‹ค์ƒ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ์—๋Š” [ ์ธํ„ฐํŽ˜์ด์Šค๋ช… + Impl ] ์ด๋ฆ„์„ ๊ฐ€์ง„ ๊ตฌํ˜„์ฒด๋ฅผ ๋งŒ๋“ค๋ฉด ์›ํ•˜๋Š” ๋ถ€๋ถ„๋งŒ ์ปค์Šคํ…€ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

// 1. ์‚ฌ์šฉํ•˜๋Š” ๋ฉ”์„œ๋“œ์˜ ์ธํ„ฐํŽ˜์ด์Šค๋งŒ ์ •์˜ํ•œ๋‹ค.
public interface MemberRepositoryCustom {
    List<Member> findMemberCustom();
}
//2. ํ•ด๋‹น ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค. ์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์ปค์Šคํ…€ํ•œ๋‹ค.
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {

    private final EntityManager em;
    public MemberRepositoryImpl(EntityManager em) { this.em = em; }

    @Override
    public List<Member> findMemberCustom() {
        return em.createQuery("select m from Member m")
            .getResultList();
    }
}
//3. ๊ทธ ์™ธ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ๋ฉ”์„œ๋“œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ฃผ์ž…๋ฐ›๋Š”๋‹ค.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}

์ฐธ๊ณ ๋กœ Spring Data 2.0 ์ด์ „์—๋Š” [ JpaRepository๊ฐ€ ์—ฐ๊ฒฐ๋œ ์ธํ„ฐํŽ˜์ด์Šค์˜ ์ด๋ฆ„ ] ์„ ์‚ฌ์šฉํ–ˆ์–ด์•ผํ–ˆ๋‹ค.

์ฆ‰ MemberRepositoryImpl ๋กœ ์ด๋ฆ„์„ ์ง€์—ˆ์–ด์•ผ ํ–ˆ๋Š”๋ฐ ์ตœ์‹  ๋ฒ„์ „์—์„œ๋Š” ์œ„ ์˜ˆ์ œ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋” ์ž์—ฐ์Šค๋Ÿฝ๋‹ค. 

 

[ ์ธํ„ฐํŽ˜์ด์Šค๋ช… + Impl ] ์ด๋ฆ„ ๊ทœ์น™์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด SpringDataJpa์—์„œ ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

๊ทธ๋ฆฌ๊ณ  JpaRepository ๊ตฌํ˜„์ฒด์— ์ปค์Šคํ…€ํ•œ ๋ฉ”์„œ๋“œ๊ฐ€ ์ ์šฉ๋˜๋„๋ก ๋งŒ๋“ค์–ด์ค€๋‹ค.

// ์ด๋ ‡๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด, ๋‚ด๊ฐ€ ์ปค์Šคํ…€ํ•œ ๋ฉ”์„œ๋“œ๊ฐ€ ์‹คํ–‰๋œ๋‹ค.
List<Member> result = memberRepository.findMemberCustom();

 

์ฐธ๊ณ ๋กœ ์ด๋ฆ„ ๊ทœ์น™์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ๋‹ค. ๋ฌผ๋ก  ๊ตณ์ด ๋ณ€๊ฒฝ ํ•ด์•ผํ•  ์ด์œ ๋Š” ์—†๋‹ค. ๊ด€๋ก€๋ฅผ ๋”ฐ๋ฅด์ž.

@EnableJpaRepositories(basePackages = "study.datajpa.repository",
                            repositoryImplementationPostfix = "Impl") // ์—ฌ๊ธฐ

 

๋‹ค๋งŒ ์ด๋ ‡๊ฒŒํ•˜๋ฉด MemberRepository๊ฐ€ ๋„ˆ๋ฌด ๋ณต์žกํ•ด์ง„๋‹ค๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค.

๊ทธ๋ž˜์„œ ์•„์˜ˆ ์กฐํšŒ์„ฑ ์ฟผ๋ฆฌ์™€ ์ปค๋งจ๋“œ์„ฑ ์ฟผ๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค. ์ด๋•Œ๋Š” ๊ตณ์ด CustomRepository.. ์ด๋Ÿฐ๊ฑธ ๋งŒ๋“ค์ง€ ๋ง๊ณ , ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํ•˜๋‚˜ ๋” ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

@Repository
public class MemberQueryRepository{

    private final EntityManager em;
    
    List<Member> findAllMembers(){
    	...
    }
}

์ด ๊ฒฝ์šฐ์—๋Š” SpringDataJpa์™€๋Š” ๋ณ„ ๊ด€๋ จ์—†๊ณ , ๊ทธ๋ƒฅ ์Šคํ”„๋ง ๋นˆ์œผ๋กœ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํ•˜๋‚˜ ๋” ๋“ฑ๋กํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

 

 

๐Ÿ’ญ Auditing (๊ฐ์‚ฌ, ๋“ฑ๋ก์‹œ๊ฐ„, ์ˆ˜์ •์‹œ๊ฐ„)

๋Œ€๋ถ€๋ถ„์˜ ํ…Œ์ด๋ธ”์— ๋“ฑ๋ก์ผ, ์ˆ˜์ •์ผ์€ ํ•„์ˆ˜๋กœ ๋“ค์–ด๊ฐ„๋‹ค. ์šด์˜์ƒํ™ฉ์—์„œ๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ฑฐ๋‚˜ ๋งž์ถฐ์•ผํ•˜๋Š” ์ผ์ด ๋งŽ์€๋ฐ, ์ด ๋•Œ ์ด๋Ÿฐ ๊ฐ’์ด ์—†์œผ๋ฉด ๋กœ๊ทธ๋ฅผ ๋‹ค ๋’ค์ง€๋Š” ๋…ธ๊ฐ€๋‹ค๋ฅผ ํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

๊ฑฐ๊ธฐ์— ํ•„์š”์— ๋”ฐ๋ผ ๋“ฑ๋ก์ž, ์ˆ˜์ •์ž๊ฐ€ ๋ˆ„๊ตฐ์ง€๋„ ๊ธฐ๋กํ•˜๊ธฐ๋„ ํ•œ๋‹ค. ์ด๋Ÿฐ ๋ถ€๋ถ„๋“ค์„ Auditing์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

 

์ˆœ์ˆ˜ JPA์—์„œ Auditing์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•

  • @PrePersist / @PostPersist ๋ฉ”์„œ๋“œ
  • @PreUpdate / @PostUpdate ๋ฉ”์„œ๋“œ
๋”๋ณด๊ธฐ

@Entity๋งˆ๋‹ค ์ง์ ‘ ์ ์–ด๋„ ๋˜์ง€๋งŒ, ์•„๋ž˜์™€ ๊ฐ™์ด ๊ณตํ†ต๋œ ๋ถ€๋ถ„์„ @MappedSuperclass๋กœ ๋ฝ‘์•„ ์“ฐ๊ธฐ๋„ ํ•œ๋‹ค.
์ฐธ๊ณ ๋กœ @MappedSuperclass๋Š” ์‹ค์ œ ์ƒ์† ๊ด€๊ณ„๊ฐ€ ์•„๋‹ˆ๊ณ , ์ž๋ฐ”์˜ extend๋ฅผ ์ด์šฉํ•ด ์†์„ฑ๋งŒ ๋ฐ›์•„ ์“ฐ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

public class Member extends JpaBaseEntity {}

์•„๋ž˜์™€ ๊ฐ™์ด ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์˜์†ํ™” ๋˜๊ธฐ์ „/ ์—…๋ฐ์ดํŠธ ๋˜๊ธฐ ์ „์— ํ•  ํ–‰๋™์„ ์ •์˜ํ•  ์ˆ˜ ์žˆ๋‹ค.
์ด๋ฅผ ์ด์šฉํ•ด ์ˆ˜์ •์ผ, ๋“ฑ๋ก์ผ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

@MappedSuperclass
@Getter
public class JpaBaseEntity {

    @Column(updatable = false)
    private LocalDateTime createdDate;
    private LocalDateTime updatedDate;

    @PrePersist
    public void prePersist() {
        LocalDateTime now = LocalDateTime.now();
        createdDate = now;
        updatedDate = now;
    }

    @PreUpdate
    public void preUpdate() {
        updatedDate = LocalDateTime.now();
    }
}

 

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ JPA์—์„œ๋Š” ์ด๋ฅผ ์œ„ํ•ด ๋ช‡๊ฐ€์ง€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์ œ๊ณตํ•ด์ค€๋‹ค.

๋‹จ ์‚ฌ์šฉํ•˜๊ธฐ ์ „ @EnableJpaAuditing์„ ํ™œ์„ฑํ™” ํ•ด์ฃผ์–ด์•ผํ•œ๋‹ค.

@EnableJpaAuditing // JPA Auditing ์• ๋…ธํ…Œ์ด์…˜์„ ํ™œ์„ฑํ™”์‹œํ‚ต๋‹ˆ๋‹ค.
@SpringBootApplication
public class DataJpaApplication {

    public static void main(String[] args) {
        SpringApplication.run(DataJpaApplication.class, args);
    }

}

๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜์˜ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

  • @EntityListeners(AuditingEntityListener.class)
  • @CreatedDate / @LastModifiedDate
  • @CreatedBy / @LastModifiedBy
public class Member extends BaseEntity {}
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@Getter
public class BaseEntity {

    @CreatedDate
    @Column(updatable = false) // ์ด ์„ค์ •์€ ์•ˆํ•ด๋„ ๋˜์ง€๋งŒ, ์ผ์ข…์˜ ์•ˆ์ „์žฅ์น˜.
    private LocalDateTime createdDate;
    
    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

์ฐธ๊ณ ๋กœ ์ตœ์ดˆ ์ƒ์„ฑ ์‹œ์ ์— ์ˆ˜์ •์ผ์ž๋„ ํ•จ๊ป˜ ๊ธฐ๋ก๋œ๋‹ค. ๋ณดํ†ต ์ด๋ ‡๊ฒŒ ํ•˜๋Š”๊ฒŒ ๋ณ€๊ฒฝ์„ ์ถ”์ ํ•˜๊ธฐ ํŽธํ•ด์„œ ์ผ๋ฐ˜์ ์ด๋‹ค.

๋งŒ์•ฝ ๋ฐ”๊พธ๊ณ  ์‹ถ๋‹ค๋ฉด @EnableJpaAuditing(modifyOnCreate = false) ๋กœ ์ˆ˜์ •์ž๋Š” null๋กœ ์‹œ์ž‘ํ•˜๊ฒŒ๋” ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๋‹ค.

 

 

๋‹ค๋งŒ ์‹œ๊ฐ„์€ ๊ทธ๋ ‡๋‹ค ์น˜๋”๋ผ๋„, ์•„๋ž˜์™€ ๊ฐ™์ด ๋“ฑ๋ก์ž, ์ˆ˜์ •์ž๋Š” ์–ด๋–ป๊ฒŒ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ• ๊นŒ?

@LastModifiedBy
private String lastModifiedBy; // JPA๊ฐ€ ์ˆ˜์ •์ž ์ด๋ฆ„์ด ๋ฌด์—‡์ธ์ง€ ์–ด๋–ป๊ฒŒ ์•Œ์ง€?

์ด๋Š” ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ๋“ฑ๋กํ•ด์ฃผ์–ด์—ฌํ•œ๋‹ค. auditorProvider๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•ด์ฃผ๋ฉด ๋œ๋‹ค. ๊ทผ๋ฐ ์ž˜ ์‚ฌ์šฉ์€ ์•ˆํ•  ๋“ฏ.

@Bean
public AuditorAware<String> auditorProvider() {
	// ๋ณดํ†ต ์„ธ์…˜ ์ •๋ณด๋‚˜, ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ID๋ฅผ ๋ฐ›์•„์˜จ๋‹ค.
    return () -> Optional.of(UUID.randomUUID().toString());
}

 

๐Ÿ“‘ ์ข€ ๋” ์‹ค์šฉ์ ์ธ ๋ฐฉ๋ฒ•(@CreationTimestamp)

๊ทผ๋ฐ @EntityListeners๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๊ณผ์ •์ด ๋ฒˆ๊ฑฐ๋กญ๊ธดํ•˜๋‹ค. ๋ฌผ๋ก  ์ „์—ญ์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ์ง€๋งŒ ๊ทธ๋ž˜๋„ ๊ท€์ฐฎ๋‹ค.
META-INF/orm.xml ์„ค์ •์„ ํ†ตํ•ด @EntiyListeners๋ฅผ ์—”ํ‹ฐํ‹ฐ ์ „์ฒด์— ์ž๋™ ์ ์šฉ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•.

๋”๋ณด๊ธฐ
<?xml version=“1.0” encoding="UTF-8”?>

<entity-mappings xmlns=“http://xmlns.jcp.org/xml/ns/persistence/orm”
 xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
 xsi:schemaLocation=“http://xmlns.jcp.org/xml/ns/persistence/
orm http://xmlns.jcp.org/xml/ns/persistence/orm_2_2.xsd”
 version=“2.2">
 <persistence-unit-metadata>
     <persistence-unit-defaults>
         <entity-listeners>
            <entity-listener class="org.springframework.data.jpa.domain.support.AuditingEntityListener”/>
         </entity-listeners>
     </persistence-unit-defaults>
 </persistence-unit-metadata>
 
</entity-mappings>

 

์ด ๋•Œ๋Š” ์‹ค์šฉ์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ JPA๊ฐ€ ์•„๋‹Œ Hybernate์—์„œ ์ œ๊ณตํ•˜๋Š” ์•„๋ž˜ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

  • @CreateTimestamp - ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด INSERT ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ ํ˜„์žฌ ์‹œ๊ฐ„์„ ํ•ด๋‹น ํ•„๋“œ์˜ ๊ฐ’์œผ๋กœ ์ฑ„์šด๋‹ค.
  • @UpdateTimestamp - ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด UPDATE ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ ํ˜„์žฌ ์‹œ๊ฐ„์„ ํ•ด๋‹น ํ•„๋“œ์˜ ๊ฐ’์œผ๋กœ ์ฑ„์šด๋‹ค.

์„ค๋ช…์„ ๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, ํ•ด๋‹น ์–ด๋…ธํ…Œ์ด์…˜๋“ค์€ Spring์ด๋‚˜ JPA์˜ EntityListeners์™€๋Š” ์•„๋ฌด๋Ÿฐ ๊ด€๋ จ์ด ์—†๋‹ค.

ํ•˜์ด๋ฒ„๋„ค์ดํŠธ ์ž์ฒด์—์„œ ์ฟผ๋ฆฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์‹œ์ ์— ํ•„๋“œ์˜ ๊ฐ’์„ ์ฑ„์›Œ์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

@Entity
@Getter
public class Post {
    @Id
    @GeneratedValue
    private Long id;

    @CreationTimestamp // INSERT ์‹œ ์ž๋™์œผ๋กœ ๊ฐ’์„ ์ฑ„์›Œ์คŒ
    @Column(name = "created_at")
    private LocalDateTime createdAt;

    @Column(name = "updated_at")
    @UpdateTimestamp // UPDATE ์‹œ ์ž๋™์œผ๋กœ ๊ฐ’์„ ์ฑ„์›Œ์คŒ
    private LocalDateTime updatedAt;
}

์ฐธ๊ณ ๋กœ LocalDateTime์€ ํ˜„์žฌ ๊ตญ๊ฐ€์— ์ ์šฉ๋˜๋Š” ์‹œ๊ฐ„์ด๊ณ , ์ „์„ธ๊ณ„ ๊ธฐ์ค€์€ ZonedDateTime ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

 

๋ฌผ๋ก  ์ด๋Š” JPA ๊ณต์‹ ๋ช…์„ธ์— ์žˆ๋Š” ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋ผ์„œ, ์‚ฌ์šฉํ•˜๊ธฐ ๊บผ๋ ค์ง€๊ธดํ•œ๋‹ค. ํ•˜์ง€๋งŒ ํ˜„์‹ค์ ์œผ๋กœ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ์—…์€ ์—†๊ณ , ๋‚˜์ค‘์— ๋ณ€๊ฒฝํ•˜๊ธฐ์—๋„ ์–ด๋ ต์ง€ ์•Š๊ธฐ์— ์‚ฌ์šฉํ•ด๋„ ํฐ ์ƒ๊ด€์—†๋‹ค๊ณ  ๋ณธ๋‹ค. 

 

์กฐ๊ธˆ ๋” ๋‚˜์•„๊ฐ€, ๋„๋ฉ”์ธ์— ๊ธฐ์ˆ ์ ์ธ ์˜์กด์„ฑ(hibernate)๊ฐ€ ๋“ค์–ด๊ฐ€๋ฉด ์•ˆ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ทผ๋ฐ ์ƒ๊ฐํ•ด๋ณด๋ฉด JPA ๋˜ํ•œ ์ถ”์ƒํ™”๋œ ๋„๋ฉ”์ธ์ด ์•„๋‹Œ, ๊ธฐ์ˆ ์ ์ธ ์˜์กด์„ฑ์ด๋ผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋งž๋‹ค.

 

ํ•˜์ง€๋งŒ ํ˜„์‹ค์ ์œผ๋กœ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๋„๋ฉ”์ธ์— ๋งคํ•‘ํ•˜๊ธฐ์—๋Š” ๊นŒ๋‹ค๋กญ๊ธฐ์—, ํƒ€ํ˜‘์„ ๋ด์„œ JPA ์ •๋„๊นŒ์ง€๋Š” ๋„๋ฉ”์ธ์— ์ถ”๊ฐ€ํ•ด์„œ ๊ฐ€๋Š” ์‹ค์šฉ์ ์ธ ์„ ํƒ์ง€๋ฅผ ๊ณจ๋ž๋‹ค๋ฉด, hibernate๋ฅผ ํฌํ•จํ•˜๋Š”๊ฒƒ ์ •๋„๋Š” ํ—ˆ์šฉ๊ฐ€๋Šฅํ•œ ์ˆ˜์ค€์ด๋ผ๊ณ  ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

 


๐Ÿ’ญ Web ํ™•์žฅ๊ธฐ๋Šฅ1 - ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ ์ปจ๋ฒ„ํ„ฐ

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด ์“ฐ์ง€๋งˆ๋ผ. ์“ธ ์ผ ์—†๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

๋ฌผ๋ก  ํŠธ๋žœ์žญ์…˜์ด ์—†๋Š” ๋ฒ”์œ„์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ–ˆ์œผ๋ฏ€๋กœ, ๋ณ€๊ฒฝํ•ด๋„ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋‚˜ DB์— ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š”๋‹ค. ์—†์œผ๋‹ˆ๊นŒ!

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/members/{id}") 
    public String findMember(@PathVariable("id") Member member) {
        // ArgumentResolver์—์„œ JpaRepository๋ฅผ ์ด์šฉํ•ด์„œ ๊บผ๋‚ด์˜จ๋‹ค.
        return member.getUsername();
    }
}

 

 

๐Ÿ’ญ Web ํ™•์žฅ๊ธฐ๋Šฅ2 - ํŽ˜์ด์ง•๊ณผ ์ •๋ ฌ

ArgumentResolver์—์„œ Pageable์„ ์ƒ์„ฑํ•ด์„œ ๋ฐ›์„ ์ˆ˜๋„ ์žˆ๋‹ค.

์š”์ฒญ URI /members? page=0 & size=3 & sort=id,desc & sort=username,desc
page : ํ˜„์žฌํŽ˜์ด์ง€ offset (0๋ถ€ํ„ฐ ์‹œ์ž‘)
size : ํ•œ ํŽ˜์ด์ง€์— ๋…ธ์ถœํ•  ๊ฑด ์ˆ˜, limit
sort : ์ •๋ ฌ ์กฐ๊ฑด (๊ธฐ๋ณธ asc)
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

 

์ฐธ๊ณ ๋กœ URI ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฉ”ํƒ€๋ฅผ ์ƒ๋žตํ•˜๋ฉด ์Šคํ”„๋ง ๋ถ€ํŠธ์— ์žˆ๋Š” ๊ธฐ๋ณธ ์„ค์ • ๊ฐ’์„ ์‚ฌ์šฉํ•œ๋‹ค.

spring.data.web.pageable.default-page-size=20 /# ๊ธฐ๋ณธ ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ/
spring.data.web.pageable.max-page-size=2000 /# ์ตœ๋Œ€ ํŽ˜์ด์ง€ ์‚ฌ์ด์ฆˆ/

 

์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฉ”ํƒ€๋ฅผ ์ƒ๋žตํ–ˆ์„ ๋•Œ ๋”ฐ๋กœ ๊ธฐ๋ณธ๊ฐ’์„ ์ง€์ •ํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค. ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.

@GetMapping("/members_page")
public String list(@PageableDefault(size = 12, sort = "username",
    direction = Sort.Direction.DESC) Pageable pageable) {
	...
}

 

ํ•œ๋ฒˆ์— ์—ฌ๋Ÿฌ๊ฐ€์ง€ Pageable์„ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ•๋„ ์žˆ๋‹ค. ์ฟผ๋ฆฌ์— ์ ‘๋‘์‚ฌ๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ๋œ๋‹ค.

 /members? member_page=0 & order_page=1
 
 {์ ‘๋‘์–ด}_page, {์ ‘๋‘์–ด}_size... ์ด๋Ÿฐ ์‹์œผ๋กœ ์ง€์ •ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.
@GetMapping("/members")
public String list(
 @Qualifier("member") Pageable memberPageable,
 @Qualifier("order") Pageable orderPageable){
 	...
 }

 

์•ž์—์„œ ๋ฐฐ์šด Page.map, Slice.map์™€ ํ•จ๊ป˜ ์ด์šฉํ•˜๋ฉด ๊ฝค๋‚˜ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Pageable์„ ํŒŒ๋ผ๋ฉ”ํƒ€๋กœ ๋ฐ›์•„์„œ, EntityDTO๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ฝ”๋“œ.

@GetMapping("/members")
public Page<MemberDto> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    Page<MemberDto> pageDto = page.map(MemberDto::new);
    return pageDto;
}

@GetMapping("/members") // ํ•œ์ค„๋กœ ์ž‘์„ฑํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋œ๋‹ค.
public Page<MemberDto> list(Pageable pageable) {
    return memberRepository.findAll(pageable).map(MemberDto::new);
}

 

๐Ÿ“‘ ํ˜น์‹œ Page๋ฅผ 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋„๋ก ์„ค์ •์„ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‚˜์š”?

์Šคํ”„๋ง ๋ฐ์ดํ„ฐ๋Š” Page๋ฅผ 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•œ๋‹ค.

๋ณดํ†ต์€ ๊ทธ๋ƒฅ 0 ๊ทธ๋Œ€๋กœ ์“ฐ๊ณ  ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜๋ฆฌํ•˜๊ธด ํ•˜์ง€๋งŒ, ์•„์˜ˆ ์„œ๋ฒ„์—์„œ page=1๋กœ ์ „๋‹ฌํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๋‹ค.

 

๊ทธ๋Ÿด๋• Page๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜์ง€๋ง๊ณ , MyPage<MemberDto> ์ด๋Ÿฐ์‹์œผ๋กœ DTO๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฑธ ๊ถŒ์žฅํ•œ๋‹ค.

@GetMapping("/members") // ํ•œ์ค„๋กœ ์ž‘์„ฑํ•˜๋ฉด ์ด๋ ‡๊ฒŒ ๋œ๋‹ค.
public MyPage<MemberDto> list(Pageable pageable) {
    Page<MemberDto> = memberRepository.findAll(pageable).map(MemberDto::new);
	
    /*.. Page<MemberDto> โžก MyPage<MemberDto> ๋ณ€ํ™˜ ..*/
    
    return MyPage<MemberDto>
}

 

์Šคํ”„๋ง ์ž์ฒด์— page๋ฅผ 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์˜ต์…˜์ด ์žˆ๊ธดํ•˜๋‹ค.

๊ทผ๋ฐ ์‚ฌ์šฉํ–ˆ๋‹ค๊ฐ€๋Š” ๋‚˜์ค‘์— ํด๋ผ์ด์–ธํŠธ์—์„œ ์‹ค์ˆ˜ํ•˜๊ธฐ ์‰ฌ์›Œ์„œ ์ถ”์ฒœํ•˜์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

# resource/application.yml
spring:
  data:
    web:
      pageable.one-indexed-parameters = true

์™œ๋ƒํ•˜๋ฉด ์ด๋Š” ์ปจํŠธ๋กค๋Ÿฌ ์ž…์ถœ๋ ฅ์—์„œ๋งŒ page๊ฐ€ -1 ๋  ๋ฟ, ์‹ค์ œ ํŽ˜์ด์ง€ ๊ฐ์ฒด์•ˆ์˜ ๊ฐ’์€ ๊ทธ๋Œ€๋กœ์ด๊ธฐ ๋•Œ๋ฌธ. ์‚ฌ์šฉ์ด ๋ฒˆ๊ฑฐ๋กญ๋‹ค.

https://mysite/members?page=0 (page 0์„ ์ „๋‹ฌํ•ด๋„ page 1๋กœ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.)
https://mysite/members?page=1 
{
     "id": 1 // ์ฒซ๋ฒˆ์งธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ์ „๋‹ฌ๋˜์—ˆ๋‹ค.
     "content": [...],
     "pageable": {
         "offset": 0,
         "pageSize": 10,
         "pageNumber": 0 // ??? ๋„Œ ์™œ 0์ด๋‹ˆ
     }
}

https://mysite/members?page=2
{
     "id": 2 // ๋‘๋ฒˆ์งธ ๋ฐ์ดํ„ฐ๊ฐ€ ์ž˜ ์ „๋‹ฌ๋˜์—ˆ๋‹ค.
     "content": [...],
     "pageable": {
         "offset": 0,
         "pageSize": 10,
         "pageNumber": 1 // ????
     }
}

 

 

๐Ÿงจ SpringDataJpa์—์„œ .save(entity)๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์ฃผ์˜ํ• ์ .

์ด๋Š” SimpleJpaRepository.save() ์˜ ๊ตฌํ˜„์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ๋ฐ”๋กœ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

  • ์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด merge๋กœ ์ €์žฅํ•œ๋‹ค. ( Detached๋œ ๊ฒฝ์šฐ, ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ID๋งŒ ๋‚จ์•„์žˆ๋Š” ๊ฒฝ์šฐ)
@Transactional
@Override
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

JPA๋ฅผ ๋ฐฐ์› ๋‹ค๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, merge๋Š” ์„ฑ๋Šฅ์ƒ ์ข‹์ง€ ์•Š๋‹ค. ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์€ JPA์˜ ๋ณ€๊ฒฝ๊ฐ์ง€๋กœ ํ•˜๋Š”๊ฒŒ ๋งž๋‹ค.
์• ์ดˆ์— merge๋Š” ์ค€์˜์† -> ์˜์†์œผ๋กœ ๋ฐ”๊ฟ€ ๋•Œ ์“ฐ๋Š”๊ฑฐ์ง€, ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋Šฅ์ด ์•„๋‹ˆ๋‹ค.

  • persist()๋Š” ์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ๋ฅผ DB insert ์ฟผ๋ฆฌ๋กœ ์ €์žฅํ•œ๋‹ค.
  • merge()๋Š” ๋จผ์ € DB select๋ฅผ ๋‚ ๋ ค์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ฐ€์ ธ์˜จ ํ›„, ๊ฑฐ๊ธฐ์— ๊ฐ’์„ ์ฑ„์›Œ์„œ ์ƒˆ๋กœ์šด update ์ฟผ๋ฆฌ๋ฅผ ๋‚ ๋ฆฐ๋‹ค.
    ์ •ํ™•ํžˆ๋Š” ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ํ•ด๋‹น id๊ฐ€ ์žˆ๋‹ค๋ฉด update ์ฟผ๋ฆฌ๋งŒ ์‹คํ–‰ํ•˜๊ณ , ์—†์„๋•Œ๋งŒ DB select๋ฅผ ์‹œ๋„ํ•œ๋‹ค.

 

์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ์ธ์ง€ ํŒ๋ณ„ํ•˜๋Š” entityInformation.isNew(..)๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ๋™์ž‘ํ•œ๋‹ค.

  • Entity.getId() == null ( ๋งŒ์•ฝ ๊ฐ์ฒด๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด 0 )์ด๋ผ๋ฉด ์ƒˆ๋กœ์šด ๊ฐ์ฒด๋กœ ํŒ๋‹จ, true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
@Entity
@Getter
class Item{
    @Id
    @GeneratedValue
    private Long id; // ์—ฌ๊ธฐ์— ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ, ์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ๋กœ ์ทจ๊ธ‰
}

@GeneratedValue๋กœ ์ƒ์„ฑ๋œ ์•„์ด๋””๋Š” em.persist() ๋ฉ”์„œ๋“œ์—์„œ ์ฃผ์ž…๋œ๋‹ค. ๊ทธ๋ž˜์„œ merge๊ฐ€ ๋ฐœ์ƒํ•  ์ผ์€ ์—†๋‹ค.

 

ํ•˜์ง€๋งŒ @GenratedValue๋ฅผ ์“ฐ์ง€ ์•Š๊ณ  ์—”ํ‹ฐํ‹ฐ ID๋ฅผ setter๋กœ ์ง์ ‘ ์„ค์ •ํ•˜๋Š”๊ฒฝ์šฐ, persist๊ฐ€ ์•„๋‹Œ merge๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.

๋‹น์—ฐํžˆ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์—๋Š” ๋ฏธ๋ฆฌ ์ €์žฅ๋œ ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์—†์œผ๋ฏ€๋กœ ์“ธ๋•Œ์—†์ด ์ฟผ๋ฆฌ๊ฐ€ 2๋ฒˆ๋‚˜๊ฐ„๋‹ค. (select -> update)

 

๋ฌธ์ œ๋Š” ์ฟผ๋ฆฌ๊ฐ€ 2๋ฒˆ ๋‚˜๊ฐ€๋Š” ๊ฑด ์ถ”์ ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์™œ ์ด๋Ÿฐ ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜๋Š”์ง€ ์ฐพ๊ธฐ ๊นŒ๋‹ค๋กญ๋‹ค.

์• ์ดˆ์— ์ •๋ง ํŠน์ˆ˜ํ•œ ์ƒํ™ฉ์ด ์•„๋‹ˆ๋ผ๋ฉด ํ˜„์—…์— ์ค€์˜์† ์—”ํ‹ฐํ‹ฐ๋ฅผ merge() ํ•  ์ผ์ด ์—†์–ด์„œ ์ž˜ ๋ชจ๋ฅด๋Š” ๊ฒƒ๋„ ์žˆ๋‹ค.

 

๐Ÿ“‘ ๊ทธ๋Ÿผ Id๋ฅผ ์ง์ ‘ ์ง€์ •ํ• ๋•Œ์—๋Š” ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜๋‚˜์š”?

์•„๋ž˜์™€ ๊ฐ™์ด interface Persistable<ID>๋ฅผ ๊ตฌํ˜„ํ•ด์„œ isNew() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ํ•ด์ฃผ์ž.

๊ทธ๋Ÿผ ์ƒˆ๋กœ์šด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ €์žฅํ•  ๋•Œ merge๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ์‹ค์ˆ˜๋ฅผ ๋ง‰์„ ์ˆ˜ ์žˆ๋‹ค.

import org.hibernate.annotations.CreationTimestamp;
import org.springframework.data.domain.Persistable;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member implements Persistable<Long> {

    @Id
    private Long id;

    @CreationTimestamp
    private LocalDateTime createdDate;

    void setId(Long id) {
        this.id = id;
    }

    @Override
    public boolean isNew() {
        /* entityInformation.isNew() ๋กœ์ง์„ ์ง์ ‘ ์ž‘์„ฑ */
        return createdDate == null;
    }
}

 

 

๐Ÿ’ญ ์ž˜ ์‚ฌ์šฉํ•˜์ง€์•Š๋Š” ๊ธฐํƒ€๊ธฐ๋Šฅ๋“ค

์‹ค๋ฌด์—์„œ ๊ฑฐ์˜ ์‚ฌ์šฉํ•  ์ผ ์—†์ง€๋งŒ, ์Šคํ”„๋ง ๊ณต์‹๋ฌธ์„œ์—๋Š” ์žˆ๋Š” ๊ธฐ๋Šฅ๋“ค์ด๋‹ค.

QureyDSL์ด ๋„ˆ๋ฌด ํŽธ๋ฆฌํ•˜๊ธฐ๋„ ํ•˜๊ณ , ๊ธฐ๋Šฅ ์ž์ฒด์— ์กฐ๊ธˆ์”ฉ ํ•˜์ž(?)๊ฐ€ ์žˆ์–ด์„œ ๊ทธ๋ ‡๋‹ค. ์žฌ๋ฏธ๋กœ ๋ณด๊ณ  ๋„˜์–ด๊ฐ€์ž. 

 

๐Ÿ“‘ native ์ฟผ๋ฆฌ

JPA์—์„œ๋Š” SQL์„ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” native ์ฟผ๋ฆฌ๋ฅผ ์ œ๊ณตํ•ด์ค€๋‹ค.

์Šคํ”„๋ง JPA์—์„œ๋„ @Qurey(nativeQurey=true)๋กœ ์ถ”์ƒํ™”ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.

๊ณผ๊ฑฐ์—๋Š” ๊ตฌ๋ ค์„œ ๊ฑฐ์˜ ์‚ฌ์šฉ์„ ์•ˆํ–ˆ์ง€๋งŒ ์ตœ๊ทผ์—๋Š” ๋‚˜๋ฆ„ ์“ธ๋งŒํ•ด์กŒ๋‹ค. ๋ฌผ๋ก  ๊ทธ๋ž˜๋„ QueryDSL ์“ฐ์ง€ ์ด๋Ÿฐ๊ฑฐ ์•ˆ์“ด๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    // ์ด๋ ‡๊ฒŒ DB SQL์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    @Query(value = "select * from member where username = ?", nativeQuery = true)
    Member findByNativeQuery(String username);

}

์•ฑ ์‹คํ–‰์‹œ์ ์— ํ•ด๋‹น ์ฟผ๋ฆฌ์˜ ์œ ํšจ์„ฑ์„ ์•Œ ์ˆ˜ ์—†๊ณ , JPQL๊ณผ ์Œฉ SQL์˜ ์ฐจ์ด๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋งค์šฐ ๋ณต์žกํ•˜๋‹ค.

ํ•˜์ง€๋งŒ ๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  '์“ธ๋งŒํ•ด์กŒ๋‹ค'๊ณ  ๋งํ•œ ์ด์œ ๋Š”, [์ธํ„ฐํŽ˜์ด์Šค DTO]์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ๋‚˜๋ฆ„ ์œ ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {

    @Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
        "FROM member m left join team t",
        countQuery = "SELECT count(*) from member", // JPA ์ฟผ๋ฆฌ๊ฐ€ ์•„๋‹˜. count ์ฟผ๋ฆฌ ํ•„์ˆ˜
        nativeQuery = true)
    Page<MemberProjection> findByNativeProjection(Pageable pageable);

}

 

ํ•˜์ง€๋งŒ QueryDSL์ด ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋Ÿฐ ๊ฑฐ ์•ˆ์“ด๋‹ค.

๊ทนํ•œ์˜ ์กฐํšŒ ์„ฑ๋Šฅ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ทธ๋ƒฅ ์Œฉ์ฟผ๋ฆฌ - JdbcTemplate ์ด๋‚˜ MyBatis, jooq ๊ฐ™์€ ์™ธ๋ถ€๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ์„ ๊ถŒ์žฅํ•œ๋‹ค.

์‚ฌ์‹ค Hadoop & Spark & kafka ์กฐํ•ฉ์ด ๋‚˜์˜จ ๋’ค๋กœ๋Š” ์—„์ฒญ๋‚˜๊ฒŒ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ๋ฅผ ์งค ์ผ์ด ๋งŽ์ด ์ค„์—ˆ๊ธด ํ•˜๋‹ค.

 

 

๐Ÿ“‘ DTO Projections (DTO ํ”„๋กœ์ ์…˜)

์ด๊ฑด ์‹ ๊ธฐ๋Šฅ์ด๋ผ๊ธฐ๋ณด๋‹จ, Repository์—์„œ Entity๊ฐ€ ์•„๋‹Œ ์ปค์Šคํ…€๊ฐ์ฒด(EntityDTO)๋ฅผ ๋ฐ˜ํ™˜๋ฐ›๋Š” ๊ฑธ ์˜๋ฏธํ•œ๋‹ค.

 

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜๋ฉด, ๋‚˜๋ฆ„ ์œ ์šฉํ•œ ๊ธฐ๋Šฅ์ด๋‹ค.

๋‹ค๋งŒ ๋‹จ์ ์ด DTO ํ”„๋กœ์ ์…˜ ๋Œ€์ƒ์ด root ์—”ํ‹ฐํ‹ฐ๋ฉด ์ƒ๊ด€์—†๋Š”๋ฐ, ๊ทธ๊ฒŒ ์•„๋‹ˆ๋ผ๋ฉด JPQL select ์ตœ์ ํ™”๊ฐ€ ์•ˆ๋œ๋‹ค.

( Item - ItemOption - Option )์—์„œ Item ์—”ํ‹ฐํ‹ฐ ์ •๋„๋Š” ๊ดœ์ฐฎ์ง€๋งŒ, ๊ทธ ๋ฐ‘์— ์žˆ๋Š”๊ฑด ์ตœ์ ํ™”๊ฐ€ ์•ˆ๋œ๋‹ค๋Š” ๋ง.

 

๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ, DTO ํ”„๋กœ์ ์…˜์€ ์กฐํšŒ์„ฑ๋Šฅ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ๋ƒฅ QureyDSL์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

์„ฑ๋Šฅ์ด ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ๊ตณ์ด DTO๋กœ ๋ฐ›๊ธฐ๋ณด๋‹จ, ๊ทธ๋ƒฅ Entity๋ฅผ ๋ฐ›์•„์„œ ๋ณ€ํ™˜ํ•˜๋Š”๊ฒŒ ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ ๋” ๋‚˜์„ ์ˆ˜ ์žˆ๋‹ค.

 

๊ฐ„๋‹จํ•˜๊ฒŒ ์†Œ๊ฐœ๋ฅผ ํ•ด๋ณด์ž๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ DTO๋กœ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

// 1. ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋งŒ๋“ ๋‹ค.
public interface UsernameOnly {
    String getUsername();
}
// 2. ๊ทธ๋ƒฅ ์“ด๋‹ค. (ํ”„๋ก์‹œ ๊ฐ์ฒด๊ฐ€ ์‚ฝ์ž…๋œ๋‹ค.)
public interface JpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
    
    List<UsernameOnly> findByUsername(String username);

}

 

์ด ๊ธฐ๋Šฅ์€ interface ๋ฟ ์•„๋‹ˆ๋ผ Class๋Š” ๋‹น์—ฐํžˆ ๋œ๋‹ค. ์‹ฌ์ง€์–ด๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์ œ๋„ค๋ฆญ๊นŒ์ง€ ์ง€์›ํ•œ๋‹ค.

์ œ๋„ค๋ฆญ ์ฒ˜๋Ÿผ ํƒ€์ž…์— ๋”ฐ๋ผ ๋‹ค๋ฅด๊ฒŒ ๊ฐ’์ด ์ฑ„์›Œ์ง€๋Š” ๊ฒƒ์„ Dynamic Projection์ด๋ผ๊ณ  ํ•œ๋‹ค. (๋™์  ํ”„๋กœ์ ์…˜)

// ์ฐธ๊ณ ๋กœ find(...)By ์ด ์‚ฌ์ด์—๋Š” ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์ด๋ฆ„์„ ๋„ฃ์œผ๋ฉด ๋œ๋‹ค.
<T> List<T> findProjectionsByUsername(String username, Class<T> type);

List<UsernameOnly> result = memberRepository.findProjectionsByUsername("m1", UsernameOnly.class);

 

์ค‘์ฒฉ ๊ตฌ์กฐ๋„ ์ง€์›ํ•œ๋‹ค. 

public interface NestedClosedProjection {

    String getUsername();
    TeamInfo getTeam();
 
    interface TeamInfo {
        String getName();
    }
}
/*
์ฐธ๊ณ ๋กœ Member, Team์„ ์กฐ์ธํ•ด์„œ ํ•„์š”ํ•œ๊ฑฐ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค.
select
    m.username as col_0_0_,
    t.teamid as col_1_0_,
    t.teamid as teamid1_2_,
    t.name as name2_2_ 
from
    member m 
left outer join
    team t 
        on m.teamid=t.teamid 
where
    m.username=?
*/

 

 

SpEL ๋ฌธ๋ฒ• ์ด์šฉํ•ด์„œ ์ƒˆ๋กœ์šด ๊ฐ’์„ ๋งŒ๋“ค์ˆ˜ ์žˆ๋‹ค. (*๋‹ค๋งŒ DB์—์„œ ํ•„๋“œ๋ฅผ ์กฐํšŒํ•œ ํ›„์— ๊ฐ€๊ณตํ•˜๋Š”๊ฑฐ๋ผ, ์ฟผ๋ฆฌ ์ตœ์ ํ™”๋Š” ์•ˆ๋œ๋‹ค.)

์ฐธ๊ณ ๋กœ ์ฟผ๋ฆฌ๋ž‘ DTO๋ž‘ ์ •ํ™•ํ•˜๊ฒŒ ์ผ์น˜ํ•˜๋ฉด close projection, ํ•„์š”์—†๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋” ๊ฐ€์ ธ์˜ค๋ฉด open projection๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

public interface UsernameOnly {

    @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}")
    String getUsername();
}

 

 

๐Ÿ“‘ Predicate ๊ธฐ๋Šฅ (*DDD ์˜ Specifications)

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด ์•ˆ์“ด๋‹ค. JPA Criteria๋ฅผ ์ถ”์ƒํ™”ํ•ด์„œ ๋งŒ๋“  ๊ธฐ๋Šฅ์ธ๋ฐ, Criteria๋ฅผ ์•„๋ฌด๋„ ์•ˆ์จ์„œ ์ด๊ฒƒ๋„ ํ•จ๊ป˜ ์•ˆ์“ด๋‹ค.

 

DDD์—์„œ ์—๊ทธ๋ฆฌ๊ฑฐํŠธ๊ฐ€ ํŠน์ • ์กฐ๊ฑด(์ŠคํŽ™)์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•  ๋•Œ, Specification์„ ์ถ”์ƒํ™”ํ•˜์—ฌ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค.

public interface Specification<T> {
    public boolean isSatisfiedBy(T agg);
}
public class OrdererSpec implements Specification<Order> {
    private String ordererId;
    public OrdererSpec(String ordererId) {
        this.overerId = ordererId;
    }

    @Override
    public boolean isSatisfiedBy(Order agg) {
        return agg.getOrdererId().getMemberId().getId().equals(ordererId);
    }
}

 

์ด๋Ÿฌ๋ฉด ์ด Specification๋“ค์„ ๋…ผ๋ฆฌ์—ฐ์‚ฐ (and, or)์œผ๋กœ ์กฐํ•ฉํ•˜๊ฑฐ๋‚˜, ์•„๋ž˜์™€ ๊ฐ™์ด ๋งŒ์กฑํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๋ฅผ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์ข‹๋‹ค.

Specification<Order> ordererSpec = new OrdererSpec("madvirus");
List<Order> orders = orderRepository.findAll(ordererSpec);

AndSpec<T> spec = new AndSpec(ordererSpec, orderDateSpec); // Spec1 And Spec2
List<Order> orders = orderRepository.findAll(spec);
public class MemoryOrderRepository implements OrderRepository {
    public List<Order> findAll(Specification spec) {
        List<Order> allOrders = findAll(); // ํ•ด๋‹น ์ŠคํŽ™์„ ๋งŒ์กฑํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ๋งŒ ๋ฐ˜ํ™˜.
        return allOrders.stream().filter(order -> spec.isSatisfiedBy(order)).collect(toList());
    }
}

 

SpringDataJpa๋Š” JPA Criteria๋ฅผ ํ™œ์šฉํ•ด์„œ ๋งŒ๋“  Predicate๋ผ๋Š” ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.

๋‹ค๋งŒ Jpa Criteria๊ฐ€ ๋„ˆ๋ฌด ๊ตฌ๋ ค์„œ ์•„๋ฌด๋„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ์— ํ•จ๊ป˜ ๋ฌปํ˜€๋ฒ„๋ฆฐ ์•ˆํƒ€๊นŒ์šด ๊ธฐ๋Šฅ

 

Specification<> ๊ณผ JpaSpecificationExecutor<>๋“ฑ์„ ์ด์šฉํ•ด์„œ ์œ„์— ์˜ˆ์ œ์ฒ˜๋Ÿผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฒ‰๋ณด๊ธฐ์—” ํŽธํ•˜๊ณ  ์‹ ๋ฐ•ํ•˜๊ธด ํ•˜์ง€๋งŒ, ์ด๋ฅผ ์“ฐ๊ณ ์‹ถ์œผ๋ฉด JPA Criteria ์„ ๋ฐฐ์›Œ์•ผํ•œ๋‹ค. ๋„ˆ๋ฌด ๋ณต์žกํ•˜๊ณ  ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

// org.springframework.data.jpa.domain.Specification
Specification<Member> spec = MemberSpec.teamName("A").and( MemberSpec.username("kim") );
List<Member> result = memberRepository.findAll(spec);

๋Œ€์ถฉ ์ด๋Ÿฐ์‹์œผ๋กœ ์กฐ๊ฑด ๊ฐ์ฒด์ธ Specification.toPredicate(...)์„ ๊ตฌํ˜„ํ•ด์„œ ์‚ฌ์šฉํ•œ๋‹ค.

 

 

 

๐Ÿ“‘ Qurey By Example (์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์‹ ๊ธฐ๋Šฅ)

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด ์•ˆ์“ด๋‹ค. ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ๋™์ ์ฟผ๋ฆฌ๋ฅผ Example๊ฐ์ฒด๋กœ ์ž๋™ ์ƒ์„ฑํ•ด์ฃผ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

 

๋‹ค๋งŒ ์ข‹์€์ ์€ JpaRepository์— ์ด๋ฏธ ํฌํ•จ๋˜์–ด์žˆ์–ด์„œ, ๋ณ„๋„์˜ ๊ตฌ์„ฑ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

interface JpaRepository๋ฅผ ๊นŒ๋ณด๋ฉด ์œ„์—์„œ ์–ธ๊ธ‰ํ•˜์ง€์•Š์€ QueryByExampleExecutor<T> ๋ผ๋Š”๊ฒŒ ์žˆ๋‹ค.

์—ฌ๊ธฐ์žˆ์ง€๋กฑ

 

์ด๋ฅผ ์ด์šฉํ•ด์„œ ๋™์ ์ฟผ๋ฆฌ๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ด์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

Member member = new Member("m1");
Example<Member> example = Example.of(member); // Example์„ ์ƒ์„ฑํ•œ๋‹ค.

// Member("m1")๊ณผ ๋™์ผํ•œ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒ.
List<Member> result = repository.findAll(example);

 

๋ณต์žกํ•œ ์กฐํšŒ ์กฐ๊ฑด์ด ์žˆ๋‹ค๋ฉด, ExampleMatcher๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.

//ExampleMatcher ์ƒ์„ฑ, age ๊ฐ’์€ ๋ฌด์‹œ
 ExampleMatcher matcher = ExampleMatcher.matching()
                                         .withIgnorePaths("age");
 
 // age๋ฅผ ์ œ์™ธํ•œ ๋‹ค๋ฅธ ํ•„๋“œ ๊ฐ’์ด ๊ฐ™์€ Member๋“ค์„ ์กฐํšŒ
 Example<Member> example = Example.of(member, matcher);
 List<Member> result = memberRepository.findAll(example);

 

๊ฐ„๋‹จํ•œ ์กฐ๊ฑด์ด๋ฉด ์œ ์šฉํ•˜๊ฒŒ ์“ฐ๊ฒ ์ง€๋งŒ, ์‹ค์ œ ์กฐํšŒ์—์„œ๋Š” ์กฐ๊ฑด์ด ๋ณต์žกํ•˜๊ฑฐ๋‚˜ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•  ์ผ์ด ๋งŽ๋‹ค.

ํ•˜์ง€๋งŒ Example์˜ ๊ธฐ์ˆ ์  ํ•œ๊ณ„์ƒ, Inner Join์„ ์ œ์™ธํ•œ ๋‹ค๋ฅธ ์กฐ์ธ์„ ์ œ๊ณตํ•ด์ฃผ์ง€ ์•Š๋Š”๋‹ค.

// ์‚ฌ์‹ค ์กฐ์ธ์ด ์•„๋‹ˆ๋”๋ผ๋„, ์•„๋ž˜์™€ ๊ฐ™์ด ๋ณต์žกํ•œ ์กฐ๊ฑด๋„ ์ฒ˜๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.
firstname = ?0 or (firstname = ?1 and lastname = ?2)

 

๊ตณ์ด ์ด๋ ‡๊ฒŒ ํ•˜์ง€์•Š๊ณ  ๊ทธ๋ƒฅ QueryDSL์„ ์“ฐ๋Š”๊ฒŒ ํ›จ์”ฌ ๊น”๋”ํ•˜๋‹ค. ์ฆ‰ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๊ธฐ๋Šฅ.

 

๋‹ค๋งŒ QueryByExample์€ ๋น„๊ต์  ์ตœ๊ทผ(2020, 1.1๋ฒ„์ „)์— ๋‚˜์˜จ ๊ธฐ๋Šฅ์ด๊ณ  SpringData์— ํฌํ•จ๋œ ๊ธฐ๋Šฅ์ด๋ผ JPA์— ์˜์กด์„ฑ์ด ์—†์–ด์„œ, ๋ฏธ๋ž˜์— ๋‚˜์˜ฌ ์Šคํ”„๋ง ์‹ ๋ฒ„์ „์—์„œ ๊ธฐ๋Šฅ์ด ์ข‹์•„์ง„๋‹ค๋ฉด ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ๋‹ค.  

 

๋ธ”๋กœ๊ทธ์˜ ์ •๋ณด

JiwonDev

JiwonDev

ํ™œ๋™ํ•˜๊ธฐ