[SpringBoot] Chap 4 - 도서 관리 서비스 JPA 사용 및 트랜잭션

01_JPA(Java Persistence API)


📍JPA(Java Persistence API)

객체와 관계형 데이터베이스의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙

JPA를 실제 코드로 작성한 가장 유명한 프레임워크가 Hibernate

Hibernate는 내부적으로 JDBC를 사용한다.

 

src/main/resources/application.yml

spring:
  jpa:
    hibernate:
      ddl-auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

🔍 코드 설명

  • spring.jpa.hibernate.ddl-auto : 스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지에 대한 옵션
    • create : 기존 테이블이 있다면 삭제 후 다시 생성한다.
    • create-drop : 스프링이 종료될 때 테이블을 삭제한다.
    • update : 객체와 테이블이 다른 부분만 변경한다.
    • validate : 객체와 테이블이 동일한지 확인한다.
    • none : 별다른 조치를 하지 않는다.
  • spring.jpa.properties.hibernate.show_sql :JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄지 결정
  • spring.jpa.properties.hibernate.format_sql :JPA를 사용해 DB에 SQL을 날릴 때 SQL을 예쁘게 포맷팅할지 결정
  • spring.jpa.properties.hibernate.dialect :JPA가 알아서 Database끼리 다른 SQL을 조금씩 수정

 

 

@Entity

이 클래스가 DB에 저장되는 객체라고 JPA에게 알려준다.

JPA에 의해 테이블과 매핑된 객체는 파라미터를 가지지 않은 기본 생성자가 꼭 필요하다.

 

domain/user/User

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;
    
    @Column(nullable = false, length = 20)
    private String name;
    private Integer age;
    
    protected User() { // 기본 생성자 추가
    }
    
    (중략)
}

🔍 코드 설명

  • @Id : 이 필드를 primary key로 간주한다.
  • @GeneratedValue : primary key는 DB에서 자동 생성해 주기 때문에 이 어노테이션을 붙여야 한다.
  • @Column : 이 필드가 DB 테이블의 컬럼과 연결된다는 것을 알려준다.
  • 기본 생성자 : JPA에 의해 테이블과 매핑된 객체는 기본 생성자가 반드시 필요

 

 

 

02_Spring Data JPA


📍POST API 수정

domain/user/UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
}

 

service/user/UserServiceV2

@Service
public class UserServiceV2 {
    private final UserRepository userRepository;

    public UserServiceV2(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void saveUser(UserCreateRequest request) {
        User u = userRepository.save(new User(request.getName(), request.getAge()));
    }
}

🔍 코드 설명

  • save : 주어지는 객체를 저장하거나 업데이트해준다. JpaRepository에 내장되어 있는 기능. 

 

 

 

📍GET API 수정

service/user/UserServiceV2

@Service
public class UserServiceV2 {
    (중략)
    
    public List<UserResponse> getUsers() {
        return userRepository.findAll().stream()
                .map(UserResponse::new)
                .collect(Collectors.toList());
    }
}

🔍 코드 설명

  • findAll : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져온다.

 

 

 

📍PUT API 수정

service/user/UserServiceV2

@Service
public class UserServiceV2 {
    (중략)
    
    public void updateUser(UserUpdateRequest request) {
        // select * from user where id = ?
        User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalAccessError::new);
        user.updateName(request.getName());
        userRepository.save(user);
    }
}

🔍 코드 설명

  • findById : id를 기준으로 특정한 1개의 데이터를 가져온다. 이때 Java 라이브러리의 Optional 이 반환되는데, orElseThrow 를 사용하면 User가 비어있는 경우 에러를 던지게 된다.

 

 

 

📍DELETE API 수정

이름을 기준으로 User 정보 확인하기 위해 repository에 findByName 메소드 시그니처를 작성해야 한다.

 

domain/user/UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
    User findByName(String name);
}

🔍 코드 설명

  • findByName : 함수 이름만 작성하면, 알아서 SQL이 조립된다. 
    • find라고 작성하게 되면, 1개의 데이터를 가져온다.
    • By 뒤에 붙는 필드 이름으로 SELECT 쿼리의 WHERE 문이 작성된다.

 

service/user/UserServiceV2

@Service
public class UserServiceV2 {
    (중략)
    
    public void deleteUser(String name){
        // select * from user where name = ?
        User user = userRepository.findByName(name).orElseThrow(IllegalArgumentException::new);
        userRepository.delete(user);
    }
}

🔍 코드 설명

  • delete : 주어지는 객체를 삭제한다.

 

 

 

 

03_트랜잭션(Transaction)


📍트랜잭션(Transaction)

쪼갤 수 없는 업무의 최소 단위

여러 SQL을 사용해야 할 때 한 번에 성공시키거나 하나라도 실패하면 모두 실패시키는 기능

 

1. 서비스 메소드가 시작할 때 트랜잭션이 시작되어

START TRANSACTION;

 

2-1. 서비스 메소드 로직이 모두 정상적으로 성공하면 commit 되고

 

COMMIT;

 

2-2. 서비스 메소드 로직 실행 도중 문제가 생기면 rollback

 

ROLLBACK;

 

 

 

 

트랜잭션을 사용하면 영속성 컨텍스트가 생겨난다.

1. 변경 감지(Dirty Check) :영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save 해주지 않아도 알아서 변경 감지하여 저장

    @Transactional
    public void updateUser(UserUpdateRequest request) {
        User user = userRepository.findById(request.getId())
                .orElseThrow(IllegalAccessError::new);

        user.updateName(request.getName());
//      userRepository.save(user); // 생략 가능
    }

2. 쓰기 지연 : 트랜잭션이 commit 되는 시점에 SQL을 모아서 한 번만 날린다.

3. 1차 캐싱 : ID 기준으로 Entity를 기억한다.

 

 

 

📍트랜잭션 적용하기

대상 메소드에 @Transactional 어노테이션을 붙여주면 된다.

Unchecked Exception에 대해서만 롤백이 일어난다.

 

service/user/UserServiceV2

@Service
public class UserServiceV2 {
    (중략)

    @Transactional
    public void saveUser(UserCreateRequest request) {
        (생략)
    }

    @Transactional(readOnly = true)
    public List<UserResponse> getUsers() {
        (생략)
    }

    @Transactional
    public void updateUser(UserUpdateRequest request) {
        (생략)
    }

    @Transactional(readOnly = true)
    public void deleteUser(String name){
       (생략)
    }
}

🔍 코드 설명

  • @Transaction : 함수가 시작될 때 start transaction; 을 해준다.
    • 함수가 예외 없이 잘 끝났다면 commit / 혹시라도 문제가 있다면 rollback
    • readOnly : 데이터의 변경이 없고 조회 기능만 있을 때 true