이번 우리 프로젝트에서는 DAO 패턴으로 설계

왜 역할을 나누는가?
하나의 클래스에 모든 코드를 넣으면:
DB 연결 코드 + 비즈니스 로직 + 화면 출력이 뒤섞임
한 곳을 고치면 다른 곳이 망가질 위험
코드를 읽기가 매우 어려워짐
역할을 분리하면:
DTO : 데이터를 담는 그릇 (Lombok 으로 간결하게)
DAO : DB 와 직접 대화 (SQL 실행)
Service : 비즈니스 규칙 처리 (대출 가능 여부 확인 등)
View : 사용자 입출력 처리
각 층이 자신의 역할만 담당 → 수정/테스트/읽기가 쉬워짐
이번 작업에서는 밑에 구조처럼 역할을 나눠서 작업하기로 했다.
패키지 구조
src/main/java
com.tenco.library
├── util
│ └── DatabaseUtil.java ← DB 연결 담당
├── dto
│ ├── Book.java ← 도서 데이터 그릇
│ ├── Student.java ← 학생 데이터 그릇
│ └── Borrow.java ← 대출 기록 데이터 그릇
├── dao
│ ├── BookDAO.java ← 도서 SQL 실행
│ ├── StudentDAO.java ← 학생 SQL 실행
│ └── BorrowDAO.java ← 대출/반납 SQL 실행
├── service
│ └── LibraryService.java ← 비즈니스 로직
├── view
│ └── LibraryView.java ← 사용자 화면
└── Main.java ← 프로그램 시작점
역할을 클래스별로 나눈 모습
CREATE DATABASE IF NOT EXISTS library;
USE library;
-- 도서 테이블
CREATE TABLE IF NOT EXISTS books (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
publisher VARCHAR(255),
publication_year INT,
isbn VARCHAR(13),
available BOOLEAN DEFAULT TRUE
);
-- 학생 테이블
CREATE TABLE IF NOT EXISTS students (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
student_id VARCHAR(20) NOT NULL UNIQUE
);
-- 대출 테이블
CREATE TABLE IF NOT EXISTS borrows (
id INT PRIMARY KEY AUTO_INCREMENT,
book_id INT,
student_id INT,
borrow_date DATE NOT NULL,
return_date DATE,
FOREIGN KEY (book_id) REFERENCES books(id),
FOREIGN KEY (student_id) REFERENCES students(id)
);
-- 샘플 데이터
INSERT INTO books (title, author, publisher, publication_year, isbn, available) VALUES
('자바 프로그래밍 입문', '김영훈', '한빛미디어', 2023, '9788968481234', TRUE),
('데이터베이스 기초', '이수진', '길벗', 2022, '9788968485678', TRUE),
('알고리즘 문제 해결', '박민수', '인사이트', 2021, '9788968489012', FALSE),
('웹 개발 입문', '최지영', '한빛아카데미', 2024, '9788968483456', TRUE),
('소프트웨어 공학', '정현우', '생능출판사', 2020, '9788970507890', FALSE);
INSERT INTO students (name, student_id) VALUES
('홍길동', '20230001'),
('김민서', '20230002'),
('이준호', '20230003');
INSERT INTO borrows (book_id, student_id, borrow_date, return_date) VALUES
(3, 1, '2025-05-01', NULL), -- 홍길동 → 알고리즘 문제 해결 대출 중
(5, 2, '2025-05-03', NULL); -- 김민서 → 소프트웨어 공학 대출 중
일단 사용할 데이터 베이스 테이블을 만들어 두자
util/DatabaseUtil.java
package com.tenco.library.util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseUtil {
private static final String URL = "jdbc:mysql://localhost:3306/library?serverTimezone=Asia/Seoul";
private static final String DB_USER = System.getenv("DB_USER");
private static final String PASSWORD = System.getenv("DB_PASSWORD");
// 새로운 DB 연결 객체를 반환 합니다.
public static Connection getConnection() throws SQLException {
// 재미삼아 효과 만들어 보기
Thread thread = new Thread(() -> {
System.out.print("Connecting to database");
for (int i = 0; i < 5; i++) {
System.out.print(".");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Connection connection = DriverManager.getConnection(URL, DB_USER, PASSWORD);
System.out.println();
System.out.println(connection.getMetaData().getDatabaseProductName());
System.out.println(connection.getMetaData().getDatabaseProductVersion());
return connection;
}
// TODO - 삭제 예정
// 테스트 코드 작성
public static void main(String[] args) {
try {
DatabaseUtil.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Thread.sleep하면 현재 실행 중인 스레드를 재운다
Runnable로 할 일만 넘겨주면
스레드(Thread)는 실행 담당, Runnable은 작업 내용 담당으로 나뉘어서
“이 스레드가 무엇인지”와 “무슨 일을 하는지”를 분리할 수 있다.
Runnable은 함수형 인터페이스라서
람다식으로 짧게 쓸 수 있다.
람다식은
“추상 메서드가 하나만 있는 인터페이스” 에만 쓸 수 있다.
이걸 함수형 인터페이스라고 한다.
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("작업 실행");
}
}
public class Main {
public static void main(String[] args) {
Runnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
}
}
원래 이렇게 실행하는걸
public class Main {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("작업 실행");
}
};
Thread t = new Thread(r);
t.start();
}
}
이렇게 줄일 수 있다.
“상속하거나 구현한 자식 클래스를 따로 이름 붙여 만들지 않고, 그 자리에서 바로 만들어 쓰는 문법”이게 익명클래스다.
“참조변수 타입이 익명 클래스의 부모 타입(또는 인터페이스 타입)이어야 한다”
DAO 설계
이제 Dao를 설계한다.
Data Access Object로 데이터에 접근할 역할만 따로 모아둔 객체이다.
package com.tenco.library.dao;
import com.tenco.library.dto.Book;
import com.tenco.library.util.DatabaseUtil;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class BookDAO {
// 도서 추가
public int addBook(Book book) throws SQLException {
String sql = """
INSERT INTO books(title, author, publisher, publication_year, isbn)
values(?, ?, ?, ?, ?)
""";
try (Connection conn = DatabaseUtil.getConnection()) {
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, book.getTitle());
pstmt.setString(2, book.getAuthor());
pstmt.setString(3, book.getPublisher());
pstmt.setInt(4, book.getPublicationYear());
pstmt.setString(5, book.getIsbn());
int rows = pstmt.executeUpdate();
return rows;
}
}
// 도서 전체 조회
public List<Book> getAllBooks() throws SQLException {
List<Book> bookList = new ArrayList<>();
String sql = """
SELECT * FROM books ORDER By id
""";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
bookList.add(mapToBook(rs));
}
}
return bookList;
}
// 제목으로 도서 검색
public List<Book> searchBooksByTitle(String title) throws SQLException {
List<Book> bookList = new ArrayList<>();
String sql = """
select * from books where title like ?
""";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + title + "%");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
bookList.add(mapToBook(rs));
}
}
}
return bookList;
}
private Book mapToBook(ResultSet rs) throws SQLException {
return Book.builder()
.id(rs.getInt("id"))
.title(rs.getString("title"))
.author(rs.getString("author"))
.publisher(rs.getString("publisher"))
.publicationYear(rs.getInt("publication_year"))
.isbn(rs.getString("isbn"))
.available(rs.getBoolean("available"))
.build();
}
// 테스트 코드 작성
public static void main(String[] args) {
try {
List<Book> resultList = new BookDAO().searchBooksByTitle("입문");
System.out.println(resultList);
System.out.println("------------------");
System.out.println(new BookDAO().getAllBooks());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
prepared statement의 역할은
SQL 문장을 준비함
? 자리에 값 바인딩 가능
DB를 통해 실제로 실행가능
executeQuery()
executeUpdate()
execute()
StudentDAO
package com.tenco.library.dao;
import com.tenco.library.dto.Student;
import com.tenco.library.util.DatabaseUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class StudentDAO {
// 학생 등록
public void addStudent(Student student) throws SQLException {
String sql = """
INSERT INTO students(name, student_id) VALUES (? , ?)
""";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, student.getName());
pstmt.setString(2, student.getStudentId());
pstmt.executeUpdate();
}
}
// 전체 학생 조회
public List<Student> getAllStudents() throws SQLException {
List<Student> studentList = new ArrayList<>();
String sql = """
SELECT * FROM students ORDER BY id
""";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
studentList.add(mapToStudent(rs));
}
}
return studentList;
}
// 학번으로 학생 조회 - 로그인
public Student authenticateStudent(String studentId) throws SQLException {
String sql = """
SELECT * FROM students WHERE student_id = ?
""";
try (Connection conn = DatabaseUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return mapToStudent(rs);
}
}
}
return null;
}
// ResultSet -> Student 변환 메소드
private Student mapToStudent(ResultSet rs) throws SQLException {
return Student.builder()
.id(rs.getInt("id"))
.name(rs.getString("name"))
.studentId(rs.getString("student_id"))
.build();
}
}
Buider로 새로운 student 객체를 생성하고 값을 넣은다음 student로 반환하는 모습이다.