9. JPA
너무 대충 작성한 글이라서 취소 처리합니다.
나중에 시간이 된다면 다시 작성할 예정입니다.
"스프링부트 백엔드 프로그래밍"이라는 주제로 약 8주간 글을 작성할 예정입니다. 제 스터디가 잘못된 방향으로 가지 않도록, 의견 및 조언을 아낌없이 해주시길 부탁드립니다.
지난주에는 AOP, 스프링 캐시 추상화에 대해서 공부하엿습니다. 이번 주에는 JPA 에 대해서 공부합니다. JPA 는 제가 자신없는 기술이고, 선호하지 않는 기술이라서 커리큘럼에 넣고 싶지 않았지만... 5주차 과정으로 넣어봤습니다. JPA 는 내용이 매우 방대합니다. 글 하나로 정리할 수 있는 내용이 절대 아닙니다. 이 글은, JPA 를 처음 접하는 개발자에게, JPA 가 뭐인지 설명하는 수준입니다. 잘못된 내용은 꼭 제보해주세요!
어떻게 설명하면 주니어 개발자들이 쉽게 이해할 수 있을지 많은 고민을 했습니다만, 누군가에게 지식을 전달하는 것은 너무 어려운 일입니다. 스터디 처음 시작은 가벼운 마음으로 시작하였습니다만, 튜터링이 생각처럼 쉽지 않은 것 같습니다.
1주 차 - 스프링부트란 무엇인가?, 간단한 API 서버 만들어보기
2주 차 - 스프링 프레임워크 기본 개념 이해하기
3주 차 - Rest API, 테스트 코드 작성하기, 예외 처리하기
[미정, 나중에 시간되면 작성] 6. Rest API (HTTP 기본 개념)
4주 차 - AOP, 스프링부트 캐시 추상화, Redis 연동하기
5주 차 - JPA
9. [이번 글] JPA
6주 차 - Spring EventListener, MQ, Pub/Sub
10. Spring EventListener, MQ, Pub/Sub 기본 개념
11. RabbiMQ, KAFKA 사용해보기
7주차 - 보안(인증)
12. Spring Session, JWT
13. Spring Security
[미정] 8주 차 - 병렬, 비동기 프로그래밍
14. Spring Async
[미정] Spring Cloud, Spring Session 등
JPA 에 대해서 공부하기 전에, 이것저것 가볍게 알아봅니다.
JDBC는 데이터베이스에 접근하기 위한 자바 표준 API 입니다.
- JDBC 드라이버를 로딩한다.
- DBMS에 연결한다.
- SQL 문을 데이터베이스에 전송하고 결과 값을 받는다.
참고로, JDBC Driver는 자바 프로그램의 요청을 DBMS가 이해할 수 있는 프로토콜로 변환해주는 클라이언트 어댑터입니다. 일반적으로 데이터베이스 연동 작업은 커넥션 연결, Sql 쿼리 전송 등의 작업을 수행합니다.
- Connection : 데이터베이스와 연결(세션)
- Statement : SQL문을 실행하거나 SQL 문의 결과를 반환하는 데 사용
자바 프로그래밍 언어로 샘플 코드는 아래와 같습니다. DB 에 직접 커넥션을 맺는 구문을 작성해야 합니다.
[Java]
Connection connection;
Class.forName("driverClassName");
Connection connection = DriverManager.getConnection(url, user, password);
Statement statement = connection.createStatement();
String sql = "select * from coffees";
ResultSet result = statement.executeQuery(sql);
connection.close();
하지만, 저는 실무에서 이런 코드를 사용해본적은 없습니다.
스프링 프레임워크에서는 JDBC를 쉽게 사용할 수 있도록, JDBCTemplate 를 제공합니다. JDBCTemplate는 아래 기능을 지원합니다.
- Connection 열기와 닫기
- Statement 준비와 닫기
- Statement 실행
- Exception 처리와 반환
- Transaction 처리
JDBC Template는 JDBC를 쉽게 사용할 수 있게 도와줍니다.
[Java]
@Autowired
JdbcTemplate jdbcTemplate;
생략...
jdbcTemplate.execute("DROP TABLE cafe IF EXISTS");
jdbcTemplate.execute("CREATE TABLE coffees(" +
"id SERIAL, name VARCHAR(255))");
List<String> coffees = Arrays.asList("mocha", "latte", "americano");
jdbcTemplate.batchUpdate("INSERT INTO... 생략
jdbcTemplate.query("SELECT id...생략
Java 로 직접 커넥션을 맺는 코드는 필요 없어졌습니다. 간단한 데이터베이스 연동 작업이라면 JDBC Template를 사용해도 괜찮을 것 같다는 생각입니다. 하지만, 도메인이 복잡해지고, 많은 기능을 구현해야 한다면, JDBCTemplate 로 작성한 코드는 유지보수가 쉽지 않습니다. 대안으로, MyBatis, JPA(Hibernate) 등의 기술을 많이 사용합니다.
- Mybatis
- Hibernate (JPA)
Mybatis는 자바에서 데이터베이스 연동 프로그래밍을 쉽게 해주는 프레임워크입니다. 위에서 설명한 JDBCTemplate 로 개발하는 경우에는 세부적인 작업을 직접 구현해야하며, 작업별로 각각의 메서드를 호출해야 합니다. 즉, 다수의 메서드를 호출하고 객체를 해제해야 하는 단점이 있습니다. Mybatis를 사용면 생산성도 뛰어나고, SQL문을 소스 코드에서 분리할 수 있는 장점이 있다고 합니다.
[xml]
<select id="selectUsers" resultType="map">
select
<include refid="userColumns"><property name="alias" value="t1"/></include>,
<include refid="userColumns"><property name="alias" value="t2"/></include>
from some_table t1
cross join some_table t2
</select>
Mybatis 에 대한 경험이 많지는 않지만, 현재 회사에서도 사용하고 있고, 전 회사에서도 일부 프로젝트에서 사용을 했었습니다. Mybatis 가 좋다, JPA 가 좋다, 등 어떤 기술이 좋고 나쁨에 대해서 이 글에서는 토론하지 않습니다. 각자 맡고 있는 도메인에 맞게 기술을 잘 선택해야 합니다.
ORM(Object-Relational Mapping)이란 이름대로, 객체와 관계형 DB를 매핑해주는 개념입니다. OOP 언어와 RDBMS의 상이한 시스템을 맵핑하여, 개발자가 데이터 접근보다 로직 자체에 집중할 수 있도록 해줍니다. ORM 이 없을 때는 데이터베이스 작업을 하기 위해서 SQL 문을 직접 만들어야했습니다. 반면에, ORM을 사용하면 SQL 문에서 자유로워지고 유지보수가 간편해집니다. 물론, ORM 도 단점이 있습니다.
ORM 은 추상적인 개념입니다. ORM 기술에 대한 표준 명세가 바로 JPA이고, JPA 표준을 구현한 프레임워크가 바로 Hibernate(하이버네이트)입니다. 대표적은 ORM 프로젝트는 아래와 같습니다.
많은 ORM 기술이 있지만, 스프링에서는 하이버네이트를 주로 사용합니다. JPA는 자바 ORM 기술에 대한 표준 명세를 정의한 것이며, JPA 만으로는 실제로 무언가를 할 수는 없습니다. JPA 표준 명세를 실제로 구현한 구현체가 필요하며, 대표적인 구현체가 바로 Hibernate 인 것입니다. 참고로, 스프링부트의 Spring Data JPA 디펜던시를 추가하면, 기본으로 Hibernate가 구현체로 설정됩니다. 아무튼 JPA를 사용하면 어떤 장점이 있을까요?
- 도메인을 객체지향 관점에서 설계 및 구현할 수 있다.
- 직접 SQL문을 작성하지 않고, JPA API를 통해서 만들어진 SQL을 통해서 DBMS를 사용할 수 있다.
- JPA 표준으로 인해서, 다른 구현 기술로 변경이 용이하다.
- 동일 트랜잭션 내 데이터 중복 조회 시 한 번만 DB에 전달한다.
- 유지보수가 쉬워진다.
사실, JPA 에 대한 호불호가 꽤 갈리며, 단점도 있습니다. 어쨌든 스프링 프로젝트에서 JPA는 빠질 수 없는 필수 기술이 된 상황이기는 합니다. JPA & 하이버네이트라는 기술은... 결국에는 JDBC Driver를 사용합니다. JDBC와 Java Application 사이에 JPA+Hibernate 레이어가 추가되었다고 생각하시면 됩니다.
"위 내용에 대해서 잘못된 점이 있다면 피드백 부탁드립니다."
영속성 컨텍스트에 대한 자세한 설명은 생략합니다. 중요하지 않아서가 아닙니다. 너무 중요합니다. 구글링을 하시면, 수많은 글을 찾을 수 있습니다. 영속성 컨텍스트를 이해해야, 이 글을 제대로 읽을 수 있습니다.
잠시 이 글을 나가신 후,
영속성 컨텍스트에 대해서 공부한 후 다시 돌아오세요.....
아주 심플한 호텔 예약 시스템을 구현해봅시다.
호텔 예약 시스템에는 4개의 도메인 모델을 정의합니다.
- Hotel
- Room
- Customer
- Reservation
도메인 모델을 상위 수준의 애그리거트로 묶어서 표현하면, 아래와 같습니다.
Hotel, Room 두개의 모델을 객실이라는 애그리거트로 묶었습니다.
ERD 는 아래와 같습니다.
글로 설명은 생략. 스터디 시간에 코드로 설명 예정
너무 중요한, 연관관계 매핑에 대해서는 다른 문서를 참고하시길 바랍니다. 너무 중요한 내용인데, 제대로 설명할 자신이 없습니다... ㅠㅠ 인프런 강의를 추천하며, 주변 선배 개발자가 있다면 물어보시길 바랍니다.
일단, 스터디를 위해서 몇가지 의견만 정리하였습니다. 지극히 개인적인 의견입니다...
1)연관관계 매핑은 같은 애그리거트 내에서만 매핑을 하였습니다. 샘플 코드에서는 '객실' 이라는 애그리거트에 Hotel, Room 도메인을 정의하였습니다. 연관관게 매핑은 아래와 같이 4가지 관계가 있습니다.
- 다대일
- 일대다
- 일대일
- 다대다
해당 두 도메인은, 호텔 입장에서는 1대다 매핑입니다. Hotel 이 "1"이고, Room 이 "다" 입니다. 하나의 호텔에 여러 Room 이 속해 있습니다. 반대로 Room 입장에서는 "다" 대 "일" 입니다. 도메인 비즈니스에 따라서 변경이 될 수 있습니다.
영속성 전이를 할 수 있도록, cascade 설정도 추가하였습니다. fetch 설정은 생략되었네요. fetch 는 매우 중요합니다. fetch 는 두가지 설정이 있스빈다.
fetch = FetchType.EAGER : 연관 엔티티를 동시에 조회합니다.
fetch = FetchType.LAZY : 연관 엔티티를 실제 사용할 때 조회합니다.
fetch 에 대해서는, 스터디 시간에 설명하겠습니다만, 따로 공부하세요.
어쨋든, 방향성에는 단방향, 양방향 두가지 관계가 가능합니다. Hotel, Room 객체가 서로 참조할 수 있도록 설정한다면, 양방향이 됩니다. 단, 양방향은 단방향에 비해서 설정이 조금 더 복잡해집니다.
자세한 설명은 생략...
예약 도메인에서는, 관련있는 Room, Customer 도메인에 연관관계 매핑을 하지 않았습니다. ID 참조를 하도록 설정하였습니다.
ID 참조 방식은 장단점이 있습니다. ID 참조 방식을 사용하면, N+1 문제가 발생할 수도 있고.. 등등..
스터디 시간에 설명하기로...
로컬에서 개발할 때는 주로, H2 를 사용합니다. 저는, 테스트 코드를 돌릴때 주로 H2 를 사용합니다.
gradle 에 H2 관련 의존성을 추가해줘야 합니다. 스프링부트를 사용하기 때문에, AutoConfiguration 이 되어서, DataSource 빈을 정의하지 않아도 자동으로 빈이 등록됩니다. 물론, 실무에서는 Bean 을 재정의해서 사용하는 경우가 많습니다. 어쩃든, Active Profile 을 H2 로 설정하면, H2 DB 실행이 되며, 미리 정의한 샘플 데이터를 저장할 수 있습니다.
어쨋든, 자세한 설명은 이글에서는 생략... 스터디 시간에 말로 할게요....
고객 정보의 데이터를 제공하는, CustomerRepository 인터페이스를 아래와 같이 정의합니다.
CustomerRepository 인터페이스를 구현한, 구현체 클래스를 작성해보자.
@PersistenceContext 어노테이션과 함께 EntityManager 를 선언해줍니다. @PersistenceContext 어노테이션이 선언된 변수에, EntityManager 객체를 자동으로 할당해서 주입해줍니다.
아... EntityManager 가 뭔지 아시나요? ㅠㅠ 설명을.. 패스한거 같은데, 이것도 각자 공부하세요.
ID 로 고객을 조회하는 메서드는 아래와 같습니다. EntityManager 의 find 를 사용하면 됩니다.
find 메서드는, 영속성 컨텍스트에 엔티티가 존재하는지 확인한 후, 있으면 사용합니다. 만약, 영속성 컨텍스트에 없다면 DB 에서 데이터를 새로 조회해서 영속성 컨텍스트에 등록한 후 사용합니다.
영속성 컨텍스트에 대한 그림 추가 예정
-- 그림으로 설명...ㅠㅠ 문서 작성 중...
아무튼, Controller 을 하나 만들어서 호출해봅시다.
실제로 DB 에도 당연히 저장되어 있습니다.
엔티티 변경은 어떻게 하면 될까? 예를 들어서, 고객의 이메일 정보를 업데이트하고 싶습니다. Customer 도메인에 changeEmail 이라는 메서드를 정의하였습니다. 참고로, Customer 객체에 @Setter 어노테이션 또는 @Data 어노테이션이 선언되어있지 않습니다. 즉, Service 레이어에서 Customer 객체에 setEmail() 와 같은 방식으로 객체를 변경하지 않습니다. Customer 에 도메인에 관련한 메서드를 직접 선언해주고, 해당 메서드를 호출할 수 있도록, 변경에 대한 책임을 Customer 이 다 할 수 있도록 합니다...
자세한 내용은 스터디 시간에..
어쨋든, 위와 같이 이메일 변경에 대한 기능을 정의하였습니다. 이메일 업데이트는 Service 응용 레이어에서 실행합니다. 아래와 같습니다. JPA 의 더티 체킹(Dirty Checking) 개념에 의하면, 엔티티가 변경된 것을 감지한 후, DB 에 업데이트를 자동으로 해줄 것입니다.
하지만, 위 코드에 의해서는, DB 에 업데이트 되지 않습니다. 이유는, 트랜잭션 코드가 누락되었기 때문입니다.
아래와 같은 순서로 동작해야 합니다.
- (누락)트랜잭션 시작
- 엔티티를 조회
- 엔티티의 값을 변경하고
- (누락)트랜잭션을 커밋
JPA 코드로는 아래와 같습니다.
EntityManager entityManager = entityManagerFactory.createEntityManager();
트랜잭션 시작 : entityManager.getTransaction().begin();
엔티티 조회 : find
엔티티 변경 :
트랜잭션 커밋 : entityManager.getTransaction().commit();
트랜잭션을 설정하지 않고 코드를 작성하면, 실제로 DB 에 업데이트가 되지 않습니다. 테스트 코드를 작성해서 실패를 검증해봅시다.
테스트는 예상대로 실패합니다.
하이버네이트 쿼리 로그를 보면, Select 쿼리는 발생하였지만, Update 로그는 없는 것을 확인할 수 있습니다. 즉, DB에 업데이트가 되지 않았습니다.
트랜잭션 코드를 추가해봅시다. EntityManager 객체를 사용해서 트랜잭션을 시작, 커밋 할 수 있습니다. 하지만, 제 샘플 소스에서는 스프링에서 제공하는, @Transactional 어노테이션을 사용하겠습니다.
테스트 코드를 실행해봅시다. 테스트는 성공합니다. 그리고, update 로그를 확인할 수 있습니다.
그림으로 그려봅시다.
그림.. 너무 중요.. 나중에 추가 예정..
가능하면, 어려운 설명은... 그림으로 설명하고 싶습니다만, 시간 관계상... 나중에
제 글이 이해가 잘 되시나요? 안되시면, 아래 링크를 읽어주세요.
https://jojoldu.tistory.com/415
조금 다른 사례로 설명해보겠스비다.
문서 정리 중..
persisit 관련 내용 정리해서 추가할 것..
-- 중요!!
신규 고객을 저장하는 기능을 구현해봅시다. 트랜잭션 관련 로직이 전혀 없이, entityManager.persist 를 실행하였습니다.
persist 를 실행하면, 영속성 컨텍스트에 엔티티를 관리할 수 있도록 등록이 됩니다. 하지만, 실제로 DB 에 업데이트는 트랜잭션이 커밋이 되어야 합니다. 위 코드만으로는, DB 에 신규 고객이 저장되지 않습니다.
ㅇ
ㅇ
ㅇ
위에서 선언했던 CustomerRepository 인터페이스를 다시 확인해봅시다. findById, save 메서드는 이미 구현하였습니다.
findAll, findByName 은 어떻게 처리하면 될까요? JPA 는 데이터베이스에 의존하지 않습니다. 즉, MySQL, 오라클, H2 등 어떤 DB 를 사용하는지와 상관없이 표준화된 동작을 지원해야 합니다. 데이터베이스마다 SQL 구문이 조금씩 다르기 때문에, 특별한 방법이 필요했습니다. 그래서, JPA 에서는 JPQR (JPA Query Language) 라는 특별한 언어를 제공합니다.
자세한 내용은 생략합니다.
어쨋든, 모든 데이터를 조회하는 findAll 메서드는 아래와 같이 JPQL 을 사용해서 구현하였습니다. 몇년만에 사용하는거라서,, 이렇게 하는게 맞는지도 모르겠네요.
JPA 2.0 버전부터는, Criteria 라는 동적 쿼리 빌더를 제공합니다. 이름으로 고객을 찾는 기능인, findByLastName 메서드는 Criteria 를 사용해봣습니다. 역시 오랫만에 사용하는거라서 확실하진 않습니다..
사실... 반전은
위 4개의 인터페이스는 Spring Data JPA 를 사용하게 되면,
개발자가 별도로 구현을 하지 않아도 됩니다...
9.3 에서, JPA 샘플 소스를 보면, Repository 에 메서드를 전부 구현하였습니다.
도메인 모델의 종류가 많아지고, Repository 에서 제공해야 하는 기능이 많아질수록, Repository 는 복잡해집니다.
Spring Data JPA 를 사용하면, 위와 같은 단점을 해결할 수 있습니다. Spring Data Common 에서는 CrudRepository 를 제공합니다. CrudRepository 에는 아래와 같이 기본 메서드를 제공합니다. 그리고, CrudRepository 를 상속하는 JpaRepository 인터페이스를 Spring Data JPA 에서 제공합니다.
Spring Data Common 에서는 CRUDRepository 가 정의되어있다면,
Spring Data JPA 에서는 JpaRepository 를 제공합니다.
ㅇㅇ
아래와 같이 정의해주면 됩니다.
ㅇ
ㅇ
개발자가 Repository 의 상세한 로직을 굳이 구현할 필요가 없습니다.
그렇다면, JPARepository 를 구현하는 실제 구현체는 어떻게 구성되어 있을까요? 즉, 우리가 샘플 코드에서 작성했던, CustomerRepositoryImpl 와 유사한 역할을 하는 구현체가 있어야 할 것입니다. 바로, SimpleJpaRepository 에서 그 역할을 수행합니다. (중요
해당 클래스에서 save 메서드를 찾아봅시다.
중요합니다. Spring Data JPA 에서 제공해주는 save 메서드에는, 그냥 JPA 를 사용했을 때와 마찬가지로, EntityManager 의 persist 를 사용하며, @Transactional 어노테이션이 선언되어 있습니다.
설명 추가 예정..
Spring Data JPA 에 대해서 설명하빈다.
d
ㅇ
Spring Data JPA, 스프링부트에서 AutoConfiguration, queryDSL 등에 대해서는 말로 설명합니다.
일단, 금일 2시 스터디 일정에 맞춰서 먼저 발행 후...
나머지 내용은 설 연휴, 시간 많을 때 마무리하겠습니다... 샘플 코드 역시, 정리가 안되어서 나중에 공개하겠습니다.
차주에는 JPA 심화 과정 또는 MQ 둘 중 하나 주제 선택해서 진행하겠습니다..