: 응용 서비스는 표현 영역과 도메인 영역을 연결하는 매개체 역항르 하는 데 이는 디자인 패턴에서 파사드와 같은 역할을 수행한다.
: 응용 서비스 자체는 복잡한 로직을 수행하지 않기 때문에 응용 서비스의 구현은 어렵지 않다.
1. 응용 서비스의 크기
: 응용 서비스 자체의 구현은 어렵지 않지만 몇 가지 생각할 아이템이 있다. 그중 하나가 응용 서비스의 크기다.
: 회원 도메인을 구상해보자, 응용 서비스는 회원 가입, 회원 탈퇴, 회원 비밀번호 변경, 비밀번호 초기화와 같은 기능을 구현하기 위해 도메인 모델을 사용하게 된다.
: 이 경우 응용 서비스는 보통 다음의 두 가지 방법 중 하나로 구현한다.
•
한 응용 서비스 클래스에서 회원 도메인의 모든 기능 구현하기
•
구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
: 먼저 첫 번째 방식, 한 응용 서비스 클래스에서 회원 도메인의 모든 기능 구현하기
•
장점은 각 상위 기능들의 구현에 필요한 부가 로직들을 위한 코드 중복을 제거하기가 쉽다는 것이다.
•
모든 기능이 한 서비스 클래스에 모여서 크기가 커지게 되는데 코드 크기가 커지면 연관성이 적은 코드가 한 클래스에 함께 위치할 가능성이 높아지고 관련 없는 코드가 뒤섞여 코드 이해를 방해한다는 단점이 있다.
: 두 번째 방식의 구분되는 기능별로 응용 서비스 클래스를 따로 구현하기
•
구분되는 기능별로 구현하는 방식은 한 응용 서비스 클래스에 2~3개의 기능을 구현한다.
•
클래스의 개수는 많아지지만 한 클래스에 관련 기능을 모두 구현하는 것과 비교해서 코드 품질을 일정 수준으로 유지하는데 도움이 된다.
•
각 클래스별로 필요한 의존 객체만 포함하므로 다른 기능을 구현할 코드에 영향을 받지 않는다.
•
각 기능마다 동일한 로직을 구현할 경우 여러 클래스에 중복된 코드를 구현할 가능성이 높아진다. 하지만 별로 클래스에 로직을 구현해서 코드가 중복되는 것을 방지할 수 있다.
2. 응용 서비스의 인터페이스와 크래스
: 응용 서비스를 구현할 때 논쟁이 될만한 것이 인터페이스가 필요한 가이다. 다음과 같이 인터페이스를 만들고 이를 상속한 클래스를 만드는 것이 필요할까?
public interface ChangePasswordService {
public void changePassword(String memberId, String curPw, String newPw);
}
public class ChangePasswordServicelmpl implements ChangePasswordService {
...구현
}
Java
복사
: 인터페이스가 필요한 몇 가지 상황이 있는데, 그중 하나는 구현 클래스가 여러 개인 경우다.
: 구현 클래스가 다수 존재하거나 런타임에 구현 객체를 교체해야 할 때 인터페이스를 유용하게 사용할 수 있다. 그러나 응용 서비스는 런타임에 교체하는 경우가 거의 없고 한 응용 서비스의 구현체가 두 개인 경우도 드물다. 이 때문에 인터페이스와 클래스를 따로 구현하면 소스 파일만 많아지고 구현 클래스에 대한 간접 참조가 증가해서 전체 구조가 복잡해진다.
: 만약 테스트 주도 개발을 즐겨하고 표현 영역부터 개발을 시작한다면 미리 응용 서비스를 구현할 수 없으므로 응용 서비스의 인터페이스부터 작성하게 될 것이다.
: 표현 영역이 아닌 도메인 영역이나 응용 영역의 개발을 먼저 시작하면 응용 서비스 클래스가 먼저 만들어진다. 이렇게되면 표현 영역의 단위 테스트를 위해 응용 서비스 클래스의 가짜 객체가 필요한데 이를 위해 인터페이스를 추가할 수도 있다. 하지만 Mockito와 같은 테스트 도구는 클래스에 대해서도 테스트용 대역 객체를 만들 수 있으므로 인터페이스가 굳이 없어도 된다.
3. 메서드 파라미터와 값 리턴
: 응용 서비스가 제공하는 메서드는 도메인을 이용해서 사용자가 요구한 기능을 실행하는데 필요한 값을 파라미터로 전달 받아야 한다.
: 파라미터는 각 값을 개별로 전달 받을 수도 아니면 별도 데이터 클래스(DTO)를 만들어서 전달 받을 수 있다.
: 응용 서비스는 파라미터로 전달받은 데이터를 사용해서 필요한 기능을 구현하면 된다.
: 클래스에 대한 변환은 스프링 MVC와 같은 프레임워크의 객체 변환 기능을 이용하면 되므로 응용 서비스에 데이터로 전달할 요청 파라미터가 두 개 이상 존재하면 데이터 전덜알 위한 별도 클래스를 사용하는 것이 편리하다.
@Controller
@RequestMapping("/member/changePassword")
public class MemberPasswordController {
@PostMapping()
public String submit(ChangePasswordRequest changePwdReq) {
Authentication auth = SecurityContext.getAuthenticatoin();
changePwdReq.setMemberId(auth.getId());
try {
chagePasswordService.changePassword(changePwdReq);
} catch(NoMemberException ex) {
...
}
}
}
Java
복사
: 응용 서비스의 결과를 표현 영역에서 사용해야 하면 응용 서비스 메서드의 결과로 필요한 데이터를 리턴한다. 결과 데이터가 필요한 대표적인 예가 식별자다.
: 온라인 쇼핑몰은 주문 후 주문 상세 내역을 볼 수 있는 링크를 바로 보여준다. 이 링크를 제공하려면 방금 요청한 주문의 번호를 알아야하는데, 이 요구를 충족하려면 주문 응용 서비스는 주문 요청 처리 후에 주문번호를 결과로 리턴해야 한다.
: 애그리거트 객체를 그대로 리턴할 수도 있다. 이렇게 했을 경우, 응용 계층의 코드가 줄어 코딩이 편리해질 수는 있으나 도메인 모델이 그대로 표현 계층으로 넘어가 로직을 표현 영역에서도 실행할수 있게되므로 응집도를 낮추는 원인이 된다.
: 애그리거트를 리턴해도 애그리거트가 제공하는 기능을 컨트롤러나 뷰에서 실행하면 안된다는 규칙을 정할 수도 있지만 그보단 필요한 데이터만 리턴하는 것이 실행 로직의 응집도를 높이는 확실한 방법이다.
4. 표현 영역에 의존하지 않기
: 응용 서비스의 파라미터 타입을 결정할 때 주의할 점은 표현 영역과 관련된 타입을 사용하면 안 된다는 점이다.
: 예를 들어 다음과 같이 표현 영역에 해당하는 HttpServletRequest나 HttpSession을 응용 서비스에 파라미터로 전달하면 안 된다.
@Controller
@RequestMapping("/member/changePassword")
public class MemberPasswordController {
@PostMapping
public String submit(HttpServletRequest request) {
try {
changePasswordService.changePassword(request);
} catch(NoMemberException ex) {
...
}
}
}
Java
복사
: 응용 서비스에서 표현 영역에 대한 의존이 발생하면 응용 서비스만 단독으로 테스트하기가 어려워진다.(HttpServletRequest를 상속하는 등)
: 게다가 표현 영역의 구현이 변경되면 응용 서비스의 구현도 함께 변경해야 하는 문제도 발생한다.
: 이 두 문제보다 더 심각한 것은 응용 서비스가 표현 영역의 역할까지 대신하는 상황이 벌어질 수도 있다는 것이다.
: 표현 영역에 의존을 하지 않는 가장 쉬운 방법은 서비스 메서드의 파라미터와 리턴 타입으로 표현 영역의 구현 기술을 사용하지 않는 것이 바람직하다.
5. 트랜잭션 처리
: 회원 가입에 성공했다고 하면서 실제로 회원 정보를 DB에 삽입하지 않으면 고객은 로그인을 할 수 없다.
: 이는 트랜잭션과 관련된 문제로 트랜잭션을 관리하는 것은 응용 서비스의 중요한 역할이다.
: 스프링과 같은 프레임워크가 제공하는 트랜잭션 관리 기능을 이용하면 쉽게 트랜잭션을 처리할 수 있다.
public class ChangePasswordService {
@Transactional
public void changePassword(ChangePasswordRequest req) {
Member member = findExistingMember(req.getMemberld());
member.changePassword(req.getCurrentPassword(), req.getNewPassword());
}
...
}
Java
복사
: 프레임워크 단에서 제공하는 트랜잭션 기능을 적극 사용하는 것이 좋다. 프레임워크가 제공하는 규칙을 따르면 간단한 설정만으로도 트랜잭션을 시작하여 커밋하고 익셉션이 발생하면 롤백할 수 있다.
: 스프링은 @Transactional이 적용된 메서드가 RuntimeException을 발생시키면 트랜잭션을 롤백하고 그렇지 않으면 커밋하므로 이 규칙에 따라 코드를 작성하면 트랜잭션 처리 코드를 간결히 유지할 수 있다.