궤도
[Spring] [면적면적[9]] 책 정보 수정. DTO와 함께 파일 받아서 aws s3 bucket에 파일 업로드 하기 본문
[Spring] [면적면적[9]] 책 정보 수정. DTO와 함께 파일 받아서 aws s3 bucket에 파일 업로드 하기
영이오 2021. 8. 1. 17:25북적북적의 백엔드를 클론하고 있다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-aws', version: '2.2.6.RELEASE'
implementation 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
implementation 'org.projectlombok:lombok:1.18.18'
runtimeOnly 'mysql:mysql-connector-java'
implementation 'com.googlecode.json-simple:json-simple:1.1.1'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
src/main/resources/application.yml
aladin:
url: http://www.aladin.co.kr/ttb/api/ItemLookUp.aspx?ttbkey=비밀키&itemIdType=ISBN13&output=js&Version=20131101&ItemId={isbn}
naver:
id: 아이디
secret: 비번
secret:
key: 비번
cloud:
aws:
credentials:
access-key: 엑세스 키
secret-key: 시크릿 키
s3:
bucket: 버킷 이름
region:
static: 지역
stack:
auto: false
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3305/데베?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: 비번
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
hibernate:
ddl-auto: none
properties:
hibernate:
format_sql: true
show-sql: true
#logging:
# level:
# org:
# hibernate:
# type:
# descriptor:
# sql: trace
server:
servlet:
encoding:
force-response: true
logging:
level:
com:
amazonaws:
util:
EC2MetadataUtils: error
요구사항 정리
책 정보 보기
[Request] {bookId, GET}
1. 입력값 유효성 확인
[Response] {bookInfo, HttpStatus.OK}
책 정보 수정
[Request] {bookId, info, PUT}
1. 입력값 유효성 확인
[Response] {HttpStatus.NO_CONTENT}
책 정보 수정을 하기 전에 북적북적 어플로 몇가지 실험을 해보았다.
책을 처음 저장할 때 사용자가 보유한 책들의 isbn을 기반으로 중복을 체크하는데, 이미 존재하는 다른 책의 isbn으로 수정하고 저장했더니 잘 저장됐다. 그니까 책 수정으로 통해 같은 isbn의 책이 2권 이상 저장될 수 있단 것이다.
처음엔 그냥 수정시 중복을 체크할까 하고 코드도 그런식으로 짰지만, 그냥 isbn을 수정하지 못하게 하기로 했다.
왜냐면 isbn은 책의 pk와 같은건데...이건 수정하지 못하는게 맞을 것 같았다.
책 정보 보기
넘겨줘야 하는 책 정보는 id, 책 소개, 출판사, isbn, 전체 쪽수, 자세히 보기 링크이다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class BookshelfInfoDto {
private Long id;
private String description;
private String publisher;
private String isbn;
private Integer totPage;
private String url;
}
dto를 만들고
/**
* 책의 나머지 정보들
* localhost:8080/bookshelf/info?bookId=42
*
* @param user
* @param bookId
* @return ResponseEntity
*/
@GetMapping("/info")
public ResponseEntity<?> bookshelfInfo(@AuthenticationPrincipal User user,
@RequestParam Long bookId) {
log.info("[Request] Retrieve one book info " + bookId);
return new ResponseEntity<>(bookshelfService.bookshelfInfo(user, bookId), HttpStatus.OK);
}
컨트롤러
/**
* bookshelfInfo
*
* @param user
* @param id
* @return BookshelfInfoDto
*/
public BookshelfInfoDto bookshelfInfo(User user, Long id) {
Book book = commonService.getBook(user, id);
return bookshelfMapper.INSTANCE.toBookshelfInfoDto(book);
}
서비스
먼저 getBook으로 입력에 대한 validation을 하고, mapStruct를 사용해서 내보낸다.
@Test
@DisplayName("Bookshelf Detail | Fail : No Such Data")
void bookshelfFailNoSuchData() throws Exception {
User user = userRepository.findByEmail("test@test.com").orElse(null);
assertNotNull(user);
NoSuchDataException e = assertThrows(NoSuchDataException.class, () ->
bookshelfService.bookshelfInfo(user, 1000L));
assertEquals("Book id = 1000", e.getMessage());
}
@Test
@DisplayName("Bookshelf Detail | Fail : Access Deny")
void bookshelfFailAccessDenied() throws Exception {
User user = userRepository.findByEmail("test2@test.com").orElse(null);
assertNotNull(user);
AccessDeniedException e = assertThrows(AccessDeniedException.class, () ->
bookshelfService.bookshelfInfo(user, 70L));
assertEquals("Book id = 70", e.getMessage());
}
@Test
@DisplayName("Retrieve Book Info | Success")
void bookInfoSuccess() {
User user = userRepository.findByEmail("test@test.com").orElse(null);
assertNotNull(user);
assertEquals("1155351312 9791155351314", bookshelfService.bookshelfInfo(user, 42L).getIsbn());
}
테스트 코드 작성하고 실행하면
잘된다.
포스트맨으로도 확인
책 정보 수정
그냥 로컬로 해도 상관없지만, AWS s3 bucket을 사용하기로 했다.
https://myunji.tistory.com/488
이 글에서 파일 업로드에 대한 감을 좀 잡았다.
https://victorydntmd.tistory.com/334
이 블로그를 참고해서 코드를 작성했다. 거의 똑같이 쓰긴 했다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class S3Service {
private AmazonS3 s3Client;
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Value("${cloud.aws.region.static}")
private String region;
@PostConstruct
public void setS3Client() {
AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
s3Client = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(this.region)
.build();
}
public String upload(MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename() + UUID.randomUUID().toString(); //파일 이름 겹치지 않도록 uuid 사용
s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
.withCannedAcl(CannedAccessControlList.PublicRead));
return s3Client.getUrl(bucket, fileName).toString();
}
}
파일 이름이 겹칠 수 있으니 uuid를 이용했다.
/**
* 책 정보 수정
* localhost:8080/bookshelf/info/42
*
* @param user
* @param bookId
* @param thumbnail
* @param bookshelfInfoUpdateReqDto
* @return ResponseEntity
*/
@PutMapping(value = "/info/{bookId}")
public ResponseEntity<?> bookshelfInfo(@AuthenticationPrincipal User user,
@PathVariable Long bookId,
@RequestPart(required = false) MultipartFile thumbnail,
@Valid @RequestPart BookshelfInfoUpdateReqDto bookshelfInfoUpdateReqDto) {
log.info("[Request] Update book info " + bookId);
bookshelfService.bookshelfUpdateInfo(user, bookId, thumbnail, bookshelfInfoUpdateReqDto);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
컨트롤러
파일과 dto를 받는다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class BookshelfInfoUpdateReqDto {
@NotNull
private String title;
private String author;
private String publisher;
private Integer totPage;
}
dto는 이렇게 생겼다.
/**
* bookshelfUpdateInfo
*
* @param user
* @param id
* @param file
* @param req
*/
@Transactional
public void bookshelfUpdateInfo(User user, Long id, MultipartFile file, BookshelfInfoUpdateReqDto req) {
Book book = commonService.getBook(user, id);
String thumbnail = book.getThumbnail();
if (file != null) {
try {
thumbnail = s3Service.upload(file);
} catch (IOException e) {
throw new S3Exception("file = " + file.getOriginalFilename());
}
}
book.updateBookInfo(req.getTitle(), req.getAuthor(), req.getPublisher(), req.getTotPage(), thumbnail);
bookRepository.save(book);
}
서비스
일단 getBook으로 id에 대한 validation을 한다.
커버 이미지를 수정하지 않을 수도 있으니 file에 대한 널체크를 하고, 업로드를 한다.
그리고 그냥 정보 수정하고 save로 dirty checking하면 된다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {
@Id
@Column(name = "book_sn")
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_sn")
@NotNull
private User user;
@NotNull
private String title;
@Column(length = 300)
private String thumbnail;
private String author;
private String publisher;
@Lob
private String description;
@NotNull
private String isbn;
private Integer totPage;
@Column(length = 300)
@NotNull
private String url;
@Enumerated(EnumType.STRING)
private BookStatus bookStatus;
private LocalDate startDate;
private LocalDate endDate;
private Integer score;
private Integer readPage;
@Lob
private String expectation;
public void updateBookStatus(BookStatus bookStatus, LocalDate startDate, LocalDate endDate,
Integer score, Integer readPage, String expectation){
this.bookStatus = bookStatus;
this.startDate = startDate;
this.endDate = endDate;
this.score = score;
this.readPage = readPage;
this.expectation = expectation;
}
public void updateBookInfo(String title, String author, String publisher, Integer totPage, String thumbnail){
this.title = title;
this.author = author;
this.publisher = publisher;
this.totPage = totPage;
this.thumbnail = thumbnail;
}
}
수정 로직은 엔티티에 넣어놨다.
s3라...어떤식으로 테스트 코드를 짜야할지 모르겠어서 그냥 바로 포스트맨으로 확인한다.
잘 올라왔고
해당하는 책을 찾아보면 잘 수정됐다.
아 근데 실행하면 무슨 에러메세지가 뜰텐데
https://h-kkaemi.tistory.com/24
이런거라고 한다.