[회사일] GenericController 만들기

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…

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

@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 마무리하러 가야겠습니다.

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

앞에서 만들었던 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)를 하면 안된느가?

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

이번엔 바로 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 입니다. 캬.. 간단하군요.

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

이제 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도 만들어야지