✍ 오늘의 학습 내용
즉시 로딩과 지연 로딩이란❓
1) 즉시 로딩 : 필요하지 않은 속성까지도 한번에 조회하는 것
➡ 설정법 : @[관계 / ex) OnetoMany](fetch = fetch.EAGER)
2) 지연 로딩 : 필요한 것만 조회하고 필요하지 않은 것은 나중에 필요할 때
다시 조회하는 것
➡ 설정법 : @[관계 / ex) OnetoMany](fetch = fetch.LAZY)
아래에서 예를 들어 설명해보겠다.
Member 엔티티와 Team 엔티티가 N:1
관계로 형성되어 있고, 안에는 데이터를 미리 채워놨다.
✅ Member 테이블
✅ Team 테이블
여기서 Member 엔티티에 즉시로딩을 설정해준 뒤 Member 테이블에 id와 username만 조회해본다고 하자.
// Member 클래스에서 작성
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "Team_id")
private Team4 team;
// Main 클래스
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"hibernateproject");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member4 member4 = em.find(Member4.class, 1);
System.out.println(member4.getId());
System.out.println(member4.getUsername());
tx.commit();
}
}
이랬을때, 즉시로딩은 단순히 Member 테이블에서 id 와 username 만 가져오는데도 Team 테이블과 LEFT JOIN 을 아래와 같이 자동으로 실시한다.
이처럼 즉시 로딩은 내가 필요한 데이터가 굳이 테이블 조인을 하지 않아도 조회할 수 있지만 조인을 해버린다.
반대로 이제는 지연로딩을 설정해준 뒤 똑같이 Member 테이블에서 id와 username을 조회해보고 이어서 Team_id 까지 조회해본다고 하자.
// Member 클래스에서 작성
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "Team_id")
private Team4 team;
// Main 클래스
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"hibernateproject");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
Member4 member4 = em.find(Member4.class, 1);
System.out.println(member4.getId());
System.out.println(member4.getUsername());
System.out.println(member4.getTeam().getName());
tx.commit();
}
}
위처럼 지연 로딩을 걸어주면 테이블 조인을 하지 않고, 먼저 Member 테이블에서 id와 username을 조회하고 이어서 다시 한번더 Team_id를 조회하는 것을 아래와 같이 볼 수 있을 것이다.
지연 로딩은 이렇게 먼저 필요한 것만 조회하고 필요하지 않은 것은 나중에 필요해지면 다시 조회한다.
이때 그럼 나중에 필요한 데이터를 어디에 저장해놓고 가져오는 것일까❓라는 의문을 품을 수 있다.
그것이 바로 프록시 객체이다. 위의 예를 들었을때 처음에 필요한 것은 Member 테이블의 id
와 username
이 필요해서 DB에서 찾아서 1차 캐시에 저장을 한다.
이때, Team_id는 조회를 하지 않았기 때문에 1차 캐시에 저장이 안되는데, 나중에 필요할 수 있는 것이다.
따라서 이러한 데이터를 프록시 객체에 저장시켜놨다가 나중에 필요해서 조회할때 이 프록시 객체에서 저장되어 있는 데이터를 불러오게 되는 것이다.
🐯 지연로딩과 즉시로딩 실습해보기
- User(회원) 엔티티에는 id, pw, name 이 있다.
- Order(주문) 엔티티에는 id, productName 이 있다.
- Grade(회원 등급) 엔티티에는 id, grade 가 있다.
- 회원과 회원등급은
N:1
관계이다.- 회원과 주문은
1:N
관계이다.
- 회원의 정보를 조회하면 회원등급까지 같이 조회된다. ( 즉시 로딩 )
- 회원의 정보를 조회하고, 필요 시 해당회원의 주문정보를 조회한다. ( 지연 로딩 )
✅ User 엔티티
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String pw;
private String name;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "User_id")
private List<Order> orderList = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "Grade_id")
private Grade grade;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getPw() {
return pw;
}
public void setPw(String pw) {
this.pw = pw;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Order> getOrderList() {
return orderList;
}
public void setOrderList(List<Order> orderList) {
this.orderList = orderList;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
}
✅ Order 엔티티
@Entity
@Table(name="`Order`")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String productName;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
}
✅ Grade 엔티티
@Entity
public class Grade {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String grade;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
}
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory(
"hibernateproject");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
// 데이터 삽입 코드
// for (int i = 0; i < 5; i++) {
// Grade grade = new Grade();
// grade.setGrade("등급"+(i+1));
// for (int j = 1; j <= 10; j++) {
// User user = new User();
// user.setName("회원"+(i*10+j));
// user.setPw("비밀번호" + (i*10+j));
// user.setGrade(grade);
// em.persist(user);
//
// for (int k=1; k<=3; k++) {
// Order order = new Order();
// order.setProductName("상품" + (i*10+k));
// em.persist(order);
// user.getOrderList().add(order);
// }
// }
// em.persist(grade);
// }
User user1 = em.find(User.class, 30);
System.out.println("회원명 : " + user1.getName());
System.out.println("비밀번호 : " + user1.getPw());
System.out.println("등급 : " + user1.getGrade().getGrade());
User user2 = em.find(User.class, 2);
System.out.println("회원명 : " + user2.getName());
System.out.println("비밀번호 : " + user2.getPw());
System.out.println("등급 : " + user2.getGrade().getGrade());
System.out.print("주문목록 : ");
for(Order order : user2.getOrderList()) {
System.out.print(order.getProductName() + " ");
}
tx.commit();
}
}
LEFT JOIN
하여 쿼리문이 1번 실행하여 결과가 조회된다.🐷 Spring Data JPA 실습하기
이제까지 JPA에 대한 이론과 실습을 진행하였고, 아래부터는 Spring Data JPA 에 대한 실습을 진행하겠다.
Spring Data JPA 에는 JPA 와 마찬가지로 Entity(테이블과 직접적으로 매핑되는 클래스)가 있고 이전에 JDBC로 실습할때 생성하였던 DAO 클래스 대신에 Entity 객체들을 처리하는 기능을 가진 Repository 인터페이스가 있다.
Spring Data JPA 를 사용하기 위해서는 pom.xml
에 아래의 라이브러리를 추가해주고, application.yml
파일에도 설정을 추가해줘야된다.
// ✅ pom.xml에 작성
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
// ✅ application.yml에 작성 ( 들여쓰기 매우 중요!! )
spring:
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
show-sql: true
본격적인 Spring Data JPA 실습 내용
1) 영화( 영화 이름, 가격 ) CRUD 구현
2) 영화에 대한 리뷰 기능( 별점, 내용 ) CRUD 구현
3) 영화와 리뷰는 1:N
관계로 양방향 설정
➡ 작성은 DB와 가까운 순으로 작성하는 것이 좋다.
따라서 Repository - DTO(전송 객체) - Service - Controller 순으로 작성하면 된다.
먼저 영화에 대한 CRUD 구현 결과이다.
// ✅ MovieRepository 인터페이스
@Repository
public interface MovieRepository extends JpaRepository<Movie, Integer> {
}
// ✅ MovieDto 클래스
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MovieDto {
private Integer id;
private String name;
private Integer price;
private List<MovieReviewDto> movieReviewDtoList = new ArrayList<>();
}
// ✅ MovieService 클래스
@Service
public class MovieService {
MovieRepository movieRepository;
public MovieService(MovieRepository movieRepository) {
this.movieRepository = movieRepository;
}
// 💻 CREATE 기능
public void create(MovieDto movieDto) {
movieRepository.save(Movie.builder()
.name(movieDto.getName())
.price(movieDto.getPrice())
.build());
}
// 💻 LIST 기능 (전체 목록 조회)
public List<MovieDto> list() {
List<Movie> result = movieRepository.findAll();
List<MovieDto> movieDtos = new ArrayList<>();
for(Movie movie : result) {
List<MovieReviewDto> movieReviewDtos = new ArrayList<>();
List<MovieReview> movieReviewList = movie.getMovieReviewList();
for(MovieReview movieReview : movieReviewList) {
MovieReviewDto movieReviewDto = MovieReviewDto.builder()
.id(movieReview.getId())
.star(movieReview.getStar())
.text(movieReview.getText())
.build();
movieReviewDtos.add(movieReviewDto);
}
MovieDto movieDto = MovieDto.builder()
.id(movie.getId())
.name(movie.getName())
.price(movie.getPrice())
.movieReviewDtoList(movieReviewDtos)
.build();
movieDtos.add(movieDto);
}
return movieDtos;
}
// 💻 READ 기능 (한개만 조회)
public MovieDto read(Integer id) {
Optional<Movie> result = movieRepository.findById(id);
if (result.isPresent()) {
Movie movie = result.get();
List<MovieReviewDto> movieReviewDtos = new ArrayList<>();
for(MovieReview movieReview : movie.getMovieReviewList()) {
movieReviewDtos.add(MovieReviewDto.builder()
.id(movieReview.getId())
.star(movieReview.getStar())
.text(movieReview.getText())
.build()
);
}
return MovieDto.builder()
.id(movie.getId())
.price(movie.getPrice())
.name(movie.getName())
.movieReviewDtoList(movieReviewDtos)
.build();
} else {
return null;
}
}
// 💻 UPDATE 기능
public void update(MovieDto movieDto) {
movieRepository.save(Movie.builder()
.id(movieDto.getId())
.name(movieDto.getName())
.price(movieDto.getPrice())
.build());
}
// 💻 DELETE 기능
public void delete(Integer id) {
movieRepository.delete(Movie.builder().id(id).build());
}
}
// ✅ MovieController 클래스
@RestController
@RequestMapping("/movie")
public class MovieController {
MovieService movieService;
public MovieController(MovieService movieService) {
this.movieService = movieService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(MovieDto movieDto) {
movieService.create(movieDto);
return ResponseEntity.ok().body("생성");
}
@RequestMapping(method = RequestMethod.GET, value = "/list")
public ResponseEntity list() {
return ResponseEntity.ok().body(movieService.list());
}
@RequestMapping(method = RequestMethod.GET, value = "/read")
public ResponseEntity read(Integer id) {
return ResponseEntity.ok().body(movieService.read(id));
}
@RequestMapping(method = RequestMethod.PATCH, value = "/update")
public ResponseEntity update(MovieDto movieDto) {
movieService.update(movieDto);
return ResponseEntity.ok().body("수정");
}
@RequestMapping(method = RequestMethod.DELETE, value = "/delete")
public ResponseEntity delete(Integer id) {
movieService.delete(id);
return ResponseEntity.ok().body("삭제");
}
}
// ✅ MovieReviewRepository 인터페이스
@Repository
public interface MovieReviewRepository extends JpaRepository<MovieReview, Integer> {
}
// ✅ MovieReviewDto 클래스
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MovieReviewDto {
private Integer id;
private Integer star;
private String text;
private MovieDto movieDto;
}
// ✅ MovieReviewService 클래스
@Service
public class MovieReviewService {
MovieReviewRepository movieReviewRepository;
public MovieReviewService(MovieReviewRepository movieReviewRepository) {
this.movieReviewRepository = movieReviewRepository;
}
// 💻 CREATE 기능
public void create(Integer movieid, MovieReviewDto movieReviewDto) {
movieReviewRepository.save(MovieReview.builder()
.movie(Movie.builder().id(movieid).build())
.star(movieReviewDto.getStar())
.text(movieReviewDto.getText())
.build());
}
// 💻 LIST 기능 (전체 목록 조회)
public List<MovieReviewDto> list() {
List<MovieReview> result = movieReviewRepository.findAll();
List<MovieReviewDto> movieReviewDtos = new ArrayList<>();
for (MovieReview movieReview:result) {
Movie movie = movieReview.getMovie();
MovieDto movieDto = MovieDto.builder()
.id(movie.getId())
.name(movie.getName())
.price(movie.getPrice())
.build();
MovieReviewDto movieReviewDto = MovieReviewDto.builder()
.id(movieReview.getId())
.star(movieReview.getStar())
.text(movieReview.getText())
.movieDto(movieDto)
.build();
movieReviewDtos.add(movieReviewDto);
}
return movieReviewDtos;
}
// 💻 READ 기능 (한개만 조회)
public MovieReviewDto read(Integer id) {
Optional<MovieReview> result = movieReviewRepository.findById(id);
if(result.isPresent()) {
MovieReview movieReview = result.get();
return MovieReviewDto.builder()
.id(movieReview.getId())
.star(movieReview.getStar())
.text(movieReview.getText())
.movieDto(MovieDto.builder()
.id(movieReview.getMovie().getId())
.name(movieReview.getMovie().getName())
.price(movieReview.getMovie().getPrice())
.build())
.build();
} else {
return null;
}
}
// 💻 UPDATE 기능
public void update(MovieReviewDto movieReviewDto) {
Optional<MovieReview> result = movieReviewRepository.findById(movieReviewDto.getId());
if(result.isPresent()) {
MovieReview movieReview = result.get();
movieReview.setStar(movieReviewDto.getStar());
movieReview.setText(movieReviewDto.getText());
movieReviewRepository.save(movieReview);
}
}
// 💻 DELETE 기능
public void delete(Integer id) {
movieReviewRepository.delete(MovieReview.builder().id(id).build());
}
}
// ✅ MovieReviewController 클래스
@RestController
@RequestMapping("/movie_review")
public class MovieReviewController {
MovieReviewService movieReviewService;
public MovieReviewController(MovieReviewService movieReviewService) {
this.movieReviewService = movieReviewService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(Integer movieid, MovieReviewDto movieReviewDto) {
movieReviewService.create(movieid, movieReviewDto);
return ResponseEntity.ok().body("리뷰 작성 성공");
}
@RequestMapping(method = RequestMethod.GET, value = "/list")
public ResponseEntity list() {
return ResponseEntity.ok().body(movieReviewService.list());
}
@RequestMapping(method = RequestMethod.GET, value = "/read")
public ResponseEntity read(Integer id) {
return ResponseEntity.ok().body(movieReviewService.read(id));
}
@RequestMapping(method = RequestMethod.PATCH, value = "/update")
public ResponseEntity update(MovieReviewDto movieReviewDto) {
movieReviewService.update(movieReviewDto);
return ResponseEntity.ok().body("리뷰 수정 성공");
}
@RequestMapping(method = RequestMethod.DELETE, value = "/delete")
public ResponseEntity delete(Integer id) {
movieReviewService.delete(id);
return ResponseEntity.ok().body("리뷰 삭제 성공");
}
}
오늘의 느낀점 👀
오늘은 Spring Data JPA를 본격적으로 배워본 시간이었다. 먼저 얘기하고 싶은건 위의 코드를 구현하는것에서 가장 어려웠던것은 조회(LIST, READ)
였다. 실제로 서비스를 운영하는 기업의 DB에서 가장 많이 일어날 수 있는 것이 조회인데, 그래서 그런지 유독 구현하는것도 복잡했다. 하지만 처음이라 그런거라고 생각한다. 수업 들을때는 어려웠지만, 집에 와서 복습해보면서 다시 한번 코드를 짜보니 이제는 왜 그렇게 짜는지 눈에 보이기 시작했다.
사실 코드는 몇번이고 반복해가면서 익숙해질때까지 백지 상태에서 완벽히 구현할때까지 반복하면 익숙해질 것이다. 그보다 중요한건 개념이라고 생각한다. 어제부터 오늘까지 배운 ORM 그리고 JPA 에 대한 명확한 개념을 숙지하는것이 나에게 가장 필요한 부분이 아닐까 싶다.
특히, 여기서 발생할 수 있는 문제 중에 대표적으로 n+1 문제
가 있다고 하는데 이것은 내일 수업때 다뤄볼 예정이다. 이부분까지 포함하여 JPA 에 대한 완벽한 개념을 숙지한다면 한단계 더 성장한 내가 될 것이다.