ORM은 객체와 관계형 데이터베이스의 테이블을 자동으로 매핑하는 방법이다. ORM을 통해서 쿼리문 작성이 아닌 코드로 데이터를 조작할 수 있다.
JPA는 자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다. JPA의 매커니즘을 보면 내부적으로 JDBC를 사용한다. JPA 구현체는 Hibernate, EclipseLink, DataNucleus 등이 있다.
JPA는 자바에 비유하면 인터페이스이고, JPA구현체는 클래스에 해당한다.
하이버네이트는 자바의 ORM 프레임워크이자, JPA 구현체 중 하나이다. 보통 하이버네이트의 기능을 더욱 편하게 사용할 수 있도록 모듈화한 Spring Data JPA를 활용한다.
6.4.1 Spring Data JPA
Spring Data JPA를 통해 하이버네이트의 엔티티 매니저(Entity Manager)를 직접 다루지 않고, 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작한다.
영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행한다. 영속성 컨텍스트는 세션 단위의 생명주기를 가진다. 영속성 컨텍스트에 들어온 객체를 영속 객체(Persistence Object)라고 한다. 엔티티 매니저는 영속성 컨텍스트에 접근하기 위한 수단으로 사용된다.
(세션은 보통 일정시간 동안 클라이언트의 상태 정보를 유지하고 관리하는 데 사용된다. ex : 사용자 인증 정보 유지, 장바구니 유지, 사용자 활동 추적, 상태관리 등)
6.5.1 엔티티 매니저(Entity Manager)
엔티티 매니저는 엔티티 매니저 팩토리(Entity ManagerFactory)에서 만들어지는 엔티티를 관리하는 객체로, 데이터베이스에 접근해서 CRUD 작업을 수행한다.
Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터베이스에 접근하는데, 내부 구현체에서 엔티티 매니저를 사용한다. 그리고 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정으로 엔티티 매니저 팩토리가 동작한다.
반면 하이버네이트에서는 엔티티 매니저 팩토리 객체를 사용하기 위해 persistence.xml이라는 설정 파일을 구성하고 사용할 수 있다.
6.5.2 엔티티의 생명주기
비영속(new), 영속(managed), 준영속(detached, 영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태), 삭제(removed)
6.6.1 프로젝트 생성
Spring Data Jpa 의존성을 추가해 준다. gradle기준으로 db로 mysql을 사용할 경우 보통 아래와 같이 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.mysql:mysql-connector-j'
그리고 데이터베이스 정보를 application.properties에 작성해 준다.
// 1. 데이터베이스의 드라이버 정의
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
// 2. 해당 db의 경로를 지정(여기서는 zeroorder라는 테이블 경로까지 지정)
spring.datasource.url=jdbc:mysql://localhost:3306/zeroorder?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true
// 3. DB접속 아이디 입력
spring.datasource.username=root
// 4. DB접속 암호 입력
spring.datasource.password={본인 패스워드}
// 5. 하이버네이트가 생성한 쿼리문 출력 옵션
spring.jpa.show-sql=true
// 6. 데이터베이스 자동으로 조작하는 옵션
spring.jpa.hibernate.ddl-auto=create
db 경로를 지정할 때 쿼리 파라미터를 통해서 useSSL(DB에 SSL로 연결할건지 여부), allowPublicKeyRetrieval(서버에서 RSA 공개키를 검색하거나 가져와야하는지 여부) 등 설정을 할 수 있다. 보통 useSSL=false로 지정하면 보안을 위해 allowPublicKeyRetrieval=true로 해서 RSA 공개키를 가져오게 하는게 좋다.
RSA : 공개키 암호화 시스템
하이버네이트가 생성한 쿼리문 출력 옵션을 true로 해서 출력할 경우 'spring.jpa.properties.hibernate.format_sql=true'를 추가 입력하여 보기 좋게 포맷팅 할 수 있다.
자동조작 옵션은 다음과 같이 있다. 주로 운영환경에서는 기존 데이터의 보호를 위해 validate, none을 주로 사용하고, 개발환경에서는 create 또는 update를 주로 사용한다.
JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 따라서 엔티티에 어노테이션을 사용하여 테이블 간의 연관관계를 정의한다.
6.7.1 엔티티 관련 기본 어노테이션
6.8.1 리포지터리 인터페이스 생성
리포지토리는 Spring Data JPA가 제공하는 인터페이스이다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
위와 같이 JpaRepository를 상속받아 인터페이스를 만든다. <>에는 해당 엔티티와 아이디의 타입을 입력한다.
6.8.2 리포지터리 메서드의 생성 규칙
DAO는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다. DAO는 스프링에서 리포지터리 클래스로 대체될 수도 있다. 규모가 큰 서비스에서는 데이터를 다루는 중간 계층을 두는 것이 유지보수 측면에서 용이한 경우가 많다.
6.9.1 DAO 클래스 생성
1. DAO 인터페이스를 생성한다.
2. 생성한 인터페이스를 구현하는 DAO 클래스를 만든다. 클래스를 만들 때 스프링이 관리하는 빈으로 등록하기 위해 @Service 또는 @Component 어노테이션을 지정한다.
3. 클래스 내 각 메서드를 구현한다.
리턴값을 DTO객체에 담아서 전달할 지 엔티티 객체로 전달할 지 견해 차이가 있다.
// 1. DB에 저장
@Override
public Member insertMember(Member member){
Member savedMember = memberRepository.save(member);
return savedMember;
}
// 2. DB조회
@Override
public Member selectMember(Long id){
Member selectedMember = memberRepository.getById(id);
return selectedMember;
}
// 3. DB업데이트
@Override
public Member updateMemberName(Long id, String name) throws Exception{
Optional<Member> selectedMember = memberRepository.findById(id);
Member updatedMember;
if (selectedMember.isPresent()) {
Member member = selectedMember.get();
member.setName(name);
member.setUpdatedAt(LocalDateTime.now());
updatedMember = memberRepository.save(member);
} else {
throw new Exception();
}
return updatedMember;
}
// 4. DB 데이터 삭제
@Override
public Member deleteMember(Long id) throws Exception{
Optional<Member> selectedMember = memberRepository.findById(id);
if (selectedMember.isPresent()) {
Member member = selectedMember.get();
memberRepository.delete(member);
} else {
throw new Exception();
}
}
데이터를 조회할 때 getById() 또는 findById()를 사용할 수 있는데, 각 차이는 다음과 같다.
그리고 데이터를 업데이트, 삭제할 경우 findById를 통해 객체를 불러온 후 저장 또는 삭제한다.
6.10.1 서비스 클래스 만들기
기능이 복잡할 경우 서비스 레이어와 비즈니스 레이어로 나누기도 한다. 나누어 구현할 경우 도메인을 활용한 세부 기능들은 비즈니스 레이어에서 구현하고, 핵심 기능을 전달하도록 서비스 레이어에 구현한다.
각 메서드의 반환값은 보통 dto클래스를 통해 데이터를 전송한다. 이 때 입력받는 값의 dto클래스, 반환하는 값의 dto클래스를 각각 따로 만들수 있다. dto 클래스를 구현할 때 빌더 메서드를 추가로 구현할 수도 있다. 빌더 메서드는 데이터 클래스를 사용할 때 생성자로 초기화할 경우 모든 필드에 값을 넣거나 null을 명시적으로 사용해야 하는데 이러한 단점을 보완하여 필요한 데이터만 설정할 수 있도록 한다.
반복되는 코드를 줄이기 위해 롬복 라이브러리를 많이 사용한다. 아래와 같은 어노테이션이 주로 사용된다.
@Getter / @Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor : 필드 중 final이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동 생성한다.
@EqualsAndHashCode