[JPA] ORM과 엔티티, 영속성 컨텍스트 개념, JDBC Database Access의 여러 방법과 명령어, 비교

dejeong·2024년 10월 8일

DBMS

목록 보기
7/10
post-thumbnail

ORM(Object-Relational Mapping)

자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법으로 데이터베이스의 값을 마치 객체처럼 사용할 수 있다. SQL을 전혀 몰라도 자바 언어로만 데이터베이스에 접근해서 원하는 데이터를 받아올 수 있는 것으로 객체와 데이터베이스를 연결해 자바 언어로만 데이터베이스를 다룰 수 있게 하는 도구를 ORM이라고 한다.

[장점]

  • SQL을 직접 작성하지 않고 사용하는 언어로 데이터베이스에 접근할 수 있다.
  • 객체지향적으로 코드를 작성할 수 있기 때문에 비즈니스 로직에만 집중할 수 있다.
  • 데이터베이스 시스템이 추상화 되어있기 때문에 MySQL에서 PostgreSQL로 전환한다고 해도 추가로 드는 작업이 거의 없다. → 데이터베이스 시스템에 대한 종속성이 줄어든다.
  • 매핑하는 정보가 명확하기 때문에 ERD에 대한 의존도를 낮출 수 있고 유지보수할 때 유리하다.

[단점]

  • 프로젝트의 복잡성이 커질수록 사용 난이도도 올라간다.
  • 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능한 경우가 있다.

자바에서는 ORM 기술 표준으로 JPA(Java Persistence API)를 사용하는데, JPA를 사용하면 객체와 관계형 데이터베이스의 테이블 데이터를 매핑할 수 있다.

JPA(Java Persistence API)

자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스, 인터페이스이므로 실제 사용을 위해서는 ORM 프레임워크를 추가로 선택해야 한다. 대표적으로는 하이버네이트(hibernate)를 많이 사용하며, 하이버네이트는 JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크이다. 내부적으로는 JDBC API를 사용하고, 하이버네이트의 목표는 자바 객체를 통해 데이터베이스 종류에 상관없이 데이터베이스를 자유자재로 사용할 수 있게 하는 데 있다.

엔티티

데이터베이스의 테이블과 매핑되는 객체, 자바 객체이므로 일반 객체와 다르지 않지만 데이터베이스의 테이블과 직접 연결된다는 특징이 있어 구분지어 부른다. 엔티티는 객체이긴 하지만 데이터베이스에 영향을 미치는 쿼리를 실행하는 객체이다.

엔티티 매니저

엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 한다. (엔티티 매니저를 만드는 곳이 엔티티 매니저 팩토리) 회원 2명이 동시에 회원가입을 하려는 경우 엔티티 매니저는 회원1의 요청에 대해 가입 처리 할 엔티티 매니저를 엔티티 매니저 팩토리가 생성하면 이를 통해 가입 처리해 데이터베이스에 회원 정보를 저장(회원 2도 같음) 하고, 회원 1, 2를 위해 생성된 엔티티 매니저는 필요한 시점에 데이터베이스와 연결한 뒤에 쿼리한다.

스프링 부트에서는 직접 엔티티를 매니저 팩토리를 만들어서관리하지 않고, 내부에서 엔티티 매니저 팩토리를 하나만 생성, 관리, @PersistenceContext 또는 @Autowired 어노테이션을 사용해서 엔티티 매니저를 사용한다.

// 스프링 부트가 엔티티 매니저를 사용하는 방법
@PersistenceContext
EntityManager em;    // 프록시 엔티티 매니저. 필요할 때 진짜 엔티티 매니저를 호출

스프링 부트는 기본적으로 빈을 하나만 생성해서 공유하므로 동시성 문제가 발생할 수 있기 때문에 실제로는 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시(가짜) 엔티티 매니저를 사용한다. (필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출) 스프링에서는 이 엔티티 매니저를 Spring Data JPA에서 관리하므로 개발자가 직접 생성하거나 관리할 필요가 없다.

영속성 컨텍스트

JPA의 중요한 특징 중 하나로, 엔티티를 관리하는 가상의 공간을 말하며. ****엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다. 영속성 컨텍스트가 있기 때문에 데이터베이스에서 효과적으로 데이터를 가져올 수 있고, 엔티티를 편하게 사용할 수 있다.

데이터 조작을 위하여 SQL쿼리를 직접 작성하는 방법도 있지만, 스프링 부트에서는 이런 쿼리를 자바 코드로 작성하고 이를 JPA가 알아서 쿼리로 변경해주고 있다.

[영속성 컨텍스트의 특징]

데이터베이스의 접근을 최소화하여 성능을 높일 수 있다

  • 1차 캐시 : 영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 이때 캐시의 키는 엔티티의 @Id 어노테이션이 달린 기본키 역할을 하는 식별자이며 값은 엔티티이다. 엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환한다. 값이 없으면 데이터베이스에서 조회해 1차 캐시에 저장한 다음 반환하기 때문에 캐시된 데이터를 조회할 때에는 데이터베이스를 거치지 않고도 데이터를 가져올 수 있기 때문에, 빠른 데이터 조회가 가능하다.
  • 쓰기 지연 : 트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미한다. 적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있다. (데이터 추가 쿼리가 3개라면 영속성 컨텍스트는 트랜잭션을 커밋하는 시점에 3개의 쿼리를 한꺼번에 전송)
  • 변경 감지 : 트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경사항을 감지해 변경된 값을 데이터베이스에 자동으로 반영한다. 쓰기 지연과 마찬가지로 적당한 묶음으로 쿼리를 요청할 수 있고, 데이터베이스 시스템의 부담을 줄일 수 있다.
  • 지연 로딩 : 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라, 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미한다.

엔티티 상태

특정 메소드를 호출해 변경할 수 있는데요, 필요에 따라 엔티티의 상태를 조절해 데이터를 올바르게 유지하고 관리할 수 있다.

  • 분리(detached)상태 : 영속성 컨텍스트가 관리하고 있지 않은 상태
  • 관리(managed)상태 : 영속성 컨텍스트가 관리하는 상태
  • 비영속(transient)상태 : 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 삭제(removed)상태
// 엔티티 상태 변경
public class EntityManaberTest {
		@Autowired
		EntityManager em;

		public void example() {
				// 1. 엔티티 매니저가 엔티티를 관리하지 않는 상태(비영속 상태)
				Member member = new Member(1L, "홍길동");

				// 2. 엔티티가 관리되는 상태
				em.persist(member);

				// 3. 엔티티 객체가 분리된 상태
				em.detach(member);

				// 4. 에닡티 객체가 삭제된 상태
				em.remove(member);
		}
}

엔티티는 처음 만들면 비영속 상태가 된다.

  • persist() : 엔티티를 관리 상태로 만들 수 있으며, Member 객체는 영속성 컨텍스트에서 관리 상태가 된다.
  • detach() : 엔티티를 영속성 컨텍스트에서 관리하고 싶지 않을 때, detach() 메소드를 사용해 분리 상태로 만들 수 있다.
  • remove() : 객체가 더 이상 필요 없을 때 영속석 컨텍스트와 데이터베이스에서 삭제

JDBC Database Access

JDBC API : 개발자가 사용 ⇒ Connection

public void connectToAndQueryDatabase(String username, String password) {
    Connection con = DriverManager.getConnection(
                         "jdbc:myDriver:myDatabase",
                         username,
                         password);

    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM student");

    while (rs.next()) {
        int x = rs.getLong("id");
        String s = rs.getString("name");
        int age = rs.getInt("age");
        //decimal
        float age = rs.getFloat("test");
    }
}

public void connectToAndQueryDatabase2(String username, String password) {
    Connection con = DriverManager.getConnection(
                         "jdbc:myDriver:myDatabase",
                         username,
                         password);

    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM instructor");

    while (rs.next()) {
        int x = rs.getLong("id");
        String s = rs.getString("name");
    }
}

반복되는 코드를 PagingAndSortingRepository 기능을 활용하여 반복되는 것을 줄일 수 있다. JPA에 항상 동작하는 건 아니고 힌트만 줘도 JPA가 자동으로 만들어서 동작할 수 있게 해준다.

직접 선언하여 사용, 수정할 때 관련된 모든 것을 함께 수정해야 하는 번거로움이 있다.

package com.estsoft.example;

import java.sql.*;

public class PlainJdbcExample {
    static final String DB_URL = "jdbc:mysql://localhost:3306/test";
    static final String USER = 
    static final String PASS = 
    static final String QUERY = "SELECT * FROM student";

    public static void main(String[] args) {
        // Open & get a connection
        try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(QUERY);) {
            // Extract data from result set after execute query
            while (rs.next()) {
                // Retrieve by column name
                System.out.println("======[student]======]");
                System.out.print("ID: " + rs.getLong("id"));
                System.out.print(", name: " + rs.getString("name"));
                System.out.print(", Age: " + rs.getInt("age"));
                System.out.println(", desc: " + rs.getString("desc"));
            }
        } catch (SQLException e) {
            System.out.println(e.getErrorCode());
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }
}

jdbc를 사용하여 호스트 연결, application.properies 에 등록되어 있는 정보를 사용하여 자동으로 호스트를 가져온다.

JDBC Driver : RDBMS 벤더에서 (oracle, mysql, mssql..)
spring.application.name=spring-project-test

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=
spring.datasource.password=

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

#logging.config=classpath:logback-local.xml
package com.estsoft.springprojecttest.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;

@Repository("StudentJdbcRepository")
public class StudentJdbcRepository {
    @Autowired
    private JdbcTemplate jdbcTemplate; 

    public List<String> selectStudentNameList(){
        return jdbcTemplate.queryForList("select name from student", String.class);
    }
    
    public List<String> selectStudentName(String name){
        return jdbcTemplate.queryForList("select name from student where name='" + name + "'", String.class);
    }

    public Map<String, Object> selectStudentName2(String name, Integer age){
        return jdbcTemplate.queryForMap("select name from student where name= ? and age >= ?", name);
    }

    public List<String> selectInstructorNameList(){
        return jdbcTemplate.queryForList("select name from instructor", String.class);
    }
}
package com.estsoft.springprojecttest.controller;

import com.estsoft.springprojecttest.repository.StudentJdbcRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class StudentController {
    @Autowired
    private StudentJdbcRepository studentJdbcRepository;

    @GetMapping("/student/name/list")
    public List<String> getListOfStudentName(){
        return studentJdbcRepository.selectStudentNameList();
    }

    @GetMapping("/student/name")
    public Map<String, Object> getStudentName(@RequestParam String name){
        return studentJdbcRepository.selectStudentName(name);
    }

    @GetMapping("/student/name2")
    public Map<String, Object> getStudentName2(@RequestParam String name, @RequestParam int age){
        return studentJdbcRepository.selectStudentName(name, age);
    }

}
### GET student
GET http://localhost:8080/student/name/list
Content-Type: application/json

### GET student
GET http://localhost:8080/student/name?name=김철수&age=20
Content-Type: application/json

### GET student
GET http://localhost:8080/student/name2?name=김철수&age=20
Content-Type: application/json

? 부분에서 잘못 매칭될 수도 있기 때문에 또 다시 새로운 방법으로 NamedParameterJdbcTemplate

 @Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public Map<String, Object> selectStudentName2(String name, Integer age){
    return namedParameterJdbcTemplate.queryForMap("select name from student where name = :name and age > :age", new MapSqlParameterSource("name", name).addValue("age",age)); // 명시적으로 선언해서 순서 바뀜으로 생길 수 있는 오류 방지 가능
}

자바 클래스로 관리하고 싶을 때 사용할 수 있는 방법

public Student selectStudentName3(String name, Integer age){
    return namedParameterJdbcTemplate.queryForObject("select * from student where name = :name and age > :age", new MapSqlParameterSource("name", name).addValue("age",age),(rs, rowNum) -> new Student(rs.getLong("id"),rs.getString("name"), rs.getInt("age"), rs.getString("desc"),rs.getDate("created_at")));
}
package com.estsoft.springprojecttest.repository;

import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.Date;

// ROW MAPPER ===> select * from student 1줄 1줄 결과를 row -> 이 row data에 mapping되는 java object로 연결해주는 기능

@Getter
@Setter
public class Student {
    private Long id;
    private String name;
    private Integer age;
    private String desc;
    private Date createdAt;

    public Student(){}
    public Student(Long id, String name, Integer age, String desc, Date createdAt) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.desc = desc;
        this.createdAt = createdAt;
    }
}
@GetMapping("/student/name3")
public Map<String, Object> getStudentName3(@RequestParam String name, @RequestParam int age){
    return studentJdbcRepository.selectStudentName(name, age);
}

반복되는 코드를 편하게 쓰기 위해서 ROWMAPPER자체를 기능화

package com.estsoft.springprojecttest.repository;

import org.springframework.jdbc.core.RowMapper;

import java.sql.ResultSet;
import java.sql.SQLException;

public class StudentRowMapper implements RowMapper<Student> {
    @Override
    public Student mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Student(
                rs.getLong("id"),
                rs.getString("name"),
                rs.getInt("age"),
                rs.getString("desc"),
                rs.getDate("created_at")
        );
    }
}
public Student selectStudentName4(String name, Integer age){
    return namedParameterJdbcTemplate.queryForObject(
		    "SELECT * FROM student WHERE name = :name AND age > :age", 
		    new MapSqlParameterSource("name", name).addValue("age",age), 
		    new StudentRowMapper()
    );
}

queryForList : 복수 건을 조회, ID는 안됨

queryForMap : 한 건만 조회

queryForObject : 한 건만 조회

fineAll() : 0~여러 건일 때 조회이지만 리스트로 List

findById : 한 건만, 레포지토리에 만들지 않았지만 엔티티를 만들 때 항상 @Id 어노테이션으로 약속되어 있기 때문에 자동으로 제공해준다.

profile
룰루

0개의 댓글