RawJPA
(아아아앗 mvc jdbc 랑 entity manager 의 노가다 가 생각난다......)
ORM 의 탄생
왜 ORM 의 탄생 배경을 알아야 하나?
1. 내가 입사할 회사에 JPA 가 적용안된 프로젝트가 있을 수 있다!
DB 탄생후 수십년 휘에 ORM 이 나오고
또 수십년 뒤에 진화해서 JPA 가 나왔습니다.
JDBC > ( QueryMapper > ORM(JPA))
복습!

JDBC 는 여러 타입의 DB 와 연결 할 수 있는 기능을 제공합니다.
JDBC Driver Manager 는 런타임 시점에
Connection (연결) 을 생성하여 퀘리를 요청할수 있습니다 // Connection con
Statement(상태) 를 생성하여 퀘리를 요청 하게 주고
ResultSet (결과 셋) 을 생성해 쿼리 결과를 받올 수 있게 해줍니다.
🚫 꼭 사용후에는 각각 close() 를 호출해서 자원 해제를 시켜줘야 합니다! 🚫

Docker 설치
docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=teasun -e POSTGRES_DB=messenger --name postgres_boot -d postgres
해설
run : 작동
-p 5432:5432 : 포트 5432 연결
-e POSTGRES_PASSWORD=pass : POSTGRES 패스 워드는 pass
-e POSTGRES_USER=sehun : POSTGRES 유저는 sehun
- POSTGRES_DB = messenger : POSTGRES DB 를 messenger 로
--name postgres boot : 컨테이너 이름
-d postgress : -d 는 백그라운드로 띄우겠다, postgres 는 설치 되어 있는서버를 띄운다.

새로운 컨테이너가 생깁니다.

Docker 실행
docker exec -i -t postgres_boot bash

su - postgres
// psql --username sehun --dbname messenger
Docker 커맨드
\list (데이터 베이스 조회)

\dt (테이블 조회)
Grandle.build
implementation 'org.postgresql:postgresql:42.2.27';
test1
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "sehun";
String password = "pass";
try{
Connection connection = DriverManager.getConnection(url,username,password);
String creaseSql = "CREATE TABLE ACCOUNT(id SERIAL PRIMARY KEY, username varchar(255),password varchar(255))";
PreparedStatement statement = connection.prepareStatement(creaseSql);
statement.execute();
statement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}

JDBC 삽입 조회
try (Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection created: " + connection);
String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
statement.execute();
}
// then
String selectSql = "SELECT * FROM ACCOUNT";
try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
var rs = statement.executeQuery();
while (rs.next()) {
System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
rs.getString("password"));
}
}
}
try () 안에 Connection PreparedStatement 넣으면 자동으로 close 됨
DAO 랑 VO 를 사용
DAO ( Data Access Object )
를 만들어서 (그냥 sql 커맨드 적는거 귀찬으니 객체로 만들어서 재활용 하는 용도)
public class AccountDAO {
private Connection conn = null;
private PreparedStatement stmt = null;
private ResultSet rs = null;
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "sehun";
String password = "pass";
private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD) "
+ "VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)";
private final String ACCOUNT_GET = "SELECT * FROM account WHERE ID = ?";
public Integer insertAccount(AccountVO vo) throws SQLException {
var id = -1;
String[] returnId = {"id"};
try {
conn = DriverManager.getConnection(url,username,password);
assert conn != null;
stmt = conn.prepareStatement(ACCOUNT_INSERT, returnId);
stmt.setString(1, vo.getUsername());
stmt.setString(2, vo.getPassword());
stmt.executeUpdate();
try (ResultSet rs = stmt.getGeneratedKeys()) {
if (rs.next()) {
id = rs.getInt(1);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.close();
}
return id;
}
public AccountVO selectAccount(Integer id) throws SQLException {
AccountVO account = null;
try {
conn = DriverManager.getConnection(url,username,password);
assert conn != null;
stmt = conn.prepareStatement(ACCOUNT_GET);
stmt.setInt(1, id);
rs = stmt.executeQuery();
if (rs.next()) {
account = new AccountVO();
account.setId(rs.getInt("ID"));
account.setUsername(rs.getString("USERNAME"));
account.setPassword(rs.getString("PASSWORD"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.close();
}
return account;
}
}
AccountVO (정보, //sql 에 서 뽑아올)
public class AccountVO {
private Integer id;
private String username;
private String password;
public AccountVO() {
}
public AccountVO(String username, String password) {
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
@Test
@DisplayName("JDBC DAO 삽입/조회 실습")
void jdbcDAOInsertSelectTest() throws SQLException {
// given
AccountDAO accountDAO = new AccountDAO();
// when
var id = accountDAO.insertAccount(new AccountVO("new user", "new password"));
// then
var account = accountDAO.selectAccount(id);
assert account.getUsername().equals("new user");
}
2. JDBC 의 여러 문제로 QueryMapper 이 탄생했다.
- DBC 로 직접 SQL을 작성했을때의 문제
- SQL 쿼리 요청시 중복 코드 발생
- DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
- Connection, Statement 등.. 자원 관리를 따로 해줘야함
- 안해주면 메모리 꽉차서 서버가 죽음
문제 해결을 위해 처음으로 Persistence Framework 등장!
Persistence Framework 의 두가지!
- SQL Mapper : JDBC Template, MyBatis 👈 요게 먼저나옴
- ORM : JPA, Hibernate
- SQL Mapper (QueryMapper)
- SQL ↔ Object
- SQL 문과 객체(Object)의 필드를 매핑하여 데이터를 객채화
JDBC Template
- SQL Mapper 첫번째 주자로 JDBCTemplate 탄생
- 쿼리 수행 결과와 객채 필드 매핑
- RowMapper 로 응답필드 매핑코드 재사용
- Connection, Statement, ResultSet 반복적 처리 대신 해줌
- 😵💫 But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함

예제
grandle 에 lib 추가
// 3. PostgreSQL 의존성 추가
implementation 'org.postgresql:postgresql'
// 4. SpringBoot 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// JUNIT 테스트를 위한 기본 의존성
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
// 5. JDBC Template 등.. Spring 의존성을 받기위한 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
새파일 추가 resource 에
application.yml
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/messenger
username: sehun
password: pass
JDBC Template
@JdbcTest // Jdbc Slice Test
@AutoConfigureTestDatabase(replace = Replace.NONE) // 테스트용 DB 쓰지 않도록
@Rollback(value = false) // Transactional 에 있는 테스트 변경은 기본적으론 롤백 하도록 되어있다.
public class JDBCtemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@DisplayName("SQL Mapper - JDBC Template 실습")
void sqlMapper_JDBCTemplateTest() throws SQLException {
// given
var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);
// when
var id = accountTemplateDAO.insertAccount(new AccountVO("new user2", "new password2"));
// then
var account = accountTemplateDAO.selectAccount(id);
assert account.getUsername().equals("new user2");
}
}
DAO
public class AccountTemplateDAO {
private JdbcTemplate jdbcTemplate=null;
//query
private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD) "
+ "VALUES((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), ?, ?)";
private final String ACCOUNT_GET = "SELECT * FROM account WHERE ID = ?";
public Integer insertAccount(AccountVO vo) throws SQLException {
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(con -> {
PreparedStatement ps = con.prepareStatement(ACCOUNT_INSERT,new String[]{"id"});
ps.setString(1,vo.getUsername());
ps.setString(2,vo.getPassword());
return ps;
},keyHolder);
return (Integer) keyHolder.getKey();
}
AccountVO selectAccount(Integer id){
return jdbcTemplate.queryForObject(ACCOUNT_GET,new AccountRowMapper(),id);
}
public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
ROW MAPPER
public class AccountRowMapper implements RowMapper<AccountVO> {
@Override
public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {
var account = new AccountVO();
account.setId(rs.getInt("ID"));
account.setUsername(rs.getString("USERNAME"));
account.setPassword(rs.getString("PASSWORD"));
return account;
}
}
SQL Mapper 두번째 주자로 MyBatis 탄생
- 반복적인 JDBC 프로그래밍을 단순화
- SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!
- 😵💫 But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적) 😫
- 😵💫 But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.

// 코드
Grandle.build
// 6. MyBatis 실습을 위한 의존성
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.0'
//testCompileClasspath 'org.jpastudy.spring.boot:mybatis-spring-boot-starter-test:3.0.1'
runtimeOnly 'org.postgresql:postgresql'
resources 에 AccountMapper 추가
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--
[템플릿 설명]
- 해당 파일은 SQL 문을 작성하는 곳입니다.
-->
<mapper namespace="me.whitebear.jpastudy.mybatis.mapper.AccountMapper">
<select id="selectAccount" resultType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
SELECT id,
username,
password
FROM account
WHERE id = #{id}
</select>
<insert id="insertAccount" parameterType="me.whitebear.jpastudy.mybatis.vo.AccountMyBatisVO">
INSERT INTO account(username, password)
VALUES (#{username}, #{password});
</insert>
</mapper>
DB Configuration 추가
@Configuration
@MapperScan(basePackages = "me.sehun.*")
@EnableTransactionManagement
public class DBConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
final SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapping xml 파일 위치를 bean mapper location 에 등록
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath:mappings/*.xml"));
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
메인 테스트 클래스
@SpringBootTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)
public class MyBatisTest {
// Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
@Autowired
AccountMapper accountMapper;
@Autowired
AccountMapperV2 accountMapperV2;
@Test
@DisplayName("SQL Mapper - MyBatis 실습 XML 사용시")
void sqlMapper_MyBatisTest() {
// given
// when
accountMapper.insertAccount(new AccountMyBatisVO("new user3", "new password3"));
var account = accountMapper.selectAccount(1);
// then
assert !account.getUsername().isEmpty();
}
@Test
@DisplayName("SQL Mapper - MyBatis V2 실습 어노테이션 사용시")
void sqlMapper_MyBatisV2Test() {
// given
// when
accountMapperV2.insertAccount(new AccountMyBatisVO("new user4", "new password4"));
var account = accountMapperV2.selectAccount(1);
// then
assert !account.getUsername().isEmpty();
}
}
Mapper & Mapperv2 & VO
@Mapper
public interface AccountMapper {
AccountMyBatisVO selectAccount(@Param("id") int id);
void insertAccount(AccountMyBatisVO vo);
}
@Mapper
public interface AccountMapperV2 {
@Select("SELECT * FROM account WHERE id = #{id}")
AccountMyBatisVO selectAccount(Integer id);
@Insert("INSERT INTO ACCOUNT (username, password) VALUES (#{username}, #{password})")
void insertAccount(AccountMyBatisVO vo);
}
public class AccountMyBatisVO {
private Integer id;
private String username;
private String password;
public AccountMyBatisVO() {
}
public AccountMyBatisVO(String username, String password) {
this.username = username;
this.password = password;
}
public Integer getId() {
return id;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
}
QueryMapper 의 DB의존성 및 중복 쿼리 문제로 ORM 이 탄생했다.
ORM 은 DB의 주도권을 뺏어왔다고 표현해도 과언이 아닙니다.
- ORM 은 DAO 또는 Mapper 를 통해서 조작하는것이 아니라 테이블을 아예 하나의 객체(Object)와 대응시켜 버립니다.
- 말이 쉽지…. 객체지향(Object) 을 관계형 데이터베이스(Relation) 에 매핑(Mapping) 한다는건 정말 많은 난관이 있습니다.

릴레이션(관계형 데이터베이스)를 객체(도메인 모델)로 매핑 하려는 이유?
- 객체 지향 프로그래밍의 장점을 활용할 수 있다.
- 이를 통해, 비즈니스 로직 구현 및 테스트 구현이 편리함
- 각종 디자인 패턴 사용하여 성능 개선 가능
- 코드 재사용

하지만, 객체를 릴레이션에 맵핑하려니 발생하는 문제들
ORM 이 해결해야하는 문제점 과 해결책
상속의 문제
- 객체 : 객체간에 멤버변수나 상속관계를 맺을 수 있다.
- RDB : 테이블들은 상속관계가 없고 모두 독립적으로 존재한다.
해결방법 : 매핑정보에 상속정보를 넣어준다. (@OneToMany, @ManyToOne)
관계 문제
- 객체 : 참조를 통해 관계를 가지며 방향을 가진다. (다대다 관계도 있음)
- RDB : 외래키(FK)를 설정하여 Join 으로 조회시에만 참조가 가능하다. (즉, 다대다는 매핑 테이블 필요)
해결방법 : 매핑정보에 방향정보를 넣어준다. (@JoinColumn, @MappedBy)
탐색 문제
- 객체 : 참조를 통해 다른 객체로 순차적 탐색이 가능하며 콜렉션도 순회한다.
- RDB : 탐색시 참조하는 만큼 추가 쿼리나, Join 이 발생하여 비효율적이다.
해결방법 : 매핑/조회 정보로 참조탐색 시점을 관리한다.(@FetchType, fetchJoin())
밀도 문제
- 객체 : 멤버 객체크기가 매우 클 수 있다.
- RDB : 기본 데이터 타입만 존재한다.
해결방법 : 크기가 큰 멤버 객체는 테이블을 분리하여 상속으로 처리한다. (@embedded)
식별성 문제
- 객체 : 객체의 hashCode 또는 정의한 equals() 메소드를 통해 식별
- RDB : PK 로만 식별
해결방법 : PK 를 객체 Id로 설정하고 EntityManager는 해당 값으로 객체를 식별하여 관리 한다.(@Id,@GeneratedValue )
대신, ORM 이 얻은 최적화 방법
1차 2 차 캐시

캐싱 기능은 객체지향 프로그래밍이 가진 가장 큰 장점이다.
- 1차 캐시
- 영속성 컨텍스트 내부에는 엔티티를 보관하는 저장소가 있는데 이를 1차 캐시라고 한다.
- 일반적으로 트랜잭션을 시작하고 종료할 때까지만 1차 캐시가 유효하다.
- 1차 캐시는 한 트랜잭션 계속해서 원본 객체를 넘겨준다.
- 자세한 내용은 밑에서…👇
- 2차 캐시
- 애플리케이션 범위의 캐시로, 공유 캐시라고도 하며, 애플리케이션을 종료할 때 까지 캐시가 유지된다.
- 2차 캐시는 캐시 한 객체 원본을 넘겨주지 않고 복사본을 만들어서 넘겨준다.
- 복사본을 주는 이유는 여러 트랜잭션에서 동일한 원본객체를 수정하는일이 없도록 하기 위해서이다.
Entity에 @Cacheable 적용 후 설정 추가
@Entity
@Cacheable
public class Team {
@Id @GeneratedValue
private Long id;
...
}
# application.yml
spring.jpa.properties.hibernate.cache.use_second_level_cache: true
# 2차 캐시 활성화합니다.
spring.jpa.properties.hibernate.cache.region.factory_class: XXX
# 2차 캐시를 처리할 클래스를 지정합니다.
spring.jpa.properties.hibernate.generate_statistics: true
# 하이버네이트가 여러 통계정보를 출력하게 해주는데 캐시 적용 여부를 확인할 수 있습니다.
spring.jpa.properties.javax.persistence.sharedCache.mode: ENABLE_SELECTIVE
영속성 컨텍스트(1차 캐시)를 활용한 쓰기지연
- 영속성 이란?영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
- 그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
- 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.

- 영속성 4가지 상태 ( 비영속 > 영속 > 준영속 | 삭제)② 영속(managed) - 엔티티가 영속성 컨텍스트에 저장되어, 영속성 컨텍스트가 관리할 수 있는 상태④ 삭제(removed) - 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제하겠다고 표시한 상태
- 객체의 영속성 상태는 Entity Manager 의 메소드를 통해 전환된다.
- 💁♂️ Raw JPA 관점에서 순서대로 요약정리 해보자면
- persist(),merge() > (영속성 컨텍스트에 저장된 상태) > flush() > (DB에 쿼리가 전송된 상태) > commit() > (DB에 쿼리가 반영된 상태)
- ③ 준영속(detached) - 엔티티가 영속성 컨텍스트에 저장되어 있다가 분리된 상태로, 영속성컨텍스트가 더 이상 관리하지 않는 상태
- ① 비영속(new/transient) - 엔티티 객체가 만들어져서 아직 저장되지 않은 상태로, 영속성컨텍스트와 전혀 관계가 없는 상태

예제 코
Item item = new Item(); // 1
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager(); // 2
EntityTransaction transaction = em.getTransaction(); // 3
transaction.begin();
em.persist(item); // 4-1
em.flush(item). // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit(); // 5
em.close(); // 6
1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환
- 쓰기 지연이 발생하는 시점
- flush() 동작이 발생하기 전까지 최적화한다.
- flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
- 쓰기 지연 효과
- 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
- 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
- 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
- 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
키 생성전략이 generationType.IDENTITY 로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.
- why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.
Team teamA = new Team();
teamA.setName("TeamA");
em.persist(teamA);
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
Member member_A = new Member();
member_A.setName("memberA");
member_A.setTeam(teamA);
em.persist(member_A);
// em.flush();
Member findMember = em.find(Member.class, member_A.getId());
Team findTeam= findMember.getTeam();
System.out.println(findTeam.getName());
//flush가 있는 경우
create member
create team
insert team // flush로 인해 쓰기지연이 발생하지 않음
insert member // flush로 인해 쓰기지연이 발생하지 않음
print "TeamA" (memberA.getTeam())
///flush가 없는 경우
create member
create team
print "TeamA" (memberA.getTeam()) // 쓰기 지연이 발생하더라도 영속성 컨텍스트에서 조회해옴
insert team // 쓰기 지연이 발생한 부분
insert member // 쓰기 지연이 발생한 부분