[인프런 워밍업 클럽 1기/BE] 3번째 발자국

[인프런 워밍업 클럽 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를 보낼 수 있으니, 앞으로는 시큐리티 부분과 테스트 코드 위주로 공부를 더 해나가려 한다.

댓글을 작성해보세요.

채널톡 아이콘