[Spring JPA] ORM과 JPA
Status: Done ✨
ORM이란?
ORM은 객체와 DB 테이블이 매핑을 이루는 것을 의미한다. 즉, 내가 코드 상에서 생성한 객체가 DB상에 어떤 테이블과 연결이 된다는 것을 의미한다. 이렇게 되면 내가 객체를 조작함으로써 DB를 조작할 수 있게 된다.
이러한 예시로 우리는 JPA를 떠올릴 수 있다.
우리는 JPA에서 DB에 대한 접근을 시도할 때 직접 sql 쿼리문을 만들지 않는다. 다만 객체를 이용한 메소드를 통해 이를 관리할 뿐이다.
예시를 보자
SELECT * FROM user
→ user.findAll()
해당 예시는 user라는 테이블에 모든 정보를 가져오는 것인데 우리는 이를 user라는 객체와 user테이블을 맵핑한 후 findAll이라는 메소드를 통해 정보를 가져오는 것을 볼 수 있었다.
정말 편해보이고 예쁘게 DB에 대한 접근이 가능해졌다. 그럼 이러한 방법이 어떻게 가능한 것인지 알아보자.
먼저 해당 과정에 대한 전체적인 구조도는 다음과 같다.
이를 이해하기 위해 하나씩 살펴보자
1. JDBC
JDBC는 Java Database Connectivity의 약자로 자바에서 데이터 베이스에 접속 할 수 있도록 해주는 자바 API이다.
DB의 종류는 많고 이에 따른 연결 설정도 다양해진다. 만약 개발자가 사용하는 DB에 따라 이러한 설정을 모두 정의하고 사용하게 된다면 코드는 매우 복잡해질 수 있다. 더군다나 여러 종류의 DB를 사용하게 된다면 코드는 더욱이 복잡해질 수 있다. 이러한 여러 서비스를 같은 곳에서 사용하는 경우는 많은 곳에서 볼 수 있는데 (SLF4J..) 대부분의 해결책은 각각의 End Point에 대한 드라이버 설정을 관리하는 driver manager를 만들고 이를 API화 시키는 것이다.
Java 또한 여러 DB와의 연결을 지원하기 위해 다음과 같이 DB와의 연결을 설정할 수 있게 해준다.
이러한 JDBC를 사용하여 코딩을 하면 다음과 같이 DB에 쿼리를 날릴 수 있다.
Connection conn = null;
try {
String dbConnectionString = "jdbc:Driver 종류://IP:포트번호/DB명"
String dbUser = "DB 아이디"
String dbPassword = "DB 비밀번호"
}catch(SQLException ex){
// 에러 발생
}finally{
if(conn != null) try {conn.close();} catch(SQLException ex){}
}
2. Hibernate
이제 우리는 Java에서 DB와 연결을 어떤식으로 성립하는지 알게 되었다. 하지만 우리의 목표는 이러한 연결을 객체와 맵핑 시켜서 메소드로 SQL쿼리를 날릴 수 있게 하는 것이다.
먼저 흔히들 말하는 Hibernate에 대한 정의는 다음과 같다
즉, JPA는 인터페이스 이고 Hibernate는 이러한 JPA를 구현하는 구현체 라는 것이다. 그럼 이렇게 생각 할 수 있다, Hibernate말고 다른 구현체를 사용해도 되는거 아냐? 맞다. Hibernate이외에도 DataNucleus, EclipseLink등이 있는데 ORM을 위해서는 주로 Hibernate를 사용한다. 이는 Hibernate가 굉장히 성숙한 라이브러리이기 때문이라는데 이에 대해 깊이 공부를 해보고 나중에 생각해봐야겠다.
그럼 당연히 JPA를 알아야 보다 Hibernate를 잘 이해할 수 있을 것이다.
3. JPA
JPA를 이해하기 위해서는 JPA가 어떻게 객체를 관리하고 이를 테이블과 mapping시키려고 하는지 이해 해야 할 것이다.
즉, JPA에서 중요한 것을 정리하면 다음과 같이 2가지로 정리할 수 있을 것이다.
- 객체와 관계형 DB 연결
- JPA 내부 동작
사실 위의 내용은 다음과 같이 정리할 수 있을 것이다.
JPA는 영속성 컨텍스트인 EntityManager를 통해 Entity를 관리하고 이러한 Entity가 DB와 매핑되어 사용자가 Entity에 대한 CRUD를 실행을 했을 때 Entity와 관련된 테이블에 대한 적절한 SQL 쿼리문을 생성하고 이를 관리하였다가 필요시 JDBC API를 통해 DB에 날리게 된다.
이제 이 말을 이해해보자.
영속성 컨텍스트
영속성 컨텍스트란 Entity를 저장하는 비휘발성 환경이라는 뜻이다.
Entity란?
Entity는 DB의 Entity와 동일한 개념이고 단지 이를 Java라는 객체지향 언어에서 객체로 관리하는 것을 의미한다. 즉, Entity를 관리한다는 것은 우리가 코드 상에서 쓰이는 DB의 테이블을 객체화 해서 관리한다는 것을 의미한다.
그럼 그냥 Heap 처럼 되어있는 메모리의 일부분일까?
그러면 뭔가 영속성 컨텍스트같은 멋있는 말을 붙이지 않았을 것이다.
우리가 영속성 컨텍스트라는 것을 명명하고 정의하고 사용하는 이유는 DB와의 연결은 결국에는 kernel 에게 IO처리를 하게 하는 네트워크 통신이기 때문이다.
즉, 우리는 DB와의 통신 횟수나 방식으로 효율적으로 관리해야 서버의 속도가 빨라진다는 것이다.
이러한 이유는 memory나 cache를 두는 이유와 동일하다. 따라서 영속성 컨텍스트는 Entity에 대한 캐시라고 이해하면 쉬울 것이다. 이렇게 Entity를 캐시처럼 다루면서 얻는 이점은 다음과 같이 정리 될 수 있다.
1차 캐시
다음과 같이 우리가 특정 row를 불러오고 이에 대해 수정을 여러번 진행하는 상황을 가정해보자.
정보1 불러오기 -> 정보 1 수정하기 -> 정보 1 저장하기 -> ..... -> 정보 1 수정하기
우리가 정보 1을 캐싱을 해놓지 않고 수정을 한다고 한다면 DB와의 통신을 한번 더 해야할 것이다. 하지만 우리가 Entity를 캐싱해놓기 때문에 이러한 불필요한 DB와의 통신이 줄어들 것이다.
동일성 보장
우리가 사용하는 Entity 인스턴스에 대한 참조값 비교가 동일하다는 것을 의미한다. 다음 예시를 보자.
이렇게 DB가 공유잠금을 지원하지 않아도 Application상에서 이를 지원한다는 것이다.
Member a = entityManager.find(Member.class, "member1");
Member b = entityManager.find(Member.class, "member1");
System.out.println(a == b); // true
사실 이러한 특성 때문에 문제를 발생시킬 수도 있기 때문에 이러한 특성은 잘 이해해두어야 할것이다.
쓰기 지연
다음 예시를 보자
빈번한 쓰기 작업을 하게 되면 context switching이 여러번 발생할 것이다. 따라서 이러한 Context Switching에 대한 overhead를 줄여주는게 쓰기 지연이다. 즉, 쓰기를 원하는 entity를 저장해 두었다가 한번에 처리한다는 것을 의미한다.
EntityManager entityManager = emf.createEntityManager();
EntityTransaction transaction = entityManager.getTransaction();
transaction.begin(); entityManager.persist(memberA);
entityManager.persist(memberB);
// 이때까지 INSERT SQL을 DB에 보내지 않는다.
// 커밋하는 순간 DB에 INSERT SQL을 보낸다.
transaction.commit(); // Transaction 커밋
Dirty Checking
영속성 컨텍스트에서 관리하는 Entity에 대한 db상의 실제 업데이트는 영속성 컨테스트에서 commit이나 flush할 때 Entity에 대한 값이 조회할때를 기준으로 변경되었는지 확인한 후 바뀌 부분에 대해서 한꺼번에 db에 대한 업데이트를 지원하기 때문에 여러 비지니스 로직상에서 db상에 값이 꼬이는 경우를 방지해준다.
JPA에서는 이러한 영속성 컨텍스트에 대한 구현체를 Entity Manger라고 하고 이러한 Entity Manager를 관리하는 것을 EntityMangerFactory라고 한다.
우리가 Entity를 관리한다는 것을 EntityManger가 Entity에 대한 생명주기를 관리한다는 것을 의미할것이다. 마치 메모리에서 데이터를 언제 올리고 어떻게 내릴지에 관해 관리하는 것처럼 말이다. 이러한 Entity에 대한 Life cycle을 정리하면 다음과 같이 정리될 수 있다.
- 비영속
영속성 컨텍스트와 전혀 관계가 없는 상태로 엔티티를 만들기만한 상태를 의미한다.
MemberEntity member = MemberEntity.builder().id("member1").name("회원");
- 영속
영속성 컨텍스트에 저장된 상태를 의미하고 해당 Entity가 영속성 컨텍스트에 의해 관리되는 상태를 의미한다.
EntityManager entityManager = entityManagerFactory.createEntityManager(); # EntityManagerFactory를 통해서 EntityManager를 만드는 것을 의미한다. entityManager.getTransaction().begin(); # EntityManger가 시작되는 것을 의미한다. entityManager.persist(member); # 방금 만든 Entity에 대하여 EntityManager에 등록하는 것을 의미한다.
- 준영속
영속성 컨텍스트에 저장되었다가 분리된 상태로 현재 영속석 컨텍스트에서 지워진 상태를 의미한다.
entityManager.detach(manager);
- 삭제
실제 DB 삭제를 요청한 상태이다
entityManager.remove(member);
JPA의 동작 과정
- 저장 과정
- Entity Manager에게 Entity Object를 Persist하여 등록하게 된다.
- Entity Manager가 이러한 Entity Object를 분석한다
- 해당 Entity와 관련한 SQL문을 생성한다.
- JDBC API를 사용하여 SQL을 DB에 날리게 된다.
- 조회 과정
- Entity Manger와 관련한 pk값을 JPA에게 넘긴다.
- 엔티티의 매핑정보를 바탕으로 SELECT SQL문을 작성한다.
- JDBC API를 통해 SQL을 DB에 날린다.
- DB로 부터 받은 결과를 Entity Object에 매핑하게 된다.
- 이를 JPA가 반환해준다
다시 앞써 이야기 했던 문장을 보자
JPA는 영속성 컨텍스트인 EntityManager를 통해 Entity를 관리하고 이러한 Entity가 DB와 매핑되어 사용자가 Entity에 대한 CRUD를 실행을 했을 때 Entity와 관련된 테이블에 대한 적절한 SQL 쿼리문을 생성하고 이를 관리하였다가 필요시 JDBC API를 통해 DB에 날리게 된다.
- JPA는 영속성 컨테스트를 사용하여 db와의 통신을 효율적으로 관리한다.
- 이러한 영속성 컨텍스트에 대한 JPA상에서 구현체를 EntityManger라고 한다.
- 이러한 EntityManger는 Entity에 대한 생명주기를 관리하고 db와의 연결정보를 저장해둔다.
- Entity에 대한 CRUD는 연결정보를 바탕으로 JPA가 자동으로 생성해준다.
- 이러한 쿼리문은 EntityManger가 관리를 하다가 필요시 처리하게 된다.
4. Spring Data JPA
이는 스프링에서 JPA를 보다 편리하게 사용할 수 있도록 지원하는 프로젝트로 다음과 같은 3가지의 특성을 지원한다.
- CRUD처리를 위한 공통 인터페이스 제공
- repository 개발 시 인터페이스만 작성하면 실행시점에 스프링 데이터 JPA가 구현객체를 생성하여 DI시켜줌
- CRUD를 위한 여러 메소드들이 자동으로 생성된다.
Q) Spring Data JPA가 자동으로 설정해주는 Method 이외의 SQl문을 실행하고 싶을때는?
사실 이게 이러한 글을 쓰게 된 원인이다.
내가 겪은 문제상황은 다음과 같다.
💡 우리가 필드의 조건에 따라 정보를 불러와야하는데 필드가 계속 바뀐다면 어떻게 할까?
단순한 방법으로는 다음과 같이 메소드를 짤 수도 있을 것이다.
@Autowired
MemberRepository memberRepository
public List<MemberEntity> FindMeberWithCondition(String field, Object value){
if(field == "name"){
return memberRepository.findByName(value)
}
else if(field == "age"){
return memberRepository.findByAge(value)
}
....
}
개인적으로 이렇게 분기문을 작성해서 수정이 필요할 때마다 else if문을 넣는게 맞는 방법일까 생각했었다.
물론 field를 명시해서 진행하는 방법이 더 안전할 수도 있지만 뭔가 방법이 있을 것 같은 느낌에 방법을 찾다보니 @Query에 대해서 찾을 수 있었다.
@Query(value = "SELECT * FROM member WHERE :field = :value", nativeQuery=true)
List<MemberEntity> findByCondition(@Param("field") String field, @Param("value")String value);
이는 사용자가 직접 작성한 쿼리문을 Repository상에서 메소드로 정의하고 이를 JPA와 연결시켜주는 작업을 해주는 어노테이션이다.
하지만 이러한 방법 또한 문제가 있었는데 field를 지정해주지 않으면 hibernate가 이를 수행하지 못한다는 것이다.
이렇게 코드를 짜서 실행시키면
이렇게 Hibernate문이 실행되고 결과가 나오지를 않는다.
찾아보니 필드가 지정되지 않아서 그렇다는 것이다.
그럼 SQL문을 직접 DB에 날리는 방법이 있지 않을까라는 생각에 찾다가 EntityManger에 직접 쿼리를 생성해서 날리는 방법을 찾았다.
@PersistenceContext
private EntityManager entityManager;
public List<MemberEntity> findByCondition(String field, String value) {
String jpqlString = "SELECT * FROM member WHERE member." + field + " = ?1";
try {
List<MemberEntity> result = entityManager.createNativeQuery(jpqlString, MemberEntity.class).setParameter(1, value).getResultList();
return result;
}catch(Exception e) {
return null;
}
}
해당 코드는 String으로 만든 SQL쿼리문을 entityManager에 직접 주입시키고 반환 값을 MemberEntity에 직접 매핑시켜 값을 반환하는 메소드이다.
이렇게 코드를 짜니 어떤 field인지에 상관없이 조건문 조회가 가능한 것을 볼 수 있었다.
이러한 방식의 위험성은 뭐가 있는지 궁금하다..
참고
https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa/
https://gmlwjd9405.github.io/2019/08/04/what-is-jpa.html
https://www.tutorialspoint.com/jdbc/jdbc-introduction.html
'개발 > Spring' 카테고리의 다른 글
[Spring] Spring Security의 이해 (0) | 2022.01.19 |
---|---|
[Spring] Spring 을 이용한 웹 서비스 구조 (0) | 2022.01.12 |
[Spring]@Transactional과 JUnit Test (2) | 2022.01.11 |
[Spring MVC] DispatcherServlet은 어떻게 request랑 controller를 이어줄까? (0) | 2022.01.06 |
[Spring] Controller request 유효성 검사 (0) | 2022.01.05 |