GuestBook 프로젝트 (마지막) (Reply 댓글 추가 / 수정 / 삭제 적용, @RestController)
GuestBook 프로젝트 (마지막) (Reply 댓글 추가 / 수정 / 삭제 적용, @RestController)
GuestBook 프로젝트 (9) (QuerydslRepositorySupport, 검색 (search) 처리 및 적용) GuestBook 프로젝트 (9) (QuerydslRepositorySupport, 검색 (search) 처리 및 적용) GuestBook 프로젝트 (8) (조회 / 수정 / 삭제 처리, Controller 화
soohykeee.tistory.com
해당 movieReview 프로젝트는 위의 guestbook 프로젝트를 한번 구현해 보고, 다대다 관계 및 파일 업로드, Security를 적용 테스트 해볼 프로젝트이다.
다대다 관계의 대표적인 예시는 아래와 같다.
- 학생 - 수업 : 한 명의 학생은 여러 수업에 참여하고, 하나의 수업은 여러 학생이 수강한다.
- 상품 - 카테고리 : 하나의 상품은 여러 카테고리에 속하고, 하나의 카테고리는 여러 상품을 가지고 있다.
- 상품 - 회원 : 하나의 상품은 여러 회원이 구매할 수 있고, 한 명의 회원은 여러 상품을 구매할 수 있다.
하지만 다대다 관계의 경우는 실제 테이블로 설계를 할 수 없다. 위의 경우 들을 생각해보면, 학생 - 수업의 경우 수강 테이블을 따로 두어 학생 - 수강, 수강 - 수업 으로 일대다 관계를 2가지로 두어 해소가 가능하다. 이러한 테이블을 매핑테이블이라고 부른다. 매핑 테이블의 특징은 아래와 같다.
- 매핑 테이블의 작성 이전에 다른 테이블들이 먼저 존재해야만 한다.
- 매핑 테이블은 주로 명사가 아닌 동사나 히스토리에 대한 데이터를 보관하는 용도이다.
- 매핑 테이블은 중간에서 양쪽의 PK를 참조하는 형태로 사용된다.
따라서 해당 MovieReview 프로젝트에서 영화와 회원은 다대다 관계이지만, 매핑테이블로 리뷰를 두어서 영화 - 리뷰, 리뷰 - 회원 으로 나누어 구현해야 한다.
movieReview 프로젝트 생성
spring의 버전 갱신으로 원활환 프로젝트가 진행되지 않을 수 있기에, 이전의 guestbook에서 설정해주었던 gradle과 같은 버전으로 수정을 해주었다.
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'org.springframework.boot' version '2.7.5'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
id 'java'
id 'war'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
implementation "com.querydsl:querydsl-apt:${queryDslVersion}"
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.8'
compile group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-java8time'
}
tasks.named('test') {
useJUnitPlatform()
}
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
jpa = true
querydslSourcesDir = querydslDir
}
sourceSets {
main.java.srcDir querydslDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
querydsl.extendsFrom compileClasspath
}
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
enabled: true
force: true
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/movieReview
username: sa
password:
jpa:
properties:
hibernate:
format_sql: true
hibernate:
ddl-auto: create #create update none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
Entity 생성
위와 같이 프로젝트 기본 설정을 끝낸 후, entity 디렉토리를 생성하여 하위에 BaseEntity, Movie, MovieImage, Member 클래스를 생성해준다. BaseEntity 클래스는 Gusetbook 에서 사용했던 것과 동일하다.
또한 프로젝트 생성시 만들어진 movieReviewApplication 클래스에 @EnableJpaAuditing 어노테이션을 추가해준다.
package com.example.moviereview.entity;
import lombok.*;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class Movie extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mno;
private String title;
}
package com.example.moviereview.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = "movie")
public class MovieImage extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long inum;
private String uuid;
private String imgName;
private String path;
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
}
package com.example.moviereview.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Table(name = "m_member")
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long mid;
private String email;
private String pw;
private String nickname;
}
여기서 사용하는 MovieImage 클래스는 추후에 사용할 이미지에 대한 정보를 기록하는 클래스이다. java.util.UUID 를 이용하여 고유한 번호를 생성해서 사용할 것이고, 이미지의 저장 경로(path)는 년/월/일 폴더 구조를 의미하게 된다.
Member 클래스는 기존의 guestbook과 동일한 역할이지만, 회원과 로그인에 대한 처리는 Security를 적용해줄것이다.
앞서 다대다를 설명하며 말했듯이, movie와 member는 review라는 매핑테이블을 사이에 두고 일대다로 연결이 될 것이다.
package com.example.moviereview.entity;
import lombok.*;
import javax.persistence.*;
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString(exclude = {"movie", "member"})
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reviewnum;
@ManyToOne(fetch = FetchType.LAZY)
private Movie movie;
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
private int grade;
private String text;
}
위와 같이 entity 클래스들을 생성하고 실행해주면, H2 database에 정상적으로 테이블이 생성된것을 볼 수 있다. 만일 H2 database실행 시 생성이 되지 않는 오류가 발생한다면 아래에 작성했던 링크를 참조해보면 된다.
https://soohykeee.tistory.com/7
GuestBook 프로젝트 (2) (Querydsl Test Code, DTO, Service, ServiceImpl)
GuestBook 프로젝트 (1) (프로젝트 구조, gradle, application, querydsl 설정) GuestBook 프로젝트 (1) (프로젝트 구조, gradle, application, querydsl 설정) 코드로 배우는 스프링 부트 웹 프로젝트 - 남가람북스 코드로
soohykeee.tistory.com
Repository 및 Test 작성
repository 디렉토리를 생성 후 하위에 MovieRepository, MovieImageRepository 인터페이스를 생성해준다.
package com.example.moviereview.repository;
import com.example.moviereview.entity.Movie;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
package com.example.moviereview.repository;
import com.example.moviereview.entity.MovieImage;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MovieImageRepository extends JpaRepository<MovieImage, Long> {
}
이제 정상적으로 저장이 되는지 확인하기 위해 MovieRepositoryTests 클래스를 생성하여 테스트하는 코드를 작성해야한다. 영화와 영화이미지들은 같은 시점에 insert 처리가 되어야 한다. 때문에 Movie 객체가 있어야 MovieImage를 생성할 수 있으므로 Movie 객체 먼저 save()를 해준다. 해당 Movie가 save() 된 후에 PK에 해당하는 mno 값이 자동으로 할당이 되기에, 이를 이용하여 MovieImage를 추가해준다.
package com.example.moviereview.repository;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.entity.MovieImage;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
import java.util.stream.IntStream;
@SpringBootTest
public class MovieRepositoryTests {
@Autowired
private MovieRepository movieRepository;
@Autowired
private MovieImageRepository movieImageRepository;
@Commit
@Transactional
@Test
public void insertMovies() {
IntStream.rangeClosed(1,100).forEach(i -> {
Movie movie = Movie.builder().title("Movie..." + i).build();
movieRepository.save(movie);
int count = (int) (Math.random() * 5) + 1;
for (int j = 0; j < count; j++) {
MovieImage movieImage = MovieImage.builder()
.uuid(UUID.randomUUID().toString())
.movie(movie)
.imgName("test" + j + ".jpg")
.build();
movieImageRepository.save(movieImage);
}
});
}
}
Member는 security를 적용시켜줄 것이지만, 우선 Review를 테스트하기 위해서 우선적으로 데이터를 생성해준다.
package com.example.moviereview.repository;
import com.example.moviereview.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
package com.example.moviereview.repository;
import com.example.moviereview.entity.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.stream.IntStream;
@SpringBootTest
public class MemberRepositoryTests {
@Autowired
private MemberRepository memberRepository;
@Test
public void insertMembers() {
IntStream.rangeClosed(1, 100).forEach(i -> {
Member member = Member.builder()
.email("r" + i + "@zerock.org")
.pw("1111")
.nickname("reviewer" + i)
.build();
memberRepository.save(member);
});
}
}
package com.example.moviereview.repository;
import com.example.moviereview.entity.Review;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReviewRepository extends JpaRepository<Review, Long> {
}
package com.example.moviereview.repository;
import com.example.moviereview.entity.Member;
import com.example.moviereview.entity.Movie;
import com.example.moviereview.entity.Review;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.stream.IntStream;
@SpringBootTest
public class ReviewRepositoryTests {
@Autowired
private ReviewRepository reviewRepository;
@Test
public void insertReviews() {
IntStream.rangeClosed(1, 200).forEach(i -> {
// 영화 번호
Long mno = (long) (Math.random() * 100) + 1;
// 리뷰어 번호
Long mid = ((long) (Math.random() * 100) + 1);
Member member = Member.builder().mid(mid).build();
Review movieReview = Review.builder()
.member(member)
.movie(Movie.builder().mno(mno).build())
.grade((int) (Math.random() * 5) + 1)
.text("이 영화에 대한 느낌..." + i)
.build();
reviewRepository.save(movieReview);
});
}
}