본문 바로가기

Spring

데이터베이스 Driver와 JDBC

DB 저장 방식

 

Server Mode

  • 애플리케이션 외부에서 디비 엔진이 실행되기 때문에 애플리케이션을 종료해도 데이터가 사라지지 않음

In-memory Mode

  • 애플리케이션 내부에서 디비 엔진이 실행(실행 주체가 스프링)되기 때문에 애플리케이션을 종료하면 디비 엔진도 함께 종료
  • 애플리케이션의 메모리에 데이터가 저장됨
  • mem을 기재하여 애플리케이션실행 메모리(스프링 실행 메모리) 자체에서 디비를 사용하겠다는 것을 선언
    • 설정 코드 : spring.datasource.url=jdbc:h2:mem:{DB 이름}

Embedded Mode

  • 애플리케이션 내부에서 디비 엔진이 실행(실행 주체가 스프링)
  • 애플리케이션 외부에 데이터가 저장되므로 애플리케이션을 종료해도 데이터는 사라지지 않음
    • 설정 코드 : spring.datasource.url=jdbc:h2:{DB가 저장될 경로}

 

JDBC

Java 앱과 DB 를 연결시켜주기 위해 만들어진 기술 -> JPA도 이 기술을 사용하여 구현

 

JDBC Driver

DB와 애플리케이션(스프링)간의 통신을 중개하는 역할

 

 

JDBC Driver 동작 방식

  1. 연결 초기화
    • 애플리케이션이 드라이버에 연결 요청
    • 드라이버는 디비 서버에 로그인하고 연결 완료
  2. sql 전송 및 실행
    • 애플리케이션에서 받은 명령을 디비가 이해할 수 있는 형태로 변환
    • 변환된 명령을 디비 서버로 전송해서 실행
  3. 결과 처리
    • 디비에서 작업의 결과를 드라이버로 보내면 이 결과를 애플리케이션에서 이해할 수 있는 형태로 변환
    • 해당 결과를 드라이버는 애플리캐이션으로 전송
  4. 연결 종료
    • 작업이 완료되면 드라이버는 디비서버와의 연결을 종료

 

JDBC Driver Manager

1. Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만듦

2. Statement(상태) 를 생성하여 쿼리를 요청하게 함

3. ResultSet(결과셋) 을 생성해 쿼리 결과를 받아올 수 있게 해줌

 

Statement

동작방식 및 실행 방법

 

  • executeQuery() 나 executeUpdate() 를 실행하는 시점에 파라미터로 SQL문을 전달
  • SQL문을 수행하는 과정에서 구문 분석을 수행하기 때문에 효율성이 떨어짐

 

PreparedStatement

 

  • Statement를 상속하고 있는 Interface
  • 내부적으로 Statement의 4단계(구문분석, 치환, 실행, 인출) 과정 중 첫 번째 parse 과정의 결과를 캐싱하고,
  • 나머지 3가지 단계만 거쳐서 SQL문이 실행
  • 구문 분석(parse)의 결과를 캐싱해서 과정을 생략할 수 있으므로 Statement보다 성능이 향상
  • SQL Injection 도 방어

 

순수 JDBC

  • try가로 안에 자원을 넣으면 해당 try문이 끝나면 close 메서드를 호출해서 자원을 반환
  • 직접 Connection, Statement, ResultSet을 만들어 줘야 한다
public class JdbcApplication {

    public static void main(String[] args) throws SQLException {
        // 어플리케이션 실행 컨텍스트 생성
        SpringApplication.run(JdbcApplication.class, args);

        // 데이터베이스 연결정보
        String url = "jdbc:h2:mem:test";    // spring.datasource.url
        String username = "sa";             // spring.datasource.username

        try (Connection connection = DriverManager.getConnection(url, username, null)) {
            // 테이블 생성
            String createSql = "CREATE TABLE USERS (id SERIAL, username varchar(255))";
            try (PreparedStatement statement = connection.prepareStatement(createSql)) {
                statement.execute();
            }

            // 데이터 추가
            String insertSql = "INSERT INTO USERS (username) VALUES ('teasun kim')";
            try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
                statement.execute();
            }

            // 데이터 조회
            String selectSql = "SELECT * FROM USERS";
            try (PreparedStatement statement = connection.prepareStatement(selectSql);
                 ResultSet rs = statement.executeQuery()) {

                while (rs.next()) {
                    System.out.printf("%d, %s%n", rs.getInt("id"), rs.getString("username"));
                }
            }
        } catch (SQLException e) {
            if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
                System.out.println("USERS 테이블이 이미 존재합니다.");
            } else {
                throw new RuntimeException(e);
            }
        }
    }
}

 

JDBC Template (QueryMapper)

  • 직접 SQL을 실행하여 매핑
  • SQL 쿼리 요청시 중복 코드 발생 및 Connection, Statement 등.. 자원 관리를 따로 해줘야하고 예외가 모두 Checked Exception (SQL Exception) 처리되는 문제 발생
public List<User> getUsers() {
    String sql = "SELECT id, name, age FROM users";
    
    return jdbcTemplate.query(sql, (rs, rowNum) -> 
        new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age"))
    );
}

 

QueryMapper의 문제를 해결하기 위한 Persistence Framework 등장

SQL Mapper : JDBC Template, MyBatis

ORM : JPA, Hibernate

 

JDBC Template (RowMapper)

  • SQL Mapper 첫번째 주자로 JDBCTemplate 에 RowMapper 탄생
    • 쿼리 수행 결과와 객채 필드 매핑
    • RowMapper 로 응답필드 매핑코드 재사용
    • Connection, Statement, ResultSet 반복적 처리 대신 해줌
    • 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함
public class UserRowMapper implements RowMapper<User> {

	// JDBCTemplate 에서 row 응답을 mapRow() 메서드에 rs 파라미터로 넘겨주어 객체에 매핑하기 쉽도록 도와준다.
  @Override
  public User mapRow(ResultSet rs, int rowNum) throws SQLException {
    var user = new User();
    user.setId(rs.getInt("ID"));
    user.setName(rs.getString("NAME"));
    return user;
  }
}
@Repository
public class DataRepository {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	// 테이블 생성
	public void createTable() {
		jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS users (id SERIAL, name VARCHAR(255))");
	}

	// 사용자 추가 (Create)
	public void insertUser(String name) {
		jdbcTemplate.update("INSERT INTO users (name) VALUES (?)", name);
	}

	// 사용자 ID로 User 조회 (Read)
	public User findUserById(Long id) {
		return jdbcTemplate.queryForObject(
			"SELECT * FROM users WHERE id = ?",
			new UserRowMapper(), // 이자리에 매퍼를 생성자로 넣어주면 됨
			id
		);
	}

	// 사용자 이름 변경 (Update)
	public void updateUser(Long id, String newName) {
		jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", newName, id);
	}

	// 사용자 삭제 (Delete)
	public void deleteUser(Long id) {
		jdbcTemplate.update("DELETE FROM users WHERE id = ?", id);
	}
}