Study/Spring

[Spring 5 Recipes] Spring 5 Recipes 9장 정리 #1

꼽냥이 2021. 10. 24. 20:54
반응형

데이터 액세스


JDBC

JDBC (Java DataBase Conectivity) 는 DB 제작사에 구애받지 않고 RDBMS 에 접근할 수 있도록 표준 API 셋을 제공한다. DB 에 SQL 문을 실행하는 API 를 제공하는 것이 JDBC 의 주용도지만, JDBC 를 있는 그대로 사용하려면 직접 DB 리소스를 관리하면서 예외도 명시적으로 처리해야 해 개발에 있어 번거롭다. 스프링은 JDBC 를 추상화한 프레임워크인 JDBC Template 을 지원하는데 이를 통해 여러 유형의 JDBC 작업을 템플릿 메소드로 묶어서 제공한다. 전체 작업 흐름은 템플릿 메소드가 각각 알아서 관장하고 개발자는 원하는 작업을 오버라이딩해 사용할 수 있어 개발자에게 편의성을 제공한다.

 

스프링 ORM

JDBC 자체만으로는 필요한 요건을 충족하기 어렵고 개발 편의성을 더 추구할 경우 더 고수준의 추상화를 지원하는 스프링 ORM (Object-Relational Mapping) 솔루션을 사용할 수 있다. 스프링은 하이버네이트, JDO, iBatis, JPA (Java Persistence API) 등의 유명한 ORM 프레임워크를 지원한다. ORM 프레임워크는 매핑 메타데이터에 따라 객체를 저장하는 기술이다. 객체를 저장하는 SQL 문은 런타임에 생성되므로 특정 DB 에 있는 기능을 불러쓰거나 직접 SQL 문을 튜닝하지 않는 이상 DB 에 특정한 SQL 을 작성할 필요가 없다. 결과적으로 DB 에 독립적인 어플리케이션을 개발할 수 있고, 차후 다른 DB 로 마이그레이션하기 쉽다.

Hibernate 는 자바 커뮤니티에서 널리 알려진 고성능 오픈 소스 ORM 프레임워크로, JDBC 호환 DB 는 대부분 지원하며 DB 별 방언 (Dialect) 을 이용해 액세스할 수 있다. ORM 기본 외에 Caching, Cascading, Lazy Loading 과 같은 고급 기능들을 제공하고 HQL (Hibernate Query Language) 을 이용하면 단순하면서 강력한 객체 쿼리를 수행할 수 있다.

 

JDBC 를 직접 사용할 경우 문제점


자동차 정보를 등록하는 애플리케이션을 개발하려 한다. 자동차 레코드를 CRUD 하는 게 주된 기능이고, 레코드는 RDBMS 에 저장해 JDBC 로 접근한다. 다음은 자동차를 나타내는 Vehicle 클래스이다.

public class Vehicle {
	
    private String vehicleNo;
    private String color;
    private int wheel;
    private int seat;
    
    // Constructor, Setter, Getter ...
}

 

DAO 디자인 패턴

서로 다른 종류의 로직 (Presentation Logic, Business Logic, Data Access Logic 등) 을 하나의 거대한 모듈에 뒤섞는 설계상 실수를 저지르는 경우가 왕왕 있다. 이렇게 하면 모듈 간에 단단하게 결합돼 재사용성과 유지 보수성이 현저히 떨어지게 된다. DAO 는 이런 문제를 해결하고자 데이터 액세스 로직을 표현하고, 비즈니스 로직과는 분리해 DAO 라는 독립적인 모듈에 데이터 액세스 로직을 담아 캡슐화한 패턴이다.

자동차 등록 애플리케이션에서도 자동차 레코드를 CRUD 하는 각종 데이터 액세스 작업을 추상화할 수 있다. 다음과 같이 인터페이스로 선언하면 여러 DAO 구현 기술을 적용할 수 있다.

public interface VehicleDao {
    void insert(Vehicle vehicle);
    void insert(Iterable<Vehicle> vehicles);
    void update(Vehicle vehicle);
    void delete(Vehicle vehicle);
    Vehicle findByVehicleNo(String vehicleNo);
    List<Vehicle> findAll();
}

JDBC API 는 대부분 java.sql.SQLException 예외를 던지도록 설계됐지만 이 인터페이스의 유일한 의의는 데이터 액세스 작업의 추상화이므로 구현 기술에 의존해서는 안 된다. JDBC 에 특정한 SQLException 을 던지는 것은 취지에 어긋나므로 보통 DAO 구현체에서는 RuntimeException 의 하위 예외 (직접 구현한 Exception 하위 클래스나 제네릭형) 로 감싼다.

 

JDBC 로 DAO 구현하기

JDBC 를 사용해 DB 데이터에 액세스하려면 DAO 구현 클래스 (JdbcVehicleDao) 가 필요하고 DB 에 SQL 문을 실행하려면 드라이버 클래스명, DB URL, 유저명, 패스워드를 지정해 DB 에 접속해야 한다. 하지만 미리 구성된 javax.sql.DataSource 객체를 이용하면 접속 정보를 자세히 몰라도 DB 에 접속할 수 있다.

public class JdbcVehicleDao implements VehicleDao {
	
    private static final String INSERT_SQL =
        "INSERT INTO VEHICLE (COLOR, WHEEL, SEAT, VEHICLE_NO) VALUES (?, ?, ?, ?)";
    private static final String UPDATE_SQL =
        "UPDATE VEHICLE SET COLOR = ?, WHEEL = ?, SEAT = ? WHERE VEHICLE_NO = ?";
    private static final String SELECT_ALL_SQL =
        "SELECT * FROM VEHICLE";
    private static final String SELECT_ONE_SQL =
        "SELECT * FROM VEHICLE WHERE VEHICLE_NO = ?";
    private static final String DELETE_SQL =
        "DELETE FROM VEHICLE WHERE VEHICLE_NO = ?";
    
    @Autowired
    private final DataSource dataSource;
    
    @Override
    public void insert(Vehicle vehicle) {
        try (Connection conn = dataSource.getConnection();
            PreparedStatement ps = conn.prepareStatement(INSERT_SQL)){
            prepareStatement(ps, vehicle);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public void insert(Collection<Vehicle> vehicle) {
        vehicles.forEach(this::insert);
    }
    
    @Override
    public Vehicle findByVehicleNo(String vehicleNo) {
        try (Connection conn = dataSource.getConnection();
            PreparedStatement ps = conn.prepareStatement(SELECT_ONE_SQL)) {
            ps.setString(1, vehicleNo);
            
            Vehicle vehicle = null;
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    vehicle = toVehicle(rs);
                }
            }
            return vehicle;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Override
    public List<Vehicle> findAll() {
        try (Connection conn = dataSource.getConnection();
            PreparedStatement ps = conn.prepareStatement(SELECT_ALL_SQL);
            ResultSet rs = ps.executeQuery()) {
            List<Vehicle> vehicles = new ArrayList<>();
            while (rs.next()) {
                vehicles.add(toVehicle(rs));
            }
            return vehicles;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    
    private Vehicle toVehicle(ResultSet rs) throws SQLException {
        return new Vehicle(rs.getString("VEHICLE_NO"),
            rs.getString("COLOR"),
            rs.getInt("WHEEL"),
            rs.getInt("SEAT"));
    }
    
    private void prepareStatement(PreparedStatement ps, Vehicle vehicle)
        throws SQLException {
        ps.setString(1, vehicle.getColor());
        ps.setInt(2, vehicle.getWheel());
        ps.setInt(3, vehicle.getSeat());
        ps.setString(4, vehicle.getVehicleNo());
    }
    
    @Override
    public void update(Vehicle vehicle) { ... }
    
    @Override
    public void delete(Vehicle vehicle) { ... }
}

자동차 레코드의 등록 작업은 전형적인 JDBC 로 데이터를 수정하는 시나리오이다. insert() 를 호출할 때마다 데이터 소스에서 접속 객체를 얻어와 SQL 문을 실행한다. try-with-resources 메커니즘을 적용한 위 코드는 사용을 마친 리소스를 자동으로 닫는다. try-with-resources 블록을 사용하지 않으면 사용한 리소스를 닫았는지 잘 기억해야 하고 혹여 착오가 있으면 메모리 누수로 이어질 가능성이 있다.

 

스프링 데이터 소스 구성하기

javax.sql.DataSource 는 Connection 인스턴스를 생성하는 표준 인터페이스로, JDBC 명세에 규정되어 있다. 데이터 소스 인터페이스는 여러 구현체가 있는데, 그중 HikariCP 와 Apache Commons 가 가장 잘 알려진 오픈 소스 구현체이다. 종류는 다양하지만 모두 DataSource 라는 공통 인터페이스를 구현하기 때문에 다른 걸로 바꿔쓰기 쉽다. 스프링 역시 간단하면서 기능은 막강한 데이터 소스 구현체를 지원한다. 그중 요청을 할 때마다 접속을 새로 여는 DriverManagerDataSource 는 가장 간단한 구현체이다.

@Configuration
public class VehicleConfiguration {
	
    @Bean
    public VehicleDao vehicleDao() {
        return new JdbcVehicleDao(dataSource());
    }
    
    @Bean
    public DataSource dataSource() {
        DrvierManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(ClientDriver.class.getName());
        dataSource.setUrl("jdbc_url");
        dataSource.setUserName("ds_user");
        dataSource.setPassword("ds_pwd");
        return dataSource;
    }
}

 

DAO 실행하기

이제 새 자동차 레코드를 DB 에 등록하는 DAO 를 테스트한다. 별 문제 없으면 DB 에 등록한 자동차 정보가 곧바로 출력될 것이다.

public class Main {
	
    public static void main(String[] args) {
        ApplicationContext context =
            new AnnotationConfigApplicationContext(VehicleConfiguration.class);
        
        VehicleDao vehicleDao = context.getBean(VehicleDao.class);
        Vehicle vehicle = new Vehicle("TEM0001", "RED", 4, 4);
        vehicleDao.insert(vehicle);
        
        vehicle = vehicleDao.findByVehicleNo("TEM0001");
        System.out.println(vehicle);
    }
}

 

이렇게 JDBC 를 직접 사용해 DAO 를 구현할 수 있지만, 보다시피 비슷하게 생긴 코드가 DB 작업을 처리할 때마다 반복되는 걸 확인할 수 있다. 이처럼 코드가 계속 중복되면 DAO 메소드는 장황해지고 가독성은 떨어지게 된다.

 

다음에 정리할 내용부터, 위와 같이 JDBC 를 직접 사용했을 때의 문제를 해결하기 위한 방법들을 제공한다. JDBC Template 부터 ORM 프레임워크까지, 이 방법들을 통해 데이터 액세스 로직보다는 비즈니스 로직에 집중하는 개발 방법을 확인할 수 있다.

 

#Reference.

 

스프링 5 레시피(4판)

이 책은 스프링 5에 새로 탑재된 기능 및 다양한 구성 옵션 등 업데이트된 프레임워크 전반을 실무에 유용한 해법을 제시하는 형식으로 다룹니다. IoC 컨테이너 같은 스프링 기초부터 스프링 AOP/A

www.hanbit.co.kr