인프런 워밍업 클럽 BE 1기 - 3주차 발자국
조금 더 객체지향적으로 개발할 수 없을까?
이전에는 User를 따로 가져와 BookService에서 UserLoanHistory를 만들어 저장하였다.
조금 더 객체지향적으로 바꾸면 UserLoanHistory를 User에서 가져와 바로 대출을 처리하자!
@ManyToOne : 내가 다수이고 너가 한개
대출기록은 여러개이고 그 대출을 소유하고 있는 사용자는 한명이다 → N : 1 관계
//private long userId;
@ManyToOne
private User user;
@OneToMany : 나는 한개 너가 다수
@OneToMany
privateList<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
여러개의 대출기록 N개이기 때문에 List로 표현
이로써 User와 UserLoanHistory는 서로 연관관계가 되었다. 그 중에 주인은 누구인가?
누가 관계의 주도권을 가지고 있는가?
현재는 user_loan_history가 user_id를 DB 컬럼으로 가지고 있기 때문에 주인이다.
연관관계의 주인이 아닌 쪽에 mappedBy 옵션을 달아 주어야 한다.
@OneToMany(mappedBy = "user")
privateList<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
1:1 관계
한 사람은 한 개의 실거주 주소 만을 가지고 있다.
person 테이블이 address 테이블의 id를 가질 수도 있고, address테이블이 person 테이블의 id를 가질 수도 있다.
@Entity public class Person{ ... @OneToOne private Address address; }
@Entity public class Address{ ... @OneToOne private Person person; }
이렇게 테이블을 생성한다고 가정할때 Person이 address id를 가지고 있다.
create table person ( id bigint auto_increment, name varchar(255), address_id bigint, primary key (id) );
create table address ( id bigint auto_increment, city varchar(255), street varchar(255), primary key (id) );
→ Person이 address 주인이다! 1:1 관계지만..
따라서 mappedBy 작성
@Entity public class Address{ ... @OneToOne(mappedBy = "address") private Person person; }
연관관계의 주인 효과 → 객체가 연결되는 기준이 된다.!
@Transactional public void savePerson() { Person person = personRepository.save(new Person()); Address address = addressRepository.save(new Address()); person.setAddress(address); // setter는 임시 }
person이 address의 주인이기때문에 이렇게 코드를 작성하고 실행하면 DB에서 정상으로 테이블이 연결된다.
하지만 반대로 1:1관계이더라도 address.setPerson(person);
이렇게 작성할 경우 DB에 저장되지 않는다.
DB는 연결되었지만 !!! 객체끼리는 연결되지 않았다.
address.getPerson(); --> 트랜잭션이 끝나기전에 get하면 Null 반환
@Transactional public void savePerson() { Person person = personRepository.save(new Person()); Address address = addressRepository.save(new Address()); person.setAddress(address); // setter는 임시 address.getPerson(); --> 트랜잭션이 끝나기전에 get하면 Null 반환 }
해결책으로 setter 한번에 둘을 같이 이어주면 된다.
public void setAddress(Address address) { this.address = address; this.address.setPerson(this); --> 둘을 같이 이어주자 }
N : 1 관계
이 관계에서 주인은 무조건 숫자가 많은쪽이 주인이다.
@OneToMany를 작성하지 않고, @ManyToOne 하나만 작성해도 된다.
(단방향)
@JoinColumn
연관관계의 주인이 활용할 수 있는 어노테이션.
필드의 이름이나 null 여부, 유일성 여부, 업데이트 여부 등을 지정
@JoinColumn(nullable = false) @ManyToOne private User user;
N : M 관계 - @ManyToMany
학생과 동아리 관계를 생각하자
학생은 여러 동아리를 가입할 수 있고, 한 동아리엔 여러 학생이 있다.
→ But, 구조가 복잡하고, 테이블이 직관적으로 매핑되지 않아 사용하지 않는 것을 추천
cascade 옵션
cascade : 폭포처럼 흐르다.
: 한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 플러 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능
유저가 삭제될때 유저가 연결되어 있는 UserLoanHistory도 삭제하고 싶을때 사용하면 된다.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL) privateList<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
orphanRemoval 옵션
: 객체간의 관계가 끊어진 데이터를 자동으로 제거하는 옵션
관계가 끊어진 데이터 = orphan(고아) removal (제거)
한 유저가 빌린 책1, 책2가 있다고 가정할 때, userLoanHistory에서 책1만 리스트(자바단)에서 지웠다. → DB는 아무런 변화가 없다.
이렇게 리스트에서 지우는(연결을 끊는 것 만으로도) 것으로도 DB에서 삭제가 되길바라면 이 옵션을 쓸 수 있다.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) privateList<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
@Transactional public void deleteUserHistory() { User user = userRepository.findByName("ABC") .orElseThrow(IllegalArgumentException::new); user.removeOneHistory(); }
public void removeOneHistory() { userLoanHistories.removeIf(history -> "책1".equals(history.getBookName())); }
책 대출/반납 기능 리팩토링과 지연로딩
User와 UserLoanHistory가 직접적으로 연결되어 있다.
대출기능 리팩토링
public void loanBook(String bookName) { this.userLoanHistories.add(new UserLoanHistory(this, bookName)); }
User.java에서 새로운 UserLoanHistory객체를 만들고 넣어주기 때문에 service단에서는 loanBook메소드를 호출해주기만 하면 된다.
@Transactional public void loanBook(BookLoanRequest request) throws IllegalAccessException { Book book = bookRespository.findByName(request.getBookName()) .orElseThrow(IllegalAccessException::new); if (userLoanHistoryRepository.existsByBookNameAndIsReturn(book.getName(), false)) { throw new IllegalAccessException("이미 대출중인 책입니다."); } User user = userRepository.findByName(request.getUserName()); if(user == null) { throw new IllegalAccessException("사용자를 찾을 수 없습니다."); } //userLoanHistoryRepository.save(new UserLoanHistory(user, book.getName())); user.loanBook(book.getName()); }
반납기능 리팩토링
public void returnBook(String bookName) throws IllegalAccessException { UserLoanHistory targetHistory = this.userLoanHistories.stream() .filter(history -> history.getBookName().equals(bookName)) .findFirst() .orElseThrow(IllegalAccessException::new); targetHistory.doReturn(); }
함수형 프로그래밍을 할 수 있게 .stream을 작성해주고
.filter를 통해 들어오는 객체들 중에 다음 조건을 충족하는 것만 필터링 한다
.
.findFirst()를 통해 첫번째로 해당하는 UserLoanHistory를 찾는다.
이 결과는 Optional이기에 Optional을 제거하기 위해 없으면 예외를 던진다.
orElseThrow
그렇게 찾은 UserLoanHistory를 반납처리 한다.
@Transactional public void returnBook(BookReturnRequest request) throws IllegalAccessException { User user = userRepository.findByName(request.getUserName()); if(user == null) { throw new IllegalAccessException("사용자를 찾을 수 없습니다."); } // UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(user.getId(), request.getBookName()) // .orElseThrow(IllegalAccessException::new); // history.doReturn(); user.returnBook(request.getBookName()); }
userLoanHistoryRepository.findByUserIdAndBookName()은 사용할 일이 없어져서 삭제해었다.
→ Domain 계층에 비즈니스 로직이 들어갔다.
영속성 컨텍스트의 4번째 능력 - 지연 로딩 (Lazy Loading)
예시 - User를 가져오는 부분과, 도메인 로직 실행 중간에 Print 출력을 해보자
@Transactional public void returnBook(BookReturnRequest request) throws IllegalAccessException { User user = userRepository.findByName(request.getUserName()); if(user == null) { throw new IllegalAccessException("사용자를 찾을 수 없습니다."); } Systme.out.println("Hello"); user.returnBook(request.getBookName()); }
실행결과를 확인하면 user를 가져오는 쿼리, hello 출력 후 userLoanHistory 쿼리를 출력하는 것을 확인할 수 있다.
→ 시작하자마다 유저와 대출기록을 다 가져올 수 있지만 그러지 않고, 꼭 필요한 순간에 데이터를 가져온다.
이러한 것을 지연 로딩이라 한다.
@OneToMany에 fetch 옵션에 기본 디폴트 값이다 LAZY
만약 한번에 가지고 오고 싶다면 LAZY → EAGER를 사용하면 된다.
@OneToMany(mappedBy = “user”, cascade = CadcadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
연관관계를 사용하면 무엇이 좋을까?
각자의 역할에 집중하게 된다. 계층별로 응집성이 높아진다.
새로운 개발자가 코드를 읽을 때 이해하기 쉬워진다. (모두 다 service단에 있다면 ?)
테스트 코드 작성이 쉬워진다.
연관관계를 사용하는 것이 항상 좋을까?
지나치게 사용하면 성능상 문제가 생길 수 있고, 도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수 있다.
너무 얽혀 있으면 A를 수정했을 때 , B ~ D까지 고쳐야할 수 있다.
→ 여러부분을 생각해서 구조를 짜자!
배포란 무엇인가
현재 우리 컴퓨터에서만 작동할 수 있는 환경..
배포란 : 최종 사용자에게 SW를 전달하는 과정 → 전용 컴퓨터에 우리의 서버를 옮겨 실행시키는 것
전용 컴퓨터가 없는데..? → AWS를 빌리자!!
AWS에서 컴퓨터를 빌릴 때 한 가지 알아두어야 할점!
서버용 컴퓨터는 보통 리눅스를 사용한다.
profile과 H2 DB
똑같은 서버 코드를 실행시키지만, 우리 컴퓨터에서 실행할 때는 우리 컴퓨터의 MySQL을 사용하고,
전용 컴퓨터에서 실행할 때는 전용 컴퓨터의 MySQL을 사용하고 싶다
→ profile 개념 똑같은 서버 코드를 실행시키지만, 실행될 때 설정을 다르게 하고 싶을 때
(DB 이외에도 다른 API, 다른 기능 등 ..)
Profile을 적용해보자
똑같은 서버 코드를 실행시키지만, local이라는 Profile을 입력하면, H2 DB를 사용하고 dev라는 profile을 입력하면 MySQL DB를 사용하게 바꾸자
H2 DB란 : 경량 Database로, 개발 단계에서 많이 사용하며 디스크가 아닌 메모리에 데이터를 저장할 수 있다.
메모리에 데이터를 저장하면 휘발되지만, 개발 단계에서 테이블이 계속 변경 되기 때문에 데이터가 휘발해야하고 ddl-auto 옵션을 create로 주면 테이블을 신경쓰지 않고 코드에만 집중할 수 있다.
profile 설정
나는 인텔리제이 유료버전이라 강사님 처럼 active profile 부분이 안뜨기 때문에 다른방법으로 진행하였다.
-Dspring.profiles.active=local
local 또는 dev로 입력해주면 된다.
AWS의 EC2 사용하기
AWS 가입 후 지역을 서울로 변경
EC2
인스턴스 - 내가 빌린 컴퓨터 (현재 0)
인스턴스 시작 버튼 클릭
인스턴스 유형은 컴퓨터의 사양
t2.micro 선택 !
회고
벌써 워밍업도 마지막을 향해 달려가는 중이다!!
7번째 과제에서 아직 나는 JPA에 익숙하지않아서 뭔가 직접 쿼리를 쓰는게 더 편했던 것 같다. @Query를 통해 직접 작성해주었다가 어? 이렇게하면 JPA에서 알아서 해주는구나 하고 여러번 바꾸기도 했다!
근데 만약 쿼리에 조건이 너무 많을 경우에는 이름이 너무 길어지지 않을까라는 생각도 했었다 ㅎ..
중간에 자꾸 테이블에 해당 컬럼이 없다고 오류가 나길래 .. 그럴리가 없는데라고 생각했지만 그럴리 있었다. 역시 컴퓨터는 거짓말을 하지 않는다 하하
확실히 강의만 보는 것보다 직접 내가 생각하고 타이핑하는 것이 내가 어느정도까지 실제적으로 이해했는지 확인할 수 있는 길인 것 같다.
앞으로 남은 마지막 미니 프로젝트까지 잘 마무리하면서 개념 부분을 더 공부해보자!
댓글을 작성해보세요.