5 분 소요

1. 간단한 웹 어플리케이션의 구성 요소

간단한 웹 어플리케이션을 개발할 때 사용하는 전형적인 구조는 다음 요소를 포함한다.

  • 프론트 서블릿
  • 컨트롤러 + 뷰
  • 서비스
  • DAO
  1. 프론트 서블릿
    프론트 서블릿은 웹 브라우저의 모든 요청을 받는 창구 역할을 한다. 프론트 서블릿은 요청을 분석해서 알맞은 컨트롤러에 전달한다.
    스프링 MVC에서는 DispatcherServlet이 프론트 서블릿의 역할을 수행한다.

    간단한웹구조

  2. 컨트롤러
    컨트롤러는 실제 웹 브라우저의 요청을 처리한다. 지금까지 구현했던 스프링 컨트롤러가 이에 해당한다. 컨트롤러는 클라이언트(브라우저)의 요청을 처리하기 위해 알맞은 기능을 실행하고 그 결과를 뷰에 전달한다.컨트롤러의 주요 역할을 다음과 같다.
    • 클라이언트가 요구한 기능을 실행
    • 응답 결과를 생성하는데 필요한 모델 생성
    • 응답 결과를 생성할 뷰 선택

    컨트롤러는 어플리케이션이 제공하는 기능과 사용자 요청을 연결하는 매개체로서 기능 제공을 위한 로직을 직접 수행하지는 않는다. 대신 해당 로직을 제공하는 서비스에 그 처리를 위임한다.
    예를 들어 앞서 작성했던 ChangePasswordController의 경우 다음 코드처럼 ChangePasswordService에 비밀번호 변경 처리를 위임했다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    @PostMapping
    public String submit(
     @ModelAttribute("command") ChangePwdCommand pwdCmd,
     Errors erros, HttpSession session) {
    
     ...
    
     AuthInfo authInfo = (AuthInfo) session.getAttribute("authInfo");
     try{
         //컨트롤러는 로직 실행을 서비스에 위임한다.
         changePasswordService.changePassword(
             authInfo.getEmail(),
             pwdCmd.getCurrentPassword(),
             pwdCmd.getNewPassword());
         return "edit/changePwd";
     } ...
    }
    
  3. 서비스 서비스는 기능의 로직을 구현한다. 사용자에게 비밀번호 변경 기능을 제공하려면 수정폼을 제공하고, 로그인 여부를 확인하고, 실제로 비밀번호를 변경해야 한다.
    이 중에서 핵심 로직은 비밀번호를 변경하는 것이다. 여기서 폼은 사용자와의 상호 작용을 위한 연결 고리에 해당하지 핵심 로직인 비밀번호 변경 자체는 아니다.

  4. DAO 서비스는 DB연동이 필요하면 DAO를 사용한다. DAO는 Data Access Object의 약자로서 DB와 웹 어플리케이션 간에 데이터를 이동시켜 주는 역할을 맡는다. 어플리케이션은 DAO를 통해서 DB에 데이터를 추가하거나 DB에서 데이터를 읽어온다.

2. 서비스 구현

서비스는 핵심이 되는 긴으의 로직을 제공한다. 예를 들어 비밀번호 변경 기능은 다음 로직을 서비스에서 수행한다.

  • DB에서 비밀번호를 변경할 회원의 데이터를 구한다.
  • 존재하지 않으면 익셉션을 발생시킨다.
  • 회원 데이터의 비밀번호를 변경한다.
  • 변경 내역을 DB에 반영한다.

웹 어플리케이션을 사용하든 명령행에서 실행하든 비밀번호 변경 기능을 제공하는 서비스는 동일한 로직을 수행한다. 이런 로직들은 한 번의 과정으로 끝나기보다는 위 예처럼 몇 단계의 과정을 거치곤 한다. 이런 이유로 서비스 메서드를 트랜잭션 범위에서 실행한다.
비밀번호 변경 기능도 다음과 같이 스프링의 @Transactional을 이용해서 트랜잭션 범위에서 비밀번호 변경 기능을 수행한다.

1
2
3
4
5
6
7
8
9
10
@Transactional
public void changePassword(String email, String oldPwd, String newPwd){
    Member member = memberDao.selectByEmail(email);
    if(member == null)
        throw new MemberNotFoundException();

    member.changePassword(oldPwd, newPWd);

    memberDao.update(member);
}

서비스를 구현할 때 한 서비스 클래스가 제공할 기능의 개수는 몇 개가 적당할까?

기능별로 서비스 클래스를 작성하는 것이 좋다. 그 이유는 한 클래스의 코드 길이를 일정 수준 안에서 유지할 수 있기 때문이다. 클래스의 코드 길이가 길어지면 이후에 기존 코드를 수정하거나 기능을 확장하기 어려울 때가 많다. 기능이나 서비스 클래스를 따로 만들면 이런 문제가 발생할 가능성을 줄일 수 있다.

2.1 서비스 클래스의 메서드 파라미터

서비스 클래스의 메서드는 기능을 실행하는데 필요한 값을 파라미터로 전달받는다.
하지만 다음과 같이 필요한 데이터를 담고 있는 별도의 클래스를 파라미터로 사용하기도 한다.

1
public void regist(RegisterRequest req)

필요한 데이터를 전달받기 위해 별도 타입을 만들면 스프링 MVC의 커맨드 객체로 해당 타입을 사용할 수 있어 편하다.

아래의 컨트롤러 클래스의 코드는 다음과 같이 서비스 메서드의 입력 파라미터로 사용되는 타입을 커맨드 객체로 사용했다.

1
2
3
4
5
6
@PostMapping("/register/step3")
public String handleStep3(RegisterRequest regReq, Errors errors){
    ...
    memberRegisterService.regist(regReq);
    ...
}

커맨드 클래스를 작성한 이유는 스프링 MVC가 제공하는 폼 값 바인딩과 검증, 스프링 폼 태그와의 연동 기능을 사용하기 위함이다.

2.2 서비스 메서드 실행 후 결과

서비스 메서드는 기능을 실행한 후에 결과를 알려주어야 한다, 결과는 크게 두 가지 방식으로 알려준다.

  • 리턴 값을 이용한 정상 결과
  • 익셉션을 이용한 비정상 결과

다음은 두가지 방식의 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AuthService{
    ...

    public AuthInfo authenticate(String email, String password){
        Member member = memberDao.selectByEmail(email);
        if(member == null){
            throw new WrongPasswordException();
        }
        if(!member.matchPassword(password)){
            throw new WrongIdPasswordException();
        }
        return new AuthInfo(member.getId(), member.getEmail(),
                            member.getName());
    }
}

3. 컨트롤러에서의 DAO 접근

서비스 메서드에서 어떤 로직도 수행하지 않고 단순히 DAO의 메서드만 호출하고 끝나는 코드도 있다. 예를 들어 회원 데이터 조회를 위한 서비스 메서드를 다음과 같이 구현하곤 한다.

1
2
3
4
5
6
public class MemberService{
    ...
    public Member getMember(Long id){
        return memberDao.selectById(id);
    }
}

이 코드에서 MemberService 클래스의 getMember() 메서드는 MemberDao의 selectByEmail() 메서드만 실행할 뿐 추가 로직은 없다, 컨트롤러 클래스는 이 서비스 메서드를 이용해서 회원 정보를 구하게 된다.

1
2
3
4
5
6
@RequestMapping("/member/detail/{id}")
public String detail(@PathVariable("id") Long id, Model model){
    //사실상 DAO를 직접 호출하는 것과 동일
    Member member = memberService.getMember(id)
    ...
}

위 코드에서 memberService.getMember(id) 코드는 사실상 memberDao.selectById()메서드를 실행하는 것과 동일한다.

이경우 컨트롤러는 서비스를 사용해야 한다는 압박에서 벗어나 다음과 같이 DAO에 직접 접근해도 큰 틀에서 웹 어플리케이션의 계층 구조는 유지된다고 본다.

1
2
3
4
5
@RequestMapping("/member/detail/{id}")
public String detail(@PathVariable{"id"} Long id, Model model){
    Member member = memberDao.selectByEmail(id);
    ...
}

컨트롤러에서 서비스 계층을 거치지 않고 바로 데이터 접근 계층의 DAO를 사욯하는 방식은 개발자마다 호불호가 갈린다. 정답은 없으니 나름대로 서비스의 역할과 DAO의 역할을 정의해나가면서 선호하는 방식을 정립하는게 좋다.

4. 패키지 구성

웹 어플리케이션에서 자주 사용되는 구조에 대해 살펴봤는데 각 구성 요소의 패키지는 어떻게 구분해 줘야 할까?
다음과 같이 웹 요청을 처리하기 위한 것과 기능을 제공하긱 위한 것으로 구분할 수 있다. 간단한웹구조2

웹 요청을 처리하기 위한 영역에는 컨트롤러 클래스와 관련 클래스들이 위치한다. 커맨드 객체의 값을 검증하기 위한 Validator도 웹 요청 처리 영역에 위치할 수 있는데 관점에 따라 Validator를 기능 제공 영역에 위치시킬 수도 있다. 웹 영역의 패키지는 web, member와 같이 영역에 알맞은 패키지 이름을 사용하게 된다.

기능 제공 영역에는 기능 제공을 위해 필요한 서비스, DAO, 그리고 Member와 같은 모델 클래스가 위치한다.
기능 제공 영역은 다시 다음과 같이 service, dao, model과 같은 세부 패키지로 구분하기도 한다.

간단한웹구조3

패키지 구성에는 사실 정답이 없다. 패키지를 구성할 때 중요한 점은 팀 구성원 모두가 동일한 규칙에 따라 일관되게 패키지를 구성해야 한다는 것이다.

웹 어플리케이션이 복잡해지면
컨트롤러-서비스-DAO 구조는 간단한 웹 어플리케이션을 개발하기에는 무리가 없다. 문제는 어플리케이션이 기능이 많아지고 로직이 추가되기 시작할 때 발생한다. 로직이 복잡해지면 컨트롤러-서비스-DAO 구조의 코드도 함께 복잡해지는 경향이 있다. 특정 기능을 분석할 때 시간이 오래 걸리기도 하고 , 중요한 로직을 구현한 코드가 DAO, 서비스 등에 흩어지기도 한다.

웹 어플리케이션이 복잡해지고 커지면서 코드도 함꼐 복잡해지는 문제를 완화하는 방법 중 하나는 도메인 주도 설계를 적용하는 것이다.
도메인 주도 설계는 UI-서비스-도메인-인프라의 네 영역으로 어플리케이션을 구성한다.
여기서 UI는 컨트롤러 영역에 대응하고 인프라는 DAO 영역에 대응한다.
중요한 점은 주요한 도메인 모델과 업무 로직이 서비스 영역이 아닌 도메인 영역에 위치한다는 것이다. 또한 도메인 영역은 정해진 패턴에 따라 모델을 구현한다.

Ref.

  • 최범균, 스프링프로그래밍입문5, 가메출판사.

카테고리:

업데이트: