궤도

[Spring] [면적면적[9]] 책 정보 수정. DTO와 함께 파일 받아서 aws s3 bucket에 파일 업로드 하기 본문

💻 현생/📕 면적면적(스프링 실습)

[Spring] [면적면적[9]] 책 정보 수정. DTO와 함께 파일 받아서 aws s3 bucket에 파일 업로드 하기

영이오 2021. 8. 1. 17:25

북적북적의 백엔드를 클론하고 있다.

 

Github

 

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

 

[Spring] 파일 업로드시 dto도 같이 받고 싶으면 어떻게 할까? / 부제 : @PutMapping에 파일 업로드 해도

면적면적 개발 도중...궁금한게 생겼다. 이건 북적북적의 책 정보 수정 화면인데, 커버이미지를 수정할 수도 있다. https://gaemi606.tistory.com/m/entry/Spring-Boot-multipartform-data-%ED%8C%8C%EC%9D%BC-%E..

myunji.tistory.com

이 글에서 파일 업로드에 대한 감을 좀 잡았다.

 

https://victorydntmd.tistory.com/334

 

[SpringBoot] AWS S3 연동 (1) - 파일 업로드 기본 (AmazonS3ClientBuilder)

Springboot S3 업로드를 구현하는 시리즈입니다. [SpringBoot] AWS S3 연동 (1) - 파일 업로드 기본 (AmazonS3ClientBuilder) [SpringBoot] AWS S3 연동 (2) - 파일 조작 및 Cloud Front 전체 소스 코드는 여..

victorydntmd.tistory.com

이 블로그를 참고해서 코드를 작성했다. 거의 똑같이 쓰긴 했다.

 

@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

 

spring-cloud-starter-aws 의존성 주입 시 나는 에러

s3 서버에 파일 업로드를 구현하기 위해 spring-cloud-starter-aws 의존성 주입을 하게되면 톰캣 서버 실행 시 에러가 뜬다. com.amazonaws.SdkClientException: Failed to connect to service endpoint: // ... s..

h-kkaemi.tistory.com

이런거라고 한다.

Comments