JPA Repositori를 통한 일반적인 검색 기능은 앞장에서 SQL코스에서 여러가지방법으로 조회를 해보았습니다.

이번장에서는 영속성(Persistence)을 이용한 저장/수정/삭제 기능을 살펴보겠습니다.

CRUD는 검색(읽기) 기능을 포함하고 있습니다.

Code Link :http://git.webnori.com/projects/WEBF/repos/spring_jpa/browse/src/test/java/com/example/demo/jpa/JpaCRUD.java


Persitence



Persitence는 다음과 같은 사전적의미를 가지고 있습니다.


(lightbulb) persistence 두번째의미이며, 상태를 저장하고 유지한다는점에서 IT전반적으로 사용되어지는 단어입니다.


몇가지 연관 되어 사용되는 용어를 정리하였습니다.

  • persistent route : 웹호출시 노드가 변경안되도록 라우터가 IP기반으로 유지해줍니다. 서버캐시기능을 가진 웹서비스에서 중요한 요소입니다.
  • persitent FSM : AKKA에서 사용되는 단어이며, 분산환경에서 액터의 상태머신을 유지해줍니다.
  • redis persitence : 메모리DB인 redis의 상태가 휘발되지 않게 IO장치에도 저장하는 기능입니다.
  • rdb persitence : 가장 대표적인 데이터베이스입니다. 지속성보다 영구성의 의미가 더강하며 가장 강력합니다.

JPA Repository



JPA Repository는 목적에따라 3가지로 구분이됩니다. 역활이 지정이 되지않았다고 하면

JpaRepository를 사용하면 모든 기능 활용이 가능합니다.

  • PaginAndSortingRepository : 페이징과 소팅이되는 조회용 저장소
  • CrudRepository : 데이터 업데이트에 관련된 저장소
  • JpaRepository : 위 두가지 기능 모두 활용가능하며 추가로 일부 영속성관련 제어가능


저장소를 이용한 다양한 방식으로 조회, SP이용,네이티브쿼리사용등 앞장에서

실습을 한 내용을 간략하게 다시 요약 해보겠습니다. 

검색및 집계 처리

검색 함수쿼리

장점: 바람직한 네이밍룰에의해 검색 기능을 작동시킬수있다.

단점: 아규멘트가 길어질수록,네이밍도 같이 길어진다.

public interface AddressRepo extends CrudRepository<Address, Long>{
	
	List<Address> findBySex(String sex);
	
	List<Address> findBySex(String sex, Sort sort);
	
	List<Address> findBySexOrderByAgeDesc(String sex);
	
	List<Address> findByAgeGreaterThan(int age);
	
	List<Address> findByAgeGreaterThanEqual(int age);
		
	List<Address> findByAgeLessThan(int age);
		
	List<Address> findByAgeBetween(int low,int high);
	
	List<Address> findByAgeGreaterThanAndSex(int age,String sex);
	
	List<Address> findByAgeGreaterThanOrSex(int age,String sex);
		
	List<Address> findByAgeIn(int age[]);
}

JPQL을 사용한 집계처리

장점:SQL친화적으로 검색 인터페이스를 작성할수 있다.

단점:지원되지 않는 SQL문은 작동안한다.

	@Query("select new com.example.demo.data.AddressStatistics(t.address,AVG(t.age)) from Address t where t.age > :minage GROUP BY t.address HAVING AVG(t.age) > :filterage ")
	List<AddressStatistics> findRegionAvgage(
			@Param("minage") int minage,
			@Param("filterage") double filterage
			);


Native 쿼리

장점:네이티브 SQL문을 맘것 활용할수 있다.

단점:맵핑및 아규멘트전달에 어려움이 있으며, 다른 DB로의 호환성을 포기해야할수도 있다.

	@Query(value="SELECT address,age,name,phoneNbr,sex FROM address " + 
			"UNION " + 
			"SELECT address,age,name,phoneNbr,sex FROM address2 ",
			nativeQuery = true)
	List<?> makeUniOn();


SQL 윈도우함수를 대체하는 랭킹처리

장점:특수한 기능구현이 가능하고 어플리케이션에게 연산부담을 시킬수있다.

단점:커스텀한 로직이 늘어나 관리가 안될수 있으니 , 효율적인 SQL문 작성법부터 알아보자

	public void RankTest() {		
	     int[] score = {Integer.MIN_VALUE};
	     int[] no = {0};
	     int[] rank = {0};	
	     List<AddressAgeRank> ageRankList =  
				addressRepo.findByAgeBetween(10, 90).stream()
				.sorted((a,b) -> b.getAge() - a.getAge() )
				.map(p -> {
		             ++no[0];
		             if (score[0] != p.getAge()) rank[0] = no[0];
		             return new AddressAgeRank(p.getName(),score[0] = p.getAge(),  rank[0] );					
				})
				.collect(Collectors.toList());
	     
	     ageRankList.forEach(item ->{
	    	 System.out.println(item.toString());	    	 
	     });	    
	}


QueryDSL 지원이되는 저장소

장점:여러가지 검색옵션을 조합을 할수 있으며, 대표적으로 null 인자값에대해 유연하게 대처할수 있다.

단점: 초기셋팅이 번거럽고, DSL사용을위하  QEntityObject가 메타빌드로 자동추가 생성이 된다. (어플리케이션이 엔티티증가에따라 무거워진다는것)

//인터페이스 선언부
public interface AddressRepoDSL extends JpaRepository<Address, Long>,
QueryDslPredicateExecutor<Address>{
	
}
//사용부
	@Autowired
	private AddressRepoDSL addressRepoDSL;
	public void jpa_queryDslTest() {
		
		QAddress	userAddress = QAddress.address1;
		BooleanBuilder builder = new BooleanBuilder();
		
		builder.and(userAddress.name.eq("민수1") )
			   .and(userAddress.address.like("%" + "서" +"%"));		
				
		Iterable<Address> addressList =  addressRepoDSL.findAll(builder);
		addressList.forEach( item -> {
			String itemString = String.format("%d%s %s %s %s %s",item.getId(),item.getName(),
					item.getPhoneNbr(), item.getSex(),item.getAddress(),item.getAge());
			System.out.println(itemString);
		});
		
	}


참고 원본문서:


데이터 변경(Insert,Update,Delete)

스토어 프로시져 이용

장점: 전통적인 SP개발 방식을 이용할수 있다.

단점: 형상관리 분리로 인해, 서비스로직을 분리 관리해야하며, 상태가 없기때문에 JPA 중간저장소 기능을 100%활용못할수 있음  

예를 들면, 높은 TPS요구에대해 EntityManager의 커넥션풀 기능이 비활성화되어 커넥션 Max에러가 발생할수 있음(직접관리해야함)

CREATE OR REPLACE FUNCTION addaddress(
	address character varying,
	age integer,	
	name character varying,
	sex character varying,
	phonenbr character varying )
 
RETURNS integer AS $BODY$	
	BEGIN 
		INSERT INTO address ( address, age, name,sex,phonenbr) VALUES( address, age, name,sex,phonenbr);
		return 0;
	END; 
$BODY$
LANGUAGE plpgsql VOLATILE
public interface AddressRepo extends CrudRepository<Address, Long>{
	@Procedure
	public Integer addaddress(String address,Integer age, String name, String sex, String phonenbr);	
}


//SP 호출방법 : 커넥션풀 관리가 안되어, Loop로 트랜잭션을 유발하는것은 위험합니다.
addressRepo.addaddress("독도", 45, "포켓몬", "남", "010-1233-3321");


//호출로그 : 실제 호출로그에는 아규멘트가 5개가아닌 6개입니다. 이것은 리턴처리가,OUT 파라메터로 자동연결되었기때문입니다. 
Hibernate: 
    {call addaddress(?,?,?,?,?,?)}


more info: http://roufid.com/3-ways-to-call-a-stored-procedure-with-hibernate-jpa-2-1/


Persitence Context



PersitenceContext 는 JPA를 이해하는데 중요한 단어이며 , 엔티티를 저장하고 유지하는 논리적인 장치입니다.

이것은, JPA에서 Persitence를 이용할때 데이터베이스에 직접 쿼리를 날리지않고 중간에 어떠한 개체가 개입을 함을 의미합니다.

DBMS자체도 사용자의 질의에 반응하여, 즉시 물리적인 디스크에 IO를 발생시키지  않는다는점에서 유사한점이 있습니다.

DBMS에서 이루어지는 몇가지 중요기능인  캐시처리/지연쓰기 등을 JPA 어플리케이션에서도 활용이 가능해보입니다.

이것이 필요없으면  DBMS의 성능을 믿고 , 모두 즉시 처리전략을 선택할수 있습니다.

어쨋든  이러한 영속성기능은 동일성을 유지하는 조건내에서, DBMS가 디스크 IO접근을 최소화하려는 노력과 비슷하게

JPA Persitence는  데이터 베이스 접근을 최소화하여 성능을 높일수있는 논리적인 장치를 제공해줍니다. 



엔티티의 4가지 상태

  • 비영속(new/transient) : 객체만 생성되고 컨텐츠에 저장되기전
  • 영속(managed) : 영속 컨텐츠에 저장된 상태 
  • 준영속(detached) : 영속성 컨텐츠에 저장되었다가 분리된 상태
  • 삭제(removed) : 삭제된 상태


영속성 컨텍스트의 특징

  • 엔티티 식별자값 '@Id' 가 반드시 존재해야함 ( 기본키가 있어야된다는 의미)
  • 엔티티에 저장된(Managed) 엔티티는 트랜젝션에 커밋하는순간 반영되며, JPA에서는 Flush라고함
  • 이점: 1차캐시,동일성보장,트랜잭션을 지원하는 쓰기지연,변경감지,지연로딩



오라클과 비교해보는 JPA 영속성 장치

JPA의 영속성기능은 오라클 DBMS의 핵심기능중 축소된 Buffer Cache 처리기능과 유사해 보입니다.

필수적이지는 않으나 두가지 개념을 함께 익히면 JPA의 영속성개념을 익히는데 도움이 될듯합니다.

순서상 JPA와 별개로 데이터베이스를 잘 이용하기위해서 DBMS 아키텍쳐를 어렴풋이라도 살펴보는게 도움이되며

오라클에서 관련자료를 많이 찾을수 있습니다.

CRUD Repository



JPA에서는 기본적인 데이터 삽입/수정/조회를 위해 추가적인 구현없이 기본적인 기능을 제공하는

인터페이스를 제공해줍니다. 트랜젝션관리 영속성관리등을 직접할필요가 없기때문에 이용이 간단합니다.

단순한 데이터 변경 서비스에서는 CrudRepository만으로도 충분합니다.


인터페이스 준비

아래와 같이 조작할 엔티티(테이블)을 지정하는 인터페이스를 준비하면,

사용자테이블을 제어하기위한 기본 기능을 추가 구현없이 사용가능합니다.

/선언
public interface UserRepository extends CrudRepository<User, Long> {
}


서비스 로직정의

데이터 제어는, 제공되는 일괄적이고 단순한 save,delete,find 3가지만 이용하여

우리가 원하는 데이터제어의 서비스 코드를 작성할수가 있습니다. 

public class UserUpdateService{
....
  @Autowired
  private UserRepository userRepository;
 
  public User jpatest1C(String groupname,String name) {
		GroupInfo newGroup = new GroupInfo();
		newGroup.setName(groupname);
		groupRepository.save(newGroup);				
		// 사용자 생성
        User addUser = new User();
        addUser.setName(name);
        addUser.setEmail("test@x.com");
        addUser.setGroupInfo(newGroup);                        
        return userRepository.save(addUser);
	}
		
	public void jpatest1U(User user) {
		user.setName(user.getName() + "_mody");
		userRepository.save(user);
	}	
	
	public void jpatest1D(User user) {
		userRepository.delete(user);
	}	
	
	public void japtestList() {
        // 사용자 조회
        Iterable<User> userList = userRepository.findAll();      
        userList.forEach(item->System.out.println( String.format("V1 Name:%s  GroupName:%s", item.getName(),item.getGroupInfo().getName() )  ));	
	}	

사용예

OOP(객체) 접근방식을 통하여, 실제 데이터베이스의 저장소 변경을 할수가 있습니다.

샘플은, 사용자추가/업데이트/삭제/조회를 순차적으로 실행하였습니다.

트랜잭션은 Transactional 어노테이션을 사용하여, 트랜잭션 범위를 지정하여 활용할수 있습니다.

하지만 CRUDRepository에서는 각각의 최소단위가 이미 트랜잭션 단위이기때문에 CRUD저장소의

인터페이스를 순차적으로 사용하는곳에서 트랜잭션을 거는것은 의미가 없습니다.

CrudRepository는 트랜잭션 처리를 상당수 은닉화 하였기때문에, 복잡한 트랜잭션 처리가 요구되는곳에서는

JpaRepository 혹은 사용자정의 Repository에서 트랜잭션에 관련된 커밋,롤백등 직접반영하는것이 권장됩니다.

명령어를 통해 직접하는방식, 인터페이스에 트랜잭션 어노테이션을 지정하는방식 크게 두가지가 있으며

이 부분은 별도의 주제로 다시 살펴보겠습니다.


@Transactional	
public void jpa_test1() {		
		User userA = jpatest1C("학생A","minsu");
		User userB = jpatest1C("학생B","minsu");
		jpatest1U(userA);
		jpatest1D(userB);
		japtestList();
	}

실제로 아래와같은 SQL문이 차례로 수행됩니다.(SQL약식표현)

insert com.example.demo.data2.User – minsu,학생A 추가

insert com.example.demo.data2.User – minsu,학생B 추가

update com.example.demo.data2.User –첫번째 추가된 minsu의 이름변경

delete com.example.demo.data2.User –두번째 추가된 minsu data 삭제


japtestList() ==> 결과( 첫번째 추가된 이름이 변경된 minsu만 조회됨

V1 Name:minsu_mody  GroupName:학생A


Custom Repository



조회및 트랜잭션에 관련된 비지니스 로직이 복잡할시 CrudRepository의 인터페이스로는 

구현하기가 힘들어질수도 있습니다. ( 상당수 추상화되어 간단한 기능만 제공하기 떄문에 )

이경우 복잡한 비지니스 로직에대해 영속성을 고려한 디테일한 트랜잭션 전략을 구성할수가 있습니다. 

여기서 앞장에서 설명하지 못한 JPA의 중요요소인 영속성에대한 기능도 함께 설명하여 살펴보도록하겠습니다.

인터페이스 준비

public interface MyAddressRepository  {
	void someTest(Address2 address2);
}


public class AddressRepositoryImpl implements MyAddressRepository { 
	@PersistenceContext
	private EntityManager em;
	  
	@Override
	@Transactional
	public void someTest(Address2 address2) {
        // 트랜젝션처리가 단순한 저수준의 em객체를 이용할수가 있습니다.
		em.refresh(address2);		
		em.detach(address2);
		em.persist(address2);
		em.lock(address2, null);
		em.flush();
	}	  
}
public interface AddressRepository extends CrudRepository<Address2, Long>,MyAddressRepository {

}

네이밍 룰

네이밍설명
MyAddressRepository

인터페이스이며, 커스텀함 함수규격을 미리 설명을 해놓아야합니다.

네이밍 특성 : 특성+테이블명+저장소

My + Address + Repository

AddressRepositoryImpl

커스텀한 구현체를 정의합니다.

네이밍 특성: 테이블명+저장소+구현

MyAddressRepositoryImpx ( X )

AddressRepository

최종 사용가능해진 저장소이름입니다. JPA의 기본 저장소 특성과함께

사용자 정의 기능을 함께 사용가능합니다.

네이밍특성: 테이블명+저장소



영속성을 활용한 쓰기지연

위 그림이 중요한 이유는, 각 상태를 이해하고 즉시쓰기전략이 아닌 지연쓰기 전략의

장점을 이해하는것과,  주의점을 아는것입니다. 영속성의 사이클 변화에서

상태가 2단계를 건너 뛸수 없음을 의미합니다.

예를 들어 Remove된 엔티티는 deatach(준영속) 상태가 될수 없습니다. 


영속성을 이해하기위해서, 하나의 함수(트랜젝션에) 다양한 데이터 변경 처리를 진행해보겠습니다.

사용예

public class AddressRepositoryImpl implements MyAddressRepository { 
	@PersistenceContext
	private EntityManager em;
	
	@Override
	@Transactional
	public void addAddress() {
		
		//비영속( 단순하게 엔티티 객체만 생성함)
		Address2 address2 = new Address2();
		address2.setName("영희");
		address2.setSex("여");
		address2.setAddress("서울/마포");
		
		//엔티티 저장하고, 준영속화(분리함)
		em.persist(address2);
		em.detach(address2);
		
		// 첫번째 데이터를 찾아서 삭제대기...
		Address2 firstAddress = em.find(Address2.class, 1L );
		em.remove(firstAddress);
				
		//엔티티를 다시 영속 관리객체로 복귀
		em.merge(address2);
		//추가된 엔티티의 데이터가 변경되었음으로 변경감지.
		address2.setName("영희2");
				
		//마지막 상태 Db반영(쓰기지연)
		em.flush();
		
		//준영속
		//em.detach(address2);		
		//다시영속시킴
		//em.merge(address2);				
		//예외발생-롤백
		//assertTrue(false);
	}	

Hibernate:
/* insert com.example.demo.data.Address2
*/ insert
into
Address2
(address, age, name, phoneNbr, sex)
values
(?, ?, ?, ?, ?)


Hibernate:
select
address2x0_.ADDRESS_ID as ADDRESS_1_1_0_,
address2x0_.address as address2_1_0_,
address2x0_.age as age3_1_0_,
address2x0_.name as name4_1_0_,
address2x0_.phoneNbr as phoneNbr5_1_0_,
address2x0_.sex as sex6_1_0_
from
Address2 address2x0_
where
address2x0_.ADDRESS_ID=?


Hibernate:
/* load com.example.demo.data.Address2 */ select
address2x0_.ADDRESS_ID as ADDRESS_1_1_0_,
address2x0_.address as address2_1_0_,
address2x0_.age as age3_1_0_,
address2x0_.name as name4_1_0_,
address2x0_.phoneNbr as phoneNbr5_1_0_,
address2x0_.sex as sex6_1_0_
from
Address2 address2x0_
where
address2x0_.ADDRESS_ID=?


Hibernate:
/* delete com.example.demo.data.Address2 */ delete
from
Address2
where
ADDRESS_ID=?


코드분석:

  • 단지 '@Transactional' 어노테이션을 함수에 선언함으로, 어플리케이션에서 발생하는 예외까지 트랜젝션 과정에 포함시켜, 자동 롤백을 시킬수가 있습니다.
  • 기존 SQL/SP개발 방식에서는 하나의 함수에 즉각적인 삽입과 삭제가이루어 졌을것입니다. 하지만 지연쓰기방식은 변경점을 조사하여 변경이 없는 부분은 SQL문을 수행하지 않습니다.
  • 즉 , 실제 DB에 반영하는 flush() 타임시 변경점만을 반영을 합니다. 


장점요약:

  • 어플리케이션 코드내에서 예외에 대한 트랜젝션처리를 유연하게 할수가 있습니다.
  • 변경점만 반영됨으로, 불필요한 쿼리호출을 방지할수 있으며 이것은 OOP접근만으로 작동이 가능합니다.
  • 이와같은 혼합처리를 위해 전통적인 방식에서 SP개발은 여러개의 단위 SP를 만들어야할것입니다. SQL문은 반복적으로 여러개 준비되어야할것입니다.


단점요약:

  • DB를 저장하는 새로 생긴 중간 단계에 익숙하지 못할수 있습니다.
  • 영속성을 사용하기위해 준비해야할 단계가 많으며, 어플리케이션 자체가 무거워지는것은 분명한 사실입니다.
  • 데이터중심에서 메시지중심개발로 패러다임을 변경해야하며 디테일한 메시지 처리가 필요하며 결론적으로 데이터를 단순하계 설계해야합니다.
  • 데이터를 단순하계 설계한다는것? 데이터의 군더더기를 뺀다란 의미이며 불필요한것을 빼고 동일한 기능을 보장하고 성능을 올린다는것은 훨씬 어려운 주제입니다.




  • No labels