🍃프로그래머스 백엔드 데브코스 4기 교육과정을 듣고 정리한 글입니다.🍃
RDB 접근 방법 4가지
JDBC
그림 출처
- 자바 애플리케이션은 JDBC API를 이용해서 DB와 통신한다.
@Slf4j
public class JDBCTest {
static final String JDBC_DRIVER = "org.h2.Driver";
static final String DB_URL = "jdbc:h2:~/test";
static final String USER = "sa";
static final String PASS = "";
static final String DROP_TABLE_SQL = "DROP TABLE users IF EXISTS";
static final String CREATE_TABLE_SQL = "CREATE TABLE users(id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))";
static final String INSERT_SQL = "INSERT INTO users (id, first_name, last_name) VALUES(1, 'myeonghan', 'yu')";
@Test
void jdbc_sample() {
try {
Class.forName(JDBC_DRIVER);
Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
Statement statement = connection.createStatement();
statement.executeUpdate(DROP_TABLE_SQL);
statement.executeUpdate(CREATE_TABLE_SQL);
statement.executeUpdate(INSERT_SQL);
ResultSet resultSet = statement.executeQuery("SELECT id, first_name, last_name FROM users WHERE id = 1");
while(resultSet.next()) {
log.info(resultSet.getString("first_name"));
}
statement.close();
connection.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 처리해야 할 Exception이 많다.
- 쿼리 실행 전, DriverManager를 통해 Connection을 가져온다.
- Statement를 이용해서 쿼리를 실행한다.
- ResultSet으로 조회 결과를 받아 사용한다.
- 다 사용한 Statement, Connection을 반납한다.
- 위와 같이 공통적인 작업이 반복된다.
JDBC Template
@Slf4j
@SpringBootTest
public class JDBCTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
private static class User {
private Long id;
private String firstName;
private String lastName;
}
@Test
void jdbc_template_sample() {
jdbcTemplate.execute("DROP TABLE IF EXISTS users");
jdbcTemplate.execute("CREATE TABLE users(id SERIAL, first_name VARCHAR(255), last_name VARCHAR(255))");
jdbcTemplate.execute("INSERT INTO users (id, first_name, last_name) VALUES(1, 'myeonghan', 'yu')");
User user = jdbcTemplate.queryForObject(
"SELECT id, first_name, last_name FROM users WHERE id = 1",
(resultSet, rowNum) -> {
User u = new User();
u.setId(resultSet.getLong("id"));
u.setFirstName(resultSet.getString("first_name"));
u.setLastName(resultSet.getString("last_name"));
return u;
}
);
log.info("User: {}", user);
}
}
- 기존 JDBC를 이용할때의 반복적인 작업을 생략 가능하다.
- 하지만 자바 코드상에 String 타입으로 쿼리문을 직접 다루기 때문에 유지보수가 힘들다.
Mybatis (QueryMapper)
mybatis:
type-aliases-package: com.example.model
mapper-locations: classpath:mapper/*Mapper.xml
@Mapper
public interface UserXmlMapper {
void save(User user);
void update(User user);
User findById(long id);
List<User> findAll();
}
<?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">
<mapper namespace="com.example.repository.UserXmlMapper">
<insert id="save">
INSERT INTO users (id, first_name, last_name)
VALUES (#{id}, #{firstName}, #{lastName})
</insert>
<update id="update">
UPDATE users
SET first_name=#{firstName},
last_name=#{lastName}
WHERE id = #{id}
</update>
<select id="findById" resultType="User">
SELECT *
FROM users
WHERE id = #{id}
</select>
<select id="findAll" resultType="User">
SELECT *
FROM users
</select>
</mapper>
- xml 설정을 통해 쿼리를 작성한다.
- 쿼리를 User POJO의 필드 값을 가져와서 생성한다.
@Mapper
public interface UserAnnotationMapper {
@Insert("INSERT INTO users (id, first_name, last_name) VALUES(#{id}, #{firstName}, #{lastName})")
void save(User user);
@Update("UPDATE users SET first_name=#{firstName}, last_name=#{lastName} WHERE id=#{id}")
void update(User user);
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") long id);
@Select("SELECT * FROM users")
List<User> findAll();
}
- 참고로 xml이 아닌, 어노테이션을 활용해서 사용할 수 있다.
@Slf4j
@SpringBootTest
public class MyBatisTest {
@Autowired
private UserXmlMapper userXmlMapper;
@Test
void mybatis_test() {
User user = new User();
user.setId(1L);
user.setFirstName("myeonghan");
user.setLastName("yu");
userXmlMapper.save(user);
user.setFirstName("update");
userXmlMapper.update(user);
List<User> users = userXmlMapper.findAll();
for (User u : users) {
log.info("User: {}", u);
}
User foundUser = userXmlMapper.findById(1L);
log.info("Found User: {}", foundUser);
}
}
- JDBC Template와 달리, 쿼리문을 자바 코드와 독립적으로 관리하기 때문에 유지보수가 용이하다.
앞서 다룬 3가지 방법(JDBC, JDBC Template, Mybatis)들은 한계가 있다.
- 자바 객체와 RDB 테이블이 각각 갖고 있는 패러다임에 대한 불일치가 발생
- 패러다임이란 자바 또는 RDB같은 어떠한 분야에 사용되는 사고방식이나 모델을 의미
- 자바 패러다임은 객체지향 프로그래밍을 기반으로 함
- 클래스와 객체를 중심으로 코드를 구성하며, 클래스는 데이터와 해당 데이터를 처리하는 메서드를 포함
- 상속, 캡슐화, 다형성 등의 객체지향 개념을 활용
- RDB 패러다임은 관계형 데이터베이스 관리 시스템을 기반으로 함
- 데이터는 행과 열로 구성된 테이블에 저장되며, 쿼리를 통해 작업을 수행함
- 객체지향 개념을 사용하지 않음
JPA (Object Relation Mapper, ORM)
@Getter
@Setter
@Entity
public class User {
@Id
private Long id;
private String firstName;
private String lastName;
}
- Entity를 설정한다.
- @Id로 Pk값 필드를 명시한다.
- Jpa는 기본적으로 Entity의 getter, setter, 기본 생성자로 동작한다.
public interface UserJpaRepository extends JpaRepository<User, Long> {
}
- 엔티티와 맵핑되는 JpaRepository 인터페이스를 사용한다.
- 제네릭 타입으로 엔티티 타입과, pk값 타입을 설정해준다.
@Slf4j
@SpringBootTest
@Transactional
public class JpaTest {
@Autowired
private UserJpaRepository userJpaRepository;
@Test
void jpa_test() {
User user = new User();
user.setId(1L);
user.setFirstName("myeonghan");
user.setLastName("yu");
userJpaRepository.save(user);
user.setFirstName("update");
userJpaRepository.save(user);
List<User> users = userJpaRepository.findAll();
for (User u : users) {
log.info("User: {}", u);
}
User foundUser = userJpaRepository.findById(1L).orElse(null);
log.info("Found User: {}", foundUser);
}
}
- RDB 테이블을 마치 자바 객체처럼 다룰수 있다.
- SQL에 의존적인 개발이 아닌, 객체 중심으로 생산적인 개발이 가능하다.
- 따라서 객체와 RDB 테이블의 패러다임 불일치 문제가 해결된다.
JPA 관련 Bean 설정
DataSource
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:~/test");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
JpaVendorAdapter
@Bean
public JpaVendorAdapter jpaVendorAdapter(JpaProperties jpaProperties) {
AbstractJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setShowSql(jpaProperties.isShowSql());
adapter.setDatabasePlatform(jpaProperties.getDatabasePlatform());
adapter.setGenerateDdl(jpaProperties.isGenerateDdl());
return adapter;
}
- 인터페이스인 JPA의 구현체를 등록해준다.
- 구현체는 대부분 하이버네이트를 사용한다.
- JPA의 properties를 설정해준다.
EntityManagerFactory
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapter,
JpaProperties jpaProperties) {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.kdt.lecture.domain");
em.setJpaVendorAdapter(jpaVendorAdapter);
Properties properties = new Properties();
properties.putAll(jpaProperties.getProperties());
em.setJpaProperties(properties);
return em;
}
- RDB 테이블과 맵핑될 엔티티를 관리하는 객체가 EntityManager이다.
- 이러한 EntityManager를 만드는 객체가 EntityManagerFactory이고, 이를 빈으로 등록한다.
- 이때 DataSource, JpaVendorAdapter, JpaProperties를 사용한다.
TransactionManager
@Bean
public PlatformTransactionManager transactionManager(LocalContainerEntityManagerFactoryBean entityManagerFactory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(entityManagerFactory.getObject());
return transactionManager;
}
- 만들어진 EntityManagerFactory 빈을 주입받아 TransactionManager 빈을 등록한다.
- TransactionManager 빈은 @Transactional와 함께 사용되어 트랜잭션의 시작과 종료, 롤백, 커밋을 자동으로 처리한다.
- 즉, RDB 트랜잭션을 관리해준다.
- 참고로 트랜잭션 안에서, EntityManager에 의해 Entity의 라이프 사이클이 관리되며, 변경 감지등의 작업을 수행한다.
Spring Boot의 auto-configuration은 JPA를 사용하기 위한 위 4가지 빈들을 yml 파일을 읽어 내부적으로 자동 생성해준다.