# DB 연결 설정
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost:3306/bootes
spring.datasource.username=bootuser
spring.datasource.password=bootuser
# JPA 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
JPA 를 사용하기 위해서 스프링에서 설정해야 하는 것들이 몇 가지 있다. application.properties 에 위의 코드를 작성해주자. ddl-auto 는 프로젝트가 시작할 때 자동으로 쿼리를 생성하는 것으로 테이블이 없다면 create 문을 생성하고, 있으면 alter 문을 생성하게 된다. format_sql 은 Hibernate 가 동작할 때 발생하는 SQL 을 출력시켜준다. show-sql 은 JPA 가 동작할 때 발생하는 SQL 을 출력시켜준다.
Entity & Repository
Spring Data JPA 개발에는 두 종류의 코드가 필요하다. 테이블 정보를 가지는 엔티티 클래스와 엔티티객체 처리 기능을 가진 Repository 가 필요하다. 유의사항이 하나 있다. 두 클래스의 패키지를 꼭 구분시켜주어야 한다. 또한 패키지 명은 마음대로 정하는 것이 아니라 정해진 이름대로 작업을 해야 협업이 가능해진다.entity(테이블 정보를 가짐) 에는 클래스 파일을 생성해서 넣어주고 repository(앤티티객체를 처리하는 기능을 가짐) 에는 Interface 파일을 생성해서 넣어준다.
먼저 클래스 위의 Annotation 부터 살펴보자. @Entity 는 JPA 가 관리하는 객체라는 것을 의미한다. @Table 은 실제 테이블 이름을 작성한다. 만약 여기서 해당 Annotation 을 생략하면 클래스 이름이 테이블 이름이 됩니다. 나머지 Annotation 은 클래스를 쉽게 사용하기 위해서 lombok 기능을 사용했습니다.
그 다음에 클래스 내부를 살펴봅시다. @Id 는 Primary Key 필드를 만들기 위해서 사용합니다. Entity 에서는 Primary Key 가 있어야 합니다. 즉 @Id 는 무조건 한 번 사용해야 합니다. Primary Key 는 Not null 속성을 자동으로 갖는데, 생성 방식을 지정해줄 수 있습니다. @GeneratedValue 에서 사용된 strategy = GenerationType.IDENTITY 는 일련번호를 자동으로 생성해서 부여해주어서 not null 을 지켜줄 수 있습니다.
그 다음 @Column 에서는 길이와 Null 가능 여부를 설정하여 속성을 하나 추가해주었습니다. Dashboard 를 통해서 프로젝트를 실행시켜보겠습니다. 실행 결과를 보면 성공적으로 진행된 것을 알 수 있습니다. 또한 어떠한 쿼리 문이 실행되었는지 볼 수 있습니다. (우리가 이전에 설정을 하였기 때문에 나타납니다.) 또한 테이블이 생성된 것이 없기 때문에 create 문이 실행된 것을 볼 수 있습니다. application.properties 에서 ddl-auto=update 라고 설정해주었기 때문에 이렇게 작동하고 있습니다.
실제로 DBeaver 를 확인해보면 테이블이 정상적으로 생성된 것을 확인할 수 있습니다. 여기서 중요한 것은 우리가 테이블을 생성할 때는 실행된 쿼리문을 확인해야하고, 실제로 생성된 테이블도 확인해주어야 한다.
Hibernate 는 여러 인터페이스를 제공하는데, 가장 많이 사용되는 것이 JpaRepository 인터페이스이다. JPA 관련 작업을 별도의 코드 없이 처리할 수 있게 지원한다. 따라서 나의 MemeRepository 가 해당 인터페이스를 상속받으면 사용할 수 있다. 다만, 단순 검색, 삭제, 저장 말고 복잡한 것은 어쩔 수 없이 내가 기능을 구현해주어야 한다. 여기서 특이한 점은 인터페이스를 상속받았음에도 구현체가 없다는 것인데, 이거은 JPA 가 자동으로 구현체를 만들어서 빈으로 등록하기 때문이다. 그래서 실제로 MemoRepository 의 Autowired 를 만들어서 살펴보면 이상한 주소값을 볼 수 있다. JPA 가 자동으로 빈을 생성해서 사용하고 있기 때문이다. JpaRepository 인터페이스를 상속받을 때 2개의 매개변수를 던져주는데 Memo 와 Primary Key 의 자료형을 던져주면 된다.
package com.example.demo.repository;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.demo.entity.Memo;
@SpringBootTest
public class MemoRepositoryTest {
@Autowired
MemoRepository memoRepository;
@Test
public void 리퍼지토리인스턴스를가져왔는지확인() {
System.out.println("memoRepository = " + memoRepository);
}
@Test
public void 데이터등록() {
Memo memo1 = new Memo(0, "새글입니다");
memoRepository.save(memo1); // Insert
Memo memo2 = new Memo(0, "새글입니다");
memoRepository.save(memo2); // Insert
Memo memo3 = new Memo(0, "새글입니다");
memoRepository.save(memo3); // Insert
}
@Test
public void 데이터단건조회() {
Optional<Memo> result = memoRepository.findById(1); // 키 값으로 조회. 조회결과를 optional로 반환
if(result.isPresent()) { // 값이 있는지 확인
Memo memo = result.get();
System.out.println(memo);
}
}
@Test
public void 데이터전체조회() {
List<Memo> list = memoRepository.findAll(); //전체 목록은 리스트로 반환
for(Memo memo : list) {
System.out.println(memo);
}
}
@Test
public void 데이터수정() {
Memo memo = new Memo(1,"글이수정되었습니다"); //1번 메모의 내용 변경
memoRepository.save(memo); //데이터 추가,수정 모두 save 함수를 사용함
}
@Test
public void 데이터삭제() {
memoRepository.deleteById(1); //1번 메모가 없으면, DataAccessException 에러가 발생함
}
@Test
public void 데이터전체삭제() {
memoRepository.deleteAll();
}
}
Repository 의 목적은 Entity 를 컨트롤하는 것이다. 즉 테이블의 CRUD 의 기능을 수행하기 위한 기능이 Repository 에 들어있다.
그런데 Repository 는 엄연히 interface 이다. 즉 불완성된 상태라는 것인데, 어떻게 기능을 수행한다는 것일까? 그 이유는 JPA 가 자동으로 클래스를 만들어서 기능이 사용가능하게 끔 구현체를 만들었기 때문이다.
그래서 MemoRepository 의 bean 인 memoRepository 를 만들어서 다양한 함수들을 사용할 수 있게 된다. 여기서 사용할 수 있는 기본적인 기능들이 바로 다음과 같다.
실제로 테스트 코드를 하나씩 실행시켜보자.
=== save ; 데이터 추가하기 ===
실행시켜보니 다음과 같은 결과창을 볼 수 있다.
우선 Primary Key 값에 계속 0을 넣어서 오류가 날 것 같은데 오류가 나지 않는 이유는 우리가 Primary Key 에 auto_increment 설정을 주었기 때문에 0을 계속 주어도 오류가 나지 않고 있다.
실제로 데이터가 추가된 것을 보면 우리가 예상한대로 데이터가 추가된 것을 확인할 수 있습니다.
=== findById(x) ; 단건 데이터 조회하기 === 결과 값을 보면 Id 가 1인 데이터를 잘 검색해서 보여주는 것을 확인할 수 있습니다. 여기서 Hibernate 의 쿼리 문의 where 절에서 ? 가 있는 것을 볼 수 있는데, 실제로 어떤 값을 검색했는지는 보여주지 않습니다. 우리가 1을 검색했기 때문에 1이 여기에 들어간다고 이해할 수 있습니다.
=== findAll; 전체 데이터 조회 === 전체 데이터 조회는 findAll 메서드를 활용합니다. 정상적으로 모든 데이터가 출력된 것을 확인할 수 있습니다.
=== save; 데이터 수정 === ** save 는 데이터 추가와 수정 기능을 같이 수행합니다. 존재하면 update 존재하지 않으면 insert 가 실행됩니다. 존재한다의 기준은 Primary Key 값을 기준으로 동작하게 됩니다.
Primary Key = 1 은 있기 때문에 text 의 속성 값이 변경된 것을 확인할 수 있습니다. 그리고 Hibernate 의 출력문이 2개인 것을 볼 수 있습니다. 이런 일이 벌이진 이유는 해당 테이블의 내용이 실제로 있는지 먼저 살펴본 다음 업데이트할지 생성할지 결정하기 때문에 검색하는 Hibernate 쿼리가 우선 동작한 것입니다.
=== deleteById; 데이터 하나 삭제 ===
Primary Key = 1 에 해당하는 데이터가 삭제된 것을 볼 수 있습니다. 단, 여기서 해당 쿼리를 한 번 더 실행시키면 Primary Key = 1 에 해당하는 데이터가 없기 때문에 오류가 발생하게 됩니다.
우선 게시판이 작성될 때 사용자가 직접 입력해야 하는 정보는 글의 제목과 글의 내용이다. 나머지는 서비스 제공자가 기본적으로 데이터를 넣어주어야 한다. 우선 위의 코드에서 @CreatedDate 는 한 번 값이 생성되면 변경이 불가능하고 @LastModifiedDate 는 계속 값이 수정 가능하다. @DateTimeFormat 은 시간의 포멧을 정해준다. 즉 초까지? 분까지? 시간까지 어떤 형태로 시간을 저장할 것인지 저장한다. LocalDateTime 클래스 자료형을 사용하면 된다.
Main 메서드에 @EnableJpaAuditing 을 추가해주어야 정상적으로 코드가 동작하게 된다.
그 다음 Repository 를 생성했다. 프레임워크의 특징이 잘 드러나는데 바깥으로는 잘 보이지 않지만 내부에서는 많은 작업이 자동화되어 처음 입문하는 개발자에게는 쉽게 접근하기가 어려운 면이 있다. 아무튼 이번에는 테스트 코드를 작성해보자.
=== 테스트코드 ===
package com.example.demo.repository;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import com.example.demo.entity.Board;
@SpringBootTest
public class BoardRepositoryTest {
@Autowired
BoardRepository boardRepository;
@Test
public void 데이터등록() {
Board board1 = new Board();
board1.setTitle("1번글");
board1.setContent("내용입니다");
boardRepository.save(board1);
Board board2 = new Board(0, "2번글", "내용입니다", null, null);
boardRepository.save(board2);
}
@Test
public void 데이터단건조회() {
Optional<Board> result = boardRepository.findById(1);
if(result.isPresent()) {
Board board = result.get();
System.out.println(board);
}
}
@Test
public void 데이터전체조회() {
List<Board> list = boardRepository.findAll();
for(Board board: list) {
System.out.println(board);
}
}
@Test
public void 데이터수정() {
Optional<Board> result = boardRepository.findById(1);
Board board = result.get();
board.setContent("내용이수정되었습니다");
boardRepository.save(board);
}
@Test
public void 데이터삭제() {
boardRepository.deleteById(1);
}
@Test
public void 데이터전체삭제() {
boardRepository.deleteAll();
}
}
이제 하나씩 살펴보겠습니다. 데이터 등록 코드를 진행하면 다음과 같은 테이블이 생성됩니다.