주요글: 도커 시작하기
반응형

도메인 모델의 데이터를 뷰에 노출하는 방법 때문에 늘상 고민을 하는데, 그런 고민을 하는 이유는 도메인 객체의 기능을 뷰에 노출하고 싶지 않기 때문이다. 도메인 객체가 단순 데이터 구조라면 도메인 객체를 그대로 (JSP나 Velocity와 같은) 뷰에 노출해도 setter 메서드만 조심해서 사용하면 크게 문제될 것이 없다. 하지만, 도메인 객체가 암호 변경, 만료일 연장과 같은 도메인 기능을 제공하고 있다면, 뷰 코드에서 이들 기능을 실행하지 않는 것을 선호하는데, 그 이유는 최대한 한 레이어에서 기능 실행을 제어하길 원하기 때문이다. (예를 들어, 도메인 기능은 바로 위 어플리케이션 레이어에서만 실행하도록 하고 컨트롤러나 뷰에서는 실행하지 않도록 함으로써, 도메인 기능을 한 곳에서만 실행하도록 코드를 제어하고 싶다.)


본 글에서는 도메인 객체를 뷰에 전달할 때 사용할 수 있는 세 가지 방법을 살펴볼 것이며, 이를 통해 여러분은 각 방법 중 자신에게 알맞은 방식을 선택할 수 있을 것이다.


방법1: 그냥 전달하기


이 방법은 가장 쉬운 방법이다. 그냥 도메인 객체를 그대로 뷰에 전달하는 방법이다. 예를 들어, 스프링 컨트롤러에서 다음과 같이 뷰에 도메인 객체를 그대로 전달한다.


public class UserInfoController {

    private UserRepository userRepository;


    @RequestMapping("/user/info")

    public String info(ModelMap modelMap) {

        User user = userRepository.findOne(getUserId());

        if (user == null) {

            return "user/userNotFound";

        }

        modelMap.addAttribute("user", user);

        return "user/userInfo";

    }

    ...

}


JSP와 같은 뷰 코드에서는 다음과 같이 user 객체를 사용한다.


${user.id} , ${user.name}


-- 연관된 객체에 접근

${user.team.name} 


-- 도메인 객체가 제공하는 기능 실행 가능

${user.changePassword('newpassword')}


위 방식은 다음의 특징이 있다.

  • 뷰 코드에서 연관된 객체에 접근할 수 있도록 해야 한다.
    • JSP와 같은 ORM 기술을 사용할 경우, 트랜잭션 범위를 뷰 영역까지 확장하거나 연관된 객체를 미리 읽어오는(eager loading) 등의 처리가 필요하다.
  • 도메인 객체가 제공하는 기능을 뷰에서 실행하는 것에 대한 엄격한 규칙이 필요하다.
    • 암호 변경과 같은 기능을 뷰에서 실행할 수 있다는 것은 도메인 기능 실행을 요청하는 곳이 여러 영역으로 분산된다는 의미인데, 이는 유지보수에 도움이 되지 않는다. 따라서, 뷰에서는 도메인 기능을 실행하지 않도록 제한하는 것이 좋다.


방법2: 읽기 전용 인터페이스


두 번째 방법은 읽기 전용 인터페이스를 만드는 것이다. 예를 들어, 다음과 같이 데이터 제공을 위한 전용 인터페이스를 작성한다. 예를 들어, 다음과 같이 정보 제공을 하는 인터페이스를 작성한다.


public interface ReadonlyUser {

    public Long getId();

    public String getName();

    public ReadonlyTeam getTeam();

}


public interface ReadonlyTeam {

    public Long getId();

    public String getName();

}


public class User implements ReadonlyUser {

    @Override

    public Long getId() { return id; }

    @Override

    public String getName() { return name; }

    @Override

    public Team getTeam() { return team; } // 자바는 리턴타입에 하위 타입 지정이 가능하다.


    public void changePassword(String newPassword) { ... }

}


public class Team implements ReadonlyTeam {

    ...

}


읽기 전용 인터페이스를 만들었다면, 읽기 전용 인터페이스를 이용해서 데이터를 읽어오는 객체를 추가한다.


public class UserDataLoaderImpl implements UserDataLoader {


    public ReadonlyUser getUser(Long id) {

        User user = userRepository.findOne(id);

        throwExceptionWhenUserIsNull(user);

        return user;

    }

}


이제 컨트롤러는 다음과 같이 UserDataLoader를 사용해서 읽기 전용 User 타입을 가져온다.


public class UserInfoController {

    private UserDataLoader userDataLoader;


    @RequestMapping("/user/info")

    public String info(ModelMap modelMap) {

        try {

            ReadonlyUser user = userDataLoader.getUser(getUserId());

            modelMap.addAttribute("user", user);

            return "user/userInfo";

        } catch(UserNotFoundException ex) {

            return "user/userNotFound";

        }

    }

    ...

}


그런데, 위와 같이 코드에서는 읽기전용 인터페이스를 사용했다고 하더라도 뷰 코드에서는 리플렉션을 이용하기 때문에 실제로는 다음과 같이 도메인 기능을 사용할 수 있다.


-- 리플렉션으로 실행 가능

${user.changePassword('newpassword')}


따라서, 완전한 읽기 전용 객체를 전달하고 싶다면, 다음과 같이 User 객체가 아닌 프록시 객체를 만들어서 전달하는 것이 좋다.


public class UserProxy implements ReadonlyUser {

    private User user;

    private ReadonlyTeam team;


    public UserProxy(User user) {

        this.user = user;

        this.team = new TeamProxy(user.getTeam());

    }


    @Override

    public Long getId() { return user.getId(); }

    ...

    @Override

    public ReadonlyTeam getTeam() { return team; }

}


DataLoader에서는 다음과 같이 User를 리턴하는 대신에 UseProxy를 생성해서 리턴한다.


public class UserDataLoaderImpl implements UserDataLoader {


    public ReadonlyUser getUser(Long id) {

        User user = userRepository.findOne(id);

        throwExceptionWhenUserIsNull(user);

        return new UserProxy(user);

    }

}


이제 뷰에는 UserProxy 객체가 전달되므로, 완전한 읽기 전용 객체를 뷰에서 사용하게 되며, 더불어 도메인 기능을 뷰에서 실행할 수 있는 가능성도 없어졌다.


방법3: Exporter 사용하기


세 번째 방법은 자신이 필요한 데이터만 가져가도록 도메인 객체에 Exporter 인터페이스를 추가하는 것이다. 이 방법을 사용하려면 다음과 같이 도메인 객체의 데이터를 받을 수 있는 인터페이스를 먼저 정의한다.


public interface UserExporter<T> {

    public void id(Long id);

    public void name(String name);

    public void team(Long teamId, String teamName);

    public T build();

}


도메인 객체는 UserExporter에게 데이터를 전달해주기 위한 메서드를 제공한다.


public class User {


    public <T> T export(userExporter<T> expoter) {

        exporter.id(this.id);

        exporter.name(this.name);

        exporter.team(this.team.getId(), this.geam.getName());

        return exporter.build();

    }

}


뷰 영역에 데이터를 전달해주어야 하는 객체는 다음과 같이 필요한 데이터를 받는 Exporter 구현체와 DTO를 만든다.


public class DataLoaderImpl .. {


    public UserDto getUser(Long id) {

        User user = userRepository.findOne(id);

        throwExceptionWhenUserIsNull(user);

        return user.export(new MyUserExporter());

    }


    private class MyUserExporter implements UserExporter<UserDto> {

        private UserDto userDto;

        public MyUserExporter() {

            userDto = new UserDto();

        }

        public void id(Long id) { userDto.setId(id); }

        public void name(String name) { userDto.setName(name); }

        ...

        public UserDto build() { return userDto; }

    }

}


DataLoader를 사용하는 코드는 UserDto를 사용하므로, 결과적으로 뷰는 데이터만 사용하게 된다.



+ Recent posts