실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기

Ch03. 요구사항 추가(type, 대출현황) - UserLoanHistory의 isReturn을 Enum으로 변경하기

webmaster 2022. 11. 5. 23:55
728x90

Boolean 필드의 단점

현재 UserLoanHistory 엔티티 같은 경우 isReturn 필드가 Boolean으로 되어있다. -> Enum으로 사용하는 것이 더 좋아 보인다.

  • 만약 Boolean 필드를 사용하는 필드가 많이 발생하게 된다면? -> 2^n 을 경우에 수가 발생하고, 개발자가 고려해야 할 로직이 많아진다.
  • Enum 필드로 관심사가 같은 필드끼리는 묶어서 개발하면, 관리하는 필드가 줄고, 경우의 수도 줄어든다.
  • 또한 Boolean 필드를 많이 가지게 되면, 비즈니스 적으로 불가능한 경우(DB에 존재할 수 없는 조합)가 발생할 수도 있다.
  • EnumType을 사용하면, 필드 1개로 여러 상태를 표현할 수 있기에 코드의 이해가 쉬워지고, 정확하게 유의미한 상태만 나타낼 수 있기 때문에 코드의 유지보수가 용이해진다.
  • 현재 isReturn 필드 같은 경우 지금은 단순 대출/ 반납 기능만 하지만 나중에는 장기 대출, 임시 반납 등 새로운 기능이 추가될 확률이 크기 때문에 Enum을 적극적으로 활용하는 게 좋다.

Enum

UserLoanStatus

enum class UserLoanStatus {
    RETURNED, //반납 되어 있는 상태
    LOANED, //대출 중인 상태
}

UserLoanHistory

@Entity
class UserLoanHistory(
    @ManyToOne
    val user: User,
    val bookName: String,
    var status: UserLoanStatus = UserLoanStatus.LOANED, //Enum으로 설계하는것이 더 좋다

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
) {
    fun doReturn() {
        this.status = UserLoanStatus.RETURNED
    }

    companion object {
        fun fixture(
            user: User,
            bookName: String = "이상한 나라의 엘리스",
            status: UserLoanStatus = UserLoanStatus.LOANED,
            id: Long? = null,
        ): UserLoanHistory {
            return UserLoanHistory(
                user = user,
                bookName = bookName,
                status = status,
                id = id,
            )
        }
    }
}

BookService

@Transactional
fun loanBook(request: BookLoanRequest) {
    val book =
        bookRepository.findByName(request.bookName) ?: fail() //항상 반복되므로 리펙토링 가능
    if (userLoanHistoryRepository.findByBookNameAndStatus(request.bookName, UserLoanStatus.LOANED) != null) {
        throw IllegalArgumentException("진작 대출되어 있는 책입니다")
    }
    val user =
        userRepository.findByName(request.userName) ?: fail()
    user.loanBook(book)
}

UserLoanHistoryRepository

interface UserLoanHistoryRepository : JpaRepository<UserLoanHistory, Long> {
    fun findByBookNameAndStatus(bookName: String, status: UserLoanStatus): UserLoanHistory?
}

BookServiceTest

@SpringBootTest
class BookServiceTest @Autowired constructor(
    private val bookService: BookService,
    private val bookRepository: BookRepository, //마지막에 콤마를 찍을 수 있다 -> git diff에 변하는 부분만 보기 위해 해당 기능 허용
    private val userRepository: UserRepository,
    private val userLoanHistoryRepository: UserLoanHistoryRepository,
) {

    @AfterEach
    fun clean() {
        bookRepository.deleteAll()
        userRepository.deleteAll()
    }

    @Test
    @DisplayName("책 등록이 정상 동작한다")
    fun saveBookTest() {
        //given
        val request = BookRequest("이상한 나라의 엘리스", BookType.COMPUTER)

        //when
        bookService.saveBook(request)

        //then
        val books = bookRepository.findAll()
        assertThat(books).hasSize(1)
        assertThat(books[0].name).isEqualTo("이상한 나라의 엘리스")
        assertThat(books[0].type).isEqualTo(BookType.COMPUTER)
    }


    @Test
    @DisplayName("책 대출이 정상 동작한다")
    fun loanBookTest() {
        //given
        bookRepository.save(Book.fixture("이상한 나라의 엘리스"))
        val savedUser = userRepository.save(
            User(
                "최태현",
                null
            )
        )
        val request = BookLoanRequest("최태현", "이상한 나라의 엘리스")

        //when
        bookService.loanBook(request)

        //then
        val results = userLoanHistoryRepository.findAll()
        assertThat(results).hasSize(1)
        assertThat(results[0].bookName).isEqualTo("이상한 나라의 엘리스")
        assertThat(results[0].user.id).isEqualTo(savedUser.id)
        assertThat(results[0].status).isEqualTo(UserLoanStatus.LOANED)
    }


    @Test
    @DisplayName("책이 진작 대출되어 있다면, 신규 대출이 실패한다")
    fun loanBookFailTest() {
        //given
        bookRepository.save(Book.fixture("이상한 나라의 엘리스"))
        val savedUser = userRepository.save(User("최태현", null))
        userLoanHistoryRepository.save(UserLoanHistory.fixture(savedUser, "이상한 나라의 엘리스")) //대출 중인 상태
        val request = BookLoanRequest("최태현", "이상한 나라의 엘리스")

        //when & then
        assertThrows<IllegalArgumentException> {
            bookService.loanBook(request)
        }.apply {
            assertThat(message).isEqualTo("진작 대출되어 있는 책입니다")
        }
    }

    @Test
    @DisplayName("책 반납이 정상 동작한다")
    fun returnBookTest() {
        //given
        val savedUser = userRepository.save(
            User(
                "최태현",
                null
            )
        )
        userLoanHistoryRepository.save(
            UserLoanHistory.fixture(
                savedUser,
                "이상한 나라의 엘리스",
            )
        ) //대출 중인 상태
        val request = BookReturnRequest("최태현", "이상한 나라의 엘리스")

        //when
        bookService.returnBook(request)

        //then
        val results = userLoanHistoryRepository.findAll()
        assertThat(results).hasSize(1)
        assertThat(results[0].status).isEqualTo(UserLoanStatus.RETURNED)
    }
}
  • Test 같은 경우 fixture 메서드로 변경해 최대한 영향을 받지 않게 하자
  • Enum 필드로 변경해, 오류가 나는 부분을 수정하자
  • Repository 같은 경우, 실제 런타임 오류(SpringDataJPA가 인터페이스를 만들면서 오류가 발생)가 발생하기 때문에 좋지 않다 -> 후에 QueryDSL로 변경하면 이러한 문제를 해결할 수 있다.
728x90