이 절에서는 응용 서비스를 구현할 때 몇 가지 고려할 사항과 트랜잭션과 같은 구현 기술의 연동에 대해 살펴본다
1) 응용 서비스의 크기
응용 서비스 자체의 구현은 어렵지 않지만 응용 서비스의 크기를 나누는 것은 고민거리이다
응용 서비스는 보통 다음 두 가지 방법 중 한 가지 방식으로 구현한다
한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
구분되는 기능별로 응용 서비스 클래스 따로 구현하기
1) 한 응용 서비스 클래스에 회원 도메인의 모든 기능 구현하기
장점
한 도메인과 관련된 기능을 구현한 코드가 한 클래스에 위치하므로 각 기능에서 동일 로직에 대한 코드 중복을 제거할 수 있다
단점
한 서비스 클래스의 크기 (코드 줄 수)가 커진다
코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지게 되므로 관련 없는 코드가 뒤섞여 코드를 이해하는 데 방해가 될 수 있다
한 클래스에 코드가 모이기 시작하면 분리하는 것이 좋은 상황임에도 습관적으로 기존에 존재하는 클래스에 억지로 끼워 넣게 되고, 코드를 점점 얽히게 만들어 코드 품질을 낮추는 결과를 초래한다
2) 구분되는 기능별로 응용 서비스 클래스 따로 구현하기
한 응용 서비스 클래스에서 한 개 내지 2~3개의 기능을 구현한다
공통 로직의 경우 별도 클래스에 로직을 구현하면 코드가 중복되는 것을 방지할 수 있다
장점
한 클래스에 관련 기능을 모두 구현하는 것보다 코드 품질을 일정 수준으로 유지하는 데 도움이 된다
각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현한 코드에 영향을 받지 않는다
단점
클래스의 개수가 많아진다
참고로 필자는 한 클래스가 많은 역할을 갖는 것보다 각 클래스마다 구분되는 역할을 갖는 것을 선호한다고 한다
2) 응용 서비스의 인터페이스와 클래스
응용 서비스의 인터페이스는 대개 필요하지 않다
인터페이스가 필요한 몇 가지 상황이 있는데, 그중 하나는 구현 클래스가 여러 개인 경우다
응용 서비스는 런타임에 교체하는 경우가 거의 없고 한 응용 서비스의 구현 클래스가 두 개 이상인 경우도 드물다
따라서 응용 서비스의 인터페이스와 클래스를 따로 구현하면 필요성에 비해 구조가 복잡해지는 문제가 발생한다
테스트 주도 개발을 즐겨하고 표현 영역부터 개발을 시작한다면 미리 응용 서비스를 구현할 수 없으므로 응용 서비스의 인터페이스부터 작성하게 된다
표현 영역이 아닌 도메인 영역이나 응용 영역의 개발을 먼저 시작하면 응용 서비스 클래스가 먼저 만들어진다
Mockito와 같은 테스트 도구를 이용하면 테스트용 대역 객체를 만들 수 있으므로 응용 서비스에 대한 인터페이스가 없어도 표현 영역을 테스트할 수 있다
이는 결과적으로 응용 서비스에 대한 인터페이스 필요성을 약화시킨다
3) 메서드 파라미터와 값 리턴
응용 서비스의 메서드가 필요한 값을 개별 파라미터로 전달받거나 VO 등의 데이터 클래스를 만들어 전달받을 수 있다
응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전달을 위해 별도 클래스를 사용하는 것이 편리하다
응용 서비스의 결과를 표현 영역에서 사용해야 하면 응용 서비스 메서드의 결과로 필요한 데이터를 리턴한다
예를 들어 식별자 또는 애그리거트 객체를 리턴할 수 있다
이때, 애그리거트 자체를 리턴하면 코딩은 편해지더라도 도메인의 로직 실행을 응용 서비스와 표현 영역에 두 곳에서 할 수 있으므로 코드의 응집도를 낮추는 원인이 된다
4) 표현 영역에 의존하지 않기
응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안 된다는 점이다
예를 들어 표현 영역에 해당하는 HttpServletRequest나 HttpSession을 응용 서비스에 파라미터로 전달하면 안 된다
응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다
게다가 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제가 발생한다
더 심각한 것은 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다는 것이다
예를 들어 응용 서비스에서 표현 영역과 관련된 타입을 파라미터로 받아서표현 영역의 상태를 처리한다고 생각해보자
이렇게 되면 표현 영역의 코드만으로 표현 영역의 상태가 어떻게 변경되는지 추적하기 어려워진다
결국 표현의 응집도가 깨져서 코드 유지 보수 비용이 증가하게 된다
위의 이유로 응용 서비스가 표현 영역의 기술을 사용하지 않도록 분리해야 한다
이를 지키기 위한 가장 쉬운 방법은 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않는 것이다
5) 트랜잭션 처리
응용 서비스의 중요한 역할은 트랜잭션을 관리하는 것이다
스프링과 같은 프레임워크가 제공하는 트랜잭션 관리 기능을 이용하면 쉽게 트랜잭션을 처리할 수 있으므로 적극 사용하는 것이 좋다
스프링은 @Transacional이 적용된 메서드가 RuntimeException을 발생시키면 트랜잭션을 롤백하고 그렇지 않으면 커밋하므로 이 규칙에 따라 코드를 작성하면 트랜잭션 처리 코드를 간결하게 유지할 수 있다
6.4 표현 영역
표현 영역의 책임은 크게 다음의 세가지와 같다
1) 사용자가 시스템을 사용할 수 있는 흐름(화면)을 제공하고 제어한다
2) 사용자의 요청을 알맞은 응용 서비스에 전달하고 결과를 사용자에게 제공한다
이 과정에서 표현 영역은 사용자의 요청 데이터를 응용 서비스가 요구하는 형식으로 변환하고 응용 서비스의 결과를 사용자에게 응답할 수 있는 형식으로 변환한다
또한 응용 서비스의 실행 결과를 사용자에게 알맞은 형식(뷰, JSON, 예외 메시지 등)으로 제공한다
3) 사용자의 세션을 관리한다
6.5 값 검증
값 검증은 표현 영역과 응용 서비스 두 곳에서 모두 수행할 수 있다
원칙적으로는 모든 값에 대한 검증은 응용 서비스에서 처리한다
표현 영역은 사용자로부터 입력받는 값에 잘못된 값이 존재하면 이를 사용자에게 알리고 값을 다시 입력받아야 한다
표현 영역에서 필수 값과 값의 형식을 검사하면 실질적으로 응용 서비스는 ID 중복 여부와 같은 논리적인 오류만 검사하면 된다
즉, 표현 영역과 응용 서비스가 값 검사를 나눠서 수행하는 것이다
응용 서비스를 사용하는 표현 영역 코드가 한 곳이면 구현의 편리함을 위해 다음과 같이 역할을 나누어 검증을 수행할 수도 있다
표현 영역: 필수 값, 값의 형식, 범위 등을 검증한다
응용 서비스: 데이터의 존재 유무와 같은 논리적 오류를 검증한다
응용 서비스에서 얼마나 엄격하게 값을 검증해야 하는지에 대해서는 의견이 갈릴 수 있다
필자는 응용 서비스에서 필수 값 검증과 논리적인 검증을 모두 하는 편이라고 한다
응용 서비스에 필요한 값 검증을 모두 처리하면 작성할 코드가 늘어나는 불편함이 있지만 반대로 응용 서비스의 완성도가 높아지는 이점이 있다고 한다
6.6 권한 검사
보안 프레임워크의 복잡도를 떠나 보통 다음 세 곳에서 권한 검사를 수행할 수 있다
표현 영역
응용 서비스
도메인
표현 영역에서 할 수 있는 기본적인 검사는 인증된 사용자인지 아닌지 검사하는 것이다
예를 들어 회원 정보 변경을 처리하는 URL에 대해 표현 영역은 다음과 같이 접근 제어를 수행한다
이런 접근 제어를 하기에 좋은 위치가 서블릿 필터(Servlet Filter)이다
서블릿 필터에서 사용자의 인증 정보를 생성하고 인증 여부와 권한을 검사한다(스프링 시큐리티도 이와 유사한 방식으로 동작한다)
URL 만으로 접근 제어를 할 수 없는 경우 응용 서비스의 메서드 단위로 권한 검사를 수행한다
이것이 꼭 응용 서비스의 코드에서 직접 권한 검사를 해야 한다는 것을 의미하지는 아니다
예를 들어 스프링 시큐리티는 AOP를 활용해서 다음과 같이 애너테이션으로 서비스 메서드에 대한 권한 검사를 할 수 있는 기능을 제공한다
public class BlockMemberService {
private MemberRepository memberRepository;
@PreAuthorize("hasRole('ADMIN')")
public void block(String memberId) {
Member member = memberRepository.findById(new MemberId(memberId))
.orElseThrow(() -> new NoMemberException());
member.block();
}
}
개별 도메인 객체 단위로 권한 검사를 해야 하는 경우엔 구현이 복잡해진다
예를 들어 게시글 삭제는 본인 또는 관리자 역할을 가진 사용자만 할 수 있으므로 게시글 작성자가 본인인지 확인하려면 게시글 애그리거트를 먼저 로딩해야 한다
즉, 응용 서비스의 메서드 수준에서 권한 검사를 할 수 없으므로 직접 권한 검사 로직을 구현해야 한다
스프링 시큐리티와 같은 보안 프레임워크를 확장해서 개별 도메인 객체 수준의 권한 검사 기능을 프레임워크에 통합할 수 있다
도메인 객체 수준의 권한 검사 로직은 도메인별로 다르므로 도메인에 맞게 보안 프레임워크를 확장하려면 프레임워크에 대한 높은 이해가 필요하다
이해도가 높지 않다면 프레임워크를 사용하는 대신 도메인에 맞는 권한 검사 기능을 직접 구현하는 것이 코드 유지 보수에 유리하다