[Spring Boot] 섹션5 JPA

정수현·2025년 4월 15일

캡스톤

목록 보기
6/8

2025-04-14

학습 목표

  1. 문자열 SQL을 직접 사용하는 것의 한계를 이해하고, 해결책인 JPA, Hibernate, Spring Data JPA가 무엇인인지 이해한다.
  2. Spring Data JPA를 이용해 데이터를 생성 ,조회, 수정, 삭제할 수 있다.
  3. 트랜잭션이 왜 필요한지 이해하고 스프링에서 트랜잭션을 제어하는 방법을 익힌다.
  4. 영속성 컨텍스트와 트랜잭션의 관계를 이해하고, 영속성 컨텍스트의 특징을 알아본다.



23강 문자열 SQL의 한계

기존 방식

  • Postman : 외부에서 API 호출
    Spring Boot : 스프링 컨테이너가 관리 (Controller, Service, Repository)
    MySQL
  • Repository에서 SQL을 직접 작성하였다.

SQL 직접 작성의 아쉬운 점

  1. 문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다.
    ↪ 컴파일 시 발견되지 않고, 런타임 시점에 발견되기 때문
  2. 특정 데이터베이스에 종속적이게 된다.
  3. 반복 작업이 많아진다. 테이블을 하나 만들 때마다 CRUD 쿼리가 항상 필요하다.
  4. 데이터베이스의 테이블과 객체는 패러다임이 다르다. (단방향 구성이거나, 클래스 상속일 때)

개선 방법

JPA (Java Persistence API)

  • Persistence : 영속성
    ⇒ 서버가 재시작 되어도 데이터는 영구적으로 저장되는 속성
  • API : 정해진 규칙

자바 진영의 ORM
(Object-Relation Mapping)

  • Object : 객체
  • Relation : 관계형 DB의 테이블
  • Mapping : 객체의 정보와 테이블의 정보를 짝짓는다.

JPA

  • 객체와 관계형 DB 테이블을 짝지어,
  • 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙

Hibernate

  • JPA를 구현해서 코드로 작성한 것
  • 구현체

정리

JPA - 규칙
Hibernate - 내부에 JDBC를 사용하고 있다.



24강 유저 테이블에 대응되는 Entity Class 생성

Java 객체와 MySQL 매핑

  • @Entity
    ⑴ 스프링이 User 객체와 User 테이블을 같은 것으로 바라본다.
    ⑵ 저장되고, 관리되어야 하는 데이터
@Entity
public class User 
{
    private long id;
    private String name;
    private Integer age;
    
    ...
}

@Id, @GeneratedValue

  • @Id : 이 필드를 PRIMARY KEY로 간주한다.
  • @GeneratedValue : PRIMARY KEY는 자동 생성되는 값이다. (AUTO_INCREMENT)
@Entity
public class User
{
	@Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    
    protected User()
    {
    	// 기본 생성자
    }
}
  • JPA 를 사용하기 위해서는 기본 생성자가 필요하다.
  • 이때 기본 생성자는 protected로 매개 변수 없이 생성할 수 있다.

@Column

  • @Column : 객체의 필드와 Table의 필드를 매핑한다.
  • nullable 속성이나, length, 테이블 변수명 등을 일치시킨다.
  • 테이블 칼럼과 코드가 동일할 경우에는 @Column 어노테이션 생략 가능하다.
@Entity
public class User {    
    @Column(nullable = false, length = 20, name = "user_name")
    private String name;
    
    private Integer age; // @Column 생략 가능 
}

application.yaml

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

ddl-auto

스프링이 시작할 때 DB에 있는 테이블과 객체의 필드가 다를 경우 어떻게 처리할지에 대한 것

  • create : 기존 테이블이 있다면 삭제 후 다시 생성
  • create-drop : 스프링이 종료될 때 테이블을 모두 제거
  • update : 객체와 테이블이 다른 부분만 변경
  • validate : 객체와 테이블이 동일한지 확인
  • none : 별다른 조치를 하지 않는다.

format_sql

  • SQL을 보여줄 때 예쁘게(?) 포맷팅할 것인가에 대한 것

show_sql

  • JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 지에 대한 것

dialect

  • 현재 옵션으로 DB를 특정하면 설정한 기준에 맞춰서 SQL을 수정해준다. (지금은 MySQL 사용)



25강 Spring Data JPA를 이용해 자동으로 쿼리 날리기

학습 목표

  • SQL을 작성하지 않고, 기존에 만들었던 User INSERT / SELECT/ UPDATE 기능을 리팩토링 한다.

구현

  • UserJdbcRepository 클래스명 변경
  • UserRepository 인터페이스 생성
    JpaRepository 상속
public interface UserRepository extends JpaRepository<User, Long>
{

}
  • JpaRepository를 상속받기 때문에 @Repository 어노테이션을 생략 해도 된다.

JpaRepository

  • 기본 CRUD 기능이 다 들어있는 인터페이스
  • <User> : 이 Repository가 관리할 엔티티 클래스
  • <Long> : User의 PK 타입 (보통 @Id가 붙은 필드 타입)
    → 고유한 데이터를 구분하기 위함

① INSERT 기능

Service

// [INSERT]
public void insertUser(UserCreateRequest request)
{
        User user
        	= userRepository.save(new User(request.getName(), request.getAge()));
}
  • userRepository (JpaRepository 상속) 를 통해 데이터를 INSERT 할 수 있다.

② SELECT 기능

Service

private List<UserResponse> selectUser()
{
	List<User> userList = userRepository.findAll();

	return userList.stream()
    	.map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
        .collect(Collectors.toList());
}
  • 함수 반환 타입
    여기서 함수 반환 타입이 List<User> 가 아닌 <UserResponse> 인 이유는 <User>는 DB와 연결된 객체이고, 사용자에게 보여지기 위해서는 DB에 직접 연결된 <User> 보다는 <UserResponse>를 통해 한번 감싸서 보여지는 것이 보안상 더 안전하기 때문이다.

  • findAll()
    : 자동으로 SQL을 날려서 해당 테이블에 있는 모든 데이터를 가져온다. (SELECT * FROM user)
    ↪ 그렇게 가져온 정보는 List가 된다. 여기서 List는 DB에 접근한 객체이기 때문에 <User> 타입이다.

  • RETURN
    ⑴ 그렇게 만들어진 userListstream()을 통해서 모든 데이터에 접근한다.
    map()을 통해 List의 <User> 타입을 함수 반환 타입인 <UserResponse> 타입으로 변환한다.
    User 객체의 id, name, age 값을 꺼내서(get 메서드) UserResponse 객체를 생성한다. (= DTO 로 변환한다.)

  • stream() : 리스트 같은 데이터를 한줄씩 꺼내면서, 편하게 가공할 수 있는 파이프 라인 (반복문을 더 깔끔하게 만들었다고 보면 됨)
  • map() : 각 User 객체를 DTO로 변환한다. 즉, 스트림 내부의 데이터를 다른 타입으로 변환한다.
  • collect() : 스트림을 다시 묶어서 반환
  • Collectors.toList() : 리스트로 묶는다.

더 간단하게 쓰기

// [UserResponse 생성자]
public UserResponse(User user)
{
	this.id = user.getId();
	this.name = user.getName();
	this.age = user.getAge();
}
// [Service]
return userList.stream()
	.map(UserResponse::new)
    .collect(Collectors.toList());
  • UserResponse::new : 생성자 참조
    user -> new UserResponse(user)

③ UPDATE 기능

Service

public void updateUser(UserUpdateRequest request)
{
	// 1) id 존재 여부 판단
	User user = userRepository.findById(request.getId())
		.orElseThrow(IllegalArgumentException::new);

	// 2) uddate
	user.updateName(request.getName()); 
	userRepository.save(user); // save() 메서드를 호출한다. -> 자동으로 UPDATE SQL이 실행됨
}
  1. id 존재 여부 판단
    findById() : Id 에 해당하는 user를 조회한다. (SELECT * FROM user WEHRE user_id = ?)
    Id에 해당하는 user가 있다면 user 객체에 저장
    Id에 해당하는 user가 없다면 예외 발생

  2. update
    save() → 자동으로 UPDATE SQL이 실행됨

주요 기능 정리

save() : 주어지는 객체를 저장하거나 업데이트 시켜준다.
findAll() : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져온다. (SELECT * FROM user)
findById() : Id를 기준으로 특정한 1개의 데이터를 가져온다. (SELECT * FROM user WEHRE user_id = ?)

SQL을 작성하지 않아도 동작하는 이유

  • Spring Data JPA
    복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리
    save() | findAll() | findById()

  • Spring Data JPA > JPA > Hibernate(JPA 구현체) > JDBC
    ⇒ Spring Data JPA는 JDBC를 사용하기 때문에 SQL을 작성하지 않아도 동작할 수 있다.



26강 Spring Data JPA를 이용해 다양한 쿼리 작성하기

④ DELETE 기능

UserRepository

public interface UserRepository extends JpaRepository<User, Long> 
{
    User findByName(String name);
}
  • find만 작성하면, 1개의 데이터만 가져온다.
  • By 뒤에 붙는 필드 이름으로 SELECT 쿼리의 WHERE 문이 작성된다.
public void deleteUser(String name)
{
	// SELECT * FROM user WHERE name = ?
	// name을 찾아서 해당 user가 존재하면 user 객체 생성
	// 해당 user가 없으면 null 반환
	User user = userRepository.findByName(name);
        
	if (user == null)
		throw new IllegalArgumentException();
        
	userRepository.delete(user);
}
  • userRepository.delete() : 주어지는 데이터를 DB에서 제거한다. (DELETE SQL)

By 앞에 들어갈 수 있는 구절

  • find : 1건을 가져온다.
    반환 타입 - 객체 or Optional<T>
    예시) User user = findByName(String name);
    Name에 해당하는 user 객체를 찾는다.

  • findAll : 쿼리의 결과물이 N개인 경우에 사용한다.
    반환 타입 - List<T>
    예시) List<User> userList = userRepository.findAll();
    → user 테이블의 모든 정보를 반환하여 리스트에 저장

  • exists : 쿼리 결과가 존재하는지 확인한다.
    반환 타입 - boolean
    예시) existsByAge()
    → Age에 해당하는 회원의 존재 여부를 반환한다.

  • count : SQL의 결과 개수를 센다.
    반환 타입 - long
    예시) countByAge(Integer age)
    → 해당 Age를 가진 user의 수를 반환한다.

By 뒤에 들어갈 수 있는 기능

  1. AND or OR로 조합할 수 있다.
    List<User> userList = findAllByNameAndAge(String name, Integer age);
    SELECT * FROM user WHERE user_name= ? AND age = ?

  2. GreaterThan : 초과
    GreaterThanEqual : 이상
    LessThan : 미만
    LessThanEqual : 이하

  3. Between : 사이에
    예시) List<User userList = findAllByBetween(int startAge, int endAge);
    SELECT * FROM user WHERE user_age BETWEEN ? AND ?;

  1. StartWith : ~로 시작하는
    EndsWith : ~로 끝나는



27강 트랜잭션 이론편

트랜잭션

  • 쪼갤 수 없는 업무의 최소 단위
  • 모든 SQL을 성공시키거나, 하나라도 실패하면 모두 실패시킨다.

트랜잭션 명령어

  • 트랜잭션 시작하기 : START TRANSACTION;
  • 트랜잭션 정상 종료하기 : COMMIT;
  • 트랜잭션 실패 처리하기 : ROLLBACK;

START TRANSACTION;

  • 서로 다른 단말기에서는 SQL의 결과물이 보이지 않는다.
    ⇒ A 단말기에서 INSERT한 결과가 B 단말기에서 보이지 않는다.

COMMMIT;

  • A 단말기에서 COMMIT;을 실행해야 B 단말기에서도 결과를 볼 수 있다.

ROLLBACK;

  • A 단말기에서 ROLLBACK;을 실행하면 A, B 단말기 모두 결과를 볼 수 없다.



28강 트랜잭션 적용과 영속성 컨텍스트

우리가 구현할 것

  1. 서비스 메서드가 시작할 때 트랜잭션이 시작된다. → START TRANSACTION;
  2. 서비스 메서드 로직이 모두 정상적으로 성공하면 → COMMIT;
  3. 서비스 메서드 로직 실행 도중 문제가 생기면 → ROLLBACK;

@Transactional

동작 원리

  1. @Transactional 어노테이션의 아래에 있는 함수가 시작될 때 → START TRANSACTION;
  2. 함수가 예외 없이 잘 끝났다면 → COMMIT;
  3. 함수에 문제가 있다면 → ROLLBACK;
// [INSERT]
@Transactional
public void insertUser(UserCreateRequest request)
{
	User user = userRepository.save(new User(request.getName(), request.getAge()));
}

// [SELECT]
@Transactional(readOnly = true)
public List<UserResponse> selectUsers()
{
	List<User> userList = userRepository.findAll();

	return userList.stream() // 현재 List는 stream 형태이므로 collect()를 통해 리스트로 변환한다.
		.map(user -> new UserResponse(user.getId(), user.getName(), user.getAge()))
		.collect(Collectors.toList());
}
  • @Transactional(readOnly = true)
    : SELECT 쿼리만 사용한다면, readOnly 옵션을 사용할 수 있다.
    ↪ 데이터 변경을 위한 불필요한 기능이 빠지기 때문에 성능적 이점이 있다.
  • IOException과 같은 Checked Exception은 예외가 발생해도 ROLLBACK;이 일어나지 않는다.

영속성 컨텍스트

  • 테이블과 매핑된 Entity 객체를 관리/보관하는 역할
  • 스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고,
    트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.

영속성 컨텍스트의 기능

  1. 변경 감지 (Dirty Check)
    → 영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.

  2. 쓰기 지연
    INSERT, UPDATE, DELETE 시, 객체를 세 개 저장할 때, DB와 세 번 통신하는 것이 아니라, 영속성 컨텍스트가 세 개의 객체를 기억해뒀다가 한 번에 DB에 저장하여 총 한 번 통신하도록 한다.

  3. 1차 캐싱
    → ID를 기준으로 Entity를 기억한다.
    SELECT 시 동일한 데이터를 여러번 조회할 경우, 한 번 조회했던 데이터를 계속 사용할 수 있다.
    → 캐싱된 객체(영속성 컨텍스트가 잠깐 저장한 객체)는 완전히 동일하다. (주소까지)

0개의 댓글