https://bobaejin.tistory.com/304
🌻ORM, MyBatis : SQL과 자바의 역할 분리
응집도를 높히고, 결합도를 낮추기 위해 사용 MyBatisDAO를 Service가 사용하고 있는데 Service를 보면 이전에 만든 PlusMemberDAO가 의존주입되어 있는데 @Autowired private MybatisMemberDAO memberDAO;이렇게 새로 업
bobaejin.tistory.com
기존에 이미 Repository를 전부 작성해두었다
그 중 MyBatis 적용할 테이블을 정하고 connectLog, event Repository
이렇게 두 가지 코드를 한 번 수정해보겠다
기존의 ConnectLogRepository 전체 코드
package bugsandwich.ornably.connectLog;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class ConnectLogRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
// 새 접속 기록 추가
private static final String INSERT_CONNECT_LOG =
"INSERT INTO CONNECT_LOG (ACCOUNT_PK, CONNECT_IP, CONNECT_DEVICE) " +
"VALUES (?, ?, ?)";
// 특정 사용자 로그 전체 조회
private static final String SELECT_ALL_ACCOUNT_CONNECT_LOG =
"SELECT * " +
"FROM CONNECT_LOG " +
"WHERE ACCOUNT_PK = ? " +
"ORDER BY CONNECT_DATE DESC";
// 특정 사용자 로그 최신 접속 1건 조회
private static final String SELECT_LATEST_ACCOUNT_CONNECT_LOG =
"SELECT * FROM CONNECT_LOG " +
"WHERE ACCOUNT_PK = ? " +
"ORDER BY CONNECT_DATE DESC " +
"LIMIT 1";
// 사용자 로그 전체 삭제
private static final String DELETE_ACCOUNT_CONNECT_LOG =
"DELETE FROM CONNECT_LOG " +
"WHERE ACCOUNT_PK = ?";
public List<ConnectLogDTO> selectAll(ConnectLogDTO connectLogDTO){
System.out.println("[로그] ConnectLogRepository의 selectAll 시작");
// 특정 사용자 로그 전체 조회
if("SELECT_ACCOUNT_CONNECT_LOG".equals(connectLogDTO.getCondition())) {
System.out.println("[로그] selectAll의 SELECT_ACCOUNT_CONNECT_LOG");
return jdbcTemplate.query(
SELECT_ALL_ACCOUNT_CONNECT_LOG,
new BeanPropertyRowMapper<>(ConnectLogDTO.class),
connectLogDTO.getAccountPk()
);
}
System.out.println("[로그][경고] ConnectLogRepository_selectAll_condition 없음");
// 조건이 없으면 빈 리스트 반환
return java.util.Collections.emptyList();
}
public ConnectLogDTO selectOne(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository의 selectOne 시작");
// 특정 사용자 로그 최신 접속 1건 조회
if("SELECT_LATEST_CONNECT_LOG".equals(connectLogDTO.getCondition())) {
System.out.println("[로그] selectOne의 SELECT_LATEST_CONNECT_LOG");
return jdbcTemplate.queryForObject(
SELECT_LATEST_ACCOUNT_CONNECT_LOG,
new BeanPropertyRowMapper<>(ConnectLogDTO.class),
connectLogDTO.getAccountPk()
);
}
System.out.println("[로그][경고] ConnectLogRepository_selectOne_condition 없음");
return null;
}
public boolean insert(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository의 insert 시작");
int result = 0;
// 새 접속 기록 추가
if("INSERT_CONNECT_LOG".equals(connectLogDTO.getCondition())) {
System.out.println("[로그] insert의 INSERT_CONNECT_LOG");
result = jdbcTemplate.update(
INSERT_CONNECT_LOG,
connectLogDTO.getAccountPk(),
connectLogDTO.getConnectIP(),
connectLogDTO.getConnectDevice()
);
}
else {
System.out.println("[로그][경고] ConnectLogRepository_insert_condition 없음");
}
return result > 0;
}
private boolean update(ConnectLogDTO connectLogDTO) {
return false;
}
public boolean delete(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository의 delete 시작");
int result = 0;
// 사용자 로그 전체 삭제
if("DELETE_ACCOUNT_CONNECT_LOG".equals(connectLogDTO.getCondition())) {
System.out.println("[로그] delete의 DELETE_ACCOUNT_CONNECT_LOG");
result = jdbcTemplate.update(
DELETE_ACCOUNT_CONNECT_LOG,
connectLogDTO.getAccountPk()
);
}
System.out.println("[로그][경고] ConnectLogRepository_delete_condition 없음");
return result > 0;
}
}
Pom.xml에 설정하기
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
MyBatis를 스프링 부트랑 자동으로 연결해주는 핵심 설정
MyBatis를 Spring Boot에 붙여서, 설정·연결·주입을 거의 자동으로 해주는 스타터
application.properties 설정

mybatis.mapper-locations=classpath:mappers/**/*.xml
MyBatis가 SQL이 들어있는 XML 파일들을 어디서 찾을 수 있는지 알려주는 설정
🔹 자세히 뜯어보면 classpath : src/main/resources 기준이라는 뜻
즉, src/main/resources/mappers/ConnectLogMapper.xml
🔹 mappers/**/*.xml
mappers 폴더 아래
모든 하위 폴더
모든 .xml 파일
이 설정이 없으면 XML을 읽지 못한다
mybatis.type-aliases-package=bugsandwich.ornably
🔹 DTO 패키지를 등록해서 XML에서 짧은 이름으로 쓰게 해주는 설정
이 설정이 없으면 XML에서 resultType="bugsandwich.ornably.connectLog.ConnectLogDTO" 이렇게 사용해야 한다
이 설정이 있으면 resultType="connectLogDTO" 그리고 parameterType="connectLogDTO" 이렇게 가능
🔹 이렇게 설정을 하면 bugsandwich.ornably 하위 모든 DTO가
클래스명 ➡ 소문자 별칭으로 자동 등록된다
즉, ConnectLogDTO 가 자동으로 parameterType="connectLogDTO" resultType="connectLogDTO"
예를 들어 mybatis.type-aliases-package=bugsandwich.ornably.event 이렇게 설정을 하면

bugsandwich.ornably.event 패키지 안 클래스만 등록된다
bugsandwich.ornably.event
├─ EventDTO.java ✅ 등록됨 (자동 별칭: eventDTO)
├─ EventRepository.java ✅ 등록됨 (자동 별칭: eventRepository)
├─ EventService.java ✅ 등록됨 (자동 별칭: eventService)
등록된 별칭은 어디에 사용되나?
별칭은 주로 MyBatis Mapper XML이나 어노테이션 기반 쿼리에서 사용된다
<resultMap id="EventResult" type="eventDTO">
<id property="eventPk" column="EVENT_PK"/>
<result property="eventName" column="EVENT_NAME"/>
</resultMap>
<select id="selectEvent" resultType="eventDTO">
SELECT * FROM EVENT WHERE EVENT_PK = #{eventPk}
</select>
이렇게 Mapper XML을 작성한다고 하면
type="eventDTO" → EventDTO.class를 참조
별칭 덕분에 fully-qualified class name(bugsandwich.ornably.event.EventDTO)을 쓰지 않아도 된다
🔹 핵심 요약
- type-aliases-package → 패키지 내 모든 클래스 등록, 자동 별칭 생성
- 별칭 사용 → Mapper XML이나 어노테이션에서 type, resultType, parameterType 등
- 클래스 등록 기준 → .class 파일이면 등록됨, public이어야 함
- DTO가 아닌 Repository/Service도 등록은 되지만 실제 사용되는 건 DTO 중심
Service 인터페이스 만들기

두 파일만 존재했었는데
Mybatis를 사용하기 위해 Interface를 추가했습니다
package bugsandwich.ornably.connectLog;
import java.util.List;
public interface ConnectLogService {
boolean insertMember(ConnectLogDTO dto);
boolean updateMember(ConnectLogDTO dto);
boolean deleteMember(ConnectLogDTO dto);
ConnectLogDTO getMember(ConnectLogDTO dto);
List<ConnectLogDTO> getMemberList(ConnectLogDTO dto);
}
🔼 전체 코드
역할(계약)과 구현을 분리해서
결합도를 낮추고, 교체·확장·테스트를 쉽게 하기 위해서이다
Mapper XML 작성

resources에 mappers 폴더를 하나 생성한 뒤 ConnectLogMapper.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="ConnectLog">
<!-- 새 접속 기록 추가 -->
<insert id="insert" parameterType="connectLogDTO">
INSERT INTO CONNECT_LOG (ACCOUNT_PK, CONNECT_IP, CONNECT_DEVICE)
VALUES (#{accountPk}, #{connectIp}, #{connectDevice})
</insert>
<!-- 사용자 로그 전체 삭제 -->
<delete id="delete" parameterType="connectLogDTO">
DELETE FROM CONNECT_LOG
WHERE ACCOUNT_PK = #{accountPk}
</delete>
<!-- 특정 사용자 로그 최신 접속 1건 조회 -->
<select id="getOne" parameterType="connectLogDTO" resultType="connectLogDTO">
SELECT * FROM CONNECT_LOG
WHERE ACCOUNT_PK = #{accountPk}
ORDER BY CONNECT_DATE DESC
LIMIT 1
</select>
<!-- 특정 사용자 로그 전체 조회 -->
<select id="getList" resultType="connectLogDTO">
SELECT *
FROM CONNECT_LOG
WHERE ACCOUNT_PK = #{accountPk}
ORDER BY CONNECT_DATE DESC
</select>
</mapper>
#{} = 파라미터 바인딩
resultType은 DTO (별칭 사용)
🔹 MyBatis의 파라미터 바인딩 규칙
#{xxx} ➡ DTO의 getter 이름에서 getXXX()를 찾는다
#{} 안에는 DTO의 “필드명(camelCase)”을 정확히 써야 한다.
#{accountPk}가 set이 아니라 get을 호출하는가?
MyBatis에서 #{...}은 SQL 실행 시 값을 읽어오기 위한 것
SQL에 값을 넣는 읽기(read) 동작이지 쓰기(write)가 아님
따라서 MyBatis는 객체에서 값을 가져올 때 getter 메서드를 사용
<insert id="insert" parameterType="connectLogDTO">
INSERT INTO CONNECT_LOG (ACCOUNT_PK, CONNECT_IP, CONNECT_DEVICE)
VALUES (#{accountPk}, #{connectIp}, #{connectDevice})
</insert>
이렇게 코드가 존재하면
ps.setInt(1, connectLogDTO.getAccountPk());
ps.setString(2, connectLogDTO.getConnectIp());
ps.setString(3, connectLogDTO.getConnectDevice());
MyBatis는 내부적으로 이렇게 처리한다
즉, 값을 읽어오는 시점에서 getter 호출
ConnectLogRepository.java 전체 코드
package bugsandwich.ornably.connectLog;
import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class ConnectLogRepository {
@Autowired
private SqlSession sqlSession;
private static final String NAMESPACE = "ConnectLog.";
public List<ConnectLogDTO> selectAll(ConnectLogDTO connectLogDTO){
System.out.println("[로그] ConnectLogRepository의 selectAll 시작");
// 특정 사용자 로그 전체 조회
List<ConnectLogDTO> list = sqlSession.selectList(NAMESPACE + "getList", connectLogDTO);
System.out.println("[로그] selectAll count = " + list.size());
// selectList() : 조회 결과가 없으면 빈 리스트 반환
return list;
}
public ConnectLogDTO selectOne(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository selectOne 시작");
// 특정 사용자 로그 최신 접속 한 건 조회
ConnectLogDTO result = sqlSession.selectOne(NAMESPACE + "getOne", connectLogDTO);
if (result != null) {
System.out.println("[로그] selectOne 성공");
}
else {
System.out.println("[로그] selectOne 결과 없음");
}
return result;
}
public boolean insert(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository의 insert 시작");
// 새 접속 기록 추가
if(sqlSession.insert(NAMESPACE + "insert", connectLogDTO) > 0) {
System.out.println("[로그] ConnectLogRepository insert 성공");
return true;
}
else {
System.out.println("[로그] insert 실패");
return false;
}
}
private boolean update(ConnectLogDTO connectLogDTO) {
return false;
}
public boolean delete(ConnectLogDTO connectLogDTO) {
System.out.println("[로그] ConnectLogRepository의 delete 시작");
// 사용자 로그 전체 삭제
if(sqlSession.delete(NAMESPACE + "delete", connectLogDTO) > 0) {
System.out.println("[로그] delete 성공");
return true;
}
else {
System.out.println("[로그] delete 실패");
return false;
}
}
}
MyBatis Model 전체 흐름
@PostMapping("/login")
public String login(AccountDTO accountDTO) {
accountService.login(accountDTO);
}
1️⃣ Controller → DTO 생성
여기서 핵심은
Controller는 SQL을 모른다
요청 파라미터는 DTO에 바인딩한다
DTO는 Model 데이터 덩어리
@Service
public class AccountService {
@Autowired
private ConnectLogRepository connectLogRepository;
public void login(AccountDTO accountDTO) {
// 비즈니스 판단
connectLogRepository.insert(connectLogDTO);
}
}
2️⃣ Service → 비즈니스 흐름 담당
Service 역할은 무엇을 할지를 결정한다
트랜잭션 관리, 여러 Repository 조합이 가능하다
Service는 MyBatis를 직접 사용 안하는게 정석이다
@Repository
public class ConnectLogRepository {
@Autowired
private SqlSession sqlSession;
public boolean insert(ConnectLogDTO dto) {
return sqlSession.insert("ConnectLog.insert", dto) > 0;
}
}
3️⃣ Repository → MyBatis 진입 지점
여기서 중요한 포인트
SqlSession = MyBatis 실행기
"ConnectLog.insert"에서
ConnectLog ➡ mapper namespace
insert ➡ XML의 id
<mapper namespace="ConnectLog">
<insert id="insert" parameterType="connectLogDTO">
INSERT INTO CONNECT_LOG (ACCOUNT_PK, CONNECT_IP, CONNECT_DEVICE)
VALUES (#{accountPk}, #{connectIp}, #{connectDevice})
</insert>
</mapper>
4️⃣ Mapper XML → SQL 정의
여기서 벌어지는 일
parameterType ➡ DTO 타입
#{connectIp} ➡DTO의 getConnectIp() 호출
SQL 실행
<select id="selectOne"
parameterType="connectLogDTO"
resultType="connectLogDTO">
SELECT *
FROM CONNECT_LOG
WHERE ACCOUNT_PK = #{accountPk}
</select>
5️⃣ 결과 매핑 (SELECT일 경우)
내부 동작은
1. MyBatis가 쿼리 실행
2. ResultSet 생성
3. 컬럼명 ➡ DTO 필드 매핑
CONNECT_IP ➡ connectIp
언더스코어 ➡카멜 자동 변환
mybatis.configuration.map-underscore-to-camel-case=true 이 옵션이 켜져 있어야 한다
ConnectLogDTO result = sqlSession.selectOne("ConnectLog.selectOne", dto);
6️⃣ Repository → 결과 반환
결과가 없으면 null 있으면 List 반환
if (result != null) {
// 성공 로직
} else {
// 실패 로직
}
7️⃣ Service → 후처리
| 구성요소 | 역할 |
| DTO | 데이터 운반 (파라미터 + 결과) |
| Mapper XML | SQL 정의 |
| Repository | SQL 호출 창구 |
| SqlSession | 실행 엔진 |
'🎅 오너먼트 프로젝트' 카테고리의 다른 글
| Node 설치 (0) | 2026.02.19 |
|---|---|
| DTO 멤버변수 타입 래퍼타입으로 일치시키기 (0) | 2026.02.14 |
| 람다식 BeanPropertyRowMapper로 코드 줄여보기 (0) | 2026.02.07 |
| BeanPropertyRowMapper로 자동 매칭 (0) | 2026.02.07 |
| 테이블 이관 작업 진행 (Oracle ➡️ Mysql) (0) | 2026.02.01 |