기록은 기억의 연장선

더 많은 것을 기억하기 위해 기록합니다


  • Home

  • Tags

  • Categories

  • Archives

  • Search

[db] 트랜잭션 격리 수준(isolation level)

Posted on 2019-01-06 | Edited on 2020-11-02 | In db | Comments:

트랜잭션 격리수준(isolation level)이란 동시에 여러 트랜잭션이 처리될 때, 트랜잭션끼리 얼마나 서로 고립되어 있는지를 나타내는 것이다.
즉, 간단하게 말해 특정 트랜잭션이 다른 트랜잭션에 변경한 데이터를 볼 수 있도록 허용할지 말지를 결정하는 것이다.

격리수준은 크게 아래의 4개로 나뉜다.

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ
  • SERIALIZABLE

아래로 내려갈수록 트랜잭션간 고립 정도가 높아지며, 성능이 떨어지는 것이 일반적이다.
일반적인 온라인 서비스에서는 READ COMMITTED나 REPEATABLE READ 중 하나를 사용한다.
(oracle = READ COMMITTED, mysql = REPEATABLE READ)

ISOLATION LEVEL 조회, 변경, 테스트

  • 조회
1
2
3
SHOW VARIABLES like 'tx_isolation';
-- 또는
SELECT @@tx_isolation;
  • 변경
    나와있는데로 해봤는데 안바뀌는데 방법좀…

READ UNCOMMITTED

READ UNCOMMITTED 격리수준에서는 어떤 트랜잭션의 변경내용이 COMMIT이나 ROLLBACK과 상관없이 다른 트랜잭션에서 보여진다.
이 격리수준에서는 아래와 같은 문제가 발생할 수 있다.

  1. A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꿈
  2. 아직 커밋하지 않음
  3. B 트랜잭션에서 10번 사원의 나이를 조회함
  4. 28살이 조회됨

    이를 더티 리드(Dirty Read)라고 한다

  5. A 트랜잭션에서 문제가 발생해 ROLLBACK함
  6. B 트랜잭션은 10번 사원이 여전히 28살이라고 생각하고 로직을 수행함

이런식으로 데이터 정합성에 문제가 많으므로, RDBMS 표준에서는 격리수준으로 인정하지도 않는다.

READ COMMITTED

어떤 트랜잭션의 변경 내용이 COMMIT 되어야만 다른 트랜잭션에서 조회할 수 있다.
오라클 DBMS에서 기본으로 사용하고 있고, 온라인 서비스에서 가장 많이 선택되는 격리수준이다.

여기서는 B 트랜잭션에서 10번 사원의 나이를 조회해도 27살이 조회된다.(커밋되지 않았기 때문에)
(이는 언두 영역에 저장된 데이터이다. MVCC 참조)
A 트랜잭션에서 최종 커밋하면 B 트랜잭션에서 28살 이라는 값을 받아볼 수 있다.

언뜻보면 정합성 문제가 해결된 것 처럼 보이지만, 여기서도 NON-REPETABLE READ 부정합 문제가 발생할 수 있다.

  1. B 트랜잭션에서 10번 사원의 나이를 조회
  2. 27살이 조회됨
  3. A 트랜잭션에서 10번 사원의 나이를 27살에서 28살로 바꾸고 커밋
  4. B 트랜잭션에서 10번 사원의 나이를 다시 조회(변경되지 않은 이름이 조회됨)
  5. 28살이 조회됨

이는 하나의 트랜잭션내에서 똑같은 SELECT를 수행했을 경우 항상 같은 결과를 반환해야 한다는 REPEATABLE READ 정합성에 어긋나는 것이다.
일반적인 웹 어플리케이션에서는 크게 문제되지 않지만, 작업이 금전적인 처리와 연결되어 있다면 문제가 발생할 수 있다.
예를 들어 여러 트랜잭션에서 입금/출금 처리가 계속 진행되는 트랜잭션들이 있고
오늘의 입금 총 합을 보여주는 트랜잭션이 있다고하면, 총합을 계산하는 SELECT 쿼리는 실행될 때 마다 다른 결과값을 가져올 것이다.

이런 문제가 발생할 수 있기 떄문에 격리수준에 의해 실행되는 SQL 문장이 어떤 결과를 출력할 지 정확히 예측하고 있어야 한다.

REPETABLE READ

REPETABLE READ 격리수준은 간단하게 말해서
트랜잭션이 시작되기 전에 커밋된 내용에 대해서만 조회할 수 있는 격리수준이다.
MySQL DBMS에서 기본으로 사용하고 있고, 이 격리수준에서는 NON-REPETABLE READ 부정합이 발생하지 않는다.

REPETABLE READ

  1. 10번 트랜잭션이 500000번 사원을 조회
  2. 12번 트랜잭션이 500000번 사원의 이름을 변경하고 커밋
  3. 10번 트랜잭션이 500000번 사원을 다시 조회
  4. 언두 영역에 백업된 데이터 반환

즉, 간단하게 말해서 자신의 트랜잭션 번호보다 낮은 트랜잭션 번호에서 변경된(+커밋된) 것만 보게 되는 것이다.
(모든 InnoDB의 트랜잭션은 고유한 트랜잭션 번호(순차적으로 증가하는)를 가지고 있으며,
언두 영역에 백업된 모든 레코드는 변경을 발생시킨 트랜잭션의 번호가 포함되어 있다.)

REPETABLE READ 격리수준에서는 트랜잭션이 시작된 시점의 데이터를 일관되게 보여주는 것을 보장해야 하기 때문에
한 트랜잭션의 실행시간이 길어질수록 해당 시간만큼 계속 멀티 버전을 관리해야 하는 단점(?)이 있다.
하지만 실제로 영향을 미칠 정도로 오래 지속되는 경우는 없어서… READ COMMITTED와 REPETABLE READ의 성능차이는 거의 없다고 한다.

REPETABLE READ에서 발생할 수 있는 데이터 부정합

  1. UPDATE 부정합

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    START TRANSACTION; -- transaction id : 1
    SELECT * FROM Member WHERE name='junyoung';

    START TRANSACTION; -- transaction id : 2
    SELECT * FROM Member WHERE name = 'junyoung';
    UPDATE Member SET name = 'joont' WHERE name = 'junyoung';
    COMMIT;

    UPDATE Member SET name = 'zion.t' WHERE name = 'junyoung'; -- 0 row(s) affected
    COMMIT;

    이 상황에서 최종 결과는 name = joont가 된다.
    REPETABLE READ이기 때문에,
    2번 트랜잭션에서 name = joont로 변경하고 COMMIT을 하면 name = junyoung의 내용을 언두로그에 남겨놔야 한다.
    그래야 1번 트랜잭션에서 일관되게 데이터를 보는 것을 보장해줄 수 있기 때문이다.

    이 상황에서 아래 구문에서 UPDATE 문을 실행하게 되는데, UPDATE의 경우 변경을 수행할 로우에 대해 잠금이 필요하다.
    하지만 현재 1번 트랜잭션이 바라보고 있는 name = junyoung 의 경우 레코드 데이터가 아닌 언두영역의 데이터이고,
    언두영역에 있는 데이터에 대해서는 쓰기 잠금을 걸 수가 없다.

    그러므로 위의 UPDATE 구문은 레코드에 대해 쓰기 잠금을 시도하려고 하지만 name = junyoung인 레코드는 존재하지 않으므로,
    0 row(s) affected가 출력되고, 아무 변경도 일어나지 않게 된다.
    그러므로 최종적으로 결과는 name = joont가 된다. 자이언티가 되지 못해 아쉽다.

    간단하게 말해 DML 구문은 멀티버전을 관리하지 않는다

  2. Phantom READ
    한 트랜잭션 내에서 같은 쿼리를 두 번 실행했는데, 첫 번째 쿼리에서 없던 유령(Phantom) 레코드가 두 번째 쿼리에서 나타나는 현상을 말한다.
    REPETABLE READ 이하에서만 발생하고(SERIALIZABLE은 발생하지 않음), INSERT에 대해서만 발생한다.
    아래와 같은 상황에서 재현될 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    START TRANSACTION; -- transaction id : 1 
    SELECT * FROM Member; -- 0건 조회

    START TRANSACTION; -- transaction id : 2
    INSERT INTO MEMBER VALUES(1,'joont',28);
    COMMIT;

    SELECT * FROM Member; -- 여전히 0건 조회
    UPDATE Member SET name = 'zion.t' WHERE id = 1; -- 1 row(s) affected
    SELECT * FROM Member; -- 1건 조회
    COMMIT;

    REPETABLE READ에 에 의하면 원래 출력되지 않아야 하는데 UPDATE 문의 영향을 받은 후 부터 출력된다.
    이 시점에 스냅샷을 적용시키는 것 같다.

    참고로 DELETE에 대해서는 적용되지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    START TRANSACTION; -- transaction id : 1 
    SELECT * FROM Member; -- 1건 조회

    START TRANSACTION; -- transaction id : 2
    DELETE FROM Member WHERE id = 1;
    COMMIT;

    SELECT * FROM Member; -- 여전히 1건 조회
    UPDATE Member SET name = 'zion.t' WHERE id = 1; -- 0 row(s) affected
    SELECT * FROM Member; -- 여전히 1건 조회
    COMMIT;

SERIALIZABLE

가장 단순하고 가장 엄격한 격리수준이다.
InnoDB에서 기본적으로 순수한 SELECT 작업은 아무런 잠금을 걸지않고 동작하는데,
격리수준이 SERIALIZABLE일 경우 읽기 작업에도 공유 잠금을 설정하게 되고, 이러면 동시에 다른 트랜잭션에서 이 레코드를 변경하지 못하게 된다.
이러한 특성 때문에 동시처리 능력이 다른 격리수준보다 떨어지고, 성능저하가 발생하게 된다.

참고 : 이성욱, 『Real MySQL』, 위키북스(2012)

Read more »

[db] MVCC

Posted on 2019-01-06 | Edited on 2020-11-02 | In db | Comments:

Multi Version Concurrency Content 의 약자이며, Multi Version이라 함은 하나의 레코드에 대해 여러 버전이 관리된다는 의미이다.
일반적으로 레코드 레벨의 트랜잭션을 지원하는 DBMS가 제공하는 기능이며, 가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는데 있다.
(멀티버전 없이 일관된 읽기를 보장하려면 읽고 있는 레코드는 외부에서 수정이 불가능하도록 해야한다)
MySQL은 언두 로그를 이용해 이 기능을 구현한다.

가령 아래와 같은 업데이트 문을 실행하고,

1
UPDATE member SET area = '경기' WHERE id = 12

아직 COMMIT이나 ROLLBACK을 하지 않은 상태에서 사용자가(다른 트랜잭션에서) 아래와 같이 조회하면 어떻게 될까?

1
2
3
SELECT * 
FROM Member
WHERE id = 12

정답은 MySQL 초기화 파라미터에 설정된 격리 수준에 따라 다르다.
격리 수준이 READ_UNCOMMITED라면 버퍼 풀이나 데이터 파일로부터 데이터를 읽어서 반환하지만,
격리 수준이 READ_COMMITED 이상이라면 버퍼 풀이나 데이터 파일에 있는 데이터를 읽는 대신에 변경 이전의 내용을 보관하고 있는 언두 로그 영역의 데이터를 반환한다.

언두 로그

(디스크 데이터 파일에 데이터가 업데이트 되어있을 수도 있고, 아닐수도 있기 때문에 ???라고 표시되어있는데, 기본적으로 InnoDB는 ACID를 보장하므로 버퍼풀과 데이터파일은 같은 값이라고 봐도 무방하다)

보다시피 기본적으로 UPDATE 구문은 연산의 결과를 바로 레코드에 반영하고, 이전 데이터에 대해서는 멀티버전으로 관리한다.
이를 MVCC라고 한다. (INSERT나 DELETE도 멀티버전으로 관리하는지는 잘 모르겠다)

이렇게 관리되는 멀티버전은 데이터베이스 격리수준에 따라 보여지는게 달라지게 된다.

READ UNCOMMITTED면 애초에 멀티버전을 관리하지 않으므로 다른 트랜잭션에서 레코드의 내용을 바로 바라보게 되고,
READ COMMITTED면 커밋 전까지 다른 트랜잭션에 멀티 버전을 보여줄 것이고,
REPETABLE READ면 커밋을 해도 먼저 시작한 다른 트랜잭션에게는 멀티 버전을 보여줄 것이다

(참고로 위 처럼 UPDATE를 실행한 로우는 LOCK이 걸리기 때문에 외부에서 수정이 불가능하다)

위 상황에서 커밋을 실행하면

  • 더 이상의 변경작업 없이 지금 상태를 영구적인 데이터로 만들어버린다
  • 언두 로그 영역의 내용을 더 이상 필요로 하는 트랜잭션이 없을 경우, 언두 로그에서 해당 내용을 삭제한다.

    격리수준에 따라 삭제하는 시점이 다를 것이다

롤백을 실행하면

  • 언두 로그 영역에 있는 데이터를 복구한다
  • 언두 로그에서 해당 내용을 삭제한다.

참고 : 이성욱, 『Real MySQL』, 위키북스(2012)

Read more »

copyright, copyleft

Posted on 2019-01-04 | Edited on 2020-11-02 | In etc | Comments:

http://www.mediaus.co.kr/news/articleView.html?idxno=18500

이러한 저작권을 비롯한 지적재산권의 국제적 강화와 표준화는 산업국들의 이익을 대변한다는 비판과 함께 저작권에 반대하는 운동이 함께 진행되어 왔다. 바로 카피레프트(Copyleft)이다.
카피라이트(Copyright), 즉 저작권이 저작자가 저작물의 사용, 복제, 변경 및 배포를 금지하는 것과는 반대로,
카피레프트는 지적 창작물의 복제, 사용, 변경 및 배포 등을 자유롭게 할 수 있도록 허가하는 것이다.
즉, 저작권에 기반을 둔 ‘이용제한’이 아니라, 저작권을 기반으로 한 ‘정보공유’인 것이다. 다시 말하면,
저작물의 소유권을 주장해 사용을 제한하는 것과는 달리, 새로운 저작물은 이미 앞선 세대가 창작한 저작물들을 바탕으로 창조된 것이기에 개인자산일 뿐 아니라 인류의 공공자산임으로 모두에게 공유되어야 한다는 논리이다.
또한 소수 특권층에 의한 지식과 정보의 독점과 상업화에 대한 거부이기도 하다.

Read more »

google dark theme 적용하기

Posted on 2019-01-03 | Edited on 2020-11-02 | In etc | Comments:

Dark Theme for Google 확장 프로그램을 설치한다.
https://chrome.google.com/webstore/detail/dark-theme-for-google/apiabgjfojnkcepfmbdechlhfocpeenc

Read more »

[git] github dark theme 적용하기

Posted on 2019-01-03 | Edited on 2020-11-02 | In git | Comments:

https://github.com/StylishThemes/GitHub-Dark

위 레파지토리를 이용해 접근 가능하다.

  1. 확장 프로그램에서 Stylus 를 설치한다.

https://chrome.google.com/webstore/detail/stylus/clngdbkpkpeebahjckkjfobafhncgmne

  1. Installation의 첫번쨰 Install the usercss을 누른다.
  2. 왼쪽 상단에 install style을 누르면 바로 적용된다!!
  3. 이제 눈이 안아프다.
  4. 이제 눈이 안아프다.
Read more »

[git] github을 이용하는 전체 흐름

Posted on 2019-01-03 | Edited on 2020-11-02 | In git | Comments:

1편 : https://blog.outsider.ne.kr/865

2편 : https://blog.outsider.ne.kr/866

  1. repository fork
  2. local clone
  3. 개발하면서 local에 commit

브랜치는 맞춰주는 것이 좋고, 원본 저장소를 따로 라모트로 추가해서 개발 중 변경사항을 주기적으로 pull 받아 맞춰주는 것이 좋다

  1. 개발이 완료되면 본인 remote repository push
  2. 원본 repository로 pull request 생성

오픈소스 프로젝트마다 Pull Request를 받아주는 약속이 다르므로 보내기 전에 이를 먼저 확인해야 한다.
소스 수정이 잘 되었더라도 이 약속을 제대로 지키지 않으면 받아주지 않는다.
탭에서 Commits나 Files Changed를 클릭하면 Pull Request를 보내는 커밋과 변경사항이 제대로 되었는지 확인할 수 있다.

Pull Request를 이용한 개발 흐름 : https://blog.outsider.ne.kr/1199

  1. 작업 시작과 동시에 PR을 올려서 내 작업 진행과정을 투명하게 공유한다

다른 작업자들이 내 작업 진행과정을 알 수 있어서 나중에 merge 작업이나 새로운 작업 시작시에 편리함. 코드 리뷰를 할 수 있다는 장점도.

  1. PR Comment에 할 일 목록을 만들고 체크리스트로 표시하도록 함. 작업중일떄는 WIP, 작업이 끝났을 경우 DONE
Read more »

[java] stacktrace 읽는법

Posted on 2019-01-02 | Edited on 2020-11-02 | In java | Comments:

https://okky.kr/article/338405

  1. 첫번째 줄이 가장 마지막에 실행된 메서드
  2. 밑에 줄은 위의 메서드를 호출한 메서드
  3. CausedBy가 실제 발생한 에러 메세지임.
1
2
3
4
5
6
7
Exception in thread "main" java.lang.IllegalStateException: A book has a null property
at com.example.myproject.Author.getBookIds(Author.java:38)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
at com.example.myproject.Book.getId(Book.java:22)
at com.example.myproject.Author.getBookIds(Author.java:35)
... 1 more

이렇게 발생되었다면 보통 아래와 같이 구현된 것임

1
2
3
4
5
try {
....
} catch (NullPointerException e) {
throw new IllegalStateException("A book has a null property", e)
}
Read more »

[jpa] JPQL

Posted on 2019-01-01 | Edited on 2020-11-02 | In jpa | Comments:

JPA에서 현재까지 사용했던 검색은 아래와 같다.

  • 식별자로 조회 EntityManager.find()
  • 객체 그래프 탐색 e.g. a.getB().getC()

하지만 현실적으로 이 기능만으로 어플리케이션을 개발하기에는 무리이다.
그렇다고 모든 엔티티를 메모리에 올려두고 어플리케이션 내에서 필터하는 것은 현실성이 없는 소리이다.
즉, 데이터베이스에서 필터해서 조회해올 무언가가 필요하고, 그게 객체지향 쿼리 언어(JPQL)이다.

JPQL은 엔티티 객체를 조회하는 객체지향 쿼리 언어이다.
문법은 SQL과 비슷한데, 실제론 SQL을 추상화 한것이기 때문에 특정 데이터베이스에 의존하지 않는 특징이 있다.

SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
(참고로 엔티티 저장은 그냥 entityManager.persist를 사용하면 되므로 INSERT 문은 없다.)
JPQL에서 UPDATE, DELETE 문은 벌크 연산이라고 해서 뒤에서 따로 설명할 것이므로, SELECT 문만 작성하겠다.

기본 문법

기본 형태는 아래와 같다.

1
SELECT m FROM Member AS m WHERE m.username = 'Hello'
  1. 대소문자 구문
    • 엔티티와 속성은 대소문자를 구분한다. Member와 member는 다르고 username과 USERNAME은 다르다.
    • SELECT, FROM 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
  2. 엔티티 이름
    • FROM 이후에 오는 대상은 테이블 이름이 아니라 엔티티 이름이다.
    • 기본값인 클래스명을 엔티티명으로 사용하는 것을 추천한다.
  3. 별칭은 필수
    • JPQL은 별칭을 필수로 사용해야 한다. AS 뒤에 m이 Member의 별칭이다.
    • AS는 생략 가능하다.

TypedQuery, Query

작성한 JPQL을 실행시키기 위해 만드는 쿼리 객체이다.
JPQL이 반환할 타입을 명확하게 지정할 수 있으면 TypedQuery를 사용하고, 명확하게 지정할 수 없으면 Query를 사용하면 된다.

1
2
3
4
5
// 조회대상이 정확히 Member 엔티티이므로 TypedQuery 사용 가능
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);

// 조회대상이 String, Integer로 명확하지 않으므로 Query 사용
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");

TypedQuery로 실행된 쿼리는 두번쨰 인자로 주어진 클래스를 반환하고,
Query의 경우 예제처럼 조회 컬럼이 1개 이상일 경우 Object[], 1개일 경우 Object를 반환한다.

참고로 String 타입의 필드만 조회하고 TypedQuery<String[]> 를 사용하는 방식은 안된다.
하나하나 다 체크하기에는 너무 많으니까…

결과 조회

쿼리 객체에서 아래의 메서드들을 사용해 JPQL을 실행한다.

  • query.getResultList()

    결과를 컬렉션으로 반환한다. 결과가 없으면 빈 컬렉션이 반환된다. 1건이면 1건만 들어간 컬렉션이 반환된다.

  • query.getSingleResult()

    결과가 정확히 1건 일때 사용한다.
    결과가 없으면 javax.persistence.NoResultException, 결과가 1건 이상이면 javax.persistence.NonUniqueResultException이 발생한다.
    근데 얘는 Optional을 반환해야 하지 않을까?

파라미터 바인딩

아래와 같은 이름 기준 파라미터 바인딩을 지원한다.

1
2
3
4
5
TypedQuery<Member> query = 
em.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class)
.setParameter("username", "joont1"); // JPQL은 대부분 메서드 체인 방식으로 되어있어서 이렇게 연속해서 작성하는 것이 가능하다

List<Member> result = query.getResultLst();

username은 Member 클래스에 정의된 프로퍼티 이름이다. 앞에 :를 붙여서 바인딩한다.
username 에 joont1 이 바인딩 될 것이다.

참고로 아래와 같이 위치 기준 파라미터 바인딩도 지원하기는 한다.

1
2
3
TypedQuery<Member> query = 
em.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
.setParameter(1, "joont1");

이것보다는 전자가 더 명확하다.

참고로 LIKE 연산처럼 % 같은 특수문자가 필요할 경우 전달하는 파라미터에 붙여서 사용하면 된다.

1
2
3
TypedQuery<Member> query = 
em.createQuery("SELECT m FROM Member m WHERE m.username LIKE :username", Member.class)
.setParameter("username", "%joont%"); // 이런식으로

파라미터 바인딩 방식은 선택이 아닌 필수이다

  • JPQL에 직접 문자를 더하면 SQL Injection을 당할 수 있다
  • JPA에서 파라미터만 다를 뿐 같은 쿼리로 인식하므로, JPQL을 SQL로 파싱한 결과를 재사용할 수 있다
  • SQL 내에서도 같은 쿼리는 결과를 재사용한다

프로젝션

조회할 대상을 지정하는 것을 프로젝션이라고 한다.
SELECT [프로젝션 대상] FROM 으로 대상을 지정한다.
대상은 엔티티 타입, 임베디드 타입, 스칼라 타입이 있다.

엔티티 프로젝션

1
2
3
SELECT m FROM Member m // member

SELECT m.team FROM Memher m // team

둘 다 엔티티를 프로젝션 대상으로 사용했다.
참고로 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

임베디드 타입 프로젝션

엔티티를 통해서 조회한다.

1
2
3
Address address = 
em.createQuery("SELECT m.address FROM Member m", Address.class)
.getSingleResult();

임베디드 타입은 엔티티 타입이 아닌 값 타입이므로
이렇게 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.

스칼라 타입 프로젝션

1
2
3
4
5
6
7
8
9
10
11
// 이름조회
TypedQuery<String> query = em.createQuery("SELECT m.username FROM Member m", String.class);
List<String> resultList = query.getResultList();

// 이름조회(중복제거)
TypedQuery<String> query = em.createQuery("SELECT DISTINCT m.username FROM Member m", String.class);
List<String> resultList = query.getResultList();

// 통계 쿼리
TypedQuery<Double> query = em.createQuery("SELECT AVG(o.orderAmount) FROM Order o", Double.class);
List<Double> resultList = query.getResultList();

조회되는 컬럼이 1건이라 TypedQuery를 사용하였다. 보다시피 통계 쿼리도 스칼라 타입으로 조회할 수 있다.

여러 값 조회

아래와 같이 여러값으로 조회했을 때는 TypedQuery를 사용할 수 없고, Query만 사용할 수 있다.

1
2
3
4
5
6
7
8
Query query = em.createQuery("SELECT m.username, m.age, m.team FROM Member m");
List<Object[]> resultList = query.getResultList();

for(Object[] row : resultList){
String username = (String)row[0];
Integer age = (Integer)row[1];
Team team = (Team)row[2];
}

물론 아때도 조회한 엔티티는 영속성 컨텍스트에서 관리된다.

NEW 명령어

NEW 명령어를 사용하면 Object[] 대신 바로 객체로 생성해서 받아볼 수 있다.

1
2
3
4
TypedQuery<UserDTO> query = 
em.createQuery("SELECT NEW com.joont.dto.UserDTO(m.username, m.age, m.team) FROM Member m", UserDTO.class);

List<UserDTO> resultList = query.getResultList();

기존이라면 하나하나 번거롭게 변환했어야 했을 작업을 NEW 명령어를 사용해서 간단하게 처리했다.
NEW 명령어를 사용하려면 아래 2가지를 주의해야 한다.

  • 패키지명을 포함한 클래스명을 입력해야 한다.
  • 순서와 타입이 일치하는 생성자가 필요하다.

직접 쓰라고 있는 기능은 아닌 것 같다.
라이브러리들이 적절히 구현해라고 만들어놓은 기능인듯(QueryDSL 등)

페이징 API

JPA는 데이터베이스들의 페이징들을 아래의 두 API로 추상화했다.
(페이징은 데이터베이스마다 문법이 다 다르다)

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResult(int maxResult) : 조회할 데이터 수
1
2
3
4
5
6
TypedQuery<Member> query = 
em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);

query.setFirstResult(10);
query.setMaxResult(20);
query.getResultList();

11번쨰 데이터부터 시작해서 20개를 조회한다. 즉 11~30번 데이터를 조회하게 된다.
지원하는 모든 데이터베이스를 추상화했기 때문에 데이터베이스가 바껴도 방언만 바꿔주면 된다.

집합과 정렬

집합 함수

함수 설명 리턴타입
COUNT 결과 수를 구한다 Long
MAX, MIN 최대, 최소값을 구한다 대상에 따라 다름
AVG 평균값을 구한다. 숫자타입만 사용할 수 있다. 숫자가 아니면 0을 리턴한다. Double
SUM 합을 구한다. 숫자타입만 사용할 수 있다. 정수합 : Long
소수합 : Double

집합 함수 사용 시 참고사항

  • 통계를 계산할 때 NULL값은 무시된다(COUNT(*)은 제외).
  • 값이 없을 때 SUM, AVG, MAX, MIN를 사용하면 NULL을 리턴한다. COUNT는 0을 리턴한다.
  • DISTINCT를 집합 함수안에 사용하면 중복된 값을 제거하고 집합을 구한다.

그룹핑

GROUP BY, HAVING도 사용할 수 있다.

1
2
3
4
SELECT t.name, COUNT(m.age), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age)
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
HAVING AVG(m.age) >= 10

(team의 이름으로 그룹화한 뒤 나이의 평균이 10살 이상인 그룹에 대해서 집합을 구했다.)
문법은 아래와 같다.

group by절 : GROUP BY {단일값 경로 | 별칭}
having절 : HAVING 조건식

이런식의 통계 쿼리는 보통 전체 데이터를 기준으로 사용하므로 실시간으로 사용하기에는 부담이 많다.

정렬

ORDER BY도 사용할 수 있다.

1
2
3
4
SELECT t.name, COUNT(m.age) AS cnt  
FROM Member m LEFT JOIN m.team t
GROUP BY t.name
ORDER BY t.name ASC, cnt DESC

문법은 아래와 같다.

order by절 : ORDER BY {상태필드 경로 | 결과변수 [ASC | DESC]}

상태필드는 m.name 같이 객체의 상태를 나타내는 필드를 말하고,
결과변수는 SELECT 절에 나타나는 값을 말한다. 위의 예제에서는 cnt가 결과변수이다.

조인

내부 조인

1
SELECT m FROM Member m INNER JOIN m.team t

보다시피 일반적인 SQL 조인과 조금 다르다.
가장 큰 특징은 연관 필드를 사용해서 조인한다는 점이다.
즉, 조인을 사용하려면 엔티티에 연관관계 명시는 필수적으로 되어있어야 한다.
(위와 같이 작성하면 Member의 team 필드에서 관계의 정보를 얻은 뒤 조인할 것이다)

아래는 잘못 작성된 JPQL 조인이다.

1
SELECT m FROM Member m INNER JOIN Team t // 잘못된 조인

만약 조인한 두 개의 엔티티를 조회하려면 다음과 같이 작성하면 된다.

1
2
SELECT m,t
FROM Member m INNER JOIN m.team t

서로 다른 타입의 두 엔티티를 조회했으므로 TypedQuery는 사용할 수 없다.

1
2
3
4
5
6
List<Object[]> list = em.createQuery(jpql).getResultList(); // 위에서 작성한 쿼리

for(Object[] o : list){
Member m = (Member)o[0];
Team t = (Team)o[1];
}

외부 조인

키워드만 바꿔주면 된다.

1
SELECT m FROM Member m LEFT JOIN m.team t

컬렉션 조인

일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 말한다.
아래와 같이 컬렉션 값 연관 필드를 사용하면 된다.

1
SELECT t, m FROM Team t LEFT JOIN t.members m

세타 조인

CROSS JOIN을 말한다.
CROSS JOIN이란 일반적으로 INNER JOIN에 ON절을 주지 않을 것을 말한다.
그러므로 JPQL에서는 CROSS JOIN으로 외부 조인을 사용할 수 없다.
(조인시 ON절이 자동 생성되므로)

ON절

JPA 2.1부터 조인할 때 ON 절을 지원한다.

1
2
3
SELECT m, t 
FROM Member m LEFT JOIN m.team t
ON t.name = 'A'

실행 결과는 아래와 같다.

1
2
3
SELECT m.*, t.*
FROM Member m
LEFT JOIN Team t ON m.team_id = t.id AND t.name = 'A'

ON 절을 사용하면 조인 대상을 필터링 하고 사용할 수 있다.

패치 조인

패치조인은 SQL에 있는 개념은 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
간단하게 말해서 연관된 엔티티나 컬렉션을 한번에 같이 조회한 뒤에 대상 객체의 필드에 set 해서 내려주는 것이다.
문법은 아래와 같다.

fetch join : [ LEFT [ OUTER ] | INNER ] JOIN FETCH 조인경로

엔티티 패치 조인

1
2
SELECT m 
FROM Member m INNER JOIN FETCH m.team

실행해보면 연관 엔티티의 값을 채워주기 위해 M.*, T.*의 형태로 연관된 팀까지 함께 조회한다.
그리고 기존의 INNER JOIN 에서 Object[] 로 받아야 했던것 과는 달리, Member의 team 변수에 값이 다 채워진 상태로 리턴된다.
즉, 객체 그래프를 그대로 유지하면서 받을 수 있는 방법이다. 그러므로 성능 최적화를 위해 제공화는 기능이라고 하는 것이다.

1
2
3
4
5
6
List<Member> list = 
em.createQuery(jpql /* 위에서 작성한 쿼리 */, Member.class).getResultList();

for(Member m : list){
System.out.println(m.getTeam().getName()); // LAZY 로딩 발생 안함
}

(Member의 Team은 fetch가 LAZY라고 가정한다)
패치 조인을 통해 이미 연관된 팀을 같이 조히했으므로 위와 같이 수행해도 LAZY 로딩이 발생하지 않는다.

컬렉션 패치 조인

일대다 관계에서도 패치 조인을 사용할 수 있다.

1
2
SELECT t
FROM Team t INNER JOIN FETCH t.members

이것 또한 SELECT 절에 t 만 명시했음에도 불구하고, T.*, M.*의 형태로 연관된 회원까지 함께 조회된다.
근데 여기서 주의할 점이 있는데, 쿼리의 결과가 증가해서 그런지 위 jpql의 결과를 리스트로 받아보면 Team의 개수가 Member의 개수와 동일함을 볼 수 있다.

1
2
3
4
5
6
7
8
9
List<Team> list = em.createQuery(jpql, Team.class).getResultList(); // 위에서 작성한 쿼리

for(Team t : list){
System.out.println(t);

for(Member m : t.getMembers()){
System.out.println("-> " + m);
}
}

Team@0x100
-> Member@0x200
-> Member@0x300
Team@0x100
-> Member@0x200
-> Member@0x300

이렇듯 일대다 조인은 결과가 증가할 수 있음에 주의해야 한다.

DISTINCT

JPQL의 DISTINCT는 SQL에 DISTINCT를 추가하는 것은 물론이고, 어플리케이션에서 한번 더 중복을 제거한다.
이 특징을 이용해서 위의 컬렉션 패치 조인에서 리스트가 중복되서 나오는 문제를 해결할 수 있다.

1
2
SELECT DISTINCT t
FROM Team t INNER JOIN FETCH t.members

이렇게 작성하면 먼저 SQL에 DISTINCT가 적용된다.
하지면 지금은 로우의 데이터가 다르므로 DISTINCT는 효과가 없다.

다음으로 어플리케이션에서 DISTINCT 명령을 보고 중복된 데이터를 걸러낸다.
SELECT DISTINCT t는 Team 엔티티의 중복을 제거하라는 의미이므로, 여기에서 중복이 제거되고, 예상했던 결과를 받아볼 수 있게된다.

패치조인과 일반조인의 차이

위에서도 언급했지만, 일반조인의 경우 결과를 반환할 때 연관관계까지 고려하지 않는다.
단지 SELECT 절에 지정한 엔티티만을 조회하고, 연관된 엔티티에 대해서는 프록시나 컬렉션 래퍼를 반환한다.

1
2
3
4
5
List<Team> list = em.createQuery(jpql, Team.class).getResultList(); // 일반 조인 쿼리

for(Team t : list){
System.out.println(t.getMembers.get(0)); // ?
}

그러므로 위와 같이 조회하면
fetchType이 LAZY일 경우 ? 부분에서 LAZY 로딩이 발생할 것이고,
fetchType이 EAGER일 경우 회원 컬렉션을 즉시 로딩하기 위해 쿼리를 한번 더 실핼하게 된다.

패치조인의 특징과 한계

패치조인은 글로벌 전략보다 우선한다. (글로벌 전략 : 엔티티에 직접 적용하는 로딩 전략 e.g. fetch=FetchType.LAZY)
그러므로 최적화를 위해 글로벌 로딩 전략을 즉시 로딩으로 설정하기 보다는,
글로벌 전략은 지연 로딩으로 설정하고 최적화가 필요한 곳에서 패치조인을 사용하는 것이 전체적으로 봤을때 훨씬 효과적이다.

물론 이런 좋은 패치조인에도 한계가 있다.

  • 패치조인 대상에는 별칭을 줄 수 없다

별칭을 줄 수 없다는 말인 즉 SELECT, WHERE, 서브쿼리에 패치조인 대상을 사용할 수 없음을 말한다.
하이버네이트를 포함한 몇몇 구현체들은 패치조인에 별칭을 지정하는 것을 허용해주나 잘못 사용하면 무결성이 깨질 수 있으므로 조심해서 사용해야 한다.

  • 둘 이상의 컬렉션을 패치할 수 없다

컬렉션 * 컬렉션의 카테시안 곱이 만들어지므로 주의해야한다.
하이버네이트를 사용할 경우 MultipleBagFetchException이 발생한다.
(근데 컬렉션이 Set이면 왜 될까?)

  • 컬렉션을 패치 조인하면 페이징 API를 사용할 수 없다

HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
와 같은 경고로그를 남기면서 메모리에서 페이징한다.
데이터가 적으면 상관없겠지만 많으면 성능 이슈가 발생할 수 있어서 위험하다.

driven 엔티티의 중복이 제거된 다음 페이징을 하려고 메모리에서 하는건데… 왜 그런걸까?
DISTINCT 없는 패치조인일 때는 그냥 다 뿌려줬으면서… 이것도 그냥 다 뿌려진 데이터에 페이징을 넣으면 되는 것 아닌지?

이렇듯 패치조인으로 모든것을 해결할수는 없다. 필요할 때만 사용하여 성능 최적화를 꾀하는 것이 좋다.

경로 표현식

경로 표현식이란 .(점)을 찍어 그래프를 탐색하는 것을 말한다.

1
2
3
4
5
SELECT m.username
FROM Member m
INNER JOIN m.team t
INNER JOIN m.orders o
WHERE t.name = 'TeamA';

여기서 m.username, m.team, m.orders, t.name 모두 경로 표현식이다.

아래는 경로 표현식의 종류와 특징들이다.

  • 상태필드 : 단순히 값을 저장하기 위한 필드. 일반적인 자바 기본 타입의 컬럼들을 말한다.
    • m.username, t.name이 해당한다.
    • 더는 탐색할 수 없다
  • 연관필드 : 연관관계를 위한 필드, 임베디드 타입
    • 단일 값 연관 필드 : 대상이 엔티티인것을 말한다. (@ManyToOne, @OneToOne)
      • m.team이 해당된다.
      • 묵시적으로 내부 조인이 일어난다.
      • 계속 탐색할 수 있다
      • 임베디드 타입도 단일 값 연관 필드이지만 연관관계가 없으므로 조인이 일어나지 않는다.
    • 컬렉션 값 연관 필드 : 대상이 컬렉션것을 말한다. (@OneTomany, @ManyToMany)
      • m.order가 해당된다.
      • 묵시적으로 내부 조인이 일어난다.
      • 기본적으로 더는 탐색할 수 없으나, FROM 절에서 별칭을 얻으면 별칭으로 탐색할 수 있다.

단일 값 연관 경로 탐색 예제

1
SELECT o.member from Order o

위 JPQL은 아래와 같이 변환된다.

1
2
3
SELECT m.*
FROM Order_ o
INNER JOIN Member m ON o.member_id = m.id

위처럼 JPQL에 JOIN을 적어주지 않았는데 JOIN이 발생하는 것을 묵시적 조인이라고 하고, JOIN을 직접 적어주는 것을 명시적 조인이라고 한다.
묵시적 조인은 내부 조인만 가능 하다. 외부 조인을 하고 싶으면 명시적 조인을 사용해야 한다.

컬렉션 값 연관 경로 탐색

컬렉션 값에서는 경로 탐색이 불가능하다(가장 많이 하는 실수)

1
SELECT t.members.username FROM Team t // 실패

만약 경로 탐색을 하고 싶으면 명시적 조인을 사용해서 외부 별칭을 획득해야 한다.

1
2
3
SELECT m.username
FROM Team t
INNER JOIN t.members m

참고로 컬렉션을 컬렉션의 크기를 구할 수 있는 size라는 특별한 기능을 제공한다.

1
SELECT t.member.size FROM Team t

는 COUNT 함수를 사용하는 함수로 적절히 변환된다.

기본적으로 쿼리에서 조인이 성능상 차지하는 부분은 아주 크다.
단순하면 별로 문제될 것 없으나, 복잡하고 성능이 중요하면 분석이 용이하도록 명시적 조인을 사용하는 것이 좋다.

서브쿼리

JPQL에서는 서브쿼리를 WHERE, HAVING 절에서만 사용할 수 있다.
SELECT, FROM 절에서는 사용할 수 없다.
아래는 간단한 서브쿼리 예시이다.

1
2
3
4
// 회원들의 평균 나이를 넘는 회원 조회
SELECT m
FROM Member m
WHERE m.age > (SELECT AVG(m2.age) FROM Member m2)

서브쿼리 함수

  • EXISTS

    • 문법 : [NOT] EXISTS {subquery}
    • 설명 : 서브쿼리가 결과에 존재하면 참이다(NOT은 반대)
    1
    2
    3
    4
    5
    6
    7
    8
    // teamA에 소속인 회원
    SELECT m
    FROM Memner m
    WHERE EXISTS (
    SELECT t
    FROM m.team t
    WHERE t.name = 'teamA'
    )
  • ALL | ANY | SOME

    • 문법 : { ALL | ANY | SOME } {subquery}
    • 설명 : 비교 연산자와 같이 사용한다
      • ALL : 조건을 모두 만족하면 참
      • ANY, SOME : 둘은 같은 의미임. 조건을 하나라도 만족하면 참
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 전체 상품 각각의 재고보다 주문량이 많은 주문들  
    SELECT o
    FROM Order o
    WHERE o.orderAmoun > ALL(
    SELECT p.stockAmoun from Product p // o.p가 아니고?
    )

    // 어떤 팀이든 팀에 소속된 회원
    SELECT m
    FROM Member m
    WHERE m.team = ANY(
    SELECT t
    FROM Team t // 이것도 좀 이상한데...
    )
  • IN

    • 문법 : [NOT] IN {subquery}
    • 설명 : 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. IN은 서브쿼리가 아닌 곳에서도 사용할 수 있다.

조건식

타입 표현

종류 설명 예제
문자 작은 따옴표 사이에 표현.
작음 따옴표를 표현하고 싶으면 작은 따옴표 2개(’’) 사용
‘HELLO’
‘She’'s ’
숫자 L(Long 타입 지정)
D(Double 타입 지정)
F(Float 타입 지정)
10L
10D
10F
날짜 DATE {d ‘yyyy-mm-dd’}
TIME {t ‘hh:mm:ss’}
TIMESTAMP {ts 'yyyy-mm-dd hh:mm:ss.f}
m.createDate = {d ‘2012-03-24’}
Boolean TRUE, FALSE
Enum 패키지명을 포함한 전체 이름 com.joont.MemberType.Admin
엔티티 타입 엔티티의 타입을 표현함. 주로 상속과 관련해 사용. TYPE(m) = Member

연산자 우선 순위

  1. 경로 탐색 연산 : .
  2. 수학 연산 : +(단항 연산), -(단항 연산), *, /, +, -
  3. 비교 연산 : =, >, >=, <, <=, <>,
    [NOT] BETWEEN, [NOT] LIKE, [NOT] IN,
    IS [NOT] NULL, IS [NOT] EMPTY, [NOT] MEMBER [OF], [NOT] EXISTS
  4. 논리연산 : NOT, AND, OR

IS [NOT] EMPTY, [NOT] MEMBER [OF] 만 뺴면 사용법은 일반적인 SQL과 동일하다.
이 두개는 컬렉션 식으로써 JPA에서 제공하는, 컬렉션에만 사용가능환 특별 기능이다.

컬렉션 식

컬렉션에만 사용될 수 있음에 주의해야 한다. 컬렉션이 아닌 곳에 사용하면 오류가 발생한다.

  • 빈 컬렉션 비교 식

    • 문법 : {컬렉션 값 연관 경로} IS [NOT] EMPTY
    • 설명 : 컬렉션에 값이 비었으면 참
    1
    2
    3
    SELECT m
    FROM Member m
    WHERE m.orders IS EMPTY

    는 아래와 같이 실행된다.

    1
    2
    3
    4
    5
    6
    7
    SELECT m.*
    FROM Member m
    WHERE EXISTS (
    SELECT o.id
    FROM Order_ o
    WHERE o.member_id = m.id
    )
  • 컬렉션 멤버 식

    • 문법 : {엔티티나 값} [NOT] MEMBER [OF] {컬렉션 값 연관경로}
    • 설명 : 엔티티나 값이 컬렉션에 포함되어 있으면 참
    1
    2
    3
    4
    // 전달된 멤버가 포함되어 있는 팀 조회  
    SELECT t
    FROM Team t
    WHERE :memberParam MEMBER OF t.members

스칼라 식

숫자, 문자, 날짜, case, 엔티티 타입 같은 가장 기본적인 타입들을 스칼라 타입이라고 한다.

  • 문자함수

    함수 설명 예제
    CONCAT(문자1, 문자2) 문자를 합한다 CONCAT(‘A’, ‘B’) = AB
    SUBSTRING(문자, 위치[, 길이]) 위치부터 시작해 길이만큼 문자를 구한다. 길이 값이 없으면 나머지 전체 길이를 뜻한다 SUBSTRING(‘ABCDEF’, 2, 3) = BCD
    TRIM([[LEADING TRAILING BOTH] [트림 문자] FROM] 문자)
    LOWER(문자) 소문자로 변경 LOWER(‘ABC’) = abc
    UPPER(문자) 대문자로 변경 UPPER(‘abc’) = ABC
    LENGTH(문자) 문자 길이 LENGTH(‘ABC’) = 3
    LOCATE(찾을 문자, 원본 문자[, 검색 시작 위치]) 검색위치부터 문자를 검색한다. 1부터 시작하고 못찾으면 0을 반환한다. LOCATE(‘DE’, ‘ABCDEFG’) = 4
  • 수학함수

    함수 설명 예제
    ABS(식수학식) 절대값을 구한다 ABS(-10) = 10
    SQRT(수학식) 제곱근을 구한다 SQRT(4) = 2.0
    MOD(수학식, 나눌 수) 나머지를 구한다 MOD(4, 3) = 1
    SIZE(컬렉션 값 연관 경로식) 컬렉션의 크기를 구한다 SIZE(t.members)
    INDEX(별칭) LIST 타입 컬렉션의 위치값을 구함. 단 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용할 수 있다 t.members m where INDEX(m) > 3
  • 날짜함수

    함수 설명
    CURRENT_DATE 현재 날짜
    CURRENT_TIME 현재 시간
    CURRENT_TIMESTAMP 현재 날짜 + 시간

    하이버네이트는 날짜 타입에서 년,월,일,시,분,초 값을 구하는 기능을 지원한다
    (YEAR,MONTH,DAY,HOUR,MINUTE,SECOND)

    1
    SELECT YEAR(m.createdDate), MONTH(m.createdDate), DAY(m.createdDate) FROM Member;

CASE 식

  • 기본 CASE

    • 문법 :
      1
      2
      3
      4
      CASE  
      {WHEN <조건식> THEN <스칼라식>}+
      ELSE <스칼라식>
      END
  • 심플 CASE

    • 문법 :
      1
      2
      3
      4
      CASE <조건대상>  
      {WHEN <스칼라식1> THEN <스칼라식2>}+
      ELSE <스칼라식>
      END
  • COALESCE

    • 문법 : COALESCE(<스칼라식>, {,<스칼라식>}+)
    • 설명 : 스칼라식을 차례대로 조회해서 null이 아니면 반환한다. IFNULL과 약간 비슷하다.
    1
    2
    SELECT COALESCE(m.usernae, 'nobody') 
    FROM Member m
  • NULLIF

    • 문법 : NULLIF(<스칼라식>, <스칼라식>)
    • 설명 : 두 값이 같으면 null 반환, 다르면 첫번째 값을 반환한다.

다형성 쿼리

상속관계(@Inheritance)로 구성된 엔티티를 JPA에서 조회하면 그 자식 엔티티도 같이 조회한다.
이건 기존과 동일하다.

TYPE

상속 구조에서 조회 대상을 특정 타입으로 한정할 때 사용한다.

1
2
3
SELECT i
FROM Item i
WHERE TYPE(i) IN(Book, Movie)

는 아래와 같이 실행된다

1
2
3
SELECT i.*
FROM Item i
WHERE i.DTYPE IN('B', 'M')

TREAT

상속 구조에서 부모 타입을 특정 타입으로 다룰 때 사용한다.(자바의 타입 캐스팅과 비슷하다)
JPA 표준은 FROM, WHERE절에서만 사용 가능하고, 하이버네이트의 경우 SELECT에서도 가능하다.

1
2
3
SELECT i
FROM Item i
WHERE TREAT(i as Book).author = 'kim'

Item을 자식 타입인 Book으로 다뤘다. 그래서 Book의 필드인 author에 접근할 수 있다.

사용자 정의 함수 호출(since JPA2.1)

JPA 2.1부터 사용자 정의 함수를 지원한다.

문법 : FUNCTION(function_name {, function_arg}*)

1
2
SELECT FUNCTION('group_concat', i.name)
FROM Item i

하이버네이트를 사용할 경우 아래와 같이 방언 클래스를 상속해서 사용할 데이터베이스 함수를 미리 등록해야 한다.

1
2
3
4
5
6
7
8
public class MyH2Dialect extends H2Dialect{
public MyH2Dialect(){
registerFunction(
"group_concat",
new StandardFunction("group_concat", StandardBasicTypes.STRING)
);
}
}

registerFunction의 두번째 인자로는 하이버네이트의 SQLFunction 구현체를 주면 된다.
지금은 기본 함수를 사용하겠다는 의미로 StandardFunction을 사용하였고,
첫번째 인자로 함수 이름, 두번째 인자로 리턴 타입을 주고 있는 모습이다.

상속한 Dialect는 아래와 같이 등록하면 되고,

1
<property name="hibernate.dialect" value="com.joont.dialect.MyH2Dialect" />

하이버네이트를 사용하면 기본 문법보다 축약해서 사용할 수 있다.

1
2
SELECT group_concat(i.name)
FROM Item i

엔티티 직접 사용

객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별하기 때문에
JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
몇 가지 예시를 보자.

1
2
SELECT COUNT(m)
FROM Member m

은 아래와 같이 변환된다.

1
2
SELECT COUNT(m.id)
FROM Member m
  • 기본키 비교에 엔티티 사용

    1
    2
    3
    4
    List<Member> result = 
    em.createQuery("SELECT m FROM Member m WHERE m = :member")
    .setParameter("member", member) // 엔티티 객체 직접 사용
    .getResultList();

    member가 영속성 컨텍스트에 있을 필요는 없다. 그냥 식별자만 가지고 있으면 된다.
    실행되는 sql은 아래와 같다.

    1
    2
    3
    SELECT m.*
    FROM Member m
    WHERE m.id = ? -- member 파라미터 id 값
  • 외래키 비교도 마찬가지다

    1
    2
    3
    4
    List<Member> result = 
    em.createQuery("SELECT m FROM Member m WHERE m.team = :team")
    .setParameter("team", team) // 엔티티 객체 직접 사용
    .getResultList();

    실행되는 sql은 아래와 같다.

    1
    2
    3
    SELECT m.*
    FROM Member m
    WHERE m.team_id = ? -- team 파라미터 id 값

    MEMBER 테이블은 이미 TEAM의 식별자 값을 가지고 있기 때문에 묵시적 조인은 일어나지 않는다.

Named 쿼리(정적 쿼리)

em.createQuery("select ... ") 처럼 JPQL을 직접 문자로 넘기는 것을 동적 쿼리라고 하고,
미리 정의한 쿼리에 이름을 부여해서 해당 이름으로 사용하는 것을 Named 쿼리(정적 쿼리)라고 한다.

Named 쿼리는 어플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해두므로 오류를 빨리 확인할 수 있고, 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다.

Named 쿼리는 @NamedQuery 어노테이션을 사용해서 자바 코드에 작성하거나 XML 문서에 작성할 수 있다.

어노테이션에 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@NamedQueries({
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member WHERE m.username = :username"
),
@NamedQuery(
name = "Member.count",
query = "SELECT COUNT(m) FROM Member m"
)
})
class Member{
// ...
}

위처럼 엔티티에 @NamedQuery, @NamedQueries 어노테이션을 사용해서 직접 정의해주면 된다.
(Named 쿼리의 이름에 있는 Member가 뭔가 기능적으로 하는게 있는 것은 아니다. 그냥 관리의 편의성을 위함이다.
그리고 Named 쿼리는 영속성 유닛 단위로 관리되므로 충돌을 방지하기 위해 이름으로 구분한 것이기도 하다)
그리고 아래와 같아 사용해주면 된다.

1
2
3
4
List<Member> result = 
em.createNamedQuery("Member.findByName", Member.class)
.setParameter("username", "joont")
.getResultList();

XML에 정의

사실상 자바로 멀티라인 문자를 다루는 것은 상당히 귀찮은 일이므로, Named 쿼리를 작성할 때는 XML을 사용하는 것이 더 편리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--xml version="1.0" encoding="UTF-8"?-->
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" version="2.0">

<named-query name="Member.findByUserName">
<query>
select m
from Member m
where m.username = :username
</query>
</named-query>

<named-query name="Member.findByAgeOver">
<query><![CDATA[
select m
from Member m
where m.age > :age
]]></query>
</named-query>

<named-native-query name="Inter.findByAlal" result-class="sample.jpa.Inter">
<query>select a.inter_seq, a.inter_name_ko, a.inter_name_en from tb_inter a where a.inter_name_ko = ?</query>
</named-native-query>

</entity-mappings>

XML에서 &, <, >,는 예약문자어 이므로 &amp;, &lt;, &gt;를 사용해야 한다.
<![CDATA[ ]]>를 사용하면 그 사이에 있는 문자를 그대로 출력하므로 예약 문자도 사용할 수 있다.

기타

  • Enum은 = 비교연산만 지원한다 는 JPQL 명세이고, 하이버네이트에서는 아래가 가능하다

    1
    2
    3
    Delivery delivery = 
    em.createQuery("select d from Delivery d where d.deliveryStatus like '%CO%'", Delivery.class)
    .getSingleResult();
  • 임베디드 타입은 비교를 지원하지 않는다 또한 JPQL 명세이고, 하이버네이트에서는 아래가 가능하다

    1
    2
    3
    4
    Delivery foundDelivery = 
    em.createQuery("select d from Delivery d where d.address = :address", Delivery.class)
    .setParameter("address", new Address("seoul", "새마을로", "1111-1111"))
    .getSingleResult();

하이버네이트에서만 지원하는건가?

  • JPA는 ''를 길이 0인 Empty String으로 정했지만 데이터베이스에 따라 ''를 null로 사용하는 곳이 있으니 확인하고 사용해야 한다.
  • NULL 정의
    • 조건을 만족하는 데이터가 하나도 없으면 NULL 이다
    • NULL은 알수 없는 값이다. NULL과의 모든 수학적 연산은 NULL이다.
    • JPA 표준명세에서 정하는 NULL과의 논리연산은
      NULL과 False를 AND 연산하면 False.
      NULL과 True를 OR 연산하면 True이다.

네이티브 SQL

JPA는 표준 SQL이 지원하는 대부분의 SQL 문법과 함수들을 지원하지만,
특정 데이터베이스만 지원하는 함수나 문법, SQL 쿼리 힌트 같은 것들은 지원하지 않는다.
이런 기능을 사용하기 위해선 네이티브 SQL을 사용해야 한다.
네이티브 SQL이란 JPA에서 일반 SQL을 직접 사용하는 것을 말한다.

실제 데이터베이스 쿼리를 사용한다는 점 외에는 JPQL을 사용할때와 거의 비슷하다.
(원래는 위치기반 파라미터만 지원하지만 하이버네이트는 이름기반 파라미터까지 지원한다)

엔티티 조회

Query createNativeQuery(String sqlString, Class resultClass) 를 말한다.

반환타입을 줘도 TypedQuery가 아닌 Query를 반환하는 이유는,
JPA 1.0에서 규약이 그렇게 정의되어 버렸기 때문에 그렇다고하니 신경쓰지 않아도 된다.

  • 이 메서드로 조회해온 엔티티는 영속성 컨텍스트에서 관리된다.
  • 그러므로 모든 필드를 다 조회하는 SQL을 실행해야 한다
  • 특정 필드만 조회해오면 오류가 발생한다.
1
2
3
4
5
6
String sql = "SELECT * FROM Member WHERE id = 1";

Member memberFromNative = (Member)em.createNativeQuery(sql, Member.class).getSingleResult();
Member memberFromJPQL = em.find(member.class, 1);

assertSame(memberFromnNative, memberFromJPQL); // success

값 조회

Query createNativeQuery(String sqlString) 를 말한다.

1
2
3
4
5
6
7
8
9
String sql = "SELECT id, name, age FROM Member";

List<Object[]> list = em.createNativeQuery(sql).getResultList();

for(Object[] row : list){
Integer id = row[0];
String name = row[1];
Integer age = row[2];
}

결과 매핑 사용

네이티브 쿼리에서 여러값들이 나올 때 결과를 여러 엔티티나 엔티티+스칼라 형태로 적절히 합치는건데… 굳이 이것까지…

Named 네이티브 쿼리

어노테이션의 경우 @NamedNativeQuery 사용하면 되고,
XML의 경우 <named-native-query> 사용하면 된다.

될수 있으면 JPQL을 사용하고, 기능이 부족하면 HQL 등을 사용해보고, 그래도 안되면 네이티브 SQL을 사용하자

스토어드 프로시저

JPQL에서 사용 가능하지만(Named 스토어드 프로시저도 가능) MySQL에서 성능 이점이 그리 많지 않아 잘 사용되지 않으니 패스

벌크 연산(UPDATE, DELETE)

JPQL로 여러 건을 한번에 수정하거나 삭제할 떄 사용한다.
아래는 UPDATE 벌크 연산이다.

1
2
3
4
5
6
7
String sql = "UPDATE Product p " +
"SET p.prce = p.price * 1.1 " +
"WHERE p.stockAmount < :stockAmount";

int resultCount = em.createQuery(sql)
.setParameter("stockAmount", 10)
.executeUpdate();

executeUpdate 메서드를 사용한다. 벌크 연산으로 영향을 받은 엔티티 건수를 반환한다.
아래는 DELETE 벌크 연산이다.

1
2
3
4
5
6
String sql = "DELETE FROM Product p " +
"WHERE p.price < :price";

int resultCount = em.createQuery(sql)
.setParameter("price", 100)
.executeUpdate();

벌크 연산시 주의사항

벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다는 특징이 있으므로 주의해야 한다.
아래는 발생가능한 문제 상황이다.

1
2
3
4
5
6
7
8
Product product = em.find(Product.class, 1);
assertThat(product.getPrice(), is(1000));

String sql = "UPDATE Product p " +
"SET p.prce = p.price * 1.1";
em.createQuery(sql).executeUpdate();

assertThat(product.getPrice(), is(1100)); // FAIL!!

벌크 연산은 영속성 컨텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 쿼리한다.
따라서 위와 같이 영속성 컨텍스트와 데이터베이스 간에 데이터 차이가 발생할 수 있는 것이다.
이를 해결하기 위한 방법은 아래와 같다.

  • em.refrest(entity) 사용

    벌크 연산 직후에 em.refresh(entity)를 사용하여 데이터베이스에서 다시 상품을 조회하면 된다.

  • 벌크 연산 먼저 실행

    벌크 연산을 가장 먼저 실행하면 이미 변경된 내용을 데이터베이스에서 가져온다.
    가장 실용적인 해결책이다.

  • 벌크 연산 수행 후 영속성 컨텍스트 초기화

    영속성 컨텍스트가 초기화되면 데이터베이스에서 다시 조회해오기 때문에 이것도 방법이다.

영속성 컨텍스트와 JPQL

영속성 컨텍스트에 이미 있는 엔티티를 JPQL로 다시 조회해올 경우 어떻게 처리될까?

1
2
3
4
5
Member member1 = em.find(Member.class, 1);

List<Member> list =
em.createQuery("SELECT m FROM Member m", Member.class)
.getResultLst();

이미 영속성 컨텍스트에 들어있는 1번 member가 JPQL에 의해 다시 한번 조회되는 상황이다.
결과부터 말하자면 JPQL 쿼리는 쿼리대로 다 날라가고, 조회한 엔티티를 영속성 컨텍스트에 다 저장한다.
여기서 중요한 점은 1번 member의 경우 영속성 컨텍스트에 이미 들어있으므로, JPQL로 조회해온 1번 member는 그냥 버려진다는 것이다.

JPQL 조회시 영속성 컨텍스트

보다시피 조회해온 member들 중 1번 member는 영속성 컨텍스트에 이미 있으므로 그 결과가 버려진다.
영속성 컨텍스트에 없는 2번 member의 경우 영속성 컨텍스트에 저장된다.

새로 조회해온 결과를 기존 영속성 컨텍스트에 덮어쓰지 않는 이유는 영속 상태인 엔티티의 동일성을 보장해야하기 때문이다.

find로 들고오든, JPQL로 들고오든 동일한 엔티티를 반환해야 한다.

1
2
3
4
5
6
7
Member member1 = em.find(Member.class, 1);
Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.getSingleResult();

assertSame(member1, member2); // SUCCESS

JPQL 실행시 플로우

보다시피 영속성 컨텍스트에 1번 member 엔티티가 있더라도 무조건 SQL을 실행해서 조회해온다.
(JPQL을 분석해서 영속성 컨텍스트를 조회하는 것은 너무 힘들기 때문이다.)
그리고 조회해온 엔티티를 영속성 컨텍스트에 넣을 때, 이미 있는 엔티티일 경우 결과를 버린다.

JPQL과 플러시 모드

플러시 모드는 FlushMode.AUTO(Default), FlushMode.COMMIT이 있다.
이때까지 FlushMode.AUTO 는 트랜잭션이 끝날때나 커밋될 때만 플러시를 호출하는 것으로 알고 있었으나, 사실은 시점이 하나 더 있다. JPQL 쿼리를 실행하기 직전이다.

1
2
3
4
5
6
7
8
9
Member member1 = em.find(Member.class, 1);
member1.setName("modified name");

Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.getSingleResult();

assertThat(member1.getName(), member2.getName());

변경감지는 플러시 될때 발생하므로, JPQL에서 아직 변경되지 않은 name 값을 가진 1번 member를 가져올 것이라고 생각할 수 있지만,
FlushMode.AUTO는 영속 상태인 엔티티의 동일성을 보장하기 위해 JPQL 실행 전에 플러시를 수행한다.
그러므로 위의 테스트는 성공한다.

어떻게 동작하는지 정확히는 모르겠으나, 영속성 컨텍스트에 있는 엔티티에 대해 JPQL을 실행할 떄만 플러시를 수행한다.
즉, 위의 상황에서 JPQL로 Team을 조회해올 경우 플러시가 발생하지 않는다.

여기서 플러시 모드를 FlushMode.COMMIT으로 설정하면 쿼리전에 플러시를 수행하지 않으므로 위의 테스트가 실패하게 된다.
이때는 직접 em.flush를 호출해주거나, Query 객체에 직접 플러시 모드를 설정해주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
em.setFlushMode(FlushMode.COMMIT); // 커밋시에만 플러시

Member member1 = em.find(Member.class, 1);
member1.setName("modified name");

em.flush(); // 1. em.flush 직접 호출

Member member2 =
em.createQuery("SELECT m FROM Member WHERE m.id = :id", Member.class)
.setParameter("id", 1)
.setFlushMode(FlushMode.AUTO) // 2. setFlushMode 설정
.getSingleResult();

assertThat(member1.getName(), member2.getName());

FlushMode.COMMIT은 너무 잦은 플러시가 일어나는 경우, 플러시 횟수를 줄여서 성능을 최적화하고자 할 때 사용할 수 있다.

한 트랜잭션 안에서 특정 엔티티에 대한 insert, update, delete를 수행하고
그 뒤에서 JPQL로 전혀 다른 엔티티의 값을 읽어올 경우 불필요한 flush가 날라가게 된다.

Read more »

[jpa] 값 타입

Posted on 2018-12-28 | Edited on 2020-11-02 | In jpa | Comments:

JPA의 데이터 타입은 크게 엔티티 타입과 값 타입이 있다.
엔티티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순 값으로 사용하는 자바 기본 타입이나 객체를 말한다.
(영한님은 여기서 엔티티 타입은 살아있는 생물이고, 값 타입은 단순한 수치 정보라고 표현했다.)

JPA는 값 타입을 표현하는 방법을 몇가지 더 제공한다.
아래는 JPA에서 제공하는 값 타입의 목록이다.

  • 기본값 타입(primitive, wrapper, String)
  • 임베디드 타입
  • 값 타입 컬렉션

값 타입은 기본적인 특징은 아래와 같다.

  • 식별자가 없다
  • 생명주기가 엔티티에 의존한다
  • 공유하면 안된다

값 타입

자바의 primitive 타입, Wrapper 클래스, String 클래스를 말한다.

1
2
3
4
5
6
7
8
@Entity
class Member{
@Id
private Long id;

private String name;
private int age;
}

임베디드 타입(복합 값 타입)

여러개의 값 타입을 묶어서 하나의 값 타입으로 정의하는 방법이다.
우선 값 타입을 적용하기 전의 코드는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
class Member{
@Id
private Long id;

// 근무기간
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;

// 집주소
private String city;
private String street;
private String zipCode;
}

위 처럼 엔티티가 모든 속성을 flat 하게 가지는 것은 객체지향적이지 않다.
근무기간, 집주소로 묶을 수 있다면 더 좋을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Entity
class Member{
@Id
private Long id;

@Embedded
private Period workPeriod;

@Embedded
private Address homeAddress;
}

@Embeddable
class Period{
@Temporal(TemporalType.DATE)
private Date startDate;
@Temporal(TemporalType.DATE)
private Date endDate;

public boolean isWork(Date date){
// 값 타입을 위한 메서드 또한 작성 가능
}
}

@Embeddable
class Address{
@Column(name = "city") // 매핑할 컬럼 지정 가능
private String city;
private String street;
private String zipcode;
}

작성한 값 타입은 다른 곳에서 재사용 될수도 있고, 값 타입만을 위한 메서드도 작성 가능하다.
엔티티가 더욱 의미있고 응집력있게 변했다

이러한 임베디드 타입을 정의하려면 아래의 2가지 어노테이션이 필요하다.

  • @Embeddable : 값 타입을 정의하는 곳에 표시
  • @Embedded : 값 타입을 사용하는 곳에 표시

임베디드 타입과 테이블 매핑

이렇게 작성한 임베디드 타입은 테이블에 아래와 같이 매핑된다.
임베디드 타입과 테이블 매핑

임베디드 타입은 엔티티의 값일 뿐이다. 그러므로 보다시피 임베디드 타입을 사용하기 전과 후에 매핑되는 테이블은 같다.

ORM을 사용하지 않았더라면 객체와 테이블은 대부분 1:1로 매핑되었을 것을,
ORM을 사용함으로써 객체와 테이블을 더 세밀하게 매핑할 수 있다.
(잘 설계한 ORM 어플리케이션은 매핑힌 클래스의 수가 테이블의 수보다 더 많다)

임베디드 타입의 포함과 연관관계

임베디드 타입은 다른 임베디드 타입을 포함할 수 있고, 다른 엔티티를 참조할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Entity
class Member{
// ...
@Embedded
private Address address;

@Embedded
private PhoneNumber phoneNumber;
}

@Embeddable
class Address{
private String city;

private String street;

@Embedded // 포함 가능
private Zipcode zipcode;
}

@Embeddable
class Zipcode{
String zip;

Strign code;
}

class PhoneNumber{
String areaCode;

String localNumber;

@ManyToOne // 연관관계 가능
PhoneServiceProvider phoneServiceProvider;
}

@Entity
class PhoneServiceProvider{
@Id
private String name;
}

속성 재정의: @AttributeOverride

아래와 같이 정의하고 싶을 수 있다.

1
2
3
4
5
6
7
8
class Member{
// ...
@Embedded
private Address homeAddress;

@Embedded
private Address companyAddress;
}

ORM 에서만 객체로 묶을 뿐, 테이블 레벨에선 flat하게 펴지므로 위와 같이 정의하는 것은 불가능하다.
컬럼명이 중복되기 때문이다.
이럴땐 @AttributeOverride를 통해 컬럼명을 재정의해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Member{
// ...
@Embedded
private Address homeAddress;

@Embedded
@AttributeOverrides({
@AttributeOverride(name = "city", column = @Column(name = "company_city")),
@AttributeOverride(name = "street", column = @Column(name = "company_street")),
@AttributeOverride(name = "zipcode", column = @Column(name = "company_zipcode"))
})
private Address companyAddress;
}

name에는 Address 내의 필드명을 써주고, column에는 @Column 어노테이션을 써서 재정의 해주면 된다.
어노테이션을 너무 많이 사용되서 지저분하긴 하지만, 다행히(?) 이렇게 한 엔티티에 중복해서 임베디드를 사용할 일이 많이 없다.

@AttributeOveride는 엔티티에 설정해야 한다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 한다.

임베디드 타입과 null

임베디드 타입이 null 이면 매핑한 컬럼 값을 모두 null 이 된다(!!)

1
2
// city, street, zipcode가 모두 null이 됨  
member.setAddress(null);

임베디드 타입의 딜레마

값을 다룰때는 기본적으로 참조가 아닌 복제의 형태를 따른다.
하지만 여기서 문제는, JPA에서는 임베디드 타입이 값 타입인데, 형태는 일반적인 클래스라 참조 방식으로 동작한다는 것이다.

불변성

참조 방식으로 동작하므로 아래와 같은 상황을 막을 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Address address1 = new Address("city", "street", "zipcode1");
Member member1 = Member.builder()
.name("joont1")
.homeAddress(address1)
.build();

Address address2 = address1;
address2.setZipcode("zipcode2");
Member member2 = Member.builder()
.name("joont2")
.homeAddress(address2)
.build();

em.persist(member1);
em.persist(member2);

기대하는 것은 member1에 zipcode1, member2에 zipcode2가 저장되어야 하는 것이지만(값 타입의 특성상),
당연히 그렇게 처리되지 않는다. 참조 방식으로 동작하기 때문이다.

때문에 JPA에서 값 타입을 사용할때는 setter 등을 모두 제거한 불변객체로 다루어야 하고,
(자바에서 불변 객체로 만드는 가장 간단한 방법은 setter 제거이다)
값을 재사용 할 때는 deep copy를 수행해서 절대 재사용 되는 일이 없도록 해야한다.

1
2
3
4
5
6
7
8
9
10
11
@AllArgsConstructor // 전체 프로퍼티를 받는 생성자
@Embeddable
class Address implements Cloneable{
private String city;
private String street;
private String zipcode;

public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Address address1 = new Address("city", "street", "zipCode1");
Member member1 = Member.builder()
.name("joont1")
.homeAddress((Address)address1)
.build();

Address address2 = address1.clone(); // 복사
address2.setZipcode("zipcode2");
Member member2 = Member.builder()
.name("joont2")
.homeAddress((Address)address2)
.build();

em.persist(member1);
em.persist(member2);

현재는 Address 내부가 flat해서 간단히 Object.clone()의 호출만으로도 클로닝이 되지만,
다른 임베디드 타입을 사용하거나 배열을 사용하고 있었을 경우 해당 필드까지 전부 deep clone을 해줘야한다.
하지만 또 반대로 임베디드 타입이 엔티티를 가지고 있을 경우 deep clone 하면 안된다.
이렇듯이 골치아픈게 deep clone이기 때문에, 가급적이면 @Embeddable 내부는 flat 하게 유지해주는 것이 좋다.

비교

값 타입이라면 동일성 비교(==)나 동등성 비교(equals)가 동작해야 한다.
하지만 현재 Address는 그것이 보장되지 않으므로, equals 메서드를 재정의 해줘야한다.

1
2
3
4
5
6
7
8
@AllArgsConstructor
@EqualsAndHashCode // 전체 필드에 대해 equals와 hashCode 재정의
@Embeddable
class Address{
private String city;
private String street;
private String zipcode;
}

임베디드 타입의 equals 메서드를 재정의 할 때는 보통 모든 필드의 값을 비교하도록 구현한다.
그리고 equals 메서드를 재정의하면 hashCode까지 같이 재정의 해주는 것이 좋다.
그렇지 않으면 해시를 사용하는 컬렉션(HashSet, HashMap)에서 문제가 발생할 수 있기 때문이다.

값 타입 컬렉션

여러개의 값 타입을 저장할 떄 사용한다. 그러려면 컬렉션에 저장해야 하는데, RDB에서는 필드에 컬렉션을 저장할 수 없다.
그러므로 값 만을 저장하는 테이블을 따로 만들어서 사용해야 한다.
값 타입 컬렉션 ERD

위 ERD를 엔티티에서 매핑하면 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Entity
class Member{
// ...

@Embedded
private Address homeAddress;

@ElementCollection
@CollectionTable(
name = "FAVORITE_FOOD",
joinColumns = @JoinColumn(name = "member_id")
)
@Column(name = "food_name")
private List<String> favoriteFoodList = new ArrayList<>();

@ElementCollection
@CollcetionTable(
name = "ADDRESS_HISOTRY",
joinColumns = @JoinColumn(name = "member_id")
)
private List<Address> addressHistory = new ArrayList<>(); // Address는 위와 동일
}

@ElementCollection으로 값 타입 컬렉션 인것을 알려주고,
@CollectionTable로 해당 값들을 저장한 테이블을 알려주면 된다(외래키랑 같이).
favorite_food처럼 값으로 사용되는 컬럼이 하나일 경우 @Column을 사용해서 컬럼명을 지정할 수 있다.
값 타입 컬렉션은 무조건적으로 CascadeType.ALL, orphanRemoval = true가 붙은것 처럼 동작한다.

값 타입 컬렉션 사용

저장

1
2
3
4
5
6
7
8
9
10
11
12
Member member = new Member();

member.setHomeAddress(new Address("city", "street", "zipCode4"));

member.getFavoriteFoodList().add("pork");
member.getFavoriteFoodList().add("beef");

member.getAddressHistory().add(new Address("city1", "street1", "zipcode1"));
member.getAddressHistory().add(new Address("city2", "street2", "zipcode2"));
member.getAddressHistory().add(new Address("city3", "street3", "zipcode3"));

em.persist(member);

member : insert 1번
homeAddress : 임베디드 값 타입이므로 member에 포함됨
favoriteFoodList : insert 2번
addressHistory : insert 3번

조회

값 타입 컬렉션도 조히할 때 패치 전략을 사용할 수 있다. Default는 LAZY이다.

1
@ElementCollection(fetch = FetchType.LAZY)

조회 방식은 일반적인 @OneToMany 조회 할때와 동일하다.
직접 사용할 때 조회된다.

수정

1
2
3
4
5
6
7
Member member = em.find(Member.class, 1);
List<String> favoriteFoodList = member.getFavoriteFoodList();
favoriteFoodList.set(0, "changed pork");
favoriteFoodList.set(1, "changed beef");

List<Address> addressHisotry = member.getAddressHistory();
addressHisotry.get(0).setStreet("changed street");

값 타입은 식별자가 없는 단순한 값들의 모음으로 테이블에 저장된다.
그래서 여기에 저장된 값이 변경되면 데이터베이스에 저장된 원본 데이터의 값을 찾기 어렵게 된다.
이러한 문제로 인해 JPA는 값 타입 컬렉션에 변경사항이 발생하면,
값 타입 컬렉션에 매핑된 테이블의 모든 데이터를 삭제하고, 현재 값 타입 컬렉션에 있는 모든 값을 다시 데이터베이스에 저장한다.

즉, 위와 같은 코드에서는 아래와 같이 쿼리가 발생한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** address_history **/
delete
from
ADDRESS_HISTORY
where
member_id=1

insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city1", "street1", "zipcode1")
insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city2", "changed street", "zipcode2") -- insert modifired data
insert
into
ADDRESS_HISTORY
(member_id, city, street, zipcode)
values
(1, "city3", "street3", "zipcode3")

/** favorite_food **/
delete
from
FAVORITE_FOOD
where
member_id=1

insert
into
FAVORITE_FOOD
(member_id, food_name)
values
(1, "changed pork"); -- insert modifired data
insert
into
FAVORITE_FOOD
(member_id, food_name)
values
(1, "changed beef"); -- insert modifired data

이러한 비효율적인 특징이 있으므로 만약 값 타입 컬렉션에 매핑된 테이블에 데이터가 많다면 값 타입 컬렉션 대신 일대다 관계를 고려해보는 것이 좋다.
게다가 값 타입 컬렉션은 모든 컬럼을 묶어서 기본키를 구성하므로, 컬럼에 null을 입력할 수 없고, 중복된 값을 입력할 수 없는 제약조건도 있다.

Read more »

UML

Posted on 2018-12-27 | Edited on 2020-11-02 | In etc | Comments:

UML 종류

https://myeonguni.tistory.com/752
클래스 다이어그램, 시퀀스 다이어그램, 활동 다이어그램 외에는 잘 안쓴다
패키지 다이어그램 가끔씩 쓰고…

UML 정의와 표기법

http://www.nextree.co.kr/p6753/

generalization : 상속(extends)
realization : 구현(implemetns)
assocication : 참조
aggregation : 컬렉션으로 참조(whole과 part의 관계)
composition : 컬렉션으로 참조(whole이 part의 전체 생명주기를 책임짐. 강력한 aggregation)

Read more »
1…101112…19

JunYoung Park

182 posts
18 categories
344 tags
RSS
© 2020 JunYoung Park
Powered by Hexo v3.6.0
|
Theme – NexT.Muse v7.1.0