궤도

[Spring] [면적면적[8]] 책에 메모 CRUD 하기 본문

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

[Spring] [면적면적[8]] 책에 메모 CRUD 하기

영이오 2021. 7. 30. 18:40

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

 

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 '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: 비번
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

요구사항 정리

 

메모 추가

[Request] {Memo, POST}

1. 입력값 유효성 확인

 

[Response] {HttpStatus.NO_CONTENT}

 

메모 보기

[Request] {bookId, GET}

1. 입력값 유효성 확인

 

[Response] {List<Memo>, HttpStatus.OK} / {HttpStatus.NO_CONTENT}

 

메모 수정

[Request] {memoId, Memo, PUT}

1. 입력값 유효성 확인

 

[Response] {HttpStatus.NO_CONTENT}

 

메모 삭제

[Request] {memoId, DELETE}

1. 입력값 유효성 확인

 

[Response] {HttpStatus.NO_CONTENT}


메모 추가

 

저기 사진을 보면 알겠지만 메모엔 별게 없다. 해당 메모가 소속된 책의 id와 내용 그리고 저장날짜 정도가 필요한데, 저장 날짜는 그냥 now로 하면 되니까 책의 id와 내용만 필요하다.

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @NotNull
    public static class BookshelfMemoReqDto {

        private Long id;
        private String content;
    }

dto를 만들고

 

    /**
     * 메모 추가하기
     * localhost:8080/bookshelf/memo
     *
     * @param user
     * @param bookshelfMemoReqDto
     * @return ResponseEntity
     */
    @PostMapping("")
    public ResponseEntity<?> bookshelfMemo(@AuthenticationPrincipal User user,
                                           @Valid @RequestBody BookshelfMemoReqDto bookshelfMemoReqDto) {
        log.info("[Request] Add memo book id = " + bookshelfMemoReqDto.getId());
        bookshelfMemoService.bookshelfAddMemo(user, bookshelfMemoReqDto);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

이건 컨트롤러이다. 메모를 추가해도 별 일 안생기니까 그냥 NO_CONTENT로 보낸다.

 

    /**
     * bookshelfAddMemo
     *
     * @param user
     * @param bookshelfMemoReqDto
     * @return Memo
     */
    @Transactional
    public Memo bookshelfAddMemo(User user, BookshelfDetailDtos.BookshelfMemoReqDto bookshelfMemoReqDto) {
        Book book = commonService.getBook(user, bookshelfMemoReqDto.getId());
        return memoRepository.save(Memo.builder()
                .book(book)
                .content(bookshelfMemoReqDto.getContent())
                .saved(LocalDateTime.now())
                .build());
    }

일전에 쓴 책이랑 유저에 대한 validation을 하는 getBook 메소드를 그대로 쓰고, 그냥 바로 빌더에 넣어서 저장한다.

 

이번 글은 별 내용이 없으니 테스트 코드는 마지막에 한번에 보여주도록 하겠다.


메모 보기

 

메모의 id와 내용 그리고 저장날짜를 보내주면 되겠다. 근데 최근에 저장한 메모부터 보내줘야 한다.

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public static class BookshelfMemoResDto {
        private Long id;
        private String content;
        private LocalDateTime saved;
    }

그래서 이건 response로 보내줄 메모 리스트의 dto고

 

    /**
     * 책의 메모
     * localhost:8080/bookshelf/memo?bookId=42
     *
     * @param user
     * @param bookId
     * @return ResponseEntity
     */
    @GetMapping("")
    public ResponseEntity<?> bookshelfRetrieveMemo(@AuthenticationPrincipal User user,
                                                   @RequestParam Long bookId) {
        log.info("[Request] Retrieve all Memos in book id = " + bookId);
        List<BookshelfMemoResDto> result = bookshelfMemoService.bookshelfMemo(user, bookId);
        if (result.isEmpty())
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        return new ResponseEntity<>(result, HttpStatus.OK);
    }

컨트롤러 코드

요청한 책에 대한 메모가 없을 수도 있으니 NO_CONTENT도 가능하다.

 

    /**
     * bookshelfMemo
     *
     * @param user
     * @param id
     * @return List<BookshelfMemoResDto>
     */
    public List<BookshelfMemoResDto> bookshelfMemo(User user, Long id) {
        Book book = commonService.getBook(user, id);
        return memoRepository.findAllByBookOrderBySavedDesc(book).stream()
                .map(bookshelfMapper.INSTANCE::memoToBookshelfMemoResDto)
                .collect(Collectors.toList());
    }

서비스 코드

마찬가지로 요청에 대한 validation을 getBook 메소드로 하고 뭐...메모 레포지토리에서 book에 대한 메모를 최신 저장 순으로 다 불러온 뒤, MapStruct 통해서 dto에 넣고 반환하면 된다.


메모 수정

 

메모를 수정하면 저장날짜도 바뀌나? 했는데 바뀌지 않는다. 그냥 내용만 수정하면 된다.

 

    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @NotNull
    public static class BookshelfUpdateMemoReqDto {
        private String content;
    }

겨우 1개지만 뭐...dto를 만들고(사실 이정도는 그냥 Map으로 해도 되지 않을까?)

 

    /**
     * 메모 수정하기
     * localhost:8080/bookshelf/memo/233
     *
     * @param user
     * @param memoId
     * @param bookshelfUpdateMemoReqDto
     * @return ResponseEntity
     */
    @PutMapping("/{memoId}")
    public ResponseEntity<?> bookshelfMemo(@AuthenticationPrincipal User user,
                                           @PathVariable Long memoId,
                                           @Valid @RequestBody BookshelfUpdateMemoReqDto bookshelfUpdateMemoReqDto) {
        log.info("[Request] Update memo Memo id = " + memoId);
        bookshelfMemoService.bookshelfUpdateMemo(user, memoId, bookshelfUpdateMemoReqDto);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

컨트롤러 코드

 

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Memo {

    @Id
    @Column(name = "memo_sn")
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "book_sn")
    @NotNull
    private Book book;

    @Lob
    @NotNull
    private String content;

    @NotNull
    private LocalDateTime saved;

    public void updateMemo(String content){
        this.content = content;
    }
}

메모 엔티티에도 비즈니스 로직 넣어주고

 

    /**
     * bookshelfUpdateMemo
     *
     * @param user
     * @param id
     * @param bookshelfUpdateMemoReqDto
     */
    @Transactional
    public void bookshelfUpdateMemo(User user, Long id, BookshelfUpdateMemoReqDto bookshelfUpdateMemoReqDto) {
        Memo memo = memoRepository.findById(id)
                .orElseThrow(() -> new NoSuchDataException("Memo id = " + id));
        commonService.checkUser(user, memo.getBook());

        memo.updateMemo(bookshelfUpdateMemoReqDto.getContent());
        memoRepository.save(memo);
    }

서비스 코드이다.

먼저 요청받은 메모 id에 대한 validation을 진행한다.

해당 id의 메모가 실제하는지 확인한뒤, memo.getBook().getUser()로 흘러가서 나온 user가 요청을 준 user와 동일한지 확인한다.

 

그리고 뭐 update하면 끝이다.


메모 삭제하기

 

이건 할게 더 없다.

 

    /**
     * 메모 삭제하기
     * localhost:8080/bookshelf/memo/233
     *
     * @param user
     * @param memoId
     * @return ResponseEntity
     */
    @DeleteMapping("/{memoId}")
    public ResponseEntity<?> bookshelfMemo(@AuthenticationPrincipal User user,
                                           @PathVariable Long memoId) {
        log.info("[Request] Delete memo Memo id = " + memoId);
        bookshelfMemoService.bookshelfDeleteMemo(user, memoId);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

컨트롤러

 

    /**
     * bookshelfDeleteMemo
     *
     * @param user
     * @param id
     */
    @Transactional
    public void bookshelfDeleteMemo(User user, Long id) {
        Memo memo = memoRepository.findById(id)
                .orElseThrow(() -> new NoSuchDataException("Memo id = " + id));
        commonService.checkUser(user, memo.getBook());

        memoRepository.delete(memo);
    }

서비스


테스트 코드

 

 

package jpa.myunjuk.module.service;

import jpa.myunjuk.module.model.domain.Memo;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.model.dto.bookshelf.BookshelfDetailDtos;
import jpa.myunjuk.module.repository.BookRepository;
import jpa.myunjuk.module.repository.UserCharacterRepository;
import jpa.myunjuk.module.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest
@DisplayName("Bookshelf Memo Service Test")
@Transactional
public class BookshelfMemoServiceTest {

    @Autowired
    BookshelfMemoService bookshelfMemoService;
    @Autowired
    SearchService searchService;
    @Autowired
    UserRepository userRepository;
    @Autowired
    BookRepository bookRepository;
    @Autowired
    UserCharacterRepository userCharacterRepository;

    @Test
    @DisplayName("Retrieve all Memos | Success")
    void allMemosSuccess() throws InterruptedException {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        BookshelfDetailDtos.BookshelfMemoReqDto req = getBookshelfMemoReqDto("hello hello hello this is memo.");
        bookshelfMemoService.bookshelfAddMemo(user, req);
        Thread.sleep(2000);
        BookshelfDetailDtos.BookshelfMemoReqDto req2 = getBookshelfMemoReqDto("hello hello hello this is memo2.");
        bookshelfMemoService.bookshelfAddMemo(user, req2);

        List<BookshelfDetailDtos.BookshelfMemoResDto> memos = bookshelfMemoService.bookshelfMemo(user, 42L);
        assertEquals("hello hello hello this is memo2.", memos.get(0).getContent());
    }

    @Test
    @DisplayName("Add Memo | Success")
    void addMemoSuccess() {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        BookshelfDetailDtos.BookshelfMemoReqDto req = getBookshelfMemoReqDto("hello hello hello this is memo.");
        Memo memo = bookshelfMemoService.bookshelfAddMemo(user, req);

        assertEquals("hello hello hello this is memo.", memo.getContent());
        assertEquals(42L, memo.getBook().getId());
        assertEquals(user, memo.getBook().getUser());
        assertEquals(0, Duration.between(memo.getSaved(), LocalDateTime.now()).getSeconds());
    }

    @Test
    @DisplayName("Update Memo | Success")
    void updateMemoSuccess() {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        BookshelfDetailDtos.BookshelfMemoReqDto req = getBookshelfMemoReqDto("hello hello hello this is memo.");
        Memo memo = bookshelfMemoService.bookshelfAddMemo(user, req);

        bookshelfMemoService.bookshelfUpdateMemo(user, memo.getId(),
                BookshelfDetailDtos.BookshelfUpdateMemoReqDto.builder()
                        .content("Updated Memo")
                        .build());
        assertEquals("Updated Memo", memo.getContent());
    }

    @Test
    @DisplayName("Delete Memo | Success")
    void deleteMemoSuccess() {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        BookshelfDetailDtos.BookshelfMemoReqDto req = getBookshelfMemoReqDto("hello hello hello this is memo.");
        Memo memo = bookshelfMemoService.bookshelfAddMemo(user, req);
        int size = bookshelfMemoService.bookshelfMemo(user, 42L).size();

        bookshelfMemoService.bookshelfDeleteMemo(user, memo.getId());
        assertEquals(size - 1, bookshelfMemoService.bookshelfMemo(user, 42L).size());
    }

    private BookshelfDetailDtos.BookshelfMemoReqDto getBookshelfMemoReqDto(String content) {
        return BookshelfDetailDtos.BookshelfMemoReqDto.builder()
                .id(42L)
                .content(content)
                .build();
    }
}

전체 메모 crud에 대한 테스트 코드다.

 

    @Test
    @DisplayName("Retrieve all Memos | Success")
    void allMemosSuccess() throws InterruptedException {
        User user = userRepository.findByEmail("test@test.com").orElse(null);
        assertNotNull(user);

        BookshelfDetailDtos.BookshelfMemoReqDto req = getBookshelfMemoReqDto("hello hello hello this is memo.");
        bookshelfMemoService.bookshelfAddMemo(user, req);
        Thread.sleep(2000);
        BookshelfDetailDtos.BookshelfMemoReqDto req2 = getBookshelfMemoReqDto("hello hello hello this is memo2.");
        bookshelfMemoService.bookshelfAddMemo(user, req2);

        List<BookshelfDetailDtos.BookshelfMemoResDto> memos = bookshelfMemoService.bookshelfMemo(user, 42L);
        assertEquals("hello hello hello this is memo2.", memos.get(0).getContent());
    }

다 그냥 대충 읽어보면 알만한 코드지만 하나만 설명하면

이건 메모를 2개 저장하고 최신순으로 잘 나오는지 확인하는 코드다.

 

이제보니 존재하지 않는 메모 id로 request해서 실패하는 코드가 없긴 하구만

포스트맨은 귀찮아서 생략하는데 잘됐었답니다

Comments