바닥부터 알아보는 Spring Data JPA with MySQL
서론
새로운 프로젝트를 시작하며, 데이터베이스와 백엔드 시스템을 연결하기 위해 Spring Data JPA를 사용할 일이 생겼다.나는 몇달 전에 SpringBoot를 기반으로 한 웹 서비스 프로젝트에 참여하며, 데이터베이스 접근 체계를 만든 적이 있지만, 그 당시에는 Spring Data JPA가 아니라 MyBatis라는 프레임워크를 배워 사용했었다. 그렇다보니 JPA가 뭔데? MyBatis랑 많이 다른가? 싶은 많은 의문과 무지(?)를 가지게 될 수밖에 없었다.
학습에 도움이 될만한 자료를 찾아보다가, Spring 공식 사이트에서 제공하는 간단한 Spring Data JPA with MySQL 튜토리얼을 발견하게 되었다. 튜토리얼을 간단하게 따라하는 것만으로도 Spring Data JPA가 적용된 초간단 서버를 구현할 수 있어, Spring Data JPA의 첫걸음을 떼는데에 큰 도움이 되는 것 같았다. 튜토리얼 원문 링크 바로가기
사실, 이번에 새롭게 공부를 해 보기 전에는, JPA나 ORM, Hibenate같은 것에 대한 개념조차 잘 몰랐었다. 그냥 쓰면 돌아가니까 일단 쓴다는 느낌에 가깝게 프로젝트를 진행했던지라, 그렇게 되어버렸던 것 같다... 그래서 이번 글에서는 여러 프레임워크/기술 등의 개념과, 이 Spring Data JPA를 이용해 MySQL을 서버에서 사용하는 방법에 대해 기록해보고자 한다.
[주의] 이 게시글은 전문성이 부족한 초보 개발자가 작성한 단순 기록용 글이기 때문에, 모든 내용은 부정확할 가능성이 있다. 그럴 가능성이 있는 수준이 아니다. 아주 높다... 신빙성있는 정보를 얻고 싶다면 각종 공식 사이트의 문서나 해설을 참고하는 것이 좋다. (Spring Data JPA reference page)
"Spring Data JPA" 라는 건 정확히 뭘까?
현재(2024년) 많은 프로그램이나 강의, 책에서 Spring Data JPA를 이용해 서버에서 데이터베이스를 접근하는 매커니즘을 구축하고 있는 것으로... 알고 있다. 그렇다면 이 JPA라는 게 정확히 뭔지를 알아야 잘 활용할 수 있지 않을까 하는 생각이 들어, 각종 개념에 대해 간단하게 알아보았다.
생각해보면, 우리가 서버를 구현하기 위해 사용하는 Java와 데이터베이스에는 많은 차이가 있다. 때문에 둘 사이에 데이터를 주고 받는 등의 상호작용을 하기 위해서는 어느정도의 '기술'이 필요한데, 그런 기술들을 통틀어서 ORM(Object Relational Mapper)이라고 한다. 이 ORM 소개문에서는, ORM이 데이터베이스와 객체지향 프로그램간의 연결다리라고 소개하고 있다. 데이터베이스와 객체지향 프로그램은 각각의 고유한 데이터 취급 방식이 있기 때문에, 그 차이를 메꾸어줄 수단이 필요한 것이다.
이러한 ORM 기술의 Java 표준이 JPA(Java Persistence API)이다. 다만 구현된 프로그램은 아니고 인터페이스이기 때문에, JPA를 구현한 여러 프레임워크(Hibernate 등) 중 하나를 사용해야 실사용이 가능하다. 정리하자면, 객체지향 프로그램에서 데이터베이스에 접근/수정 등을 수행하는 기술을 ORM이라고 하며, 그것의 Java 표준 인터페이스가 JPA, JPA를 구현한 프레임워크에는 Hibernate 등이 있다.
그렇다면 저 많은 개념들 중 Spring Data JPA는 어디에 끼워질까? Spring Data JPA는 JPA를 구현한 구현체(혹은 JPA 제공자)를 이용하는 추상체(abstraction)이다. JPA 구현체나 제공자가 아니다! 이 글에서는, Spring Data JPA는 JPA 구현체 상단에 씌워지는 추상체 레이어라고 설명하고 있다. 즉, 우리는 Spring Data JPA를 통해 JPA 구현체를 사용하는 셈이다. 실제로, Spring Data JPA를 이용해 실행하는 쿼리의 내용을 출력하는 옵션을 true로 설정하면, 프로그램 작동 중 로그를 통해 Hibernate라는 문구가 종종 표기되는 것을 볼 수 있다.
굳이 Spring Data JPA를 사용하는 이유
MyBatis를 이용해 스프링 백엔드 서버를 구현해본 사람은 한 번쯤 경험해봤을텐데, 데이터베이스 접근을 하나하나 구현하다보면 단순 CRUD와 관련된 내용을 계속해서 작성하게 된다. 테이블이 달라졌다는 이유 하나로 똑같은 쿼리를 계속해서 작성하고, 반복되는 내용이니까 기존 코드를 복사해서 쓰려고 하니, 변수 한두개정도를 바꾸지 않은 탓에 에러가 발생하기도 한다.
이렇게 거의 변경하지 않거나 전혀 변경하지 않고 반복적으로 사용되는 코드를 boilerplate code라고 한다.(참고) 위의 내 경험담은 불필요한 단순작업과 실수, 오류를 야기하고 있으므로, boilerpolate code의 단점이 드러나고 있는 예시라고 볼 수 있겠다. Spring Data JPA는 이런 boilerplate code가 반복되는 것을 막기 위해, repository라는 추상를 주요 개념으로 내세우고 있다.
서론에서 소개한 튜토리얼에서는, Repository를 상속한 CrudRepository라는 interface를 사용해, CRUD와 관련된 모든 쿼리를 자동으로 제작해주는 기능을 사용하고 있다. 단순 CRUD를 구현하기 위해 시간을 쏟아부을 필요가 없어지는 것이다! 또, MyBatis와 다르게, Spring Data JPA는 쿼리문을 직접 작성하지 않고 메서드를 이용해 쿼리를 지정, 실행시킬 수 있다는 특징이 있다. 이 외에도 Spring Data JPA에는 많은 특징, 혹은 장단점이 존재하나, 우선은 공식 튜토리얼을 따라 실습하는 단계로 넘어가보자.
SpringBoot로 Spring Data JPA with MySQL 써 보기
서론에서도 소개했지만, 이 튜토리얼에서는 Spring Data JPA를 이용해 MySQL DB와 상호작용하는 법에 대해서 다루고 있다. 이 문서를 따라 차근차근 코드를 구현해보면서, Spring Data JPA의 실 사용법에 대해 알아보자.
환경
이 글에서는 다음과 같은 것들을 기반으로 실습을 진행했다.
- windows 10
- SpringBoot
- OpenJDK 17 - Liberica
- Gradle - Groovy
- MySQL Community Server LTS 8.4.1
- MySQl Workbench 8.0.38
- IntelliJ IDEA community
- Postman
Spring Project 생성
가장 첫 단계로, SpringBoot를 기반으로 한 Spring 프로젝트를 생성해야 한다. 공식 튜토리얼에서는 https://start.spring.io/ 를 이용해 프로젝트 파일을 생성할 것을 권유하고 있다. 의존성 관리 툴이나 언어, 버전 등은 자유롭게 해도 좋으나, 의존성(Dependencies) 설정은 잘 맞춰줘야 한다. 다음과 같은 의존성을 추가하자 : Spring Web, Spring Data JPA, MySQL Driver
튜토리얼 원문에서는 Docker Compose Support와 Testcontainers도 추가하여 사용했으나, 이 두 가지는 MySQL을 docker에 띄워 테스트하기 위한 의존성이다. docker를 이용하려면 docker를 설치해야 하기도 하고, 프로그램을 통해 변동된 DB의 내용을 로컬에서 바로 확인할 수 없다는 번거로움이 있어, 이 글에서는 로컬에 MySQL을 직접 설치하여 사용하는 것으로 했다.
참고 : MySQL 설치
나의 경우 MySQL Community Server와 MySQL Workbench를 각각 설치하여 사용했다. Workbench는 사용 편의를 위해 설치한 것으로, Server만 설치해도 상관없다.
프로젝트 열기
위처럼 설정한 후 프로젝트 파일을 생성하면, 압축 파일이 받아질 것이다. 압축해제한 후, 인텔리제이를 이용해 열어보면, 내부 구조는 다음과 같이 되어있다.
이들 중 JpaMysqlApplication이 실제 Spring 응용 프로그램을 실행할 때 main이 되는 부분이고, application.properties는 SpringBoot의 실행환경에 관련된 설정을 하는 부분, build.gradle은 프로젝트의 의존성에 대해 기록하는 부분이 되겠다. 이들 중 JpaMysqlApplication과 build.gradle은 이미 설정이 완료되어있다. Spring Initializer가 설정해준 덕분이다!
간단하게 build.gradle을 보면, 아까 추가했던 의존성들과 관련된 설정이 적혀있는 것을 볼 수 있다.
IntelliJ나 Gradle 환경에서 처음 프로젝트를 실행해보는 사람을 위해, 여러 참고사항을 작성했다. 필요한 사람만 보면 되는 부분이니, 접은 글로 작성한다.
참고 : IntelliJ community에서 SpringBoot 실행하기
무료 버전 인텔리제이는 정말 치사하게도, SpringBoot 프로젝트를 기본 지원해주지 않는다. 즉, Spring 프로젝트를 불러온다 하더라도, IntelliJ는 어디를 main으로 잡고 실행을 해야할지 알지 못한다는 것이다. (아는데 돈 안 냈다고 이 악물고 외면하는 걸테지만 말이다.) 때문에 어디를 main으로 잡고 실행해야할지 설정해줄 필요가 있다.

우상단에 'Current File'이라는 버튼이 띄워져 있을텐데, 그 옆에 있는 작은 꺾쇠모양 화살표를 누르면 'Edit Configurations' 항목을 발견할 수 있다. 이걸 눌러보자. 이후 오른쪽 영역의 'add new run configuration'을 클릭한 후, 'Application'을 선택하자. 그러면 다음과 같이 상세 설정을 할 수 있는 페이지가 떠오르게 된다.

여기에서 module은 사용하고 있는 JDK를 선택해주도록 하고, '-cp'에서는 main을 골라주도록 하자. 마지막으로 main class는, Spring에서 기본적으로 생성해주는 Application.java 파일을 선택해주면 된다. 이 글에서 진행하고 있는 실습에서는 JpaMysqlApplication이라는 이름으로 존재하고 있다.

이후 apply를 눌러주고, 아까의 우상단을 보면 아까와는 달리 실행 가능한 applicaiton이 떠올라 있다.

참고 : Gradle Build and Run 설정
위 설정을 마치고 실행해보려고 하면, 다음과 같은 에러가 나면서 컴파일에 실패할 수 있다.

뭔가 Depricated한 기능을 사용하고 있어, Gradle 9.0과 호환되지 않는 상태라는 의미... 인 것 같다. 이 문제는 프로젝트의 Gradle 설정을 통해서 해결할 수 있다.
우상단의 톱니바퀴를 눌러 setting에 접속하자. 이후 'Build, Execution, Deployment' 항목의 'Build Tools' -> 'Gradle'을 들어가보면, Build and Run에 사용할 Gradle을 설정하는 부분이 있다. 이 부분이 아마 Gradle(default)로 되어있을텐데, 이것을 IntelliJ IDEA로 변경해준다.

이 모든 설정이 끝난 후에 실행을 해 보면...

알 수 없는 에러 메시지를 잔뜩 뱉으며 종료되지만, 아무튼 Spring이 실행되기는 한다!
MySQL 데이터베이스와 관련된 설정하기
application.properties에 접속할 MySQL 데이터베이스와 관련된 정보를 입력하자. 여기에서는 test라는 데이터베이스가 로컬 MySQL Server에 생성한 후, 그와 관련된 정보를 입력해주었다.
spring.application.name=jpa-mysql
server.port=80
server.servlet.context-path=/jpa
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/{DB명}?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username={유저명}
spring.datasource.password={비밀번호}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql=true
spring.jpa.defer-datasource-initialization=true
spring.datasource~ 부분이 DB에 접근하기 위한 설정 부분이라고 봐 주면 된다. 상단의 server. 관련 설정은, 지금 만들고 있는 서버의 포트와, 주소에 추가로 붙는 루트를 설정해준 부분이다.
@Entity 모델 생성하기
데이터베이스를 사용하기 위해서는, 어떤 데이터를 저장하고, 혹은 주고받을지를 표기할 필요가 있다. 데이터베이스를 이용한 간단한 실습을 해보았다면, 데이터베이스를 사용하기 위해서는 가장 먼저 데이터베이스, 그리고 테이블을 생성해야한다는 사실을 알 것이다.
이것을 @Entity라는 annotation을 포함한 클래스를 생성하는 것으로 간단하게 표현할 수 있다. Entity란, 관계형 데이터베이스의 테이블을 객체지향 프로그램에서 표현한 것이다. (출처) 각 엔티티 인스턴스는 테이블의 행을 표현한다고 한다. 우리는 Entity를 생성함으로써 테이블을 생성할 수 있는 것이다. 튜토리얼에서는 다음과 같은 간단한 형식의 Entity를 생성하고 있다.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy= GenerationType.AUTO)
private Integer id;
private String name;
private String email;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
// ... (이하로 쭉 getter와 setter)
}
클래스 선언부 위에는 @Entity, Primary Key가 될 멤버 변수에는 @Id, @GeneratedValue가 달려있는 것을 볼 수 있다. @Id는 해당 멤버 변수가 Primary Key가 됨을 나타내며, @GeneratedValue는 해당 Primary Key를 자동 생성할 때 사용할 전략을 설정하는 어노테이션이다. 각 어노테이션의 상세는 jakarta.persistence의 annotation interface 항목을 참고하면 자세히 알 수 있다. (docs 링크 바로가기)
아무튼 위와 같이 User.java를 구성하는 것만으로도 사용할 테이블을 간편하게 설정할 수 있다. 공식 튜토리얼에서는, Hibernate가 이 Entity를 자동으로 해석하여 테이블로 변환할 것이라고 말하고 있다.
Repository 만들기
Entity를 생성했다면, 그 다음으로 해야할 일은 Repository를 생성하는 것이다. Repository는 Spring Data JPA에서 제공하는 인터페이스로, 데이터베이스를 접근하기 용이하게 만들어준다.
예시에서는 CrudRepository라는 인터페이스를 상속받아, UserRepository를 구현하고 있다.
import com.example.jpa_mysql.entity.User;
import org.springframework.data.repository.CrudRepository;
// 기존의 인터페이스인 CrudRepository를 상속하는 것만으로도, Spring 알아서 인터페이스를 빈으로 구현해준다.
// 참고 : CrudRepository<T, ID>의 T, ID는 각각 도메인(엔티티) 타입과, 도메인 id의 타입을 나타냄
public interface UserRepository extends CrudRepository<User, Integer> {
}
CrudRepository가 선언하고 있는 메서드 리스트는 이 docs에서 확인할 수 있다. 언뜻 보았을때 알 수 있지만, 각종 기본적인 CRUD가 선언되어 있다.
여기까지 만들었다면, 데이터베이스에 접근할 모든 준비를 마쳤다고 볼 수 있다!
테스트를 위한 RestController 생성
그러면 이제 실제로 데이터베이스를 활용해보자. 개인적으로 REST 형식의 controller가 편해 @RestController 어노테이션을 이용해 작업을 진행하였다. 성공적으로 쿼리를 실행한 경우 200 OK를 반환하도록 하였고, 그렇지 않은 경우 500 INTERNAL SERVER ERROR를 반환하도록 하였다. 새 entity를 삽입하는 save(), 모든 entity를 조회하는 findAll(), 그리고모든 entity를 삭제하는 deleteAll()을 활용하도록 구성하였다.
import com.example.jpa_mysql.entity.User;
import com.example.jpa_mysql.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/")
@CrossOrigin("*")
public class UserController {
private UserRepository userRepository;
// 생성자를 통한 의존성 주입 시행
public UserController(UserRepository userRepository){
this.userRepository = userRepository;
}
@PostMapping("/add")
public ResponseEntity<?> addUser(@RequestBody Map<String, String> request) {
// id는 자동으로 생성되므로 따로 insert하지 않음
User user = new User();
user.setName(request.get("name"));
user.setEmail(request.get("email"));
try {
userRepository.save(user);
} catch(Exception e){
return ResponseEntity.internalServerError().build();
}
return ResponseEntity.ok().build();
}
@GetMapping("/all")
public ResponseEntity<?> listUser() {
// 유저 목록 전체를 불러온다
Iterable<User> userList = new ArrayList<>();
Map<String, Object> body = new HashMap<>();
try {
userList = userRepository.findAll();
body.put("userList", userList);
return ResponseEntity.status(200).body(body);
} catch(Exception e){
return ResponseEntity.internalServerError().build();
}
}
@DeleteMapping("/clear")
public ResponseEntity<?> clearUser() {
// 유저 목록 전체를 지운다
try {
userRepository.deleteAll();
return ResponseEntity.status(200).build();
} catch(Exception e){
return ResponseEntity.internalServerError().build();
}
}
}
기능 테스트
이제 Postman을 이용해 직접 RestController와 통신해보자. Postman에 적절한 주소와 포트를 입력한 후, 입력이 필요한 경우 body - raw data - JSON 형식으로 입력해보자. 예시로, addUser를 이용하기 위한 리퀘스트를 첨부한다.
적당한 테스트 데이터를 2회 삽입한 후, 전체 유저를 조회해보면, 서버 로그에 다음과 같이 사용한 쿼리의 리스트가 떠오른다.
API를 이용해 전체 유저를 조회해보거나, DB를 직접 확인해보면 데이터가 잘 삽입되는 것을 확인할 수 있다.
마치며
이렇게 아아아주 간단하게 JPA와 관련된 개념과, Spring Data JPA를 활용한 실습을 진행해보았다. 공부하면서 진행해보니 생소한 개념이 많았어서, 앞으로도 많은 공부가 필요할 것 같다는 느낌이 팍팍 온다.
앞으로도 힘내자. 화이팅!