군만두의 IT 개발 일지

패스트캠퍼스 백엔드 개발 부트캠프 8기 그룹스터디 - 도서관 시스템 설계 (Java, JDBC, MySQL) 본문

개발일지/패스트캠퍼스

패스트캠퍼스 백엔드 개발 부트캠프 8기 그룹스터디 - 도서관 시스템 설계 (Java, JDBC, MySQL)

mandus 2024. 3. 15. 10:54

목차

    ⭐ 요약


    • 멘토님이 프로젝트를 처음 해보는 사람도 있으니 스터디그룹 내에서 미니 프로젝트를 해보라고 하셔서 개발하게 되었다.
    • Java, JDBC, MySQL을 활용한 도서관 관리 시스템을 설계하고 구현한다.
    • 회원 관리, 도서 관리, 대출 관리, 예약 관리 총 4가지 기능을 MVC와 유사한 패턴으로 설계했다.
    • 싱글톤 패턴, DTO 패턴, DAO 패턴 등 다양한 디자인 패턴을 실습한다.

    ⭐ 프로젝트 개요


    그룹스터디 팀원들과 함께 콘솔 기반 도서관 관리 시스템을 Java로 직접 설계하고 구현하는 프로젝트를 진행한다. 단순히 코드를 작성하는 것에 그치지 않고, 레이어 분리 구조, 디자인 패턴, Git 커밋 컨벤션까지 함께 적용한다.

    항목 내용
    언어 Java 17
    DB MySQL + JDBC (mysql-connector-j 8.3.0)
    개발 환경 IntelliJ IDEA
    주요 기능 회원 관리, 도서 관리, 대출 관리, 예약 관리
    적용 패턴 MVC 유사 구조, DAO 패턴, DTO 패턴, 싱글톤 패턴

    ⭐ Git 커밋 컨벤션


    팀 협업의 기본인 커밋 메시지 규칙부터 먼저 정했다. 커밋 메시지는 Header(필수), Body(생략 가능), Footer(생략 가능)로 구성하며, 각 영역은 빈 행으로 구분한다.

    타입 내용
    feat 새로운 기능에 대한 커밋
    fix 버그 수정에 대한 커밋
    build 빌드 관련 파일 수정 / 모듈 설치 또는 삭제에 대한 커밋
    chore 그 외 자잘한 수정에 대한 커밋
    docs 문서 수정에 대한 커밋
    style 코드 스타일 혹은 포맷 등에 관한 커밋
    refactor 코드 리팩토링에 대한 커밋
    test 테스트 코드 수정에 대한 커밋
    perf 성능 개선에 대한 커밋

    작성 예시는 아래와 같다.

    fix: Safari에서 모달을 띄웠을 때 스크롤 이슈 수정
    
    모바일 사파리에서 Carousel 모달을 띄웠을 때,
    모달 밖의 상하 스크롤이 움직이는 이슈 수정.
    
    issues: #1137

     

    ⭐ 패키지 구조 및 레이어 설계


    Spring Boot 없이 순수 Java로 구현하면서 레이어를 분리했다. 패키지는 도메인 단위로 나누고, 각 도메인 안에 역할에 따라 클래스를 분리했다.

    패키지 역할 주요 클래스
    db DB 연결 및 해제 관리 Database
    user 회원 도메인 User, UserController, UserView
    book 도서 도메인 Book, BookController, BookView, BookDB
    bookDto 도서 DTO BookDto, EBookDto, PhysicalBookDto
    loan 대출 도메인 Loan, LoanController, LoanDAO
    reservation 예약 도메인 ReservationDAO, ReservationVO, BookVO

    ⭐ 주요 구현 내용


    1. DB 연결 - Database 클래스

     

    DB 접속 정보(서버 주소, DB명, 계정, 비밀번호)는 config.properties 파일로 분리하여 관리한다. 이 파일은 .gitignore에 등록하여 민감 정보가 깃허브에 올라가지 않도록 처리했다.

    public static void connect() throws IOException {
        prop.load(new FileInputStream("config.properties"));
        String classname = prop.getProperty("db.classname");
        String server = prop.getProperty("db.server");
        String database = prop.getProperty("db.database");
        String username = prop.getProperty("db.username");
        String password = prop.getProperty("db.password");
    
        Class.forName(classname); // JDBC 드라이버 로드
        con = DriverManager.getConnection(
            "jdbc:mysql://" + server + "/" + database +
            "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC",
            username, password
        );
    }

     

    2. 회원 관리 - UserController

     

    회원 관련 CRUD와 로그인 기능을 구현했다. DB 연결은 try-with-resources 구문을 사용하여 Connection과 PreparedStatement가 자동으로 닫히도록 처리했다. RETURN_GENERATED_KEYS 옵션으로 INSERT 후 자동 생성된 PK를 즉시 반환받는다.

    // try-with-resources로 자원을 자동으로 해제한다.
    try (Connection conn = Database.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
    
        pstmt.setString(1, name);
        pstmt.setString(2, phone);
        pstmt.setString(3, email);
        pstmt.setString(4, password);
        pstmt.setTimestamp(5, Timestamp.valueOf(LocalDateTime.now()));
        pstmt.executeUpdate();
    
        // INSERT 후 자동 생성된 PK를 가져온다.
        try (ResultSet generatedKeys = pstmt.getGeneratedKeys()) {
            if (generatedKeys.next()) {
                int userId = generatedKeys.getInt(1);
                return new User(userId, name, phone, email, LocalDateTime.now(), password);
            }
        }
    }

     

    3. 도서 검색 - 공백 무시 검색 구현

     

    사용자가 공백을 포함하거나 제외하고 검색해도 동일한 결과를 반환하도록 MySQL의 REPLACE 함수를 활용했다. 검색어와 DB 데이터 모두에서 공백을 제거한 뒤 비교하는 방식이다.

    // 제목에서 공백을 제거한 뒤 LIKE 검색을 수행한다.
    String sql = "SELECT * FROM Book WHERE REPLACE(title, ' ', '') LIKE ?";
    pstmt.setString(1, "%" + searchQuery.replace(" ", "") + "%");

     

    4. 대출 관리 - LoanDAO

     

    대출 기능은 아래 순서로 동작한다. 이미 대출 중인 사용자가 또 대출하거나, 이미 대출 중인 책을 대출하는 예외 케이스를 사전에 검증하도록 설계했다.

    • 해당 사용자가 이미 대출 중인지 확인 (return_date IS NULL)
    • 대출할 책을 제목으로 조회
    • 해당 책의 is_available 상태 확인
    • Loan 테이블에 대출 이력을 INSERT하고, Book 테이블의 is_available을 0으로 UPDATE
    // 1. 해당 사용자가 이미 대출 중인지 확인한다.
    String checkUserLoan = "SELECT * FROM loan WHERE user_id=? AND return_date IS NULL;";
    pstmt = conn.prepareStatement(checkUserLoan);
    pstmt.setInt(1, userId);
    rs = pstmt.executeQuery();
    if (rs.next()) {
        return "-1"; // 이미 대출 중
    }
    
    // 2. 대출 이력을 삽입하고, 도서 상태를 대출 불가로 변경한다.
    String insertSql = "INSERT INTO loan (book_id, user_id, start_date, end_date, return_date)" +
            "VALUES (?, ?, now(), date_add(now(), INTERVAL 7 DAY), null);";
    
    String updateSql = "UPDATE book SET is_available = 0 WHERE book_id = ?;";

     

    5. 예약 관리 - ReservationDAO

     

    예약 기능은 대출과 별개로 동작하며, 예약 취소 시 해당 책에 남은 예약이 없으면 is_available을 다시 1로 업데이트하는 로직을 포함한다.

    // 예약 취소 후 잔여 예약이 없으면 도서 상태를 대출 가능으로 변경한다.
    String selectReservationsSql =
        "SELECT COUNT(*) AS num_reservations FROM Reservation WHERE book_id = ?";
    pstmt = conn.prepareStatement(selectReservationsSql);
    pstmt.setInt(1, bookId);
    rs = pstmt.executeQuery();
    
    if (rs.next()) {
        int numReservations = rs.getInt("num_reservations");
        if (numReservations == 0) {
            String updateBookSql = "UPDATE Book SET is_available = 1 WHERE book_id = ?";
            pstmt = conn.prepareStatement(updateBookSql);
            pstmt.setInt(1, bookId);
            pstmt.executeUpdate();
        }
    }

     

    6. 싱글톤 패턴 - BookDB, LoanTable

     

    인메모리 데이터를 관리하는 클래스에 싱글톤 패턴을 적용했다. BookDBLazyHolder 방식으로 구현하여 멀티스레드 환경에서도 안전하게 인스턴스를 생성한다.

    // LazyHolder 방식: 클래스가 로딩될 때 JVM이 스레드 안전하게 초기화를 보장한다.
    public class BookDB {
        private BookDB() { }
    
        private static class LazyHolder {
            public static final BookDB INSTANCE = new BookDB();
        }
    
        public static BookDB getInstance() {
            return LazyHolder.INSTANCE;
        }
    }

     

    7. DTO 패턴 - BookDto 상속 구조

     

    도서를 실물 도서(PhysicalBook)전자책(EBook)으로 구분하고, 공통 속성은 부모 BookDto에 담고 각각의 추가 필드는 자식 클래스에서 관리하도록 상속 구조를 설계했다.

    // 공통 속성을 담는 부모 DTO
    public class BookDto {
        private String title;
        private String author;
        private int publicationYear;
        private Long ISBN;
        private Category category;
        private boolean is_available;
    }
    
    // 실물 도서: 위치 정보(location) 추가 필드
    public class PhysicalBookDto extends BookDto {
        private String location;
    }
    
    // 전자책: 부모 DTO의 속성만 사용
    public class EBookDto extends BookDto {
    }

    ⭐ DB 테이블 구조


    코드에서 사용하는 주요 테이블 구조는 아래와 같다.

    테이블 주요 컬럼
    User user_id(PK), name, phone, email, password, registered_at
    Book book_id(PK), title, author, publication_year, isbn, is_available, publisher, category
    Loan loan_id(PK), book_id(FK), user_id(FK), start_date, end_date, return_date
    Reservation reservation_id(PK), book_id(FK), user_id(FK), register_date

    ⭐ 어려웠던 점


    • 대출과 예약 기능이 모두 is_available 컬럼을 공유하기 때문에, 예약 취소 시 단순히 1로 되돌리는 것이 아니라 잔여 예약 수를 먼저 확인하는 로직이 필요했다.
    • Spring Boot의 의존성 주입(DI) 없이 순수 Java로 객체를 직접 생성하다 보니, 의존 관계가 복잡해질수록 코드가 지저분해지는 문제를 직접 느꼈다. Spring의 필요성을 체감할 수 있었다.
    • DB 접속 정보를 코드에 하드코딩하지 않고 config.properties로 분리하고 .gitignore에 등록하는 처리를 팀 전체가 통일하는 과정에서 협의가 필요했다.

    ⭐ 후기


    • Spring Boot 없이 순수 Java와 JDBC로 도서관 시스템 전체를 직접 구현하면서, 프레임워크가 자동으로 처리해주는 것들이 얼마나 많은지 다시 한번 실감했다. 기초를 직접 구현해보는 경험이 Spring을 더 잘 이해하는 데 큰 도움이 된다.
    • 커밋 컨벤션부터 패키지 구조, DB 설계까지 기능 구현 못지않게 팀원 간의 의사소통과 설계 합의가 중요하다는 점을 다시 느꼈다.
    • 싱글톤 패턴, DAO 패턴, DTO 패턴을 교재에서만 보다가 직접 적용해보니 각 패턴이 왜 필요한지 훨씬 명확하게 이해되었다.

     

    이 글은 패스트캠퍼스백엔드 개발 캠프에서 공부한 내용을 작성한 것입니다.
    Comments