티스토리 뷰

JPA

SQL의 문제점

Jaime.Lee 2020. 4. 1. 01:06

제목을 편의상 SQL의 문제점이라고 적었습니다만 소스코드에서 직접 SQL을 다룰 때 발생하는 문제에 대해 이야기해보려고 합니다.

자바로 개발하는 대부분의 애플리케이션에서는 관계형 데이터베이스를 저장소로 사용합니다.

데이터 관리를 위해선 SQL을 사용해야 하고 이는 자바 개발자에게는 매우 익숙한 환경 & 상황이라고 할 수 있습니다.

1. 필연적인 반복

자바 애플리케이션에서 JDBC API를 이용하여 데이터베이스와 연동하기 위해서는 수많은 반복을 필요로 합니다.

CRUD를 위한 API를 만드는 작업과 조회한 결과를 매핑하는 작업 등이 이에 해당합니다.

간단히 예를 들어보겠습니다.

축덕인 저는 축구 선수들의 스탯을 기록하는 player라는 테이블을 생성하였습니다.

package io.lcalmsky.jpa.jdbc.model;

import lombok.Data;

@Data
public class Player {
    private Long id;
    private String name;
    private Integer goals;
    private Integer assists;
}

축구 선수 객체를 데이터베이스를 이용해 관리할 목적으로 데이터 접근 객체도 생성하였습니다.

package io.lcalmsky.jpa.jdbc.domain.model.dao;

import io.lcalmsky.jpa.jdbc.domain.model.dto.Player;

public interface PlayerDao {
    Player findPlayerById(Long id);
}

인터페이스를 작성하였습니다.

PlayerDao는 ID로 선수 정보를 조회하는 API를 가지고 있습니다.

이제 구현체를 작성할 시간입니다. 네이밍 컨벤션도 이전 기술에 어울리게 구닥다리로 작성하였습니다.
(너무 오랜만에 JDBC API를 써봐서 구글에 검색해가며 작성한 건 안 비밀)

package io.lcalmsky.jpa.jdbc.domain.model.dao;

import io.lcalmsky.jpa.jdbc.domain.model.dto.Player;
import lombok.extern.slf4j.Slf4j;

import java.sql.*;

@Slf4j
public class PlayerDaoImpl implements PlayerDao {
    private final String url = "db 주소";
    private final String user = "db 계정 정보(아이디)";
    private final String password = "db 계정 정보(비밀번호)";

    @Override
    public Player findPlayerById(Long id) {
        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        final String sql = "SELECT NAME, GOALS, ASSISTS FROM PLAYER WHERE ID = ?";
        try {
            connection = getConnection();
            preparedStatement = connection.prepareStatement(sql);
            resultSet = preparedStatement.executeQuery();
            if (resultSet.next()) {
                Player player = new Player();
                player.setId(id);
                player.setName(resultSet.getString("NAME"));
                player.setGoals(resultSet.getInt("GOALS"));
                player.setAssists(resultSet.getInt("ASSISTS"));
                return player;
            }
        } catch (SQLException e) {
            log.error("failed to find player from database", e);
        } finally {
            try {
                if (resultSet != null) resultSet.close();
                if (preparedStatement != null) preparedStatement.close();
                if (connection != null) connection.close();
            } catch (SQLException e) {
                log.error("failed to close resources", e);
            }
        }
        return null;
    }

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, user, password);
    }
}

여기까지가 겨우 "조회" 기능이었습니다.

그렇다면 "저장"도 해봐야겠죠?

PlayerDao 인터페이스에 API를 추가하고 구현체에도 역시 추가해줬습니다.

package io.lcalmsky.jpa.jdbc.domain.model.dao;

import io.lcalmsky.jpa.jdbc.domain.model.dto.Player;

public interface PlayerDao {
    Player findPlayerById(Long id);

    void savePlayer(Player player);
}

PlayerDaoImpl.java

@Override
public void savePlayer(Player player) {
    Connection connection = null;
    PreparedStatement preparedStatement = null;
    final String sql = "insert into player values (?, ?, ?)";

    try {
        connection = getConnection();
        preparedStatement = connection.prepareStatement(sql);
        preparedStatement.setString(1, player.getName());
        preparedStatement.setInt(2, player.getGoals());
        preparedStatement.setInt(3, player.getAssists());
        preparedStatement.executeUpdate();
    } catch (SQLException e) {
        log.error("failed to find player from database", e);
    } finally {
        try {
            if (preparedStatement != null) preparedStatement.close();
            if (connection != null) connection.close();
        } catch (SQLException e) {
            log.error("failed to close resources", e);
        }
    }
}

다음은 수정과 삭제 API를 만들어야겠죠.

만약에 데이터베이스를 사용하지 않고 그냥 메모리에 저장한다고 가정하면

package io.lcalmsky.jpa.jdbc.domain.model.dao;

import io.lcalmsky.jpa.jdbc.domain.model.dto.Player;

import java.util.Map;

public class PlayerMemoryDao implements PlayerDao {
    private final Map<Long, Player> internalMap;

    public PlayerMemoryDao() {
        this.internalMap = new HashMap<>();
    }


    @Override
    public Player findPlayerById(Long id) {
        return internalMap.get(id);
    }

    @Override
    public void savePlayer(Player player) {
        internalMap.put(player.getId(), player);
    }
}

이렇게 간단히 끝낼 수 있는 작업이지만 데이터베이스와 객체는 다른 구조를 가지고 있기 때문에 개발자가 중간에서 변환하는 작업을 직접 해줘야 합니다.

물론 JDBC API를 사용하더라도 공통적인 부분을 추상화하고 그때그때 달라지는 부분만 구현체로 넘겨주는 등의 방식으로 Context 클래스를 작성하면 저 작업도 매우 수월해집니다. (하지만 이러한 추상화 작업의 결과물이 프레임워크라는 사실!)

테이블이 여러 개라면, 또 같은 조회라도 다른 식으로 구현하고 싶다면 더 많은 반복이 필요하게 되고, 결정적으로 연동할 데이터베이스의 테이블의 구조에 대해 개발자 본인 스스로 알고 있어야 하기 때문에 IDE 하나만 띄워놓고 작업하는 것은 불가능에 가깝습니다.

2. 의존성

위의 예제에서 선수 정보에 총 드리블 횟수, 성공한 드리블 횟수를 추가하려고 합니다.

먼저 Player 클래스를 수정해줍니다.

package io.lcalmsky.jpa.jdbc.domain.model.dto;

import lombok.Data;

@Data
public class Player {
    private Long id;
    private String name;
    private Integer goals;
    private Integer assists;
    private Integer dribbleTotalCount;
    private Integer dribbleSuccessCount;
}

쿼리도 달라져야하고 매핑하는 로직 또한 추가되어야 합니다.

public Player findPlayerById(Long id) {
    // ...
    final String sql = "SELECT NAME, GOALS, ASSISTS, DRIBBLE_TOTAL_COUNT, DRIBBLE_SUCCESS_COUNT FROM PLAYER WHERE ID = ?";
    // ...
    if (resultSet.next()) {
        Player player = new Player();
        player.setId(id);
        player.setName(resultSet.getString("NAME"));
        player.setGoals(resultSet.getInt("GOALS"));
        player.setAssists(resultSet.getInt("ASSISTS"));
        player.setDribbleTotalCount(resultSet.getInt("DRIBBLE_TOTAL_COUNT"));
        player.setDribbleSuccessCount(resultSet.getInt("DRIBBLE_SUCCESS_COUNT"));
        return player;
    }
    // ...

여간 번거로운 작업이 아닙니다.

필드가 하나 추가될 때마다 모든 부분의 소스 코드가 수정되게 됩니다.

이는 SQL에 의존적인 소스 코드가 되는 것이고 DAO, DTO가 수정될 때마다 해당하는 클래스를 직접 확인해가며 개발해야 하는 불편함을 유발합니다.

3. JPA가 출동하면 어떻게 될까?

(발퀄 죄송합니다)

JPA를 사용하면 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 통해 데이터를 관리할 수 있습니다.

JPA가 제공하는 CRUD API에 대해 간단히 살펴보면 아래와 같습니다.

- 저장

jpa.persist(player);

- 조회

Long id = "1234L";
Player player = jpa.find(Player.class, id);

- 수정 (데이터베이스와 연동시 UPDATE SQL을 전달)

Player player = jpa.find(Player.class, id);
player.setGoals(player.getGoals() + 1);

- 커스텀 클래스를 가지는 객체에서의 조회

Player player = jpa.find(Player.class, id);
Team team = player.getTeam(); // 이 시점에 조회

위에서 언급한 메모리에 Collection 객체 등을 이용해 관리하는 것과 크게 다르지 않습니다.

매우 익숙한 API들을 추가로 구현할 필요 없이 바로 사용 가능합니다.

이러한 API에 대해서는 이후 포스팅에서 살펴보기로 하고 그전에 객체를 다루는 방식과 관계형 데이터베이스를 다루는 방식의 차이점에서 발생하는 문제점에 대해서도 살펴봐야겠죠.

다음 포스팅에서 뵙겠습니다!

 

본 포스팅은 자바 ORM 표준 JPA 프로그래밍을 참고(+ 내 지식, 내 뇌피셜, TMI)하여 작성하였습니다.

'JPA' 카테고리의 다른 글

JPA란?  (0) 2020.04.03
객체와 데이터베이스의 체계와 한계  (0) 2020.04.02
JPA를 공부하는 이유  (2) 2020.03.31
기본 키(Primary Key) 매핑 전략  (0) 2019.10.01
JPA 데이터베이스 스키마 자동 생성  (0) 2019.09.30
댓글