[인프런 워밍업 클럽 1기/BE] 3번째 발자국
section5
31강. 대출 기능 개발하기
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 내용이다.
DTO
public 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를 보낼 수 있으니, 앞으로는 시큐리티 부분과 테스트 코드 위주로 공부를 더 해나가려 한다.
댓글을 작성해보세요.