고전적인 Spring MVC에서

Controller -> Service Interface -> Service Implement -> Dao (또는 Dao Interface -> Dao Implement) -> DB

이런식으로 구현한 소스가 흔했었는데요, 살펴보면 대부분의 경우가 Service Interface 하나에 구현체도 하나, Dao도 그렇게 일대일로 개발해 놓은 경우가 많았습니다.

OOP Polymorphism 사상에서 본다면 Service 구현체가 여러가지 케이스가 있을 수 있기 때문에 그렇게 개발하는 것이라고 생각했지만, 그동안 여러곳의 프로잭트를 경험하면서 구현체가 여러개 였던 적이 한번도 없습니다. 대부분 그때 그때 조건에 따라 if~else if~ else 이러식으로 필요한 부분에서 분기를 해서 로직을 처리 하는 경우가 대부분 이었습니다.

단순한 웹페이지의 경우 하나의 화면을 만들기 위해서

  • Controller 메서드 한개
  • Service 인터페이스 메소드 원형 한개
  • Service 구현체에 매소드 한개, 내용은 Dao 메소드 호출 후 리턴하는 코드 한줄
  • Dao 인터페이스 메소드 원형 한개
  • Dao 구현체에 매소드 한개, 내용은 쿼리맵을 호출해서 결과를 리턴하는 코드 한줄
  • 결과를 보여주는 페이지

거의 모든 페이지가 이렇게 개발되어 있는 홈페이지 소스를 본적이 있는데... 유지보수를 하면서 조회 쿼리에 파라메터를 String에서 콘렉션으로 바꾸려면 콘트롤러에 Service를 호출 하는 부분부터 줄줄이 수정 해야 했습니다.

너무 비합리적인 구조인데 왜 이렇게 만들었을까... 해묵은 논잭이긴 하지만 저랑 비슷한 생각을 하시는 개발자들이 많았었는지 요즘은 서비스나 DAO에 Interface를 만들지 않고 바로 클래스를 호출하는 방식으로 간편하게 많이들 하시는 것 같습니다.

어디서 본 소스에는 Controller에 비즈니스 로직을 모두 구현하고 Controller에서 바로 DAO를 호출 하는 경우도 본적이 있는데... 스프링 MVC 사상에는 어긋나긴 하지만... 예외처리와 Data Rollback만 잘 되게 해놓았다면 문제 없다는 생각입니다.

그런데 조금 아쉬운게... 서비스에서 상황별로 여러가지 분기를 해야 하는 경우 if ~ else if ~ else 이렇게 구현하다 보니 소스도 엄청 복잡해지고 OOP 답지 않게 후져 보이고... 로직이 많이 복잡한경우 if ~ else fi 블럭사이에 수백줄의 코드가 들어가야 하는 경우도 있습니다.

얼마전 그런일이 있어서 이것을 펙토리 패텬으로 바꿔 볼까 해서 시작했는데...

상품을 관리하는 프로그램인데 상품이 딱 정해진게 아니고 계속 늘어나는 상황입니다. 편의상 A상품, B상품, C상품... E상품 이렇게 향후 계속 상품이 늘어나는 상황인데 A상품과 B상품은 로직의 유사도가 높아서 하나의 ProductService 라는 클래스에 담았는데 C상품은 기본적인 내용외에 처리해야 하는 로직이 상당히 다른 형태였습니다. 앞서 말한대로 if ~ else if ~ else 형태로 가기에는 소스가 너무 지저분 해지는 상황이 되었습니다.

ProductController.java

@Autowired 
ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    return new ResponseEntity(productService.getProductInfo(prodCd), HttpStatus.OK); 
} 

 

ProductService.java

public Map<String,Object> getProductInfo(String prodCd) { 

    Map<String,Object> productInfo = null; 

    // 공통 처리 로직 

    switch (prodCd) { 
        case "A" : 
            // A상품 로직 
            break; 
        case "B" : 
            // B상품 로직 
            break; 
        case "C" : 
            // C상품 로직 
            break; 
         case ... 
    } 

    return productInfo; 
} 

대충 이런 상황이었습니다.

상품별로 처리해야 하는 로직이 늘어나거나, 상품에 대한 공통 로직과 상품개별 조직을 번갈아 가면서 수행해야 하는 경우 중간 중간에 계속 분기문이 나타나서 엄청 지저분하고 가족성이 떨어지는 코드가 되어 버렸습니다.

결국 상품 서비스 인터페이스를 만들고 상품별로 각기 다른 서비스를 구현체로 분리하고 간단하게 Factory Pattern을 적용해서 콘트롤러에서 상품 서비스 인터페이스를 이용하여 호출 하는 방식으로 구현하기로 생각했습니다.

ProductService.java (상품 서비스 인터페이스)

public interface ProductService { 
    public Map<String,Object> getProductInfo(String prodCd); 
} 

 

AProductServiceImpl.java (A상품 서비스 구현체)

@Autowired 
ProductDao productDao; 

public class AProductServiceImpl implements ProductService { 
    @Override 
    public Map<String,Object> getProductInfo(String prodCd) { 

        // 로직 처리 

        Map<String,Object> productInfo = productDao.getProductInfo(prodCd); 

        // 로직 처리 

        return productInfo; 
    } 
} 

 

그리고 이 서비스들을 상품코드에 따라 골라쓸 ProductServiceFactory.java

public ProcuctService getService(String prodCd) { 
    switch (prodCd) { 
        case "A" : return new AProductService(); 
        case "B" : return new BProductService(); 
        case "C" : return new CProductService(); 
    } 
} 

 

콘트롤러의 내용입니다.

ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    return new ResponseEntity(ServiceFactory.getService(prodCd).getProductInfo(prodCd), HttpStatus.OK); 
} 

대략 이런 식으로 구현했던 것 같은데. 기억을 더듬어 급하게 만든거라 생략된 부분도 많고 실제 동작하는 소스는 아닙니다.

그런데 별로 마음에 안드는게... 상품이 신규로 늘어날 때 마다 상품별 Service Class만 새로 만들어 주면 되는게 아니고, 몇줄 안되기 하지만 ProductServiceFactory.java에 코드를 몇출 추가해햐 합니다. 좋은 방법이 없을까 고민하다가 런타임에 클래스를 동적으로 로딩할 수 있는 Class.forName(...) 이 생각났습니다.

Class.forName(...)은 낮설지가 않은게 Java를 처음 배울 때 JDBC 연결을 처음 구현하면서 대부분의 개발자들이 사용해 보는 방식입니다.

Class.forName("com.mysql.jdbc.Dirver");

이런 코드 생각나시죠?


Service Factory를 바꿨습니다.

public ProcuctService getService(String prodCd) { 
    return Class.forName("mypackage.service."+prodCd+"ProductService"); 
} 

정확한 코드는 기억이 안나고 이렇게 비슷하게... 했던 것 같네요.

그리고 실행을 해보니 아뿔싸... ProductService에 Autowired로 선언되어 있는 prodDao가 null 입니다.

그렇습니다 스프링의 Autowired 어노테이션은 스프링이 처음 실행되면서 콤포넌트 스켄에 지정된 위치의 콤포넌트 들을 메모리에 로드하면서 이 어노페이션을 만나면 해당 프로포티를 자동으로 초기화 시켜주는 역활을 합니다.

디버그 모드로 톰켓을 올리면 우리가 만든 Controller, Service, Dao 들이 메모리에 올라가는 것이 콘솔창에 보입니다.

제가 사용하고자 하는 방식은 스프링 런타임 중에 사용자의 선택에 따라 실시간으로 클래스를 로딩하는 방식을 생각했기 때분에 내부에 Autowired 어노테이션은 작동하지 않은 것입니다.

Autowired 어노테이션을 사용하지 않고 productDao를 new 로 생성해서 사용하는 방법도 있었지만 웬지 후져 보이고 이런 생각을 나만 한것이 아닐 것 이라 생각하고 여러가지 키워드로 구글링을 해보았습니다.

생각보다 원하는 방식의 예제를 찾기가 매우 어려웠습니다. 혹시 스프링에서 다른 더 좋은 깔끔한 방식이 있는 것인지? 아니면 동적으로 클래스를 로드하는 것에 대한 성능이슈가 있는 것인지는 모르겠지만, 상당히 오랜 기간 이런 저런 키워드로 구글링을 한 기억이 납니다. 역시나 Stackoverflow 에서 원하는 답을 찾았습니다.

org.springframework.beans.factory.config.AutowireCapableBeanFactory

이런 놈이 있었네요, 이놈을 가지고 이렇게 만들어 봤습니다.

ProductServiceFactory.java 에 넣으려고 하다가... 클래스의 Full 패키지 네임을 넣으면 해당 빈을 리턴하는 공통용으로 만들어 보자 싶어서 

DynamicAutowiredImplementsService.java

@Service 
public class DynamicAutowiredImplementsService { 

    @Autowired 
    private AutowireCapableBeanFactory beanFactory; 
     
    public Object getBean(String beanFullName) { 
        Object bean = null; 
         
        try { 

            Class<?> beanClass = Class.forName(beanFullName); 
             
            bean = beanFactory.createBean(beanClass, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, false); 
            beanFactory.initializeBean(bean, beanFullName); 
             
        } catch (Exception e) { 
            throw new IllegalArgumentException("Couldn't instantiate class " + beanFullName, e); 
        } 
        return bean; 
    } 
}

 

그리코 콘트롤러에서 

@Autowired 
DynamicAutowiredImplementsService dynamicAutowiredImplementsService;

//ProductService productService; 

@RequestMapping("/productInfo") 
@ResponseBody 
public ResponseEntity<Map<String,Object> productInfo ( 
             @RequestParam(value="prodCd", required=true) String prodCd) { 

    ProductService productService = dynamicAutowiredImplementsService("mypackage.service."+prdtCd+"ProductService");
    return new ResponseEntity(productService.getProductInfo(prodCd), HttpStatus.OK); 
} 

이렇게 했습니다.

결과는? 잘~ 동작합니다.

추가 (커뮤니티에 올리니까 바로 문제점을 지적해 주시는 고수 분들이 계시네요, 콘트롤러에서 ProductService 를 전역변수로 사용하면서 경합이 발생할 경우 문제가 될 수 있습니다. 이현재 ProductService 는 매소드 내에 지역변수로 이동 했고 DynamicAutowiredImplementsService 역시 경합이 발생할 경우 문제가 될 수 있습니다. 이것은 어떻게 할 지 고민을 좀 해야봐 할 것 같습니다.)

부하 테스트까지 해보지 않아서... 사실 Autowired로 주입한 것과 어떤 차이가 있는지 검증은 못해 봤습니다. 그리고 스프링에서 Polymophism을 구현하는 방식으로 이렇게 하는 것이 바람직 한 것인지는 짧은 식견으로 좀 더 연구를 해봐야 겠지만... 만들고 있는 어플리케이션이 동시접속자가 많다기 보다는 로직이 매우 복잡하고 다양한 케이스 였기 때문에 상품이 추가 될 때 Service쪽만 새롭게 만들어 주면 되어서 유지보수성이 많이 향상된 것 같습니다.

보시는 분 중에 혹시라도 문제점이나 잘못된 곳이 있다면 지적 부탁드립니다.

읽어주셔서 감사합니다.

+ Recent posts