인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.
스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고, 참여하면서 느낀 부분을 회고해 보고자 한다.
(1) 7일 차 : 2024-02-27(Tue)
1. Repository 영역에서 지금까지 작성한 코드
(1) Repository에서 데이터베이스와의 통신을 위해 직접적으로 쿼리를 사용했다. 하지만 SQL을 직접적으로 사용하면서 아래와 같은 단점들이 존재했다.
1-1. 쿼리를 직접 작성하면 아쉬운 점?
(1) 문자열 기반으로 작성되기 때문에 실수할 수 있고 실수를 인지할 수 있는 시점이 느려지게 된다.
- 컴파일 시점이 아닌 런타임 시점에 오류가 발생하게 된다.
(2) 특정 데이터베이스 벤더에 종속적인 쿼리가 발생하게 된다.
- 현재는 MySQL을 사용, 상황, 요건에 따라 PostgreSQL을 써야 한다면 PostgreSQL의 문법으로 모든 코드를 수정해야 함
(3) CRUD 쿼리는 테이블마다 항상 필요하며 반복적이다.
1-2. 객체와 테이블 간 패러다임 불일치가 생긴다.
(1) 데이터베이스 테이블과 자바의 객체의 패러다임은 불일치하다. (대표적으로 연관관계 패러다임의 불일치 발생)
- 교실과 학생 관계를 객체로 바라보는 경우 각 객체의 참조를 통해 각 객체 간 탐색이 가능하다.
- 하지만 테이블의 관점에서는 교실 테이블에서 학생 테이블로의 관계는 학생 테이블이 갖고 있는 교실 테이블의 외래 키를 통해 조인함으로써 탐색이 가능하게 된다
- 연관관계 관점에서 보면 객체와 테이블은 이렇게 패러다임이 불일치하다는 것을 알 수 있다.
(2) 상속구조를 표현할 수 없다.
- 객체는 부모, 자식 클래스로 계층 구조를 이루지만, 테이블은 이러한 상속 구조가 없어서 객체의 상속구조를 표현하기 어렵다는 점이 존재한다.
(3) 이처럼 현재 대부분의 웹 애플리케이션은 객체지향적 설계를 지향하고 있기 때문에 이러한 데이터베이스 테이블을 직접적으로 사용하기에 위와 같은 아쉬운 점이 항상 존재한다. 이를 어떻게 해결할까?
2. 해결 방안 : JPA(Java Persistence API)
2-1. JPA(Java Persistence API)
(1) 자바의 객체와 테이블을 매핑하기 위한 설계된 표준 API로 자바 진영의 ORM(Object Relational Mapping) 기술이다.
(2) ORM(Object Relational Mapping)은 객체와 관계형 데이터베이스 테이블을 매핑하여 RDB를 보다 더 객체지향적으로 사용할 수 있게 도와주는 기술이다.
2-2. 하이버네이트(Hibernate)
(1) 하이버네이트는 표준 API인 JPA를 실제 코드로 구현한 구현체를 말한다.
(2) 이러한 하이버네이트는 자바 진영의 JPA의 구현체이기 때문에 내부적으로 JDBC를 사용하고 있다.
3. 엔티티 (Entity) 설계
3-1. 엔티티
(1) 엔티티는 데이터베이스에서 정보를 저장하고 관리하기 위한 것으로, 데이터 모델링에서 사용되는 객체를 의미한다.
(2) 실제 데이터베이스 테이블을 객체지향적으로 사용하기 위해 자바 객체와 테이블을 매핑해 주는 작업이 필요하다.
3-2. User.java
@Entity
@Getter
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@Column(nullable = false, length = 20)
private String name;
private Integer age;
public User(String name, Integer age) {
/** 회원의 이름은 공백일 수 없다 */
if (name == null || name.isBlank()) {
throw new IllegalArgumentException(String.format("잘못된 회원 이름 name(%s)이 입력되었습니다.", name));
}
this.name = name;
this.age = age;
}
public User(long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
protected User() {}
}
3-3. 테이블, 테이블의 컬럼과 객체를 매핑하기 위한 여러 가지 어노테이션들
(1) @Id
- 테이블의 Primary key를 매핑한다.
(2) @GeneratedValue(strategy = GenerationType.IDENTITY)
- Primary key의 생성 전략을 설정한다. MySQL의 경우 Auto-increment, 이는 JPA에서 IDENTITY 생성 전략과 일치한다
(3) 기본 생성자 외에 추가적인 생성자가 존재하는 경우 JPA에서는 프록시 기술 등에 사용되기 위해 자바 기본 생성자를 필요로 한다 접근 제어자는 public, protected 둘 중 하나로 선택한다.
(4) @Column
- 테이블의 컬럼과 매핑하는 어노테이션
- @Column의 속성으로는 실제 컬럼 이름과 객체 필드의 이름이 다를 때 사용하는 name, null 여부를 선택하는 nullable, 컬럼의 데이터 길이를 제한하는 length 등 다양한 속성이 존재한다.
3-4. JPA에 관한 기본 설정 진행 : application.yml
(1) spring.jpa.hibernate.ddl-auto : 스프링 애플리케이션이 시작될 때 데이터베이스 테이블을 어떻게 처리해야 할지에 대한 옵션
- create : 기존 테이블 존재 시, 테이블을 삭제 후 재생성한다.
- create-drop : create와 동일하며 애플리케이션이 종료될 때 테이블을 모두 삭제한다.
- update : update는 엔티티로 등록된 클래스와 매핑되는 테이블이 없으면 새로 생성하는 것은 create와 동일하지만 기존 테이블이 존재한다면 위의 두 경우와 달리 테이블의 컬럼을 변경하게 된다.
- validate : 테이블을 생성하거나 수정하지 않고, 엔티티 클래스와 테이블이 정상적으로 매핑되는지만 검사한다. 만약 테이블이 아예 존재하지 않거나, 테이블에 엔티티의 필드에 매핑되는 컬럼이 존재하지 않으면 예외를 발생시키면서 애플리케이션을 종료한다.
- none : 별도의 조치를 하지 않는다.
(2) spring.jpa.properties.hibernate.show_sql
- JPA를 사용해 데이터베이스에 쿼리를 날릴 때 쿼리의 로그를 보여줄지 결정한다.
(3) spring.jpa.properties.hibernate.format_sql
- 쿼리를 찍어서 보여줄 때 포맷팅해서 보여줄지 결정한다.
(4) spring.jpa.properties.hibernate.dialect
- JPA가 쿼리를 날릴 때 데이터베이스 벤더사에 맞게 적합한 쿼리를 생성해서 보내는데, 해당 데이터베이스 벤더를 적는 옵션이다.
- MySQL을 사용하고 있으므로 org.hibernate.dialect.MySQL8Dialect을 적는다.
4. Spring Data JPA 사용해서 Repository 작성
4-1. Spring Data JPA
(1) Spring Data는 스프링에서 데이터베이스에 접근을 단순화하고 효율적으로 처리할 수 있도록 도와주는 프로젝트 라이브러리이다.
(2) Spring Data JPA는 이러한 Spring Data의 라이브러리 중 하나로 JPA를 사용해서 데이터베이스와 상호작용할 때 도움을 줄 수 있는 라이브러리이다.
(3) 특히 기본적인 CRUD 처리를 높은 레벨로 추상화하여 단순한 CRUD의 경우 Repository에서 직접 코드를 작성하지 않아도 된다.
(4) 따라서 Spring Data JPA가 제공하는 기본적인 CRUD 인터페이스를 생성해 보자.
4-2. UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
(1) 하나의 엔티티를 대상으로 Repository를 생성한다. 제네릭 타입으로 왼쪽은 엔티티 클래스, 오른쪽에는 엔티티 클래스의 식별자가 갖는 데이터 타입을 명시한다.
(2) 실제로 UserRepository를 사용하는 서비스 계층의 코드를 작성해 보자. 서비스 계층의 경우 기존 JDBC를 사용하던 메서드들에 대해서 요구사항은 동일하지만 Spring Data JPA를 사용하면서 코드가 변경되었다.
(3) 추가적으로 기존 JDBC 리포지토리의 코드를 받아서 사용하던 서비스 계층의 이름은 UserServiceV1으로 변경했다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceV2 {
private final UserRepository userRepository;
@Transactional
public void saveUser(UserCreateRequestDto request) {
userRepository.save(new User(request.getName(), request.getAge()));
}
public List<UserListResponseDto> findAllUser() {
return userRepository.findAll().stream()
.map(UserListResponseDto::new)
.collect(Collectors.toList());
}
@Transactional
public void updateUser(UserUpdateRequestDto request) {
User findUser = userRepository.findById(request.getId())
.orElseThrow(() -> new IllegalStateException("존재하지 않는 회원입니다."));
findUser.updateName(request.getName());
// userRepository.save(findUser);
}
@Transactional
public void deleteUser(UserDeleteRequestDto request) {
User findUser = userRepository.findByName(request.getName());
if (findUser == null) {
throw new IllegalStateException("존재하지 않는 회원입니다.");
}
userRepository.delete(findUser);
}
}
(1) saveUser()
- 유저 엔티티를 새롭게 저장하는 기능이다.
- 요청 DTO에서 받아온 이름, 나이 정보를 기반으로 새로운 유저 엔티티를 save()를 통해 영속화한다.
- 트랜잭션 커밋 시점에 INSERT SQL이 데이터베이스로 전송되어 실제 회원 정보가 저장된다.
(2) findAllUser()
- userRepository에서 findAll() 메서드로 모든 엔티티를 찾아온다.
- 이후 스트림을 돌려서 UserListResponseDto 객체에 매핑시킨다.
- UserListResponseDto을 리스트의 형태로 반환한다.
(3) updateUser()
@Entity
@Getter
public class User {
// 기타 코드 생략
public void updateName(String name) {
this.name = name;
}
}
- findById() 메서드로 이름을 변경할 특정한 유저의 아이디의 식별자를 기준으로 엔티티를 조회해 온다.
- 처음 반환 형태가 Optional이므로 orElseThrow를 통해 식별자에 대한 유저가 없는 경우 예외를 발생시킨다.
- 이후 조회해 온 엔티티 객체를 대상으로 엔티티의 이름 필드를 변경한다.
- save() 메서드를 호출해 merge가 실행되게 함으로써 데이터를 변경하거나 또는 해당 메서드를 주석 처리하고 변경 감지로 트랜잭션이 커밋되는 시점에 UPDATE SQL을 보내서 엔티티의 변경사항을 반영한다.
(4) deleteUser()
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
User findByName(String name);
}
- 추가적으로 findByname(String name)의 경우 기본적인 인터페이스에서 제공하지 않는 메서드이다.
- 따라서 직접 만들어 줘야 하는데, Spring Data JPA의 경우 정해진 형식에 따라 메서드 이름을 지어주면 그에 맞는 쿼리를 자동으로 생성한다.
- findByName이 만들어내는 쿼리는 "SELECT FROM user WHERE name = ?"이다.
- 삭제할 회원의 이름 정보를 기반으로 회원을 엔티티를 찾아온다. 만약 null이라면 해당 이름을 가진 회원이 없는 것이므로 예외를 발생시킨다.
- 그런 경우가 아니라면 이름을 기준으로 탐색한 회원 엔티티를 삭제한다.
4-3. 몇 가지 의문점?
(1) 서비스 계층에서 받아서 사용하는 건 인터페이스이다. 이에 대한 구현체는 어디에?
(2) 스프링 데이터 JPA는 JPA를 보다 더 편리하게 사용해 주기 위해 지원해 주는 기술이고 편리함을 높이기 위해 추상화 레벨을 높였다. 이 덕분에 JPA 인터페이스만 보고도 해당 구현체를 직접 만들어 주고 서비스 계층에서는 이를 주입받아서 쓸 수 있게 된다.
(3) 스프링 데이터 JPA를 사용하는 덕분에 복잡한 JPA 코드를 직접 사용하지 않고 추상화된 기능(CRUD에 대한 공통화, 쿼리 메서드 기능)으로써 사용할 수 있게 되는 것이다.
4-4. 스프링 데이터 JPA 쿼리 메서드 - By 앞에는 다음과 같은 구문들이 들어갈 수 있다.
(1) find
- 반환 타입은 객체가 될 수도 있고, Optional<타입>이 될 수도 있다.
(2) findAll
- 쿼리의 결과물이 N개인 경우 사용한다. 반환 타입은 List<타입>이다.
(3) exists
- 쿼리 결과가 존재하는지 확인한다. 반환 타입은 boolean이다.
(4) count
- 쿼리의 결과 개수를 카운트한다. 반환 타입은 long이다.
(5) And나 Or으로도 조합될 수 있다.
List<User> findAllByNameAndAge(); // select * from user where name = ? and age = ?;
List<User> findAllByNameOrAge(); // select * from user where name = ? or age = ?;
(6) GreaterThan : 초과
(7) GreaterThanEqual : 이상
(8) LessThan : 미만
(9) LessThanEqual : 이하
(10) Between : ~ 사이
(11) StartsWith : ~로 시작하는
(12) EndsWith : ~로 끝나는
5. 개인 회고
(1) Spring Data JPA가 갖는 편리함 덕분에 개발 생산성은 좋아지겠지만 내부적으로 동작하는 메커니즘을 알기 위해선 기존 JPA에 대한 깊은 이해가 필요하다고 생각했다.
(2) 지연 로딩, 영속성 컨텍스트, 연관관계 매핑, fetch join 등 주요 개념을 다시 한 번 정리해 보자.
6. Reference
(1) 관련 레퍼런스는 인프런에서 활동하고 계시는 최태현 강사님의 온라인 강의를 듣고 내용을 정리했습니다.
※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.
개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다
'기록, 회고 > InFlearn Warming-up 0기 BE' 카테고리의 다른 글
[8일 차] - 내용 정리(트랜잭션, JPA 영속성 컨텍스트), 개인 회고 (0) | 2024.02.26 |
---|---|
[7일 차] - 과제 수행 : Spring Data JPA 사용하기 (0) | 2024.02.26 |
[6일 차] - 과제 수행 : Controller - Service - Repository 분리 (0) | 2024.02.23 |
[6일 차] - 내용 정리, 개인 회고 (0) | 2024.02.23 |
[5일 차] - 과제 수행 : 클린 코드, 코드 리팩토링 (0) | 2024.02.21 |
댓글