본문 바로가기
우아한테크코스/레벨 1 - Java

[JAVA] JDBC 연결

by shyun00 2024. 4. 4.

Java에서 JDBC 드라이버를 사용해 DB와 연결하는 작업을 수행했다. 그 과정을 순서대로 정리해보았다.

JDBC란 JavaDatabase Connectivity로, 자바에서 데이터베이스에 접속할 수 있도록 하는 자바 API이다.

이번 프로젝트에서는 MySql 서버를 사용하는 프로그램을 만들기로 했다.

 

1. Gradle 의존성 추가

build.gradle에 mysql-connector-java 를 추가해준다.

runtimeOnly("com.mysql:mysql-connector-j:8.3.0")

 

2. 설정파일 추가

데이터베이스에 접근하기 위한 정보를 yml파일을 이용해 설정한다.

application.yml파일

server: localhost:13306 // MySQL 서버 주소
database: chess // MySQL 데이터베이스 이름
option: ?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root // MySQL 서버 아이디
password: root // MySQL 서버 비밀번호

 

3. Connection 생성하는 객체 정의 

DB와의 connection을 생성하기 위해, ConnectionGenerator를 정의해주었다.

정적팩터리메서드를 통해 위에서 생성한 yml 파일을 생성을 위한 파라미터로 받아오도록 했다.

public class ConnectionGenerator {
    private final Properties properties;

    public ConnectionGenerator(Properties properties) {
        this.properties = properties;
    }

    public static ConnectionGenerator from(String configurationFileName) {
        return new ConnectionGenerator(loadProperties(configurationFileName));
    }

    private static Properties loadProperties(String configurationFileName) {
        try {
            FileInputStream fileInputStream = new FileInputStream(configurationFileName);
            Properties properties = new Properties();
            properties.load(fileInputStream);
            return properties;
        } catch (FileNotFoundException e) {
            throw new NoSuchElementException("해당되는파일이 없습니다.");
        } catch (IOException e) {
            throw new IllegalArgumentException("파일을 읽어올 수 없습니다.");
        }
    }

    public Connection getConnection() {
        try {
            return DriverManager.getConnection(
                    "jdbc:mysql://" + properties.get("server") + "/"
                            + properties.get("database") + properties.get("option"),
                    properties.get("username").toString(), properties.get("password").toString());
        } catch (SQLException e) {
            handleSQLException(e);
            throw new DBConnectionException("데이터베이스 연결에 실패했습니다.");
        }
    }

    public void handleSQLException(SQLException e) {
        System.err.println("DB 연결 오류:" + e.getMessage());
        e.printStackTrace();
    }
}

 

4. Dao 생성

DB와 상호작용하기 위한 Dao 클래스를 정의한다.

아래 코드는 체스게임에서 체스 판의 상태를 저장하는 Dao 클래스이다.

ConnectionGenerator를 통해 connection을 생성하고, PreparedStatement(혹은 Statement)로 수행할 Sql을 명시한다. 그리고 executeQuery로 Sql문을 수행하고, ResultSet으로 쿼리 실행 결과를 받아온다. 받아온 ResultSet에서 필요한 데이터를 꺼내 원하는 타입으로 만들어 줄 수 있다.

connection을 사용할 때, try-catch문을 통해 connection을 관리할 수 있다. try문 실행 중 예외가 발생하면 connection은 종료된다.

public class ChessBoardDao {
    private final ConnectionGenerator connectionGenerator;

    public ChessBoardDao(ConnectionGenerator connectionGenerator) {
        this.connectionGenerator = connectionGenerator;
    }

    public List<ChessGameComponentDto> findAll() {
        try (final Connection connection = connectionGenerator.getConnection()) {
            final PreparedStatement statement = connection.prepareStatement("SELECT * FROM chess_boards");
            final ResultSet resultSet = statement.executeQuery();

            return getChessGameComponentDtos(resultSet);
        } catch (SQLException e) {
            connectionGenerator.handleSQLException(e);
            throw new DBConnectionException("진행중인 게임 데이터를 가져올 수 없습니다.");
        }
    }
    
    private List<ChessGameComponentDto> getChessGameComponentDtos(ResultSet resultSet) throws SQLException {
        final List<ChessGameComponentDto> chessBoardComponents = new ArrayList<>();
        while (resultSet.next()) {
            File file = File.convertToFile(resultSet.getString("file"));
            Rank rank = Rank.convertToRank(resultSet.getInt("rank"));
            Type type = Type.convertToType(resultSet.getString("type"));
            Color color = Color.convertToColor(resultSet.getString("color"));
            String gameName = resultSet.getString("game_name");
            ChessGameComponentDto chessGameComponentDto
                    = new ChessGameComponentDto(Position.of(file, rank), type.generatePiece(color), gameName);
            chessBoardComponents.add(chessGameComponentDto);
        }
        return chessBoardComponents;
    }
    ...
}

 

그런데 이 경우 트랜잭션 관리가 어렵다. try문 안에서 예외가 발생하더라도 이전에 커밋된 기록은 남아있게 된다.

트랜잭션 관리를 위해 service 레이어를 분리하고, service레이어에서 connection을 생성해서 Dao로 넘겨주도록 수정했다.

 

5. Service 레이어 분리

서비스 계층을 분리해 Dao와 ConnectionGenerator를 필드로 갖게 했다. 세부 구현 방식은 아래 코드에 주석으로 설명해두었다.

public class ChessService {
    private final GameInformationDao gameInformationDao;
    private final ChessBoardDao chessBoardDao;
    private final ConnectionGenerator connectionGenerator;

    public ChessService(GameInformationDao gameInformationDao, ChessBoardDao chessBoardDao,
                        ConnectionGenerator connectionGenerator) {
        this.gameInformationDao = gameInformationDao;
        this.chessBoardDao = chessBoardDao;
        this.connectionGenerator = connectionGenerator;
    }

    public void updateChessBoard(List<Position> movedPath, GameInformation gameInformation) {
        // connection을 생성한다.
        Connection connection = connectionGenerator.getConnection();
        try {
            // AutoCommit을 비활성화한다.
            connection.setAutoCommit(false);
            // 생성된 connection을 파라미터로 넘겨준다.
            chessBoardDao.update(movedPath.get(SOURCE_INDEX), movedPath.get(TARGET_INDEX), connection);
            gameInformationDao.updateTurn(gameInformation, connection);
            // 모든 로직이 정상 수행된 후 commit한다.
            connection.commit();
        } catch (RuntimeException | SQLException e) {
            rollback(connection);
            throw new DBConnectionException(e.getMessage());
        } finally {
            // 로직이 완료되고나면 connection을 종료한다.
            closeConnection(connection);
        }
    }
    ...   
}

 

여기까지 JDBC를 적용하는 방법에 대해 알아봤다. 나중에 Spring Data JPA를 사용하게 되면 직접 적용하게될 일은 많지 않겠지만, 데이터베이스와 상호작용하는 흐름과 원리를 알 수 있어서 좋았다.

 

참고자료

PreparedStatement

ResultSet