Search

4. 생애 최초 JPA 사용하기

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

문자열 SQL을 직접 사용하는 것이 너무 어렵다

지금까지 우리가 작성한 코드를 살펴보면...
스프링 부트 코드를 Postman이라는 API 테스트 프로그램을 사용해서 외부에서 API를 호출했다.
Layered architecture, 스프링 컨테이너와 관리하는 빈들(컨트롤러, 서비스, 레파지토리) 쭉 거쳐서
레파지토리에서 MySql과 sql을 사용해서 직접 통신하게 됐다.
레파지토리에서는 직접 SQL를 작성해서 진행했다.

SQL을 직접 작성하면 어떤 점이 아쉬울까?!

1. 문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다!
user인데 users라고 치면 별도로 빨간줄로 알려주지 않는다.
컴파일 시점에 발견되지 않고, 저 해당 코드를 실행할때 런타임 시점에 발견된다!
2. 특정 데이터베이스에 종속적이게 된다.
데이터를 표처럼 관리한 데이터베이스 종류에는 mysql 말고도 mssql, postgresql, mariadb 등 다양한 데이터베이스들이 있다. 그래서 상황이나 정책에 따라서 다양한 데이터베이스를 쓴다.
만약 다른 DB로 갈아타야 한다면 작성한 모든 레파지토리 쿼리문을 바꿔줘야 한다.
3. 반복 작업이 많아진다. 테이블을 하나 만들 때마다 CRUD 쿼리가 항상 필요하다
만약 조회해야하는 필드가 50개 이상이라면?…윽..
4. 데이터베이스의 테이블과 객체는 패러다임이 다르다.
대표적으로 연관관계가 있다.
간단하게 소스로 짜보면
package com.group.libraryapp.student; public class ClassRoom { private String name; private List<Student> student = new ArrayList<>(); }
Java
복사
package com.group.libraryapp.student; public class Student { private String name; private ClassRoom classRoom; }
Java
복사
ClassRoom에도 Student를 사용하고
Student에도 ClassRoom 정보를 들고 있다.
교실과 학생을 객체로 표현하게 되면 교실도 학생을 가지고 있고 학생도 교실을 가지고 있게 된다
즉, 양방향 관계
이렇게 교실과 학생 관계를 데이터베이스의 테이블로 표현하게 되면
교실 테이블에는 아이디와 이름이 들어가고
학생 테이블은 아이디와 이름 그리고 어떤 교실에 들어있는지 교실 아이디가 들어가게 된다.
객체와의 차이점은 교실은 학생을 가리키지 않고, 학생은 교실을 가리킨다.
즉 양방향이 아니라 학생만 교실을 가리키게 된다.
이렇게 데이터베이스 테이블과 객체 패러다임은 다르다.
또 다른 예시로 상속이 있다.
이렇게 부모클래스와 하위클래스로 이루어진 관계는 데이테베이스의 테이블로 표현하기 애매한 부분들이 있다.

아쉬운 점을 정리

1. 문자열을 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다!
2. 특정 데이터베이스에 종속적이게 된다.
3. 반복 작업이 많아진다. 테이블을 하나 만들 때마다 CRUD 쿼리가 항상 필요하다.
4. 데이터베이스의 테이블과 객체는 패러다임이 다르다.
현재 대부분의 웹 애플리케이션은 객체 지향적인 설계를 지향하고 있기 때문에 데이터베이스 테이블을 직접적으로 사용하기에 아쉬운 점이 있다.

그래서 나온게 JPA

JPA (Java Persistence API)는 자바 진영의 ORM (Object-Relational Mapping)이다.
JPA (Java Persistence API)
데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙
Persistence 영속성 : 서버가 재시작되어도 데이터는 영구적으로 저장되는 속성
우리가 서버를 실행시켜 API를 동작시키기까지 일어나는 일
서버가 종료되면 RAM에 있는 모든 정보는 사라진다!
API : 정해진 규칙
자바 진영의 ORM (Object-Relational Mapping)
Object
Relational 관계형 DB의 테이블을 의미!
Mapping 둘을 짝짓는다!

총 정리 - JPA란?!

객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙
JPA는 규칙인데 여기서 규칙이라는건 말 그대로 규칙일뿐이고 그 규칙을 실제 코딩을 해줘야 한다.
규칙과 그 규칙을 실제대로 구현한 코드가 존재하는데. 자바에서 인터페이스같은 개념
그 구현한 코드가 바로 Hibernate

말로 되어 있는 규칙을 코드로 구현해야 한다!

JPA와 Hibernate

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

유저 테이블에 대응되는 Entity Class 만들기

Java객체와 MySQL Table을 매핑할 것이다!

이전에 만들어두었던 User 객체를 활용하자!

객체에는 name과 age가 있다!

우리가 만든 Table과 비교해보자!

유저 테이블하고 객체를 JPA를 이용해서 맵핑하도록 할거다.
가장 먼저 해줄거는 User 클래스에 @Entity를 붙여준다.
@Entity public class User { . . }
Java
복사

JPA 어노테이션

@Entity : 스프링이 User객체와 user 테이블을 같은 것으로 바라본다.

Entity의 의미

저장되고, 관리되어야 하는 데이터

빠져 있는 id를 추가해주자!

@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null;
Java
복사
@Id : 이 필드를 primary key로 간주한다.
@GeneratedValue : primary key는 자동 생성되는 값이다.
DB의 종류마다 자동 생성 전략이 다르다!
우리는 MySQL의 auto_increment를 사용했고, 이는 IDENTITY 전략과 매칭된다
근데 아직도
빨간줄이 사라지지 않았다.

JPA를 사용하기 위해서는 기본 생성자가 꼭 필요하다!

protected User(){}
Java
복사

id가 아닌 기본 column을 매핑하는 어노테이션

@Column : 객체의 필드와 Table의 필드를 매핑한다!

User 객체의 name에 적용해보자

@Column(nullable = false, length = 20, name = "name") //name varchar(20) private String name;
Java
복사

@Column

null이 들어갈 수 있는지 여부, 길이 제한, DB에서의 column 이름 등등...
네임이 같으면 생략할 수 있다.
@Column(nullable = false, length = 20) //name varchar(20) private String name;
Java
복사
사실 Column은 생략 할 수도 있다!

우리가 만든 Table과 비교해보자!

이제 객체와 테이블의 매핑은 끝이 났다!!

단, JPA를 사용하니 추가적인 설정을 해주어야 한다.

application.yml

jpa: hibernate: ddl-auto: none properties: hibernate: show_sql: true format_sql: true dialect: org.hibernate.dialect.MariaDBDialect
XML
복사
스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리할지 설정해주는 부분이다
객체랑 테이블을 맵힝했는데 만약 테이블이랑 객체가 다르게 생겼다.
맵팡하는 코드를 잘못 짠건데 이 경우에는 무시할 수도 있고 테이블을 바꿀 수도 있다. 그런 선택지를 스프링이 시작할 때 DB에 있는 객체에 있는 맵핑이 다르면 어떻게 할건지에 대한 부분.
spring.jpa.hibernate.ddl-auto
create : 기존 테이블이 있다면 삭제 후 다시 생성
create-drop : 스프링이 종료될 때 테이블을 모두 제거
update : 객체와 테이블이 다른 부분만 변경
validate : 객체와 테이블이 동일한지 확인
none : 별다른 조치를 하지 않는다.
JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 것인가
SQL을 보여줄 때 예쁘게 포맷팅 할 것인가
dialect : 방언, 사투리
이 옵션으로 DB를 특정하면 조금씩 다른 SQL을 수정해준다

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

SQL을 작성하지 않고, 우리가 만들었던 유저 생성 / 조회 / 업데이트 기능을 리팩토링 해볼 것이다.
기존에 있던 UserRepository를 UserJdbcRepository로 이름을 변경해준다.
UserRepository 인터페이스를 User 옆에 만들어주자!
그 다음 JpaRepository를 상속 받아야 한다!
public interface UserRepository extends JpaRepository<User, Long> { }
Java
복사
여기는 @Repository를 붙이지 않아도 JpaRepository를 상속한거만으로 스프링 빈으로 관리가 된다.
그리고 기존의 UserService 이름을 UserServiceV1으로 변경. Jdbc 소스를 남겨둘려고 하는거다.
그리고 같은 위치에 UserServiceV2를 만든다
package com.group.libraryapp.service.user; @Service public class UserServiceV2 { private final UserRepository userRepository; public UserServiceV2(UserRepository userRepository) { this.userRepository = userRepository; } }
Java
복사
유저 저장 기능
public void saveUser(UserCreateRequest userCreateRequest){ userRepository.save(new User(userCreateRequest.getName(), userCreateRequest.getAge())); }
Java
복사
save 메소드에 객체를 넣어주면 INSERT SQL이 자동으로 날라간다!
save되고 난 후의 User는 id가 들어 있다!
User에다가 id갖고오는 부분 추가
public Long getId() { return id; }
Java
복사
public List<UserResponse> getUsers(){ List<User> users = userRepository.findAll(); return users.stream() .map(user -> new UserResponse(user.getId(), user.getName(), user.getAge())) .collect(Collectors.toList()); }
Java
복사
findAll을 사용하면 모든 데이터를 가져온다!
select * from user;
UserResponse에 생성자를 추가하면 코드가 깔끔해진다!
public UserResponse(User user) { this.id = user.getId(); this.name = user.getName(); this.age = user.getAge(); }
Java
복사
그럼 밑에 처럼 바꿔줄 수 있다.
public List<UserResponse> getUsers(){ List<User> users = userRepository.findAll(); return users.stream() .map(UserResponse::new) .collect(Collectors.toList()); }
Java
복사

유저 업데이트 기능

1. id를 이용해 User를 가져와 User가 있는지 없는지 확인하고
2. User가 있다면 update 쿼리를 날려 데이터를 수정한다.
User
public void updateName(String name){ this.name = name; }
Java
복사
UserServiceV2
public void updateUser(UserUpdateRequest userUpdateRequest){ // select * from user where id = ?; // Optional<User> User user = userRepository.findById(userUpdateRequest.getId()) .orElseThrow(IllegalAccessError::new); user.updateName(userUpdateRequest.getName()); userRepository.save(user); }
Java
복사
findById를 사용하면 id를 기준으로 1개의 데이터를 가져온다.
Optional의 orElseThrow를 사용해 User가 없다면 예외를 던진다.
객체를 업데이트 해주고, save 메소드를 호출한다. 그러면 자동으로 UPDATE SQL이 날라가게 된다.

지금까지 사용한 기능 정리

save : 주어지는 객체를 저장하거나 업데이트 시켜준다.
findAll : 주어지는 객체가 매핑된 테이블의 모든 데이터를 가져온다.
findById : id를 기준으로 특정한 1개의 데이터를 가져온다.

어떻게 SQL을 작성하지 않아도 동작하지?! JPA인가?!

반은 맞고, 반은 틀렸다!!
Spring Data JPA (JPA랑은 다른거다)

Spring Data JPA

복잡한 JPA 코드를 스프링과 함께 쉽게 사용할 수 있도록 도와주는 라이브러리
SimpleJpaRepository이라는 파일을 찾아서 보면 Spring Data JPA 가 제공해주는 코드가 나온다
save를 검색하면 나오는데 이것들이 JPA 코드를 감싼 Spring Data JPA이다.
가장 바깥에 Spring Data JPA가 있고, 이 Spring Data JPA는 JPA를 쓰고 있다.
그 다음 이 JPA는 규칙이기 때문에 코드가 한 줄도 없고 그래서 JPA를 구현한 Hibernate이 코드 역할을 하고 있음 (구현체)
이 Hibernate은 다시 내부적으로 JDBC를 쓰고 있다.
그래서 정리해보면 이렇게 Spring Data JPA를 저희가 직접 쓰지만 사실은 Spring Data JPA 안의 JPA 구현체인 Hibernate의 JDBC를 쓰고 있다

Spring Data JPA를 이용해 자동으로 쿼리 작성하기

기존 JDBC 버전 서비스 소스

1) 이름을 기준으로 User가 있는지 확인하고
2) User가 있다면 DELETE 쿼리를 날린다.
삭제 기능을 Spring Data JPA로 변경하자!

Spring Data JPA를 활용해 조회 쿼리

근데 이름을 가지고 변경해야하는데
findByName을 제공해주고 있지 않다.
그래서 UserRepository에 가서
package com.group.libraryapp.domain.user; public interface UserRepository extends JpaRepository<User, Long> { User findByName(String name); }
Java
복사
User로 반환하는 함수를 하나 만들어준다.
반환 타입은 User이다. 유저가 없다면, null이 반환된다.
함수 이름만 작성하면, 알아서 SQL이 조립된다!!
find라고 작성하면, 1개의 데이터만 가져온다.
By 뒤에 붙는 필드 이름으로
SELECT 쿼리의 WHERE 문이 작성된다.
SELECT * FROM user WHERE name = ?;

삭제 기능 변경 완료!

UserServiceV2
public void deleteUser(String name){ // SELECT * FROM user WHERE name = ? User user = userRepository.findByName(name); if(user == null){ throw new IllegalArgumentException(); } userRepository.delete(user); }
Java
복사
주어지는 데이터를 DB에서 제거한다. (delete SQL)

모든 기능을 바꾸었으니, 테스트 해보자!

테스트 전에
UserController
package com.group.libraryapp.controller.user; @RestController public class UserController { private final UserServiceV2 userService; private final FruitService fruitService; public UserController(UserServiceV2 userService, @Qualifier("main") FruitService fruitService){ this.userService = userService; this.fruitService = fruitService; } . . . }
Java
복사
새로 만든 서비스를 사용하게끔 바꿔준다.
이제 들어가서 회원 가입하고 목록가니까 잘 뜨고
def으로 변경도 잘 됐다.
삭제누르니까
삭제도 잘됨.

다양한 Spring Data JPA 쿼리

By 앞에 들어갈 수 있는 기능부터 살펴보자!

By 앞에 들어갈 수 있는 구절 정리

find : 1건을 가져온다. 반환 타입은 객체가 될 수도 있고, Optional<타입>이 될 수도 있다.
UserRepository에 옵셔널로 변경하고
Optional<User> findByName(String name);
Java
복사
UserServiceV2
public void deleteUser(String name){ User user = userRepository.findByName(name).orElseThrow(IllegalArgumentException::new); /* if(user == null){ throw new IllegalArgumentException(); } */ userRepository.delete(user); }
Java
복사
findAll : 쿼리의 결과물이 N개인 경우 사용. List<타입> 반환.
exists : 쿼리 결과가 존재하는지 확인. 반환 타입은 boolean
UserRepository
public interface UserRepository extends JpaRepository<User, Long> { User findByName(String name); boolean existsByName(String name); //존재 여부에 따른 값 }
Java
복사
UserServiceV2
public void deleteUser(String name){ if(!userRepository.existsByName(name)){ throw new IllegalArgumentException(); } User user = userRepository.findByName(name); userRepository.delete(user); }
Java
복사
count : SQL의 결과 개수를 센다. 반환 타입은 long이다.
long countByAge(Integer age); //해당 이름을 가진 유저의 명수가 반환
Java
복사

이제 By 뒤에 들어갈 수 있는 기능들을 살펴보자!

각 구절은 And 나 Or로 조합할 수도 있다.

SELECT * FROM user WHERE name = ? AND age = ?;

By 뒤에 들어갈 수 있는 구절 정리

GreaterThan : 초과
GreaterThanEqual : 이상
LessThan : 미만
LessThanEqual : 이하
Between : 사이에
StartsWith : ~로 시작하는
EndsWith : ~로 끝나는
SELECT * FROM user WHERE age BETWEEN ? AND ?;
서비스 계층의 역할은 끝나지 않았다! - 트랜잭션

트랜잭션 이론편

트랜잭션이란?!

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

쇼핑몰 사이트에서 물건을 주문하면..?!

1) 주문 기록을 저장하고
2) 포인트를 저장하고
3) 결제 기록을 저장해야 한다!

자 그런데.. 만약 중간에서 에러가 난다면?!

주문 기록과 포인트는 있는데, 결제 기록이 없다!
주문 기록은 있는데, 포인트와 결제 기록이 없다!

어떻게 이런 문제를 해결할 수 있을까?!

모든 SQL을 성공시키거나, 하나라도 실패하면 모두 실패시키자!!

트랜잭션 시작하기

start transaction;
SQL
복사

트랜잭션 정상 종료하기 (SQL 반영)

commit;
SQL
복사

트랜잭션 실패 처리하기 (SQL 미반영)

rollback;
SQL
복사
서로 다른 터미널을 열어서 같은 테이블을 조회해보기전에 트랜잭션을 시작해주고
좌측 터미널에는 값을 하나 추가해주고 우측에서 조회했는데 뜨지 않는다. 분명 넣어줬는데!
이게 바로 트랜잭션 때문에 그렇다.
왜냐면 아직 성공인지 실패인지 말을 해주지 않았기 때문에 커밋이나 롤백이 쓰지 않아서.
커밋하고 나면 우측 터미널에서도 이제 조회가 된다.
이제 실패했을 때를 생각해서 C,D의 값을 넣고 롤백을하면
우측에 조회를 해도 C,D에 대한 값을 출력되지 않는다

묶어서 저장된다는 의미

터미널 2개를 띄어서
접속1에서는 트랜잭션을 시작하고 A값 등록
아직 트랜잭션 안의 SQL이 반영되지 않아, 접속 2는 A 유저를 확인할 수 없다!!!

트랜잭션

쪼갤 수 없는 업무의 최소 단위 = 모두다 성공시키거나 모두 다 실패시키거나

트랜잭션을 우리 코드에 어떻게 적용시킬 수 있을까?!

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

우리가 원하는 것은...

1. 서비스 메소드가 시작할 때 트랜잭션이 시작되어
2-1. 서비스 메소드 로직이 모두 정상적으로 성공하면 commit되고
2-2. 서비스 메소드 로직 실행 도중 문제가 생기면 rollback 되는 것

어떻게 트랜잭션을 적용할 수 있을까?!

@Transactional
Java
복사

어노테이션으로 간단히 적용 가능하다.

UserServiceV2에다가 @Transactional 적용시켜주자.
@Transactional public void saveUser(UserCreateRequest userCreateRequest){ userRepository.save(new User(userCreateRequest.getName(), userCreateRequest.getAge())); } @Transactional(readOnly = true) public List<UserResponse> getUsers(){ List<User> users = userRepository.findAll(); return users.stream() .map(UserResponse::new) .collect(Collectors.toList()); } @Transactional public void updateUser(UserUpdateRequest userUpdateRequest){ User user = userRepository.findById(userUpdateRequest.getId()) .orElseThrow(IllegalAccessError::new); user.updateName(userUpdateRequest.getName()); userRepository.save(user); } @Transactional public void deleteUser(String name){ User user = userRepository.findByName(name).orElseThrow(IllegalArgumentException::new); userRepository.delete(user); }
Java
복사
그냥 어노테이션 하나만 붙이면 된다.

SELECT 쿼리만 사용한다면, readOnly 옵션을 쓸 수 있다!

이렇게 쓰먄 약간의 성능 이점이 있다.

테스트

// 아래 있는 함수가 시작될때 start transaction; 을 해준다 (트랜잭션 시작!) // 함수가 예외 없이 잘 끝났다면 commit // 혹시라도 문제가 있다면 rollback @Transactional public void saveUser(UserCreateRequest userCreateRequest){ userRepository.save(new User(userCreateRequest.getName(), userCreateRequest.getAge())); throw new IllegalArgumentException(); }
Java
복사
유저 저장할때 일부로 에러나게끔 해서 트랜잭션이 작동하는지 보자
잘 적용되서 등록조차 안됐다!

주의 사항!

IOException과 같은 Checked Exception은 롤백이 일어나지 않는다.

영속성 컨텍스트란?

테이블과 매핑된 Entity 객체를 관리/보관하는 역할

이것만 기억하면 됩니다!

스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.

영속성 컨텍스트의 특수 능력 4가지

[1] 변경 감지 (Dirty Check)

영속성 컨텍스트 안에서 불러와진 Entity는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.
save 함수가 있든
없든 알아서 업데이트가 된다.
실제로 save 함수를 지워주고 테스트 해보자
save를 명시하지 않아도 알아서 변경을 감지해서 자동으로 업뎃해줌.

[2] 쓰기 지연

DB의 INSERT / UPDATE / DELETE SQL을 바로 날리는 것이 아니라, 트랜잭션이 commit될 때 모아서 한 번만 날린다.

[2] 쓰기 지연 - 쓰기 지연이 없다면?

이렇게 총 스프링과 DB 사이에 통신이 3번 일어난다.

[2] 쓰기 지연 - 쓰기 지연이 있다면?

끝날때 1번만 통신해서 한꺼번에 저장.

[3] 1차 캐싱

ID를 기준으로 Entity를 기억한다!
첫번째 코드가 실행되고 이제 조회를 한다
id가 1인 유저를 가지고 와서 기억해둔다.
두번째를 조회
이때 이미 id 1를 가진 유저를 알고 있으니까 영속성 컨텍스트가 유저 정보를 넘겨준다.
두번째랑 같은 방식이다.
이렇게 되면 쓰기 지연과 비슷하게 DB와 계속 통신을 해서 시간이 더 걸리고 네트워크를 잡아먹는 게 아니라 영속성 컨텍스트 덕분에 최초 한 번만 데이터를 조회하고 그 후에는 한 번 조회했던 데이터를 계속 사용할 수 있다는 장점이 있다.
이렇게 캐싱된 객체는 완전이 동일하다!

다음으로

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

다음 요구조건

*참고