Object Relational Mapping Framework
객체 관계 맵핑 프레임 워크
객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블 간의 불일치를 해결해주는 기술
User 클래스 -> id, name 필드users 테이블 -> id, name 컬럼이 둘을 매핑해주는 것이 ORM의 역할이다.
| 언어 | ORM 도구 |
|---|---|
| Java | JPA(Hibernate), MyBatis(부분 ORM) |
| Python | SQLAlchemy, Django ORM |
| C# | Entity Framework |
SQL을 XML 또는 어노테이션으로 분리해 관리하면서, 자바 객체와 DB의 데이터를 매핑해주는 반자동 ORM 프레임워크
Hibernate 같은 완전 자동 ORM과 달리, 직접 SQL을 작성해야 한다.
-> 쿼리 튜닝이나 복잡한 조인 처리에 유리
Mapper XML
<select>, <insert>, <update>, <delete> 태그 사용Mapper Interface
SqlSession
// Mapper XML (UserMapper.xml)
<mapper namespace="com.example.UserMapper">
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
// Mapper Interface (UserMapper.java)
public interface UserMapper {
User getUserById(int id);
}
// 사용 예 (Service 등에서)
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getUserById(1);
Spring 프레임워크 자체에서 MyBatis와 연동하는 기능을 제공
SqlMapClientFactoryBean, SqlMapClientTemplate 같은 스프링 전용 클래스
Spring 자체에서 더 이상 MyBatis 연동 기능을 제공 X
스프링 내부 API로 MyBatis를 다루는 지원이 제거
MyBatis에서 만든 mybatis-spring 별도 연동 모듈 사용해야한다.
<select id="selectPerson" parameterType="int" resultType="hashmap">
SELECT * FROM PERSON WHERE ID = #{id}
</select>
| 항목 | 설명 |
|---|---|
id="selectPerson" | 이 쿼리의 이름(Mapper 메서드명과 연결됨) |
parameterType="int" | 넘겨받는 파라미터 타입 (int id 라는 의미) |
resultType="hashmap" | 결과를 Java의 HashMap으로 반환 (컬럼명이 키가 됨) |
#{id} | 파라미터 바인딩 부분 → SQL의 ?로 변환됨 |
MyBatis가 내부적으로 PreparedStatement의 ?로 치환하고 해당 위치에 전달받은 값을 설정한다
String selectPerson = "SELECT * FROM PERSON WHERE ID = ?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1, id); // id를 바인딩
| 표현식 | 설명 |
|---|---|
#{} | 바인딩: SQL의 ?로 변환되어 안전하게 파라미터 전달됨 |
${} | 문자열 치환: 쿼리 안에 직접 삽입됨 (주의: SQL 인젝션 위험 있음) |
| 속성명 | 간단 설명 | 실무 예시 |
|---|---|---|
| useCache="true" | 조회 결과를 2차 캐시에 저장해 재사용함 | 자주 조회되며 자주 바뀌지 않는 데이터 (예: 지역 목록, 카테고리) |
| flushCache="true" | 이 쿼리를 실행하면 1차 캐시(세션 캐시)를 비움 | 트랜잭션 이후 캐시된 결과가 오래되었을 수 있으므로 강제 초기화 |
| resultOrdered="true" | 계층형 데이터 정렬 보장 | 부모-자식 조인 쿼리에서 순서 보장 필요할 때 (예: 게시글-댓글) |
| timeout="3" | 쿼리 최대 대기 시간을 설정 (초) | 응답 속도가 중요한 API에서 느린 쿼리 차단 |
| fetchSize="100" | 한 번에 가져올 row 수를 드라이버에 힌트 | 대용량 결과를 페이지 단위로 효율적으로 가져오고 싶을 때 |
| statementType="CALLABLE" | 저장 프로시저 호출용 | CALL my_procedure(#{param}) |
| resultSetType="SCROLL_INSENSITIVE" | 커서 앞뒤 이동 허용 (변경 감지 X) | 결과셋을 반복적으로 참조하거나 이동하면서 처리할 때 |
| databaseId="mysql" | DB 종류에 따라 쿼리 분기 | DB별 SQL 문법이 다를 경우 (예: LIMIT vs ROWNUM) |
<select id="getUserById"
parameterType="int"
resultType="User"
flushCache="false"
useCache="true"
timeout="5"
fetchSize="100"
statementType="PREPARED"
resultSetType="FORWARD_ONLY">
SELECT * FROM users WHERE id = #{id}
</select>
flushCache="true" (기본값)
변경 쿼리가 실행되면 1차 캐시를 비운다.
-> 이 쿼리를 실행한 후에는 같은 세션 안에서 이전에 조회했던 데이터가 다시 사용되지 않도록 캐시를 강제로 비우는 것
useCache="false" (기본값)
변경 쿼리는 캐시하지 않는다.
-> insert나 update 결과를 저장해도 재사용할 일이 없어 캐싱할 필요가 없다.
select : 읽기 성능 향상을 위해 캐시를 씀
insert/update/delete : 데이터 일관성 보장을 위해 캐시를 비움, 캐싱하지 않음
보통 insert, update, delete는 특별한 설정 없이도 사용 가능하다
| 속성명 | 기본값 | 설명 | 실무 예시 |
|---|---|---|---|
| flushCache | true | 쿼리 실행 시 1차 캐시 자동 초기화 | 데이터 변경 후 항상 캐시를 새로 가져오도록 보장 |
| useCache | false | 데이터 변경 쿼리는 보통 캐시하지 않음 | 필요하면 useCache="true"로 설정 가능 (거의 안 씀) |
| timeout | 드라이버 기본값 | 쿼리 최대 대기 시간 (초) | 트랜잭션 중 오래 걸리는 update 쿼리 방지 |
| statementType | PREPARED | JDBC 쿼리 실행 방식 지정 | 저장 프로시저 호출 시 CALLABLE 사용 |
| databaseId | - | DB별 구문 분기 | MySQL, Oracle 각각의 update 구문 다를 경우 사용 |
MyBatis에서 SQL 조회결과(ResultSet)를 Java 객체에 정확하게 매핑하기 위한 설정이다.
단순한 경우에는 resultType으로 매핑이 가능하지만,
쿼리 결과의 컬럼 이름과 Java 객체의 필드 이름이 다르거나 복잡한 경우에, 매핑 방법을 수동으로 정의할 수 있다.
<resultMap> 태그를 이용해 XML에서 설정하거나, 어노테이션 방식으로 사용한다.
<!-- XML -->
<resultMap id="userResultMap" type="com.example.User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
<result property="email" column="email_address"/>
</resultMap>
<select id="getUserById" resultMap="userResultMap">
SELECT user_id, user_name, email_address
FROM users
WHERE user_id = #{id}
</select>
<!-- 중접 객체 매핑 -->
<resultMap id="orderResultMap" type="com.example.Order">
<id property="id" column="order_id"/>
<result property="date" column="order_date"/>
<association property="user" javaType="com.example.User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
</association>
</resultMap>
| 태그 | 설명 |
|---|---|
<id> | 객체의 주 키에 해당하는 필드 매핑 |
<result> | 일반 필드 매핑 |
<association> | 다른 객체(1:1 관계)와의 매핑 |
<collection> | 리스트, 컬렉션(1\:N 관계)과의 매핑 |
column | SQL 결과의 컬럼명 |
property | Java 객체의 필드명 |
중첩 객체를 정확히 매핑하지 않으면 N + 1 문제나 잘못된 데이터 매핑이 발생할 수 있다.
// 의존성 추가
// Maven (pom.xml)
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
// application.yml
// application.properties
spring:
datasource:
url: jdbc:mysql://localhost:3306/testdb
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/ *.xml
type-aliases-package: com.example.demo.model
// 도메인 클래스 (User.java)
public class User {
private int id;
private String name;
// Getter, Setter
}
// Mapper 인터페이스 (UserMapper.java)
@Mapper
public interface UserMapper {
User getUserById(int id);
}
// Mapper XML (resources/mapper/UserMapper.xml)
<?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.demo.mapper.UserMapper">
<select id="getUserById" parameterType="int" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
</mapper>
// 서비스 클래스 (UserService.java)
@Service
public class UserService {
private final User Mapper userMapper;
public UserService(Usermapper userMapper){
this.userMapper = userMapper;
}
public User getUser(int id){
return userMapper.getUserById(id);
}
}
// 컨트롤러 (UserController.java)
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService){
this.userService = userService;
}
@GetMapping("/{id}")
public User getUser(@PathVariable int id){
return userService.getUser(id);
}
}
오늘은 ORM과 MyBatis에 대해 공부했다.
ORM은 SQL을 숨기고 객체 중심으로 처리하는 반면, MyBatis는 SQL을 직접 작성하면서도 매핑은 자동으로 해주는 반(半)자동 ORM이다. select는 캐시를 사용하고, insert, update, delete는 캐시를 비우는 기본 동작 차이도 확인했다. 아직은 공부만 해본 단계지만, 기회가 된다면 select 쿼리에서 캐시 기능을 실제로 활용해보고 싶다.