섹션 4. 생애 최초 JPA 사용하기
현재는 외부에서 API 호출을 하면 스프링 컨테이너가 빈들을 관리하고 DB는 MySQL과 연동되어 있다.
SQL을 직접 작성하면 어떤 점이 아쉬울까?
-> 문자열을 직접 작성하기 때문에 실수할 수 있고, 실수를 인지하는 시점이 느리다. 컴파일 시점에 발견되지 않고, 런타임 시점에 오류가 발생한다.
또한, 특정 데이터베이스에 종속적이게 된다. 그리고 테이블을 만들 때마다 CRUD 쿼리가 필요해서 반복 작업이 많아지게 된다.
데이터베이스의 테이블과 객체는 패러다임이 다르다.
그래서 JPA가 등장하게 되었다.
JPA는 데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙이다.
객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙이라고 생각하자!
Hibernate는 JPA - 규칙을 구현하고 JDBC를 사용해서 자바에서 데이터베이스를 사용할 수 있게 해준다.
이전에 만든 User 객체를 사용해서 진행해보자.
기존의 데이터베이스의 user 테이블에는 자동 증가하는 id, name, age가 있었다. 현재 객체의 User에는 name과 age만 존재하므로 id를 추가해주자.
@Id // id annotation -> 이 필드를 primary key로 간주한다
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 (auto_increment)
private Long id = null;
@Column
: 객체의 필드와 Table의 필드를 매칭한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id = null;
@Column(nullable = false, length = 20, name = "name") // name varchar(20)
private String name;
// @Column 생략 가능
private Integer age;
protected User() {
}
이렇게 완전히 매핑에 성공했다.
jpa:
hibernate:
ddl-auto: none
properties:
hibernate:
show_sql: true
format_sql: true
dialect: org.hibernate.dialect.MySQL8Dialect
그리고 yaml 파일에 의존성을 추가해줬다.
이제 기본 세팅을 끝났다. 이제 SQL을 작성하지 않고 CRUD 기능을 수행해보자.
먼저 UserRepository를 User 객체 옆으로 옮겨주자. 기존의 UserRepository를 UserJdbcRepository로 바꿔줬다. 그리고 인터페이스로 UserRepository를 만들고 JpaRepository를 상속 받아야 한다.
// UserRepository
public interface UserRepository extends JpaRepository<User, Long> {
}
// 유저 저장 기능
public void saveUser(UserCreateRequest request) {
userRepository.save(new User(request.getName(), request.getAge()));
}
// 유저 조회 기능
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());
}
save 메소드에 객체를 넣어주면 INSERT SQL이 자동으로 날라간다!
그리고 stream을 이용해서 findAll을 사용하면 모든 데이터를 가져온다. 이를 통해 유저 조회 기능도 만들었다.
유저 업데이트 기능은 id를 이용해 있는지 없는지 확인하고 존재한다면 update 쿼리를 통해 데이터를 수정한다.
public void updateUser(UserUpdateRequest request) {
User user = userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new);
user.updateName(request.getName());
userRepository.save(user);
}
// 삭제 기능
public void deleteUser(String name) {
User user = userRepository.findByName(name);
if (user == null) {
throw new IllegalArgumentException();
}
userRepository.delete(user);
}
반환 타입은 User이며 함수 이름을 findByName으로 작성하면 알아서 조립되어서 이름에 따라 찾을 수 있게 된다. 물론 반환 할 것이 없으면 Null을 반환한다.
이렇게 모든 기능을 JPA를 사용해서 바꿔봤다.
트랜잭션
트랜잭션이랑 쪼갤 수 없는 업무의 최소 단위를 의미한다.
public class OrderService {
public void completePayment() {
orderRepository.save(new Order());
pointRepository.save(new Order());
billingHistoryRepository.save(new BillingHistory());
}
}
만약 이런 주문 서비스에서 에러가 나면 어떻게 처리할까? 예를 들면 포인트가 적립이 되지 않는다거나 주문은 했으나 영수증이 나오지 않는 등의 에러가 발생한다면 어떻게 처리해야할까? 그럴 때는 하나라도 실패하면 모두 실패시키고 모든 SQL을 성공시키거나 2가지로 나눠서 생각한다. 이것이 트랜잭션이다.
트랜잭션을 시작하고 commit을 해주어야 해당 과정이 끝나게 된다.
트랜잭션을 우리 코드에 어떻게 적용시킬 수 있을까?
우리가 원하는 것은 서비스 메서드가 시작할 때 트랜잭션이 시작되어 서비스 메서드 로직이 정상 성공하면 commit, 아니라면 rollback해주는 것이다.@Transactional
을 사용하면 가능하다!
@org.springframework.transaction.annotation.Transactional
public void saveUser(UserCreateRequest request) {
userRepository.save(new User(request.getName(), request.getAge()));
throw new IllegalArgumentException();
}
추가적으로 SELECT 쿼리만 사용한다면 readonly 옵션을 쓸 수 있다.
주의할 점은 IOException은 롤백되지 않는다.
영속성 컨텍스트란?
테이블과 매핑된 Entity 객체를 관리/보관하는 역할
스프링에서는 트랜잭션을 사용하면 영속성 컨텍스트가 생겨나고, 트랜잭션이 종료되면 영속성 컨텍스트가 종료된다.
영속성 컨텍스트의 특수 능력
변경 감지 -> 영속성 컨텍스트 안에서 불러와진 엔티티는 명시적으로 save하지 않더라도, 변경을 감지해 자동으로 저장된다.
쓰기 지연 -> DB의 INSERT, UPDATE, DELETE SQL을 바로 날리는 것이 아니라 트랜잭션이 commit 될 때 된다.
1차 캐싱 -> ID를 기준으로 Entity를 기억한다.
또한 이렇게 캐싱된 객체는 완전 동일하다.다음 section에서 ^_^
댓글을 작성해보세요.