[인프런 워밍업 클럽 1기/BE] 3번째 발자국
section531강. 대출 기능 개발하기32강. 책 반납 기능 개발하기33강. 조금 더 객체지향적으로 개발할 수 없을까?34강. JPA 연관관계에 대한 추가적인 기능들35강. 책 대출/반납 기능 리팩토링과 지연 로딩1. 대출기능 개발 - 새로운 테이블 생성현재 user, book 2개의 테이블이 존재한다. 하지만 이 2개의 테이블 만으로는 대출 기능을 만들 수 없다. 새로운 테이블 user_loan_history 이 필요하다.create table user_loan_history ( id bigint auto_increment, user_id bigint, book_name varchar(255), is_return tinyint(1), primary key (id) )user_id : 어떤 유저가 빌렸는지 알 수 있도록, 유저의 id를 가지고 있도록 한다.is_return : 타입은 tinyint 인데, entity 객체의 필드 중 boolean에 매핑하게 되면, true인 경우 1, false인 경우 0이 저장된다.2. 책 반납 기능 개발 - @ManyToOne 으로 리팩토링 위의 HTTP Body 는 반납 request 의 요청 형식이다. 그런데 '책 대출' 과 '책 반납'의 api body가 똑같다. 이때 똑같더라도 별개의 class로 작성하는 것이 좋다. 두 기능 중 한 기능에 변화가 생겼을때, 유연하고 다른 부가적인 문제없이 대처할 수 있기 때문이다.아래는 반납 관련 DTO와 Controller, service 내용이다.DTOpublic class BookReturnRequest { private String userName; private String bookName; public BookReturnRequest(String userName, String bookName) { this.userName = userName; this.bookName = bookName; } public String getUserName() { return userName; } public String getBookName() { return bookName; } }Controller @PutMapping("/book/return") public void returnBook(@RequestBody BookReturnRequest request){ bookService.returnBook(request); }Service@Transactional public void returnBook(BookReturnRequest request) { User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new); UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(user.get Id(), request.getBookName()) .orElseThrow(IllegalArgumentException::new); history.doReturn(); }위의 코드를 조금 더 객체지향적으로 개발하기 위해서JPA 연관관계를 활용할 수 있다. 이렇게 바꾸기 위해서는 UserLoanHistory와 User 가 서로 직접 알고 있어야 한다.UserLoanHistory의 userId 를 user로 변경해보자. @Entity public class UserLoanHistory { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @JoinColumn(nullable = false) @ManyToOne private User user; private String bookName; private boolean isReturn; public Long getId() { return id; } public String getBookName() { return bookName; } public boolean isReturn() { return isReturn; } public UserLoanHistory(User user, String bookName) { this.user = user; this.bookName = bookName; this.isReturn = false; } public void doReturn(){ this.isReturn = true; } public UserLoanHistory() { } }@ManyToOne 은 N(나) : 1(너) 관계로 위에서는 N이 UserLoanHistory가 되고, 1이 User가 된다.User 클래스에서 1명의 유저는 N개의 UserLoanHistroy를 가지고 있을 수 있기 때문에, UserLoanHistroy를 List 형태로 가지고 있어야 한다.@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id = null; @Column(nullable = false, length = 20, name = "name") //name varchar(20) private String name; @Column(nullable = false) private Integer age; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) //주인이 가진 필드 이름 // fetch = FetchType.LAZY private List<UserLoanHistory> userLoanHistories = new ArrayList<>(); protected User() { } public Long getId() { return id; } public String getName() { return name; } public int getAge() { return age; } public void updateName(String name){ this.name = name; } public User(String name, int age) { if (name == null || name.isEmpty()) throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다.", name)); this.name = name; this.age = age; } public void loanBook(String bookName){ this.userLoanHistories.add(new UserLoanHistory(this, bookName)); } public void returnBook(String bookName){ UserLoanHistory targetHistroy = this.userLoanHistories.stream() .filter(history -> history.getBookName().equals(bookName)) .findFirst() .orElseThrow(IllegalArgumentException::new); targetHistroy.doReturn(); } }요 List에는 @OneToMany 를 붙여준다.이때 연관관계의 주인을 정해주어야 한다. 현재 user 테이블과 user_loan_history 테이블을 보면, user_loan_history는 user를 알고 있다. 반면 user는 user_loan_history 를 알지 못한다. 즉, 관계의 주도권을 user_loan_history 가 가지고 있는 것이다.테이블에서는 이를 알 수 있지만,JPA 에서는 모르는 상태이니 mappedBy 옵션을 달아주어 이제 알려주자.user가 주인이 아니므로, @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) //주인이 가진 필드 이름 // fetch = FetchType.LAZY private List<UserLoanHistory> userLoanHistories = new ArrayList<>();이렇게 하여 user 와 userLoanHistory가 서로를 알 수 있도록 하였다. 하지만, 여전히 BookService는 User와 UserLoanHistory를 각자 다루고 있다. 온전히 협력하지 못하므로 이를 수정해보자.+ @JoinColumn 은 연관관계의 주인 클래스에서 사용할 수 있다. Service 코드에서 UserLoanHistory를 직접 사용하지 않고, User 를 통해 대출 기록을 저장하도록 변경해보자.일단 BookService는 아래와 같이 변경했다. @Transactional public void loanBook(BookLoanRequest request) { //1. 책 정보를 가져온다. Book book = bookRepository.findByName(request.getBookName()) .orElseThrow(IllegalArgumentException::new); //2. 대출기록 정보를 확인해서 대출중인지 확인합니다. //3. 먄약에 확인했는데 대출중이라면 예외를 발생시킨다. if (userLoanHistoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) //대여중임 throw new IllegalArgumentException("이미 대출되어 있는 책입니다."); //4. 유저 정보를 가져온다. User user = userRepository.findByName(request.getUserName()) .orElseThrow(IllegalArgumentException::new); user.loanBook(book.getName()); }위에서 UserLoanHistory 객체를 직접적으로 사용하지 않고 있다. user 객체의 함수인 loanBook를 불러오고 있는데 여기 메소드를 살펴보면, public void loanBook(String bookName){ this.userLoanHistories.add(new UserLoanHistory(this, bookName));User 의 필드중 userLoanHistories 에 UserLoanHistory 객체를 집어넣는다.이렇게 바꾸어서 User 와 UserLoanHistory 2개 객체가 서로 협력하도록 변경했다.section 6배포를 하기 위해서는 aws 의 ec2를 사용한다.ec2는 계속 돌아가는 전용 컴퓨터와 비슷한 개념이다. ec2 인스턴스를 생성한 후, 이에 ssh 연결하여 필요한 것들을 설치한 후, 프로젝트 build 후 실행을 background에서 하면 된다.회고강의를 90% 들은 시점에서 들은 생각은 이 강의와 인프런 워밍업 클럽 스터디를 하기 잘했다는 것이다. 이제는 기본적으로 api를 보낼 수 있으니, 앞으로는 시큐리티 부분과 테스트 코드 위주로 공부를 더 해나가려 한다.