자바 진영의 ORM (Object-Relational Mapping)
JPA : 객체와 관계형 데이터베이스의 테이블을 짝지어서 데이터를 영구히 보관하기 위해 Java 진영에서 정해진 규칙
규칙일 뿐! (REST API 등과 같이..)
JPA 를 구현하고 있는 건 Hibernate(구현체)이다. Hibernate는 내부적으로 Jdbc를 사용한다.

@Entity : 스프링이 user 객체와 user 테이블을 같은 것으로 바라보도록 함
엔티티: 저장되고 관리되어야 하는 데이터를 의미함
com.group.libraryapp.domain.user 위치에 있던 User 클래스를 다시 가져와서, 클래스 위에 @Entity 어노테이션을 붙이자.

PK를 생성하고 매핑하는 방법을 알아보자. 아래와 같이 id값을 PK로 선언하고, 자동생성(auto increment) 되도록 설정할 수 있다.
@Id // 이 필드를 PK로 간주한다는 의미
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동생성되는 값임을 의미
private Long id = null;
이 때, @Entity 객체에는 반드시 기본 생성자가 있어야 하므로 protected 값으로 하나 생성해주도록 하자.
protected User() {}
또한,PK가 아닌 다른 필드들에서 객체의 필드와 테이블의 필드를 매핑하기 위해서는 @Column 라는 어노테이션을 사용해야 한다.
@Column 어노테이션에서는 주로 null 여부, 길이 제한, 데이터베이스에서의 컬럼 이름 등등의 정보들이 들어간다.
이 때, 데이터베이스에서의 컬럼 이름과 객체에서의 컬럼 이름이 name= "name”과 같이 동일한 경우는 컬럼이름을 따로 기재해주지 않아도 괜찮다.
@Column(nullable = false, length = 20)
private String name;
private Integer age;
추가로, application.yml 파일에 설정을 해줘야 함
spring:
datasource:
url: "jdbc:mysql://localhost/library"
username: "root"
password: "rlaalswl"
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: none // 스프링이 시작할 때 테이블과 객체의 정보가 다를 경우 어떻게 처리할지를 기재하는 옵션
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
ddl-auto : 스프링이 시작할 때 테이블과 객체의 정보가 다를 경우 어떻게 처리할지를 기재하는 옵션create : 기존 테이블이 있다면 삭제 후 다시 생성 -> 서버를 시작할 때마다 테이블이 모두 삭제되고 자연히 테이블 안의 정보값들도 사라짐create-drop : 스프링이 종료될 때 테이블을 모두 제거 -> 서버를 시작할 때마다 테이블이 모두 삭제되고 자연히 테이블 안의 정보값들도 사라짐update: 객체와 테이블이 다른 부분만 변경validate : 객체와 테이블이 동일한지 확인none : 별다른 조치를 하지 않음show_sql : jpa를 사용해 데이터베이스에 sql을 날릴 때 sql문을 보여줄 것인가
format_sql : sql문을 보여줄 때 formatting해서 깔끔하게 보여줄 것인가dialect : 이 옵션으로 데이터베이스를 특정해주면 자동으로 조금씩 sql을 다르게 수정해줌. (데이터베이스 서비스, 버전 등을 명시하면 됨)UserRepository는 UserJdbcRepository로 수정하기User 옆에 UserRepository 인터페이스 생성해주기UserRepository 인터페이스로 JPA 레포지토리 상속 받기JpaRepository<엔티티 객체, PK>UserService 의 이름을 UserServiceV1으로 바꾸기: JDBC를 사용하는 코드를 보존하기 위해 여러 버전으로 나눠서 생성하기 위한 작업UserServiceV2 추가로 생성하기save : 주어지는 객체를 저장하거나 업데이트 시킴findAll : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져옴findById : id를 기준으로 특정한 1개의 데이터 가져오기Spring Data JPA : 복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리이다.SimpleJpaRepository 를 검색하면 복잡한 코드들을 확인할 수 있다. findById만 지원하는데, 나는 이름을 기준으로 검색해야 한다면?UserRepository 인터페이스에 가서 아래와 같이 설정해줘야 한다. package com.group.libraryapp.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
// @Repository 어노테이션 없이도 JpaRepository 를 상속한 것 만으로도 스프링빈으로 관리됨
// 함수 만들기: 이 때 함수의 이름이 아주 중요함 (함수명을 따라서 쿼리가 작성됨)
User findByName(String name);
}
findByName는 아무렇게나 지은 이름이 아니다. find 를 사용해서 하나의 결과만이 반환되었고, by 뒤에 오는게 Name 이므로 이름을 기준으로 찾아서 반환하게 된다. package com.group.libraryapp.service.user;
import com.group.libraryapp.domain.user.User;
import com.group.libraryapp.domain.user.UserRepository;
import com.group.libraryapp.dto.user.request.UserCreateRequest;
import com.group.libraryapp.dto.user.request.UserUpdateRequest;
import com.group.libraryapp.dto.user.response.UserResponse;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserServiceV2 {
private final UserRepository userRepository;
public UserServiceV2(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 유저 저장 기능: save 메소드에 객체를 넣어주면 INSERT sql이 자동으로 요청됨
public void createUser(UserCreateRequest request) {
User u = userRepository.save(new User(request.getName(), request.getAge()));
// u.getId(); // 자동 생성 된 아이디 값을 가져올 수 있음!
}
// 유저 조회 기능
public List<UserResponse> getUsers() {
return userRepository.findAll().stream()
.map(user -> new UserResponse(user.getId(), user.getName(), user.getAge())) // .map(UserResponse::new) 로 대체 가능
.collect(Collectors.toList());
}
// 유저 업데이트 기능
public void updateUser(UserUpdateRequest request) {
// 1. id를 이용해 user를 가져와서 해당 user가 데이터베이스에 존재하는지 하지 않는지 확인
User user = userRepository.findById(request.getId())
.orElseThrow(); // IllegalAccessException::new 왜 오류나지..?
// 2. user가 있다면 update 쿼리를 날려서 데이터를 수정
user.updateName(request.getName());
userRepository.save(user);
}
// 유저 삭제 기능
public void deleteUser(String name) {
// 사용자 이름으로 유저 가져오기
User user = userRepository.findByName(name);
if (user != null) {
userRepository.delete(user);
} else {
throw new IllegalArgumentException("User not found");
}
}
}
@RestController
public class UserController {
private final UserServiceV2 userService;
public UserController(UserServiceV2 userService) { // 생성자 추가
this.userService = userService;
}
By 앞에 들어갈 수 있는 기능
find : 1건의 데이터를 반환함. 반환타입은 객체 or Optional<타입>findAll : N건의 데이터를 반환함. 반환타입은 List<타입>exists : 쿼리 결과가 존재하는지 확인. 반환타입은 boolean각 구절은 And 나 Or 등을 활용하여 자유롭게 조합할 수 있음
List<User> FindAllByNameAndAge(String name, int age)
SELECT * FROM user WHERE name = ? AND age = ?;
GreaterThan (초과), GreaterThanEqual (이상), LessThan (미만), LessThanEqual (이하), Between (사이에), StartsWith (~로 시작하는), EndsWith (~로 끝나는)List<User> FindAllByAgeBetween(int startAge, int endAge)
SELECT * FROM user WHERE age BETWEEN ? AND ?;
트랜잭션이란?: 쪼갤 수 없는 업무의 최소단위
트랜젝션 명령어
start transaction;: 트랜젝션 시작commit : 트랜젝션 시작 후 한 몸인 SQL들을 한번에 반영하는 정상 종료rollback : 실패처리 (SQL에 반영되지 않음)트랙젝션은 시작한 후, commit (성공했으며 이제 SQL들을 한번에 반영해도 된다는 의미)이나 rollback (실패했으며 직전 commit으로 돌아가야 한다는 의미)을 입력하기 전까지는 작성한 쿼리의 결과가 다른 창에서 보이지 않는다. 아래의 사진에서도 볼 수 있듯이, 왼쪽 창에서 start transaction 이후 입력한 insert into user (name, age) values ('minji', 23);는 오른쪽 창에 반영되어있지 않다. 아직 commit 을 하지 않았기 때문이다.

이후 한 줄의 데이터를 더 추가한 후 commit 을 해주면 아래와 같이 오른쪽 창에서도 변경사항이 반영되어서 보인다.

rollback으로 실패처리를 하면 아래와 같이 오른쪽 창에는 transaction 내에서 변경된 내용들이 보이지 않는다.

Service 클래스에 트랜젝션 적용하기Service 메소드가 시작될 때 트랜젝션이 시작됨Service 메소드 로직이 모두 정상적으로 실행되면 commit 됨Service 메소드 로직 실행 도중 하나라도 문제가 생기면 rollback 됨@Transactional 어노테이션을 메소드 위에 붙여주면, 해당 메소드가 시작될 때 start transaction;으로 트랜젝션이 시작되고, 메소드가 종료될 때 commit이나 rollback으로 종료시켜줌.

이 때, 만약 SELECT 쿼리만 사용하는 경우에는 @Transactional(readOnly = true)와 같이 설정해줄 수 있다.
트랜젝션을 테스트해보기 위해 아래와 같이 오류를 throw 해준 후 유저 저장이 되는지 안되는지 살펴보자. 정석대로라면 오류가 발생했으므로 롤백되어서 저장되지 않아야 한다.
// 유저 저장 기능: save 메소드에 객체를 넣어주면 INSERT sql이 자동으로 요청됨
@Transactional
public void createUser(UserCreateRequest request) {
User u = userRepository.save(new User(request.getName(), request.getAge()));
throw new IllegalArgumentException(); // 저장은 됐는데 오류가 발생했으니 저장로직까지 전부 롤백되어서 저장이 안 될 것임.
}


IOException, SQLException, FileNotFoundExceptionpublic void readFile(String fileName) throws IOException {
FileReader reader = new FileReader(fileName);
}
→ IOException은 Checked Exception이기 때문에, 반드시 try-catch로 처리하거나 throws로 넘겨야 함.
NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentExceptionpublic void divide(int a, int b) {
int result = a / b; // b가 0일 경우 ArithmeticException 발생
}
→ ArithmeticException은 Unchecked Exception이라 try-catch 없이도 컴파일 가능.
Entity 객체(예제에서는 User 객체)를 관리/보관하는 역할 // 유저 업데이트 기능
@Transactional
public void updateUser(UserUpdateRequest request) {
// 1. id를 이용해 user를 가져와서 해당 user가 데이터베이스에 존재하는지 하지 않는지 확인
User user = userRepository.findById(request.getId())
.orElseThrow(); // IllegalAccessException::new 왜 오류나지..?
// 2. user가 있다면 update 쿼리를 날려서 데이터를 수정
user.updateName(request.getName());
// userRepository.save(user); // 이런 저장이 불필요함!
// 자동으로 유저의 변경을 감지함.
}
쓰기 지연: DB에 INSERT, UPDATE, DELETE sql을 바로 날리는 것이 아니라, 트랜젝션이 commit될 때 모아서 한번만 날린다.
⮕ 스프링과 데이터베이스간의 통신횟수를 줄여줌
1차 캐싱: ID를 기준으로 Entity를 기억함
⮕ 영속성 컨텍스트가 ID를 기준으로 Entity를 기억해뒀다가, 알고 있는 Entity 중 필요한 ID에 부합하는 정보가 있다면 바로바로 넘겨줌
⮕ 이렇게 캐싱된 객체는 객체의 주소까지 해서 완전히 동일함!