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

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

3주차 발자국을 찍으며..

목표를 세우는 것을 좋아하는데, 그 목표가 무너질 때마다 조금씩 힘이 들었던 것 같다.
매 정해진 날짜의 강의만큼은 다 들어야지 생각을 했었는데, 이번주는 여러 일이 겹치며 다 듣지 못해 너무 아쉽고 완주조건인 100%를 언제까지 채웠어야 하는 지는 모르겠으나 어떻게든 짬을 내 최선을 다 해 마무리 완강하고자 한다.
완강 한 이후 이 발자국에 수정하여 적거나, 다음주차 발자국이라도 찍어서 완성했다는 뿌듯함을 꼭 느끼고 싶다.
그래도 배포 전 개념적으로 배워야 하는 수업까지는 완료했기 때문에 큰 걱정은 덜은 것 같다.
git이나 데이터베이스, 리눅스 서버, 배포에 대해서는 어느정도 알고 해보았기 때문에 수업을 이전보다는 어렵지 않게 마무리 할 수 있을 것으로 예상한다.
수업 마무리 열심히 듣고 마지막으로 다음주까지 미니 프로젝트에 대한 과제도 있는데,
미니 프로젝트를 그동안 열심히 배운 것을 활용해서 재밌게 최대한 좋게 잘 만들어 보려고 한다.


섹션5. 책 요구사항 구현하기

조금 더 복잡한 기능을 API로 구성하기. 30~32강

  • 섹션5. 책 요구사항 구현하기

    • 책 생성, 대출, 반납 API를 온전히 개발하며 지금까지 다루었던 모든 개념을 실습해본다.

    • 객체지향적으로 설계하기 위한 연관관계를 이해하고, 연관관계의 다양한 옵션에 대해 이해한다.

    • JPA에서 연관관계를 매핑하는 방법을 이해하고,
      연관관계를 사용해 개발할 때와 사용하지 않고 개발할 때의 차이점을 이해한다.

 

  • [책 생성 API 개발하기]

  • HTTP Method : POST

  • HTTP Path : /book

  • HTTP Body(JSON)

{
  "name" : String // 책 이름
}
  • 결과 반환 X (HTTP 상태 200 OK이면 충분하다)

  • ① book 테이블 생성

    create table book (
    	id bigint auto_increment,
      name varchar(255),
      primary key (id)
    );
    
    • JPA @column의 length 기본값이 255라서 테이블까지 255로 맞추면 @column 생략 가능, 문자열 필드는 최적화를 해야하는 경우가 아니면 조금 여유롭게 설정하는 것이 좋다.

    • 컬럼 뒤에 not null을 써주면 null을 넣을 수 없음

  • ② Book 객체 생성

     

    image③ BookRepository 생성

image④ DTO 생성

image⑤ Controller와 Service 생성, 위와 같은 순서로 완성해나갔다. 비슷한 방식으로 나머지 기능을 완성해나간다.

 

  • [대출기능 개발하기]

    • 사용자가 책을 빌릴 수 있다. 다른 사람이 그 책을 진작 빌렸다면, 빌릴 수 없다.

  • API 스펙

    • HTTP Method : POST

    • HTTP Path : /book/loan

    • 결과 반환 X (HTTP 상태 200 OK이면 충분하다)

    • HTTP Body(JSON)

       

{
  "userName" : String,
  "bookName" : String
}
  • ⇒ 이것만으론 대출 기록을 저장할 수 없다.(User/Book 테이블만 가지고는), 새 테이블 필요

  • user_loan_history 테이블 생성 + domain 생성

  • boolean으로 객체를 처리하면 DB의 tinyint에 잘 매핑된다!(0-false, 1-true)

  • ⑥ DTO / Controller / Service 구현

  • [반납기능 개발하기]

    • 요구사항 : 사용자가 책을 반납할 수 있다.

  • API 스펙

  • HTTP Method : PUT

  • HTTP Path : /book/return

  • 결과 반환 X (HTTP 상태 200 OK이면 충분하다)

  • HTTP Body(JSON)

     

{
  "userName" : String,
  "bookName" : String
}
  • 테이블은 건드릴 것이 없고 DTO / Controller / Service 구현하면 될 것 같음

  • 반납 / 대출 API 스펙이 완전히 동일하다.

    • 이런 경우, DTO를 새로 만드는게 좋을까? 아니면 재활용하는게 좋을까?

    • 강사님은 새로 만드는 것을 선호, 나도 그렇게 생각하는 편이다.

    • 그래야 두 기능 중 한 기능에 변화가 생겼을 때 유연하고 side-effect 없이 대처할 수 있기 때문

  • 반납 기능까지 구현완료

     

     

  • 여기서 고민할만한 내용이 있다.

    • Java 언어는 객체지향적 언어이고, 대규모 웹 어플리케이션을 다룰 때에도
      절차지향적인 설계보다 객체지향적인 설계가 좋다!

    • "20강에서 스프링 컨테이너를 왜 쓸까?" 를 살펴보았던 이유 역시
      보다 객체지향적인 설계를 하기 위한 맥락에서 출발했다.

    • 지금 코드를 조금 더 객체지향적으로 만들 수 없을까??

      • User/UserLoanHistory가 직접 협업할 수 있게 처리할 수 없을까?

     

객체지향과 JPA 연관관계. 33~36강

[33. 조금 더 객체지향적으로 개발할 수 없을까?]

  • 대출 기능 개선 방향

image

⇒ 위의 것을 아래와 같이 바꿀 수 있겠다

image

  • 현재 반납 기능 상황

image

⇒ 아래와 같이 개선할 수 있겠다.

image

  • 이것들의 선행조건 : User와 UserLoanHistory가 서로를 알아야 한다.

  • N : 1 관계

    • 학생 여러명이 교실에 들어갈 수 있다. 학생N : 교실1

    • @ManyToOne(UserLoanHistory 클래스에 있는 User에),
      @OneToMany(User 클래스에 있는 UserLoanHistory에) 어노테이션 붙여주기

  • 연관관계의 주인

    • Table을 보았을 때 누가 관계의 주도권을 가지고 있는가

    • UserLoanHistory가 User를 가리키고 있음(주도권이 있음)

    • 연관관계의 주인이 아닌 쪽에 mappedBy 옵션을 달아주어야 한다.

    • 연관관계의 주인의 값이 설정되어야만 진정한 데이터가 저장된다.

    image


[34. 연관관계]

  • 1:1 관계

    • ex) 사람 : 실거주 주소

    • 서로 Entity에 포함되어 있음(연결되어 있음)

    image

    • DB 테이블은 한 쪽만 상대의 id를 가지고 있어도 연결시킬 수 있다.

    • person이 address의 id를 가지고 있다고 가정

    • ⇒ person이 연관관계의 주인.

    • 이 때도 주인이 아닌 쪽에 주인 Entity의 필드명을 mappedBy를 붙여줘야 한다.

  • 연관관계의 주인 효과

    • 객체가 연결되는 기준이 된다.

    image

    • 주인 Entity에서 setter를 해주지 않으면 DB 상에서 연결이 되어있지 않게 된다.

  • 결론

    • 상대 테이블을 참조하고 있으면 연관관계의 주인

    • 연관관계의 주인이 아니면 mappedBy를 사용

    • 연관관계의 주인의 setter가 사용되어야만 DB 테이블이 연결됨

  • 연관관계 사용 시 주의사항

    • 트랜잭션이 끝나지 않았을 때, 한쪽만 연결해둔 상태에선 반대쪽에선(반대 객체에선) 값을 알 수 없다.

    • 해결법

      • setter를 한 번에 둘을 같이 이어주자

      • 주인 쪽의 setter에서 반대 객체의 setter를 불러 자신을 넣어주면 된다.

  • N:1 관계

    • @ManyToOne과 @OneToMany

    • 연관관계의 주인은 항상 N쪽임

    • @ManyToOne은 단방향으로 사용할 수도 있다.
      (User Entity에서 mappedBy가 붙은 것을 삭제해도 정상동작한다)

    • @JoinColumn

      • 연관관계의 주인이 활용할 수 있는 어노테이션(주인쪽에만 붙임)

      • 필드의 이름이나 null 여부, 유일성 여부, 업데이트 여부 등을 지정

  • N:M 관계(@ManyToMany)

    • ex) 학생과 동아리의 관계

    • 구조가 복잡하고, 테이블이 직관적으로 매핑되지 않아 사용하지 않는 것을 추천

    • 이 경우 ManyToOne으로 풀어헤칠 수 있음

      • 1:N 관계를 두 개 연결시켜놓은 것처럼 풀어나가는 방식을 추천

  • cascade 옵션

    • 한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러
      연결되어 있는 객체도 함께 저장되거나 삭제되는 기능

    • @OneToMany 쪽에(주인이 아닌 쪽에) 옵션으로 cascade = CascadeType.ALL 을 주면 된다.

    • ex) user가 지워질 때 user_loan_history 쪽 데이터도 함께 지워진다.

  • orphanRemoval 옵션

    • 이 옵션 없이 객체 내에서 user에서 user_loan_history 리스트의 하나를 지운다고해도
      DB에는 영향이 가지 않는다!

    • 이 옵션도 주인이 아닌 User쪽에서 @OneToMany 쪽에 옵션으로 orphanRemoval = true 를 주면 된다.

    • 객체 간의 관계가 끊어진 데이터를 자동으로 제거하는 옵션


[35. 책 대출/반납 기능 리팩토링과 지연 로딩]

image

* 위의 그림을 아래와 같이 변경해보자

image

  • BookService와 UserLoanHistory와의 관계를 없애고 User와 UserLoanHistory가 협업하게 변경하자

  • 도메인 계층에 있는 User와 UserLoanHistory가 직접적으로 협력하게 바뀐 것을 가리켜
    도메인 계층에 비즈니스 로직이 들어갔다 라고 표현하기도 함

image

  • 변경작업

@Transactional
    public void returnBook(BookReturnRequest request) {
        User user = userRepository.findByName(request.getUserName())
                .orElseThrow(IllegalArgumentException::new);

//        UserLoanHistory history = userLoanHistoryRepository.findByUserIdAndBookName(
//                        user.getId(), request.getBookName())
//                .orElseThrow(IllegalArgumentException::new);
//        history.doReturn();

        // 위의 코드를 쓰지 않으므로
        // 유저만 가지고 데이터를 처리할 수 있게됨
        user.returnBook(request.getBookName());
  • 영속성 컨텍스트의 4번째 능력

    • 반납기능을 리팩토링하면서 발동되었다

    image

    • @OneToMany의 fetch 함수 default가 LAZY라 이렇게 됨

    • 옵션을 fetch = FetchType = EAGER로 변경하면 바로 다 가져오게 됨

    • 지연로딩영속성 컨텍스트가 있어야만 가능함.

    • 즉, 트랜잭션 내에서만 가능함

    • 지연로딩을 사용하게 되면, 연결되어 있는 객체를 꼭 필요한 순간에만 가져온다.

  • 연관관계를 사용하면 무엇이 좋을까?

image

  • 서비스 계층의 역할

    • 꼭 필요한 경우 서로 다른 도메인끼리 협업할 수 있도록 도와주고

    • 트랜잭션 관리

    • 외부 의존성(스프링 빈이 필요한 상황) 관리

  • 도메인의 역할

    • 도메인 객체가 표현하고 있는 비즈니스, 관심사에 대해서 로직을 처리

     

  • 연관관계를 사용하는 것이 항상 좋을까? ⇒ 꼭 그렇지는 않다!

    • 지나치게 사용하면, 성능상의 문제가 생길 수도 있고
      도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수 있다.

    • 또한, 너무 얽혀 있으면 A를 수정했을 때 B C D까지 영향이 퍼지게 된다.

    • 비즈니스 요구사항, 기술적인 요구사항, 도메인 아키텍처 등 여러 부분을 고민해서 연관관계 사용을 선택해야 한다.


과제6. 과제#4에서 만들었던 API를 분리해보며,
Controller-Service-Repository 계층에 익숙해져보기

https://slime-feels-660.notion.site/8cf3f404a3bb43f6a0fe08637f0b27e6?pvs=4

6-1. 3계층 레이어로 분리하는 첫번째 문제는 과제#4를 할 때 이미 배운대로 분리해서 작성을 했다보니 엉겹결에 처리되었다.

6-2. @Qualifier와 @Primary를 이용해 인터페이스 레포지토리를 구현한 2가지 레포지토리를 필요에 따라 적용되도록 배운대로 적용해보는 과제였다. 수업에 나온 내용대로 해보면 되는 것이었기 때문에 6-1이 이미 되어 있어 조금 빠르게 마무리할 수 있었다.

과제7. 몇 가지 문제를 통해 JPA를 연습해 봅시다

https://slime-feels-660.notion.site/60c761713fc348a8b963539f11eca3ac?pvs=4

7-1. 테이블 변경을 하고, JPA에 맞게 원래 작성했던 코드에서 수정하였다.
(Entity에서는 카멜케이스로 작성, DB에서는 스네이크케이스로 작성하는 등..)
이유는 정확히 모르겠지만 @Query 어노테이션을 사용하면서 오류가 발생했었는데,
구글링 시 어노테이션 안에 오는 SQL은 JPQL을 사용하는 것이라는 것을 발견했고,
리턴타입으로 Entity가 아닌 것을 반환받으려면 Interface based Projection을 활용해야 한다는 글을 보고
이 인터페이스를 추가해 리턴타입으로 바꾼 후 추후에 Service단에서 원래 reponse타입으로
변경해주는 작업을 넣으니 결과가 잘 나왔었습니다.
※ 추후 더 공부해서 원리를 제대로 알고 쓸 수 있고 싶다는 생각을 했습니다.

7-2. 2번은 Response만 결과에 맞게 생성하고 count만 하면 되어 간단하게 할 수 있었던 과제였던 것 같습니다.

7-3. 1번과 마찬가지로 컬럼을 변경하고 DB와 연결하는 작업 등을 먼저 해주었습니다.
조금 작업이 필요한 과제였는데, 완성하고 나서 결과를 확인했을 때 되게 뿌듯한(?) 느낌이 들었습니다.
이걸로 미니 프로젝트를 제외한 모든 과제를 마무리했는데, 특히 마지막 과제를 하면서 API에 대해 어느정도 익숙해진
느낌이 들며 혼자 해냈다는 생각을 들게 해주어 보람찼습니다. 아마 과제를 내주신 강사님도 이런 느낌을 느끼길 바라 시며 내주신 것 같아서 이런 과제를 만들어주셔서 감사하다는 말씀을 드리고 싶었습니다.


마무리

3주가 진짜 순식간에 지나갔다. 이번주는 특히나 일이 겹쳐 강의도 계획대로 듣지 못했다.
하지만 17일 특강도 듣고 강사님께 질문도 해보며, 나름대로 개발에 대한 생각이라고 해야할까 이런 게 많이 바뀌었다.
하면 할수록 할 수 있는 것도 많아지는 것 같고 정말 재미있다고 생각한다.
조금 남아있는 수업 또한 마무리해서 완강을 찍을 수 있도록 할 것이고..
이렇게 앞으로도 공부할 것을 하나씩 작은 계획을 세우고 계획을 노력해 완성하고 그 계획을 완성했던 동안을 회고하며 이런 방식으로 또 공부할 수 있으리라는 생각을 하게 만들어주는 워밍업클럽이었다.
비록 아쉬운 부분이야 있지만 지금처럼 회고를 하며 의지를 계속 다짐하다보면 잘해나갈 수 있지 않을까 기대해본다.

댓글을 작성해보세요.

채널톡 아이콘