가장 크게 달라진 부분은 Repository 대신 MyBatis Mapper를 사용하여 데이터베이스와 통신한다는 점이다.
이전 프로젝트에서는 JdbcTemplate을 이용하여 Repository 안에서 SQL을 직접 실행하였다. 반면 이번 프로젝트에서는 Mapper 인터페이스만 작성하고 SQL은 XML에서 관리하도록 구조가 변경되었다.
또한 Service 계층에서는 단순히 Mapper를 호출하는 것이 아니라 DTO와 Entity를 변환하고 필요한 비즈니스 로직을 수행하는 역할을 담당하였다.
이번 파트에서는 Mapper와 Service가 실제로 어떻게 동작하는지를 중심으로 정리하였다.
| Spring JDBC | MyBatis |
|---|---|
| Repository 클래스 | Mapper Interface |
| JdbcTemplate 사용 | Mapper 메서드 호출 |
| SQL을 Java에서 작성 | SQL을 XML에서 작성 |
| RowMapper 필요 | 자동 매핑 |
Repository 클래스가 없어지고 Mapper 인터페이스가 데이터 접근을 담당하게 되었다.
@Mapper
public interface BoardMapper {
int insert(Board board);
List<Board> findAll();
Board findById(long id);
int update(Board board);
int delete(long id);
}
Mapper에는 SQL이 존재하지 않는다.
메서드만 선언하면 MyBatis가 XML에 작성된 SQL과 연결하여 실행한다.
BoardService
↓
BoardMapper.insert()
↓
MyBatis
↓
BoardMapper.xml
↓
INSERT SQL 실행
↓
Database
즉, 우리가 호출하는 것은 인터페이스의 메서드이지만 실제 실행은 XML에 작성된 SQL이 담당한다.
Spring은 실행 시 Mapper 인터페이스의 구현 객체(Proxy)를 자동으로 생성하여 주입한다.
따라서 개발자는 구현 클래스를 직접 만들 필요가 없다.
이번 프로젝트에서도 Service의 역할은 이전 프로젝트와 동일하다.
하지만 Repository 대신 Mapper를 호출한다는 점이 달라졌다.
public void create(BoardFormDTO dto) {
boardMapper.insert(dto.toEntity());
}
Service에서는
BoardFormDTO
↓
toEntity()
↓
Board Entity
↓
BoardMapper.insert()
순서로 처리된다.
DTO 내부의 toEntity()를 이용하여 Entity를 만든 뒤 Mapper에 전달한다.
조회에서는 조금 다른 흐름을 사용한다.
return boardMapper.findAll()
.stream()
.map(BoardViewDTO::fromEntity)
.toList();
조회 결과는
List<Board>
이다.
하지만 화면에서는
List<BoardViewDTO>
가 필요하다.
그래서 Stream API를 이용하여
List<Board>
↓
stream()
↓
BoardViewDTO.fromEntity()
↓
List<BoardViewDTO>
로 변환한다.
기존 프로젝트에서는 반복문을 사용할 수도 있었지만, Stream API를 사용하면 객체 변환을 더 간결하게 작성할 수 있다.
수정 과정은 다음과 같다.
게시글 조회
↓
Entity 수정
↓
Mapper.update()
코드를 보면
Board board = boardMapper.findById(id);
board.setTitle(dto.title());
boardMapper.update(board);
기존 데이터를 조회한 뒤 필요한 값만 변경하여 다시 저장하는 방식이다.
MemberMapper에서 흥미로운 점은
List<MemberViewDTO> findAll();
이다.
BoardMapper는 Entity를 반환하지만,
MemberMapper는 처음부터 DTO를 반환한다.
즉,
Database
↓
MyBatis
↓
MemberViewDTO
로 바로 변환된다.
필드명이 동일하면 MyBatis가 자동으로 DTO에 값을 매핑할 수 있기 때문에 가능한 방식이다.
MemberService 역시 구조는 동일하다.
memberMapper.insert(dto.toEntity());
입력 DTO를 Entity로 변환한 뒤 Mapper를 호출한다.
조회는
return memberMapper.findAll();
한 줄로 처리된다.
MemberMapper가 DTO를 바로 반환하기 때문에 Service에서 별도의 변환 과정이 필요하지 않다.
이 부분은 BoardService와 비교했을 때 가장 큰 차이점이다.
| Board | Member |
|---|---|
| Entity 반환 후 DTO 변환 | DTO를 바로 반환 |
| Stream API 사용 | 변환 과정 없음 |
| fromEntity() 호출 | 자동 매핑 활용 |
같은 MyBatis를 사용하더라도 설계 방식에 따라 구현이 달라질 수 있다는 점을 확인하였다.
| 학습 내용 | 이해한 점 |
|---|---|
| Mapper | Repository 없이도 데이터 접근 계층을 구성할 수 있다. |
| Proxy 객체 | MyBatis가 Mapper 인터페이스의 구현 객체를 자동 생성한다. |
| XML 매핑 | SQL을 Java 코드와 분리하여 관리할 수 있다. |
| Stream API | Entity 목록을 DTO 목록으로 간결하게 변환할 수 있다. |
| DTO 직접 반환 | 상황에 따라 Mapper가 DTO를 바로 반환하도록 설계할 수도 있다. |
이번 프로젝트를 통해 MyBatis는 단순히 SQL을 실행하는 라이브러리가 아니라, Mapper 인터페이스와 XML을 연결하여 데이터 접근을 추상화하는 프레임워크라는 점을 이해하게 되었다. 또한 Board와 Member의 구현 방식을 비교하면서 항상 하나의 방식만 사용하는 것이 아니라, 상황에 따라 Entity를 반환한 뒤 Service에서 DTO로 변환할지, Mapper에서 DTO를 직접 반환할지 선택할 수 있다는 점도 새롭게 배울 수 있었다.