mvc 에서 business 의 복잡도에 따라 service layer 가 비대해지는 현상.

2015-10-12 11:22

현업하면서 느끼는 부분인데... 일반적으로 service layer 에서는 아래의 상황이 반복되지 않나 싶습니다.

  1. db select 를 위한 parameter 검증.
  2. select 를 통한 model get(vo, dto 등)
  3. business 로직에 따라 model data에 의존적인 parameter 검증.
  4. model data 에 parameter set 등
  5. db update, 혹은 insert.

대개 controller 에서 넘어온 데이터를 다른 service 와 repository 를 호출하고 모델만들고 db insert, update 등이 발생하는 것.. 결국 퍼사드패턴의 성격을 띈다고 생각합니다.

이렇게 작업을 하다 보니 드는 생각은.. model 이란 것에서 setter나 생성자 쪽에서 parameter 검증을 하면 어떨까 싶은 부분이 있습니다. 상수화하기 곤란한 부분이 있다거나 무척 복잡한 검증이 필요한 프로세스라면 service 에서 진행하는게 맞다고 봅니다만. model 의 다른 field 값에 의존적이라던가.. (이런 경우도 조금 곤란할 수 있긴 하겠네요... field setting 이 순서가 있어야 한다거나 할테니까요. 하지만 setter 함수를 따로 만들어 여러 인자를 받을 수 있긴 하겠습니다만..) 맥시멈이나 미니멈이 나와있는 값이라던가 ...

이런걸 setter 나 혹은 다른 네이밍을 가져가서 set 할 때 파라미터 검증을 해주면 어떨까 싶습니다. 그렇게 되면 아마 exception throw가 모델에서 발생할 수 있겠네요.

다른 service 나 repository access 는 몇 줄 안되는데 각 파라미터 검증이 코드의 4/5이상을 차지하는데... 결국 model 의 데이터 검증을 위해 service layer가 무척 비대해지는 느낌입니다.

mvc 에는 이게 맞을지 모르겠으나 객체지향적인 건지 의문이네요.

아니면 제가 오해하고 작성하는 것일 수도 있구요.

(model 에서는 순수 setter, getter 만이 존재하며 throw 가 있거나 하는 business 로직은 없어야 한다는...)

다른 분들은 이런 현상이 없는지... 어떻게 작업중인지 여쭙고 싶었습니다 ^^;;

7개의 의견 from SLiPP

2015-10-13 22:54

개인적인 생각으로 OOP에서 중요한건 객체의 상태라고 생각합니다. 객체 내부에 들어갈 데이터의 검증이 외부에서 이루어지는것이 아니라 (개방폐쇄의 원칙에 위배. 유지보수 어려워짐) OOP의 사상처럼 객체에게 메세지를 보내는(질문하는) 형태로 이루어질 수 있을것 같은데요.

1.파라미터 체크 예를들면 회원가입을 위해 User라는 모델객체가 있다면 User를 생성하기전에 주입되는 데이터를 외부에서 체크하는게 아니라 우선 전부 대입한뒤 if (user.canRegistable())와 같이 활용해볼 수 있지 않을까요? 세부적인 아이디, 패스워드, 닉네임 등의 정합성 등의 로직도 객체내에 존재하구요

2.복잡한 객체, 상수객체 생성 또, 상수객체가 필요한 경우라면 (이럴경우에는 setter가 존재하지 않는 immutable한 value object가 되겠지요) 생성자 호출과정을 통해 객체를 생성하는동안 exception을 가져도 될것 같습니다. 만약 객체생성에 복잡한 과정이 필요하다면 차라리 builder Pattern을 사용하는게 나을것 같구요.

3.Check, UnCheck, Throw 어떤부분을 체크할지 혹은 체크하지말지, exception을 throw할지를 결정하는건 참 어려운것 같네요 ^^ 데이터베이스 조회를 위한 key값 전달과 같이 비즈니스로직에 꼭 필요한 데이터라면 RuntimeException을 일으키고, 앞단에 이러한 Exception을 모아서 처리하게끔 하는것도 하나의 대안이될 수 있을것 같습니다.

2015-10-14 10:18

@한석봉 나도 너의 의견에 백퍼 공감하고 그렇게 생각을 하고 있는데... 그러다보니 드는 고민은 그럼 자칫 객체가 다른 객체에 의존하는 상황이 필연적으로 발생할 느낌이라. 그렇게까지 가면 설계 자체가 실패한거 아닌가 싶을 수도 있겠지만... 근데 생각해보면 아주 범용적인 모델이 아니라 우린 도메인에 필요한 객체(모델)들을 만드는게 일반적인데, 그렇다면 객체간에 의존성이 있어도 되지 않나 싶기도 하고... 어렵다 ㅠㅠ 그래서 여러 의견을 듣고 싶었음. ㅎㅎ

2015-10-15 18:48

이런 내용을 글로 설명하고 이해하라고 하다니.. 난 소스 코드를 보면서 이야기하고 싶다.

소스 코드를 공개해라. 그럼 더 의미있는 논의가 될 수도 있겠다. 개발자는 글이 아니라 코드로 이야기하고 싶다. ㅋㅋ

2015-10-15 20:31

@자바지기 샘플코드를 만들어보려고 이리저리 짱구를 굴려보았는데... 쉽지가 않네요 ^^; 회사 코드를 올리기엔 좀 부담되어서. 조만간 올리겠습니다 ㅎㅎ

2015-10-16 01:02

@자바지기 급조하느라 잘못된 코드가 여럿 있습니다.

sendWishProduct 라는 메소드는 친구관계를 맺고 있는 사용자에게

자신의 장바구니에 있는 상품을 친구의 장바구니에 보내주는 것입니다. 조건은 아래와 같습니다.

조건1. 이미 친구의 장바구니에 담긴 상품은 친구 장바구니에 담겨져선 안된다.

조건2. 장바구니 최대 품목 수는 100개이다.

조건3. 품목별로 담을 수 있는 갯수는 판매자가 가진 재고에 한한다.

조건4. 품목별로 구매자의 등급에 따라 노출 여부가 정해진다.

spring 의 resolver 등을 활용하면 friend, product 등은 controller 에서 예외처리가 가능할 것 같습니다.

몇몇 급조하느라 엉망으로 작성된 코드들을 제외하고 가장 아래 model의 field 에 대한 유효성체크가 핵심입니다.

public void sendWishProduct(int friendID, int productId, int amount) throws ServletException {
		
	Friend friend = friendService.getFriend(friendID);
	if (friend == null) {
		throw new ServletException("NO_SUCH_FIREND");
	}
	
	Product product = productService.getProduct(productId);
	if (product == null) {
		throw new ServletException("NO_SUCH_PRODUCT");
	}
	
	// OOP 적이지 못한 코드... grade 에서 처리할 문제같음.
	if (friend.getGrade().getIntGrade() < product.getGrade().getIntGrade()) {
		throw new ServletException("LACK_FRIEND_GRADE");
	}
		
	// 아래의 코드는 여기 서비스가 아닌, productService 라던가 하는 코드에서
        // context가 사용자(친구)일 때에 amount 체크를 하는게 더 옳다고 생각됨.
	// 여기서는 validation check 가 아니라 insert 로직만 있으면 됨.
	if (amount > product.getAmount()) {
		throw new ServletException("LACK_PRODUCT_AMOUNT");
	}

	Basket basket = friend.getBasket();
	// !!!! 질문의 핵심 코드, basket 모델에서 capacity 를 가지는게 옳다고 봄.
        // !!!! 아래 addProduct 에서 아래 exception 처리가 옳다고 봄..
	if (basket.getCount() > BASKET_CAPACITY) {
		throw new ServletException("EXCEED_FRIEND_BASKET_CAPACITY");
	}
	basket.addProduct(productId, amount);
	basketRepository.update(basket);
}

위 코드와는 달리 addProduct 를 객체안으로 넣게 될 경우 모델에선 exception throw 가 생기게되고,

점차 다른 모델들과 의존성을 가지게 되는데...

이걸 좋지 않게 보는 경우가 있어서 다른 분들은 어떻게 작업하는지 여쭙고 싶었습니다.

2015-10-16 07:09

위 코드와는 달리 addProduct 를 객체안으로 넣게 될 경우 모델에선 exception throw 가 생기게되고, 점차 다른 모델들과 의존성을 가지게 되는데...

객체들간의 의존성을 가져야 좀 더 객체 지향적인 코드가 나오고 Service 코드는 좀 더 깔끔해 질 수 있다.

Basket basket = friend.getBasket();
if (basket.getCount() > BASKET_CAPACITY) {
    throw new ServletException("EXCEED_FRIEND_BASKET_CAPACITY");
}
basket.addProduct(productId, amount);
basketRepository.update(basket);

내가 요구사항을 명확하게 이해하지 못하지만 위 코드는 다음과 같이 구현할 수 있다고 본다.

friend.addProudct(productId, amount);
friendRepository.update(friend);

이와 같이 구현하고 friend가 basket와 의존 관계를 가지고 있기 때문에 friend의 addProduct()가 다음과 같이 비스므리하게 구현하겠지.

addProduct(long productId, int amount) {
    if (basket.overCapacity()) {
        throw new OverCapacityException("EXCEED_FRIEND_BASKET_CAPACITY");
    }
    basket.addProduct(productId, amount);
}

이 정도 구현이 맞다고 생각한다.

상당 수의 개발자들을 보면 객체 지향 개념이 없는 경우 객체 간의 의존관계를 통해 문제를 해결하기 보다는 네 코드와 같이 구현하려는 경향이 있다. 이와 같은 이유는 아직까지 절차지향적인 사고를 하고 있기 때문이라고 생각한다. 절차지향적으로 구현하면 Service 코드는 비대해질 수 밖에 없다.

네 코드의 상당 수도 각 Service나 해당 객체로 모두 위임할 수 있는 부분이다. 설득하기 보다는 네가 구현하는 부분을 좀 더 객체 지향적으로 구현해서 깔끔한 코드를 보여주는 것이 가장 확실한 방법이라고 생각한다.

단, MyBatis 사용하는 경우 생각보다 객체지향적으로 구현하는 것이 쉽지 않기 때문에 더 많은 노력이 필요하다. 이 참에 JPA로 갈아타라.

2015-10-20 08:44

@자바지기 fried 안의 basket 에 대해선 현재 mybatis 를 사용하여서 생각을 못하고 있던 부분인데. ORM 참 매력적이네여. http://www.slipp.net/wiki/display/SLS/JPA jpa 에 대해서 잘 정리가 되어있네여!! jpa 용 브런치하나 따서 따로 한 부분만 작업을 좀 진행해봐야겠네여. 감사합니다. ㅎㅎ

의견 추가하기

연관태그

← 목록으로