Whiteship's Note

'프로젝트'에 해당되는 글 35건

  1. 2010.06.23 [회사일] GenericExcelView 만들기
  2. 2010.06.18 [회사일] JPA로 계층구조 매핑하기
  3. 2010.06.15 [회사일] 다대다 관계 서브 그리드 CRUD 완성
  4. 2010.06.14 [회사일] 서브 그리드 뿌리기
  5. 2010.06.14 [회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지
  6. 2010.06.14 [회사일] DateRange 추가
  7. 2010.06.14 [회사일] view.jsp 태그파일 적용
  8. 2010.06.14 [회사일] new.jsp 태그파일 적용
  9. 2010.06.14 [회사일] mgt.jsp 파일에 태그 파일 적용
  10. 2010.06.11 [회사일] edit.jsp 화면에 태그 파일 적용하기
  11. 2010.06.11 [회사일] GenericController 만들기
  12. 2010.06.11 [회사일] Generic 타입 추론 유틸 만들기
  13. 2010.06.09 [회사일] GenericService 버그 수정하기 (3)
  14. 2010.06.09 [회사일] MemberService 만들기. GenericService 만들기 (6)
  15. 2010.06.09 [회사일] Member 추가. MemberDAO 구현. GenericDAO 구현
  16. 2010.06.09 [회사일] CriteriaUtils 테스트하기 (2)
  17. 2010.06.09 [회사일] 검색에 enum 필드 추가하기
  18. 2010.06.08 [회사일] Enum 추가, Formatter 적용
  19. 2010.06.08 [회사일] GenericPersistentEnumFormatter 만들기
  20. 2010.06.08 [회사일] CRUD 화면 디자인 수정
  21. 2010.06.07 [회사일] CRUD 구현
  22. 2010.06.07 [회사일] add 팝업 만들기
  23. 2010.06.03 [회사일] 그리드 정렬 기능 구현하기
  24. 2010.06.03 [회사일] 검색및 페이지 처리 구현하기
  25. 2010.06.03 [회사일] 그리드 출력하기 with JqGrid
  26. 2010.06.03 [회사일] 화면 레이아웃 잡기
  27. 2010.06.01 [회사일] DAO 테스트 만들기
  28. 2010.06.01 [회사일] 초간단 계층형 아키텍처 만들기
  29. 2010.06.01 [회사일] 프로젝트 세팅 (2)
  30. 2010.06.01 [회사일] 간단한 재고 조사 시스템 개발

[회사일] GenericExcelView 만들기

프로젝트/SLT : 2010.06.23 11:24


현재 업무를 엑셀 파일로 처리하고 있기 때문에 엑셀 파일과의 연동을 필수적입니다. 따라서 시스템에 들어있는 정보를 엑셀로 받아볼 수 있게 하거나 엑셀 정보를 시스템에 올려서 DB에 저장되게 하는 것도 필요합니다.

엑셀 업로드야 엑셀 파일 형식이 도메인 클래스 마다 다를 테고 비즈니스 로직마다 다를테니 일반화하기 힘들지만(뭐 어떤 규칙을 정하면 일반화를 못할 것도 없겠지만..) 엑셀 다운로드 경우에는 일반화하기 좋습니다. 리포트 형식의 엑셀을 원하는게 아니라면 말이죠;; 그런건 여기서 받은 엑셀 파일을 기초데이터로 삼아서 다른 직원보고 편집해서 리포트 만들라고 시키면 될테니 이런 간단한 재고관리 시스템에 무리한 기능을 넣고 싶진 않군요.

어쨋든.. 일반화 시켜봅시다.

<page:mgtpage label="재고 관리">
    <page:search label="재고 검색">
        <s:input label="제품명" path="name" />
        <s:input label="제품번호" path="number" />
        <s:input label="색상" path="colorName" />
        <s:select label="성별" path="sexValue" items="${ref.sexList}" />
        <s:input label="공급업체" path="suppName" />
        <s:input label="창고" path="locationName" />
        <s:date label="날짜" fromPath="dateRange.from" toPath="dateRange.to"/>
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '제품명', '제품번호', '색상', '사이즈', '공급업체', '창고', '수량', '날짜'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'item.name', index:'item.name', width:90},
                    {name:'item.number', index:'item.number', width:90},
                    {name:'item.colorName', index:'item.color', width:90},
                    {name:'item.size', index:'item.size', width:90},
                    {name:'item.supp.name', index:'item.supp', width:80},
                    {name:'location.name', index:'location.name', width:80},
                    {name:'qty', index:'qty', width:80},
                    {name:'date', index:'date', width:80}
                ]
            });
        });
    </script>
</page:mgtpage>

화면에 보여줄 그리드 정보가 들어있는 mgt.jsp 페이지 소스입니다. 태그 파일을 사용해서 중복 코드를 제거하고 여기서 꼭 다뤄야하는 정보만 남겨뒀습니다. 이 상태에서 엑셀을 일반화 시켜서 만들때 서버쪽으로 넘겨줘야 하는 정보가 들어있습니다. 컬럼명과 각 컬럼 값이 들어있는 path 정보입니다.

화면에 등록된 그리드 정보를 그대로 엑셀로 옮기고 싶다면 그 두 정보를 가져와야 합니다. 구현 과정을 전부다 설명하기는 다소 귀찮군요.. 흠.. 할일도 많으니까 대충 생략하겠습니다.


저기서 버튼을 누르면 컬럼 정보, Path 정보, 검색 매개변수들을 서버쪽으로 넘겨줍니다.

그럼 서버가 받아서 SpEL과 JExcel API와 스프링이 제공해주는 AbstractJExcelView 엑셀뷰를 사용해서 엑셀을 만들어 줍니다.

public class GenericExcelView extends AbstractJExcelView {

    protected void buildExcelDocument(Map<String, Object> model, WritableWorkbook wb, HttpServletRequest req, HttpServletResponse res) throws Exception {
        List<?> list = (List<?>) model.get("list");
        String modelName = (String) model.get("modelName");
        List<String> colNameList = (List<String>) model.get("colNameList");
        List<String> colPathList = (List<String>) model.get("colPathList");

        res.setHeader("Content-Disposition", "attachment; filename=" + modelName + ".xls");
        WritableSheet sheet = wb.createSheet(modelName, 0);

        for(int column = 0 ; column < colNameList.size() ; column++){
            sheet.addCell(new Label(column, 0, colNameList.get(column)));
        }

        ExpressionParser parser = new SpelExpressionParser();
        int row = 1;
        for(Object object : list){
            StandardEvaluationContext context = new StandardEvaluationContext(object);
            for(int column = 0 ; column < colPathList.size() ; column++){
                String path = colPathList.get(column).replace(".", "?.");
                String value = parser.parseExpression(path).getValue(context, String.class);
                sheet.addCell(new Label(column, row, value));
            }
            row++;
        }
    }
}

이걸 사용하도록 GenericController 쪽에도 기능을 추가해줬는데 그거야 뭐 간단하니깐. 생략하겠습니다. 이제 재고관리와 상품관리쪽에 엑셀 업로드를 만들어야겠습니다. 내일할까나;; 흠.


top

Write a comment.


[회사일] JPA로 계층구조 매핑하기

프로젝트/SLT : 2010.06.18 15:24


하이버 번역서를 보시면 여러가지 방법이 나와있는데 그 중에서 가장 간단하면서, 성능도 좋고, 다형성까지 살릴 수 있는 방법으로 "계층 구조 당 테이블" 매핑 방법을 소개하고 있습니다.

원래 이 회사에서 다루는 재고가 '신발' 하나였는데 이제 곧 '가방'까지 늘어날 예정인가 봅니다. 고객(울 회사 대표님)과 대화를 하지 않았다면 몰랐을텐데.. 초기에 알았으니 그나마 다행입니다. Item이라는 클래스에 신발 정보 다 넣어놓고 이런 얘기 했다면... @_@ 좀 피곤했을텐데 말이죠. 다행히 테스트용으로 두 세개 밖에 안넣어봤거든요.ㅋ

어쨋든..

Item을 상위클래스로 두고, Shoes와 Bag을 하위 클래스로 설계하고 싶어졌습니다.

그래서 모든 상품에 기본적으로 들어갈만한 속성은 Item에 남겨두고 JPA 계층 구조 매핑을 추가했습니다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    name = "ITEM_TYPE",
    discriminatorType = DiscriminatorType.STRING
)
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column
    @Type(type = "text")
    private String descr;

    @Column(length = 50)
    private String name;

    @Column(length = 50)
    private String number;

    @Column
    private String image;

    @Column
@Type(type="smdis.domain.usertype.SexUserType")
    private Sex sex;

    @ManyToOne
    private Color color;
...
}

그다음 이 클래스를 상속받는 Shoes라는 도메인을 만들었습니다.

@Entity
@DiscriminatorValue("SHO")
public class Shoes extends Item {
    
    @Column(name = "SHO_SIZE")
@Type(type="smdis.domain.usertype.ShoesSizeUserType")
    private ShoesSize size;

    public ShoesSize getSize() {
        return size;
    }

    public void setSize(ShoesSize size) {
        this.size = size;
    }
}

간단하네요. Bag도 추가해봐야겠습니다.



top

Write a comment.


[회사일] 다대다 관계 서브 그리드 CRUD 완성

프로젝트/SLT : 2010.06.15 14:17



팝업은 팬시박스를 사용했습니다. 이게 저번에 선택했던 컬러 머시기 보다 더 깔끔하더군요.

http://fancybox.net/home

jqGrid 다중 선택 기능을 사용했는데 아주 깔끔합니다. 그리드도 잘 선택한것 같아요.

URL은 RESTful 하게 만든다고 만들어 봤는데..

    @RequestMapping(value="/{id}/rights", method = RequestMethod.GET)
    public void rights(@PathVariable Integer id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.POST)
    public @ResponseBody String addRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.addRightsToMember(id, ids);
        return "success";
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.DELETE)
    public @ResponseBody String deleteRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.deleteRightsFromMember(id, ids);
        return "success";
    }

잘 동작합니다. 히든 필드 이용해서 _method에 원하는 RequestMethod 실어서 보내주면 되니깐 아주 간단하네요. HTML5 부터는 이런일 안해도 될려나.. 근데 HTML5 지원하는 브라우저가 있어도 그걸 써야 말이죠;;

지금 상태에서 한가지 굉장히 맘이 불편한 코드가 있는데.. 바로 서비스 코드입니다.

    List<Right> rightListOfMember(Integer memberId, PageParam pageParam);

    void addRightsToMember(Integer memberId, Integer[] rightsIds);

    void deleteRightsFromMember(Integer memberId, Integer[] rightsIds);

별로 객체지향적으로 보이지 않아요.. Request Parameter Oriented 프로그래밍 같지 않나요.@PathVariable로 넘어오는 값에도 분명히 바인딩이 적용될텐데... 컨트롤러에서부터 Member 타입으로 바인딩해서 가져오면 저런 코드가..

    List<Right> rightListOfMember(Member member, PageParam pageParam);

    void addRightsToMember(Member member, Right[] rights);

    void deleteRightsFromMember(Member member, Right[] rights);

이렇게 될텐데 말이죠. 흠.. 뭐좀 먹고 와서 해봐야지.

top

Write a comment.


[회사일] 서브 그리드 뿌리기

프로젝트/SLT : 2010.06.14 18:53



계정 정보 뷰 화면에 권한 정보를 뿌리도록 서브 그리드를 추가합니다.

화면 코드에서는 jqGrid를 사용해서 코드를 작성합니다.

    <p class="ui-widget-content">
        <h2 class="smdis-pane-title" id="smdis-grid-title">
            <ul class="smdis-left-icons">
                <li id="smdis-add-button" class="ui-state-default ui-corner-all" title="권한 추가"><span class="ui-icon ui-icon-plusthick"></span></li>
                <li id="smdis-delete-refresh" class="ui-state-default ui-corner-all" title="권한 삭제"><span class="ui-icon ui-icon-minusthick"></span></li>
            </ul>
            ${model.loginId} 권한 정보
        </h2>
        <table id="smdis-sub-grid"></table>
        <div id="smdis-sub-pager"></div>
    </p>
    <script type="text/javascript">
        $(function(){
            $("#smdis-sub-grid").jqGrid({
                colNames:['id', '이름', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ],
                pager: '#smdis-sub-pager',
                url:'${baseUrl}/${model.id}/rights'
            });
        });
    </script>

빨간 부분을 그리는데 이런 코드가 필요했습니다. 아직 버튼에 이벤트를 안달아서;; 좀 더 코드가 늘것 같네요. 일단은 서브 그리드를 그리는 기능만 만들겠습니다.

중요한 저 그리드에서 사용할 URL 핸들러를 만드는 거죠.

@Controller
@RequestMapping("/system/member")
public class MemberController extends GenericController<MemberService, MemberRef, Member, MemberSearchParam> {

    @RequestMapping("/{id}/rights")
    public void rights(@PathVariable int id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

}

MemberController에 핸들러를 추가합니다. 평범해 보이지만 스프링 3.0 컨텐츠 네고 기능을 타고 JSON으로 바껴서 보내지게 됩니다. (물론 브라우저 주소창에 저 URL을 입력하면 JSP 페이지를 찾다가 원하는 페이지가 없다고 404 에러 코드가 보여지겠죠.)

public interface MemberService extends GenericService<Member, MemberSearchParam>{

    List<Right> rightListOfMember(int id, PageParam pageParam);
}

MemberService 인터페이스를 타고.. 

@Service
@Transactional
public class MemberServiceImpl extends GenericServiceImpl<MemberDao, Member, MemberSearchParam> implements MemberService {

    @Autowired RightService rightService;

    @Override
    public List<Right> rightListOfMember(int id, PageParam pageParam) {
        Member member = dao.getById(id);
        return rightService.listOfMember(member, pageParam);
    }
}

MemberServiceImpl에 와서.. id에 해당하는 Member를 꺼내주고 나머진 RightService에 위임합니다.

public interface RightService extends GenericService<Right, RightSearchParam>{
    List<Right> listOfMember(Member member, PageParam pageParam);
}

그럼 RightService 인터페이스를 타고..

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

    public List<Right> listOfMember(Member member, PageParam pageParam) {
        pageParam.initCurrentPageInfosWith(member.getRights().size());
        return dao.listOf(member, pageParam);
    }
}

RightServcieImpl에서는 PageParam 값을 설정하고, DB에 다녀와야 할 일은 RightDao로 위임합니다.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
    List<Right> listOf(Member member, PageParam pageParam);
}

그럼 RightDao 인터페이스를 타고..

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }

    public List<Right> listOf(Member member, PageParam pageParam) {
        Criteria c = getCriteriaOf(Right.class);
        c.createAlias("members", "m");
        c.add(Restrictions.eq("m.id", member.getId()));

        applyPagingParam(c, pageParam);
        applyOrderingParam(c, pageParam);
        
        return c.list();
    }
}

여기서 member가 가지고 있는 권한 목록을 가져옵니다.

public class RightDaoImplTest extends SpringTest {

    @Autowired RightDao rightDao;

    @Test
    public void di(){
        assertThat(rightDao, is(notNullValue()));
    }

    @Test
    public void listOf() throws Exception {
        insertXmlData("testData.xml");

        Member member = new Member();
        member.setId(1);

        PageParam pageParam = new PageParam();
        pageParam.setRows(20);
        pageParam.setPage(0);

        List<Right> rights = rightDao.listOf(member, pageParam);
        assertThat(rights.size(), is(2));

        Right right = (Right)rights.get(0);
        assertThat(right.getId(), is(1));
        assertThat(right.getName(), is("admin"));
    }

}

<dataset>
<member id="1" loginid="whiteship" password="1" email="email@com" name="기선" />

    <rights id="1" name="admin"/>
    <rights id="2" name="user"/>

    <member_rights members_id="1" rights_id="1"/>
    <member_rights members_id="1" rights_id="2"/>
</dataset>

DAO 테스트에서는 원하는 Right를 가져오는지 확인합니다.

    @Test
    public void listOfMember() throws Exception {
        insertXmlData("testData.xml");
        Member member = memberDao.getById(1);

        PageParam pageParam = new PageParam();
        pageParam.setPage(0);
        pageParam.setRows(20);

        List<Right> rs = rightService.listOfMember(member, pageParam);

        assertThat(pageParam.getListSize(), is(2));
        assertThat(pageParam.getTotalPageSize(), is(1));
    }

서비스 테스트에서는 PageParam에 값이 잘 설정되는지 확인했습니다.

이제 잘 될 줄 알고 서버를 켜고 돌려봤더니.. 이런;;;


이런 문제가 생기더군요. 물론 그리드에도 제대로 표시가 되지 않았습니다.

Member <--> Right 양방향 관계로 설정했을 때 쌍방에서 가지고 있는 rights나 members가 비어있더라도 JSON 뷰로 넘어갔을 때 OSIV 필터에 의해서 무조건 값을 읽어오게 됩니다. 실제로 해당 레퍼런스가 아주 null이었으면 가져오려는 시도도 안할텐데 null도 아닙니다. Member나 Right에서 초기화 해준적도 없는데도 null이 아닙니다. 그건 하이버네이트 Lazy loading 하려고 프록시 객체를 만들어 놔서 그런것 같더군요. 어떻게 막아야 하나... 고민을 했습니다.

프로젝션과 DTO로 해결하려고 했으나.. 실패. 

흠.. JSON으로 만들지 않았으면 좋겠는데.. 그런거 없나 찾아보다 @JsonIgnore 발견.
그걸 적용해서 성공했습니다.


휴.. 집에가야지
top

Write a comment.


[회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지

프로젝트/SLT : 2010.06.14 16:21


@Entity(name="rights")
public class Right {

    @Id
@GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(length = 100, unique = true)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    private String name;

    @Column
    private String descr;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescr() {
        return descr;
    }

    public void setDescr(String descr) {
        this.descr = descr;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Right right = (Right) o;

        if (name != null ? !name.equals(right.name) : right.name != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

필드 몇개 추가하고 JPA 애노테이션 추가한 다음 Member 쪽에 Set 타입으로 @ManyToMany 연결.
이때 right는 DB 예약어일 수 있으니 rights라는 이름을 사용하도록 설정 해줌. 
IDEA가 코드 생성 메뉴 상숑해서 hashCode랑 equals 구현. 이때 name 필드만 사용.

자 이제 GenericXXX를 이용해서 CRUD 코드 5분 완성.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
}

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }
    
}

public interface RightService extends GenericService<Right, RightSearchParam>{
}

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

}

@Controller
@RequestMapping("/system/right")
public class RightController extends GenericController<RightService, RightRef, Right, RightSearchParam> {
}

@Component
public class RightRef {
}

public class RightSearchParam {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

이걸로 서버쪽 코딩 끝. 이제 화면 코드는 태그 파일 활용해서 역시 5분 완성.

<page:editpage label="권한 수정">
    <f:input label="권한" path="name" />
    <f:textarea label="설명" path="descr" />
</page:editpage>

<page:mgtpage label="권한 관리">
    <page:search label="권한 검색">
        <s:input label="이름" path="name" />
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '권한명', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ]
            });
        });
    </script>
</page:mgtpage>

<page:newpage label="새 권한">
    <f:input label="이름" path="name"/>
    <f:textarea label="설명" path="descr" />
</page:newpage>

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="설명" value="${model.descr}"/>
</page:viewpage>

끝..


도메인 코딩 5분, 서버 코딩 5분, 화면 코딩 5분...

초간단 CRUD 구현하는데 15분.













top

Write a comment.


[회사일] DateRange 추가

프로젝트/SLT : 2010.06.14 14:33



오른쪽에 보이는 검색 필드를 MemberSearchingParam에 추가하는데 아무래도 이런 범위 검색 필드는 자주 사용될 것 같더군요.

그래서 별도의 클래스로 분리했습니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...
}

나머진 요다(?) 조다(?) 라이브러리만 넣어두면 기본 포매터가 동작해서 잘 바인딩 해 줍니다. 아.. 암튼 이번엔 바인딩 얘기가 아니라..  저 클래스 DateRange 얘기입니다.

MemberSearchParam을 MemberDao코드에 적용합니다.

@Repository
public class MemberDaoImpl extends GenericDaoImpl<Member, MemberSearchParam> implements MemberDao {

    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "loginId", searchParam.getLoginId());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalLike(c, "email", searchParam.getEmail());
        CriteriaUtils.addOptionalLike(c, "phoneNumber", searchParam.getPhoneNumber());
        CriteriaUtils.addOptionalLike(c, "jobTiTle", searchParam.getJobTitle());
        CriteriaUtils.addOptionaBetween(c, "birthDay", searchParam.getBirthDayRange());
    }
}

이게 DAO 코드 전부입니다.

유틸 코드로 가야겠네요.

 
   @Test
    public void testOptionalBetween() {
        String dateRangeSearchingFiled = "birthDay";

        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        DateRange dateRange = new DateRange();
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        dateRange.setFrom(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.ge(any(String.class), dateRange.getFrom()));

        reset(c);
        dateRange.setTo(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.between(any(String.class), dateRange.getFrom(), dateRange.getTo()));

        reset(c);
        dateRange.setFrom(null);
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.le(any(String.class), dateRange.getTo()));
    }

장황해 보이지만 별거 없는 테스트 입니다.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.getFrom() != null && dateRange.getTo() != null){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.getFrom() != null && dateRange.getTo() == null){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.getFrom() == null && dateRange.getTo() != null){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

이 코드를 테스트하고 있죠. 그런데 코드가 좀;; Don't Tell, Ask가 생각나더군요. 저런 조건 문을 inline 뭐시기 리팩토링으로 분리할 수도 있지만 그보다 일단 if 조건문에 들어있는 내용이 들어있어야 할 위치가... DateRange가 되어야 할 것 같단 생각이 듭니다.

머 일단 if안에 있는 조건문 들을 extract method 리팩토링으로 빼냅니다. 이런 리팩토링을 인라인 머시기 리팩토링이라고 했던것 같은데 잊어버렸네요. @_@;;

 
   public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

    private static boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    private static boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    private static boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }

그럼 이렇게 되는데 여기서 아래 세개의 static 메서드를 DateRange 쪽 멤버 메서드로 옯겨줍니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...

    public boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    public boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    public boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }
}

그리고 CriteriaUtils 코드를 수정해주면 되죠.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

마지막으로 테스트 한번 돌려주면 깔끔하게 끝~









top

TAG DateRange

Write a comment.


[회사일] view.jsp 태그파일 적용

프로젝트/SLT : 2010.06.14 12:21


member/view.jsp

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="로그인ID" value="${model.loginId}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="이메일" value="${model.email}"/>
</page:viewpage>

code/view.jsp

<page:viewpage label="코드 정보">
    <v:text label="id" value="${model.id}" />
    <v:text label="코드종류" value="${model.cate.descr}" />
    <v:text label="코드값" value="${model.code}" />
    <v:text label="코드명" value="${model.name}" />
    <v:text label="설명" value="${model.descr}" />
</page:viewpage>

흠냐.. 뷰용 태그 파일 네임스페이스는 v로 정했습니다.

01


자 거럼 이제 찍어낼 준비가 거의 다 된 것 같군요... 흠.. 아니군요. 아직 갈길이 멀군요.
일단 Member에 Date 타입 필드 부터 추가해봐야겠습니다.

다음에는 Right 도메인을 찍어내고, Member랑 다대다 관계 연결해서 뷰까지 또 프레임워크성 코드를 빼내야 하고...

그담엔 시큐리티 적용하고..

그담엔 자동완성 만들고.. 

엑셀 다운/업로드 만들고..

멀구나 멀어..
top

Write a comment.


[회사일] new.jsp 태그파일 적용

프로젝트/SLT : 2010.06.14 11:54


code/new.jsp

<page:newpage label="새 코드">
    <f:select label="코드종류" path="cate" items="${ref.codeCateList}" />
    <f:input label="코드값" path="code" />
    <f:input label="코드명" path="name" />
    <f:textarea label="설명" path="descr" />
</page:newpage>

member/new.jsp

<page:newpage label="새 계정">
    <f:input label="로그인 ID" path="loginId"/>
    <f:input label="비밀번호" path="password"/>
    <f:input label="이름" path="name"/>
    <f:input label="이메일" path="email"/>
</page:newpage>

01


new.jsp 페이지는 edit.jsp랑 비슷했으니까 간단간단 이제 view.jsp 페이지만 남았군요.
top

Write a comment.


[회사일] mgt.jsp 파일에 태그 파일 적용

프로젝트/SLT : 2010.06.14 11:38



가운데는 그리드가 와야하고 오른쪽에는 검색 화면이 나와야하는 페이지. 이 페이지에 태그 파일을 적용해서...

<page:mgtpage label="계정 관리">
    <page:search label="계정 검색">
        <s:input label="이름" path="name" />
        <s:input label="이메일" path="email" />
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '아이디', '이름', '이메일'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'loginId', index:'loginId', width:80},
                    {name:'name', index:'name', width:90},
                    {name:'email', index:'email', width:90},
                ]
            });
        });
    </script>
</page:mgtpage>

이런식으로 만들었습니다. 딱 봐도.. 검색 부분에 머머가 있구나.. 그리드에는 머머가 있구나가 보이니깐 이정도면 만족합니다. 태그 파일을 계속 써보니까 그 장점 중 하나가 태그에 좀 더 의미있는 이름을 줄 수 있다는 것이 있더라구요. 대신 단점으로는 이 태그 파일만 가지고 테스트 할 수 없다는게 좀 안타깝습니다. 계속해서 서버를 띄워둔 상태에서 릴로딩 해가면서 확인하고 있는데.. 아마 프리마커 같은 템플릿 엔진의 장점은 바로 테스트 용이성이 아닐까 싶습니다.

머 어쨋든 지금은 일단 JSP + 태그파일로 후딱 만들기로 했습니다. 
나중에 프리마커 + 매크로를 사용해서 대체해보는 방법도 생각해볼 수 있겠지만.. 머 나중에;

태그 파일을 분류해서 page는 페이지 구조와 관련된 태그를 만들고..
s는 검색에 사용할 폼 태그들을 만들고..
f는 수정이나 추가할 때 사용할 폼 태그들을 만들기로 했습니다.
 
top

Write a comment.


[회사일] edit.jsp 화면에 태그 파일 적용하기

프로젝트/SLT : 2010.06.11 14:05


먼저 Code 도메인의 edit.jsp 코드를 보겠습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <div class="ui-layout-center">
            <h2 class="smdis-pane-title" id="smdis-grid-title">
                <ul class="smdis-left-icons">
                    <li id="smdis-save-button" class="ui-state-default ui-corner-all" title="저장"><span class="ui-icon ui-icon-disk"></span></li>
                    <li id="smdis-delete-button" class="ui-state-default ui-corner-all" title="삭제"><span class="ui-icon ui-icon-trash"></span></li>
                    <li id="smdis-reload-button" class="ui-state-default ui-corner-all" title="원래값으로"><span class="ui-icon ui-icon-refresh"></span></li>
                </ul>
                <ul class="smdis-right-icons">
                    <li id="smdis-list-button" class="ui-state-default ui-corner-all" title="목록으로"><span class="ui-icon ui-icon-search"></span></li>
                    <li id="smdis-back-button" class="ui-state-default ui-corner-all" title="뒤로"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
                </ul>
                코드 수정
            </h2>
            <form:form commandName="model" action="/base/code/${model.id}" method="PUT">
                <p class="ui-widget-content"><label>코드종류</label><form:select path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" /><form:errors path="cate" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>코드값</label><form:input path="code"/><form:errors path="code" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>코드명</label><form:input path="name"/><form:errors path="name" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>설명</label><form:textarea path="descr"/></p>
            </form:form>
        </div>

        <%--//검색/정렬--%>
        <div class="ui-layout-east">
            <h2 class="smdis-pane-title">
                <ul class="smdis-left-icons">
                    <li id="smdis-grid-apply" class="ui-state-default ui-corner-all" title="적용"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
                </ul>
                <ul class="smdis-right-icons">
                    <li class="ui-state-default ui-corner-all" title="적용하고 닫기"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
                </ul>
                검색
            </h2>
        </div>

    <script type="text/javascript">
        $(function() {
            $("button").button();

            $("#smdis-save-button").click(function(){
                var result = confirm("저장 하시겠습니까?");
                if(!result) return;

                $("form input[name='_method']").attr("value", "PUT");
                $("form").submit();
            });

            $("#smdis-delete-button").click(function(){
                var result = confirm("삭제 하시겠습니까?");
                if(!result) return;
                
                $("form input[name='_method']").attr("value", "DELETE");
                $("form").submit();
            });

            $("#smdis-reload-button").click(function(){
                var result = confirm("변경 사항을 취소 하시겠습니까?");
                if(!result) return;

                $(document).attr("location", '/base/code/${model.id}/form');
            });

            $("#smdis-back-button").click(function(){
                $(document).attr("location", '/base/code/${model.id}');
            });

            $("#smdis-list-button").click(function(){
                $(document).attr("location", '/base/code/mgt');    
            });
        });
    </script>
    </page:body>
</page:page>

일부는 page라는 태그파일을 적용해서 코드를 많이 줄여놨습니다. 그래도 너무 길고.. Member의 edit.jsp 파일을 만들 때 손대는 곳은 굵은 글씨 밖에 없습니다. 신경쓸 곳만 신경쓰게 만들고 나머진 신경쓰고 싶지도 않고 보고 싶지도 않습니다. 그리고 위와 같은 코드가 여러 JSP에 중복되면.. 결국 자바 중복 코드랑 다를께 없습니다. 화면 코드에서도 중복을 제거해야 합니다.

Member 도메인의 edit.jsp 파일은 결국 다음과 같이 바꼈습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="계정 수정">
            <osaf:input label="로그인ID" path="loginId"/>
            <osaf:input label="비밀번호" path="password"/>
            <osaf:input label="이름" path="name"/>
            <osaf:input label="이메일" path="email"/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

정말 이게 전부입니다. 잡습 코드도 잡습이 적용될 코드가 들어있는 태그 파일로 넣어놨습니다. Code의 edit.jsp 파일에도 태그 파일을 적용하면..

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="코드 수정">
            <osaf:select label="코드종류" path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" />
            <osaf:input label="코드값" path="code"/>
            <osaf:input label="코드명" path="name"/>
            <osaf:textarea label="설명" path="descr"/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

이렇게 됩니다. 사실 저기서 위아래 세줄 정도도 관심 대상이 아니라서 page:editpage 같은 태그 파일을 만들어 줄일 수도 있습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:editpage label="코드 수정">
    <osaf:select label="코드종류" path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" />
    <osaf:input label="코드값" path="code"/>
    <osaf:input label="코드명" path="name"/>
    <osaf:textarea label="설명" path="descr"/>
</page:editpage>

좋아 좋아!!! 이제부터는 이렇게 딱 다섯줄만 코딩하면...


이런 화면을 만들 수 있는 겁니다.

이대로 끝내면 아쉬우니까 태그파일 코드 몇개만 공개하겠습니다.

WEB-INF/tags/page/editpage.tag

<%@ tag pageEncoding="utf-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ attribute name="label" required="true"%>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="${label}">
            <jsp:doBody/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

WEB-INF/tags/osaf/select.tag

<%@ tag pageEncoding="utf-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ attribute name="label" required="true"%>
<%@ attribute name="path" required="true"%>
<%@ attribute name="items" type="java.util.List" required="true"%>
<%@ attribute name="itemValue" required="false"%>
<%@ attribute name="itemLabel" required="false"%>

<p class="ui-widget-content">
    <label>${label}</label>
    <form:select path="${path}" items="${items}" itemValue="${itemValue}" itemLabel="${itemLabel}" />
    <form:errors path="${path}" cssClass="smdis-error-message"/>
</p>
top

Write a comment.


[회사일] GenericController 만들기

프로젝트/SLT : 2010.06.11 12:46


CodeController에서 구체적인 정보는 최대한 추상화 시킵니다. 예를 들어 "code" 같은 문자열은 별로 좋치 않습니다. 이걸 차라리 "model"이라는 추상적인 이름으로 바꾸면 다른 도메인에서도 사용하기 좋을 겁니다. 애초에 문자열 자체가 그다지 추상화는데 좋치 않긴 하지만 그렇게라도 해야지요. ㅋ

그렇게 해서 일단 추상화 시킨 코드가 이렇습니다.

@Controller
@RequestMapping("/base/code")
@SessionAttributes("model")
public class CodeController {

    @Autowired CodeService codeService;
    @Autowired CodeRef ref;
    String baseUrl;

    public CodeController() {
        RequestMapping rm = this.getClass().getAnnotation(RequestMapping.class);
this.baseUrl = rm.value()[0];
    }

    @ModelAttribute("ref")
public CodeRef ref() {
return ref;
}

    @RequestMapping(value="/mgt")
    public void mgt(Model model){
        model.addAttribute("searchParam", new CodeSearchParam());
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("list", codeService.list(pageParam, searchParam));
    }

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String addForm(Model model){
        model.addAttribute("model", new Code());
        return baseUrl + "/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid @ModelAttribute("model") Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/new";
        status.setComplete();
        codeService.add(code);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("model", codeService.getById(id));
        return baseUrl + "/edit";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid @ModelAttribute("model") Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/edit";
        status.setComplete();
        codeService.update(code);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        codeService.deleteBy(id);
        return  "redirect:" + baseUrl + "/mgt";
    }


    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String view(@PathVariable int id, Model model){
        model.addAttribute("model", codeService.getById(id));
        return baseUrl + "/view";
    }

}

이전 코드에 비해서 달라진게 별로 없지만.. 일단은 baseUrl 속성을 둬서 @RequestMapping에 넣어주는 값을 읽어서 사용하도록 코드를 수정했습니다. 그리고 "code"를 "model"로 바꿨고, "codeList"를 "list"로 바꿨습니다. "ref"애초부터 추상화 시킨 이름을 사용했으니 손댈 필요가 없었습니다.

다음에 할 작업은 화면과 연결하는 겁니다. 화면에 전달하는 객체 이름이 바꼈으니 화면에서 EL 부분을 손봐줍니다.

다음은 GenericControlle을 만듭니다.

@SessionAttributes("model")
public abstract class GenericController<GS extends GenericService<E, S>, R, E, S> {

    @Autowired ApplicationContext applicationContext;

    GS service;
    R ref;

    Class<GS> serviceClass;
    Class<R> refClass;
    Class<E> entityClass;
    Class<S> searchParamClass;

    protected String baseUrl;

    public GenericController() {
        RequestMapping rm = this.getClass().getAnnotation(RequestMapping.class);
this.baseUrl = rm.value()[0];

        this.serviceClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 0);
        this.refClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 1);
        this.entityClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 2);
        this.searchParamClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 3);
    }

    @ModelAttribute("ref")
public R ref() {
return ref;
}

    @RequestMapping(value="/mgt")
    public void mgt(Model model){
        try {
            model.addAttribute("searchParam", searchParamClass.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, S searchParam, PageParam pageParam) {
        model.addAttribute("list", service.list(pageParam, searchParam));
    }

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String addForm(Model model){
        try {
            model.addAttribute("model", entityClass.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return baseUrl + "/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid @ModelAttribute("model") E e, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/new";
        status.setComplete();
        service.add(e);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("model", service.getById(id));
        return baseUrl + "/edit";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid @ModelAttribute("model") E e, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/edit";
        status.setComplete();
        service.update(e);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        service.deleteBy(id);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String view(@PathVariable int id, Model model){
        model.addAttribute("model", service.getById(id));
        return baseUrl + "/view";
    }

    @PostConstruct
    public void setUp(){
        this.service = applicationContext.getBean(serviceClass);
        this.ref = applicationContext.getBean(refClass);
    }

}

이전 글에서 만든 GenericUtils를 이용해서 타입 추론을 하고 그렇게 알아낸 타입을 사용해서 new Code() 하던 부분은 codeClass.newInstance()로 바꾸고, GS service = applicationContext.getBean(serviceClass) 이렇게 applicationContext에서 Class 타입으로 빈을 가져올 때 사용합니다.

필요한 타입은 4개 GenericService, Ref, Entity, SearchParam
Code 도메인 기준으로는 : CodeService, COdeRef, Code, CodeSearchParam이 필요합니다.

자 이제 CodeController 코드를 GenericController를 사용하도록 수정합니다.

@Controller
@RequestMapping("/base/code")
public class CodeController extends GenericController<CodeService, CodeRef, Code, CodeSearchParam> {

}

끝입니다. 맨 위에 있는 CodeController 코드와 비교하면.. 뭐. 거의 10배는 코드량을 줄인것 같네요. 이제 부터 찍어내는 일만... 아... 아니군요;;

뷰 코드까지 정리해야 찍어낼 수 있습니다. 컨트롤러까지만 만들면 뭐하나요. 화면이 안나오는데.. OTL...


top

Write a comment.


[회사일] Generic 타입 추론 유틸 만들기

프로젝트/SLT : 2010.06.11 12:23


@Transactional
public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S> {

    @Autowired ApplicationContext applicationContext;
    private Class<D> daoClass;
    protected D dao;

    public GenericServiceImpl(){
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.daoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.daoClass = (Class<D>) type;
        }
    }

...
}

지난 번에 작성했던 GenericService 코드입니다. 여기서 저 가운데 부분이 GenericDao나 GenericController에서 사용될 가능성이 높습니다. 사실 지금 GenericController를 작성하던 중이었는데 타입 추론할께 하두 많아서;; @_@  유틸로 빼고 있습니다.

구현은 간단합니다.

public class GenericUtils {

    public static Class getClassOfGenericTypeIn(Class clazz, int index){
        ParameterizedType genericSuperclass = (ParameterizedType) clazz.getGenericSuperclass();
        Type wantedClassType = genericSuperclass.getActualTypeArguments()[index];
        if (wantedClassType instanceof ParameterizedType) {
            return (Class) ((ParameterizedType) wantedClassType).getRawType();
        } else {
            return (Class) wantedClassType;
        }
    }

}

테스트도 해봐야겠죠.

public class GenericUtilsTest {

    @Test
    public void testGetClassOfGenericTypeIn() throws Exception {
        Class<String> stringClass = GenericUtils.getClassOfGenericTypeIn(SampleClass.class, 0);
        assertThat(stringClass.toString(), is("class java.lang.String"));

        Class<Map> mapClass = GenericUtils.getClassOfGenericTypeIn(SampleClass.class, 1);
        assertThat(mapClass.toString(), is("interface java.util.Map"));
    }

    class SampleGenericClass<S, M>{}
    class SampleClass extends SampleGenericClass<String, Map> {}
}

잘 되는군요. GenericController 마무리하러 가야겠습니다.


top

Write a comment.


[회사일] GenericService 버그 수정하기

프로젝트/SLT : 2010.06.09 17:45


앞에서 만들었던 GenericService에는 버그가 있었습니다. 저걸 적용한 뒤에 애플리케이션을 실행해보니까 제대로 동작하지 않더군요. 제길... 테스트를 만들껄.. 후회했습니다.  대부분의 코드가 단순 위임이라고 해서 테스트를 무시하면 안됩니다. 사실 Service 코드에서 하는 일은 DAO로 단순 위임하는 코드밖에 없어 보이지만 그 이상으로 복잡합니다.

그 중 하나가 DAO를 주입 받는 일이죠. '주입 하는 일'도 아니고 '주입 받는 일'을 무시했네요. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class CodeServiceImplTest {

    @Autowired CodeService codeService;

    @Test
    public void di(){
        assertThat(codeService, is(notNullValue()));
    }

}

초간단 테스트를 만듭니다. codeService 자체를 DI 받을 수 있는지... 빈 팩토리에서 저 빈을 만들 수 있는지 확인합니다.

에러가 납니다.. ㅋ

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [osaf.dao.GenericDao] is defined: expected single matching bean but found 2: [codeDaoImpl, memberDaoImpl]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:779)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:686)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:478)
... 43 common frames omitted

핵심적인 에러 메시지는 이 부분이다. 뭔 내용인지는 해설하지 않겠습니다. 퀴즈 삼아 맞춰보시죠.

public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S> {

    @Autowired ApplicationContext applicationContext;
    private Class<D> daoClass;

    GenericDao<E, S> dao;

    public GenericServiceImpl(){
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.daoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.daoClass = (Class<D>) type;
        }
    }

    public void add(E e) {
        dao.add(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        pageParam.initCurrentPageInfosWith(dao.totalSize(s));
        return dao.list(pageParam, s);
    }

    public E getById(int id) {
        return dao.getById(id);
    }

    public void update(E e) {
        dao.update(e);
    }

    public void deleteBy(int id) {
        dao.deleteBy(id);
    }

    @PostConstruct
    public void setUpDao(){
        this.dao = this.applicationContext.getBean(daoClass);
    }
}

GenericService 구현체를 위와같이 수정했습니다. dao를 생성자에서 바로 연결하지 않고 @PostConstruct에서 연결한 것도 설명하지 않겠습니다. 요것도 심심하신 분 계시면 퀴즈 삼아 맞춰보시기 바랍니다.

퀴즈1. 에러가 난 원인은?
퀴즈2. 왜 생성자에서 applicationContext.getBean(daoClass)를 하면 안된느가?

top

  1. 심상호 2010.06.10 14:52 PERM. MOD/DEL REPLY

    1. attribute에 @Autowired가 걸려있는 경우 Class만 보고 Injection할 Type을 가져오는데 attribute는 GenericServiceImpl Class에 선언되어 있기 때문에 GenericDao Type을 Injection하려고 해서 에러가 발생한 것입니다.

    2. 생성자에서 할 경우 depends-on 설정이 없기 때문에 Injeciton할 Dao가 생성이 안되어 있을 수 있겠네요.

    그리고 기존의 GenericServiceImpl에서 Dao에 대한 set method를 생성하고 @Autowired를 attribute가 아닌 set method에 설정한 경우 정상적으로 동작합니다.

    Favicon of http://whiteship.me BlogIcon 기선 2010.06.10 16:25 PERM MOD/DEL

    1. 맞습니다. GenericDao 타입 빈을 주입하려다 보니 그 타입 빈이 두개 이상여서 그 중에 뭘 넣어야 할지 몰라서 에러가 났습니다.

    2. 안타깝지만 틀리셨습니다. depends-on이나 dao 객체보단 일단 생성자 내부에서는 applicationContext가 null이기 때문에 뭘 가져올수가 없습니다.

  2. 심상호 2010.06.10 16:36 PERM. MOD/DEL REPLY

    아. 그러네요. -_ -;;;

Write a comment.


[회사일] MemberService 만들기. GenericService 만들기

프로젝트/SLT : 2010.06.09 16:50


이번엔 바로 GenericService 인터페이스부터 만들죠.

public interface GenericService<E, S> {

    void add(E e);

    List<E> list(PageParam pageParam, S s);

    E getById(int id);

    void update(E e);

    void deleteBy(int id);
    
}

간단합니다. GenericDao랑 비슷하죠. 이 구현체도 간단합니다. 오히려 GenericDaoImpl 보다 훨씬더..

public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S>{

    @Autowired protected D dao;

    public void add(E e) {
        dao.add(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        pageParam.initCurrentPageInfosWith(dao.totalSize(s));
        return dao.list(pageParam, s);
    }

    public E getById(int id) {
        return dao.getById(id);
    }

    public void update(E e) {
        dao.update(e);
    }

    public void deleteBy(int id) {
        dao.deleteBy(id);
    }
    
}

유일하게 복잡한 부분이 저 위에 Generic 타입 선언한 부분인데.. GenericDao에 있는 메서드를 사용하려면 D라는 타입이 GenericDao를 확장한 녀석이라는걸 알려줘야 합니다. 그리고 GenericDAO에 넘겨주는 타입이 GenericService에서 사용할 타입과 같다는것도 알려줘야 해서 저렇게 복잡해졌습니다. 만야게 GenericDao 뒤에 붙인 <E, S>를 때어내면 dao.getById(id) 부분에서 타입 불일치 때문에 컴파일 에러가 떨어집니다.ㅋ

public interface MemberService extends GenericService<Member, MemberSearchParam>{

}

public class MemberServiceImpl extends GenericServiceImpl<MemberDao, Member, MemberSearchParam> implements MemberService{
    
}

위에서 만든걸 이용해서 만든 MemberService와 MemberServiceImpl 입니다. 캬.. 간단하군요.






top

  1. 둥이아빠 2010.06.09 19:58 PERM. MOD/DEL REPLY

    저번에 AspectJ 관련 답변 감사드립니다^^

    저도 이번에 프로젝트를 진행하면서 GenericService, GenericDao 구현해 진행하고 있는데요
    Hibernate가 아닌 iBATIS에 물려서 진행하고 있습니다.

    하나 궁금한게 있는데요...

    Domain 설계 하실때...
    Master 화면들은 거의 Table 하나와 매핑 되다 보니 Domain이 CRUD를 적용하기에 명확한데요.
    조금 복잡한 화면들의 경우 Domain 자체가 복잡해 지다 보니 CRUD를 적용하기에 애매한 부분들이 있는것 같습니다.

    프로젝 하실때 그런 복잡한 화면들은 어떤식으로 처리하시는지 궁금합니다.
    예를 들어, 조금 간단한 구조로 화면에 Header에 해당하는 Form과 하단에 Detail 정보를 담을 Grid가 있을 경우 Header, Detail에 해당하는 Domain 구조를 설계하고 (예를 들어 Header Domain에 Detail Domain을 List로 구성하고..)
    해당 Domain과 동일한 구조의 ResultMap (ibatis)을 작성한뒤 한번에 조회해와서 셋팅하도록 하시는지,
    아니면 Header와 Detail을 별개의 Domain으로 설계한 후에 따로 조회를 하시는지 궁금하네요..^^;;

    그리고, 조금 더 복잡하게 Header에 여러 Master Domain들이 구성될 경우, 그런경우는 어떻게 처리하시는지도 궁금하구요..^^;;

    너무 질문이 장황한 것 같은데...
    이번에도 좋은 답변 부탁드리겠습니다^^

    Favicon of http://whiteship.me BlogIcon 기선 2010.06.09 21:04 PERM MOD/DEL

    봄싹 사이트에서 My Page 같은 경우는 여러 도메인 정보를 가져와서 한 화면에 보여줘야 했었는데요.

    MyPageController를 만들고 거기서 MemberService, StudyService 등을 사용해서 원하는 모델 객체들을 가져온다음 화면으로 전달할 ModelMap에 넣어줬습니다.

    그리고 화면에선 그냥 알아서 참조해서 EL로 뿌렸죠.

    화면당 도메인이 꼭 1:1로 매핑되야 하는건 아니고, Generic으로 빼낼 수 있는 기능들도 제한이 있으니 이럴땐 GenericService, GenericDao를 상속해서 만든 클래스와 인터페이스에 추가적인 기능이 들어갈 수도 있겠습니다.

    GenericXXX 들은 코드 중복을 줄이고 확장을 편하게 하기 위함이지 애플리케이션 아키텍처를 단순화하려는 용도는 아니기 때문에 화면에 보여줄 도메인 객체가 여러개 라고 해서 별로 문제될 것은 없을 것 같습니다. 흠냐..

  2. 둥이아빠 2010.06.09 22:25 PERM. MOD/DEL REPLY

    우선, 친절한 답변 감사드립니다.

    아.. MemberService, StudyService가 Member, Study 라는 도메인을 가지고 Controller에서 해당 도메인들을 받아 Map에 넣어서 전달한 케이스군요...

    음..
    Controller에서의 조합은 알겠는데요...
    Service가 가지는 Domain 자체가 복잡도를 가질 경우 그런 케이스를 CRUD로 구성하시는지가 궁금합니다.

    예를 들어, "구매요청" 이라는 도메인을 설계한다고 할때...
    구매요청은 말 그대로 구매요청이라는 Transaction이 있을 수 있고 등록된 구매요청 정보를 조회하는 프로세스가 있을수 있을 텐데요....
    구매요청 도메인은 예산, 업체, 결재 등의 Master Domain들을 구성하고 구매요청할 자재정보인 자재 Master Domain을 List로 구성하는 형태로 설계한다면...
    이런 복잡도를 가진 Domain을 CRUD로 구성을 하시는지 아니면 다른 방법으로 해결을 하시는지가 궁금합니다^^;;;
    Header, Detail 정보를 하나의 Domain으로 설계하는거죠...

    이번에도 좋은 답변 부탁드리겠습니다^^

    Favicon of http://whiteship.me BlogIcon 기선 2010.06.10 08:27 PERM MOD/DEL

    "구매요청"이 생기고, 수정되고, 삭제되고, 목록을 조회하고, 한건을 조회하는 등의 작업은 하실꺼자나요?

    그리고 그 "구매요청"과 관련이 있는 "예산", "업체" 등도 개별적으로 CRUD가 필요할꺼라고 생각합니다.

    "결제" 같은 경우는 (제가 생각하는것과 다를수도 있지만) '제출'된 상태인지, '반려' 됐는지, '결제' 됐는지 등 상태를 나타내기 때문에 여러 도메인에 공통으로 들어갈 수 있는 정보가 될 것 같습니다. 이런 경우는 별도로 CRUD성 DAO, Service, Controller를 만들지 않고 "결제Dao" "결제Service"같은 인터페이스를 만들고 다른 도메인의 DAO나 Service에서 추가로 구현하게 할 것 같습니다.

    Header와 Detail은 잘 안와 닿는 도메인 이름입니다. Header에 들어갈 정보가 form이라고 하셨는데 그렇다면 header쪽에는 지금 제가 회사에서 만들고 있는 화면 오른쪽에 있는 검색 화면이 될 것 같군요. 그건 하나의 도메인이라기 보단 검색용 보조 데이터입니다. 따라서 CodeSearchParam이라는 클래스를 code.support쪽에 만들어놨고 그걸 화면으로 전달해서 그안에 값을 바인딩 시켜서 가져왔습니다. 그리고 Details이라고 하신 부분은 그리드 같은데 제 회사일에도 그리드가 있죠. 그 부분은 보통 다른 도메인의 List를 뿌리는 것일테니까 그 도메인의 CRUD 중에서 R을 활용해서 스프링 ModelAndView 중에 Model에 넣어주었습니다.

    위에서 Map에 넣었다고 했는데 이게 SI에서 사용한다는(전 그쪽을 잘 모릅니다,) 그런 Map은 아니구요. ModelMap이라고 해서 key에 해당하는 값이 객체의 프로퍼티들이 아니라(그렇게 될 수도 있지만) 보통은 객체 자체 입니다.

    model.addAttribute("studyList", studyService.getList());
    model.addAttribute("wikiList", wikiService.getList());

    이런식으로 담아서 전달하는거죠. 아무리 화면이 복잡하더라도 그 화면에서 보여줄 데이터가 도메인 모델로 잘 나눠져 있다면 저런식으로 화면에서 참조할 객체들을 여러개 전달하면 되는것 뿐이죠.

    CRUD 처리도 필요한 도메인에만 만들어주면 될 뿐.. 어떤 도메인은 다른 도메인에 종속적이어서 DAO만 CRUD를 만들어 놓고 서비스나 컨트롤러는 만들지 않는것도 있습니다. 예를 들어 "연간 계획", "연간 계획 상세" 라는 도메인이 있었는데.. 연간 계획은 1년치를 총 집계해서 보는 도메인이고 연간계획상세는 그걸 달단위로 조금 상세하게 적어둔 것이었습니다. (월 게획은 또 별도로 있었는데 암튼..) 그래서 저런 경우에 "연간 계획"이 삭제되면 "연간 계획 상세"도 필요없었거든요. 그래서 "연간 계획 상세"는 DAO만 들어 놓고 나머진 "연간 계획"의 DAO, Service, Controller에서 "연간 계획 상세"의 CRUD도 같이 처리했었습니다.

    흠. 질문하신거에 대한 대답이 될지 어떨지 몰겠네요. 이게 뭐 어디 설명이 되어 있는 내용도 아니고 그냥 제가 일하는 방식이라; @_@

  3. 둥이아빠 2010.06.10 14:57 PERM. MOD/DEL REPLY

    답변 감사드립니다^^;

    방향 설정하는데 많은 도움이 되었습니다^^

    Favicon of http://whiteship.me BlogIcon 기선 2010.06.10 16:24 PERM MOD/DEL

    넵. 도움이 되셨다니;; 다행이네요.ㅋ

Write a comment.


[회사일] Member 추가. MemberDAO 구현. GenericDAO 구현

프로젝트/SLT : 2010.06.09 14:56


이제 Code 도메인 가지고 아주 기본적인 CRUD를 만들었으니 새로운 도메인을 하나 더 추가해서 중복코드를 잡아가면서 프레임워크를 뽑아내면 됩니다.

첫번쨰 도메인 CRUD를 만들때는 아키텍처 정하고 화면 구상하고 이것저것 플러그인 찾아보고 네비게이션 정하고 버튼 모양 덩덩 정하느라 시간이 많이 걸렸다면 이제부터는 중복코드와 싸움입니다.

먼저 Member 도메인을 추가합니다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    int id;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String loginId;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String password;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String name;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String email;
    @Column(length = 50)
    String phoneNumber;
    @Column(length = 50)
    String jobTitle;
    @Temporal(TemporalType.DATE)
    Date birthDay;
...
}

여기서도 애노테이션 뭉탱이를 중복제거하고 싶은데.. 일단 조금 뒤로 미루겠습니다.

다음은 DAO를 만듭니다. 인터페이스를 만들고 구현체를 만듭니다.

public interface MemberDao {

    void add(Member member);

    List<Member> list(PageParam pageParam, MemberSearchParam sp);

    int totalSize(MemberSearchParam sp);

    Member getById(int id);

    void deleteBy(int id);

    void update(Member member);

}

구현체는..

@Repository
public class MemberDaoImpl implements MemberDao {

    @Autowired SessionFactory sessionFactory;

    public void add(Member member) {
        getSession().save(member);
    }

    public Member getById(int id) {
        return (Member) getSession().get(Member.class, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from Member where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(Member member) {
        getSession().update(member);
    }

    public List<Member> list(PageParam pageParam, MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        //searching
        applySearchParam(c, searchParam);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord().equals("asc"))
            c.addOrder(Order.asc(pageParam.getSidx()));
        else
            c.addOrder(Order.desc(pageParam.getSidx()));

        return c.list();
    }


    public int totalSize(MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }

    private void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        
    }

    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
        
}

CodeDaoImple 코드와 거의 똑같습니다.

상속을 사용해서 중복을 제거하겠습니다. 먼저 인터페이스 부터..

public interface GenericDao<E, S> {

    void add(E e);

    List<E> list(PageParam pageParam, S s);

    int totalSize(S s);

    E getById(int id);

    void deleteBy(int id);

    void update(E e);

}

그리고 이걸 CodeDao와 MemberDao에 사용합니다.

public interface CodeDao extends GenericDao<Code, CodeSearchParam>{
    
}

public interface MemberDao extends GenericDao<Member, MemberSearchParam>{
   
}

오퀘 인터페이스가 텅 비었습니다. 아주 일반적인 CRUD 이외 추가기능이 생기면 눈에 확 띄겠죠.

다음은 GenericDao 구현체를 만듭니다.

public abstract class GenericDaoImpl<E, S> implements GenericDao<E, S>{

    @Autowired protected SessionFactory sessionFactory;
    
    private Class<E> entityClass;

    public GenericDaoImpl() {
ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.entityClass = (Class) ((ParameterizedType) type).getRawType();
        } else {
            this.entityClass = (Class) type;
        }
}
    
    public void add(E e) {
        getSession().save(e);
    }

    public E getById(int id) {
         return (E) getSession().get(entityClass, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from" + entityClass.getSimpleName()+ " where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(E e) {
        getSession().update(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        Criteria c = getCriteriaOf(entityClass);
        //searching
        applySearchParam(c, s);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord() != null){
            if (pageParam.getSord().equals("asc"))
                c.addOrder(Order.asc(pageParam.getSidx()));
            else
                c.addOrder(Order.desc(pageParam.getSidx()));
        }
        //noinspection unchecked
        return c.list();

    }

    protected abstract void applySearchParam(Criteria c, S s);

    public int totalSize(S s) {
        Criteria c = getCriteriaOf(entityClass);
        applySearchParam(c, s);
        return (Integer) c.setProjection(Projections.count("id"))
                .uniqueResult();
    }

    protected Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    protected Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
}

그리고 이녀석을 사용하도록 CodeDaoImpl과 MemberDaoImpl을 수정합니다.

@Repository
public class CodeDaoImpl extends GenericDaoImpl<Code, CodeSearchParam> implements CodeDao {
    
    @Override
    protected void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalEqual(c, "cate", searchParam.getCateValue());
    }
    
}

@Repository
public class MemberDaoImpl extends GenericDaoImpl<Member, MemberSearchParam> implements MemberDao {

    @Override
    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {

    }
}

마지막으로 이전에 만들어 둔 CodeDaoImplTest를 한번 돌려주면 됩니다. 굳이 뭐또 GenericDaoImpl 테스트를 만들 필요는 없는것 같네요.

이렇게 과감한 코드 수정을 할 수 있었던 게 다 CodeDaoImplTest 덕분입니다.

자 그럼 잠깐 밥먹고 MemberService도 만들어야지 





top

Write a comment.


[회사일] CriteriaUtils 테스트하기

프로젝트/SLT : 2010.06.09 12:28


먼저 Criteria가 필요한데.. 애매합니다. 하이버네이트 Session이 만들어주는 객체인지라... 흠.. 난 Criateria나 Session을 테스트하고 싶은 생각은 없고 CriateriaUtils이 제대로 동작하는지만 확인하면 되는데 말이죠.

public class CriteriaUtils {
    public static void addOptionalLike(Criteria c, String fieldName, String value) {
        if(StringUtils.isEmpty(value)){
            c.add(Restrictions.ilike(fieldName, value, MatchMode.ANYWHERE));
        }    
    }

    public static void addOptionalEqual(Criteria c, String fieldName, Integer value) {
        if(value != null && value != 0){
            c.add(Restrictions.eq(fieldName, value));
        }
    }
}

이런코드가 있으니.. 조건이 잘 먹는지 제가 원하느대로 조건문을 쓴게 맞는지 확인하고 싶었습니다. 이럴때 사용할 수 있는게 목킹 프레임워죠. EasyMock이나 JMock, Mockito 등이 있는데 저는 Mockito가 편해서 이걸 쓰기로 했습니다.

검색해보니 EasyMock을 사용해서 Criteria를 사용하는 DAO를 테스트한 글이 나옵니다.
http://toby.epril.com/?p=195

저는 EasyMock대신 Mockito를 사용했고, DAO 대신 CriteriaUtils를 테스트했습니다.

public class CriteriaUtilsTest {

    @Test
    public void testAddOptionalLike() throws Exception {
        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalLike(c, "name", "whiteship");
        verify(c).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", "");
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", "   ");
        verify(c, times(0)).add(any(Criterion.class));
    }

    @Test
    public void testAddOptionalEqual() throws Exception {
        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalEqual(c, "age", 1);
        verify(c).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalEqual(c, "age", null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalEqual(c, "age", 0);
        verify(c, times(0)).add(any(Criterion.class));
    }

}

웃긴건,... any(Criterion.class) 대신에 구체적인 예상 값이 add(Restrictions.ilke("name", "whiteship", MatchMode.ANYWHERE))를 사용하면 결과가... 좀.. @_@

        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalLike(c, "name", "whiteship");
        verify(c).add(Restrictions.ilike("name", "whiteship", MatchMode.ANYWHERE));

즉 이렇게 작성하면

Argument(s) are different! Wanted:
criteria.add(name ilike %whiteship%);
-> at osaf.util.CriteriaUtilsTest.testAddOptionalLike(CriteriaUtilsTest.java:25)
Actual invocation has different arguments:
criteria.add(name ilike %whiteship%);
-> at osaf.util.CriteriaUtils.addOptionalLike(CriteriaUtils.java:16)

Expected :criteria.add(name ilike %whiteship%);
Actual   :criteria.add(name ilike %whiteship%);

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at osaf.util.CriteriaUtilsTest.testAddOptionalLike(CriteriaUtilsTest.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:94)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:165)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:60)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:110)

이런 에러가 나는데.. 도무지 콘솔을 봐도 모르겠습니다. 콘솔에 찍힌게 같은데.. 다르다니?? 먼소리람.. 어차피 내가 원하건 Criateria에 add()가 호출되는냐 마느냐 였으니까 그냥 any()로 해결했습니다.
top

  1. Favicon of http://sehaeng.tistory.com BlogIcon 쌩이~ 2010.06.09 15:03 PERM. MOD/DEL REPLY

    CriateriaUtils 이라고 초반에만 오타나신듯

    Favicon of https://whiteship.tistory.com BlogIcon 기선 2010.06.09 15:53 신고 PERM MOD/DEL

    감사합니다.ㅋㅋ
    흠.. 정리용으로 쓰고 있는 글인데 보고 계신분들이 있었군요.

Write a comment.


[회사일] 검색에 enum 필드 추가하기

프로젝트/SLT : 2010.06.09 11:47


검색 파라미터에 추가해주고 화면에 추가하고 DAO에서 검색부분 코드만 조금 바꾸면 될 것 같습니다.

코드 검색 파라미터를 하나의 클래스로 정의해놨기에 망정이지 이걸 개별적으로 다 핸들러 파라미터로 받아서 처리하면;;; @_@;; 컨트롤러 코드도 손대야 했을껍니다. 아마 서비스 코드도 마찬가지구요.

public class CodeSearchParam {

    private int cateValue;

    private String name;

    private String code;

...
}

이렇게 CodeCate의 값을 바로 가져옵니다. 스프링 바인딩 사용해서 CodeCate 타입으로 받아도 되지만 굳이 그럴필요가 없더군요. 어차피 DB에 들어있는 값과 비교하려면 CodeCate의 value가 필요하며 화면에서도 이걸 전달해 주는데 굳이 이걸 또 포매터 사용해서 CodeCate로 바꿔서 거기서 .getValue()로 꺼내서 비교할 필요가 있나 싶더라구요. 그래서 그냥 int 타입인 cateValue를 선언했습니다.

이제 화면코드로 넘어갑니다.

                <p class="ui-widget-content"><label>코드종류: </label>
                    <form:select path="cateValue">
                        <form:option value="0" label="ALL"/>
                        <form:options items="${ref.codeCateList}" itemLabel="descr" itemValue="value"/>
                    </form:select>
                </p>

아무것도 선택하지 않았을 때 기본으로 ALL이 보일테고 그걸 선택하면 0이라는 값이 넘어가게 합니다. 뭐 이부분은 맘대로인것 같은데 그닦 맘에 들진 않아도 뭐 어쩔 수 있나요. 모든 enum마다 ALL 이라는 값을 만들기도 뭐하고.. 걍 이대로가 나은것 같습니다.

DAO에서 검색 파라메터를 적용하는 부분에 코드를 추가합니다.

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalEqual(c, "cate", searchParam.getCateValue());
    }

CriteriaUtils에 메서드를 하나 추가해 줍니다.

    public static void addOptionalEqual(Criteria c, String fieldName, Integer value) {
        if(value != null && value != 0){
            c.add(Restrictions.eq(fieldName, value));
        }
    }

끝. 오예 잘 됩니다.

컨트롤러나 서비스 코드는 한줄도 건드리지 않았답니다. 
한번 만들어 논 뒤로는 거의 본적이 없으니 어떻게 생겼는지도 기억나지 않는군요.

01

top

Write a comment.


[회사일] Enum 추가, Formatter 적용

프로젝트/SLT : 2010.06.08 17:47


Code 도메인에 enum으로 CodeCate 타입 필드를 하나 추가합니다. Enum을 사용할 땐 DB에 저장될 값과 화면에 보여줄 값과 순서를 지정하는게 보통인지라 그런 기능을 갖추게 하는 인터페이스를 하나 정의했었습니다.

public interface PersistentEnum {

String getDescr();

Integer getValue();

int getOrder();

}

enum이 이 인터페이스를 구현하도록 만듭니다. enum에는 상속다운 상속이 없어서 매번 똑같은 코드가 반복되지만 그래도 머.. 필요하기 때문에 어쩔 수 없습니다.

public enum CodeCate implements PersistentEnum {

COLOR(10, "색상", 2),
SEX(15, "성별", 3),
SIZE(20, "사이즈", 1);

private final Integer value;
private final String descr;
private final int order;

private CodeCate(Integer value, String descr, int order) {
this.value = value;
this.descr = descr;
this.order = order;
}

public Integer getValue() {
return value;
}
public String getDescr() {
return descr;
}
public int getOrder() {
return order;
}

}

그리고 하이버네이트에서 이 타입을 알 수 있게 UserType을 만들어줘야 하는데 GenericPesistentEnumUserType을 사용해서 간단하게 만들 수 있습니다.

public class CodeCateUserType extends GenericEnumUserType<CodeCate> implements Serializable {
    
}

이게 끝입니다. Code 도메인에 필드와 매핑 정보를 추가합니다.

        @Column
@Type(type="smdis.domain.usertype.CodeCateUserType")
private CodeCate cate;

위에서 만든 UserType을 지정해주면 됩니다.

여기까지 한다음 웹쪽을 생각해보죠.

새 코드를 입력할 때 코드 카테고리를 선택하려면 코드 카테 목록이 model에 들어있고 화면에서 그 값을 참조할 수 있어야 합니다. 이런 참조형 데이터를 한곳에 모아서 화면에 전달하도록 하죠.

@Component
public class CodeRef {

    public List<CodeCate> getCodeCateList(){
        return PersistentEnumUtil.sortedListOf(CodeCate.class);
    }
}

그리고 CodeController에는 @ModelAttribute를 사용해서 화면에 전달합니다.

        @Autowired CodeRef ref;

        @ModelAttribute("ref")
public CodeRef ref() {
return ref;
}

그럼 이제 화면에서 참조할 수 있죠.

<p class="ui-widget-content"><label>코드종류</label><form:select path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" /><form:errors path="cate" cssClass="smdis-error-message"/></p>
                
스프링 form 태그를 이용해서 Enum 타입 List를 items에 넘겨주고 그 List 엔티티의 필드 중에서 값이 될 필드와 레이블이 될 필드명을 설정해 줍니다.


그럼 이렇게 화면에 보이는 값는 PersistentEnum의 getDescr() 값으로 보여지고 실제 선택했을때 핸들러로 넘어가게 되는 값은 getValue() 값이 됩니다.

이제 적당한 값을 넣고 저장을 누르면..


이렇게 됩니다. 왜 이런 에러가 나는지는 아시겠죠? 필요한건 CodeCate 타입인데 20이라는 String 타입이 넘어와서 도무지 어떻게 바인딩해야할지 모르겠다고 에러가 나는겁니다. 해결책은 간단합니다. 알려주면 됩니다. 어떻게 바인딩 하면 되는지 스프링한테..

이전까진 PropertyEditor를 사용하던지 아님 직접 핸들러에서 매번 request에서 파라메터 받아서 처리하던지 해야했지만 스프링 3.0에서는 Converter와 Formatter라는게 추가됐습니다. 그 중에서도 웹 용으로는 특수화된 Formatter를 사용하겠습니다.

이전 글에서 만든 GenericPersistentEnumFormatter를 사용해서 만듭니다.

@Component
public class CodeCateFormatter extends GenericPersistentEnumFormatter<CodeCate> {
}

쓰레드 세이프하기 때문에 빈으로 등록해서 싱글톤으로 써도 됩니다. PropertyEditor는 2단계 호출을 거치기 때문에 그다지 안전하지 않았습니다. 매번 new를 사용해서 등록해주는것이 안전했었죠. 이젠 그럴필요가 없으니까 이렇게 합니다.

그리고 이제 이 포매터를 등록하는 일이 남았는데 조금 귀찮습니다;

public class SmdisFormattingConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {

    @Autowired CodeCateFormatter codeCateFormatter;

    @Override
    protected void installFormatters(FormatterRegistry registry) {
        super.installFormatters(registry);
        registry.addFormatterForFieldType(CodeCate.class, codeCateFormatter);
    }
}

먼저 이렇게 FormattingConversionServiceFactoryBean를 상속한 다음에 installFormatters()를 재정의해서 원하는 포매터를 등록해 줍니다.

마지막으로 이녀석을 conversionService로 등록하고 어댑터에 끼워줘야 합니다. 그나마 이부분은 스프링 3.0에 추가된 mvc 네임스페이스 덕분에 간단해 졌습니다.,

    <mvc:annotation-driven conversion-service="conversionService" />

    <bean id="conversionService" class="smdis.common.formatter.SmdisFormattingConversionServiceFactoryBean"/>

끝.. 이제는 스프링이 CodeCate 타입을 어떻게 바인딩 해야하는지 알고 있기 떄문에 잘 들어갑니다.


내일은 검색과 그리드쪽에도 CodeCate를 추가해야겠군요. 
흠.. 그리드는 간단하니깐 걍 오늘 추가해야겠네요.

top

Write a comment.


[회사일] GenericPersistentEnumFormatter 만들기

프로젝트/SLT : 2010.06.08 16:40


GenericPropertyEditor도 만들었었는데 Formatter라고 못만들까 하는 생각에 만들어봤습니다. Locale은 무시해서;; 반쪽짜리긴 하지만 그래도 쓸만합니다.

포매터를 만들 대상은 PersuistentEnum 입니다. 자세한 내용은 이전 글에서 확인하시기 바랍니다.

http://whiteship.me/search/PersistentEnum

간단히.. Enum을 사용할 떄 필요한 기능을 정의한 PersistentEnum 인터페이스를 사용할 때 필요한 UserType이나 Formatter를 쉽게 만들 수 있게 GenericPersistentEnumUserType GenericPersistentEnumProperyEditor등을 만들었습니다.

public class PersistentEnumFormatter<E extends PersistentEnum> implements Formatter<E> {

    protected Class<E> enumClass;

@SuppressWarnings("unchecked")
public PersistentEnumFormatter() {
ParameterizedType genericSuperclass = (ParameterizedType) getClass()
.getGenericSuperclass();
Type type = genericSuperclass.getActualTypeArguments()[0];
if (type instanceof ParameterizedType) {
this.enumClass = (Class) ((ParameterizedType) type).getRawType();
} else {
this.enumClass = (Class) type;
}
}
    
    public String print(E object, Locale locale) {
        return object.getDescr();
    }

    public E parse(String text, Locale locale) throws ParseException {
        return PersistentEnumUtil.valueOf(enumClass, Integer.parseInt(text));
    }
}

제네릭 타입이 필요해서 생성자에서 타입 추론을 사용했는데 그 코드만 빼면 너무도 간단합니다. PE 만들때랑 비슷하거나 더 쉬운것 같습니다.

그래도 왠지 불안하니깐 테스트 해봤습니다.

public class PersistentEnumFormatterTest {

    CodeCateFormatter ccf;

    @Before
    public void setUp(){
        ccf = new CodeCateFormatter();
    }

    @Test
    public void testPrint() throws Exception {
        assertThat(ccf.print(CodeCate.SIZE, null), is("사이즈"));
    }

    @Test
    public void testParse() throws Exception {
        assertThat(ccf.parse("10", null), is(CodeCate.COLOR));
    }

    @Test
    public void testParseNotInValue() throws Exception {
        assertThat(ccf.parse("0", null), is(nullValue()));
    }

    @Test(expected = NumberFormatException.class)
    public void testParseWrongValue() throws Exception {
        assertThat(ccf.parse("캐스팅에러", null), is(nullValue()));
    }

    class CodeCateFormatter extends PersistentEnumFormatter<CodeCate> {

    }

}

흠냐 이제 포매터로 갈아탈 시간이 됐군요. 
어서 CodeCate 이늄(enum)좀 추가하고 마무리 해야겠어요.
top

Write a comment.


[회사일] CRUD 화면 디자인 수정

프로젝트/SLT : 2010.06.08 12:37


01234

목록에서 + 버튼을 누르면 추가 팝업이 뜨고 만약 거기서 잘 입력하면 다시 팝업이 닫히면서 그리드가 갱신 됩니다. 만약 이때 엉뚱한 값을 입력해서 검증 에러가 발생하면 팝업이 아니라 전체 메뉴 속에서 에러와 함께 폼을 보여줍니다.

그리드 목록 중 하나를 더블클릭하면 뷰 화면으로 이동하고 여기서 수정 화면으로 넘어가거나 삭제를 할 수 있습니다.

수정화면에서는 내용을 편집할 수 있고 저장, 삭제를 할 수 있습니다. 또한 다시 원래 값으로 되돌릴수도 있고 목록으로 돌아가거나 뷰 화면으로 돌아갈 수도 있습니다.

네비게이션 관련 버튼은 오른쪽에 두었고 기능 버튼은 왼쪽에 두었습니다.

이제 Enum을 추가해서 CodeCate를 선택할 수 있게 해야겠습니다.
top

Write a comment.


[회사일] CRUD 구현

프로젝트/SLT : 2010.06.07 16:42


먼저 앞에서 만든 뷰를 가지고 컨트롤러 코드를 만드는데 이때 URL을 어떻게 가져갈지 구상해야합니다.

/base/code/list GET
/base/code/add GET 
/base/code/add POST
/base/code/update?id=23 GET 
/base/code/update?id=23 POST 
/base/code/delete?id=23 GET 

이전에는 이런식으로 썼었습니다. 그런데 이번에는 URL을 REST 스타일로 만들어 보고자 스프링 Roo 레퍼런스에 있는 그림을 참조했습니다.

http://static.springsource.org/spring-roo/reference/html/beginning.html


그래서 이번에는

/base/code/ GET
/base/code/form GET
/base/code POST
/base/code/{id}/form GET
/base/code/{id} PUT
/base/code/{id} DELETE

이런식으로 구성했습니다. 아직 하나 안만든건 view인데 그건 /base/code/{id} GET이 될겁니다. 

아직 익숙하지 않아서 잘 적응되고 있진 않은데 이렇게 만들어 두면 나중에 웹 서비스 별도로 만들 필요없이 저 런 RESTful URL과 스프링 3.0의 컨텐츠 네고 기능을 이용해서 모바일 용 앱을 만들때 한결 편할 것 같습니다.

암튼 이 다음은 스프링 3.0의 @Validation 기능을 적용하는 것인데 이때 하이버 벨리데이터를 의존성에 추가해 줬습니다.

        <!-- Validation -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.validator</artifactId>
            <version>4.0.0.GA</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>com.springsource.javax.xml.bind</artifactId>
            <version>2.1.7</version>
        </dependency>

스프링 웹 설정은 변경한것이 없습니다.

<mvc:annotation-driven />

이게 전부이고 나머진 예외-뷰 설정과 컨텐츠 네고 설정 뿐입니다.

@Controller
@RequestMapping("/base/code/*")
@SessionAttributes("code")
public class CodeController {

    @Autowired CodeService codeService;

    @RequestMapping(value="mgt")
    public void mgt(Model model){
        model.addAttribute("searchParam", new CodeSearchParam());
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

    @RequestMapping(value = "form", method = RequestMethod.GET)
    public String addForm(Model model){
        model.addAttribute("code", new Code());
        return "/base/code/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return "base/code/new";
        status.setComplete();
        codeService.add(code);
        return  "redirect:/base/code/mgt";
    }

    @RequestMapping(value = "{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("code", codeService.getById(id));
        return "/base/code/edit";
    }

    @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return "base/code/edit";
        status.setComplete();
        codeService.update(code);
        return  "redirect:/base/code/mgt";
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        codeService.deleteBy(id);
        return  "redirect:/base/code/mgt";
    }

}

컨트롤러는 이렇게 구현했는데 URL을 클래스 선언부와 메서드 선언부에 분할해서 설정했습니다. 그래야 나중에 상위클래스로 빼서 프레임워크화 할 때 용이하기 때문입니다. 서비스와 DAO 코드는 간단하기 때문에 생략입니다.

참 컨트롤러에서 PUT, DELETE 같은 method를 사용하고 있는데 사실은 화면에서 넘어올땐 걍 POST일 뿐이고 _method라는 히든 파람을 스프링이 읽어서 적절한 애노테이션 핸들러를 찾아주게 되어 있습니다. 히든 파람으로 RESTfult 한 API를 만들기 위해서는 web.xml에 필터를 하나 등록해줘야 합니다.

<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

자 그럼 오늘은 이만하고 내일은 Code에 새로운 속성들을 추가해 봐야겠습니다. 시간이 남으면 User 도메인 CRUD까지 구현해야겠네요. 그 다음부터는 드뎌 OSAF 3.0 코드가 조금씩 간추려 지겠군요.


top

Write a comment.


[회사일] add 팝업 만들기

프로젝트/SLT : 2010.06.07 12:17



저 쬐끄만 팝업창으로 영.. 맘에 드는 플러그인이 없어서 고르고 고르다 찾은게..

http://colorpowered.com/colorbox/

이거.. 더이상 찾기도 힘들고 디자인 하기도 귀찮으니 이것으로.. 끝.

new.jsp 페이지를 별도로 만들고, mgt.jsp 페이지에서 + 버튼을 누르면 new.jsp를 저런 형태로 보여주도록 만들었습니다.

mgt.jsp

 $("#smdis-new-button").colorbox({href:"/base/code/new"});

버튼 모양은 jquery ui의 button()을 이용해서 꾸몄습니다. 이제 화면은 만들어졌으니.. 구현으로..


top

TAG add 뷰

Write a comment.


[회사일] 그리드 정렬 기능 구현하기

프로젝트/SLT : 2010.06.03 11:47


이제 실제 회사일과 싱크 맞는다. 휴.. 이전까지 싱크 맞출려고 급하게 달렸더니 정신 없다.ㅋ

그리드를 누르면 JqGrid가 알아서 매개변수에 정렬할 필드 명과 정렬 방향을 넘겨준다. 그럼 컨트롤러에서 잘 받는지 확인부터 해볼까.

    @RequestMapping
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        System.out.println("sidx: " + pageParam.getSidx());
        System.out.println("sord: " + pageParam.getSord());

        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

잠깐 저렇게 추가해서 콘솔에 찍고나서 다시 지운다. 이럴때는 JRebel이 빛을 발한다. 짱이다. 서버 껐다켜지 않아도 이정도는 간단하게 잘 처리해준다.

서비스 코드 고칠것 없다. 이미 PageParam에 들어있으니.. 그냥 타고 갈것다.
DAO 인터페이스도 고칠것 없다. DAO 구현체의 list만 고치면 될 것 같다.

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        //searching
        applySearchParam(c, searchParam);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord().equals("asc"))
            c.addOrder(Order.asc(pageParam.getSidx()));
        else
            c.addOrder(Order.desc(pageParam.getSidx()));
        
        return c.list();
    }

고쳤다. 이전에 만들었던 테스트를 돌렸다. 혹시 이 코드를 수정하면서 이전에 되던게 안될까바 걱정되서 돌려봤다. 이런 테스트를 리그레션 테스트 또는 회기 테스트라는 어려운 말을 쓰기도 하는데 뭐.. 많이 아는척하는 사람들과의 대화에서 밀리지 않으려면 알아두는게 좋겠다.

자 그럼 이제 다시 지금 추가한 코드에 대한 테스트를 만들어보자.

    @Test
    public void testListOrdering() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        PageParam pageParam = new PageParam();
        pageParam.setRows(5);
        pageParam.setPage(1);
        pageParam.setSidx("id");  // id 필드 기준으로
        pageParam.setSord("asc"); // 내림차순

        List<Code> codeList = codeDao.list(pageParam, codeSearchParam);
        String result = "";
        for(Code code : codeList){
            result += code.getId();
        }
        assertThat(result, is("12345"));

        pageParam.setSidx("id");  // id 필드 기준으로
        pageParam.setSord("desc"); // 내림차순

        codeList = codeDao.list(pageParam, codeSearchParam);
        result = "";
        for(Code code : codeList){
            result += code.getId();
        }
        assertThat(result, is("76543"));
    }

오퀘 잘 돈다. 캬캬 사실 맨 마지막 줄은 예상을 잘못해서 "54321"로 적었었는데.. 테스트 코드가 니보다 똑똑했다. @_@;;; 아... 이런!..

테스트보다 멍청한.. 개발자.. 이게 나다. ㅠ.ㅠ

흠.. 슬슬 그리드 쪽이 정리가 되가니깐 추가/수정 기능쪽으로 넘어갈까 말까.. 
아니면 기본 정렬 기능을 넣을까..고민 된다.

사실 대표님은 가끔 "일단 CodeCate로 정렬하고 그 다음에 코드값으로 정렬해줘" 이렇게 다중 정렬을 요구한적이 있었다.

밥먹으면서 생각해봐야지.. 그전까진 잠깐 놀까나..
top

Write a comment.


[회사일] 검색및 페이지 처리 구현하기

프로젝트/SLT : 2010.06.03 11:16


사실 이 작업은 따로 따로 했지만 중간에 정리해 두지 않아서 다시 코드를 빼면서 정리하기는 귀찮아서 한번에 정리한다.

먼저 화면에서 요청을 보내도록 한다.

                $("#smdis-grid").jqGrid({
                    caption: '코드 목록',
                    url:'/base/code/list?' + $("#searchForm").serialize(),
                    colNames:['id', '코드값', '코드명', '설명'],
                    colModel :[
                        {name:'id', index:'id', width:55, hidden:true},
                        {name:'code', index:'code', width:80},
                        {name:'name', index:'name', width:90},
                        {name:'descr', index:'descr', width:90},
                    ],
                    autoencode:true
                });

그리드 구현은 이걸로 끝이다. 
다음은 /base/code/list URL을 처리한 핸들러를 구현한다.

    @RequestMapping
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

화면에서 넘어온 파라미터들 중에 CodeSearchParam과 PageParam을 구분해서 받는다. @ModelAttribute가 적용된것이며 스프링 바인딩 기능이 적용되어 요청에 들어있는 파라미터들이 저 객체들의 속성으로 들어오게 된다.

처음엔 잘 넘어오는지 궁금해서 sout을 사용해서 콘솔에 찍어보면서 확인을 했었다. 특히 잘 넘어오더라도 한글이 잘 찍히는지 확인했다. 한글이 ??.?? 이렇게 넘어왔고, 파이어버그로 봤을 때는 화면에서 한글은 잘 넘어간 것 같다. 

톰캣 server.xml을 설정해줘야겠다.

    <Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" />

이 뒤로는 한글이 잘 넘어왔다.

public class CodeSearchParam {

    private String name;

    private String code;

}

검색 옵션이고, 도메인 코드에 따라 검색 매개변수가 달랄 질 수 있으니 매번 만들어줘야겠다.

public class PageParam {

    //화면으로 넘여줄 값.
    int totalPageSize; // 전체 페이지 갯수
    int listSize; // 전체 목록 갯수
    int currentPageNumber; // 현재 보여줄 페이지

    //화면에서 넘어오는 값
    int page; //요청 받은 페이지
    int rows; //한 화면당 보여줄 줄 수
    String sidx; //정렬 기준 컬럼
    String sord; //정렬 방향

}

이건 여러 컨트롤러에서 공통으로 사용할 수 있는 코드다. 따라서 두 클래스 패키지를 잘 분리해둔다. CodeSearchParam은 base/code/support에 넣고 PageParam은 common/page 에 뒀다. PageParam에 주석으로 속성들을 분리해 뒀지만 사실 별도의 클래스로 나눌까 생각도 해봤다. 그런데 좀 귀찮았다. 어차피 서로 관련있는 정보들이기 때문에 그냥 한 곳에 뒀다.

다음은 서비스 인터페이스에 필요한 걸 정의하고 서비스 구현 클래스에서 메서드를 구현한다.

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        pageParam.initCurrentPageInfosWith(codeDao.totalSize(searchParam));
        return codeDao.list(pageParam, searchParam);
    }

이전에 만들었던 dao 코드를 써먹을 수 있게됐다. 그런데 변경해야겠다. 전체 사이즈를 구할때는 searchParam만 있으면 되고 실제 list를 가져올 땐 둘 다 넘겨줘야된다.

    public int totalSize(CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        if(pageParam != null){
            c.setFirstResult(pageParam.getFirstRowNumber());
            c.setMaxResults(pageParam.getRows());
        }
        return c.list();
    }

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(!searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }

Criteria API를 사용해서 검색 옵션과 페이징 처리를 했다. PageParam 클래스에는

    public int getFirstRowNumber() {
        return Math.max((getPage() - 1) * getRows() ,0);
    }

    public void initCurrentPageInfosWith(int totalListSize) {
        setListSize(totalListSize);
        setTotalPageSize((int)Math.ceil((double)totalListSize /getRows()));
        setCurrentPageNumber(getPage());
    }

이런 코드가 추가됐다. 이제 테스트좀 해볼까. 인텔리J에 단축키를 Ctrl+J로 등록해놨다. 이클립스에서는 별도로 플러그인을 설치해야 이런 기능을 사용할 수 있는데.. 귀찮지 아니한가?

public class PageParamTest {

    @Test
    public void testGetFirstRowNumber() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);

        pageParam.setPage(0);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(-2);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(1);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(3);
        assertThat(pageParam.getFirstRowNumber(), is(40));
    }

    @Test
    public void testInitCurrentPageInfosWith() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);

        pageParam.initCurrentPageInfosWith(20);
        assertThat(pageParam.getListSize(), is(20));
        assertThat(pageParam.getTotalPageSize(), is(1));

        pageParam.initCurrentPageInfosWith(41);
        assertThat(pageParam.getListSize(), is(41));
        assertThat(pageParam.getTotalPageSize(), is(3));
    }
}

CodeDao도 테스트 해주지 않으면 왠지 섭섭하다. Criteria API를 잔뜩 썼는데 제대로 쓴 건지 확인차 학습차 확인 해보자.

 
   /**
     * testData2.xml
     *
     * <dataset>
            <code id="1" name="블랙" code="BLK" />
            <code id="2" name="레드" code="RED" />
            <code id="3" name="그린" code="GRN" />
            <code id="4" name="블루" code="BLU" />
            <code id="5" name="옐로" code="YLW" />
            <code id="6" name="골드" code="GLD" />
            <code id="7" name="실버" code="SLV" />
        </dataset>
     */
    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));
    }

앗 이런 돌려보니 NullPointerException이다. 

        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }

여기서 발생했다. 아무래도 null 체크까지 추가해야겠다. @_@

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(searchParam.getCode() != null && !searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(searchParam.getName() != null && !searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }

코드 참.. 거시기 하다. @_@ 그래도 일단 테스트부터 성공시키자. 오퀘 테스트가 잘 돌았다. 리팩토링 하자.

public class CriteriaUtils {
    public static void addOptionalLike(Criteria c, String fieldName, String value) {
        if(value != null && !value.isEmpty()){
            c.add(Restrictions.ilike(fieldName, value, MatchMode.ANYWHERE));
        }    
    }
}

이런 유틸 하나를 만들었다.

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
    }

DAO 코딩이 한결 간편해졌다. 테스트 해보자, 잘 돌아간다. 테스트를 보강하자.

    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();

        //code 겁색
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));

        //대소문자 구분 안함.
        codeSearchParam.setCode("l");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));

        //name 검색추가
        codeSearchParam.setName("블");
        assertThat(codeDao.totalSize(codeSearchParam), is(2));
    }

잘 돈다. 이제 사이즈 구하는 쿼리는 안심이다.

    @Test
    public void testList() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        PageParam pageParam = new PageParam();
        pageParam.setRows(5); // 한 페이지에 5개씩 보자.
        pageParam.setPage(1); // 첫페이지.

        List<Code> codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(5));
        System.out.println(codeList);

        pageParam.setPage(2);
        codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(2));
    }

페이지 처리 테스트 코드인데 사실 좀 부실하다;;  그래도 이정도 해놓고 화면에서 확인해보니 잘 나온다. 오퀘.












top

Write a comment.


[회사일] 그리드 출력하기 with JqGrid

프로젝트/SLT : 2010.06.03 10:09


다음으로 한 일은 그리드다. 회사에서 엑티브 위젯인가 뭔가 하는 제품 라이선스를 샀지만 난 그냥 제이쿼리 grid를 쓰고 싶어졌다. 그래서 찾아봤더니 JqGrid가 아주 쓸만해 보였다. 애초에 서버쪽이랑 연동 되도록 만들어져있었다. 가끔 잡습 그리드들을 보면 서버랑은 전혀 별개로 페이지 기능을 제공하는 것들이 있는데... 대체 그게 무슨 소용인가 싶다. 화면에 다 보여줄 것도 아니면서 뭐하러 미리 100개를 화면으로 가져온다음에 그걸 또 페이징 해서 보여줄까? 차라리 100개를 다 보여주고 말지.. 그래서 사실상 클라이언트단 페이징은 거의 쓸모가 없고 클아이언트용 페이지는 무조건 한페이지고 서버단과 연동하여 페이징할 네비게이션 바를 별도로 구현해야하는 피곤함이 뒤따른다. 그런데 JqGrid는 그러지 않았다. 서버단과 연동하여 페이징을 하기 때문에 네비게이션바도 별도로 구현할 필요가 없었고, 그리드 상에서 편집, 트리형 그리드 등 고급 기능도 잘 지원해주고 있었다.

http://www.trirand.com/blog/

역시 예제 먼저 확인했다.

http://trirand.com/blog/jqgrid/jqgrid.html

맘에 든다. 적용하려는데 레이아웃 플러그인처럼 간단하진 않았다.

1. jquery-ui theme와 밀접한 관계가 있는 플러그인이라 테마 롤러를 사용해서 원하는 테마를 설치했다.


2. json 배열을 어떻게 다루는지 자세히 알아봤다.

 jsonReader 옵셥을 사용하면 스프링 3.0 컨텐츠 네고 기능을 사용하기 좋았다. 그렇지 않고 @ResponseBody를 사용해서 JSON만 뿌려주게 하면 인코딩 문제도 생기도 JSON 전용 핸들러가 되버리니까 좀 별로였다. 그냥Model을 사용해서 화면에 전달할 객체를 넣어주고 json 형태를 원한다고 요청하면 컨텐츠 네고를 타고서 결국 Model에 들어있는 객체들을 json으로 변환해준다. 만약 브라우저 주소창에 Ajax로 보낸 요청과 같은 URL을 입력했다면 난 .jsp 페이지를 볼 수 있었을 것이다. 그런게 컨텐츠 네고 뷰 리졸버다. 암튼 지간에.. 여기서 인코딩과 JSON 다루는 방법을 학습하느라 시간좀 뺏겼다. 그래도 결국 해냈다.


여기서 다듬기 작업 중에 몇가지 못한걸 적어둔다.
- 레이아웃 크기 변경시 그리드 사이즈 자동 조정: 레이아웃 콜백 옵션이 잘 동작하지 않았다. 버그 같은데; 버전 업을 기다려봐야겠다.
- 그리드 세로 크기도 가로 크기처럼 자동으로 꽉 채워주는 옵션이 있으면 좋겠는데 없었다. 현재는 그리드 내용에 따라서 세로 크기가 자동으로 변하게 해뒀는데 머.. 이것도 나쁘지 않았다.

top

Write a comment.


[회사일] 화면 레이아웃 잡기

프로젝트/SLT : 2010.06.03 09:59


난 화면 코드는 잘 못만든다. 만들긴 만드는데 시간이 너무 오래 걸린다. 그 시간에 자바 코딩을 하는편이 더 효율적이다. 하지만 회사에 프론트 엔드 개발자는 없다. 내가 다 해야된다. 그래서 그 동안 봄싹 개발을 하며 자바스크립트랑 도 좀 친해지고, CSS, HTML 과도 친해지려고 노력했다.

차마 ext-js까진 못했지만 jquery까지는 이제 플러그인 가져다 쓰고 옵션 주면서 쓰는데는 익숙해졌다. 화면 구상을 위해서 제일 먼저 검새해본게 jquery layout이고 젤 상위에 걸려있는 링크가 다음과 같다.

http://layout.jquery-dev.net/

언제나처럼 일단 데모 페이지로 가서 어떻게 동작하는지 살펴본다.

http://layout.jquery-dev.net/demos.cfm

일단 디자인 깔끔하고 레이아웃도 다양하게 만들 수 있어 보인다. 괜히 제일 위에 링크되어 있는게 아니었다.

http://layout.jquery-dev.net/demos/accordion.html

맘에 드는 예제를 찾았다.

상단은 제목과 로그인 정보
왼쪽은 메뉴
오른쪽은 검색 옵션
하단은 카피라이트 정도?
가운데는 그리드를 띄우고
추가/수정 화면은 별도의 창에 띄우기로 결정했다.


텍스트를 채워넣고 CSS로 스타일을 조금 넣었다.
top

Write a comment.


[회사일] DAO 테스트 만들기

프로젝트/SLT : 2010.06.01 18:55


이전 코드에서는 뭐 테스트 해보고 싶을 만한게 없었다. 사실 하두 자주 써먹었고 거의 API를 그대로 쓴 코드라서 테스트 하지 않아도 확신이 서는 코드가 대부분이었다. 그 중에서도 굳이 가장 불안한 코드를 꼽으라면 DAO 코드가 되겠다. 나머진 그냥 단순 위임이라 뭐 의심할께 없다.

DAO 코드를 보자.

    public List<Code> list() {
        return getCriteriaOf(Code.class).list();
    }

흠.. 테스트 할 맛이 안난다.

   private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }

이런 private 메서드를 쓰고 있지만 전혀 테스트 할만한 뭐시기가 감지되지 않는다.

list 사이즈를 구하는 메서드를 만들어 보기로 했다. 페이징을 구현할텐데 거기서 분명히 전체 목록 크기를 구하는 쿼리가 필요할 것이기 떄문이다.

CodeDao 인터페이스에 코드를 추가하자.

    int totalSize();

다음은 이것을 구현한다.

    public int totalSize() {
        return (Integer)getCriteriaOf(Code.class)
            .setProjection(Projections.count("id"))
            .uniqueResult();
    }

오.. 제법 낯선 코드를 두 중 정도 코딩했다. 왠지 쬐끔 불안하다. 쬐금.. API 학습 차원에서 테스트를 해봐야겠단 생각이 든다. 좋아 결심했어.

소스 코드와 동일한 패키지에 CodeDaoImpleTest라는 테스트를 만든다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class CodeDaoImplTest extends DBUnitSupport {

    @Autowired CodeDao codeDao;

    @Test
    public void testTotalSize() throws Exception {
        insertXmlData("testData.xml");
        assertThat(codeDao.totalSize(), is(1));
    }

}

여기서 주목할 것은 굵게 표시한 부분이다. 먼저 테스트에서 사용할 DB가 운영 DB와 같으면 테스트 데이터를 넣고 확인할 때 문제가 될 수 있으니 전혀 다른 DB 설정을 보도록 테스트용 애플리케이션 컨텍스트와 데이터베이스 프로퍼티 파일을 만든다.

    <context:property-placeholder location="classpath*:test.database.properties"/>

이 부분 빼곤 나머진 같다.

database.password=
database.username=sa
database.url=jdbc:hsqldb:mem:ㅇㅇㅇ
database.driverClassName=org.hsqldb.jdbcDriver
hibernate.dialect=org.hibernate.dialect.HSQLDialect

다음은 DBUnit을 사용하기 쉽게 해주는 DBUnitSupport 클래스다. DataSource를 필요로 하기 때문에 자동 주입 받도록 설정한다.

public class DBUnitSupport {

enum DataType {EXCEL, FLATXML}

@Autowired
private DataSource dataSource;

protected void cleanInsertXmlData(String fileSource) throws Exception {
insertData(fileSource, DataType.FLATXML, DatabaseOperation.CLEAN_INSERT);
}
protected void cleanInsertXlsData(String fileSource) throws Exception {
insertData(fileSource, DataType.EXCEL, DatabaseOperation.CLEAN_INSERT);
}

private void insertData(String fileSource, DataType type, DatabaseOperation operation) throws Exception {
InputStream sourceStream = new ClassPathResource(fileSource, getClass()).getInputStream();

IDataSet dataset = null;
if (type == DataType.EXCEL) {
dataset = new XlsDataSet(sourceStream);
}
else if (type == DataType.FLATXML) {
dataset = new FlatXmlDataSet(sourceStream);
}

operation.execute(
new DatabaseConnection(DataSourceUtils.getConnection(dataSource)), dataset);
}

protected void insertXmlData(String fileSource) throws Exception {
insertData(fileSource, DataType.FLATXML, DatabaseOperation.INSERT);
}

protected void insertXlsData(String fileSource) throws Exception {
insertData(fileSource, DataType.EXCEL, DatabaseOperation.INSERT);
}

}

DataSource가 필요하기 때문에 
나머지 코드에 대한 설명은 생략~

<dataset>
<code id="1" name="블랙" code="BLK" />
</dataset>

테스트는 성공한다.

자 이제 DaoTest라는 클래스를 만들어서 설정을 옮겨보자.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class DaoTest extends DBUnitSupport {
}

참고로 이 클래스를 src 밑에서 만들었다면 컴파일 에러가 난다.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

여기서 스코프를 test라고 했기 때문이다. 지워준다. 프레임워크성 코드가 생기기 시작하기 때문에 패키징을 조금 신경써서 나눠둔다.

public class CodeDaoImplTest extends DaoTest {

    @Autowired CodeDao codeDao;

    @Test
    public void testTotalSize() throws Exception {
        insertXmlData("testData.xml");
        assertThat(codeDao.totalSize(), is(1));
    }
}

자. CodeDaoImplTest가 깔끔해졌다. 앞으로 DAO 테스트를 할 땐 이렇게 DaoTest만 확장해서 만들면 되겠다.






top

Write a comment.


[회사일] 초간단 계층형 아키텍처 만들기

프로젝트/SLT : 2010.06.01 18:30


Code 라는 도메인이 있다. Item에서 사용할 색, 사이즈, 제품 성별 등을 이 '코드'라는 걸로 관리할 생각인데 그걸 대편하는 도메인이 Code다.

1. 도메인 클래스 만들기

@Entity
public class Code {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(length = 50)
    private String name;

    @Column(length = 50)
    private String code;

    @Column
    @Type(type = "text")
    private String descr;
...
}

2. JPA 애노테이션을 사용해서 매핑 정보를 입력했다. CodeDao 인터페이스를 만든다.

public interface CodeDao {
    List<Code> list();
}

3. 구현체를 만든다.

@Repository
public class CodeDaoImpl implements CodeDao {

    @Autowired SessionFactory sessionFactory;

    public List<Code> list() {
        return getCriteriaOf(Code.class).list();
    }
}

4. CodeService 인터페이스를 만든다.

public interface CodeService {
    List<Code> list();
}

5. 구현체를 만든다.

@Service
@Transactional
public class CodeServiceImpl implements CodeService{

    @Autowired CodeDao codeDao;

    public List<Code> list() {
        return codeDao.list();
    }
}

6. CodeController를 만든다.

@Controller
@RequestMapping("/base/code/*")
public class CodeController {

    @Autowired CodeService codeService;

    @RequestMapping
    public void list() {

    }
}

7. web/WEB-INF/views/base/code/list.jsp 파일을 만든다.

생략

그리고 화면에서 확인한다.

아주 지겹게 써먹고 있는 아키텍처인데 사실 아직도 잘 못하고 있다. 
코드 제위치 잡아 주는게 쉬운 일이 아니다.
이번엔 잘 해보자.



top

Write a comment.


[회사일] 프로젝트 세팅

프로젝트/SLT : 2010.06.01 18:20


본격적인 개발을 시작하기 전에 할일이 너무 많다. 그래서 처음엔 스프링 Roo로 개발할까 생각도 했다. 10분만에 내가 원하는 모든 세팅을 가지고 CRUD 애플리케이션을 돌릴 수 있었다. 하지만 디자인도 맘에 안들고 스캐폴딩 코드를 빼면 내가 뭘 어떻게 해야할지 감이 잡히지 않았다. 책이라도 있었으면 보면서 했을텐데 책은 아직 없고 레퍼런스도 빈약했다. 포기했다. 그냥 맨땅에서 다시 다 만들기로 마음 먹었다. 처음부터 그렇게 생각했으면 조금 더 빨리 시작할 수 있었을 것이다.

프로젝트 세팅 작업은 STS에서 했다. 아직 인텔리J에 완전히 익숙하지 못해서 그런지 초기 프로젝트 세팅은 왠지 이클립스가 편하다.

1. 다이나믹 웹 프로젝트 만들기

중간에 웹 컨텐츠 폴더를 web으로 바꿔줬다.

2. pom.xml 만들기

STS에는 m2c 플러긴이 기본으로 깔려있다.
프로젝트를 우클릭하고 pom.xml 파일 생성을 했다.
이제부터 메이븐과의 싸움이다.
(이전에는 뭐가 뭔 설명인지 몰라서 괜히 어려워 보였었는데 이제는 머 그냥 귀찮을 따름이다.)

3. 프로젝트 구조 설정하기.

내가 원하는 프로젝트 구조는 간단하다. 일반적인 자바 프로젝트 같이 보이는 메이븐 프로젝트를 원한다. 즉 프로젝트 최상위에는 src, test, web만 있으며 src와 test 밑에는 바로 패키지가 시작된다. 일반적인 메이븐 프로젝트 구조가 아니다. 난 이게 더 편하다. 메이븐 플젝 구조라면 소스 패키지 시작하기 전에 3중 폴더를 지나야한다. 리소스와 자바 파일도 구분되어 있다. 그림 그리기는 귀찮아서 패스.

<build>
<sourceDirectory>
${project.basedir}/src
</sourceDirectory>
<testSourceDirectory>
${project.basedir}/test
</testSourceDirectory>
        <resources>
            <resource>
                <directory>${project.basedir}/src</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>${project.basedir}/test</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </testResource>
        </testResources>
</build>

 이렇게 해서 src에 메이븐 자바 소스 파일과 리소스 파일을 넣을 거구 test에 메이븐 자바 테스트 파일과 리소스 파일을 넣겠다고 설정해줬다.

4. 메이븐 플러그인 설정

설명 생략. 역시 귀찮음;;

4-1. maven-war-plugin 설정

            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <configuration>
                    <warSourceDirectory>web</warSourceDirectory>
                </configuration>
            </plugin>

4-2. maven-compiler-plugin 설정

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

4-3. maven-resources-plugin 설정

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

4-4. maven-eclipse-plugin 설정

            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>${downSource}</downloadSources>
                    <downloadJavadocs>${downJavadoc}</downloadJavadocs>
                    <wtpversion>1.5</wtpversion>
                    <wtpContextName>${wtpContextName}</wtpContextName>
                </configuration>
            </plugin>

4-5. maven-clean-plugin 설정

            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>web/WEB-INF/lib</directory>
                        </fileset>
                        <fileset>
                            <directory>web/WEB-INF/classes</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>

4-6. 플러그인 저장소 설정

    <pluginRepositories>
        <pluginRepository>
            <id>codehaus snapshot repository</id>
            <url>http://snapshots.repository.codehaus.org/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
 
5. 프로젝트 의존성 설정 

    <properties>
        <spring.version>3.0.2.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- Spring depedencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.instrument</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web.servlet</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context.support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aop</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.transaction</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Hibernate dependecies -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate</artifactId>
            <version>3.3.2.GA</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.annotations</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.annotations.common</artifactId>
            <version>3.3.0.ga</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.ejb</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <!-- Caching dependecies -->
        <dependency>
            <groupId>net.sourceforge.ehcache</groupId>
            <artifactId>com.springsource.net.sf.ehcache</artifactId>
            <version>1.6.2</version>
        </dependency>
        <!-- DBMS dependecies -->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>com.springsource.javax.persistence</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>com.springsource.javax.transaction</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.2-507.jdbc3</version>
        </dependency>
        <!-- Logging depedencies -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.5.10</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>com.springsource.ch.qos.logback.classic</artifactId>
            <version>0.9.9</version>
        </dependency>
        <!-- Test dependencies -->
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>com.springsource.org.junit</artifactId>
            <version>4.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>com.springsource.org.hamcrest</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>com.springsource.org.mockito</artifactId>
            <version>1.8.4</version>
        </dependency>
        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>com.springsource.org.dbunit</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>com.springsource.org.hsqldb</artifactId>
            <version>1.8.0.9</version>
            <scope>test</scope>
        </dependency>
        <!-- Web dependencies -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet.jsp.jstl</artifactId>
            <version>1.2.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet</artifactId>
            <version>2.5.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet.jsp</artifactId>
            <version>2.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.tuckey</groupId>
            <artifactId>com.springsource.org.tuckey.web.filters.urlrewrite</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>com.springsource.org.apache.commons.fileupload</artifactId>
            <version>1.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>com.springsource.org.codehaus.jackson</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>com.springsource.org.codehaus.jackson.mapper</artifactId>
            <version>1.4.2</version>
        </dependency>
        <!-- AspectJ dependencies -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>com.springsource.org.aspectj.runtime</artifactId>
            <version>1.6.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>com.springsource.org.aspectj.weaver</artifactId>
            <version>1.6.5.RELEASE</version>
        </dependency>
    </dependencies>


직접 운영할 수 있거나 라이브러리 설치를 부탁할 수 있는 넥서스 같은 메이븐 저장소를 설정해주는게 좋은데 난 봄싹 저장소를 설정했다.

    <repositories>
        <repository>
            <id>springsprout nexus</id>
            <name>SpringSprout Nexus public</name>
            <url>http://dev.springsprout.org/nexus/content/groups/public</url>
        </repository>
        <repository>
            <id>springsprout snapshots</id>
            <name>SpringSprout Nexus public snapshots</name>
            <url>http://dev.springsprout.org/nexus/content/groups/public-snapshots/
            </url>
        </repository>
    </repositories>

6. web.xml 설정하기

이제 메이븐은 끝났다. 이제부턴 웹 프로젝트 설정과의 싸움이다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">

<display-name>ㅇㅇㅇ</display-name>

<context-param>
<param-name>webAppRootKey</param-name>
<param-value>ㅇㅇㅇ.root</param-value>
</context-param>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext*.xml</param-value>
</context-param>

<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter>
<filter-name>UrlRewriteFilter</filter-name>
<filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
</filter>

<filter>
<filter-name>Spring OpenSessionInViewFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>Spring OpenSessionInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>UrlRewriteFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>smdis2</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/webmvc-config.xml</param-value>
</init-param>
</servlet>

<servlet>
<servlet-name>Resource Servlet</servlet-name>
<servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>smdis2</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>Resource Servlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

<session-config>
<session-timeout>10</session-timeout>
</session-config>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/app/uncaughtException</location>
</error-page>

<error-page>
<error-code>404</error-code>
<location>/app/resourceNotFound</location>
</error-page>
</web-app>

스프링 Roo에서 만들어주는 web.xml을 배꼈다.
이 중에서 일부 필터는 사용할 ORM에 따라 바꿔야 하며, 스프링 웹 플로우도 필요하다. 아직 웹 플로우 의존성은 추가하지 않았는데 나중에 넣어도 상관없으니 그냥 쓴다.

7. URL Rewriter 설정

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN" "http://tuckey.org/res/dtds/urlrewrite3.0.dtd">

<urlrewrite default-match-type="wildcard">
<rule>
<from>/resources/**</from>
<to last="true">/resources/$1</to>
</rule>
<rule>
<from>/static/WEB-INF/**</from>
<set type="status">403</set>
<to last="true">/static/WEB-INF/$1</to>
</rule>
<rule>
<from>/static/**</from>
<to last="true">/$1</to>
</rule>
<rule>
<from>/</from>
<to last="true">/app/index</to>
</rule>
<rule>
<from>/app/**</from>
<to last="true">/app/$1</to>
</rule>
<rule>
<from>/**</from>
<to>/app/$1</to>
</rule>
<outbound-rule>
<from>/app/**</from>
<to>/$1</to>
</outbound-rule>
</urlrewrite>

web.xml과 같은 폴더에 넣어준다. 스프링 Roo를 배꼈다. 

8. 스프링 디스패처 서블릿 설정 파일 만들기

web.xml 설정에 보면 이 파일 위치가 명시되어 있는 걸 볼 수 있다. 스프링 Roo에서 사용하는 기본 위치다. 그냥 똑같이 쓰도록 하자. web.xml에서 위치를 명시하지 않으면 web.xml과 같은 위치에 디스패처 서블릿의 이름-servlet.xml 파일로 만들어 줘야한다.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd     http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<context:component-scan base-package="ㅇㅇㅇ"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>

<mvc:annotation-driven />

<mvc:view-controller path="/login" />
<mvc:view-controller path="/index" />
<mvc:view-controller path="/sample" />
<mvc:view-controller path="/uncaughtException" />
<mvc:view-controller path="/resourceNotFound" />
<mvc:view-controller path="/dataAccessFailure" />

<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"
p:defaultErrorView="uncaughtException">
<property name="exceptionMappings">
<props>
<prop key=".DataAccessException">dataAccessFailure</prop>
<prop key=".NoSuchRequestHandlingMethodException">resourceNotFound</prop>
<prop key=".TypeMismatchException">resourceNotFound</prop>
<prop key=".MissingServletRequestParameterException">resourceNotFound</prop>
</props>
</property>
</bean>
<bean
class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="atom" value="application/atom+xml" />
<entry key="html" value="text/html" />
<entry key="json" value="application/json" />
</map>
</property>
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
</list>
</property>
<property name="defaultViews">
<list>
<bean
class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
</list>
</property>
</bean>

<bean
class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
id="multipartResolver" />

</beans>

스프링 Roo에서 배낀 코드에 몇 가지 설정을 변경하고 컨텐츠 네고 설정을 추가했다. 컨텐츠 네고 설정은 토비님 책에 나오는 코드다. 머시냐면.. 웹 브라우저 주소창에 /code/list를 입력하면 JSTL 뷰를 보여주고 Ajax로 JSON 형태의 응답을 달라고 요청하면 JSON 뷰를 받을 수 있다. 정말 편하지 아니한가.. 

하지만 아마도 프로젝트 진행하는 도중 이 파일은 자주 손보게 될 것 같다. mvc:annotation에서 기본으로 등록해주는 핸들러 매핑이나 어댑터를 확장하려면 어쩔 수 없다.

9. applicationContext.xml 만들기

src 폴더에 만들어준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <context:property-placeholder location="classpath*:*.properties"/>

    <context:component-scan base-package="smdis">
        <context:exclude-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
    </context:component-scan>

    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <property name="driverClass" value="${database.driverClassName}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
    </bean>

    <tx:annotation-driven/>

    <!-- ============================================================= -->
    <!--  Hibernate                                                    -->
    <!-- ============================================================= -->
    <bean id="transactionManager"
          class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="smdis" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>

</beans>

네임스페이스 설정 빼면 별거 없다. 컴포넌트 스캔이 없던 시절은 생각하기도 싫다.

10. database.properties 파일을 만든다.

#Updated at Tue May 25 14:01:35 KST 2010
#Tue May 25 14:01:35 KST 2010
database.password=ㅇㅇㅇ
database.username=ㅇㅇㅇ
database.url=jdbc:postgresql://localhost/ㅇㅇㅇ
database.driverClassName=org.postgresql.Driver
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

DB는 PostgereSQL을 사용하기로 결정했다.

11. index.jsp 만들기

대충 Hello 찍는 JSP 파일을 만들고 끝!
드디어 코딩을 시작할 수 있겠다.

top

  1. 심상호 2010.06.01 20:10 PERM. MOD/DEL REPLY

    Maven Archetype으로 만들어놓으면 나중에 사용하기 편하겠네요.

    Favicon of http://whiteship.me BlogIcon 기선 2010.06.01 21:26 PERM MOD/DEL

    넹 deps는 빼고 나머진 아키타입으로 만들어 쓰면 좋긴한데 이 아키타입을 또 관리해야 하는 부담이 있어서;;

    뭔가 귀찮더라구요. A를 편하자고 B를 만들었더니 B를 관리하기가 귀찮아져서 일이 꼬리에 꼬리를 무는 듯한... @_@;;

    그냥 이렇게 치트코드 모아놓고 쓰는게 더 편한것 같아요.

Write a comment.


[회사일] 간단한 재고 조사 시스템 개발

프로젝트/SLT : 2010.06.01 17:38



현재 재고조사는 엑셀로 하고 있다. 엑셀에 이미지들을 잔뜩 붙여놓고 쉬트마다 또 복사해서 이미지를 잔뜩 붙여서 현재 창고에 총 수량이 몇개 남았는지 매일 매일 기록하고 있다. (내가 하고 있는게 아니라 이곳 직원들이 하고 있다.) 엑셀 용량이 50M에 달한다고 한다. 엑셀 이미지를 쉬트 한곳에만 올리고 나머진 그 링크 타고 보여주도록 하면 용량좀 줄일 수 있을 것 같은데 그렇게 되나 안되나 모르겠다. 아마도 되겠지.. 그러나 아직은 손대고 싶지 않다. 나중에 기분 좋을 때 해줘야겠다.

대표님이 원하는건 매일 창고에서 나가고 들어온 수량을 확인하고 현재 창고에 어떤 제품이 몇개나 남아있는지 알고 싶다고 하신다. 매일 저 엑셀파일로 결제를 하지만 엑셀 파일로는 위와 같은 상황이기 때문에 하루치 데이터만 유지하기도 버거운 상태다. 판매량 추이나 재고의 변화를 보려면 위해서 시스템을 필요로 하신다.

하지만 현업에 종사하시는 분들의 업무도 역시 엑셀 파일 작업만 하기도 벅찬상태다. 여기서 이 시스템에 정보를 입력해달라는 요구를 하면 나한테 짜증 낼 판국이다. 내가 시킨일도 아닌데 나한테 불평을 토로해봤자... 그래도 난 친절한 개발자니까 일단은 지금 쓰고 있는 엑셀 파일만 업로드하면 거기서 정보를 친절하게 읽어다가 DB에 넣어주도록 만들어야겠다. 그 다음엔 차차 이 시스템에 재고 정보를 입력하는데 익숙해지기를 바래야겠다.

이 시스템에서 가장 중요한 도메인은 Item 과 Inventory가 될 것 같다. 
흠냐 거럼 어디 일을 시작해볼까..
top

TAG 회사일

Write a comment.