🐯[TIL] 250714-031

byoΒ·2025λ…„ 7μ›” 29일

πŸ’« Java

🏁 Movie-Review with JPA and Pageable

🧠 model

🧠 Movie

@Entity
@Table(name="movies")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Movie {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    private Integer releaseYear;

    @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true) πŸ“Œ
    @JsonManagedReference πŸ“Œ
    private List<Review> reviews = new ArrayList<>();
}
ν¬μΈνŠΈμ„€λͺ…
@OneToMany(mappedBy = "movie")Movie 1κ°œλŠ” μ—¬λŸ¬ Reviewλ₯Ό κ°€μ§ˆ 수 μžˆλ‹€λŠ” 의미. mappedByλŠ” Review 클래슀의 movie ν•„λ“œλ₯Ό μ°Έμ‘°ν•˜μ—¬ μ—°κ΄€ κ΄€κ³„μ˜ 주인이 μ•„λ‹˜μ„ λͺ…μ‹œ. 즉, DB의 μ™Έλž˜ ν‚€λŠ” Review ν…Œμ΄λΈ”μ— 생성됨.
orphanRemoval = truereviews λ¦¬μŠ€νŠΈμ—μ„œ ν•­λͺ©μ„ μ œκ±°ν•˜λ©΄ DBμ—μ„œλ„ μžλ™ μ‚­μ œλ¨. 단, CascadeType.ALL이 ν•¨κ»˜ μžˆμ–΄μ•Ό 적용됨.
cascade = CascadeType.ALLMovie μ—”ν‹°ν‹°μ˜ 생λͺ…μ£ΌκΈ° λ³€ν™”(μ €μž₯, μˆ˜μ •, μ‚­μ œ λ“±)λ₯Ό Review에도 μ „νŒŒν•¨. 예: Movie μ‚­μ œ μ‹œ μ—°κ²°λœ Review도 ν•¨κ»˜ μ‚­μ œλ¨.
@JsonManagedReference직렬화 μ‹œ μˆœν™˜ μ°Έμ‘°λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•œ μ–΄λ…Έν…Œμ΄μ…˜. 이 μͺ½μ΄ 직렬화 λŒ€μƒμ΄λ©°, Review의 @JsonBackReference와 짝을 이룸.

🧠 Review

@Entity
@Table(name = "reviews")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String reviewer;

    @Min(1)
    @Max(5)
    private Integer rating;

    @Column(columnDefinition = "TEXT")
    private String comment;

    @ManyToOne(fetch = FetchType.LAZY) πŸ“Œ
    @JoinColumn(name = "movie_id", nullable = false) πŸ“Œ
    @JsonBackReference πŸ“Œ
    private Movie movie;
}
ν¬μΈνŠΈμ„€λͺ…
@ManyToOne(fetch = FetchType.LAZY)μ—¬λŸ¬ λ¦¬λ·°λŠ” ν•˜λ‚˜μ˜ μ˜ν™”μ— μ†ν•œλ‹€λŠ” 관계. μ™Έλž˜ ν‚€κ°€ Review ν…Œμ΄λΈ”μ— μ‘΄μž¬ν•¨. LAZY μ„€μ • μ‹œ μ‹€μ œ μ‚¬μš©ν•  λ•Œλ§Œ Movie μ—”ν‹°ν‹°λ₯Ό DBμ—μ„œ μ‘°νšŒν•¨.
@JoinColumn(name = "movie_id", nullable = false)Review ν…Œμ΄λΈ”μ— 생성될 μ™Έλž˜ ν‚€ 컬럼 이름을 μ§€μ •. nullable = false둜 ν•„μˆ˜ κ΄€κ³„μž„μ„ λͺ…μ‹œ.
@JsonBackReference직렬화 μ‹œ λ¬΄ν•œ 루프 λ°©μ§€λ₯Ό μœ„ν•΄ μ‚¬μš©. @JsonManagedReferenceκ°€ μžˆλŠ” Movie μͺ½λ§Œ JSON 응닡에 포함됨.

πŸ“Œ 직렬화(Serialization)λž€?

  • 객체(Object)λ₯Ό λ°”μ΄νŠΈ(byte) ν˜•νƒœλ‘œ λ³€ν™˜ν•˜λŠ” 과정을 의미.
    즉, 객체λ₯Ό λ„€νŠΈμ›Œν¬ μ „μ†‘μ΄λ‚˜ 파일 μ €μž₯이 κ°€λŠ₯ν•œ ν˜•νƒœλ‘œ λ§Œλ“œλŠ” 것.
  • λ°˜λŒ€ κ°œλ…: 역직렬화(Deserialization)
    λ°”μ΄νŠΈλ‚˜ JSON λ“±μ˜ 데이터λ₯Ό λ‹€μ‹œ 객체둜 λ³€ν™˜ν•˜λŠ” 것

πŸ“¦ dto

πŸ“¦ MovieDto

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MovieDto {
    @NotBlank(message = "제λͺ©μ„ μž…λ ₯ν•˜μ„Έμš”")
    private String title;

    @NotNull(message = "κ°œλ΄‰ 연도λ₯Ό μž…λ ₯ν•˜μ„Έμš”")
    private Integer releaseYear;
}

πŸ“¦ ReviewDto

@Getter
@Setter
public class ReviewDto {
    private String reviewer;

    @NotNull(message = "평점을 μž…λ ₯ν•˜μ„Έμš”")
    @Min(value = 1, message = "평점은 1~5 사이여야 ν•©λ‹ˆλ‹€")
    @Max(value = 5, message = "평점은 1~5 사이여야 ν•©λ‹ˆλ‹€")
    private Integer rating;

    @NotBlank(message = "λŒ“κΈ€μ„ μž…λ ₯ν•˜μ„Έμš”")
    private String comment;
}

🧩 service

🧩 MovieService

@Service
@RequiredArgsConstructor
@Transactional πŸ“Œ
public class MovieService {
    private final MovieRepository movieRepository;

    public Page<Movie> getAll(Pageable pageable) {
        return movieRepository.findAll(pageable);
    }

    public Movie getById(Long id) {
        return movieRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("μ˜ν™”κ°€ μ—†μŠ΅λ‹ˆλ‹€: " + id));
    }

    public Movie create(Movie movie) {
        return movieRepository.save(movie);
    }

    public Movie update(Long id, Movie movie) {
        Movie existMovie = getById(id); πŸ“Œ

        existMovie.setTitle(movie.getTitle());
        existMovie.setReleaseYear(movie.getReleaseYear());

        return movieRepository.save(existMovie);
    }

    public void delete(Long id) {
        movieRepository.delete(getById(id)); πŸ“Œ
    }
}
ν¬μΈνŠΈμ„€λͺ…
@Transactional 클래슀 λ‹¨μœ„ μ μš©μ „μ²΄ λ©”μ„œλ“œμ—μ„œ νŠΈλžœμž­μ…˜μ΄ μžλ™ μ μš©λ˜μ–΄, μ˜ˆμ™Έ λ°œμƒ μ‹œ λ‘€λ°± 처리됨.
getById() λ‚΄λΆ€ μž¬μ‚¬μš©updateλ‚˜ deleteμ—μ„œ 쀑볡 μ½”λ“œλ₯Ό λ°©μ§€ν•˜κ³  μ˜ˆμ™Έ 처리 일관성을 μœ μ§€.

🧩 ReviewService

@Service
@RequiredArgsConstructor
@Transactional
public class ReviewService {
    private final ReviewRepository reviewRepository;
    private final MovieRepository movieRepository;

    public Page<Review> getByMovie(Long movieId, Pageable pageable) {
        return reviewRepository.findByMovieId(movieId, pageable);
    }

    public Review getById(Long id) {
        return reviewRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("리뷰가 μ—†μŠ΅λ‹ˆλ‹€: " + id));
    }

    public Review create(Long movieId, Review review) {
        Movie movie = movieRepository.findById(movieId)
                .orElseThrow(() -> new NoSuchElementException("리뷰가 μ—†μŠ΅λ‹ˆλ‹€: " + movieId));

        review.setMovie(movie); πŸ“Œ

        return reviewRepository.save(review);
    }

    public Review update(Long id, Review review) { πŸ“Œ
        Review existReview = getById(id);

        existReview.setReviewer(review.getReviewer());
        existReview.setRating(review.getRating());
        existReview.setComment(review.getComment());

        return reviewRepository.save(existReview);
    }

    public void delete(Long id) {
        reviewRepository.delete(getById(id));
    }
}
ν¬μΈνŠΈμ„€λͺ…
review.setMovie(movie)리뷰 등둝 μ‹œ λ°˜λ“œμ‹œ λΆ€λͺ¨ μ—”ν‹°ν‹°(Movie)λ₯Ό μ„€μ •ν•΄μ•Ό μ™Έλž˜ ν‚€κ°€ μ œλŒ€λ‘œ μ €μž₯됨.
Review update(...) 방식뢀뢄 μˆ˜μ •μ΄ μ•„λ‹Œ 전체 ν•„λ“œ ꡐ체 λ°©μ‹μ΄λ―€λ‘œ, null ν•„λ“œκ°€ μž…λ ₯되면 κΈ°μ‘΄ 값이 μ§€μ›Œμ§ˆ 수 있음.

πŸ“‘ controller

πŸ“‘ MovieController

@RestController
@RequestMapping("/api/movies")
@RequiredArgsConstructor
public class MovieController {
    private final MovieService movieService;

    @GetMapping
    public Page<Movie> list(
           @RequestParam(defaultValue = "0") int page,
           @RequestParam(defaultValue = "10") int size
    ) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending());
        return movieService.getAll(pageable);
    }

    @GetMapping("/{id}")
    public Movie get(@PathVariable Long id) {
        return movieService.getById(id);
    }

    @PostMapping
    public Movie create(@Valid @RequestBody MovieDto movieDto) {
        Movie movie = new Movie();

        movie.setTitle(movieDto.getTitle());
        movie.setReleaseYear(movieDto.getReleaseYear());

        return movieService.create(movie);
    }

    @PutMapping("/{id}")
    public Movie update(@PathVariable Long id, @Valid @RequestBody MovieDto movieDto) {
        Movie movie = new Movie();

        movie.setTitle(movieDto.getTitle());
        movie.setReleaseYear(movieDto.getReleaseYear());

        return movieService.update(id, movie);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        movieService.delete(id);

        return ResponseEntity.noContent().build();
    }
}

πŸ“‘ ReviewController

@RestController
@RequiredArgsConstructor
public class ReviewController {
    private final ReviewService reviewService;

    @GetMapping("/api/movies/{movieId}/reviews")
    public Page<Review> listByMovie(
            @PathVariable Long movieId,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size
    ) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("id").descending());
        return reviewService.getByMovie(movieId, pageable);
    }

    @PostMapping("/api/movies/{movieId}/reviews")
    public Review add(
            @PathVariable Long movieId,
            @Valid @RequestBody ReviewDto reviewDto
    ) {
        Review review = new Review();
        review.setReviewer(reviewDto.getReviewer());
        review.setRating(reviewDto.getRating());
        review.setComment(reviewDto.getComment());

        return reviewService.create(movieId, review);
    }

    @PutMapping("/api/reviews/{id}")
    public Review update(@PathVariable Long id, @Valid @RequestBody ReviewDto ReviewDto) {
        Review review = new Review();

        review.setReviewer(ReviewDto.getReviewer());
        review.setRating(ReviewDto.getRating());
        review.setComment(ReviewDto.getComment());

        return reviewService.update(id, review);
    }

    @DeleteMapping("/api/reviews/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        reviewService.delete(id);

        return ResponseEntity.noContent().build();
    }
}
profile
πŸ—‚οΈ hamstern

0개의 λŒ“κΈ€