Toby’s 스프링 3.0 스프링 웹 기술과 스프링 MVC를 읽고서…

난 그동안 스프링 MVC를 잘 몰랐다. 다행이다. 이제는 좀 알것같다. 대충 머리속에 그려진다. 지난번 소녀시대 스프링 @MVC를 발표할 때 아주 중요한 스프링 MVC 구성요소 몇가지를 소개했었다. 물론 그것들 보다는 소녀시대 이름만 외우고 가신 분들이 많겠지만;; 이 책에서는 모든 구성요소를 다 설명해주며 그 구성요소군에 속해있는 여러 요소들의 특징과 사용법도 설명해주고 있다. 물론 지면 관계상 뷰 기술중 대부분은 생략이지만 간단한 확장 방법을 통해서 충분히 다른 것들의 사용법도 예상할 수 있었다.

가장 기가막힌 부분은 스프링 Controller(API)와 핸들러 어댑터를 확장해서 새로운 형태의 컨트롤러를 만들어 사용하는 방법이었다. 이 부분은 정말 스프링 MVC 정복기라고 부르고 싶은 부분이다. 물론 호불호가 갈릴만한 부분이기도 하다.
하지만 그런 지식과 실천은 꼭 필요하다. 왜냐면 스프링이 제공하는 웹 계층 구현방법이 너무 다양하기 때문이다. 그것을 정책으로 통일시킬 수 있겠지만 더 좋은건 그 정책을 코드에 반영하는 것이 필요하고 그것을 기반으로 코드를 작성하면 규모가 큰 프로젝트라 하더라도 코드의 일관성을 유지할 수 있을 것이다. 프레임워크 개발이 필요한 이유가 바로 이때문이 아닐까? 프레임워크를 사용하는 이유는 둘 중 하나인것 같다. 프레임워크가 강요하는 정책을 따르기로 결정했거나, 그 프레임워크를 기반으로 프로젝트 고유의 정책과 특성을 반영한 추상계층을 만들어 낼 수 있기 때문인 것 같다. 스프링은 기본적인 방법들은 제공하지만 어떤 방법을 강요하지는 않는다. 따라서 개발자마다 코딩 스타일이 많이 다를 수 있는데 그것을 해결할 수 있는 방법을 소개해준다.
또한 스프링 3.0 최신 기술 중에 컨텐츠 네고 기법을 사용한 뷰 리졸버 설명이 기가 막힌다. 전혀 간단하지 않은 동작 원리를 쉽게 풀어 설명해주어 이해하기 편했다. 이 부분은 레퍼런스나 API에서도 설명이 빈약해서 직접 소스코드를 분석해가며 쓰셨다고 하니 그저 감사할 따름이다.
마지막으로 이 책에서 정의한 프레임워크 개발에 대한 글을 (저자의 허락-‘살짝쿵 스포일러를 넣어도 대
‘-하에) 옮겨본다. 

프레임워크 개발이란 이미 있는 기술을 조합해서 어떻게 쓸지 결정하고 툴이나 공통 모듈 정도를 만들어 놓는 것이 아니다. 프레임워크란 애플리케이션의 코드가 효율적인 방법으로 개발되서 사용될 수 있도록 새로운 틀(framework)을 만드는 작업이다.”

ps: 이번 장을 보면서 새로운 형태의 OSAF 3.0을 개발해야겠고 다짐했다.

[DDD] User-Familly 구현

User-Familly 관계부터 생각해보면, User가 일방적으로 여러 Familly를 가지고 있다고 볼 수 있습니다. 다중성(?)은 User에서 Familly쪽으로 1:대 관계입니다. (형제 자매가 같이 입사한 경우 다:다가 될 수도 있지만, 드문 경우니깐 중복 데이터 상관하지 않고 일단은 1대다로 ㄱㄱ, ) 생명주기를 생각해보면, Familly가 User에 종속되어 있습니다. 타입을 생각해보면 User는 엔티티가 거의 확실하고, Familly는 Value 타입에 가까운 듯 합니다. 하지만 편의상 id를 주고 관리하렵니다.

자 그럼 이제 필요한 도메인 클래스를 추가하고 속성들을 만든 다음 관계를 명시합니다.

class User {

    @OneToMany(cascade = { CascadeType.ALL })
    Set<Familly> famillies;

}

여기서 준 @OneToMany로 다중성이 어떤지 나타나고, Familly의 생명주기가 User에 종속적이라는 것이 드러납니다.

다음은 UserController(UC)로 갑니다. FamillyController를 만들 필요 없이, Familly와 연관되는 부분과 관련하여 그 요청을 UC가 처리하게 합니다. 화면상으로는 User의 수정화면에서 Familly 그리드가 보이고, 거기에 추가하는 버튼과, 수정하는 버튼이 달려있습니다.


새로 개선한 OSAF를 사용하여 만든 컨트롤러에 Familly를 그리드, 추가, 수정을 다룰 핸들러 메서드를 추가했습니다.

@Controller
@RequestMapping(“/base/user/*.do”)
public class UserController extends GenericController<User, UserService, UserRef, UserValidator, UserParams>{

    public UserController() {
        super(User.class);
    }

    @Autowired FamillyValidator famillyValidator;

    @RequestMapping
    public void famillygrid(@RequestParam int userid, ModelMap model, OrderPage orderPage){
        if (orderPage.getOrder() == null) orderPage.setOrder(“name asc”);
        model.addAttribute(“list”, service.findFamilliesBy(userid));
        model.addAttribute(“orderPage”, orderPage);
    }

    @RequestMapping(value=”/base/user/famillyadd.do”, method=RequestMethod.GET)
    public void famillyadd(ModelMap model){
        model.addAttribute(“model”, new Familly());
    }

    @RequestMapping(method=RequestMethod.POST)
    public String famillyadd(int userid, @ModelAttribute(“model”) Familly familly, BindingResult result,
            SessionStatus status){
        famillyValidator.validate(familly, result);
        if (result.hasErrors())
            return “/base/user/famillyadd”;
        else {
            service.addFamilly(userid, familly);
            status.setComplete();
            return CommonPages.CLOSE_GRID_REFRESH;
        }
    }

    @RequestMapping(value=”/base/user/famillyupdate.do”, method=RequestMethod.GET)
    public void famillyupdate(int famillyid, ModelMap model){
        model.addAttribute(“model”, service.findFamillyById(famillyid));
    }

}

thin facade 역할을 하고 있는 service를 이용하고 있습니다.

@Service
@Transactional
public interface UserService extends GenericService<User, UserParams>{

    Set<Familly> findFamilliesBy(int userid);

    void addFamilly(int userid, Familly familly);

    Familly findFamillyById(int famillyid);

}

인터페이스에 추가해주고,,, 실제로 하는 일은 대부분이 도메인 또는 dao로 위임하는 것입니다.

@Service
@Transactional
public class UserServiceImpl extends
        GenericServiceImpl<User, UserParams, UserDao> implements
        UserService {

    public Set<Familly> findFamilliesBy(int userid) {
        return get(userid).getFamillies();
    }

    public void addFamilly(int userid, Familly familly) {
        get(userid).addFamully(familly);
    }

    public Familly findFamillyById(int famillyid) {
        return dao.fingFamillyById(famillyid);
    }

}

그럼 도메인에서 할 일은 도메인에서 처리를 하고..

    public Set<Familly> getFamillies() {
        if (famillies == null)
            famillies = new HashSet<Familly>();
        return famillies;
    }

    public void addFamully(Familly familly) {
        getFamillies().add(familly);
    }

DAO에서 할일은 DAO에서 처리합니다,

    public Familly fingFamillyById(int famillyid) {
        return (Familly) getSession().createQuery(
                “from Familly where id = ” + famillyid).uniqueResult();
    }

어디서 처리하든 tx facade 역할을 하고 있는 service를 거쳐왔기 때문에, 트랜잭션 처리가 됩니다.

ps: 스프링 @MVC 혹은 OSAF의 버그(?) 발견..

    @RequestMapping(value=”/base/user/famillyadd.do”, method=RequestMethod.GET)
    public void famillyadd(ModelMap model){
        model.addAttribute(“model”, new Familly());
    }

여기서 @RequestMapping의 value를 주지 않아도(CoC로 인해 같은 결과가 나와야 함) 똑같이 동작해야 하는데, 생략할 경우, seach.do를 실행할 때(위 메서드와는 전혀 관계 없는 OSAF GenericController의 update() 메서드 시그너쳐 int id를 Integer로 바꾸라는 에러가 발생함.. @_@)

하이버네이트 사용시 Association Fake Object라는 기술을 사용해 보세요.

이 기술은 사부 토비님이 알려준 기술입니다.
 
Fake Object라는 이름으로 배운 기술인데 보다 적당한 이름이 생각나서 붙여봤습니다. 유즈케이스를 보여드리자면 Post -> Board 관계에서 Post를 추가하려고 할 때 Post에 Board 객체를 어디선가 setBoard로 묶어줘야 합니다.

그걸 폼을 보여주기 전에 하거나 아니면 폼을 받아서 처리할 때 하거나.. 언젠간 해야되죠. 그럴 때 보통 다음과 같은 코드를 작성하게 될 겁니다.

폼 보여줄 때 세팅할 경우를 생각해보죠.

    @RequestMapping(method=RequestMethod.GET)
    public void add(int boardId, ModelMap model){
        Post post = new Post();
        post.setBoard(boardService.getById(boardId));
        model.addAttribute(post);
    }

결국 저 문장은 서비스 타고 트랜잭션 내에서 DAO로 가서 DB로 가서 board 하나를 select하는 쿼리가 날아가게 합니다. 하지만  Association Fake Object 기술을 사용하면 그 쿼리를 보내지 않고도 아주 잘~ 연관을 맺은 상태로 객체를 저장할 수 있습니다.

Association Fake Object 라는 이름이 이미 사용 중인 용어인지는 모르겠는데, 연관 맺을 때 사용하는 가짜 객체 라는 뜻입니다.

어떻게?? 비밀입니다. 아.. 사실 비밀도 아닙니다. OSAF에서 아주 잘 활용하고 있거든요, 후후훗.

오랜만에 다시 본 애니프레임

얼마만에 살펴보는 건지 모르겠지만, 많이 발전한 것 같은 느낌입니다. OSAF를 공개하고나서 프레임워크 공개가 얼마나 쉽지 않은 일인가.. 얼마나 챙길 것이 많은지를 몸소 채험하고 있는 와중에 애니프레임을 보니까 뭔가 느낌이 새롭습니다. 깔끔한 홈페이지부터 잘 정리되어 있는 문서화가 정말 부러웠습니다. 크게 Core랑 Web이랑 Tool과 예제가 있고, 그 중에서 Core, Web을 봤더니 다시 메이븐으로 세세 하게 다시 나눠 두었습니다. 보면서 몇 가지 놀라웠던 걸 정리하겠습니다.

1. 프레임워크 외적인 요소.

깔끔한 홈피와 다량의 문서화가 부러웠습니다. 사용자와 그들의 피드백을 받고 있다는 것두요. 이것도 어쩌면 일종의 피드백을 드리는 것이오니.. 제가 지금 뭘 하고 있는거죠? ㅋㅋㅋ OSAF 홈피 가꿔도 모자랄 판에 지금 애니프레임 리뷰를 해주고 있다니 ㅠ.ㅠ 그래도 괜찮습니다. 많이 배웠거든요.

2. 하이버네이트 학습 테스트

가장 놀랜건 hibernate 모듈이었습니다. HQL을 XML에서 읽어오는 서비스였습니다. 해당 기능을 제공하는 서비스 인터페이스와 구현체 하나씩을 제공하는 모듈입니다. HQL 변경하기도 쉽고 담당자가 일괄적으로 관리하기 편하겠죠.


왼쪽에 패키지 명들을 쭉 보면.. 감탄만 나옵니다. 거기에 테스트 클래스를 작성한 걸 보면 코드 뿐만이 아니라 테스트 하고자 하는 시나리오를 적어 둔 걸 볼 수 있습니다. 남을 고려한 코드가 바로 저런게 아닌가 싶더군요. 저도 개인적으로 하이버네이트 학습을 하면서 만든 학습 테스트들이 쫙 있는데 요즘은 시간이 지나서 찾아 보려고 해도 해당 테스트가 뭘 테스트 했는지 천천히 코드를 읽어보기 전까진 알기 힘든데.. 정말 좋은 방법인 것 같습니다. 한 수 배웠습니다. 하이버네이트 RI 프로젝트를 진행할 땐 저런식으로 해야겠습니다.

3. 코드 원 저자 표시 분명해짐


제가 예전에 봤을 때는 유겐이나 로드 존슨의 이름도 없이 그들이 작성한 코드를 여러 곳에서 발견할 수 있었습니다. 그걸 보고 감정이 상해서 글을 좀 겪하게 썼었는데요. 이번에는 그런 코드가 없는 것 같습니다. 이런 원저자 표기는 시각적으로, 이성적으로, 감정적으로도 여러 긍정적인 효과를 준다고 생각합니다. OSAF아직 원 저자를 표시해 줄만한 코드가 없다보니 전부 Author Toby, Whiteship 입니다.

4. private static Log(X)


이것도 역시 지난 번에 지적했었던 문제 같은데 고쳐졌습니다. 수정하신 분인 임수연님 이신듯 한데요. 이 분이 애니프레임워크에서 hibernate를 포함하여 상당히 많은 부분에 기여(?라고 하긴 좀 그런가요.ㅎㅎ) 하신 것 같네요. OSAF도 이 부분은 일찌 감치 사부님의 지시하에 private static을 사용했었죠. 그래서 저는 한동안은 원래 private static으로 하는 거구나.. 로 알고 있었습니다.ㅋㅋ
이 부분은 http://wiki.apache.org/jakarta-commons/Logging/StaticLog 이 글 혹은 http://whiteship.me/1637 이 글을 참조해서 수정이 필요합니다.

5. 애스팩트도 사용하는 구나…


사용 예제 뿐만 아니라 프레임워크에서 제공하는 Aspect도 있습니다. 성능 모니터링에 사용할 애스팩트 같은데 좋쵸 그런거 제공해주면.. OSAF도 지난 스프링 세미나에서 발표했던 애스팩트 중에 쓸만한 것들은 포함에서 M2 때 공개해야겠습니다.

6. 작명 규칙이 뭔가 맘에 안드네

그렇다고 좋은 것만 눈에 띄진 않았습니다. 인터페이스를 표기할 때 I(영문 대문자 아이)를 붙이기로 한 것 같은데 저에게는 저런 표기가 좀 낯설어서 그런지 코드 보기가 불펴했습니다. IPropertyService = 아이프로퍼티서비스. 그럼 구현체는 그냥 PropertyService인가… 아니요. PropertyServiceImpl 입니다. 그럼 그냥 인터페이스는 PropertyService라고 하고 구현체는 뒤에 Impl을 붙이는 걸로 구분하는게 낫지 않았을까 싶은데.. 뭐 개발팀에서 정할 일이니 모르겠습니다. 기왕 스프링 기반 프로젝트면 스프링 작명 스타일을 따르는게 좋치 않을까 하는 개인적인 소망입니다. OSAF는 스프링 작명을 따르고 있습니다.

7. 예제 코드의 수많은 설정 파일

자바 1.5 미만 프로젝트를 지원하기 위해서겠지만, 설정 파일이 정말 많네요. 이건 뭐.. 어찌해야 될런지.ㅋㅋ;;; 새삼 OSAF는 설정 파일이 정말 없는거구나 하는 생각이 들더군요.


예제 코드의 applicationContext만 저정도… OSAF는 물개선샌님께서 설정 파일이 없어서 어디서 설정한 건지 알 수가 없었다는… 후훗. 애노테이션이 다 그렇쵸 뭐.ㅋㅋ 장단점이 있는 것 같습니다. 애노테이션으로 하면 설정을 한 군대에서 볼 수 없으니 답답하고, 세부 내용이 기본값 등으로 숨겨져있을 수 있어서 뭔가 명시적이지가 않은데, 설정 파일 수는 줄어 들고 소스코드랑 그와 관련된 설정이 한 군대에 있으니까 소스 코드 보면서 설정 보기도 편한데 반면.. XML은 블라블라브라ㅡㅏ브ㅏ..

8. 웹 예제.. 좀 짱인듯

웹 예제를 보니까 스프링 시큐리티, 재스퍼 리포트, Flex, Ajax, 캬오.. 너무 많은 내용이 하나의 예제에 들어가있는것이 아닌가 싶었습니다. 돌려보진 않았습니다. parent 폼을 못찾아서 지금 모든 프로젝트가 다 빨간 불이거든요.ㅋㅋ OSAF와 비교해 보면 태그 파일이 전혀 없습니다. 커스텀 태그는 몇 개 보이던데 OSAF가 제공하는 화면 컴포넌트 같은 것은 없습니다. JSP는 여전히 노가다로군요. 이 부분에 대한 프레임워크 지원은 미약했습니다. 화면 컴포넌트는 OSAF가 짱입니다.

9. 약간 아쉬운 거 ‘틀’

DAO 구현에 어떤 틀이 있는가? 조금 애매합니다. 그냥 일반적인 스프링을 사용한 DAO 구현처럼 보이기도 하는데, 쿼리를 보내는 부분은 애니프레임이 제공하는 쿼리 서비스를 사용해서 합니다. OSAF와는 정반대의 모습니다. GenericDAO라는 확실한 틀을 제공하고 쿼리는 자율에 맡기고 있습니다.

Service 구현에도 어떤 틀은 없네요. CRUD같은 반복적인 코드도 DAO와 마찬가지로 직접 일일히 구현해야 합니다. 예제를 보니까 DAO와는 달리 서비스 쪽은 인터페이스를 사용해서 만들고 있습니다. 흠~ 인터페이스를 쓰려면 DAO도 쓰고 안 쓰려면 Service도 쓰지 말지 왜 그렇게 했을지 좀 의아했습니다. 어쨋든 어떤 틀 같은 건 없었습니다. OSAF는 GenericService를 제공하죠. 두 종류의 GenericService를 제공하는데 하나는 Context가 있는 것(다른 것에 종속하는 것)이고, 하난 없는 것(독립적인 것)입니다.

컨트롤러 구현에는 어떤 틀이 있습니다. 위에서 본 코드 중에 하나인 AnyframeFormController 같은 것을 상속하여 컨트롤러를 만들고 있습니다. OSAF도 두 종류의 GenericController를 제공하여 컨트롤러 구현을 매우 간편하게 할 수 있도록 멋진 기능을 제공하고 있죠. 애니프레임이 제공하는 컨트롤러를 쓰더라도 상당 부분의 코드느는 스프링을 그냥 썼을 때와 비슷해 보입니다.

결론적으로.. 애니프레임의 프레임이라는 것이.. 조금 불투명해 보인다는 것입니다. OSAF 처럼 확실한 틀이 안 보입니다.

10. 발전한 모습에 정말 놀랐습니다.

몇 개월 전에 봤던 그 프레임워크가 맞나 싶을 정도로 많이 발전한 모습이고 코드 곳곳에서 노력하신 흔적을 볼 수 있었습니다. 샘날 정도로….

이것으로 상당히 긴 애니프레임 리뷰를 마치겠습니다. 몇 달 전엔 죄송했습니다. (__)/

Why OSAF 1. 테스트 코드를 익힐 수 있습니다.

OSAF를 공개한지 한 달이 아직 안 됐습니다. 10월 23일에 공개했었죠. 지금까지 약 200에 가까운 다운로드를 기록하고 있지만, 전혀… 아무런… 반응이 없다는 것에는 가히 놀라울 뿐입니다. 그냥 제가 쓴 글의 댓글 몇 개 정도 뿐의 관심이 저에게 한 편으로는 아쉬움으로 또 다른 한 편으로는 오기로 다가옵니다.

완전 최첨단 프레임워크인 OSAF에 왜 이렇게 관심이 없을까. 고민을 많이 했습니다. 어렵나? 메이븐 떄문인가? 그거 없어도 되는데. 문서가 부족하긴 부족하고. 그래도 어떻게 이렇게 조용할 수가 있지. 홈피 디자인이 좀 구리긴 한데.. 그거 때문인가? ㅋ. OSAF 발음이 너무 어려운가? 별에 별 생각을 많이 했습니다. 당연히 기운도 빠집니다. OSAF를 공개한 건 어쩌면 OSAF에게 못씁짓을 한 건 아닌지 말이죠.(Max님의 ‘어디가서 밥은 먹고 다녀야 할텐데..’ 라는 댓글이 생각납니다.)

긍정적으로 생각하기로 했습니다. 언젠가는 빛을 보겠지. 열심히 계속 가꾸다 보면 언젠간 알아주겠지. 하고 말이죠. 그래서 OSAF가 여러분에게 어떤 도움을 줄 수 있을지 생각하고 알려드리기로 했습니다. 그 중 첫 번째가 바로 테스트 코드입니다.

OSAF의 테스트 커버리지는 60%가 조금 넘습니다. (앞으로 차차 올릴 예정입니다.) 60%의 테스트 커버리지는 전부 OSAF 개발팀에서 직접 작성한 테스트 코드입니다. 어딘가에서 배껴온 코드가 절대로 아닙니다. 테스트는 초기에 JUnit과 EasyMock을 사용해서 작성 했었습니다. 물론 스프링 테스트 기능도 사용하고 있죠. 배포 직전에는 EasyMock을 Mockito로 교체하여 비슷한 테스트를 보다 깔끔하고 직관적이며 적은 수의 코드로 대체할 수 있었습니다. DBUnit을 확장하여 OSAF가 제공하는 테스트 케이스를 이용하면 DAO 테스트가 매우 간편해질 것 입니다.

이렇게 좋은데… 한 번 들여다 보고 뭐라고 해주시지 않으시겠어요? 좋다. 잘했다. 고맙다. 이런거 말구요. 이 부분의 테스트는 이해가 안 된다. 테스트가 조금 이상하다. 이 부분의 테스트는 왜 안했냐. 어려워서 그런거냐? 이 부분의 테스트는 이렇게 고치는게 좋치 않겠냐? 이런.. 반응이 제가 가장 좋아하는 반응이자 OSAF에게 거름을 주는 방법입니다.

소스 코드는 굳이 다운 받지 않아도(장기적으론 받아 두시면 좋겠지만..)

http://www.opensprout.org:9060/browse/OSAF/osaf/trunk

위 링크로 가시면 웹에서 직접 볼 수 있습니다. 소스 코드나 OSAF 와 관련하여 문제나 제안하고 싶은 것이 있다면 주저하지 마시고 이슈를 등록해 주세요.

http://www.opensprout.org/jira/secure/Dashboard.jspa