Mybatis
- SQL Mapper로 SQL 중심의 프로젝트에 적합
→ 객체 중심의 설계라면 JPA(Hibernate)가 더 적합 - 객체와 SQL 쿼리 사이의 매핑을 처리하는 도구
- ORM처럼 객체를 자동으로 매핑하지 않고, 개발자가 직접 SQL 쿼리를 작성하고 SQL 결과를 자바 객체와 수동으로 매핑하는 방식을 사용
→ 수동이긴 하지만 객체와 데이터베이스 테이블 간의 매핑을 다룬다는 점에서 마치 ORM 처럼 사용되긴 하지만 실질적으로는 SQL Mapper - SQL 쿼리의 세밀한 제어가 필요할 때 유용하고, SQL의 최적화와 복잡한 쿼리 작성에서 강점을 보임
Mybatis 등장 배경
- 복잡한 SQL 쿼리 관리의 어려움
- 기존의 JDBC를 사용했을 때는 개발자가 직접 SQL 쿼리를 작성하고, 쿼리 실행 결과를 Java 객체로 수작업으로 매핑
- 프로젝트가 커질수록 SQL이 복잡해지고, 이로 인해 코드 유지보수가 어려워짐
- SQL과 Java 코드의 강한 결합
- JDBC를 사용할 때 SQL 쿼리는 Java 코드에 직접 포함되기 때문에 SQL과 비즈니스 로직이 강하게 결합되는 것이 문제
- 이로 인해 코드 가독성이 낮아지고, 쿼리를 수정하거나 변경하기 어려웠음
- ORM 프레임워크에 대한 과잉 추상화 문제
- MyBatis가 등장하기 전에는 Hibernate와 같은 ORM(Object Relational Mapping) 프레임워크가 주로 사용되었으나, ORM은 SQL을 추상화하여 객체 중심으로 데이터베이스를 조작하도록 설계되었기 때문에 복잡한 SQL을 요구하는 프로젝트에서는 비효율적
- 특히, 대규모 프로젝트에서는 직접 작성한 SQL이 더 효율적이고 최적화된 경우가 많았음
- SQL 중심 개발에 대한 요구
- 데이터베이스 중심의 애플리케이션에서는 복잡한 쿼리와 데이터 조작이 필수적이기 때문에 SQL을 중심으로 개발하면서도 반복적인 JDBC 작업을 줄이는 방식을 원했음
- SQL의 자유도를 유지하면서 SQL과 Java 객체를 매핑해주는 프레임워크의 필요성이 대두되었음
MyBatis의 등장을 통한 해결
- SQL의 유연성 유지 : 개발자가 작성한 SQL을 그대로 사용할 수 있도록 지원
- 반복 작업 제거 : JDBC의 반복적인 코드를 줄이고, SQL 쿼리 결과를 Java 객체로 자동 매핑
- SQL 관리 용이 : XML 파일 또는 주석 기반으로 SQL을 별도로 관리할 수 있어 가독성과 유지보수성이 향상
- 객체-관계 매핑 간소화 : 복잡한 매핑 작업을 간단한 설정으로 처리
Mybatis 동작 과정
- mybatis-config.xml
- 클래스 Alias 설정, DB 연결 설정, SQL 경로 설정 등 MyBatis의 기본 설정을 담고 있음
- SqlSessionFactoryBuilder
- mybatis-config.xml 파일을 읽어들여 build(InputStream) 메서드를 호출하여 SqlSessionFactory 객체를 생성
- SqlSessionFactory
- openSession() 메서드를 통해 데이터베이스와 연결된 SqlSession 객체를 생성
- Thread-safe하며 애플리케이션 내에서 단 한 번 생성 후 재사용
- SqlSession
- mapper.xml에 정의된 SQL을 실행하고, 결과를 반환
- 제공되는 주요 메서드는 selectOne, selectList, insert, update, delete 등
- 자동 커밋 여부 설정 가능
- Thread-safe하지 않으므로 요청마다 새로 생성, 작업 종료 후 반드시 close() 호출
- mapper.xml
- SQL 쿼리와 Java 인터페이스를 매핑한 설정 파일
- namespace를 통해 구분되며 각 메서드는 고유한 ID로 식별
Mapper.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"> <!-- dtd는 스프링부트에서 사용하는 mybatis 버전 입력 -->
<mapper namespace="com.example.mybatistest.mapper.MemberMapper">
<!-- db column(스네이크케이스)과 vo(카멜케이스)의 필드명이 상이하므로 resultmap에서 매핑해줌 -->
<resultMap id="memberMap" type="com.example.mybatistest.domain.vo.MemberVO">
<id column="member_id" property="memberId"/>
<result column="email" property="email"/>
<result column="member_name" property="memberName"/>
<result column="password" property="password"/>
</resultMap>
<!-- ※ mapper interface의 method명과 id가 일치해야 함 -->
<!-- 회원 등록 -->
<insert id="insertMember" parameterType="com.example.mybatistest.domain.vo.MemberVO" useGeneratedKeys="true" keyProperty="memberId">
INSERT INTO MEMBER (email, member_name, password)
VALUES (#{email}, #{memberName}, #{password})
</insert>
<!-- 회원 단건 조회 -->
<select id="findOneMember" parameterType="Long" resultMap="memberMap">
SELECT * FROM member WHERE member_id = #{memberId}
</select>
<!-- 회원 목록 조회 -->
<select id="findAllMember" resultMap="memberMap">
SELECT * FROM member
</select>
<!-- 회원 수정 -->
<update id="updateMember" parameterType="com.example.mybatistest.domain.vo.MemberVO">
UPDATE member set
email = #{email},
member_name = #{memberName},
password = #{password}
WHERE member_id = #{memberId}
</update>
<!-- 회원 삭제 -->
<delete id="deleteMember" parameterType="Long">
DELETE FROM member
WHERE member_id = #{memberId}
</delete>
</mapper>
MemberVO(Dao)
package com.example.mybatistest.domain.vo;
import com.example.mybatistest.domain.dto.MemberDto;
import lombok.*;
@Getter @Setter
@ToString
// 객체를 외부에서 함부로 생성하지 못하게 제한(생성 메서드를 이용하도록)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberVO {
private Long memberId;
private String email;
private String memberName;
private String password;
/**
* dto를 vo로 변환
* @param memberDto
* @return
*/
public static MemberVO toVO(MemberDto memberDto) {
MemberVO memberVO = new MemberVO();
memberVO.setEmail(memberDto.getEmail());
memberVO.setMemberName(memberDto.getMemberName());
memberVO.setPassword(memberDto.getPassword());
return memberVO;
}
/**
* 객체 생성 메서드
* @param email
* @param memberName
* @param password
* @return
*/
public static MemberVO createMember(String email, String memberName, String password) {
MemberVO memberVO = new MemberVO();
memberVO.setEmail(email);
memberVO.setMemberName(memberName);
memberVO.setPassword(password);
return memberVO;
}
/**
* 수정 메서드
* @param email
* @param memberName
* @param password
*/
public void updatemember(String email, String memberName, String password) {
this.email = email;
this.memberName = memberName;
this.password = password;
}
}
MemberDto
package com.example.mybatistest.domain.dto;
import com.example.mybatistest.domain.vo.MemberVO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter
@ToString
public class MemberDto {
private Long memberId;
private String email;
private String memberName;
private String password;
/**
* vo를 dto로 변환
* @param memberVO
* @return
*/
public static MemberDto toDto(MemberVO memberVO) {
MemberDto memberDto = new MemberDto();
memberDto.setMemberId(memberVO.getMemberId());
memberDto.setEmail(memberVO.getEmail());
memberDto.setMemberName(memberVO.getMemberName());
memberDto.setPassword(memberVO.getPassword());
return memberDto;
}
}
MemberMapper Interface
package com.example.mybatistest.mapper;
import com.example.mybatistest.domain.vo.MemberVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper // 제일 중요한 어노테이션(이걸 설정해야 스프링부트에서 이 클래스를 찾아서 빈으로 등록한다.)
public interface MemberMapper {
// ※ 메서드명과 mapper.xml의 id가 일치해야 한다.
// 등록
void insertMember(MemberVO memberVO);
// 단건 조회
MemberVO findOneMember(Long memberId);
// 목록 조회
List<MemberVO> findAllMember();
// 수정
void updateMember(MemberVO memberVO);
// 삭제
void deleteMember(Long memberId);
}
MemberService.java
package com.example.mybatistest.service;
import com.example.mybatistest.domain.dto.MemberDto;
import com.example.mybatistest.domain.vo.MemberVO;
import com.example.mybatistest.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import static com.example.mybatistest.domain.dto.MemberDto.toDto;
import static java.util.stream.Collectors.toList;
@Service
@Slf4j
@RequiredArgsConstructor
public class MemberService {
private final MemberMapper memberMapper;
/**
* 등록
* @param memberVO
*/
public void save(MemberVO memberVO) {
memberMapper.insertMember(memberVO);
}
/**
* 단건 조회
* @param memberId
* @return
*/
public MemberVO findOne(Long memberId) {
return memberMapper.findOneMember(memberId);
}
/**
* 목록 조회
* @return
*/
public List<MemberDto> findAll() {
List<MemberVO> members = memberMapper.findAllMember();
// 일반 foreach 사용
// List<MemberDto> result = new ArrayList<>();
// for (MemberVO m : members) {
// MemberDto memberDto = toDto(m);
// result.add(memberDto);
// }
// java 1.8 stream api 사용
List<MemberDto> result = members.stream()
.map(m -> toDto(m)).collect(toList());
return result;
}
/**
* 수정
*/
public void updateMember(MemberVO memberVO) {
memberMapper.updateMember(memberVO);
}
/**
* 삭제
* @param memberId
*/
public void remove(Long memberId) {
memberMapper.deleteMember(memberId);
}
}
'Language > Spring' 카테고리의 다른 글
[SQL Mapper] Mybatis의 ResultMap과 ResultType (0) | 2024.12.17 |
---|---|
[STS] JSP 사용법 (0) | 2024.12.10 |
[JPA] N+1 문제 (1) | 2024.11.18 |
[JPA] 엔티티 관계의 로딩 전략 - 즉시 로딩 / 지연 로딩 (1) | 2024.11.18 |
[Spring] Controller와 Rest Controller 차이 (0) | 2024.11.17 |