본문 바로가기
  • 코딩, 허쌤이 떠먹여 줄게
BackEnd/Java

9_1장_도서 대여 시스템

by 허쌤 2026. 1. 16.

도서 대여 시스템 - 상세 설명

📋 프로그램 개요

ArrayList를 활용하여 도서 정보를 관리하고, 대출/반납 기능을 제공하는 도서 대여 시스템입니다.
객체 지향 프로그래밍의 기본 개념(클래스, 객체, 캡슐화, 상속)을 실제 프로젝트에 적용한 예제입니다.


🏗 프로그램 구조

도서 대여 시스템
├── Library.java          (도메인 클래스 - 도서 정보)
├── LibraryManager.java   (관리 클래스 - CRUD 기능)
└── Search.java           (실행 클래스 - 메뉴 시스템)

클래스 관계도

Search (실행)
    │
    └── LibraryManager (관리)
            │
            ├── ArrayList<Library> librarys
            └── ArrayList<Library> booklocation
                    │
                    └── Library (도메인)
                            ├── String title
                            ├── String author
                            ├── String location
                            ├── String isbn
                            └── boolean available

📦 1. Library 클래스 (도메인 클래스)

역할

도서의 정보를 담는 데이터 클래스입니다. 하나의 Library 객체가 하나의 도서 정보를 나타냅니다.

주요 구성

필드 (Fields)

private String title;      // 책 제목
private String author;     // 저자
private String location;   // 책 위치 (예: "sectionA")
private String isbn;       // ISBN 번호
private boolean available; // 대출 가능 여부

캡슐화 원칙:

  • 모든 필드를 private로 선언하여 외부에서 직접 접근 불가
  • Getter/Setter 메서드를 통해서만 접근 가능

생성자 (Constructors)

1. 기본 생성자

public Library() {
}
  • 매개변수가 없는 기본 생성자
  • 모든 필드를 기본값(null, false)으로 초기화
  • 용도: 객체를 먼저 생성하고 나중에 setter로 값을 설정할 때 사용

2. 매개변수 생성자

public Library(String title, String author, String location, String isbn) {
    this.title = title;
    this.author = author;
    this.location = location;
    this.isbn = isbn;
    this.available = true;  // 새 도서는 항상 대출 가능
}
  • 도서 정보를 받아서 초기화하는 생성자
  • 중요: available = true로 자동 설정 (새 도서는 항상 대출 가능)

생성자 오버로딩:

  • 같은 이름의 생성자를 매개변수만 다르게 여러 개 정의
  • 기본 생성자와 매개변수 생성자를 함께 제공하면 유연하게 객체 생성 가능

Getter/Setter 메서드

Getter 메서드 (읽기)

public String getTitle() {
    return title;
}

public String getAuthor() {
    return author;
}

public boolean isAvailable() {
    return available;
}

Setter 메서드 (쓰기)

public void setTitle(String title) {
    this.title = title;
}

public void setAvailable(boolean available) {
    this.available = available;
}

주의사항:

  • Boolean 타입의 getter는 is 접두사 사용: isAvailable()
  • getAvailable() ❌ (문법적으로는 가능하지만 관례상 is 사용)

this 키워드:

public void setTitle(String title) {
    this.title = title;  // this.title = 인스턴스 변수, title = 매개변수
}
  • 매개변수 이름과 필드 이름이 같을 때 구분하기 위해 사용
  • this는 현재 객체를 가리킴

주요 메서드

1. void book() - 도서 대출 처리

public void book() {
    this.available = false;
}
  • 도서를 대출 처리하는 메서드
  • availablefalse로 변경하여 대출 불가능 상태로 만듦
  • 메서드 이름: rent() 또는 borrow()가 더 적절할 수 있지만, book()도 가능

2. String toString() - 객체 정보 문자열 반환

@Override
public String toString() {
    return "책 제목 : " + title + ", 저자 : " + author + ", 책 위치 : " + location 
            + ", ISBN : " + isbn + ", 대출여부 : " + (available ? "대출가능" : "대출불가능");
}

@Override 어노테이션:

  • 부모 클래스(Object)의 toString() 메서드를 재정의(오버라이딩)
  • 컴파일러에게 명시적으로 알려주는 역할
  • 오타나 시그니처 불일치 시 컴파일 에러 발생

삼항 연산자:

(available ? "대출가능" : "대출불가능")
  • 조건 ? 참일때값 : 거짓일때값
  • if-else문의 축약형

🗂 2. LibraryManager 클래스 (관리 클래스)

역할

도서 리스트를 관리하고, CRUD(생성, 읽기, 수정, 삭제) 기능을 제공하는 클래스입니다.

주요 구성

필드

private ArrayList<Library> librarys;      // 전체 도서 리스트
private ArrayList<Library> booklocation;  // 대출된 도서 리스트

두 개의 ArrayList 사용 이유:

  • librarys: 전체 도서를 관리하는 리스트
  • booklocation: 대출된 도서만 따로 관리하는 리스트
  • 같은 Library 객체를 두 리스트에서 공유 (객체 참조 개념)

생성자

public LibraryManager() {
    librarys = new ArrayList<>();
    booklocation = new ArrayList<>();

    // 더미 데이터 추가
    librarys.add(new Library("this is java", "shin", "sectionA", "979-11-691-229-8"));
    librarys.add(new Library("First Encounter with React", "Lee Inje", "Section B", "979-11-6921-169-7"));
    librarys.add(new Library("The Principles of Web Standards", "Ko Kyunghee", "Section C", "979-11-6303-622-7"));
}

더미 데이터:

  • 프로그램 테스트를 위한 샘플 데이터
  • 프로그램 실행 시 자동으로 추가됨

메서드 상세 설명

1. void allLibrary() - 대출 가능한 도서 보기

public void allLibrary() {
    System.out.println("대출 가능한 도서보기");
    for(int i = 0; i < librarys.size(); i++) {
        Library library = librarys.get(i);
        if(library.isAvailable()) {
            System.out.println(library);
        }
    }
}

동작 원리:

  1. librarys 리스트를 인덱스 기반으로 순회
  2. 각 도서의 availabletrue인지 확인
  3. 대출 가능한 도서만 출력
  4. System.out.println(library)는 자동으로 library.toString() 호출

향상된 for문으로 개선 가능:

for(Library library : librarys) {
    if(library.isAvailable()) {
        System.out.println(library);
    }
}

2. boolean booklocations(String libraryName) - 도서 대출하기

public boolean booklocations(String libraryName) {
    for(Library library : librarys) {
        if(library.getTitle().equalsIgnoreCase(libraryName) && library.isAvailable()) {
            library.book();              // 대출 처리
            booklocation.add(library);   // 대출 목록에 추가
            return true;
        }
    }
    return false;
}

동작 원리:

  1. librarys 리스트를 순회
  2. 도서 제목이 일치하고 (equalsIgnoreCase) 대출 가능한 도서 찾기
  3. 찾으면:
    • book() 호출하여 available = false로 변경
    • booklocation 리스트에 추가
    • true 반환
  4. 못 찾으면 false 반환

equalsIgnoreCase() 설명:

"this is java".equalsIgnoreCase("THIS IS JAVA")  // true (대소문자 무시)
"this is java".equalsIgnoreCase("This Is Java")  // true
  • 문자열 비교 시 대소문자를 구분하지 않음
  • 사용자 입력 오류 방지에 유용

논리 연산자 &&:

library.getTitle().equalsIgnoreCase(libraryName) && library.isAvailable()
  • 두 조건이 모두 true여야 함
  • AND 연산

객체 참조 개념:

library.book();              // library 객체의 available을 false로 변경
booklocation.add(library);   // 같은 객체를 booklocation에도 추가
  • 두 리스트가 같은 객체를 참조
  • 한 리스트에서 객체를 수정하면 다른 리스트에도 영향

3. void booklocations() - 대출한 도서 보기 (오버로딩)

public void booklocations() {
    System.out.println("대출한 도서보기");
    for(Library location : booklocation) {
        System.out.println(location);
    }
}

메서드 오버로딩:

  • 같은 이름의 메서드를 매개변수만 다르게 여러 개 정의
  • booklocations(String libraryName): 도서 대출
  • booklocations(): 대출한 도서 보기

4. void addLibrary(...) - 도서 추가하기

public void addLibrary(String newTitle, String newAuthor, String newLocation, String newIsbn) {
    librarys.add(new Library(newTitle, newAuthor, newLocation, newIsbn));
}

동작 원리:

  1. 매개변수로 받은 정보로 새로운 Library 객체 생성
  2. vehicles 리스트에 추가
  3. 생성 시 available = true로 자동 설정됨

한 줄 작성:

// 방법 1: 객체를 변수에 저장 후 추가 (주석 처리된 코드)
Library abc = new Library(newTitle, newAuthor, newLocation, newIsbn);
librarys.add(abc);

// 방법 2: 바로 추가 (현재 코드)
librarys.add(new Library(newTitle, newAuthor, newLocation, newIsbn));

5. void delLibrary(String dname) - 도서 삭제하기

public void delLibrary(String dname) {
    boolean result = false;
    for(Library library : librarys) {
        if(library.getTitle().equalsIgnoreCase(dname)) {
            if(library.isAvailable()) {  // 대출 중이 아닐 때만 삭제 가능
                librarys.remove(library);
                result = true;
                break;
            } else {
                result = false;
                break;
            }
        }
    }
    if(result) {
        System.out.println("삭제됨");
    } else {
        System.out.println("삭제 안됨");
    }
}

동작 원리:

  1. 도서 제목이 일치하는 도서 찾기
  2. 찾은 도서가 대출 가능(available == true)한지 확인
  3. 대출 가능하면 삭제, 대출 중이면 삭제 불가
  4. 결과 출력

주의사항:

  • 향상된 for문에서 직접 remove()를 사용하면 ConcurrentModificationException 발생 가능
  • 안전한 방법: Iterator 사용 또는 인덱스 기반 반복문

break 문:

  • 반복문을 즉시 종료
  • 조건에 맞는 첫 번째 항목만 처리하고 종료

6. void updateLibrary(String uname) - 도서 정보 수정하기

public void updateLibrary(String uname) {
    int i = 0;
    int index = -1;
    int menu = -1;
    boolean flag = true;
    Scanner sc = new Scanner(System.in);
    Library newA = new Library();

    // 수정할 도서 찾기
    for(Library a : librarys) {
        i++;
        if(a.getTitle().equals(uname)) {
            index = i - 1;  // 실제 인덱스 (i는 1부터 시작)
            newA = a;       // 찾은 도서 객체를 newA에 참조
        }
        System.out.println(a.getTitle().equals(uname) + " " + a.getTitle() + " " + uname);
    }

    if(index != -1) {
        System.out.print("뭘 수정할건데?\n 1.도서 이름 \t 2.도서 저자 \t 3.도서 위치 \t 4.도서ISBN \n >>");
        menu = sc.nextInt();
        sc.nextLine();  // 버퍼 초기화

        while (flag) {
            switch (menu) {
                case 1:
                    System.out.println("수정할 이름");
                    newA.setTitle(sc.nextLine());
                    librarys.set(index, newA);
                    flag = false;
                    break;
                // ... 다른 케이스들
            }
        }
    } else {
        System.out.println("찾는 도서가 없어서 업데이트할 수 없습니다.");
    }
}

동작 원리:

  1. 수정할 도서를 이름으로 찾기
  2. 인덱스와 객체 참조 저장
  3. 수정할 항목 선택 (메뉴)
  4. 해당 필드만 setter로 수정
  5. librarys.set(index, newA)로 리스트에 반영

코드 분석:

1) 인덱스 찾기:

int i = 0;
for(Library a : librarys) {
    i++;
    if(a.getTitle().equals(uname)) {
        index = i - 1;  // i는 1부터 시작하므로 -1
    }
}
  • i를 1부터 카운트하므로 실제 인덱스는 i - 1

개선 방법:

for(int i = 0; i < librarys.size(); i++) {
    if(librarys.get(i).getTitle().equals(uname)) {
        index = i;  // 바로 인덱스 사용
        break;
    }
}

2) 객체 참조:

Library newA = new Library();  // 빈 객체 생성
newA = a;  // 찾은 도서 객체를 참조 (같은 객체를 가리킴)
  • newAlibrarys의 실제 객체를 참조
  • setter로 수정하면 원본 객체가 변경됨
  • librarys.set(index, newA)는 사실 불필요할 수 있지만, 명시적으로 반영

3) Scanner 버퍼 처리:

menu = sc.nextInt();  // 숫자만 읽음, 엔터는 버퍼에 남음
sc.nextLine();        // 버퍼 초기화 (엔터 제거)

4) 디버깅 출력:

System.out.println(a.getTitle().equals(uname) + " " + a.getTitle() + " " + uname);
  • 비교 결과를 출력 (디버깅용)
  • 실제 운영 시 제거 권장

개선된 코드:

public void updateLibrary(String uname) {
    int index = -1;
    Vehicle targetLibrary = null;
    Scanner sc = new Scanner(System.in);

    // 수정할 도서 찾기 (인덱스 기반)
    for (int i = 0; i < librarys.size(); i++) {
        if (librarys.get(i).getTitle().equalsIgnoreCase(uname)) {
            index = i;
            targetLibrary = librarys.get(i);
            break;
        }
    }

    if (index != -1) {
        System.out.print("무엇을 수정할까요?\n 1.도서 이름 \t 2.도서 저자 \t 3.도서 위치 \t 4.도서ISBN \n >> ");
        int menu = sc.nextInt();
        sc.nextLine();

        switch (menu) {
            case 1:
                System.out.print("수정할 이름: ");
                targetLibrary.setTitle(sc.nextLine());
                // librarys.set(index, targetLibrary); // 불필요 (이미 같은 객체 참조)
                break;
            // ... 다른 케이스들
        }
    } else {
        System.out.println("찾는 도서가 없어서 업데이트할 수 없습니다.");
    }
}

7. void showLibrary(String sname) - 도서 상세 정보 조회

public void showLibrary(String sname) {
    for(Library a : librarys) {
        if(a.getTitle().equalsIgnoreCase(sname)) {
            System.out.println(a.toString());
        }
    }
}

동작 원리:

  • 도서 제목이 일치하는 도서의 정보 출력

🖥 3. Search 클래스 (실행 클래스)

역할

사용자와 상호작용하는 메인 프로그램입니다. 메뉴를 제공하고 사용자 입력을 처리합니다.

주요 구성

메인 메서드 구조

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    LibraryManager manager = new LibraryManager();
    boolean flag = true;

    while (flag) {
        // 메뉴 출력
        // 사용자 입력 받기
        // switch-case로 메뉴 처리
        // 8번 선택 시 flag = false로 종료
    }
    sc.close();
    System.exit(0);
}

메뉴 시스템

메뉴 출력:

System.out.println("\n도서검색 시스템에 오신 것을 환영합니다.");
System.out.println("1.대출 가능한 도서 보기");
System.out.println("2.도서 대출하기");
System.out.println("3.대출한 도서 보기");
System.out.println("4.도서 추가하기");
System.out.println("5.도서 삭제하기");
System.out.println("6.도서 정보 수정하기");
System.out.println("7.도서 내용 보기");
System.out.println("8.종료");
System.out.print("원하는 작업을 선택하세요 >> ");

입력 처리:

int choice = sc.nextInt();
sc.nextLine();  // 버퍼 초기화 (매우 중요!)

Scanner 버퍼 문제:

int choice = sc.nextInt();  // 숫자만 읽음, 엔터(\n)는 버퍼에 남음
String name = sc.nextLine(); // 버퍼에 남은 엔터를 읽어서 빈 문자열이 됨

해결 방법:

int choice = sc.nextInt();
sc.nextLine();  // 엔터를 버퍼에서 제거
String name = sc.nextLine();  // 이제 정상적으로 입력 받음

메뉴별 처리

메뉴 1: 대출 가능한 도서 보기

case 1:
    System.out.println("대출 가능한 도서");
    manager.allLibrary();
    break;

메뉴 2: 도서 대출하기

case 2:
    System.out.println("도서 대출하기");
    System.out.print("대출 하려는 도서의 이름을 입력 하세요 : ");
    String libraryName = sc.nextLine();
    if(manager.booklocations(libraryName)) {
        System.out.println("도서가 성공적으로 대출되었습니다.");
    } else {
        System.out.println("도서가 존재하지 않거나 대출 불가능합니다.");
    }
    break;

반환값 처리:

  • booklocations()boolean 반환
  • true: 대출 성공
  • false: 대출 실패

메뉴 3: 대출한 도서 보기

case 3:
    System.out.println("대출한 도서 보기");
    manager.booklocations();
    break;

메뉴 4: 도서 추가하기

case 4:
    System.out.println("도서 추가하기");
    System.out.print("추가 도서이름 : ");
    String newTitle = sc.nextLine();
    System.out.print("추가 도서저자 : ");
    String newAuthor = sc.nextLine();
    System.out.print("도서 위치 : ");
    String newLocation = sc.nextLine();
    System.out.print("도서의 ISBN : ");
    String newIsbn = sc.nextLine();
    manager.addLibrary(newTitle, newAuthor, newLocation, newIsbn);
    System.out.println("도서 추가 완료되었습니다.");
    break;

메뉴 5, 6, 7: 빈 문자열 체크

case 5:
    System.out.println("삭제 시작");
    System.out.println("삭제하는 도서 이름을 입력하세요 : ");
    String dname = sc.nextLine();
    if(dname.equals("")) {
        System.out.println("삭제 하려는 도서 이름을 다시 입력 해주세요");
        dname = sc.nextLine();
    }
    manager.delLibrary(dname);
    System.out.println("삭제 완료");
    break;

빈 문자열 체크:

  • 사용자가 엔터만 누르는 경우 방지
  • 한 번만 재입력 요청 (더 강력한 예외 처리는 반복문 사용)

개선 방법:

String dname = "";
while(dname.equals("")) {
    System.out.print("삭제하는 도서 이름을 입력하세요: ");
    dname = sc.nextLine();
    if(dname.equals("")) {
        System.out.println("도서 이름을 입력해주세요.");
    }
}

메뉴 8: 종료

case 8:
    System.out.println("프로그램 종료합니다.");
    flag = false;
    break;

프로그램 종료:

sc.close();      // Scanner 닫기
System.exit(0);  // 프로그램 종료 (0: 정상 종료)

🔍 핵심 개념 설명

1. 객체 참조 (Reference) 이해

ArrayList<Library> librarys = new ArrayList<>();
ArrayList<Library> booklocation = new ArrayList<>();

Library book = new Library("Java", "Author", "A", "123");
librarys.add(book);        // librarys 리스트에 추가
booklocation.add(book);    // booklocation 리스트에도 같은 객체 추가

// 같은 객체를 두 리스트에서 공유
book.setAvailable(false);  // 두 리스트의 같은 객체가 변경됨

시각적 표현:

librarys          booklocation
    │                   │
    └─── Library ───────┘
         (Java)
         available = false

2. ArrayList 메서드

메서드 설명 예시
add(E e) 요소 추가 librarys.add(new Library(...))
get(int index) 요소 조회 Library lib = librarys.get(0)
set(int index, E e) 요소 수정 librarys.set(0, newLib)
remove(Object o) 요소 삭제 (객체) librarys.remove(library)
remove(int index) 요소 삭제 (인덱스) librarys.remove(0)
size() 리스트 크기 int count = librarys.size()
isEmpty() 비어있는지 확인 if(librarys.isEmpty())

3. 문자열 비교

// ❌ 잘못된 방법
if (str1 == str2) { ... }  // 주소 비교 (의도한 대로 동작 안 함)

// ✅ 올바른 방법
if (str1.equals(str2)) { ... }           // 대소문자 구분
if (str1.equalsIgnoreCase(str2)) { ... } // 대소문자 무시

4. 향상된 for문 vs 일반 for문

향상된 for문 (Enhanced for loop)

for (Library library : librarys) {
    // library는 librarys의 각 요소
    // 인덱스 접근 불가
    // 삭제 시 주의 필요
}

일반 for문 (Index-based)

for (int i = 0; i < librarys.size(); i++) {
    Library library = librarys.get(i);
    // 인덱스 접근 가능 (i)
    // 삭제 시 인덱스 조정 필요
}

5. 메서드 오버로딩

// 매개변수가 다른 같은 이름의 메서드
public boolean booklocations(String libraryName) { ... }
public void booklocations() { ... }

오버로딩 조건:

  • 메서드 이름이 같아야 함
  • 매개변수의 개수 또는 타입이 달라야 함
  • 반환 타입은 오버로딩에 영향 없음

🎯 학습 포인트 정리

객체 지향 프로그래밍

  • 클래스와 객체의 개념
  • 캡슐화 (private 필드, public 메서드)
  • 생성자 오버로딩
  • Getter/Setter 패턴
  • toString() 메서드 오버라이딩
  • 메서드 오버로딩

ArrayList 활용

  • ArrayList 생성 및 초기화
  • 요소 추가 (add())
  • 요소 조회 (get())
  • 요소 수정 (set())
  • 요소 삭제 (remove())
  • 리스트 순회 (for문, 향상된 for문)

문자열 처리

  • equals() vs ==
  • equalsIgnoreCase() 사용
  • 빈 문자열 체크

제어문

  • while 루프 (메뉴 시스템)
  • switch-case문
  • if-else문
  • break 문

Scanner 사용

  • nextInt() vs nextLine() 버퍼 문제
  • sc.nextLine()으로 버퍼 초기화

💡 코드 개선 제안

1. 예외 처리 강화

// 현재 코드
int choice = sc.nextInt();

// 개선된 코드
int choice = -1;
try {
    choice = sc.nextInt();
    sc.nextLine();
} catch (InputMismatchException e) {
    System.out.println("숫자를 입력하세요.");
    sc.nextLine(); // 버퍼 초기화
}

2. 반복 입력 처리

// 현재 코드
if(dname.equals("")) {
    dname = sc.nextLine();
}

// 개선된 코드
String dname = "";
while(dname.trim().isEmpty()) {
    System.out.print("도서 이름을 입력하세요: ");
    dname = sc.nextLine();
    if(dname.trim().isEmpty()) {
        System.out.println("도서 이름을 입력해주세요.");
    }
}

3. 메서드 이름 개선

// 현재 코드
public void booklocations(String libraryName) { ... }
public void booklocations() { ... }

// 개선된 코드
public boolean rentBook(String libraryName) { ... }
public void showRentedBooks() { ... }

4. Iterator를 사용한 안전한 삭제

public void delLibrary(String dname) {
    Iterator<Library> iterator = librarys.iterator();
    boolean result = false;

    while (iterator.hasNext()) {
        Library library = iterator.next();
        if (library.getTitle().equalsIgnoreCase(dname)) {
            if (library.isAvailable()) {
                iterator.remove();  // 안전한 삭제
                result = true;
                break;
            }
        }
    }

    if (result) {
        System.out.println("삭제됨");
    } else {
        System.out.println("삭제 안됨 (대출 중이거나 존재하지 않음)");
    }
}

5. 중복 체크 추가

public boolean addLibrary(...) {
    // ISBN 중복 체크
    for (Library lib : librarys) {
        if (lib.getIsbn().equals(newIsbn)) {
            System.out.println("이미 존재하는 ISBN입니다.");
            return false;
        }
    }
    librarys.add(new Library(...));
    return true;
}

📚 관련 개념


🎓 실습 과제

  1. 반납 기능 추가: 대출된 도서를 반납하는 기능 구현
  2. 검색 기능 강화: 저자, ISBN, 위치별로 검색하는 기능 추가
  3. 통계 기능: 전체 도서 수, 대출 가능한 도서 수, 대출 중인 도서 수 출력
  4. 파일 저장/불러오기: 도서 정보를 파일에 저장하고 불러오기
  5. 예외 처리 강화: 모든 입력에 대해 예외 처리 추가