TIL_008 | 개인 과제 마감

묘한묘랑·2023년 12월 7일
0

TIL

목록 보기
8/31

개인 과제였던 호텔 예약 서비스 프로젝트를 마무리 하였다.

--
사용 언어kotlin
소요 기간1일
IDEIntteliJ
인원1인
얻은 것코틀린 문법 적응

프로젝트 Github

우선 마무리 된 프로젝트 구조이다.

📂lv4
┣ 📂hotelService
┃ ┣ 📂hotel
┃ ┃ ┗ 📜Hotel.kt
┃ ┣ 📂order
┃ ┃ ┣ 📂util
┃ ┃ ┃ ┗ 📜GetInputCheck.kt
┃ ┃ ┣ 📜AmountListOrder.kt
┃ ┃ ┣ 📜ExitOrder.kt
┃ ┃ ┣ 📜ModifyCancleOrder.kt
┃ ┃ ┣ 📜Order.kt
┃ ┃ ┣ 📜ResOrder.kt
┃ ┃ ┣ 📜ShowListOrder.kt
┃ ┃ ┗ 📜ShowSortListOrder.kt
┃ ┣ 📂room
┃ ┃ ┣ 📜Room.kt
┃ ┃ ┣ 📜RoomResInfo.kt
┃ ┃ ┗ 📜RoomResInfoDto.kt
┃ ┣ 📂user
┃ ┃ ┗ 📜User.kt
┃ ┗ 📂util
┃ ┃ ┣ 📜DateUtil.kt
┃ ┃ ┗ 📜InputUtil.kt
┗ 📜Main.kt

// Main.kt

fun main() {
    Hotel().startService();
}

단 한줄로 끝난다.
Spring Boot의 초기 셋팅이 main에는 한 줄 만이 있는 것이 개인적으로 마음에 들어서 이런 형태를 채택해 보았다.
Hotel 객채를 만듬과 동시에 startService로 곧바로 실행하도록 작성하였다.

// Hotel.kt

class Hotel {

    private val room: Room = Room();


    private fun showServiceMenu(){
        println("호텔예약 프로그램 입니다.");
        println("[메뉴]")
        println("1. 방예약                 2. 예약목록 출력")
        println("3. 예약목록 (정렬)         4. 시스템 종료")
        println("5. 금액 입출금 내역 목록    6. 예약 변경/취소")
    }

    private fun getOrder(): Order? {
        var result: Int = -1;
        try {
            print("입력 : ")
            val line = readln();
            if(line.trim() != "")
                result = line.toInt();
        }
        catch (e: Exception){
            println("잘못된 값")
            println(e.stackTrace)
        }
        return checkOrder(result);
    }

    private fun checkOrder(order: Int): Order? {
        return when(order){
            1 -> ResOrder;
            2 -> ShowListOrder
            3 -> ShowSortListOrder
            4 -> ExitOrder;
            5 -> AmountListOrder
            6 -> ModifyCancleOrder
            -1 -> {
                println("값이 입력되지 않았습니다.")
                null
            };
            else -> {
                println("1-6 사이의 값을 입력하여 주십시오.")
                null
            };
        }
    }

    fun startService(){
        while (true) {
            showServiceMenu();
            val order = getOrder() ?: continue;
            order.start(room);
        }
    }
}

메뉴를 먼저 보여주고, getOrder를 통하여 선택한 메뉴에 따른 객체를 반환하도록 작성하였다.
이렇게 구성한 이유는 추후 메뉴가 추가되었을 때, showServiceMenu와 checkOrder 함수에 각 한 라인만 작성하면 되어서 리팩토링 하기 편이하다고 생각해서이다.

// Order.kt

abstract class Order {
    abstract fun start(room: Room)
}

1. 예약

처음에 구조를 어떻게 잡을까 하다가 유일하게 Sequence Diagram을 제작한 부분이기도 하다.
사실 제대로 작성하는 방법은 모르지만 해보는게 중요하다 생각해서 이렇게 해두면 참조하기 좋을 것 같다고 생각이 드는 형태로 작성하게 되었다.
모든 메뉴를 만드려 했지만 이후 로직은 코드상으로 보아도 큰 무리가 없다 판단하여 다른 것들은 제작하지 않게 되었다.
이것 또한 개인의 판단이지만 다음부터는 하나하나 전부 작성해보는 연습을 해보는 것도 나쁘지는 않다고 생각한다.

// ResOrder.kt

object ResOrder : Order() {

    private fun payMoney(roomResInfo: RoomResInfo){
        roomResInfo.user.money -= roomResInfo.resPrice;
    }


    override fun start(room: Room) {
        var roomNum:Int;
        print("예약자분의 성함을 입력하여 주십시오. \n입력 : ");
        val user = User(readln());
        var checkIn: Int;
        var checkOut: Int;

        roomNum = InputUtil.getInputNumber("방 번호를 입력하여 주십시오", fun(value: Int): Boolean{
            val hasRoom: Boolean = room.checkHasRoom(value);
            if(!hasRoom){
                print("100-999 사이의 방 번호를 입력하여 주십시오.");
                return false;
            }
            return true;
        })
        if(roomNum == -1) return;

        checkIn = GetInputCheck.checkIn()
        if(checkIn == -1) return;

        checkOut = GetInputCheck.checkOut(checkIn);
        if(checkOut == -1) return;

        var price = (0..500000).random();
        var ans:String = "";
        while (true){
            println("예약금은 ${price}원 입니다. 예약하시겠습니까? y or n")
            print("입력 : ");
            ans = readln();
            if(ans == "y" || ans == "n") break;
        }

        if(ans == "y") {
            var roomResInfo: RoomResInfo = RoomResInfo(user, checkIn, checkOut, price);
            var isFail = true;
            while(isFail) {
                if(room.resRoom(roomNum, roomResInfo)){
                    isFail = false;
                    continue;
                }
                checkIn = GetInputCheck.checkIn()
                if(checkIn == -1) return;

                checkOut = GetInputCheck.checkOut(checkIn);
                if(checkOut == -1) return;

                roomResInfo = RoomResInfo(user, checkIn, checkOut, price)
            }
            payMoney(roomResInfo);
            println("예약이 완료되었습니다.");
        }
        else println("예약하지 않으셨습니다.");

    }
}

우선 object class를 통하여 메뉴 선택마다 객체가 반복적으로 생성되지 않게끔 제작하게 되었다.

// InputUtil.kt

companion object{
private fun readlnConverToInt(): Int? {
    try {
        return readln().toInt();
    } catch (e: NumberFormatException) {
        println("숫자를 입력하여 주십시오.")
        return null;
    } catch (e: Exception) {
        return -1;
    }
}

fun getInputNumber(text: String, action: (Int) -> Boolean): Int {
    var result: Int?;
    while (true) {
        print("$text \n0보다 작은 값을 입력할 시 초기 화면으로 돌아갑니다. \n입력 : ");
        result = readlnConverToInt();
        if (result == null) continue;
        else if (result < 0) break;
        if (!action(result)) continue;
        break;
    }
    return result ?: -1;
}

}
roomNum = InputUtil.getInputNumber("방 번호를 입력하여 주십시오", fun(value: Int): Boolean{
    val hasRoom: Boolean = room.checkHasRoom(value);
    if(!hasRoom){
        print("100-999 사이의 방 번호를 입력하여 주십시오.");
        return false;
    }
    return true;
})
if(roomNum == -1) return;

특정 명령과 숫자만 입력받기 위해서 getInputNumber를 정의하였고 함수를 추가적으로 줌으로써 예외 상황이 발생하면 다시 입력받을 수 있게 하였다.

// GetInputCheck.kt

companion object{
    fun checkIn():Int{
        return InputUtil.getInputNumber("체크인 날짜를 입력하여 주십시오. / 형식 - 20230101", fun(value: Int): Boolean{
            return when(DateUtil.checkDate(value)){
                true -> true
                else -> {
                    println("현제 날짜보다 이전의 날짜는 입력하실 수 없습니다.");
                    return false;
                }
            };
        })
    }
    fun checkOut(checkIn: Int): Int{
        return InputUtil.getInputNumber("체크아웃 날짜를 입력하여 주십시오. / 형식 - 20230101", fun(value: Int): Boolean{
            return when(DateUtil.compareDate(value, checkIn)){
                true -> true
                else -> {
                    println("체크인 날짜보다 이전 날짜는 입력하실 수 없습니다.");
                    return false;
                }
            };
        })
    }
}
// DateUtil.kt

companion object{
    fun checkIsDate(format: String, date:Int): Boolean{
        val checkIsDate = SimpleDateFormat(format);
        checkIsDate.isLenient = false;
        return try{
            checkIsDate.parse(date.toString());
            true;
        } catch (e : ParseException) {
            false
        }
    }
    
    fun checkDate(date: Int): Boolean {
        val format = "yyyyMMdd"
        val cur = LocalDateTime.now();
        val formatter = DateTimeFormatter.ofPattern(format);
        val formatted = cur.format(formatter);
        if(!checkIsDate(format, date)){
            println("${date}이 날짜 형식에 맞지 않습니다.");
            return false;
        }
        return date >= formatted.toInt();
    }
    
    fun compareDate(date: Int, target:Int): Boolean {
        val format = "yyyyMMdd"
        if(!checkIsDate(format, target)){
            println("${target}이 날짜 형식에 맞지 않습니다.");
            return false;
        }
        return date > target;
    }
    
    fun getDiffFromCurDate(checkIn: Int): Int{
        val curDate = LocalDate.now();
        val inDate = LocalDate.parse(checkIn.toString(), DateTimeFormatter.ofPattern("yyyyMMdd"));
        val diff = ChronoUnit.DAYS.between(curDate, inDate);
        return diff.toInt();
    }
}
checkIn = GetInputCheck.checkIn()
if(checkIn == -1) return;
checkOut = GetInputCheck.checkOut(checkIn);
if(checkOut == -1) return;

checkDate를 통하여 입력받은 값이 형식에 맞는가, 날짜로써 유효한가를 확인하고 날짜를 Int형으로 반환 시켰다.
Date 형태로 값을 사용할까도 생각하였지만, 이후 나올 Room Class에서 사용할 때 주소값을 참조하고 property를 체크하는 것 보다는 Int형으로 바로 비교하는 것이 더 낫다고 생각했기 때문이다.
잘못된 값이 입력 될 경우 마찬가지로 다시 입력 받도록 하였다.
글을 작성하며 깨달은 것이 있는데 format을 밖으로 꺼내어 전역 상수로 전환해야 할 것 같다.

// Room.kt
/**
 * HashMap을 사용한 이유
 * 방 번호를 부여할 때, Array로 설정할 경우 안쓰는 방이 있을 때 손실되는 공간이 너무 많고, LinkedList의 경우 방을 찾는데 O(N)의 시간.
 *
 * value를 LinkedList로 사용한 이유
 * search 능력을 O(N)으로 성능이 떨어지되 추가 삭제가 많이 일어나므로.
 * ArrayList보다 메모리 관리 측면에서 효율적이라고 보았다. 추가 삭제가 빈번히 일어나며, 언제 추가 삭제 될지 모르는 상황이기 때문이다.
 * ArrayList를 사용한다 해도 O(N)의 로직을 구현해야 하는 부분에서 search 부분에서 손해를 보는 LinkedList의 특성상 가장 알맞다 생각한다.
 */
private val room = HashMap<Int, LinkedList<RoomResInfo>>()

fun resRoom(roomNum: Int, roomResInfo: RoomResInfo): Boolean{
    var list: LinkedList<RoomResInfo>? = room[roomNum];
    if(list == null){
        list = LinkedList<RoomResInfo>();
        list.add(roomResInfo);
        room[roomNum] = list;
        return true;
    }
    if(!checkResRoom(list, roomResInfo.checkIn)) return false;
    list.add(roomResInfo);
    return true;
}

private fun checkResRoom(list: LinkedList<RoomResInfo>, checkIn: Int): Boolean{
    list.forEach{
        if(checkIn in it.checkIn..it.checkOut){
            println("이미 예약 되어 있습니다.");
            return false;
        }
    }
    return true;
}
var roomResInfo: RoomResInfo = RoomResInfo(user, checkIn, checkOut, price);
var isFail = true;
while(isFail) {
    if(room.resRoom(roomNum, roomResInfo)){
        isFail = false;
        continue;
    }
    checkIn = GetInputCheck.checkIn()
    if(checkIn == -1) return;
    checkOut = GetInputCheck.checkOut(checkIn);
    if(checkOut == -1) return;
    roomResInfo = RoomResInfo(user, checkIn, checkOut, price)
}
payMoney(roomResInfo);
println("예약이 완료되었습니다.");

room 변수를 HashMap으로 사용하면 예약이 있는 방만 key로 만들어지기 때문에 예약이 없는 방의 메모리를 낭비 하지 않아도 된다는 장점이 있다고 생각하여 채택하였다.
또한 같은 방의 이후 예약자를 저장하기위해 LinkedList를 채택하게 되었다.
반복문에서 RoomResInfo객체를 새로 생성하는 이유는 모든 property를 val로 선언하여 불변성을 유지시키기 위해서다.


2. 예약목록 출력 & 3. 예약목록 출력(정렬)

class RoomResInfoDto : Comparable<RoomResInfoDto>{
    val user: User;
    val checkIn: String;
    val checkOut: String;
    private val _checkIn: Int;
    val roomNum: Int;
    val index:Int;
    val resPrice:Int;

    constructor(roomResInfo: RoomResInfo, roomNum: Int, index:Int){
        val dateFormat = SimpleDateFormat("yyyy-MM-dd");
        val toDate = SimpleDateFormat("yyyyMMdd");
        user = roomResInfo.user;
        _checkIn = roomResInfo.checkIn;
        checkIn = dateFormat.format(toDate.parse(roomResInfo.checkIn.toString()));
        checkOut = dateFormat.format(toDate.parse(roomResInfo.checkOut.toString()));
        this.roomNum = roomNum;
        this.index = index;
        this.resPrice = roomResInfo.resPrice;
    }

    override fun compareTo(other: RoomResInfoDto): Int {
        return this._checkIn.compareTo(other._checkIn);
    }

    override fun toString(): String {
        return "사용자 : ${user.name} | 방번호: $roomNum | 체크인: $checkIn | 체크아웃: $checkOut";
    }
}
// Root.kt

fun getAllResInfo(): MutableList<RoomResInfoDto> {
    val result = mutableListOf<RoomResInfoDto>();
    room.keys.forEach(fun(roomNum: Int){
        room[roomNum]?.forEachIndexed{ index, it ->
            result.add(RoomResInfoDto(it, roomNum, index));
        }
    })
    return result;
}
// ShowListOrder.kt

object ShowListOrder : Order() {

    override fun start(room: Room) {
        val list = room.getAllResInfo();
        if (list.size == 0){
            println("예약 인원이 없습니다.");
            return;
        }
        list.forEachIndexed { index, roomResInfoDto ->
            println("${index+1}. $roomResInfoDto");
        }
    }
}

Dto를 따로 정의한 이유는 추가적으로 roomNum와, index를 줌으로써 추후 그 정보를 통해 search 작업을 다시 하지 않게끔 하기 위함과 toString의 재정의 checkIn,Out의 날짜 형식, 정렬을 위한 compareTo 정의를 위해서이다.

list.sorted().forEachIndexed { index, roomResInfoDto ->
    println("${index+1}. $roomResInfoDto");
}

4,5는 스킵하고 6. 예약 수정과 취소

fun modifyRes(checkIn:Int, checkOut:Int, index:Int, roomNum: Int): Boolean {
    val list = room[roomNum];
    val olderItem = list!![index];
    if(!checkResRoom(list, checkIn)){
        return false;
    }
    list[index] = olderItem.copy(checkIn = checkIn, checkOut = checkOut);
    return true;
}
private fun refund(roomResInfo: RoomResInfo){
    val diff = DateUtil.getDiffFromCurDate(roomResInfo.checkIn);
    val per = when{
        diff >= 30 -> 100;
        diff >= 14 -> 80;
        diff >= 7 -> 50;
        diff >= 5 -> 30;
        diff >= 3 -> 0;
        else -> 0;
    }
    roomResInfo.user.money += roomResInfo.resPrice * per / 100;
    println("환불 완료.");
}
fun cancleRes(roomNum:Int, index:Int){
    val removedItem = room[roomNum]!!.removeAt(index);
    refund(removedItem);
}

수정부분에서 data class copy method를 사용하여 checkIn,Out만 수정한 채 새로운 객체를 덮어씌우는 형태를 채택하여 불변성을 유지하였다.


프로젝트가 끝나고 느낀 점

  1. Kotlin, 개인적으로 Java보다 Kotlin이 마음에 든다.
    • 개인적으로 Java를 예전부터 왜인지 모르게 사용하고 싶지 않은 마음이 좀 많이 있었다.
      c를 학습한 이후 java에 대해 알고 책을 사서 학습을 하려 했는데 문법이 좀 길었던게 그때 당시에는 맘에 안들었던 것 같고 지금도 조금 지속되었던 것 같다
    • pointer가 없었던 것이 가장 큰 기억으로 남아버린 것 같다. 사실 지금와서야는 별 상관 없지만 예전에는 메모리를 보고 pointer를 사용 하는 것이 너무 재미있었기 때문에 더 크게 다가왔던 것 같다.
    • javascript를 사용하며 함수형 프로그래밍의 재미를 보았고 고차함수와 일급 객체 개념이 너무 마음에 쏙 들었는데 이후 java를 할려니 그 부분이 완전히 안되는 것은 아니지만 사용하기 너무 불편해서 더욱 더 그런것같다.
  2. 어째서인지 조금 익숙하다.
    • Type 정의 방식이 typescript와 같았고, nullsafety 방식이 dart와 같아서 좀 더 친숙하게 다가왔던 것 같다.
  3. 후회된다.
    • 사실 Spring Boot를 학습 하던 도중 kotlin 또한 Spring Boot 개발을 할 수 있다는 사실을 알게 되었지만 그 당시 나온지 얼마 안되어 자료가 없던 NextJs 13을 공식문서를 보며 학습하는 것이 재미있어서 거기에 더 몰두하였던 것 같다.
    • 이도저도 안되어도 그냥 둘 다 학습하는 것도 나쁘지 않았다고 생각이 들 정도로 재미있는 문법들이 있었다. (ex | (1..50)과도 같은 문법, inline 함수(좀 더 알아봐야 하겠지만.) object class, data class,by lazy 등등 직접 구현할 필요가 점점 사라진다는 게 좀 아쉽긴 하지만 너무 편리하다.
profile
상황에 맞는 기술을 떠올리고 사용할 수 있는 개발자가 되고 싶은 개발자

0개의 댓글