[TIL] Day24_도서관 API

오진선·2024년 2월 28일

TIL

목록 보기
15/29
post-thumbnail

Today I Learned

도서관 API 만들기

https://github.com/choisasa/Books

페어 프로그래밍으로 진행했지만...
처음에 상대분이 개인으로 진행한다고 생각해서 기본적인 틀을 다 만들어 두셔서
초반에는 조금 쉽게 진행할 수 있었다.
그러나 너무 간단히 풀 수 있을 거라고 생각했던 걸까 시간이 부족해서 혼났다.
어제 좀 더 열심히 해볼 걸 그랬다는 후회가 된다.
수환님이 도와주셔서 그래도 보다 수월하게 해결할 수 있었다.
물론 그럼에도 에러는 아주 빵빵 터졌다.

눈물 없이 볼 수 없는 페어프로그래밍의 애환
이하 해결해 낸 에러들과 문제점들을 기록..

CreatedAt null Error

일단 첫번째 문제는 실행을 해주는 BookApplication 클래스에
@EnableJpaAuditing 어노테이션 설정이 되어있지 않았기 때문이었다.

그러나 어노테이션을 붙여도 계속 문제가 생겼고 대체 왜 그러는지 어제 밤부터 오늘 아침까지 계속 찾아봤는데 결과적으로...

Book 엔티티 클래스에 파트너 분께서 createdAt 컬럼을 만들어 두었기 때문이었다..
먼저 Book 부분을 만들어 주셔서 미처 생각을 못 했는데 내가 만든 부분이 아니어도 잘 유심히 살펴봐야겠다는 생각을 했다.

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimestampedBook {

    @Column(updatable = false)
    @CreatedDate
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime createdAt;

}

이렇게 타임스탬프 클래스를 따로 만들어서 자동으로 값이 들어가도록 설정해 둔 뒤에 북 엔티티에서 상속 받는 방식으로 로직을 짜두었는데 북 엔티티에서 또다시 createdAt이 있어서 자동생성이 되고 있지 않았던 것이 이유였다.

항상 이렇게 어처구니 없게 해결하게 되는 것이 정말 열받지만 즐거워...

the given id must not be null

이 에러는 id가 null이 되면 안되는데 null이야!
라고 알려주는 에러이다.

여기에 대해서도 참 할 말이 많다.

foreign key를 설정해 두면 Entity 내부에서는 Book book 과 같이 Book의 Entitiy를 가져오게 되어 있다.
이점을 망각하고 계속 bookId를 Book 객체에 넣고 있었으니 id가 null이 뜰 수밖에...

Dto에 있는 BookId와 MemberId를 Long으로 바꾸고 requestDto에는 Id값을 가져와서 Service 단에서 findById로 북과 멤버 객체를 찾아와주고 toEntity 메소드를 북과 멤버 객체를 파라미터 값으로 넣어주도록 설정한 뒤 Borrow Entity를 만들어 저장했다.

  public Borrow toEntity(Book book, Member member) {
        return Borrow.builder()
                .book(book)
                .member(member)
                .build();
    }

이런 식으로... 빌터 패턴도 사용해보고 싶어서 낑낑댔는데 성공해서 기분이 좋았다.

HttpMediaTypeNotAcceptableException

클라이언트가 요청한 것과 실제로 생성할 수 있는 것이 다를 경우 발생하는 에러였다.
어떻게 잡았는가 하면 @Getter가 없어서... 였습니다.

미친 건지 뭔지 Dto에 @Getter를 안달았었네요.
너무 간단한 건데 시간을 꽤나 많이 쏟았다.
바보... 멍청이....

cannot invoke because is null

borrow가 자꾸 null이 되었다..
생성자 주입이 되지 않아서 발생한다고 그래서 확인을 수십번 했지만 아무런 일도 일어나지 않았다.
정답은 뭐였냐...!

대출을 구현하는 서비스에서 대출 가능 도서인지를 조회하는 처리가 있었는데 우리는 이 처리에 대한 메소드를 만들어 코드 중복을 줄이려 했었다. 그런데 한 가지 예상하지 못한 일.. 한 번 데이터베이스에 있는 테이블을 전부 지우고 다시 만들었는데 그 과정에서 테이블 안에 있던 테스트 데이터들이 전부 사라졌다. 처음 만들 때에는 대출 가능 도서인지 조회하는 부분이 없었고 따라서 대출 기록 데이터가 있었기 때문에 이후에 이 부분을 추가했을 때에도 잘 구동이 되었지만 테이블을 갈아엎고 난 뒤에는 데이터가 없으니 borrow를 가져오면 무조건 null이 될 수밖에 없었다.

여기서 혼란에 빠졌다.
일단 처음에는 이유를 몰라서 허둥거리며 시간을 썼고
(왜냐면 아무리 찾아봐도 생성자 주입이 문제라는 글 뿐이었기 때문...)
기술 매니저님 면담 후 함께 디버깅 하며 문제점을 알게 되기는 했지만 대출 가능 도서인지 조회는 해야만 했다. 한참 이걸 어쩌나 하고 있었는데 답은 언제나 알게 되면 쉬운 길이다. 그냥 대출 기록이 있는지 조회를 해서 예외처리를 한 번 더 하면 되는 일이잖아...

그렇게 완성된 borrowBook Service..

// 대출
    public void borrowBook(BorrowRequestDto borrowRequestDto) {

        // 도서 조회
        Book book = bookRepository.findById(borrowRequestDto.getBookId()).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 도서입니다."));

        // 멤버 조회
        Member member = memberRepository.findById(borrowRequestDto.getMemberId()).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));

        // Dto -> Entity
        Borrow borrow = borrowRequestDto.toEntity(book, member);

        // 대출 기록이 있는지 조회 -> 없으면 if문 실행
        if (!borrowRepository.existsByBook(book)) {

            // 대출 상태 변경
            borrow.returnConvert();

            // 대출 기록 저장
            borrowRepository.save(borrow);
        }

        // 대출 가능 도서인지 조회(false -> 대출됨, true -> 대출 가능)
        if (!isBookAvailableForBorrow(book)) {
            throw new IllegalArgumentException("이미 대출된 도서입니다.");
        }

        // 반납 상태 변경 (true -> false)
        borrow.returnConvert();

        // 대출 기록 저장
        borrowRepository.save(borrow);
    }
    
        // 대출 가능 도서인지 조회해주는 method
    private boolean isBookAvailableForBorrow(Book book) {

        // 도서 식별값으로 대출 내역 조회
        Borrow borrow = borrowRepository.findByBook(book);

        // 대출 내역 반환
        return borrow.isReturnStatus();
    }
    

고된 길이었다..
기술 매니저님이 Repository에서 existsBy 사용하는 법을 알려주셔서 그걸 적용해 더 간단하게 만들었다.

    // count 있으면 true, 없으면 false
    boolean existsByBook(Book book);

HttpStatus Code 반환

이 부분도 기술매니저님 면담을 통해 알아냈다.
왜 알면 참 쉬울까..?^^
파트너 분께서 작성한 코드가 맞는 코드였다. 아무 것도 모르고... 바보처럼 나는 또 돌아돌아 가게 되었던 것...
왜냐면 위의 borrow null 에러가 이 코드 작성 바로 직후에 터져서 혹시 이것 때문인가 싶어가지고 주석처리 해두어서.. 몰랐다.

그렇게 완성한 Controller

// 대출
    @PostMapping
    public ResponseEntity<?> borrowBook(@RequestBody BorrowRequestDto borrowRequestDto) {
        try{
            borrowService.borrowBook(borrowRequestDto);
            return ResponseEntity.ok("도서 대출에 성공했습니다.");
        } catch (IllegalArgumentException e){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }

ResponseEntity<?>를 반환해주는 값으로 두고 객체 내부에 존재하는 메소드를 가져다 쓰면 짜잔 하고 멋지게 에러메시지도 출력되고 상태 코드도 알 수 있다.

예외 처리는 이제 이런 방식으로 하는 걸로..
사실 Handler 쓰는 거 멋져보여서 적용시켜보고 싶었지만 시간이 너무 부족했다. 다음 과제때 시간이 괜찮으면 적용시켜봐야겠다. 일단은 자고... 내일 과제 또 시작하니까 내일 해야지..


말도 많고 탈도 많았던 과제가 끝나고
역시 직접 만들어보고 부딪혀봐야 알아가는 게 많구나 생각했다. 강의로는 절대 알 수 없을 것들을 많이 알게 되었다.
어떻게 작동하는지도 에러를 만나면서 하나 둘 익히게 되니 고된 길이긴 했지만 어느 때보다 즐거웠다. 도파민이 아주..
이래서 도박하나 싶다. 엉엉 울다가도 성공만 하면 진짜..

아무튼 어제 오늘 고생한 흔적..

잘했다 로사야..
그래도 다음번엔 꼭.. 꼬옥... 테스트 코드도 작성해보는 거야... 꼬ㄱ...
깃모지 쓰는 법도 제대로 배우고..

profile
₍ ᐢ. ̫ .ᐢ ₎

0개의 댓글