🎅 오너먼트 프로젝트

자바와 쿼리문이 합쳐져 있던 Repository 파일 MyBatis로 결합도 낮춰보기

보배 진 2026. 2. 11. 10:25

 

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)을 쓰지 않아도 된다

 

🔹 핵심 요약

  1. type-aliases-package → 패키지 내 모든 클래스 등록, 자동 별칭 생성
  2. 별칭 사용 → Mapper XML이나 어노테이션에서 type, resultType, parameterType 등
  3. 클래스 등록 기준 → .class 파일이면 등록됨, public이어야 함
  4. 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 실행 엔진