Spring Boot 기반으로 개발을 진행하다 보면, 이전에 사용하던 구조와 전혀 다른 형태의 레거시 프로젝트를 마주하는 경우가 있다. 이번 프로젝트에서는 MyBatis를 사용하며 jQuery, XML Mapper, DAO, DVO, SVO, Servlet을 명시적으로 사용하는 구조로 구성되어 있었고, 내가 기존에 사용하던 Model, Entity, DTO, Repository 중심의 JPA 구조와는 차이가 있었다. 이 레거시 구조는 SQL 중심의 명시적인 설계이고, JPA 기반 구조는 객체 중심의 추상화된 설계라고 볼 수 있다.
이 글에서는 두 구조를 비교하면서 레거시를 이해하고, 동시에 Spring의 기본 구조를 다시 정리하려 한다.
MyBatis와 JPA 구조의 본질적인 차이
먼저 전체 구조를 단순하게 보면 다음과 같다.
MyBatis 구조 Hibernate/JPA 구조
─────────────── ────────────────────
Service Service
↓ ↓
DAO Repository
↓ ↓
Mapper XML Hibernate
:sqlSession.select(...) :JPA가 구현체 자동 생성
↓ ↓
SQL 직접 작성 SQL 자동 생성
: Mapper XML
이 차이는 단순해 보이지만 굉장히 중요하다.
- MyBatis: 개발자가 SQL을 직접 작성하고 제어하는 구조이다.
- JPA(Java Persistence API): Entity 상태를 기반으로 SQL을 자동 생성한다.
즉, MyBatis는 “SQL 중심”, JPA는 “객체 중심”이다.
두 구조를 대응 관계로 보면 다음과 같다.
| Legacy(MyBatis) | Hibernate/JPA | 같은 점 | 다른 점 |
| DVO | @Entity | DB 테이블 필드 1:1 매핑 | DVO는 순수 POJO(@Data 등), Entity는 @Entity, @Table, @Column 등 매핑 어노테이션 사용 |
| SVO | DTO (Request/Response) | 화면 ↔ Service 간 데이터 전달 | SVO는 BaseAbstractGridVO 상속으로 페이징 포함, DTO는 보통 독립 객체 |
| DAO + Mapper XML | Repository | 데이터 접근 계층 분리 | DAO는 XML에 SQL 직접 작성, Repository는 메서드명으로 쿼리 자동 생성 |
| DEM.xml | @Modifying, @Query 또는 변경 감지 |
INSERT / UPDATE / DELETE 처리 |
XML에 SQL 직접 작성 vs JPA는 Entity 변경 감지로 자동 SQL |
| DQM.xml | findBy...(), @Query | SELECT 처리 | XML에 SQL 직접 작성 vs 메서드명 파싱으로 자동 쿼리 생성 |
SVO와 DVO, 그리고 DTO의 차이
이 프로젝트를 처음 보면 SVO(Service Value Object), DVO(Data Valude Object)라는 용어가 가장 먼저 눈에 들어온다.
- SVO(Service Value Object): 화면 ↔ Service
- DVO(Data Valude Object): Service ↔ DAO(Data Access Object) ↔ DB
흐름을 보면 다음과 같다.
Browser → Controller → Service → DAO → DB
실제 데이터 흐름은 조금 더 구체적이다. JPA 구조에서 활용되는 DTO(Data Transfer Object)가 어디 매핑되는지 함께 표기하였다.
Browser
↓
Controller → SVO 수신 (DTO)
↓
Service → SVO → DVO 변환 (DTO → DTO)
↓
DAO → DVO 전달 (DTO)
↓
Mapper XML → SQL 실행
↓
DB
여기서 중요한 포인트는 “객체를 분리한다”는 것이다.
SVO는 화면 요청 데이터를 담는다. DVO는 DB 처리를 위한 데이터만 담는다.
예를 들어 화면에서는 페이징이나 검색 조건이 포함될 수 있다.
pageIndex
pageSize
searchKeyword
mntrtmGroupSn
하지만 DELETE SQL에서는 다음 값만 필요하다.
mntrtmGroupSn
이때 SVO를 그대로 DAO까지 넘기면 불필요한 데이터가 DB 계층까지 전달된다.
그래서 Service에서 SVO를 DVO로 변환하는 과정이 존재한다.
JPA 구조에서는 보통 다음과 같이 단순화된다.
Controller → DTO → Service → Entity → DB
- SVO → DTO
- DVO → Entity 또는 DTO
형태로 통합되는 경우가 많다.
Layered Architecture
이 프로젝트의 핵심 구조는 계층 구조이다.
Controller → Service → DAO → DB
여기서 중요한 규칙이 있다.
- Controller는 Service만 호출한다
- Service는 DAO만 호출한다
- DAO는 DB 접근만 담당한다
이 규칙이 깨지면 구조는 바로 무너진다. 예를 들어 Service에서 SQL을 직접 실행하면 다음과 같은 문제가 발생한다.
비즈니스 로직 + 데이터 접근 로직 혼합
DAO를 분리하면 다음과 같이 바뀐다.
// Service
comDAO.deleteMntrtmGroupInfo(dvo);
Service는 “무엇을 할지”만 알고, “어떻게 SQL을 실행하는지”는 모른다.
이게 계층 분리의 핵심이다.
Front Controller와 Servlet (with AJAX)
레거시 구조에서는 Servlet이 명확하게 드러난다. 브라우저가 요청을 보내면 서버(Tomcat)가 받고, 그 요청을 실제로 처리하는 것이 Servlet이다. 모든 요청은 DispatcherServlet을 통해 들어온다.
Browser → DispatcherServlet → Controller
web.xml에는 보통 다음과 같은 설정이 있다.
<servlet-mapping>
<url-pattern>*.do</url-pattern>
<url-pattern>*.ajax</url-pattern>
</servlet-mapping>
이 구조의 목적은 단순하다.
- 인증 처리
- 로깅
- 공통 예외 처리
모든 요청을 하나의 진입점에서 처리하기 위한 것이다.
여기서 AJAX라는 말이 보이는데 AJAX란 페이지를 새로고침하지 않고 서버와 통신하는 방식이다. 그리고 이 ajax를 쉽게 쓰기 위한 것이 jQquery라고 보면 된다. jQquery는 더 많은 기능이 있지만 간단하게 이렇게만 정리하고 넘어가자.
정리하자면 jQuery로 AJAX 요청을 보내고, Servlet이 그 요청을 처리한다.
DAO와 Mapper XML
DAO(Data Access Object)는 데이터 접근을 담당하는 객체이다.
Service가 SQL을 직접 알지 않도록 분리하는 역할을 한다.
comDAO.deleteMntrtmGroupInfo(dvo);
실제 SQL은 Mapper XML에 존재한다.
<delete id="deleteMntrtmGroupInfo">
DELETE FROM mntrtm_group
WHERE mntrtm_group_sn = #{mntrtmGroupSn}
</delete>
MyBatis의 특징은 SQL이 그대로 보인다는 것이다.
장점은 명확하다.
- 복잡한 쿼리 작성이 쉽다
- 실행 SQL을 예측할 수 있다
하지만 단점도 존재한다.
- XML 관리 필요
- 반복 코드 증가
- 유지보수 비용 증가
DEM.xml과 DQM.xml
이 프로젝트에서는 XML도 역할에 따라 나뉜다. 레거시에선 대표적으로 DEM(Data Elementary Module), DQM(Data Query XML)이 있었다.
- DEM.xml → INSERT / UPDATE / DELETE
- DQM.xml → SELECT
이렇게 나누는 이유는 명확하다. 데이터 변경과 조회를 분리하기 위함이다. JPA에서는 이 역할을 Repository가 담당한다.
findBy...
@Query
@Modifying
또는 Entity 변경 감지를 통해 자동으로 SQL이 생성된다.
AOP(with Proxy 패턴)와 트랜잭션
여기서 중요한 개념 중 하나는 AOP(Aspect-Oriented Programming)이다. 이 프로젝트 뿐만 아니라 모든 프로젝트에서 트랜잭션과 관련된 내용은 중요하게 다루어져야한다.
먼저 개발자는 다음과 같이 작성한다.
// 개발자가 작성한 코드 — 트랜잭션 코드가 없음
public void delete(ComSVO svo) {
comDAO.deleteA(dvo);
comDAO.deleteB(dvo); // 여기서 에러 나면?
}
하지만 실제 동작은 다음과 같다.
begin
deleteA
deleteB
commit
(에러 발생 시 rollback)
이 과정은 AOP를 통해 처리된다. 개발자는 비즈니스 코드 안에 트랜잭션 시작, 커밋, 롤백 코드를 직접 작성하지 않아도 된다. Spring에서는 트랜잭션 AOP가 보통 Proxy 패턴 기반으로 동작한다. 즉, Service 객체를 직접 호출하는 것처럼 보이지만 실제로는 Proxy 객체가 먼저 호출되고, 이 Proxy가 트랜잭션을 시작한 뒤 실제 Service 메서드를 실행한다. 메서드가 정상 종료되면 commit을 수행하고, 예외가 발생하면 rollback을 수행한다. 다만 이 글에서는 Proxy 패턴 자체를 깊게 다루지는 않고, 트랜잭션이 AOP를 통해 비즈니스 로직과 분리된다는 점만 짚고 넘어가려 한다. 다음은 ServiceImpl 계층에 AOP를 적용하기 위한 pointcut 설정 예시이다.
<aop:config>
<aop:pointcut
id="servicePointcut"
expression="execution(* ..impl.*Impl.*(..))" />
<aop:advisor
advice-ref="txAdvice"
pointcut-ref="servicePointcut" />
</aop:config>
- pointcut = 어디에 적용할지
- advice = 무엇을 적용할지
- advisor = pointcut과 advice를 연결
결론은 AOP를 적용하면 개발자는 delete라는 비즈니스 로직까지만 신경쓰면 되기에 편리하다는 것이다. 실제로 자세하게 동작하는 모습을 코드로 직접 구현한다면 다음과 같은 형태이다.
// 자동 생성된 프록시 (개발자 눈에는 안 보임)
try {
transaction.begin();
comDAO.deleteA(dvo); // ✅
comDAO.deleteB(dvo); // ❌ 에러!
transaction.commit();
} catch (Exception e) {
transaction.rollback(); // deleteA도 취소됨 → 데이터 일관성 보장
}
이 구조의 핵심은 다음이다.
- 트랜잭션 코드 제거
- 실수 방지
- 데이터 일관성 유지
이렇게 Service 계층에 AOP를 적용함으로써 비즈니스 로직과 트랜잭션 로직을 분리할 수 있다.
Template Method 패턴과 공통 구조
*AbstractServiceImpl, BaseAbstractDAO와 같은 네이밍으로 구성된 구조는 대부분 Template Method 패턴이다. 공통 실행 흐름은 부모 클래스에 존재하고, 세부 구현은 자식 클래스가 담당한다.
BaseAbstractDAO
↑
ComDAO
이 구조를 통해 sqlSession 호출 같은 반복 코드를 줄일 수 있다.
Decorator 패턴과 SiteMesh
SiteMesh는 화면에 공통 레이아웃(header, footer 등)을 입히는 역할을 한다. 이 구조는 기존 View를 감싸는 방식으로 동작하기 때문에 Decorator 패턴으로 볼 수 있다. 일반적인 흐름은 다음과 같다.
Controller → View → SiteMesh → Browser
Controller는 View를 반환하고, SiteMesh는 이 View를 그대로 브라우저로 보내지 않고 한 번 감싼 뒤 공통 레이아웃을 추가한다. 즉, 원본 View를 수정하지 않고 외부에서 기능을 덧붙이는 구조이다.
하지만 *.ajax 요청은 다르다. AJAX 요청은 HTML이 아니라 JSON 데이터를 반환하기 때문이다.
- *.do → HTML → SiteMesh 적용
- *.ajax → JSON → SiteMesh 미적용
{ "result": "success" }
AJAX 응답에 header, footer 같은 HTML 레이아웃이 붙으면 JSON이 깨진다. 그래서 AJAX 요청에는 SiteMesh를 적용하지 않는다.
전체 흐름 정리
삭제 요청 기준으로 전체 흐름을 보면 다음과 같다.
Browser 삭제 버튼 클릭
↓
DispatcherServlet이 URL 라우팅 (Front Controller)
↓
Controller (SVO(or DTO)로 파라미터 수신)
↓
Service (SVO → DVO 변환, AOP 트랜잭션 적용)
↓
DAO (SQL 호출)
↓
Mapper XML → PostgreSQL or Other DB
↓
JSON 응답 (*.ajax → SiteMesh 미적용)
이 흐름 하나에 여러 개념이 동시에 포함된다.
- Layered Architecture
- DTO (SVO / DVO)
- DAO Pattern
- AOP
- Servlet / Front Controller
- Template Method
- Decorator
레거시 구조는 복잡해 보이지만 오히려 구조가 명확하게 드러나고 흐름이 눈에 보인다.
- 어디서 요청이 시작되는지
- 어디서 객체가 변환되는지
- 어디서 SQL이 실행되는지
반면 JPA는 많은 부분을 자동화한다. 하지만 자동화된 구조를 제대로 이해하려면 이런 명시적인 구조를 먼저 이해하는 것이 중요하다. 결국 이 레거시 구조를 분석하는 과정은 Spring의 기본 개념을 다시 정리하는 과정이다.
마지막으로 한번 더 정리해 보면 다음과 같다.
- MyBatis는 SQL 중심 구조이다
- JPA는 객체 중심 구조이다
- SVO는 화면 요청 객체이다
- DVO는 DB 처리 객체이다
- DAO는 Service와 DB를 분리한다
- Mapper XML은 실제 SQL을 가진다
- Servlet은 요청의 진입점이다
- AOP는 트랜잭션을 자동 처리한다
- SiteMesh는 화면 레이아웃을 담당한다