안녕하세요! 저번 시간에는 AWS 가입과 기본 세팅을 진행했습니다.
오늘은 S3Uploader와 Review 엔터티를 개발해 리뷰 Upload 기능을 추가해보겠습니다.
바로 시작하도록 하겠습니다.
DB Review Table 생성
우선 DB에 리뷰를 저장할 테이블을 생성해주었다.
Review Table
create table REVIEW(
reviewUid INT PRIMARY KEY,
reviewerUid VARCHAR(100),
parkCode VARCHAR(200),
reviewerNickName VARCHAR(50),
reviewImageUrl1 VARCHAR(255) NULL,
reviewImageUrl2 VARCHAR(255) NULL,
reviewImageUrl3 VARCHAR(255) NULL,
reviewImageUrl4 VARCHAR(255) NULL,
reviewImageUrl5 VARCHAR(255) NULL,
reviewText TEXT,
reviewDate VARCHAR(255),
likeCount SMALLINT,
reviewRate TINYINT CHECK (reviewRate IN (1,3,5)),
FOREIGN KEY (reviewerUid) REFERENCES BASE_USER(uid),
FOREIGN KEY (reviewerNickName) REFERENCES BASE_USER(nickname),
FOREIGN KEY (parkCode) REFERENCES PARK_DATA(prkplceNo)
);
reviewUid : Review의 UID를 저장하는 컬럼으로 주식별자이다.
reviewerUid : Reviewer의 UID를 저장하는 컬럼으로 BASE_USER 테이블의 uid 와 연관되어 있다.
parkCode : Review를 남긴 주차장의 parkcode로 PARK_DATA 테이블의 prkplceNo와 연관되어 있다.
reviewerNickName : Reviewer의 nickname을 저장하는 컬럼으로 BASE_USER 테이블의 nickname과 연관되어 있다.
reviewImageUrl1~5 : S3에 저장된 이미지의 url을 저장하는 컬럼이다.
reviewText : 남긴 ReviewText를 저장하는 컬럼이다.
reviewData : Review를 남긴 날짜와 시간을 저장하는 컬럼이다.
likeCount : Review가 받은 like 수를 기록하는 컬럼이다.
reviewRate : Review와 함께 주차장에 대해 남긴 평가 점수로 1,3,5점 중에 선택할 수 있다.
이제 이 테이블과 Mapping 해줄 Entity를 생성해야 한다.
Review Entity 생성
Review.java
@Entity
@Getter @Setter
@Table(name = "REVIEW")
public class Review {
@Id
@GeneratedValue
@Column(name = "reviewUid") //리뷰 UID
private int reviewUid;
@Column(name = "reviewerUid") //리뷰어 UID
private String reviewerUid;
@Column(name = "parkCode") //주차장코드
private String parkCode;
@Column(name = "reviewerNickName") //리뷰어 NICKNAME
private String reviewerNickName;
@Column(name = "reviewImageUrl1", nullable = true) //리뷰이미지URL(이미지로 받아서 서버에서 저장)
private String reviewImageUrl1;
@Column(name = "reviewImageUrl2", nullable = true)
private String reviewImageUrl2;
@Column(name = "reviewImageUrl3", nullable = true)
private String reviewImageUrl3;
@Column(name = "reviewImageUrl4", nullable = true)
private String reviewImageUrl4;
@Column(name = "reviewImageUrl5", nullable = true)
private String reviewImageUrl5;
@Column(name = "reviewText") //리뷰내용
private String reviewText;
@Column(name = "reviewDate") //리뷰 날짜(서버에서 저장)
private String reviewDate;
@Column(name = "likeCount") //리뷰 좋아요 갯수
private Short likeCount;
@Column(name = "reviewRate") //리뷰 평점
private Short reviewRate;
public Review(String reviewerUid, String parkCode, String reviewerNickName, String reviewText, String reviewDate, Short likeCount, Short reviewRate) {
this.reviewerUid = reviewerUid;
this.parkCode = parkCode;
this.reviewerNickName = reviewerNickName;
this.reviewText = reviewText;
this.reviewDate = reviewDate;
this.likeCount = likeCount;
this.reviewRate = reviewRate;
}
public Review(String reviewerUid, String parkCode, String reviewerNickName, String reviewImageUrl1, String reviewImageUrl2, String reviewImageUrl3, String reviewImageUrl4, String reviewImageUrl5, String reviewText, String reviewDate, Short likeCount, Short reviewRate) {
this.reviewerUid = reviewerUid;
this.parkCode = parkCode;
this.reviewerNickName = reviewerNickName;
this.reviewImageUrl1 = reviewImageUrl1;
this.reviewImageUrl2 = reviewImageUrl2;
this.reviewImageUrl3 = reviewImageUrl3;
this.reviewImageUrl4 = reviewImageUrl4;
this.reviewImageUrl5 = reviewImageUrl5;
this.reviewText = reviewText;
this.reviewDate = reviewDate;
this.likeCount = likeCount;
this.reviewRate = reviewRate;
}
}
1. @Entity 어노테이션은 DB의 테이블과 1:1로 매칭되는 객체 단위이며 Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미하며 테이블과 매핑할 클래스에 필수로 붙여줘야한다.
2. Lombok 라이브러리의 @Getter, @Setter 를 사용해 따로 게터세터를 만들지 않는다.
3. @Table 어노테이션의 name 속성으로 아까 만들어 둔 REVIEW 테이블과 Mapping시킨다.
4. @Column 어노테이션의 name 속성으로 각 Entity의 컬럼 변수들을 DB의 컬럼들과 매핑시켜준다.
5. 특히 PRIMARY KEY인 reviewUid 는 @Id 어노테이션으로 표기해주고, 기본 키 생성은 @GeneratedValue 어노테이션을 사용하여 생성된 순서부터 '1, 2, 3, 4...' 형태로 저장되도록 하였다.
6. reviewImageUrl1~5 컬럼은 Image를 등록하지 않은 리뷰가 있을 수 있으므로 @Column 어노테이션에 nullable=true 속성을 지정해주었다.
7. 등록된 이미지가 있을 경우의 생성자와 그렇지 않을 경우의 생성자 두 개를 생성해주었다.
ReviewRepository 객체 구현
엔터티와 DB의 테이블을 매핑시켰으니 이제 Repository를 생성해야 하는데, 우선 인터페이스를 생성해보겠다.
ReviewDataRepository.java
public interface ReviewDataRepository {
void add(Review review);
void delete(int reviewUid);
//review Update기능 - 차후 개발 예정
//void update();
List<Review> findByParkCode(String parkCode);
Review findByReviewUid(int reviewUid);
}
add(Review review) : 리뷰 객체를 받아 DB에 저장하는 함수이다.
delete(int reviewUid) : reviewUid 를 입력받아 DB에서 삭제하는 함수이다.
update() : review를 수정하는 기능으로 다음 포스팅에서 개발할 예정이다.
findByParkCode(String parkCode) : 주차장 코드가 parkCode인 주차장에 대한 리뷰를 DB에서 찾아 List형태로 리턴해주는 함수이다.
findByReviewUid(int reviewUid) : 리뷰의 UID가 입력받은 reviewUid 와 같은 리뷰를 DB에서 찾아 리턴해주는 함수이다.
JpaReviewDataRepository.java
@Primary
@Transactional
@Repository
public class JpaReviewDataRepository implements ReviewDataRepository {
private final EntityManager em;
@Autowired
public JpaReviewDataRepository(EntityManager em) {
this.em = em;
}
@Override
public void add(Review review) {
em.persist(review);
}
@Override
public void delete(int reviewUid) {
Review review = findByReviewUid(reviewUid);
em.remove(review);
}
@Override
public List<Review> findByParkCode(String parkCode) {
return em.createQuery("select r from Review r where r.parkCode = ?1")
.setParameter(1, parkCode)
.getResultList();
}
@Override
public Review findByReviewUid(int reviewUid) {
return em.find(Review.class, reviewUid);
}
}
JPA를 통해 위의 설명과 같은 기능을 하도록 구현체를 만들었다.
이제 이를 Repository를 서비스해줄 Service 클래스를 생성해보자.
Service 객체 구현
ReviewDataServiceImpl.java
@Primary
@Transactional
@Service
public class ReviewDataServiceImpl {
private final ReviewDataRepository reviewDataRepository;
@Autowired
public ReviewDataServiceImpl(ReviewDataRepository reviewDataRepository) {
this.reviewDataRepository = reviewDataRepository;
}
public void addReview(Review review){
reviewDataRepository.add(review);
}
public void deleteReview(int reviewUid) {
reviewDataRepository.delete(reviewUid);
}
public List<Review> findReviewByParkCode(String parkCode) {
return reviewDataRepository.findByParkCode(parkCode);
}
public Review findReviewByReviewUid(int reviewUid) {
return reviewDataRepository.findByReviewUid(reviewUid);
}
}
@AutoWired 어노테이션으로 스프링 빈으로 등록된 ReviewDataRepository를 주입받는다.
그리고 Repository의 기능들을 Service 객체로 구현해주었다. 기능은 위와 똑같으니 따로 설명하지 않겠다.
완성된 객체들은 MainConfig에 입력하여 스프링 빈으로 등록하여 사용한다.
MainConfig.java
@Configuration
public class MainConfig {
private EntityManager em;
@Bean
public ReviewDataRepository reviewDataRepository() { return new JpaReviewDataRepository(em); }
@Bean
public ReviewDataServiceImpl reviewDataService() { return new ReviewDataServiceImpl(reviewDataRepository()); }
}
마지막으로 S3Uploader와 Controller를 구현하면 끝난다.
제일 어려운 부분이므로 집중해서 잘 따라오도록 하자.
S3Uploader 생성
이제 S3에 입력받은 이미지 파일을 전송해 줄 S3Uploader 클래스를 개발해야한다.
일단 지금 상황에서 추가할 기능은 이러하다.
1. uploadFile
List<String> uploadFile(List<MultipartFile> multipartFiles, String dirName, String reviewerUid)
S3에 파일 업로드를 수행할 uploadFile 함수로 매개변수는
'이미지 파일 리스트', '저장할 S3 폴더 이름', '리뷰어의 UID' 를 받는다.
이미지 파일은 MultipartFile 형태로 최대 5개를 받아올 예정이며, 등록된 이미지의 URL 을 List형태로 리턴해주기 위해
자료형을 List<String>으로 지정했다.
2. deleteFile
void deleteFile(String fileName)
S3에 저장되어 있는 파일 삭제를 수행할 deleteFile 함수로 매개변수는
'파일의 이름' 을 받는다.
S3Uploader.java
@RequiredArgsConstructor
@Component
public class S3Uploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
private int cnt = 0;
public List<String> uploadFile(List<MultipartFile> multipartFiles, String dirName, String reviewerUid) {
List<String> fileUrlList = new ArrayList<>(Arrays.asList(null,null,null,null,null));
//지정한 AWS S3 폴더에 있는 파일 초기화
for(int i = 0;i< 5;i++){
deleteFile(dirName + "/" + reviewerUid + "-" + i + ".jpg");
}
// forEach 구문을 통해 multipartFile로 넘어온 파일들 하나씩 S3에 업로드하고 fileUrlList에 추가하여 리턴
// MultipartFile 을 따로 File로 만드는 것이 아닌 InputStream 을 받는 방식
multipartFiles.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename(), dirName, reviewerUid, cnt);
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
try(InputStream inputStream = file.getInputStream()) {
amazonS3Client.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch(IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패했습니다.");
}
fileUrlList.add(cnt,amazonS3Client.getUrl(bucket, fileName).toString());
cnt++;
});
cnt = 0;
return fileUrlList;
}
public void deleteFile(String fileName) {
amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileName));
}
private String createFileName(String uploadFile, String dirName, String reviewerUid, int cnt) {
String count = String.valueOf(cnt);
String fileName = dirName + "/" + reviewerUid + "-" + count + ".jpg";
return fileName;
}
}
위를 바탕으로 개발한 S3Uploader 클래스이다.
1. UploadFile()
uploadFile은 최종적으로 리턴할 fileUrlList을 null로 초기화 해주고 시작된다.
이미지 파일이 5개가 되지 않을 때에 나머지 값을 null로 리턴하기 위함이다.
그 아래에 deleteFlie을 5회 수행하는데 이미지 업로드 이전에 폴더를 초기화해놓기 위해 작동한다.
참고할 부분은 upload 는 최초 1회만 작동하기 때문에 deleteFile이 필요하지 않지만 테스트하는 데에 필요해 넣어주었다.
이후에 update 기능을 완성하면 저 부분은 삭제할 예정이다.
다음으로 multipartFiles를 forEach문으로 S3에 putObject하고, S3에 저장된 이미지 URL을 fileUrlList에 추가하는 과정이 이어진다.
String fileName = createFileName(file.getOriginalFilename(), dirName, reviewerUid, cnt);
createFile 을 통해 업로드할 파일의 경로와 이름을 fileName 변수에 저장한다.
파일 경로는 "review/주차장코드/유저UID/*" 형태로 지정된다.
파일 이름은 유저UID-1,2,3,4,5.jpg 형태로 저장된다.
파일 저장 로직에서는 InputStream을 받아 사용하는데 원래는 MultipartFIle을 File형태로 변환하여 PutObject하는 방식을 사용했다.
하지만 이 과정에서 이러한 문제점이 발생한다.
- 매우 무거운 작업인 '파일 쓰기작업' 이 일어난다.
- 로컬에도 파일이 저장되어 이를 지우기 위한 로직까지 구현해야 한다.
위의 문제점들 때문에 다른 방식을 찾다가 inputStream을 받는 현재 방식을 발견한 것이다.
inputStream을 통해 전송하면 Byte만 전송이 되기 때문에 ObjectMetadata에 파일에 대한 정보를 추가하여 함께 업로드해준다.
.withCannedAcl(CannedAccessControlList.PublicRead));
이 부분은 "외부에 공개할 이미지이므로, 해당 파일에 public read 권한을 추가"해주는 코드이다.
마지막으로
fileUrlList.add(cnt,amazonS3Client.getUrl(bucket, fileName).toString());
를 통해 fileUrlList에 S3에 업로드한 파일의 URL을 저장한다.
return fileUrlList;
최종적으론 이렇게 저장된 fileUrlList를 리턴해주며 함수가 종료된다.
2. deleteFile()
public void deleteFile(String fileName) {
amazonS3Client.deleteObject(new DeleteObjectRequest(bucket, fileName));
}
위의 uploadFile에 비하면 비교적 짧은 코드로 작성된 함수로 deleteObject를 수행하여 입력받은 bucket의 fileName을 삭제한다.
FileName은 위와 같이 파일의 경로와 이름으로 구성하여 입력한다.
이제 최종적으로 ReviewController 만 생성해주면 리뷰 Upload 기능 구현은 끝난다.
ReviewController 생성
ReviewController.java
@RequiredArgsConstructor
@RestController
public class ReviewController {
private final S3Uploader s3Uploader;
private final ReviewDataServiceImpl reviewDataService;
private final UserServiceImpl userService;
//parkCode로 리뷰데이터 찾기
@GetMapping("review")
public List<Review> findReviewByParkCode(@RequestParam("parkCode") String parkCode) {
return reviewDataService.findReviewByParkCode(parkCode);
}
//reviewUid로 리뷰데이터 찾기
@GetMapping("review/{reviewUid}")
public Review findReviewByReviewUid(@PathVariable int reviewUid) {
return reviewDataService.findReviewByReviewUid(reviewUid);
}
//review 업로드
@PostMapping("/review/upload")
public void uploadReview(@RequestParam("uid") String reviewerUid,
@RequestParam("parkCode") String parkCode,
@RequestParam(value = "imgs", required = false) List<MultipartFile> imgs,
@RequestParam("text") String reviewText,
@RequestParam("rate") Short reviewRate) throws IOException {
reviewerUid = reviewerUid.replace("\"", "");
parkCode = parkCode.replace("\"", "");
reviewText = reviewText.replace("\"", "");
String reviewDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(System.currentTimeMillis());
String reviewerNickName = userService.findUserNickName(reviewerUid);
List<String> reviewImageUrls;
short likeCount = 0;
try {
reviewImageUrls = s3Uploader.uploadFile(imgs, "review/" + parkCode + "/" + reviewerUid, reviewerUid); //aws s3에 업로드한 리뷰이미지 URL
Review reviewData = new Review(reviewerUid, parkCode, reviewerNickName, reviewImageUrls.get(0),reviewImageUrls.get(1),reviewImageUrls.get(2),reviewImageUrls.get(3),reviewImageUrls.get(4), reviewText, reviewDate, likeCount, reviewRate);
reviewDataService.addReview(reviewData);
} catch (NullPointerException e) {
Review reviewData = new Review(reviewerUid, parkCode, reviewerNickName, reviewText, reviewDate, likeCount, reviewRate);
reviewDataService.addReview(reviewData);
}
}
Review와 관련된 다양한 기능들을 수행할 ReviewController이다.
@RestController로 만들었으며 우선 3가지 기능을 수행하도록 추가해보았다.
1. parkCode로 주차장 리뷰 데이터 찾기
파라미터로 parkCode를 입력받아 초반에 생성한 reviewDataService의 findReviewByParkCode 함수를 수행한다.
2. reviewUid로 리뷰 데이터 찾기
PathVariable 방식으로 reviewUid를 입력받아 reviewDataService의 findReviewByReviewUid 함수를 수행한다.
3. 리뷰 업로드
- reviewerUid, parkCode, imgs, reviewText, reviewRate를 매개변수로 입력받아 리뷰 업로드 기능을 수행한다.
(입력받은 reviewerUid, parkCode, reviewText의 양 옆에 큰 따옴표(")가 붙는 문제가 발생해 인위적으로 지워주는 로직이 포함되었다.)
- reviewData는 System.currentTimeMills() 함수로 입력받아 지정된 포멧으로 입력해주었고, reviewerNickName은 userService에 findUserNickName 함수를 추가하여 탐색을 수행하였다.
- 후에 try/catch 문 내부에서 S3 Upload와 DB Insert를 수행하는데, 이미지가 있다면 S3Uploader의 uploadFile 함수로부터 리턴받은 reviewImageUrls를 DB에 저장한다.
- 만약 이미지가 없는 리뷰라면 NullPointerException으로 잡아 이미지 없이 DB에 저장하는 방식으로 수행하도록 했다.
작동 테스트
포스트맨을 사용해 업로드를 최종적으로 테스트 해보았다.
DB에 정확히 4개의 URL이 저장되었고 S3에도 4개의 이미지 파일이 저장되었다.
이름도 내가 설정한대로 '-1,-2,-3' 등이 붙으며 제대로 저장되었다.
S3는 S3 Browser 이라는 프로그램으로 확인하였다. 편하게 S3 버킷을 관리할 수 있으니 까는 것이 좋다고 생각한다.
다음 시간에는 리뷰를 수정하는 update 함수를 구현해보도록 하겠습니다!
긴 글 읽어주셔서 감사합니다 :)
참고:
'프로젝트' 카테고리의 다른 글
[SpringBoot] [어따세워] 전화번호로 주차장 찾기 기능 추가! (2) | 2021.12.14 |
---|---|
[SpringBoot] [어따세워] Servlet, JPA, MySQL 이용해서 회원가입 서비스 만들기!(2) (2) | 2021.12.06 |
[SpringBoot] [어따세워] Servlet, JPA, MySQL 이용해서 회원가입 서비스 만들기!(1) (0) | 2021.12.05 |
[어따세워] 메타버스 스터디룸 (Gather town) (0) | 2021.11.30 |
[SpringBoot] [어따세워] 첫 백엔드 프로젝트 기획 (2) | 2021.11.29 |