들어가기 전에
- JPA 설정 잘못 만졌다 DB 예쁘게 날린 허망함을 담아 작성함
- 실제 도움이 될 만한 코드는 거의 없으니, 실전적으로 코드를 짜고 싶은 경우에는 이 쪽 을 참조
- 사실 도움이 될 만한 설명도 없다
JPA
JPA는 Java의 ORM 기술 표준이다.
- JPA = ORM + Java
- 하이버네이트(ORM)는 JPA 구현체 중 하나이다.
태초에 ORM이 있었다
ORM은 Object-Relational Mapping 의 두문자어이며, 이를 번역하여 객체-관계 매핑이라고 이르는 문서도 많이 보인다. 이름을 보면, 대충 OOP와 RDB에 관련된 기술 같아 보이는데, 짐작대로 ORM은 객체지향 언어의 객체와 관계형 데이터베이스의 데이터 사이에서 나타나는 미묘한 차이와 이로 인한 문제들을 극복하기 위해 등장한 기술이다.
객체지향과 데이터
객체는, 보통 데이터와 관련된 연산의 집합 을 이르는 말이다. 데이터의 묶음은 보통 자료구조라고 한다.(보통, 자료구조에는 데이터 입출력에 관련한 연산이 수반되는 경우가 많다.) 객체의 큰 특징으로 캡슐화
, 추상화
, 다형성
, 상속
의 네 가지를 드는 경우가 많다.
- 객체 = 데이터 + 연산
- 캡슐화
- 추상화
- 다형성
- 상속
관계형 데이터베이스에서, 데이터는 연산을 가지지 않는다. 상속이나 캡슐화 등이 될 리는 더더욱 만무하다. 대신, 데이터는 외래 키를 사용한 양방향 관계 를 가질 수 있는데, 이러한 양방향 관계는 객체에서 참조를 통해 유사하게 구현할 수 있지만, 완전히 일치하게 구현하기 위해서는 많은 개발 비용이 든다.
public class Reader {
private long id;
// ...
private List<Book> bookList;
}
public class Book {
private long id;
// ...
private Reader reader;
}
- 관계 데이터와 일치하게 참조하려면 어떻게 해야 하겠는가? SQL은 어떤 것이 필요한가?
간단히 말하면, 생각보다 두 개념을 예쁘게 붙이기는 힘들다는 것이다.
그래서 ORM은…
- 결론 : ORM은, 객체와 데이터, 두 개념을 예쁘게 붙일 수 있게 도와주는 프레임워크이다.
JPA가 필요할 때
물론 JDBC를 생짜로 사용하는 사람은 별로 없다, MyBatis가 있으니까. 그러면 JPA를 언제 사용하는가? 많은 서적이나 개발 회고에서, 아래와 같은 경우에 JPA 도입을 추천, 또는 검토하였다.
- 하루 종일 SQL만 작성하고 있을 때
- 모 이런 반복 야다
- 한 서비스가 여러 DBMS를 사용할 가능성이 있을 때
- DBMS 의존적인 SQL 방언을 어떻게 제거할 것인가?
반복되는 SQL 작성
JPA를 사용하여, 개발자는 SQL 문장을 Java 코드로 대체할 수 있다. 몇 몇 프레임워크는, 특정 클래스를 상속하는 것으로 원하는 데이터에 대한 CRUD 메서드를 자동으로 생성해 개발자에게 제공한다. 그대로 사용하기엔 여러 문제점이 있지만(딜리트는 무섭다), 이 정도면 꽤 혹하지 않는가.
SQL 방언
DBMS마다 SQL 문법이 일부 다른 경우가 있는데, 이러한 문법적 차이가 발생하는 부분을 SQL 방언이라고 한다(그렇다는 것 같다).
SELECT *
FROM table
LIMIT 1, 10;
SELECT *
FROM (
SELECT page.*
FROM (
SELECT *
FROM table
) page
WHERE rnum <= 10
)
WHERE rnum >= 1;
- SQL 페이징 처리 예시. 위가
MySql
, 아래가Oracle
JPA는 연결한 DB 종류에 따라 적절한 SQL을 스스로 생성하여, 서비스가 DBMS 중립적이 될 수 있도록 한다.
Entity
개념과 선언
엔티티 클래스는 테이블과 매핑되는 클래스이다. 엔티티는 테이블의 데이터로 사상된다(아마도).
엔티티 클래스의 선언은 XML 파일에 작성하는 방법과 Java 파일에 직접 애노테이션으로 작성하는 방법이 있는데, 나는 가능하면 애노테이션을 사용하는 편을 선호한다.(나중에 읽기가 편하다)
@Entity
, @Table
, @Colum
등의 애노테이션을 사용하여 적절히 엔티티 클래스를 구성하고, 완성된 엔티티 클래스는 명세를 기반으로 테이블과 매핑된다. 애노테이션에 대한 자세한 설명은 생략한다.
@Entity
@Table(name = "reader")
public class Reader {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Colum(name = "id")
private long id;
// ...
}
- Reader 클래스는 DB의 reader 테이블에 매핑된다
엔티티의 상태
엔티티 클래스를 바탕으로 생성된 엔티티는 생성부터 제거되기까지 4가지의 상태를 가질 수 있다.
- New / 비영속
- Managed /영속
- Detached / 준영속
- Removed / 삭제
단어만 봐서는 감이 잘 오지 않는다. 엔티티의 상태는 트랜잭션, 즉 데이터의 입출력 상태와 관련이 깊다. 간단한 설명을 아래에 추가하였다.
NEW / 비영속
엔티티를 생성한 시점에서 트랜잭션 구간에 진입하기 전까지, 엔티티는 비영속 상태를 유지한다. 이 상태에서는 엔티티는 데이터베이스와 전혀 관계가 없고, JPA의 어떤 특징도 보이지 않는, 그냥 평범한 객체이다.
Reader reader = new Reader();
MANAGED / 영속
엔티티가 트랜잭션 영역에 진입하여 엔티티 매니저의 관리 하에 들어가면 해당 트랜잭션 구간동안 엔티티는 영속 상태가 된다. 영속 상태에서 엔티티는 몇 가지 중요한 특징을 가지게 된다.
- 1차 캐시 사용
- 같은 키(식별값)를 사용하는 여러 객체의 동일성 보장
- 지연된 쓰기와 변경사항 자동 업데이트
DETACHED / 준영속
엔티티가 커밋되어 트랜잭션 구간에서 빠져나오는 경우, 이 엔티티는 준영속 상태가 된다. 사실상 비영속 상태와 거의 같다.
- 영속 상태를 거쳤기 때문에, 준영속 상태의 엔티티는 식별값을 가지고 있다.
REMOVED / 삭제
엔티티가 트랜잭션 구간 내에서 관련 메서드에 의해 삭제되는 경우, 매핑되는 데이터의 삭제와 함께 엔티티 또한 삭제 상태가 된다. 객체는 사용 가능한 상태이나, 재활용 하지 않는 편이 좋다.
Entity Manager
엔티티 매니저는 이름과 같이, 엔티티를 관리하는 객체이다.
- 엔티티의 관리
- 엔티티의 저장
- 엔티티의 수정
- 엔티티의 삭제
- 엔티티의 조회
매니저의 책임이 전부 영속성 엔티티의 CRUD에 관련되어 있다. 엔티티 매니저는 영속성 상태의 엔티티 관리를 위해 DB 세션과 밀접한 연관을 가지기 때문에, 하나의 엔티티 매니저를 여러 스레드에서 공유하여 사용하면 위험하다. 스레드-안전한 엔티티 매니저 팩토리를 공유하여 각 스레드에서 엔티티 매니저를 생성하는 방식이 권장된다.
- 엔티티 매니저 팩토리로부터 엔티티 매니저 생성
- 엔티티 매니저는 DB의 커넥션 풀로부터 커넥션 획득
- 획득한 커넥션을 통해 엔티티 CRUD 관리
엔티티 매니저와 영속성
엔티티 메니저에 의해 관리되는, 영속성 상태의 엔티티는 고유한 식별값(ID)으로 구분되어 관리된다. 즉, 영속성 상태에 있는 모든 엔티티는 식별값을 가지고 있어야 한다.
영속성 상태에 있는 엔티티는 아래의 특징을 가진다
- 1차 캐시
- 엔티티는 엔티티 매니저가 가지는 내부 캐시에 저장된다. 데이터베이스에서 읽은 이력이 있는 데이터는 이 1차 캐시에 저장되어 재사용된다.
- 동일성
- 같은 식별값을 가지는 모든 엔티티는 동일하다. 즉, 참조하는 주소가 같다는 의미이다.
- 쓰기 지연과 변경 감지
- 트랜잭션 종료 시점에서, 엔티티 매니저는 엔티티의 변경사항을 감지하여 데이터베이스에 업데이트 한다. (flush)
- 변경사항이 있을 때, JSP가 기본적으로 생성하는 SQL 쿼리는 모든 필드를 업데이트 하는 쿼리이다.
- 특정 옵션을 사용 시, 변경된 필드에 대해서만 업데이트를 수행하는 쿼리를 생성할 수 있다.
- 트랜잭션 종료 시점에서, 엔티티 매니저는 엔티티의 변경사항을 감지하여 데이터베이스에 업데이트 한다. (flush)
- 지연 로딩
- lazy 옵션을 사용하여, 엔티티와 관련 있는 데이터의 로드를 해당 데이터가 필요한 시점까지 지연시킬 수 있다.
주의가 필요한 부분
- 지극히 개인적인 경험을 바탕으로 추가하였다.
Flush
영속 상태의 엔티티를 수정하면, 트랜잭션 종료 단계(커밋)에서 해당 엔티티에 대응하는 데이터에 업데이트가 발생한다.
- 트랜잭션 구간 내에서 (영속성)엔티티 수정
- 트랜잭션 종료
- 변경 사항 커밋
일부 객체의 getter / setter 로직에 따라(입력 값에 필터를 적용하거나) 개발자가 의도하지 않은 업데이트가 발생할 수 있다는 문제점이 있다.
“왜 데이터가 날아간걸까” “난 데이터가 덮어써지던데” “?” “?”
이를 잘 고려하여 엔티티 클래스를 정의할 필요가 있다.
- 잘못하면 여럿 데이터 덮어쓰게 되는 경우가 발생함
DDL.auto
JPA는 프로젝트의 엔티티 클래스 명세들로부터 이에 대응하는 테이블을 데이터베이스에 자동으로 생성하는 기능을 가지고 있다. 이 때, 자동으로 생성되는 DDL은 완벽하진 않으나 참고할 수준은 된다. 이 기능을 사용하는 하이버네이트 설정 예시는 아래와 같다.
<property name="hibernate.hbm2ddl.auto" value="create">
value에는 옵션이 들어간다. 옵션을 아래를 참고.
옵션 | 동작 | 주의사항 | 요약하면 |
---|---|---|---|
create | 애플리케이션 시작 시 엔티티 클래스 명세를 바탕으로 테이블을 추가하는 DDL을 생성하여 커밋한다. 기존에 존재하는 테이블은 드랍 한다. | 기존 테이블 드랍 | drop + create |
create-drop | 애플리케이션 시작 시 기존 테이블을 drop + create 후 애플리케이션 종료시 존재하는 테이블을 모두 드랍한다. | 기존 테이블 드랍 | drop + create + drop |
update | 엔티티 클래스 명세를 기준으로 기존 테이블을 업데이트 한다. | 기존 테이블 변경(할 가능성이 있음) | update |
valid | 애플리케이션 시작 시 엔티티 클래스와 테이블 스키마가 일치하는지 여부를 확인한다. 일지하지 않으면, 경고 메세지와 함께 애플리케이션 시작을 중단한다. |
- 참고 : 유효하지 않은 값이 value에 입력되면 기능은 비활성화 된다.
<property name="hibernate.hbm2ddl.auto" value="asdasdqwejlkanjfclkjhwas123123">
- 잘 살펴보면
- 테이블을 드랍 하는 옵션만 해도 벌써 두 개
- 테이블 명세를 바꿀 수 있는 코드는 전부 세 종류
코드를 멀쩡하게 짰으니 제 디비는 안전할 것입니다.
응 드랍~
따라서 우리는 엔티티 매니저 생성 시, 엔티티 매니저 팩토리 생성 옵션을 잘 살펴봐야 한다. 리얼 스테이지에서 DDL 관련 옵션을 사용하는 것은 대단히 위험하므로, 이 기능을 사용하고자 한다면 개발 단계에 따라 설정을 분리할 수 있는 환경을 구축 후, 신중하게 옵션을 결정하자.
뭐든 안 그렇겠느냐만, DB 날아가면 골치아픔.