MVC 디자인 패턴에서 Model은 크게 Service Layer와 Data Access Layer(Repository)로 구성된다. 이제까지는 Spring Data JPA를 이용해서 Data Access Layer를 어떻게 구성하는지 살펴봤고 이번 포스트에서는 Service Layer에 대해 살펴보자.
Model에서 Service 계층
Service Layer의 역할
Service 계층은 애플리케이션의 비지니스 로직을 처리하는 핵심 영역으로 데이터 접근 로직을 가지는 Repository와 사용자 인터페이스 로직을 갖는 Controller를 분리하여 응집도 높은 비지니스 로직을 구현하고 유지보수를 쉽게 할 수 있게 하는 역할을 한다.
Service 계층과 Controller, Model의 연동
Controller와의 연결: Controller는 사용자의 요청을 받아 Service 계층에 전달하고 Service 계층의 응답을 사용자에게 반환한다. 이를 통해 Controller는 비지니스 로직에서 분리되어 단순히 요청과 응답을 처리하는 역할에 집중하게 된다.
Repository와의 연동: Repository는 DB와의 직접적 상호작용을 담당한다. Service 계층은 비지니스 로직을 처리하고 필요하면 데이터를 Repository를 통해 저장하거나 조회한다.이를 통해 DB 접근 로직이 비지니스 로직과 분리될 수 있다.
이런 Service 계층을 사용하면 다음과 같은 장점을 가질 수 있다.
유지보수의 용이성: 비지니스 로직이 명확히 분리되기 때문에 코드의 가독성과 유지보수성이 좋아지고 새로운 기능을 추가하거나 기존 기능을 수정하려 할 때 어디에서 작업을 해야하는지가 명확해진다.
재사용성의 향상: 비지니스 로직이 잘 정의된 Service는 다른 영역에서 쉽게 재사용될 수 있고 이를 통해 코드의 중복이 줄어들고 개발 효율성이 높아진다.
테스트 용이성: 다른 영역들과 분리되기 때문에 '비지니스로직 만'테스트 하기가 용이하다.
Service 계층 작성
Service 계층을 빈으로 작성할 때는 역시 유연성을 위해 인터페이스 기반으로 작성해주는 것이 좋다. 비용 적인 면에서도 나중에 proxy가 적용될 때 interface 기반으로 작성하면 JDK 기반 proxy가 생성되고 그렇지 않으면 CGLIB 기반의 proxy가 작성되는데 후자의 비용이 메모리 사용량, 초기 생성 비용, 실행 시간 비용 측면에서 훨씬 크다.
그리고 구현체에는 @Service 애너테이션을 작성해준다. @Service는 해당 빈이 Service와 관련된 빈임을 명시적으로 나타내며 용도별로 AOP를 적용하는 등 유용하게 사용할 수 있다. Service에서는 Repository의 빈들은 주입해서 사용한다.
@Service
@AllArgsConstructor
public class MemberServiceImpl implements MemberService {
private final MemberRepo mrepo;
. . .
}
반환형에 대한 고민도 필요하다. 일반적으로 Repository는 DB와 직접 상호작용하는 계층이므로 Entity를 반환하는 것이 적절하다. Repository의 역할은 DB의 행을 자바 객체로 매핑하는 역할이기 때문이다. Service 계층에서도 물론 Entity를 반환할 수 있지만 다음의 이유로 가급적 DTO를 반환하는 것이 좋다.
갭슐화: entity는 DB와 직접 연관되는데 entity가 노출되서 사용된다면 DB 구조 변경시 코드가 영향을 받게 된다.
보안성: entity는 DB의 정보를 그대로 반영하므로 password 등 보안에 민감한 정보가 유출될 수 있다.
유연성: DTO를 사용하면 필요한 데이터만 선택적으로 반환할 수 있어서 응답의 크기를 줄이고 성능을 개선할 수 있다.
레이어 간 정보 전달
트랜젝션 처리
Service와 Transaction
Data Access Layer는개별 SQL의 처리를 목표로 한다. 즉 여러 SQL을 묶어서 사용하지 않는다. 하지만 업무는 여러 개의 SQL들이 뭉쳐서 동작한다. 예를 들어 Post 테이블에서 [게시물 삭제]라는 업무를 생각해보자. Post는 Reply에 의해 참조되고 있기 때문에 먼저 Reply 테이블에서 해당 Post에 속하는 데이터를 모두 지운 후 Post 테이블에서 삭제해야 한다.
만약 1번 게시물을 지운다면 할 때 개별 SQL로 다음의 두 쿼리가 실행되어야 한다.(Cascade는 없다고 가정하자.)
delete from reply where pno=1;
delete from post where pno=1;
그런데 만약 Reply에서의 삭제가 잘 진행된 후 어떤 원인에 의해 Post에서 삭제가 실패해버리면 어떻게 될까? 그 상태로 DB에 반영 되버린다면 댓글만 삭제되고 우리는 그 시스템을 신뢰할 수 없을 것이다.
그래서 필요한 것이 Transaction의 개념이다.Transaction은All or Nothing즉 "다 되거나 하나도 안되거나"이다. 위에서 가정한두 개의 SQL 동작이 모두 성공한다면 Commit으로 반영 확정,하나라도 실패한다면 Rollback으로 되돌리기처리를 해줘야 한다. 따라서 위 상황은 Rollback 되어서 삭제된 Reply 정보들이 복구되야 한다.
그런데 Data Access Layer는 개별 SQL들을 처리하면 끝나버리기 때문에 업무에 엮인 다른 SQL들을 살펴볼 수가 없다. 따라서여러 Data Access Layer들의 동작을 하나의 트랜젝션으로 묶어서 하나로 관리할 개념이 필요해졌고 이 역할을 수행하는 것이Service Layer이다.
기존 Service Layer의 문제점과 스프링의 대책
스프링 이전의 Service Layer가 가지는 문제점 역시 Data Access Layer가 가진 문제점과 동일하다. 다음은 JDBC를 이용한 트렌젝션 관리의 예이다.
public void delete(String code){
Connection con = DBUtil.getUtil().getConnection();
try{
con.setAutoCommit(false); // 프로그램 코드에서 트렌젝션 관리 시작
// 실제 비지니스 로직은 단 두줄
rrepo.delete(con, code);
brepo.delete(con, code);
con.commit(); // 트렌젝션 commit
}catch(Exception e){
DBUtil.getUtil().rollback(con); // 트렌젝션 rollback
}finally{
DBUtil.getUtil().close(con);
}
}
위 예에서 보듯 실제 비지니스 로직은 단 두 줄이고 나머지는 필요에 따라 추가된 횡단 관심사들이다. 심지어는 JDBC, MyBatis, JPA등 사용하는 기술에 따라 위 코드들은 달라진다.
스프링에서는 AOP를 이용해서 횡단 관심사 코드를 제거하고 비지니스 로직만으로 Service Layer를 구성할 수 있게 한다. 이때 트랜젝션을 처리하는 객체를 PlatformTransactionManager라는 인터페이스를 제공한다. 그리고 어떤 기술을 이용해서 데이터베이스를 활용하는지에 따라 다양한 구현체들이 제공된다.
PlatformTransactionManager를 이용한 트랜젝션 관리의 추상화
DataSourceTransactionManager: MyBatis나 Spring JDBC등 일반적인 JDBC 기반의 데이터 접근 기술에서 주로 적용
JpaTransactionManager: JPA를 사용하는 경우 적용
JtaTransactionManager: Java Transaction API를 활용하여 여러 데이터 소스에 대한 분산 트렌젝션을 처리하는 경우 적용
@Transactional
스프링에서는 트랜젝션 처리 방식으로 프로그래밍 방식이 아닌선언적 트랜젝션 처리(여기에 트랜젝션 처리 할꺼야~~)를 사용하는데 이를 위해@Transactional 애너테이션을 제공한다.
@Transactional은 클래스 레벨과 메서드 레벨에 선언할 수 있는 애너테이션이다. @Service의클래스 레벨에 선언하면 서비스에 포함된 모든 메서드들이 트랜젝션 하에서 동작하게 되고메서드 레벨에서 선언하면 해당 메서드만 트랜젝션 하에서 동작한다. 만약 클래스 레벨에도 있고 메서드 레벨에도 있다면 클래스에 선언된 내용이 1차 적용되고 메서드에 선언된 내용은 클래스에 선언된 내용을 재정의하게 된다.
@Transactional이 사용되면 스프링은 해당 @Service 클래스에 대해서 Around advice 타입의 proxy 객체를 생성하고 비지니스 로직 호출 전/후에 다음의 동작을 추가로 수행한다.
타겟 메서드 호출 이전에 Transaction 시작
타겟 메서드 정상 종료 후 commit 실행
타겟 메서드에서 RuntimeException 발생 시 rollback 실행
즉 스프링 이전의 서비스 코드에서 비지니스 로직을 제외하는 부분은 다 AOP에서 처리하는 구조다.
앞서 Data Access Layer의 단위 테스트에서도 @Transaction을 사용했는데단위 테스트에서 사용된 @Transaction은 언제나 rollback을 유발시키는 차이점이 있다.