QueryMethod는 메소드의 이름을 분석해서 JPQL 쿼리를 실행한다.

사용하기 위해서는 JpaRepository를 상속받아야 한다.

 

일단 테스트 환경은 다음과 같다.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

이전 포스팅인 JpaRepository를 뜯어보면서 거기서 지원해주는 findById, findAll 등등 조회를 도와주는 메소드들을 확인했었다.

근데 대부분 메소드들이 Id를 인자로 받아 처리하는 메소드가 많았는데 사실상 개발하다보면 Id가 아닌 다른 필드로 where절을 구성하는 경우도 많다.

물론 @Query 어노테이션으로 쿼리를 직접 작성하는 방법도 있지만 간단한 쿼리를 편하게 사용할 수 있는 쿼리메소드를 먼저 정리한다.

 

쿼리메소드는 특정 키워드가 존재한다.

이 키워드들은 Jpa Document에서 확인할 수 있고 목록 하단 Appendix C: Repository query keywords 아래에

Supported query method subject keywords에서 확인할 수 있다.

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

 

Description을 보면 Can be used as findBy..., findMyDomainTypeBy... or in combination with additional keywords.

라는 문장을 볼 수 있는데 ...부분에 도메인타입을 넣어주면 된다는것이다.

User에 대한 데이터 조회를 한다고 하면 findUserBy~~~ 이런 형태로.

하지만 생략도 가능하다.

그리고 보통 이미 상속받는 JpaRepository에 타입을 명시하기 때문에 findBy~~~ 형태로 단축해서 많이 사용한다고 한다.

 

 

테스트는 아래와 같이 진행했다.

 

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{

  User findByName(String name);

}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void select(){
        
        System.out.println(userRepository.findByName("coco"));
        
}

이렇게 테스트를 실행하게 되면 정상적으로 coco라는 이름을 갖고 있는 데이터를 조회해 출력해준다.

단, 이렇게 테스트를 실행했는데 만약 coco라는 이름을 갖고 있는 데이터가 복수로 존재한다면

IncorrectResultSizeDataAccessException이 발생한다.

UserRepository에서 findByName은 User 단일 객체 타입인데 여러개의 객체가 조회되기 때문에 발생하는 에러다.

이런 경우 Repository에서 타입을 List타입으로 변경해주면 된다.

 

public interface UserRepository extends JpaRepository<User, Long>{

  List<User> findByName(String name);

}

 

복수의 데이터를 가져오기 위해서는 무조건 List만 써야하는것은 아니고 Set도 가능하다.

 

그리고 JpaRepository안에 있는 메소드들 처럼 Optional 타입도 가능하다.

이렇게 리턴 타입에 대해서도 JpaDocument에 나와있다.

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

이렇게 여러 리턴 타입을 사용한다는 것은 개발자가 리턴되는 레코드의 수를 예측해 미리 타입을 정의해야 한다고 볼 수 있다.

 

Query method subject keywords를 보면 find...By, read...By, get...By, query...By, search...By, stream...By가 묶여있는것을 볼 수 있다.

 

그래서 테스트를 한번 실행.

 

public interface UserRepository extends JpaRepository<User, Long>{

  User findByName(String name);
  User getByName(String name);
  User readByName(String name);
  User queryByName(String name);
  User searchByName(String name);
  User streamByName(String name);
  User findUserByName(String name);
  User findSomethingByName(String name);
  
}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void select(){
        
        System.out.println("findByName : " + userRepository.findByName("coco"));
        System.out.println("getByName : " + userRepository.getByName("coco"));
        System.out.println("readByName : " + userRepository.readByName("coco"));
        System.out.println("queryByName : " + userRepository.queryByName("coco"));
        System.out.println("searchByName : " + userRepository.searchByName("coco"));
        System.out.println("streamByName : " + userRepository.streamByName("coco"));
        System.out.println("findUserByName : " + userRepository.findUserByName("coco"));

        System.out.println("findSomethingByName : " + userRepository.findSomethingByName("coco"));
        
}

이 테스트를 실행하면 모두 동일한 결과를 출력한다.

그럼 결국 묶여있는 모든 키워드가 동일하게 사용된다고 볼 수 있고 강의에서는 가독성을 생각해 편한것으로 작성하면 된다고 한다.

 

그리고 마지막 두줄의 테스트는 Entity 타입을 인식하는지를 볼 수 있는 테스트다.

findUserByName이나 findSomethigByName이나 둘다 동일한 결과를 리턴하는데 그말은 결국 키워드의 ... 부분에는

존재하는 제대로 된 Entity 타입을 넣어야 하는것이 아닌 단지 가독성을 위한 것이고 인식하는 것은 맨 앞 find 와 by 두 키워드를 인식하고 그 뒤의 필드명을 통해 처리한다고 볼 수 있다.

 

그리고 생각보다 자주 발생하는 오류라고 나왔던 문제점 하나.

public interface UserRepository extends JpaRepository<User, Long>{

  User findByByName(String name);
  
}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void select(){
        
        System.out.println("findByByName : " + userRepository.findByByName("coco"));
        
}

이렇게 ByBy 형태로 두번 들어가게 되면 오류가 발생한다.

find 와 By를 인식하기 때문에 ByBy의 경우 둘 다 인식하는듯 하다.

테스트를 실행해보면 BeanCreationException이 발생하고 쭉 옆으로 넘겨서 보면 

QueryCreationException: Could not create query for public abstract ...... UserRepository.findByByName(java.lang.String)! Reason: Failed to create query for .......

이런 부분을 볼 수 있는데 query를 만드는 과정에서 발생한 Exception이며 findByByName 쿼리를 생성할 수 없다는 것이다.

Jpa에서는 키워드를 통해 쿼리문을 생성하는데 등록되지 않은 키워드인 find ByBy가 나오니 오류가 발생하는것이다.

 

 

다음은 exists...By와 count...By다.

이전 Repository 내의 메소드와 동일하다고 볼 수 있다.

exists...By는 존재여부, count...By는 개수를 출력해준다.

public interface UserRepository extends JpaRepository<User, Long>{

  Boolean existsByName(String name);
  
  int countByName(Stirng name);
  
}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void select(){
        
        System.out.println("existsByName : " + userRepository.existsByName("coco"));
        
        System.out.println("countByName : " + userRepository.countByName("coco"));
        
}

exists By는 boolean타입으로 리턴하고 count는 숫자 결과를 리턴한다.

그래서 결과는 존재하는 데이터를 조회했다면 true와 그 개수를 출력해준다.

 

 

delete는 아래와 같이 사용한다.

public interface UserRepository extends JpaRepository<User, Long>{

  int deleteByName(String name);
  
}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void select(){
        
        userRepository.findAll().forEach(System.out::println);
        
        System.out.println(userRepository.deleteByName("coco"));
        
        userRepository.findAll().forEach(System.out::println);
        
}

키워드에서 오는 의미 그대로 삭제해주는 쿼리메소드다. Jpa document에서 보면 remove와 묶여있는데 동일하게 사용되기 때문에 delete만 작성.

결과는 모든 객체를 출력해준 뒤 coco라는 이름을 갖고 있는 데이터 개수를 출력해주고 해당 데이터가 삭제 된 뒤 남은 데이터들을 출력해준다.

 

JpaDocument에서 키워드를 보면 결과가 없거나 삭제 개수를 리턴한다고 되어 있다.

그래서 deleteByName을 출력하도록 하면 삭제될 데이터 개수가 출력되는 것이다.

그렇기 때문에 Repository에서 타입을 void나 int로 처리해주면 된다.

당연히 void로 하면 출력하도록 할 수 없다.

 

만약 Entity 타입을 그대로 사용하게 되면 ClassCastException이 발생한다.

 

그리고 중요한점.

기존 포스팅에서도 그렇고 대체적으로 테스트코드에서 @Transactional 어노테이션을 거의 사용하지 않았는데

deleteBy를 사용하려면 꼭 붙여줘야 한다.

붙이지 않으면 TransactionRequiredException이 발생한다.

 

근데 deleteBy는 잘 안쓰고 JpaRepository에 있는 delete 메소드로 많이 사용한다고 한다.

 

 

다음은 First와 Top이다.

Document의 Description에서 볼 수 있듯이 결과를 첫번째 데이터만 리턴한다.

테스트를 위해 5개의 데이터의 Name을 coco로 통일했다.

 

public interface UserRepository extends JpaRepository<User, Long>{

  User findTop1ByName(String name);
  
  List<user> findFirst2ByName(String name);
  
}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void select(){
        
        System.out.println("findTop1ByName : " + userRepository.findTop1ByName("coco"));
        
        System.out.println("findFirst2ByName : " + userRepository.findFirst2ByName("coco"));
        
}

이렇게 실행하면 top1에서는 id가 1인 데이터 하나만 출력하고 first2에서는 id가 1, 2 인 두개의 데이터만 출력한다.

repository에서는 Top은 User타입으로 first는 List 타입으로 받았는데 둘다 상관없다.

물론 first2ByName처럼 복수의 데이터가 리턴되어야 하는데 단일 객체 타입으로 받으면 오류가 발생한다.

Jpa Document에서는 First<number>, Top<number> 로 나타나있는데 메소드에 그냥 위 처럼 사용해주면 된다.

숫자만 붙여주면 된다.

 

그리고 이 두 쿼리 메소드의 쿼리를 확인해보면 limit를 통해 가져오는것을 볼 수 있다.

 

강의에서 first나 top이 있으니 last도 사용할 수 있지 않을까요? 했는데 findLast1ByName을 하게 되면

findByName과 동일한 결과가 출력된다.

즉, 정의되어있지 않은 키워드라는 의미다.

 

 

이 쿼리메소드들은 and, or 등 조건을 추가할 수 있는데 그건 다음 포스팅에서............................................................

 

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

JpaRepository를 확인해보면 PagingAndSortingRepository 외에 QueryByExampleExecutor를 상속받고 있는것을 볼 수 있다.

 

QueryByExampleExecutor는 흔히 QBE로 축약해서 부르기도 한다고 한다.

Entity를 Example로 만들고 Matcher를 통해 필요한 쿼리들을 만드는 방법이다.

 

이전 포스팅과 동일한 환경에서 진행했다.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

Jpa Docs에서는 QueryByExample에 대해 간단한 인터페이스를 사용하는 사용자 친화적인 쿼리기술이라고 하고 있다.

동적 쿼리 생성이 가능하며 필드 이름이 포함된 쿼리를 작성할 필요가 없다고 한다.

 

QBE는 세부분으로 구성되는데 probe, ExampleMatcher, Example로 구성된다.

 

probe는 도메인 개체의 실제 예시. Entity가 된다. Entity라고 해서 실제 Entity를 의미하진 않고 가짜 Entity라고 볼 수 있다.

 

ExampleMatcher는 특정 필드를 일치시키는 방법에 대한 세부 정보를 전달한다.

 

Example은 probe와 ExampleMatcher로 구성되어있고 사용할 query를 생성하는 역할을 한다.

 

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{

}
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.endsWith;
//endsWith()는 static으로 import 해준다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("name") //매칭할때 name은 매칭을 하지 않겠다는 것.
                .withMatcher("email", endsWith()); //withMatcher는 "email"을 통해 확인하겠다는것.
                
                
        Example<User> example = Example.of(new User("aaaa", "coco@gmail.com"), matcher);
        /*
            Example.of(probe, matcher); 형태로 구성.
        */
        
        userRepository.findAll(example).forEach(System.out::println);
}

 

주석부분에 설명했듯이 withIgnorePaths로 매칭하지 않을 필드를 명시하고 withMatcher로 매칭할 필드를 명시한다.

 

그럼 실제로 데이터에 aaaa라는 이름을 갖고 있는 데이터는 존재하지 않지만 coco@gmail.com 이라는 이메일을 갖고 있는 데이터는 존재하기 때문에 해당 데이터를 조회해 출력해주게 된다.

 

example은 예제 이전에 설명했듯이 probe와 matcher로 구성되어 있다.

Example.of를 보면 Example.of(T probe) 와 Example.of(T probe, ExampleMatcher matcher) 두가지를 볼 수 있다.

 

그래서 of 의 첫 부분인 probe 부분에 new User로 엔티티를 넣어주고 Probe를 User로 넣어줬으니 Example의 타입에도

probe의 타입과 동일한 User를 넣어주면 된다.

그리고 matcher를 넣어주면 된다.

 

실행해서 보면

이러한 쿼리를 처리한것을 볼 수 있다.

probe에는 name과 email이 존재하지만 ignore로 name을 제외하도록 했기 때문에 email만을 통해 조회한다.

 

그럼 ignore를 사용하지 않는다면?

 

이렇게 쿼리를 실행한다.

여기에서 차이를 보면 matcher에 명시한 email은 like를 통해 조회하지만 name의 경우는 like가 아닌 exact match로 조회한다.

즉, email은 부분적으로 일치하는 데이터를 찾을 수 있는 like를 사용하지만 matcher에 명시하지 않은 name은 완전 일치해야 하는 형태로 조회한다는것.

 

이 예제를 보면 probe에서 담고 있는 name, email을 모두 포함해 쿼리를 생성할 수 있지만 matcher의 ignore를 통해 원하는 필드를 제외하고 쿼리 실행이 가능하다는것을 알 수 있다.

 

하지만 실제로 이렇게 사용할 일은 없고 보통은 set을 통해 넣어주는 경우가 많기 때문에 아래와 같은 예제도 추가한다.

 

import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.endsWith;
//endsWith()는 static으로 import 해준다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        User user = new User();
        user.setName("aaaa");
        user.setEmail("coco@gmail.com");
        
        
        ExampleMatcher matcher = ExampleMatcher.matching()
                .withIgnorePaths("name")
                .withMatcher("email", endsWith());
                
                
        Example<User> example = Example.of(user, matcher);
        
        userRepository.findAll(example).forEach(System.out::println);
}

 

이렇게 default Constructor로 생성하고 set을 통해 넣어준 다음 처리한다.

 

 

하지만 Example은 문자열에 관련된 것만 쓸 수 있는 등 한계점이 있어서 조금 복잡한 쿼리문을 만들기에는 적합하지 않아 Example을 사용하기보다는 QueryDSL을 사용한다고 한다.

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

 

  • Jpa docs

 

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

제목에 JpaRepository 메소드 paging이라고 적긴 했지만

paging은 JpaRepository가 상속받고 있는 PagingAndSortingRepository에서 제공한다.

이렇게 제목을 정한 이유는 이전 포스팅들과 묶어두기 위해...

인터페이스명 그대로 paging과 sorting에 대한 메소드만이 존재한다.

sorting은 다른 포스팅에서 정리하도록 하고 여기서는 paging만 정리.

 

이전 포스팅과 동일한 환경에서 진행했다.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

PagingAndSortingRepository에 들어가보면 Page<T> findAll(Pageable pageable) 메소드를 볼 수 있다.

entitites 페이지를 반환한다는 설명이 붙어있다.

 

메소드 타입에서 볼 수 있듯이 Page타입으로 사용하면 된다.

 

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{

}

 

import org.springframework.data.domain.page

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        Page<User> users = userRepository.findAll(PageRequest.of(0, 3));
        
}

 

paging은 PageRequest.of() 통해 기능을 수행하는데 여기에는 page와 size를 받는다.

page는 zero Index로 0부터 시작하고 size는 해당 페이지에 가져올 데이터의 수이다.

 

그럼 이제 Page 인터페이스를 좀 봐야한다.

 

Page는 Slice를 상속받고 있다.

Slice는 의미 그대로 데이터 덩어리의 한 조각이라고 볼 수 있다.

getNumber(), getSize(), getContent() 등 여러 값들을 제공하고 있는데 이 정보들은 현재 Slice에 대한 값이다.

여기서 제일 기본적으로 중요한 부분은 List<T> getContent()다.

 

getContent는 Returns the page content as {@link List}. 라고 설명되어 있는것을 볼 수 있는데

content를 list로 반환한다는 것이다.

 

그리고 getSort(), getNumber(), getSize()는 우리가 인자로 제공하는 값을 리턴값으로 다시 받아 제공하는 값이다.

 

각 메소드 설명을 보면 이해 될만큼 단순한 내용들이 대부분이다.

 

다시 Page 인터페이스로 돌아와서 보면 getToalpages(), getTotalElements()를 볼 수 있다.

의미 그대로 총 페이지수와 전체 레코드 수를 의미한다.

 

그럼 슬라이스에 대한 값이 아니라는 것을 알 수 있는데

그래서 슬라이스는 그 부분집합에 대한 각각의 정보들을 제공하고 그것을 상속받는 Page는 그 슬라이스에 대한 정보도 갖고 있지만 전체 페이지에 대한 정보를 추가로 제공하고 있다고 볼 수 있다.

 

그래서 아래처럼 확인해봤다.

 

import org.springframework.data.domain.page

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        Page<User> users = userRepository.findAll(PageRequest.of(0, 3));
        
        System.out.println("page : " + users);
        System.out.println("total elements : " + users.getTotalElements());
        System.out.println("total Pages : " + users.getTotalPages());
        System.out.println("number of Elements : " + users.getNumberOfElements());
        System.out.println("sort : " + users.getSort());
        System.out.println("size : " + users.getSize());
        
        users.getContent().forEach(System.out::println);
        
}

데이터는 총 5개가 존재하고 3개씩 나눠 첫페이지인 0페이지를 users로 받았다.

 

그래서 users를 그대로 출력했을때 Page 1 of 2 처럼 첫번째 페이지라는 것을 볼 수 있다.

getTotalElement는 총 레코드 수를 가져오기 때문에 5를 출력하고있고

getTotalPages는 총 페이지수인 2를 출력한다.

getNumberOfElements는 현재 페이지의 레코드수를 가져온다. 그렇기 때문에 3을 출력.

getSort는 따로 sorting에 대해 명시하지 않았기 때문에 UNSORTED가 출력된다.

getSize는 가져오도록 요청한 레코드 수이기 때문에 3을 출력한다.

 

처음에 getNumberOfElements와 getSize값이 같다보니 내가 생각한게 맞나 싶었는데 다음 페이지 출력해보니까

제대로 알게 되었다. NumberOfElements는 가져오도록 요청한 페이지에서 담고 있는 레코드 수.

getSize는 PageRequest.of로 요청한 페이지당 레코드의 수이다.

 

그리고 데이터를 출력하는것은 getContent()를 통해 출력한다.

 

sort를 지정하게 되면 어떻게 출력되는지 보기 위해서 한번 더 테스트해봤다.

 

import org.springframework.data.domain.page

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        Page<User> users = userRepository.findAll(PageRequest.of(0, 3, Sort.by("id").descending()));
        
        System.out.println("sort : " + users.getSort());
        
        users.getContent().forEach(System.out::println);
        
}

sort는 PageRequest.of().Sort..... 형태로 사용할줄 알았는데 아니었다.

size 뒤에 적어주면 된다.

결과값은 이렇게 출력.

좀 구분하기 쉽게 Long타입으로 되어있는 id로 정렬한건데 당연히 저 안에 다른 값을 넣어도 된다.

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

 

  • Pagination Sort

 

 

[Spring Boot] Pagination과 Sort

1. 개요 Pagination은 큰 데이터를 표현할 때 도움이 됩니다. 또한 어떠한 기준으로 데이터를 정렬하며 페이징해야할 수 있습니다. 이번 튜토리얼은 Spring Data Jpa를 사용하여 페이징하는 방법에 대해

itmining.tistory.com

 

 

 

JPA JpaRepository 메소드 (save(), find~())

JPA 기초 제목을 JPA 기초라고 하긴 했으나 이 포스팅에서는 정말 기본적인 설정과 어노테이션 정도만 포스팅. 예제 환경 Intelli J SpringBoot 2.6.2 Lombok Gradle 7.3.2 JPA는 의존성 추가를 해야 사용할 수

myyoun.tistory.com

 

이전 작성한 포스팅에서의 save, find~ 말고 나머지 메소드들에 대한 포스팅.

 

위 포스팅에서와 동일한 프로젝트 환경으로 진행.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

이 포스팅에서는 아래의 메소드들을 정리.

  • count()
  • existsById()
  • delete()
  • deleteById()
  • deleteAll(), deleteAllById()
  • deleteInBatch()
  • deleteAllInBatch(), deleteAllByIdInBatch()

 

그리고 저번 포스팅과 동일한 Entity와 Repository를 사용.

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{

}

 

count()

 

count() 메소드는 의미 그대로 데이터의 개수를 의미한다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        long count = userRepository.count();
        
        System.out.println(count);
}

매핑된 테이블에 존재하는 데이터의 개수를 조회하는 메소드이다.

그리고 count() 메소드의 경우 return 타입이 long타입이기 때문에 long으로 받아야 한다.

 

 

 

existById()

 

exsistById는 해당 Id(Primary key) 값에 해당하는 데이터가 존재하는지에 대해 나타낸다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        boolean exists = userRepository.existsById(1L);
        
        System.out.println(exists);
}

처음 테스트 실행하기 전에는 boolean타입이라 어떻게 조회할지 궁금했다.

근데 의외로 select쿼리릍 통해 해당 id 값을 갖고 있는 데이터의 개수를 조회한다.

즉, 해당 쿼리를 실행시켜 결과값이 0이면 false, 0이 아니면 true가 된다.

그리고 JPQL에서만 해당하는 것인지는 모르겠지만 해당 데이터가 존재하는지 유무에 대해 count()보다 existsById()를 사용하는것이 더 좋다는 포스팅을 봤다.

내용은 다음과 같았다.

데이터가 존재하는지를 확인하기 위해 count를 사용할 때가 있다.

count의 경우 총 개수를 확인해야 하기 때문에 존재유무와 상관없이 일단 모든 데이터를 훑어보게 된다.

하지만 exists의 경우 데이터가 존재하는지만 확인하면 되기 때문에 데이터를 찾는 순간 쿼리를 종료하게 된다.

그럼 당연히 존재유무만 확인하기 위해서는 count보다는 exists가 성능이 더 좋을 수 밖에 없다 라는 내용이다.

 

이 포스팅은 아래 Reference에서 확인.

 

 

 

delete()

 

삭제 메소드이다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        User user = userRepository.findById(1L).orElseThrow(RuntimeException::new);
        
        userRepository.delete(user);
        
        userRepository.findAll().forEach(System.out::println);
}

 

여기서 주의해야할 점은 상황에 따라 달라지겠지만 위 예제처럼 진행하는 경우 findById().orElse() 보다는

findById().orElseThrow가 좀 더 낫다.

delete의 경우 CrudRepository에서 확인해보면 'entity must not be {@literal null}.' 이라고 있는것을 볼 수 있는데

null이 들어올 경우 문제가 발생한다는 것이다.

실제로 null을 넣어보면 IllegalArgumentException이 발생하는 것을 볼 수 있다.

그래서 그냥 orElseThrow로 null인 경우 Exception을 바로 발생시키게 하는것.

그냥 편의성 정도로 생각하면 될것같다.

 

쿼리문은 이렇게 로그에 출력된다.

첫 select 는 findById()에 의해 실행된 select문이다.

그리고 그 아래에 select문이 한번 더 실행된것을 볼 수 있는데 delete()메소드의 경우 delete쿼리를 실행하기 전 select로 해당 데이터가 있는지 조회한 다음에 있으면 delete쿼리를 실행하기 때문이다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        //존재하지 않는 데이터를 만들기 위해 new로 생성
        User user = new User();
        user.setId(8L);
        
        userRepository.delete(user);
        
        userRepository.findAll().forEach(System.out::println);
}

 

그래서 위 코드처럼 현재 없는 아이디값을 넣어 테스트해보면 delete쿼리는 실행되지 않고 select쿼리까지만 실행되는것을 볼 수 있다.

 

 

 

deleteById()

 

delete() 메소드처럼 하나의 데이터를 지워준다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        
        userRepository.deleteById(1L);
        
        userRepository.findAll().forEach(System.out::println);
}

 

 

 

CrudRepository를 보면 delete() 메소드는 entity를 받는 반면 deleteById() 메소드는 메소드명처럼 id를 받는다.

결과는 delete() 메소드와 동일한 결과를 출력해준다.

쿼리문에서의 차이는 findById() 메소드를 사용하지 않기 때문에 delete 처리 하기 전 select문이 하나 줄어들었다는 정도다.

 

 

SimpleJpaRepository에서 deleteById를 보면 넘어온 id값으로 findById를 사용해 delete()에 넘겨줄 데이터를 조회하는데 데이터가 null인경우에는 EmptyResultDataAccessException을 발생시키고 존재하는 경우 delete()에 id값을 인자로 호출하고 있다.

 

그럼 결국 delete() 메소드 처리과정과 동일하다고 볼 수 있게 된다.

 

userRepository.delete(userRepository.findById(1L).orElseThrow(EmptyResultDataAccessException::new);

 

이전 delete()메소드 예제에서의 이 과정을 deleteById() 내부에서 처리하고 있다고 볼 수 있다.

 

그럼 delete() 메소드나 deleteById() 메소드나 결국 처리과정이 동일하다고 볼 수 있는데 나눠져 있는 이유는?

 

findById()에서의 차이가 있다고 볼 수 있다.

 

delete 메소드는 해당 예제에서처럼 findById랑 조합해서 사용하는 경우가 많다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){


        //delete
        userRepository.delete(
            userRepository.findById(1L).orElseThrow(RuntimeException::new)
        );



        // deleteById
        userRepository.deleteById(1L);
        
}

이렇게 사용하는 측면에서만 보면 delete() 메소드는 직접 findById()와의 조합을 직접 작성해 사용하는 형태고

deleteById()는 메소드 내부에서 예외처리를 해주고 Id값만 넘겨주면 되는 차이가 있다.

 

그럼 굳이 나눠서 사용하는 방법을 알아야 하는 이유는 지금은 계속 포스팅에서 orElseThrow를 통해 null인 경우

RuntimeException을 발생시키도록 하고 있지만 필요에 따라 예외처리를 custom해서 처리할 수 있다는 장점이 있다.

 

하지만 deleteById() 메소드를 사용하면 예외처리를 커스텀하는것이 아닌 EmptyResultDataAccessException이 발생하기 때문에 예외처리에서의 차이점이 발생한다고 볼 수 있다.

 

이 차이에 대한 포스팅은 Reference에서 확인.

 

 

 

deleteAll(), deleteAllById()

 

deleteAll() 메소드는 테이블의 모든 데이터를 삭제하는 메소드다.

 

쿼리문으로만 보면 DELETE FROM user; 의 형태.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        userRepository.deleteAll();

    }

 

사용은 이렇게 사용한다.

 

하지만 CrudRepository를 보면 entities를 인자로 받아 삭제가 가능하다는 것을 볼 수 있다.

즉, 원하는 데이터만 삭제하는것도 가능하다는 것이다.

 

사용은 saveAll()를 했을때와 동일하게 사용한다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        User user1 = new User();
        user1.setId(1L);
        
        User user2 = new User();
        user2.setId(2L);
        
        userRepository.deleteAll(Lists.newArrayList(user1, user2));
        
        
        
        
        
        
        List<User> users = userRepository.findAllById(Lists.newArrayList(1L, 2L));
        
        userRepository.deleteAll(users);

    }

이 예제처럼 복수의 entity를 넘겨 처리하는 메소드이다.

쿼리문을 살펴보면 deleteAll이 실행될 때 넘겨받은 Entity의 개수만큼 select문으로 조회를 하고

그 다음 그 데이터들을 하나하나 삭제해 나간다.

 

deleteAll 역시 SimpleJpaRepository에서 확인할 수 있는데 받은 Entity들을 반복문을 통해 delete로 넘겨주는것을 볼 수 있다.

 

그리고 delete에서는 isNew로 체크하기 때문에 만약 넘겨받은 Entity 중에서 하나는 존재하지만 하나는 존재하지 않는 데이터라면 select문과 delete문 모두 한번씩만 수행하게 된다.

 

 

그리고 이와 동일한 결과를 출력하는 메소드가 deleteAllById() 메소드인데 일단 사용할때의 차이는 entity를 받느냐

Id를 받느냐의 차이다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        userRepository.deleteAllById(Lists.newArrayList(1L, 2L));
        
        
        
        
        
        List<Long> ids = new ArrayList<>();
        ids.add(1L);
        ids.add(2L);
        
        userRepository.deleteAllById(ids);

    }

일단 이렇게 표면적인 차이에서는 Id값을 받아 처리한다는 차이만 보인다.

 

그리고 SimpleJpaRepository에 들어가서 확인해보면 deleteAll()은 delete()를 호출해 처리한 반면

 

 

deleteAllById는 deleteById()를 호출하는것을 볼 수 있다.

즉, null에 대한 예외처리가 된다는 것을 볼 수 있다.

 

deleteAll()과 deleteAllById()를 묶은 이유가 여기 있다.

deleteAllById() 메소드는 강의에서도 설명이 전혀 없이 넘어갔기 때문에 따로 찾아보려 했으나

포스팅 자체가 별로 없다.

 

좀 뜯어보니 delete() 메소드와 deleteById() 메소드의 차이점과 비슷했기에 다행..

 

그럼 deleteAll()과 deleteAllById()의 경우 표면적으로만 봤을때 발생하는 차이가 일단 Entity를 받느냐

id를 받느냐의 차이다. 물론 둘다 List로 처리하지만 deleteAll은 Entity타입의 List, deleteAllById는 Id 타입의 리스트를 받게 된다.

 

출력하는 쿼리문 역시 동일하게 처리되지만 지금까지 확인한 것으로는 deleteAll() 메소드의 경우 존재하지 않는 데이터에 대한 delete() 요청은 무시한다.

Exception을 발생하지 않고 delete 쿼리역시 동작하지 않는다는것.

 

하지만 deleteAllById()의 경우 deleteById()를 호출하도록 되어있기 때문에 존재하지 않는 데이터에 대해

deleteById() 처리에서 처럼 EmptyResultDataAccessException을 발생시킨다.

 

좀 다시 정리하자면

deleteAll()은 Entity를 받아 처리하면서 존재하지않는 데이터에 대한 요청은 무시하지만

deleteAllById() 메소드는 Id값을 받아 처리하고 존재하지 않는 데이터에 대해 EmptyResultDataAccessException을 발생시킨다.

이정도..?

 

 

 

 

deleteInBatch()

 

deleteInBatch() 메소드는 Entity를 받아 처리하는 deleteAll() 처럼 entity타입의 리스트를 받아 처리한다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        List<User> users = userRepository.findAllById(Lists.newArrayList(1L, 2L));
        
        userRepository.deleteInBatch(users);

    }

 

deleteAll()과의 차이점으로는 select로 조회하지 않고 한번에 delete 처리를 한다는 것이다.

deleteAll()의 경우는 2개의 데이터를 삭제할 때 해당 데이터가 존재하는지 select 쿼리를 2번 진행한 다음

delete쿼리 역시 2번을 진행했다.

즉, 총 4번의 쿼리문이 실행이 되는데 deleteInBatch() 메소드의 경우 딱 한번의 delete쿼리만 실행된다.

아무래도 deleteAll()을 통해 여러 데이터들을 삭제하고자 한다면 데이터의 수 * 2 만큼 쿼리가 실행되기 때문에

성능이슈가 발생할 수 있다.

하지만 deleteInBatch()의 경우 delete쿼리 한번만을 실행하기 때문에 좀 더 낫다고 볼 수 있다.

 

그리고 deleteInBatch() 메소드는 인자 없이 그냥 사용할수는 없다.

 

deleteInBatch()를 사용하게 되면 

이렇게 줄이 생기는것을 볼 수 있는데 내용을 보면 'deleteInBatch(java.lang.Iterable<T>)' is deprecated 라는 설명을 볼 수 있다.

더 사용되지 않는다는 의미.

 

Jpa Document를 보더라도 

이렇게 더이상 사용되지 않으니 deleteAllInBatch()를 사용하라고 되어있다.

 

그럼에도 처리가 되긴 하는 이유는 아래에 있다.

JpaRepository를 보면 deleteInBatch는 deleteAllInBatch()를 바로 호출하면서 자신이 받은 인자를 그대로 넘겨준다.

결국 deleteInBatch() 메소드는 거쳐가는곳일 뿐 deleteAllInBatch() 메소드에서 처리하게 된다.

 

 

 

deleteAllInBatch(), deleteAllByIdInBatch()

 

deleteAllInBatch() 메소드는 deleteAll() 메소드와 사용방법이 아예 동일하다고 할 수 있다.

deleteInBatch()는 인자를 꼭 받아야 하는 반면 deleteAllInBatch() 메소드는 deleteAll() 메소드처럼 인자를 받지 않게 되면 테이블의 모든 데이터를 삭제하게 된다.

 

차이점은 deleteInBatch() 메소드에서의 설명과 동일하게 삭제처리 이전에 조회하는 과정이 없고 delete 쿼리를 한번만 실행한다는 점이다.

 

사용은 아래와 같이 한다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        
        userRepository.deleteAllInBatch();
        
        
        
        
        
        List<User> users = userRepository.findAllById(Lists.newArrayList(1L, 2L));
        
        userRepository.deleteAllInBatch(users);

    }

 

 

 

복수의 데이터를 삭제하고자 한다면 계속 조회하고 하나하나 지워나가는 deleteAll() 메소드보다는 따로 조회하지 않고 한번의 delete 쿼리를 실행하는 deleteAllInBatch()가 성능이 좋을 수 밖에 없다.

 

deleteAllByIdInBatch()를 같이 묶어둔 이유는 동일한 처리를 하지만 약간의 차이가 존재하기 때문이다.

 

deleteAllInBatch()는 deleteAll()과 마찬가지로 entiteis를 받아 처리하지만 deleteAllByIdInBatch()는 deleteAllById() 메소드처럼 Id를 받아 처리한다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){

        List<Long> ids = new ArrayList<>();
        ids.add(1L);
        ids.add(2L);
        
        userRepository.deleteAllByIdInBatch(ids);
        
        
        
        userRepository.deleteAllByIdInBatch(Lists.newArrayList(1L, 2L));

    }

이런 형태로 사용하게 된다. 그리고 deleteAllByIdInBatch는 인자를 꼭 받아야 한다.

deleteAll()과 deleteAllById()는 예외처리를 하느냐 안하느냐에서 차이가 발생했는데

deleteAllInBatch()와 deleteAllByIdInBatch()는 예외처리에서의 차이가 아닌 쿼리문에서 차이가 발생한다.

 

delteAllInBatch()의 경우 delete 쿼리를 생성할 때 where절에서 or를 사용해 여러개의 id값을 처리한다.

 

하지만 deleteAllByIdInBatch()의 경우는 where절에서 in을 통해 처리하게 된다.

 

강의에서도 다뤄주시지 않았고 차이점에 대한 비교 포스팅도 없어서 사실 이 쿼리문에서의 차이 말고는 차이점을 잘 못느끼겠다...

 

 

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

 

  • JPQL에서 count 성능 이슈 및 exists 사용

 

 

JPQL의 count 성능 이슈 및 exist 사용 방법

Spring의 JPA 사용 중 데이터가 존재하는지 확인하는 방법으로 count 함수를 사용할 때가 있다.단순히 하나의 데이터가 존재하는지 확인하는 경우(아이디 중복확인 등) count 함수를 사용하면 성능상

velog.io

 

  • deleteById와 delete의 차이

 

 

Spring Data JPA 사용 시 deleteById 와 delete 의 차이

Spring Data 란? Spring Data’s mission is to provide a familiar and consistent, Spring-based programming model for data access while still retaining the special traits of the underlying data store...

hwanchang.tistory.com

 

'Spring' 카테고리의 다른 글

Jpa QueryByExampleExecutor 인터페이스  (0) 2022.02.08
JPA JpaRepository 메소드 paging (findAll(pageable))  (0) 2022.02.03
JPA JpaRepository 메소드 (save(), find~())  (0) 2022.01.28
JPA 기초  (0) 2022.01.26
JPA란?  (0) 2022.01.13

 

 

 

JPA 기초

제목을 JPA 기초라고 하긴 했으나 이 포스팅에서는 정말 기본적인 설정과 어노테이션 정도만 포스팅. 예제 환경 Intelli J SpringBoot 2.6.2 Lombok Gradle 7.3.2 JPA는 의존성 추가를 해야 사용할 수 있다. Gradl

myyoun.tistory.com

 

이전 포스팅인 상단 기초에서 아주 간단한 설정 및 엔티티 어노테이션, 그리고 앞으로 사용하게 될 메소드가 정의되어 있는 위치를 확인했다.

 

Repository 인터페이스를 생성하고 거기서 상속받는 JpaRepository는 PagingAndSortingRepository를 상속받으며 이 인터페이스 또한 CrudRepository라는 인터페이스를 상속받는다.

 

JpaRepository를 상속받지 않고 CrudRepository를 상속받아도 될 정도로 많은 메소드가 정의되어 있다고 했는데

공부한 내용은 JpaRepository를 상속받아서 사용했고 CrudRepository안에 있는 메소드들이 JpaRepository를 통해서도

사용할 수 있기 때문에 JpaRepository만 뜯어본다!

 

일단 공부한 환경은 아래와 같다.

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

JpaRepository와 CrudRepository에 주석으로 각 메소드들에 대한 설명들이 적혀 있지만 좀 정리하고자 한다.

이 포스팅에서는 아래 메소드들만 정리.

  • save()
  • saveAll()
  • saveAndFlush()
  • saveAllAndFlush()
  • getOne()
  • findById()
  • findAll()
  • findAllById()

 

 

 

 

save()

 

save()는 의미 그대로 엔티티를 저장하고자 할 때 사용하는 메소드이다.

가장 기초적으로 사용하는 방법은 아래와 같다.

 

@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{

}
@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        User user = new User();
        user.setName("coco");
        user.setEmail("coco@gmail.com");
        
        userRepository.save(user);
}

 

이렇게 save 메소드는 User 엔티티를 받아 저장해준다.

강의에서는 H2-in-memory DB를 사용했고 별도로 MySQL에서도 테스트했기 때문에 User Entity의 GeneratedValue는

IDENTITY로 설정.

이 테스트를 실행하게 되면 User 테이블에 name = 'coco', email = 'coco@gmail.com' 이렇게 insert 된다.

그리고 Id의 경우는 GeneratedValue에 의해 1씩 증가하며 저장되게 된다.

 

그리고 CrudRepository안에 있는 메소드들을 보면 update에 대한 메소드는 보이지 않는다.

Create는 save, Read 는 find, Delete는 delete로 메소드가 정의되어 있는것을 볼 수 있는데 Update 메소드는 찾을 수 없다.

 

그 Update에 대한 처리를 진행하는 것이 save 메소드의 역할 중 하나이기 때문이다.

 

save 메소드는 새로운 데이터를 넣는 경우 insert로 동작하게 되지만 기존에 있는 데이터를 넣게 되면 update 쿼리를 실행하게 된다.

 

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        User user = new User();
        user.setName("coco");
        user.setEmail("coco@gmail.com");
        
        //insert
        userRepository.save(user);
        
        
        //update
        User user2 = userRepository.findById(1L).orElseThrow(RuntimeException::new);
        user2.setEmail("coco1@gmail.com");
        userRepository.save(user2);
        
}

 

이 테스트를 실행하면 name='coco', email='coco@gmail.com' 이라는 데이터가 저장된다.

그리고 user2 에서 사용한 findById는 Id값으로 데이터를 찾아온다는 것인데 where id = 1  이렇게 생각하면 쉽다.

현재 User Entity에서 Id는 id라는 Long 타입의 필드로 명시되어 있기 때문에 Long 타입으로 1L을 넣어주는것.

orElseThrow의 경우는 Jpa에서 Repository에서 리턴타입을 Optional로 받을 수 있도록 지원하기 때문이고

Optional은 null로 인한 예외 처리를 해줘야 한다.

 

즉, userRepository.findById(1L)은 리턴 타입이 Optional<T> 이기 때문에 null이었을 때 발생할 예외처리를 해줘야 한다는 것이다.

강의에서 orElseThrow를 계속 사용하셨기에 동일하게 작성했지만 orElse(), orElseGet() 등 여러 Optional 메소드가 존재한다.

이것은 따로 정리가 필요...

일단 Optional에 대한 포스팅은 하단 Reference에서 확인.

 

그럼 코드를 다시 보자면 user2는 findById로 Id가 1L인 데이터를 가져왔기 때문에 테스트 초반 저장된 

name='coco', email='coco@gmail.com' 을 의미하게 된다.

그리고 setEmail로 coco1@gmail.com 으로 데이터를 변경해준 뒤 save를 해주게 되면 update가 수행되게 된다.

 

이 때 로그를 확인해 쿼리를 확인하게 되면 insert로 user 테이블에 데이터를 저장해준 뒤 

select로 조회를 2번 진행한다. 그리고 update 쿼리가 수행된것을 볼 수 있다.

 

첫번째 save(user)로 인해 insert 쿼리가 실행되고 findById(1L)로 select 쿼리가 한번 실행된 후에

save(user2)가 실행되면서 select 쿼리로 조회한번 한 뒤에 update 쿼리가 실행되는 것이다.

 

코드만 봤을때는 동일한 save이지만 insert와 update를 알아서 구분해 처리해준 것이다.

어떻게 이렇게 수행되는지 보려면 SimpleJpaRepository에서 확인해야 한다.

Intelli J - window 기준 Shift 두번 눌러서 SimpleJpaRepository 검색하면 들어가서 볼 수 있다.

 

확인해야 하는 부분은 이 부분.

그리고 SimpleJpaRepository는 JpaRepositoryImplementation을 재정의 하고 있고

JpaRepositoryImplementation은 JpaRepository를 상속받고 있다.

즉, JpaRepository에 정의된 메소드들은 SimpleJpaRepository에서 구현체를 제공하고 있다는 것이다.

 

save()메소드는 제일 먼저 null 체크를 하게 되는데 entity로 받은 인자가 null이면 오류가 발생하게 된다.

 

그리고 if문으로 isNew 즉, 새로운 데이터면 em(Entity Manager)에서 insert(persist)를 수행한다.

새로운 데이터가 아닌 경우는 Entity Manager에서 update(merge)를 수행하도록 구현되어 있다.

 

isNew라는 것은 주어진 Entity가 새로운것인지에 대한 여부이기 때문에 코드 그대로

새로운 데이터라면 persist를, 존재하는 데이터라면 merge를 수행하도록 구현한 것이다.

 

isNew에 대해 좀 더 보기 위해 AbstractPersistable에 들어가서 보게 되면

isNew에 대한 리턴은 null == getId가 된다.

entity에서 Id로 지정해둔 필드가 null 이라면 새로운 데이터라는 조건이 되는것이고

null이 아니라면 존재하는 데이터이기 때문에 update를 처리하도록 하는것이다.

 

 

saveAll()

 

saveAll() 메소드는 의미 그대로 단일 데이터가 아닌 복수의 데이터를 저장하는것이다.

Lists.newArrayList를 통해 저장할 수 있다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    void crudTest(){
        User user = new User();
        user.setName("coco");
        user.setEmail("coco@gmail.com");
        
        User user2 = new User();
        user.setName("mozzi");
        user.setEmail("mozzi@gmail.com");
        
        userRepository.saveAll(Lists.newArrayList(user, user2);
}

 

 

saveAndFlush(), saveAllANdFlush()

 

saveAndFlush는 save와 flush를 수행하는 메소드이다.

save는 위에서 설명했으니 생략하고 flush에 대해 먼저 간단하게 설명.

Transaction commit이 일어날 때 flush가 동작하게 되는데 이때 insert, update, delete SQL들이 DB로 넘어간다.

이때 영속성 컨텍스트(Persistence Context)를 비우는것은 아니고 DB와 동기화 한다고 이해하면 된다.

예를들어 user테이블에 insert한 뒤에 다시 set으로 내용을 수정한다면 수정된 Entity를 쓰기 지연 SQL 저장소에 등록해 두었다가 이 쿼리를 DB에 전송한다. 전송이 발생하는것을 flush가 발생한 시점이라고 볼 수 있고 flush가 일어난 다음에 실제 commit이 일어나게 된다.

flush에 대한 자세한것은 Reference에서 참고.

 

flush는 이러한 동작을 하게 되지만 saveAndFlush에 있는 flush는 DB에 업데이트를 하는 flush가 아니라

영속성 컨텍스트내의 한 공간에 flush를 해 저장해 두었다가 Transaction이 종료되는 시점에 DB에 업데이트하는 형태다.

 

결과만 놓고 봤을때는 save() 메소드와 동일한 결과를 보여준다.

 

하지만 과정에서의 차이가 존재하는건데 하나의 테스트에서 save()나 saveAndFlush()를 한번 한 뒤에 데이터를 수정했을때, 그 과정에서의 차이가 발생한다.

 

최초 insert의 과정은 무조건 Context내의 한 공간에 업데이트가 되는것은 동일하다.

이 한 공간을 space라고 임의로 정했을 때, saveAndFlush()를 통해 업데이트를 진행하게 되면 매번 그 업데이트 쿼리를 space에 보내고 Transaction이 종료되는 시점에 DB에 업데이트를 하게 된다.

하지만 save는 Context에 마지막으로 존재하는 형태의 데이터를 query로 만들어서 Transaction 종료 시점에 DB에 업데이트 하게 된다.

 

즉, save는 context에 마지막으로 남아있는 데이터만 넘겨줄 쿼리를 한번 생성해 업데이트를 해주지만

saveAndFlush는 업데이트가 한번 발생할때마다 space에 쿼리를 만들어 보내놓고 처리하는 과정인것.

 

그렇기 때문에 효율성 측면에서는 save가 flush보다 더 낫다고 한다.

 

하지만 그렇다고 saveAndFlush가 무조건 더 안좋은것은 아니고 환경 혹은 때에 따라 saveAndFlush가 더 좋은 경우가 있다고 한다.

 

flush에 대해 아직 깊게 알지 못하기 때문에 이부분은 더 공부가 필요...

 

그리고 saveAndFlush에 대해 테스트를 많이 진행해서 그 과정을 포스팅 해주신 분들이 많으니

Reference를 참고해 볼것.

 

saveAllAndFlush는 saveAll()과 save()의 차이처럼 한번에 여러개의 데이터를 처리하는 차이이므로 설명은 생략.

 

getOne()

 

getOne() 메소드는 Id(primary key)를 통해 매칭되는 하나의 객체를 가져오는 메소드다.

결과만 놓고 봤을때는 findById() 메소드와 동일한 결과를 보여주는데 getOne은 내부적으로

EntityManager.getReference() 를 통해 엔티티를 가져오게 되어있다.

LazyLoading을 지원하고 호출되는 시점에는 일단 Proxy를 가져오며 실제로 가져온 Entity의 속성에 접근하는

순간 DB에 접근하는 방식을 사용한다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void crudTest(){
        User user = userRepository.getOne(1L);
        
        System.out.println(user);
}

 

 

 

findById()

 

findById() 메소드는 getOne처럼 Id를 통해 매칭되는 하나의 객체를 가져오는 메소드이다.

getOne은 Proxy를 먼저 가져온 후에 속성에 접근하는 순간 DB에 접근하는 방식이라고 했는데

findById는 DB를 바로 조회해서 필요한 데이터를 가져온다. 그래서 반환되는 객체 역시 데이터가 매핑되어 있는 실제

Entity 객체이다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void crudTest(){
        Optional<User> user = userRepository.findById(1L);
        
        System.out.println(user);
        
        User user2 = userRepository.findById(1L).orElse(null);
        // orElse는 값이 있다면 값을 반환하고 없다면 
        // ( ) 안에 명시한 다른 값을 반환한다.
        // 단 " " 를 통한 문자열 반환은 할 수 없다.
        
        System.out.println(user2);
}

위와 같은 형태로 사용할 수 있는데 findById는 Optional 타입으로 사용하거나 Entity 타입으로 사용할 수 있다.

 

 

 

findAll()

 

findAll() 메소드는 해당 테이블의 모든 데이터를 List로 가져오는 메소드이다.

쿼리문으로 보면 Select * from User  이런 느낌.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void crudTest(){
        
        userRepository.findAll().forEach(System.out::println);
        
        //forEach의 경우 가져온 리스트를 라인별로 출력하기 위해 사용한것.
        
        
        //name을 기준으로 내림차순 정렬
        List<User> users = userRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
        
        users.forEach(System.out::println);
        
}

이렇게 테이블의 모든 데이터를 가져올 수 있으며 Sort를 추가해 정렬된 데이터를 가져올 수 있다.

그래서 쿼리문 역시 order by user_.name desc 로 동작하는것을 로그에서 볼 수 있다.

 

 

findAllById()

 

findAllById는 여러개의 Id값을 통해 해당 데이터들을 조회하는 메소드이다.

예를들어 각각 1, 3, 5라는 Id값을 갖고 있는 3개의 데이터를 가져오려고 할때 사용한다.

@SpringBootTest
class UserRepositoryTest{

    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional
    void crudTest(){
        
        //Id 값이 1, 3, 5인 데이터를 가져오기
        
        List<User> users = userRepository.findAllById(Lists.newArrayList(1L, 3L, 5L);
        
        users.forEach(System.out::println);
        
        
        //아래와 같이 Id 값을 처리할 수도 있다.
        List<Long> ids = new ArrayList<>();
        ids.add(1L);
        ids.add(2L);
        ids.add(3L);
        
        List<User> users2 = userRepository.findAllById(ids);
        
        users2.forEach(System.out::println);
        
}

 

이렇게 실행하게 되면 각각 1, 3, 5의 Id 값을 갖고 있는 3개의 데이터가 출력된다.

그리고 로그에서 쿼리문을 보면 where에서 in절을 통해 가져오는것을 볼 수 있다.

 

 

 

 

 

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA

 

  • Optional
 

[Java] Optional 관련..

Spring Data JPA 사용 시 Repository에서 리턴 타입을 Optional로 바로 받을 수 있도록 지원하고 있습니다.Optional을 사용하면 반복적인 null 체크를 줄일 수 있기 때문에 잘 사용하면 매우 편리한 것 같습니

velog.io

 

  • flush
 

[JPA] 영속성 컨텍스트와 플러시 이해하기

영속성 컨텍스트 JPA를 공부할 때 가장 중요한게 객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과 영속성 컨텍스트를 이해하는 것 이다. 두가지 개념은 꼭 알고 JPA를 활용하자.

ict-nroo.tistory.com

 

  • save() 와 saveAndFlush()의 차이
 

[JPA] save 와 saveAndFlush의 차이

OS : MacOs Mojave DB : MySQL 5.7 DB Tool : Sequel Pro Framework : Spring Boot 2.0 맨밑에 결론있음 1. 준비 다음과 같은 member Entity를 준비했다. public class Member { @Id @GeneratedValue(strategy =..

ramees.tistory.com

 

 

'Spring' 카테고리의 다른 글

JPA JpaRepository 메소드 paging (findAll(pageable))  (0) 2022.02.03
JPA JpaRepository 메소드 (count(), existsById~(), delete~())  (0) 2022.01.31
JPA 기초  (0) 2022.01.26
JPA란?  (0) 2022.01.13
IOC(Inversion Of Control)  (0) 2021.02.21

제목을 JPA 기초라고 하긴 했으나

이 포스팅에서는 정말 기본적인 설정과 어노테이션 정도만 포스팅.

 

예제 환경

  • Intelli J
  • SpringBoot 2.6.2
  • Lombok
  • Gradle 7.3.2

 

 

JPA는 의존성 추가를 해야 사용할 수 있다.

Gradle 환경이기 때문에 build.gradle에 jpa 의존성을 추가해준다.

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

Maven Repository에 jpa만 검색해도 나오긴 하는데 Spring boot Starter data jpa로 사용.

 

 

ORM은 DB에서 테이블과 자바 객체간의 연결을 만들어 주는것인데 여기서 자바 객체가 Entity이다.

그래서 Entity를 먼저 생성하는데 프로젝트에 domain 패키지를 만들고 User 클래스를 생성한다.

 

그리고 @Entity 어노테이션을 붙여준다.

이때 Persistent entity 'User' should have primary key라는 오류가 발생하는데 User라는 Persistent Entity에는 primary key가 존재해야 한다는 것이다.

 

이 오류를 해결하기 위해서는 하나의 필드를 생성한 뒤 @Id를 붙여주면 해결된다.

그럼 @Id라는 어노테이션이 붙은 필드가 기본키가 된다.

 

@Data
@Entity
public class User{

    @Id
    private Long id;
}

 

이 Id 어노테이션은 getter 메소드에 붙여 사용할 수도 있지만 setter 메소드에 사용하게 되면 예외가 발생하게 된다.

그리고 필드에 붙여주게 되면 실행시점에 객체의 필드를 통해 직접 접근하게 된다.

 

공부한 예제에서는 lombok으로 @Data를 사용했기 때문에 그냥 필드에 붙여 사용했다.

 

JPA 에서는 Repository에서 Entity를 조회하는 경우 기본 생성자를 이용하기 때문에 거의 필수적으로

기본생성자가 필요하다.

그렇기 때문에 거의 무조건 @NoArgsConstructor를 붙여주거나 lombok을 사용하지 않는 경우라면

기본생성자를 생성해줘야 한다.

 

그리고 테스트예제에서는 @NoArgsConstructor를 그대로 사용했지만 정리하려고 여기저기 알아보다보니 그대로 사용하는것은 좋지 않다고 한다.

도메인 개념을 포함하는 별도의 생성자가 존재하더라도 해당 어노테이션으로 생성되는 기본 생성자를 그대로 둔다면, 별도의 생성자를 사용해야 하는지를 망각하고 개발하는 실수를 할 수 있고 그럼 도메인 개념에 해를 가하는 객체가 생성될 수 있기 때문이라고 한다.

@NoArgsConstructor(access = AccessLevel.PROTECTED)

그래서 위와 같이 AccessLevel을 protected로 설정해 최대한 기본 생성자를 감추도록 해야 한다.

이렇게 해도 문제가 발생하지 않는 이유는 jpa에서 protected 수준의 기본 생성자까지는 찾을 수 있기 때문이라고 한다.

 

@RequiredArgsConstructor를 명시해야 하는 경우도 있다.

이 어노테이션의 경우 꼭 필요한 인자만 받는 생성자인데 일반적으로는 아무 설정 없이 사용한다면 NoArgsConstructor와 동일하게 동작한다.

필수로 받아야 하는 인자에 대한 명시가 없기 때문인데 그래서 꼭 사용해야 하는 경우가 null이 되면 안되는 필드가 존재할때다.

 

@NonNull이라는 어노테이션이 존재하는데 이 경우 의미 그대로 null이 되면 안된다는 것이다.

그럼 이렇게 NonNull 어노테이션을 명시한 필드가 존재한다면 NoArgsConstructor는 동작할 수 없게 된다.

@AllArgsConstructor의 경우는 모든 인자를 받아야 하는 생성자이기 때문에 @RequiredArgsConstructor가 필요하게 되는것이다.

 

조금 난해한 점은 @Data, @NoArgsConstructor, @AllArgsConstructor 이렇게 하면 Data 어노테이션 내부에 RequiredArgsConstructor가 있기 때문에 잘 될것 같지만 생성자를 찾을 수 없다는 오류가 발생해 @RequiredArgsConstructor를 다시 명시해줘야 한다.

왜 이러는지 찾아봤지만 언급되어 있는 포스팅을 찾지 못했다.....

 

좀 정리하자면 필드 중 null이 되면 안되는 필드가 존재한다고 했을 때 해당 필드에 @NonNull을 붙여주고 이때의 생성자 처리는 NoArgsConstructor로 해결할 수 없기 때문에 NonNull 필드를 필수로 받게 되는 RequiredArgsConstructor를 명시해 줘야 한다. 단, Data 안에 있더라도 왜인지 알 수 없지만 오류가 발생하기 때문에 다시 명시해줘야 한다.

 

EqualsAndHashCode의 경우 강의 에서는 jpa에서 사용할일은 별로 없으나 자바에서 기본적으로 객체의 동등성을 비교하기 위해 사용하는것을 권고하고 있다고 했다.

하지만 '객체를 set에 저장한 뒤 필드 값을 변경하면 hashCode가 변경되면서 이전에 저장한 객체를 찾을 수 없다는 문제가 발생하기도 한다' 라는 이유로 지양해야 한다는 글도 봤다.

그래서 이 어노테이션에 대해서는 좀 더 공부가 필요할것 같다.

 

마지막으로 @Builder 어노테이션인데 필드값을 주입해주는데 Builder의 형식을 갖고 제공하는 것이다.

User user = User.builder()
        .name("aa")
        .email("aa@gmail.com")
        .build();

이런 형태로 사용한다. setName(), setEmail로 필드값을 주입하는것과 동일한 결과를 갖는다.

 

 

 

다시 Entity의 Primary key 부분으로 돌아온다.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User{
    
    @Id
    @GeneratedValue
    private Long id;
}

primary key는 String으로 한다거나 뭐 여러가지 방법이 많겠지만 공부하는 완전 초반에만 해도 MySQL에서 Auto Increment나 Oracle에서 Sequence를 통해 자동 증가를 많이 사용했었다.

 

사실 편하기도하고...

그래서 Id는 Long타입으로 설정하고 자동 증가하도록 작성한 예제다.

이때 자동 생성해주는 어노테이션이 @GeneratedValue인데 위에서 언급한것처럼 MySQL의 Auto Increment나 Oracle의 Sequence.NEXT_VAL 같은 형태다.

 

이 어노테이션의 경우 default로 AUTO를 갖고 있다.

strategy로 전략을 변경할 수 있는데 설정할 수 있는 경우는 아래와 같다.

 

AUTO의 경우는 DB 종류에 따라 JPA가 알맞은것을 선택하게 된다.

IDENTITY는 기본 키 생성을 데이터베이스에 위임하는것인데 MySQL, SQL Server, DB2에서 사용이 가능하다.

SEQUENCE는 데이터베이스 시퀀스를 사용해서 기본키를 할당하는 것으로 Oracle, PostgreSQL, DB2, H2에서 사용한다.

마지막으로 TABLE은 키 생성 전용 테이블을 만들어서 sequence 처럼 사용하는 것이다.

하지만 TABLE은 모든 데이터 베이스에서 사용이 가능하지만 최적화 되어 있지 않은 테이블을 사용하기에 성능문제에 이슈가 있다.

 

그럼 이제 Entity에 대한 제일 기초적인 내용은 끝이다.

 

그럼 이제 Entity 객체를 저장하고 조회하기 위해서는 Repository가 필요하다.

Repository는 interface로 생성하며 JpaRepository를 상속받아 사용한다.

 

JpaRepository의 경우는 제네릭 타입으로 <Entity, IdType>을 받는다.

 

그래서 User Entity에 대한 Repository는 아래와 같이 작성한다.

public interface UserRepository extends JpaRepository<User, Long>{


}

이렇게 JpaRepository를 상속받는 것으로 상단 이미지에 보이는 JpaRepository의 많은 메소드들을 활용해 여러 기능들을 사용할 수 있다.

 

그리고 JpaRepository는 또 PagingAndSortingRepository와 QueryByExampleExecutor를 상속받고 있는 것을 볼 수 있다.

이 인터페이스들 역시 들어가서 확인해보면 여러 메소드들을 사용할 수 있다는 것을 볼 수 있고

PaingAndSortingRepository는 CrudRepository를 또 상속받고 있다.

 

그래서 이렇게 확인해보면 결국 CrudRepository에 정의 되어 있는 메소드들이 기본적으로 사용하게 될 메소드들이라는 것을 알 수 있다.

JpaRepository가 아닌 CrudRepository를 상속받아 사용해도 될 정도로 많은 메소드들이 정의 되어 있다.

 

CrudRepository는 Repository 인터페이스를 상속받고 있는데 이 인터페이스의 경우는 최상위 Repository이며 아무것도 존재하지 않는다.

이 인터페이스는 Jpa에서 사용하는 domain Repository 타입이라는 것을 알려주기 위한 인터페이스이고 실제 메소드에 대한 정의는 존재하지 않는다.

 

 

그럼 이제 여기까지가 기본적인 Entity와 Repository 설정 및 생성 그리고 확인해야 할 부분이고

사용하는 예제에 대한 정리는 다음 포스팅부터 정리!

 

 

 

 

 

Reference

  • 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA
  • NoArgsConstructor

    https://galid1.tistory.com/729

 

Spring JPA - JPA를 이용해 Commerce App 만들기 - 3 (엔티티 개발)

Spring JPA - JPA를 이용해 Commerce App 만들기 - 3 (entity 작성) 이번 시간에는 지난 포스팅에서 다룬 설계를 토대로하여, Entity 클래스를 작성해보도록 하겠습니다. 우선 Entity를 개발하기 앞서 패키지 구

galid1.tistory.com

  • GeneratedValue 전략

    https://velog.io/@gudnr1451/GeneratedValue-%EC%A0%95%EB%A6%AC

 

@GeneratedValue 전략

직접 기본키를 생성하는 방법 @Id 어노테이션 만을 사용하여 기본키를 직접 할당해주는 방법이 있다. 기본키를 자동으로 생성하는 방법 4가지 > 기본키를 자동으로 생성할 때에는 @Id와 @GenerratedVa

velog.io

  • Lombok 에서 지양해야 할 Annotation에 대한 포스팅

    https://velog.io/@rosa/Lombok-%EC%A7%80%EC%96%91%ED%95%B4%EC%95%BC-%ED%95%A0-annotation

 

Lombok - 지양해야 할 annotation

자바에서 @Getter, @Setter 같은 annotation 기반으로, 기존 DTO, VO, Domain Class 작성할 때, 멤버 변수에 대한 Getter/Setter Method, Equals(), hashCode(), ToString()과 멤버 변수에 값을

velog.io

 

'Spring' 카테고리의 다른 글

JPA JpaRepository 메소드 (count(), existsById~(), delete~())  (0) 2022.01.31
JPA JpaRepository 메소드 (save(), find~())  (0) 2022.01.28
JPA란?  (0) 2022.01.13
IOC(Inversion Of Control)  (0) 2021.02.21
AOP(Aspect Oriented Programming)  (0) 2021.02.20

 

JPA(Java Persistance Api)는 간단하게 데이터에 접근하기 위한 API의 규격을 정의한 것이다.

JPA는 ORM에 속하기 때문에 ORM을 먼저 알아야 할 필요가 있다.

 

ORM이란?

  ORM(Object Relational Mapping)은 뜻 그대로 객체와 데이터베이스 사이의 관계를 연결해주는 것이다.

  객체지향 프로그래밍은 class 를 사용하고 관계형 데이터베이스는 Table을 사용한다.

  객체 모델과 관계형 모델간의 불일치가 존재할 수 있는데 이 때 ORM을 통해 객체간의 관계를 바탕으로 SQL을 자동

  생성해 불일치를 해결하게 된다.

  그래서 ORM을 Persistant API 라고도 할 수 있다.

 

  ORM의 장점으로는 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 집중 할 수 있도록 도와준다.

  ORM을 이용하게 되면 SQL Query가 아닌 메소드로 데이터를 조작할 수 있어 개발자가 객체 모델로 프로그래밍하는데

  집중할 수 있게 된다.

  선언문, 할당, 종료와 같은 부수적인 코드가 없거나 급격히 줄어들게 되며 각종 객체에 대한 코드를 별도로 작성하기

  때문에 코드의 가독성을 올려준다.

 

  그리고 ORM은 독립적으로 작성되어 있고 해당 객체들을 재활용 할 수 있기 때문에 재사용 및 유지보수의 편리성이

  증가한다.

  그렇기 때문에 모델에서 가공된 데이터를 컨트롤러에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데

  유리하다.

 

  DBMS에 대한 종속성도 줄어들게 되는데 객체간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의

  데이터 구조와 Java의 객체지향 모델 사이의 간격을 좁힐 수 있다.

  종속성이 줄어들기 때문에 개발자는 Object에 집중하고 DBMS를 교체하는 작업에도 비교적 적은 리스크와 시간이

  소요된다. 

  또한 자바에서 가공하는 경우엔 equals, hashCode의 오버라이드 같은 자바의 기능을 이용할 수 있으며 간결하고

  빠른 가공이 가능하다.

 

  단점은 ORM으로만 서비스를 구현하기가 어렵다.

  사용하기는 편하지만 설계를 매우 신중하게 해야하며 프로젝트의 복잡성이 커질수록 난이도가 올라갈 수 있다.

  구현이 잘못된 경우에는 속도 저하가 발생할 수 있고 심각한 경우 일관성이 무너지는 문제점이 생길 수 있다.

  일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는 등 별도의 튜닝이 필요한 경우가 있다.

  그리고 DBMS 의 고유 기능을 이용하기 어려운데 이것은 마냥 단점으로만 보기 어려운게 특정 DBMS의 고유기능을

  이용하면 이식성이 저하될 수 있기 때문이다.

 

  프로시저가 많은 시스템에서는 다시 객체로 바꿔야 하며 그 과정에서 생산성 저하나 리스크가 많이 발생할 수 있기

  때문에 ORM의 객체 지향적인 장점을 활용하기가 어렵다.

 

  세분성(Granularity), 상속성(Inheritance), 일치(Identity), 연관성(Associations), 탐색(Navigation)등의 특성에서

  객체-관계간의 불일치가 생긴다.

 

  세분성은 경우에 따라 데이터베이스에 있는 테이블의 수보다 더 많은 클래스를 가진 모델이 생길 수 있다는 것이고

 

  상속성은 RDMBS는 객체지향 언어의 특징인 상속의 개념이 없는 것이다.

 

  일치는 RDBMS에서는 기본키(primary key)를 이용해 동일성을 정의하지만 자바에서는 객체식별(a == b)와 객체

  동일성(a.equals(b))을 모두 정의 하는데에서 발생하는 차이다.

 

  연관성은 객체지향 언어에서는 방향성이 있는 객체의 참조(reference)를 사용하여 연관성을 나타내지만 RDMBS에서는

  방향성이 없는 외래키(foreign key)를 이용해서 나타내는 점이다.

 

  탐색은 자바와 RDBMS에서 객체를 접근하는 방법이 근본적으로 다른 것이다. 자바는 그래프 형태로 하나의 연결에서

  다른 연결로 이동하며 탐색하지만 RDBMS에서는 일반적으로 SQL문을 최소화 하고 JOIN을 통해 여러 Entity를

  로드하고 원하는 대상 Entity를 선택하는 방식으로 탐색한다.

 

  간단하게 정리하자면 ORM의 장점은 완벽한 객체지향적인 코드, 재사용, 유지보수, 리팩토링의 용이성, DBMS의

  종속성 하락이 있으며 단점으로는 ORM으로만 모든것을 해결 할 수 없는것과 세분, 상속성, 연관성 등에서 오는

  객체-관계 간의 불일치가 있다.

 

JPA란?

  그럼 JPA란 '데이터에 접근하기 위한 API의 규격을 정의한 것이다.'라고 했다.

  JPA는 자바 진영의 ORM 기술 표준으로 인터페이스의 모음이다.

  인터페이스이기 때문에 Hibernate, OpenJPA등이 JPA를 구현한다.

 

  ORM이 전체적인 개념이라고 한다면 JPA는 좀 더 구체적으로 그 기능을 정의했다고 볼 수 있다.

  JPA를 사용하게 되면 객체를 DB에 저장하고 관리할 때, 개발자 대신 적절한 SQL을 생성해서 DB에 전달하고, 객체를

  자동으로 Mapping해주기 때문에 개발자가 직접 SQL을 작성하지 않아도 된다.

 

  JPA는 내부적으로 JDBC API를 활용하는데 개발자가 직접 JDBC API를 활용하면 패러다임 불일치, SQL 의존성 등으로

  인해 효율성이 떨어지게 된다. 이때, JPA를 활용한다면 모든 SQL에 대해 개발자 대신 JPA가 자동으로 해결해 준다는

  점에서 생산성을 크게 높여준다.

 

  JPA는 반복적인 CRUD SQL을 처리해주기 때문에 개발자는 어떤 SQL이 실행될지 생각만 하면 된다.

  그리고 JPA는 Native SQL이란 기능을 제공해주는데 관계 매핑이 어렵거나 성능에 대한 이슈가 우려되는 경우 직접

  SQL을 작성해 사용할 수도 있다.

 

  JPA를 사용해 얻을 수 있는 가장 큰 것은 SQL이 아닌 객체 중심으로 개발할 수 있다는 것이다.

 

 

 

Reference

  ORM

    https://gmlwjd9405.github.io/2019/02/01/orm.html

 

[DB] ORM이란 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

    https://geonlee.tistory.com/207

 

ORM(Object Relational Mapping)이 뭘까? 🤔

ORM이란? ORM은 Object Relational Mapping 즉, 객체-관계 매핑의 줄임말이다. 객체-관계 매핑을 풀어서 설명하자면 우리가 OOP(Object Oriented Programming)에서 쓰이는 객체라는 개념을 구현한 클래스와 RDB(R..

geonlee.tistory.com

  JPA

    https://velog.io/@jwkim/JPA-JPA%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

 

[JPA] JPA란 무엇인가?

[JPA] JPA란 무엇인가?

velog.io

    https://dbjh.tistory.com/77

 

[Spring JPA] JPA 란?

이번 글에서는 JPA(Java Persistence API)가 무엇인지 알아보려고한다. JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음이다. 그 말은 즉, 실제적으로 구현된것이

dbjh.tistory.com

 

 

 

 

 

 

 

'Spring' 카테고리의 다른 글

JPA JpaRepository 메소드 (save(), find~())  (0) 2022.01.28
JPA 기초  (0) 2022.01.26
IOC(Inversion Of Control)  (0) 2021.02.21
AOP(Aspect Oriented Programming)  (0) 2021.02.20
의존성 주입(Dependency Injection, DI)  (0) 2021.02.18

CI/CD라는 용어는 시도때도없이 언급되고 들려오는 용어다.

처음에는 정말 단순하게 '자동배포'만으로 생각했고 그거 그냥 설정만 조금 해놓으면 버튼하나눌러서 하는거 아닌가? 했었던 때도 있었다...

지금도 깊게 아는것도 아니고 개념만 아는 정도지만 Jenkins 포스팅 한 김에 정리해두려고 한다.

 

 

CI는 Countinous Integration, 지속적인 통합이라는 의미이다.

지속적인 통합은 새로운 코드 변경 사항이 정기적으로 빌드 및 테스트 되어 공유 Repository에 통합되는것을 의미한다.

CI를 잘 구현할 경우 변경 사항이 적용 될 때 여러명의 개발자가 동시에 작업을 하더라도 서로 충돌할 수 있는 문제를 해결할 수 있다.

즉, 팀에서 개발을 진행하는 경우 다수의 개발자에 의해 쌓이는 commit들을 그때마다 기능별 빌드/테스트/병합 하려면 번거롭고 이때마다 발생되는 오류에 대해 체크해야 하는 상황이 생길 수 있다.

CI는 그것을 자동화해 수고를 덜어주고 테스트를 통과한 코드만이 Repository에 올라가기 때문에 좋은 퀄리티 유지가 가능하다.

 

CI의 목표로는 버그를 신속하게 찾아 해결하고 소프트웨어 품질 개선, 새 업데이트의 검증 및 릴리즈 시간 단축을 목표로 한다고 볼 수 있다.

 

 

CD는 Countinous Deliverty, 지속적인 제공이라는 의미와 Continous Deployment, 지속적인 배포라는 의미가 있다.

CI에서 빌드, 테스트, 병합까지 수행하고 나면 CD는 그 후에 이루어져야 할 배포를 진행하는 것이다.

 

지속적인 제공이란 CI가 수행되고 난 후 개발자가 검증 한 뒤에 배포를 수동적으로 하는 경우다.

 

지속적인 배포의 경우 CI가 수행되고 난 후 문제가 없다면 자동화를 통해 바로 배포할 수 있도록 하는것이 지속적인 배포이다.

 

 

CI/CD가 잘 구축이 되어 있다면 개발자가 배포에 너무 많은 시간을 소요하지 않아도 된다는 장점이 생기기 때문에 개발에 더 집중할 수 있는 환경이 된다고 볼 수 있다.

 

대표적인 CI/CD 툴로는 Jenkins / Travis CI / Bamboo 등이 있다.

 

 

'Web' 카테고리의 다른 글

JWT 2. JWT 구현  (0) 2022.12.01
JWT 1. JWT란?  (0) 2022.11.25
Jenkins github webhook 연동  (0) 2021.12.28
Jenkins로 Spring Boot 프로젝트 빌드&배포하기  (0) 2021.12.24

+ Recent posts