본문 바로가기
기록, 회고/InFlearn Warming-up 0기 BE

[9일 차] - 내용 정리(추가된 기능에 대한 API 개발), 개인 회고

by TwoJun 2024. 2. 27.

인프런에서 주최하는 Warming-up 클럽 0기 백엔드 스터디에 참여하고 있다.

 

스터디에 참여하면서 배우게 된 내용을 전체적으로 정리하고, 참여하면서 느낀 부분을 회고해 보고자 한다.

 

(1) 9일 차 : 2024-02-29(Thu)

 

 

 

 

 

1. 도서 등록 기능 개발

1-1. API Spec.

(1) HTTP Method : POST

(2) HTTP Path : /book

(3) HTTP Body : JSON

(4) 결과 반환 : void

HTTP Request Body

 

 

 

 

1-2. book 테이블 설계

create table book 
(
    id bigint auto_increment,
    name varchar(255),
    primary key (id)
);

(1) JPA의 @Column의 default length는 255이다.

(2) 최적화가 필요한 게 아니라면 기본값으로 두는 것이 확장성 측면에서 도움이 될 수 있다.

 

 

 

 

1-3. Book 엔티티 설계

@Entity
@Getter
public class Book {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(nullable = false)
    private String name;

    public Book(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다."));
        }
        this.name = name;
    }

    protected Book() {}
}

(1) 식별자는 MySQL의 Auto increment를 따르게 된다.

(2) 책 이름을 별도로 새로운 엔티티를 생성할 때 이름에 조건에 대한 분기가 true라면 예외가 발생하게 된다.

 

 

 

1-4. 기본적인 BookRepository.java

public interface BookRepository extends JpaRepository<Book, Long> {

}

 

 

 

 

1-5. 도서 생성정보를 처리하기 위한 DTO

@Data
public class BookCreateRequest {

    private String name;
}

 

(1) 이제 Controller, Service 계층을 요구사항에 맞게 잘 개발하면 된다.

 

 

 

 

1-6. BookController.java, BookService.java

@RestController
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;
    
    // 생성
    @PostMapping("/book")
    public void saveBook(@RequestBody BookCreateRequest request) {
        bookService.saveBook(request);
    }
}


@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookService {

    private final BookRepository bookRepository;

    @Transactional
    public void saveBook(BookCreateRequest request) {
        bookRepository.save(new Book(request.getName()));
    }
}

(1) 기본적인 CRUD 메서드를 호출함에 따라 도서 정보가 생성된다.

(2) 클래스 레벨에 선언적 트랜잭션을 적용하고, 기본값은 readOnly = true로 설정하고 단순 조회가 아닌 비즈니스에서 readOnly = false로 설정한 어노테이션을 적용하면 된다.

 

 

 

 

2. 도서 대출 기능 개발

2-1. API Spec

(1) HTTP Method : POST

(2) HTTP Path : /book/loan

(3) HTTP Body : JSON

Body spec

 

 

 

2-2. 대출 정보를 확인할 수 있는 테이블 생성

create table user_loan_history (
  id bigint auto_increment,
  user_id bigint,
  book_name varchar(255),
  is_return tinyint(1),
  primary key (id)
)

(1) 어떤 회원이 책을 대출했는지 확인하기 위해 사용자의 아이디를 생성

(2) 또한 어떤 도서인지 알기 위해 도서의 이름 정보 생성

(3) 마지막으로 대출 여부를 확인하는 is_return으로 반납/미반납 여부를 확인할 수 있다. (미반납 시 0)

 

 

 

 

2-3. 대출 정보 엔티티 생성 

@Entity
@Getter
public class UserLoanHistory {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    private long userId;

    private String bookName;

    private boolean isReturn;

    protected UserLoanHistory() {}
}

(1) 대출 여부의 경우 값이 1, 0이므로 boolean 타입으로 선언했다.

 

 

 

2-4. 대출 정보에 대한 UserLoanHistoryRepository.java

public interface UserLoanHistoryRepository extends JpaRepository<UserLoanHistory, Long> {

}

 

 

 

2-5. 대출 정보 생성을 위한 DTO

@Data
public class BookLoanRequest {

    private String userName;
    private String bookName;
}

(1) lombok 라이브러리인 @Data 어노테이션으로 Getter, Setter 메서드를 명시하지 않아도 자동 생성된다.

 

 

 

 

2-6. BookController.java, BookService.java, UserLoanHistoryRepository.java

@RestController
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;

    // 생성
    @PostMapping("/book")
    public void saveBook(@RequestBody BookCreateRequest request) {
        bookService.saveBook(request);
    }

    // 대출
    @PostMapping("/book/loan")
    public void loanBook(@RequestBody BookLoanRequest request) {
        bookService.loanBook(request);
    }
}


@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookService {

    private final BookRepository bookRepository;
    private final UserLoanHistoryRepository userLoanHistoryRepository;
    private final UserRepository userRepository;

    @Transactional
    public void saveBook(BookCreateRequest request) {
        bookRepository.save(new Book(request.getName()));
    }

    @Transactional
    public void loanBook(BookLoanRequest request) {
        // 책 존재 여부 확인
        Book findBook = bookRepository.findByName(request.getBookName())
                .orElseThrow(() -> new IllegalStateException("존재하지 않는 책입니다."));

        // 대출 중인지 확인
        if (userLoanHistoryRepository.existsByBookNameAndIsReturn(findBook.getName(), false)) {
            throw new IllegalArgumentException("이미 대출 중인 책은 대여할 수 없습니다.");
        }

        // 대출 중인 책이 아니라면 유저 정보를 가져와 새로운 대출정보를 생성
        User findUser = userRepository.findByName(request.getUserName())
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 정보입니다."));

        userLoanHistoryRepository.save(new UserLoanHistory(findUser.getId(), findBook.getName()));
    }
}

public interface UserLoanHistoryRepository extends JpaRepository<UserLoanHistory, Long> {

    // select * from user_loan_history where book_name = ? and is_return = ? (대출 중인지 확인)
    boolean existsByBookNameAndIsReturn(String name, boolean isReturn);

}

(1) 요청 정보로 넘어온 도서가 존재하는지 확인

-DTO로 넘어온 도서 이름을 기반으로 도서 테이블에 존재하는 정보인지 확인 후 존재하지 않는다면 예외를 던진다. 

 

(2) 넘어온 도서 정보가 대출 중인지 확인

- existsByBookNameAndIsReturn() 메서드를 통해 이미 대출 중(is_true = 0, false)인 책이라면 대여할 수 없다는 예외를 발생시킨다.

 

(3) 대출 중인 책이 아닌 경우

- 대출 중인 책이 아닌 경우 대출이 가능하다는 의미이므로 userLoanHistoryRepository.save()를 호출해서 새로운 대출 정보를 생성한다.

 

 

 

 

 

3. 반납 기능 개발

3-1. API Spec.

(1) HTTP Method : PUT

(2) HTTP Path : /book/return

(3) HTTP Body : JSON

 

 

 

 

3-1. 도서 정보 생성, 도서 반납을 위한 DTO를 별도로 설계한다. (BookReturn.java)

// 반납을 위한 DTO

@Data
public class BookReturnRequest {

    private String userName;
    private String bookName;
}

(1) 각 요구사항에 맞는 별도의 DTO를 알맞게 설계하는 것이 향후 Side-effect를 줄이는 데 도움이 된다.

 

 

 

 

3-2. BookController.java, BookService.java, UserLoanHistoryRepository.java

@RestController
@RequiredArgsConstructor
public class BookController {

    private final BookService bookService;

    // 생성
    @PostMapping("/book")
    public void saveBook(@RequestBody BookCreateRequest request) {
        bookService.saveBook(request);
    }

    // 대출
    @PostMapping("/book/loan")
    public void loanBook(@RequestBody BookLoanRequest request) {
        bookService.loanBook(request);
    }

    // 반납
    @PutMapping("/book/return")
    public void returnBook(@RequestBody BookReturnRequest request) {
        bookService.returnBook(request);
    }
}


@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookService {

    private final BookRepository bookRepository;
    private final UserLoanHistoryRepository userLoanHistoryRepository;
    private final UserRepository userRepository;

    @Transactional
    public void saveBook(BookCreateRequest request) {
        bookRepository.save(new Book(request.getName()));
    }

    @Transactional
    public void loanBook(BookLoanRequest request) {
        // 책 대출 여부 확인
        Book findBook = bookRepository.findByName(request.getBookName())
                .orElseThrow(() -> new IllegalStateException("존재하지 않는 책입니다."));

        // 대출 중인지 확인
        if (userLoanHistoryRepository.existsByBookNameAndIsReturn(findBook.getName(), false)) {
            throw new IllegalArgumentException("이미 대출 중인 책은 대여할 수 없습니다.");
        }

        // 대출 중인 책이 아니라면 유저 정보를 가져와 새로운 대출정보를 생성
        User findUser = userRepository.findByName(request.getUserName())
                .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원 정보입니다."));

        userLoanHistoryRepository.save(new UserLoanHistory(findUser.getId(), findBook.getName()));
    }

    @Transactional
    public void returnBook(BookReturnRequest request) {
        // 존재하는 책, 회원인지 확인 필요
        User findUser = userRepository.findByName(request.getUserName())
                .orElseThrow(() -> new IllegalStateException("존재하지 않는 회원 정보입니다."));

        Book findBook = bookRepository.findByName(request.getBookName())
                .orElseThrow(() -> new IllegalStateException("존재하지 않는 도서 정보입니다."));

        // findBook, findUser가 모두 히스토리에 존재해야 반납 가능
        UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookNameAndIsReturn(findUser.getId(), findBook.getName(), false)
                .orElseThrow(() -> new IllegalStateException("존재하지 않는 대출 정보입니다."));

        history.doReturn();
    }
}


public interface UserLoanHistoryRepository extends JpaRepository<UserLoanHistory, Long> {

    // select * from user_loan_history where book_name = ? and is_return = ? (대출 중인지 확인)
    boolean existsByBookNameAndIsReturn(String name, boolean isReturn);

    // select * from user_loan_history where user_id = ? and book_name = ? and is_return = ? (반납을 위한 대출정보 확인)
    Optional<UserLoanHistory> findByUserIdAndBookNameAndIsReturn(long id, String name, boolean isReturn);
}

(1) 도서 반납을 위한 returnBook() 메서드를 살펴보자.

 

(2) 우선 요청 DTO에서 넘어온 정보를 기반으로, 존재하는 회원인지, 존재하는 도서 정보인지 각 리포지토리 영역을 거쳐 우선 확인한다.

(만약 존재하는 정보가 없다면 예외를 던지게 된다.)

 

(3) 만약 두 가지 정보가 모두 존재한다면, 마지막으로 findByUserIdAndBookNameAndIsReturn() 메서드를 호출하여 도서 대출 테이블에도 존재하는 정보인지 확인한다. 해당 영역에서까지 정보가 존재하고 있어야 대출 중인 도서이고, 반납이 가능하기 때문이다.

 

(4) 존재하는 정보라면 도서 반납이 이루어지니 is_true를 doReturn() 메서드에 의해 1로 변경해준다. 만일 대출 정보가 없다면 예외를 던지게 된다.

 

 

 

 

4. 한 가지 고민

(1) 모든 기능 구현이 완료되었지만, 좀 더 객체지향적인 설계가 가능하지 않을까?라는 의문이 남는다.

 

(2) 각 엔티티 간의 협력을 통해 개발할 수 없을까? 라는 생각과 함께 JPA에 대한 연관관계를 떠올려 볼 수 있게 된다.

 

 

 

5. 개인 회고

(1) 현재 모든 객체들이 서로 연결되어 있거나 협력하지 않은 상태에서 기능이 구현된 상태이다. 객체의 역할과 협력을 생각해서 조금 더 객체지향적으로 설계해 보는 것이 좋을 것 같다고 생각했다.

 

(2) 엔티티 간 연관관계는 우선 DB의 테이블과 자바의 객체가 서로 연관관계를 바라보는 관점이 불일치하기 때문에 JPA의 연관관계를 통해 이를 잘 해결해야 한다. JPA에서 중요한 2가지는 이전에서도 설명한 것처럼 JPA의 내부 동작 방식 이해, 그리고 연관관계를 이용해 객체와 테이블을 정확히 매핑하는 것에서부터 시작한다.

 

 

 

 

6. Reference

(1) 관련 레퍼런스는 인프런에서 활동하고 계시는 최태현 강사님의 온라인 강의를 듣고 내용을 정리했습니다.

https://www.inflearn.com/course/%EC%9E%90%EB%B0%94-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%84%9C%EB%B2%84%EA%B0%9C%EB%B0%9C-%EC%98%AC%EC%9D%B8%EC%9B%90/dashboard

 

자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인

Java와 Spring Boot, JPA, MySQL, AWS를 이용해 서버를 개발하고 배포합니다. 웹 애플리케이션을 개발하며 서버 개발에 필요한 배경지식과 이론, 다양한 기술들을 모두 학습할 뿐 아니라, 다양한 옵션들

www.inflearn.com

 

※ 해당 포스팅에 대해 내용 추가가 필요하다고 생각되면 기존 포스팅 내용에 다른 내용이 추가될 수 있습니다.

개인적으로 공부하며 정리한 내용이기에 오타나 틀린 부분이 있을 수 있으며, 이에 대해 댓글로 알려주시면 감사하겠습니다

댓글