애플리케이션 아키텍처와 객체지향

김명일·2022년 6월 3일
0

[KSUG Seminar] Growing Application - 2nd. 애플리케이션 아키텍처와 객체지향 세미나를 듣고 정리한 내용입니다. 정리를 잘못해서 꼭 들어보면 좋을 것 같아요😊

아키텍처

아키텍처의 특징

  • 프로젝트에 참여하는 개발자들이 설계에 대해 공유하는 이해를 반영하는 주관적인 개념
  • 서비스 별로 아키텍처는 다르다 → 서로 중요하게 생각하는 부분들이 다르기 때문에, 주관적이다.
  • 변경이 어렵다 → 모든 코드가 아키텍처에 따라 짜여지기 때문에, 변경이 어렵다.

레이어 아키텍처

  • 유사한 기능들을 모아 관심사를 분리하여 책임을 분리하기 때문에 레이어 교체가 가능하다는 장점이 있다.

일반적인 레이어 아키텍처

  • presentation
    • 화면 조작, 사용자의 입력 처리
  • domain
    • 비즈니스와 관련된 레이어
  • data source
    • 도메인에서 필요로 하는 데이터를 조작하는 레이어

서비스별로 레이어가 세분화 되어 여러가지 레이어가 존재할 수 있다. 서비스에 대한 정보를 많이 담고 있는 비즈니스 로직이 담겨 있는 도메인 레이어가 가장 중요하다. 따라서 도메인 레이어가 각 레이어들에 영향을 미치게된다.

그러므로 서비스에 있어서 도메인을 어떻게 코드로 잘 반영하는가가 중요하다.


도메인 레이어 설계를 어떻게?

도메인 레이어 설계 방법에는 크게 2가지가 있다.

  • 절차지향
    • Transaction Script 패턴이라고 한다.
    • 순서에 따라 로직을 작성하는 방식
    • 한 곳에서 전체적인 로직을 확인할 수 있다.
    • 데이터와 프로세스를 따로 생각하는 방식 → 필요한 데이터를 준비하고 프로세스를 진행하는 방식
    • 서비스내에서 필요한 데이터를 준비하고 프로세스를 진행하는 구조
  • 객체지향
    • Domain Model 패턴이라고 한다.
    • 단순히 객체지향언어를 쓰는 것과는 다르다. 객체지향언어를 쓰더라도 객체지향적으로 설계할 수 있고 절차지향적으로 설계할 수 있다.
    • 데이터와 프로세스를 하나의 객체로 보고 사용하는 방식.
    • 적절한 객체에 적절한 책임을 분배하여 기능을 제공하는 구조
    • 데이터는 객체에 있지만 객체의 상태를 변경하거나 로직을 실행하는건 서비스 레이어이다.

도메인 로직과 어플리케이션 로직이란?

도메인 로직

  • 객체들의 행동 및 처리

어플리케이션 로직

  • 주문이후 이메일을 보내거나, 푸시알림을 보내는 등의 플로우를 의미
  • 도메인 로직 전후로 도메인이 바뀌지 않더라도 바뀔 수 있는 부분을 의미
  • 이러한 로직을 객체내에 넣게되면 의존성이 생기기 때문에 따로 떼어낸다. ⇒ 도메인 로직이 디비나 네트워크에 의존성이 생기면서 테스트가 어려워지기 때문이다.(즉, 결합도가 올라가고 응집도가 떨어지게 된다.)

⇒ 즉, 도메인 레이어를 캡슐화하여 다른 로직이 침투할 수 없도록 해야한다. 이를 위해 레이어를 하나 추가하고 서비스 레이어라고 한다.


도메인 모델의 바람직한 서비스레이어의 모습

async function order({customerId, productId, count}: {customerId:number, productId:number, count:number){
	// 필요한 데이터를 가져온다. (도메인 로직을 위한 준비)
	const customer = await this.customerRepository.findByIdOrFail(customerId);
	const product = await this.productRepository.findByIdOrFail(productId);
	
	// 도메인 객체에 필요한 행위를 위임한다.
	const order = customer.order(product, count);
	
	// 결과를 받아 저장한다. (도메인 로직에 대한 후처리)
	this.orderRepository.save(order);

	return order;
}
  • 트랜잭션 스크립트는 트랜잭션관리, 어플리케이션 로직, 도메인 로직이 모두 서비스안에 들어있게 된다. 그렇기 때문에 별도의 서비스레이어가 필요하지 않게 된다.

트랜잭션 스크립트와 도메인 모델의 장단점

트랜잭션 스크립트

예시)

async function order({customerId, productId, count, point}: {customerId:number, productId:number, count:number, point: number){
	// 필요한 데이터를 가져온다. (도메인 로직을 위한 준비)
	const customer = await this.customerRepository.findByIdOrFail(customerId);
	const product = await this.productRepository.findByIdOrFail(productId);
  	const rules = await this.productRuleRepository.findByProductId(productId);
	
  // 필요한 프로세스 진행
	if(!customer.hasPoint(point)){
  		throw new ApiError(httpStatus.BAD_REQUEST, ErrorInfo.NOT_ENOUGH_POINT);               
	}
  
  	if(!product.isOrderable()){
    	throw new ApiError(httpStatus.BAD_REQUEST, ErrorInfo.INVALID_PRODUCT_STATUS);
  	}
  
  	const discountPrice = rules.reduce((prev,cur)=>{
      if(cur.isValidRule()){
      	return prev + cur.getDiscountPrice(product);  
      }
      return prev;
    },0)
  
  	const order = new Order();
  	order.userId = customer.id;
  	order.productId = product.id;
  	order.price = (product.getPrice() * count) - discountPrice;
	
	
	// 저장
	this.orderRepository.save(order);

	return order;
}

장점

  • 심플하고 한 곳에 모여있기 때문에 해당하는 부분의 코드만 살펴보면 된다.

단점

  • 기존에 있던 코드를 수정하는것이 대부분이기 때문에, 수정에 대한 걱정과 두려움이 생긴다.
  • 특정 행위에 대한 내용이 드러나지 않는다. ( ex. 할인금액을 계산(getCalculateDiscountPrice)하는데, 쿠폰을 한개만 사용가능한지, 여러개 사용가능한 지 등과 같은 요구사항이 명확하게 드러나지 않음. 내부로직을 보며 유추할 뿐이다. )
  • 추가적으로 알림메시지나 메일전송 등 부가적인 행위들이 추가됨에 따라 서비스가 더 복잡해진다.

도메인 모델

예시)

async function order({customerId, productId, count}: {customerId:number, productId:number, count:number){
	// 필요한 데이터를 가져온다. (도메인 로직을 위한 준비)
	const customer = await this.customerRepository.findByIdOrFail(customerId);
	const product = await this.productRepository.findByIdOrFail(productId);
	
	// 도메인 객체에 필요한 행위를 위임한다.
	const order = customer.order(product, count);
	
	// 결과를 받아 저장한다. (도메인 로직에 대한 후처리)
	this.orderRepository.save(order);

	return order;
}

장점

  • 기존에 소스가 잘 작성되어 있다면, 수정시 기존 소스코드의 수정없이 변경할 수 있음(OCP)
  • 추상화를 잘했다면, 상위 모듈이 하위 모듈에 대해 의존성을 갖지 않음 (DIP)
  • 암묵적인 개념이 코드상으로 표시가 됨

단점

  • 한번 추상화를 통해 결정되면, 변경하기 어려워진다. → 이후 부터 개발들이 해당 추상화 위에서 개발되기 때문에, 변경이 어렵다.
  • 다른사람이 짠 코드를 이해하기가 쉽지 않다.

대부분의 초기 개발 시, 요구사항은 계속 변경되고 어떻게 변경될지 알 수 없다. 객체지향으로 작성했다고해서 요구사항을 쉽게 반영할 수 있는 경우는 거의 없다. 항상 틀어지게 된다. 그러므로 최대한 단순하게 코드를 짜야하며 변경될 때마다 계속해서 리팩터링이 이루어져야 한다.

계속해서 변경해가다가 어떤식으로 변경이 되겠구나에 대한 판단이 서면 객체지향적인 코드가 많이 도움을 줄 수 있다.

👏🏻느낀점

지금까지 controller, service를 나눠야 한다는 생각만 가지고 나눠왔지만, 강의를 듣고나니 왜 controller와 service를 나눠야 하는지 생각해보게 되었다.

  • 캡슐화
  • 재사용성
  • 테스트의 용이성

이 주된 이유라고 생각한다.

회사 서비스 당시, 회사코드는 controller와 service로 폴더 구조가 나누어져있었다. 위 강의 내용에 따르면, 트랜잭션 스크립트 방식으로 짜여져있었고 작성했던 service들은 서비스레이어가 아니라 본질적으로 도메인 레이어라고 보는게 맞는것 같다.

그리고 당시에는 data source레이어, 즉 repositroy나 dao를 따로 가지고 있지 않았다. 재사용을 위해 service내부에 데이터를 불러오는 메서드, 데이터의 상태를 확인하는 메서드 등을 만들게 되었다. 그로 인해, 서비스 폴더가 점점 커져갔고 관리가 힘들다고 느꼈다. 그치만 다른 무언가를 추가해 나눠야할지, 추가한다면 어떤 구조를 추가해야할지 도저히 감이 잡히지 않았고, 추가하지 않았었다.

이 후, nest와 orm을 사용하는 방향으로 전환했기에 이런문제들은 어느정도 해결되었지만, 생각해보면 nest로 전환하는게 아닌 단순히 data source레이어를 추가하고, 가져온 데이터를 맵핑하여 사용했더라면, 더 실용적이지 않았을까 라는 생각도 들었다.

profile
주니어 백엔드 🐶🦶🏻📏

0개의 댓글