준영속 엔티티는 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 말함.

Book객체는 이미 DB에 한번 저장되어서 식별자가 존재하는데 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티라고 볼 수 있다. 

 

예시로 테스트를 하나 작성해보면 여기서 JPA에서 book 클래스를 찾아서 이름은 저렇게 바꿔주고 트렌젝션이 커밋이되면 이름이 바뀌게되는데 이게 변경 감지(더티체킹)이다. (내가 원하는 걸로 업데이트를 칠 수 있음)

 

주문 취소 로직에서도 동일하게 값을 cancle로 바꿔주는걸 해줬음.(알아서 JPA가 커밋 시점을 찾아서 DB업데이트를 해줌)

 

하지만 준영속 엔티티란 JPA의 영속성 컨텐츠가 더이상 관리를 해주지않는 상태를 말함.(영속성 엔티티는 JPA가 다 보고있어서 변경감지가 트렌젝션 커밋시점에 일어남)

 

준영속 엔티티를 변경할 수 있는 두가지 방법

1. 변경감지 기능 사용

2. 병합(merge) 사용

 

변경 감지 기능 사용

findItem으로 찾아온것은 영속 상태임. 

그럼 파라미터로 넘어온 book으로 값을 세팅을 다 했으면 스프링의 트렌젝셔널에 의해서 트렌젝션 커밋이 됨.

커밋이 되면 JPA는 플러시(영속성 컨텍스트중에 변경된 것이 뭔지 찾는것)를 날림 >  바뀐 값을 업데이트 쿼리를 날려서 DB에 업데이트를 시킴. 

 

 

병합 사용

준영속 상태의 엔티티를 영속 상태로 변경할때 사용

그냥 위에있는 코드를 한줄로 요약한것임

 

병합 동작 로직

1. merge() 실행

2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회

2-1. 만약 1차 캐시에 엔티티가 없으면 데이터 베이스에서 엔티티를 조회하고 1차캐시에 저장.

3. 조회한 영속성 엔티티에 엔티티의 값을 채워 넣는다.

4. 영속 상태인 엔티티를 반환한다. 

 

간단히 하자면 

1. 준영속 엔티티의 식별자 값으로 영속 엔티티 조회

2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 변경(병합)

3. 트렌젝션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 업데이트 sql이 실행

 

주의할점 : 변경감지 기능을 사용하면 원하는 속성만 선택해서 값을 변경 할 수 있지만 병합 기능으로 사용한다면 모든 속성이 변경됨.

병합시 값이 없으면 null로 업데이트 할 위험도 있음(병합은 모든 필드를 교체)

실무에서는 null값이 들어갈 위험도 있으니 병합보다는 변경 감지 기능을 통해 준영속 엔티티를 업데이트 하는게 좋음

(컨트롤러에서 어설프게 엔티티 생성하지 않기, 트렌젝션이 있는 서비스 계층에 식별자(id)와 변경할 데이터 명확하게 전달하기,

트렌젝션이 있는 서비스 계층에서 영속 상태의 엔티티 조회하고, 엔티티의 데이터 직접 변경하기 > 트렌젝션 커밋 시점에 변경 감지 실행)

 

컨트롤러에서 어설프게 엔티티를 생성하지 않기 위해서\

이렇게 아까 만든 업데이트item 를 사용하여서 값을 파라미터로 넘겨주었고,

업데이트 item도 값을 받아와서 바로 변경하게끔 값을 넣어주었다.

(파라미터가 많으면 새로운 DTO 클래스를 파서 값을 넣어주기)

회원 등록과 회원 목록을 조회할수있는 기능을 구현 해볼것이다.

우선 회원 등록을 위해선 회원 등록 form이 필요하기때문에 이것먼저 구현할 것이다.

(입력받을 값은 많이는 말고 이름, 지역, 거리명, 우편번호를 받을것이다.)

이때 이름 입력을 필수로 받기 위해서 NotEmpty 어노테이션을 사용하여서 필수로 입력되게끔 해줄것이다.

이때 이 어노테이션을 사용하기 위해서는 gradle파일에 

implementation 'org.springframework.boot:spring-boot-starter-validation'

이것을 추가해줘야한다.

그 후

멤버 컨트롤러를 구현해준다.

목록을 조회해줄땐

이렇게 멤버 리스트를 html 파일에 전달을 해주면 된다.

 

다음으로는 상품 등록과 상품 목록 조회를 진행해볼것이다.

우선 책의 form형인 BookForm 클래스를 만들어준다

 

아까 만들어둔 클래스에 대한 것을 model로 리소스 하위파일인 html파일에 데이터를 넘겨줘서 화면을 구성해주고,

postmapping을 통하여 여러 값들을 넣어주고 넣어진 값들이 저장이 됐으면 redirect시켜준다.

 

목록을 조회할땐 저번과 마찬가지로 list를 뽑아서 model을 사용하여서 넘겨준다.

 

JPA 동적 쿼리를 개발할건데 사용자의 이름과 상태를 통해서 검색을 하는 검색 기능을 만들어볼 것이다.

여러 방법들로 해볼건데

우선 1번째 방법(무식한 방법 jpql에 문자열 때려넣기)

 

하지만 이런식으로 문자를 더하면서 하는것은 엄청 힘든 방식임(버그를 찾기가 힘듬)

 

다음으로는 동적쿼리를 Criteria를 사용하여서 해결해보겠다.

하지만 criteria를 사용하여서 동적 쿼리를 처리하였을때의 단점이 있다.

유지보수가 거의 불가능하다.

 

다음으로는 Querydsl로 처리를 해볼것이다.

우선 테스트 코드 작성시에 멤버에 대한 정보를 세팅해주고 book 아이템 객체를 생성하여서 이름, 가격, 재고를 설정해주었다.

그 후 order메서드를 사용하여서 주문을 걸어주었고 추출된 order id로 findOne 메서드를 사용하여서 주문객체를 받아와주었다.

 

 

테스트 목록: 주문시 상태, 주문시 상품의 종류 수, 주문한 상품의 총 가격, 주문 수량만큼 전체 재고에서 빠지는지

 

돌려본 결과 이상 없이 잘 돌아갔다.

 

다음으로는

예외 테스트로 상품 주문 수량이 총 재고보다 많을시에 상황을 테스트 할것이다.

나는 Junit5로 테스트 코드를 작성중이라 Junit4에 있는 test어노테이션에 쓸수있는 expected옵션이 없어서 

대신 assertThrows를 사용하여 람다 형식으로 예외 발생을 검증해주었다.

 

다음은 주문 취소 테스트 케이스를 구현해볼 것이다.

주문 취소 테스트 케이스는 주문 생성 후 주문 취소를 시키고 주문 취소가 된 상태가 CANCLE이 됐는지 검증

추가적으로 재고가 원복되었는지 검증해주었다.

구현 해야할 기능

- 상품 주문 기능

- 주문 내역 조회 기능

- 주문 취소 기능

 

주문 엔티티, 주문 상품 엔티티 개발

 

주문을 생성하는게 많이 복잡함

그래서 이렇게 복잡한건 별도의 생성 메서드가 있으면 좋다. 따라서 order 클래스에 생성 메서드를 만들어준다.

 

다음으로는 비즈니스 로직을 구현할 것이다.

주문 취소 로직 작성시에는 배송이 되어버린 상태라면 취소를 못하는 케이스를 넣었고,

주문 취소시에는 order상태를 cancle로 만들어주고 반복문을 이용하여서 cancle을 처리해주어야한다(소비자가 상품을 2개 주문했으면 2가지 모두에 취소를 해야하기 때문)

그래서 orderItem 클래스에도 cancle 메서드를 만들어줘야 한다.

 

 

다음으로는 비즈니스 로직 이외로 조회로직도 만들어줘야 한다.

우선 전체 가격 조회 로직을 만들어보겠다.

이렇게 for문으로 돌면서 totalprice를 가져와서 더하는 방식으로해야하는 이유는 orderItem 내부에 상품 가격과 주문 수량이 있기때문에 orderItem 클래스 내부에서 그 둘을 곱한 가격을 가져와야한다.

 

 

stream을 사용하여서 코드 축소

 

orderItem에서도 생성 메서드를 만들어줄 것이다.

 

 

주문 리포지토리 개발

 

다음으론 orderservice 클래스를 만들어야하는데

이때 구현해야 할 기능들로는 주문 기능, 취소 기능, 검색 기능 을 구현해야한다.

우선 주문 기능을 구현했다

 

다음으로는 주문 취소 기능을 구현할것이다.

주문 취소 기능은 orderid로 주문을 추출하여서 cancle메서드로 취소하는 방식으로 하였다.

 

다음은 예외 테스트로 상품 주문 갯수 재고 초과 테스트를 해볼것이다.

 

Junit5를 사용하여서 테스트 하고있기때문에 junit4에서 지원하는 test 어노테이션에 expected를 사용할수 없기에

assertThrows를 사용하여 람다 형식으로 검증을 해주었다.

 

상품 엔티티 개발

 

상품을 주문할때 재고가 늘고 줄고 하는 로직을 넣어야한다.

근데 여기서 값을 변경해줄땐 다른곳에서 참조해서 변경하는거보다 그냥 그 엔티티 자체에서 변경하는게 좋다.

비즈니스 로직중 재고 수량 관리 로직을 구현해볼것이다.

Item 클래스에서 값을 바꾸는 메서드를 작성해줌.

 

상품 리포지토리 개발

아이템 저장, 단일조회, 전체조회 기능을 구현할 것이다.

 

상품 서비스 개발

아이템 저장기능, 전체조회, 단일조회 기능을 넣어줄것이다.

Transactional 어노테이션은 기본 디폴트 값이 readOnly=false이므로 맨 최상단에 readOnly= true로 설정해주고 

개발된 기능들중 조회가 아닌 기능들에만 위에 Transactional어노테이션을 붙여주면된다.

(조회가 아닌 저장 기능에 readOnly = true 옵션을 적용해버리면 저장이 안됨)

 애플리케이션 아키텍처

유연하게 컨트롤러에서도 레포지토리 접근 가능하게 할것임.

 

개발순서: 서비스, 리포지토리 계층을 개발하고, 테스트 케이스를 작성해서 검증, 마지막에 웹 계층 적용

 

우선 회원 등록 및 회원 목록 조회 기능을 만들어 볼 것이다.

 

우선 멤버리포지토리를 만들어준다.

(JPQL 문법을 사용하여서 멤버 리스트 호출 및 특정 이름 리스트 호출하는것 잘보기)

 

그 다음으로는 멤버 서비스 클래스를 만들어줄 것이다.

멤버 서비스 클래스에서는 회원가입, 중복 회원 검증, 회원 전체조회, 회원 단일 조회 기능을 넣었다.

@RequiredArgsConstructor 어노테이션을 사용하여서 멤버 리포지토리와 멤버 서비스 클래스의 코드를 줄여주었다.

 

이제 구현한 기능들을 테스트 코드로 검증 할 것이다.(Junit 5로 테스트 케이스 작성)

테스트 요구사항

- 회원가입 성공

- 동일 이름있으면 에러 케이스 발생

 

회원가입 테스트 코드를 작성해주었다.

생성한 멤버객체에 이름을 설정해주고 가입을 시킨후 assertEquals 메서드를 사용하여 가입시킨 멤버객체와 가입시 도출된 id값으로 findOne메서드를 사용하여서 도출된 객체와 비교를 하여서 테스트를 진행하였다.

 

다음은 중복 회원 검증 테스트 코드를 작성하였다.

Junit4에서는 테스트 어노테이션에 expected 설정을 줄수있지만 나는 Junit5로 테스트를 진행중이기에 

assertThrows를 사용하여 람다형식으로 테스트를 진행해주었고, 같은 이름으로 가입시에 예외 케이스가 도출되도록 테스트를 진행하였다.

정적인 컨텐츠는 static, 데이터를 동적으로 받아오는 컨텐츠는 templates

 

근데 여기서 

이 html 글자 하나를 수정하고 다시 업데이트하려면 서버를 아예 껐다가 다시 켜야한다(굉장히 소모적임)

그래서 devtools라는 라이브러리를 사용하여서 이런 소모적인 일들을 줄여줄것임.

그 후 build에 해당 html 파일만 리컴파일 시키면 해당 파일이 업데이트가 되는걸 볼 수 있다.

 

h2데이터 베이스를 사용하기에 h2를 깔아주고 폴더의 bin폴더로 들어가서 권한을 주고 h2 터미널에서 데이터 베이스를 실행시킨다

그 후 웹사이트가 하나 실행되는데 그때 앞에있는 주소를 localhost로 변환하여 입력해주면

이렇게 창이 뜨게되는데 이때 처음 서버를 연결할땐 jdbc:h2:~/파일명
이런식으로 처음 연결을 해줘야한다 그래야 파일명으로된 .mv.db폴더가 생기게된다

그 후 첫 연결을 제외한 추후 연결은 

jdbc:h2:tcp://localhost/~/jpashop

이렇게 적어주고 연결을 하면된다

들어가면 이렇게 나오고 여기서 이제 데이터 베이스를 만들면된다.

정적인 컨텐츠는 static, 데이터를 동적으로 받아오는 컨텐츠는 templates

 

근데 여기서 

이 html 글자 하나를 수정하고 다시 업데이트하려면 서버를 아예 껐다가 다시 켜야한다(굉장히 소모적임)

그래서 devtools라는 라이브러리를 사용하여서 이런 소모적인 일들을 줄여줄것임.

그 후 build에 해당 html 파일만 리컴파일 시키면 해당 파일이 업데이트가 되는걸 볼 수 있다.

 

h2데이터 베이스를 사용하기에 h2를 깔아주고 폴더의 bin폴더로 들어가서 권한을 주고 h2 터미널에서 데이터 베이스를 실행시킨다

그 후 웹사이트가 하나 실행되는데 그때 앞에있는 주소를 localhost로 변환하여 입력해주면

이렇게 창이 뜨게되는데 이때 처음 서버를 연결할땐 jdbc:h2:~/파일명
이런식으로 처음 연결을 해줘야한다 그래야 파일명으로된 .mv.db폴더가 생기게된다

그 후 첫 연결을 제외한 추후 연결은 

jdbc:h2:tcp://localhost/~/jpashop

이렇게 적어주고 연결을 하면된다

들어가면 이렇게 나오고 여기서 이제 데이터 베이스를 만들면된다.

어플리케이션 프로퍼티스파일을 지우고 yml파일로 새로 만든후 이렇게 데이터베이스 관련 옵션을 적어준다.

근데 여기서 h2 데이터 베이스에 접근할때 저 MVCC라는 설정이 들어가있는 주소로 들어가면되는데

최신버전에서는 저 부분 옵션이 빠져서 그냥 저걸 제외한 url로 연결 하면됨.

 

@@주의할점

yml파일 작성할때 jpa 부분 줄바꿈안해서 에러남...이유 못찾아서 2시간 버림....

 

기본적인 클래스 생성 후 테스트 코드 작성

기본 세팅은 해줬으니 이제 간단한 기능을 만들어서 테스트 코드를 작성하여 검증해볼것이다.

우선 Member 클래스를 만들어준다.

아까 lombok을 설정해줬기에 getter setter의 코드를 에노테이션으로 확 줄일 수 있다.

 

그 후 기능들을 정의해줄 Member repository 클래스를 만들어준다

여기선 멤버 저장 및 조회를 만들었고 저장시에는 멤버의 id 값을 반환해주고 멤버 조회시에는 id값을 파라미터로 받아서 jpa에서 멤버의 id로 멤버를 찾아서 반환해준다.

 

테스트 코드 작성

 

 

테스트코드를 이런식으로 작성해주었다.

테스트 코드 작성시에 junit으로 작성해주게되는데 여기서 4버전에는 RunWith 에노테이션으로 따로 Spring.runner.class를 해주어야하는데 나는 junit5버전으로 하여서 그부분은 생략하였다.

우선 멤버 객체를 만들어주고 유저이름을 설정해준다.

그 후 멤버 객체를 아까 구현한 멤버 레포지토리의 save기능을 이용하여서 저장해주고 id값을 받아온다(아까 save기능 구현시에 id값을 반환하게 만듬)

그 후 findMember라는 객체에 반환한 id값을 넣어서 find기능으로 멤버를 찾아준다.

그렇다면 이제 저장된 유저와 찾은 유저가 동일한지 판단해야하는데 이건 Assertions.assertEquals(기대값, 실제값)을 통하여 검증해주었다.

저장한 멤버객체와 찾은 멤버객체 각각 id를 추출하는 getId 메서드를 사용하여 id를 검증해주고 마찬가지로 getUsername메서드를 통해 이름 또한 검증해주었다.

 

쿼리 파라미터 로그

쿼리 파라미터 로그를 찍어주기 위해선 이 trace 옵션을 설정해줘야한다 그 후 테스트 케이스를 돌려보면

잘 나오는걸 확인할수있고 다른 라이브러리를 사용하여서 봐도된다

나는 p6spy를 사용하여서 해보겠다.

이렇게 스프링 버전에 맞는 걸 깔아주고 bulid.gradle을 리프레시 시켜준다.

그 후 돌려보면

이렇게 들어가진 데이터가 잘 나오는걸 확인할수있다

하지만 운영 및 배포과정에서는 성능 테스트를 해보고 적용하여야한다.

성능 저하의 원인이 될 수 있기에 성능 테스트 후 > 이정도면 받아들일만하다라고 판단하면 적용.

+ Recent posts