[KSUG Seminar] Growing Application - 2nd. 애플리케이션 아키텍처와 객체지향

SungBum Park·2019년 11월 21일
3

세미나 정리

목록 보기
1/3
post-thumbnail

강연 영상을 통해 객체 지향과 절차 지향의 차이를 코드를 통해 매우 직관적으로 설명해주어 이해하기 좋았다. 그리고 왜 서비스 레이어가 나오게 되었는지를 알 수 있었다. 전반적으로 말씀을 너무 잘하셔서 도움이 많이 되었다.

아키텍처

  • 아키텍처: 프로젝트에 참여하는 개발자 들이 설계에 대해 공유하는 이해 를 반영하는 주관적 인 개념
  • 아키텍처는 주관적인 것이다. 같은 아키텍쳐라고 한다고 해도 서로 다른 팀에서 말하는 것이 세부적으로 다를 수 있다.
  • 아키텍처를 변경하는 것은 매우 많은 비용이 든다. 왜냐하면 모든 개발자들이 현재 아키텍처를 공유하고 있기 때문이다.
  • 아키텍처는 가급적 일찍 올바르게 결정해야 한다.
  • 아키텍처는 관심사의 분리이다.
    • 관심사의 분리는 서로 다르고 관련이 없는 책임들을 분리하는 것이다.

레이어드 아키텍처

  • 레이어드 아키텍처: 서로 다른 관심사를 분리한 아키텍처이다.
  • 유사한 것들을 한 레이어에 모아놨다.
  • 유연해지고 재사용성이 높아졌다.
  • 레이어는 팀마다 다를 수 있다. 세분화가 가능하다.
  • 여기서 가장 중요한 레이어는 도메인이다.
    • 도메인을 가장 적절하게 적용한 시스템이 성공한다.
  • 도메인 레이어를 설계하는 방법은 대표적으로 절차 지향, 객체 지향이 있다.
    • 절차 지향적으로 개발하는 것이 대부분이다.
  • 절차 지향: 트랜잭션 스크립트
  • 객체 지향: 도메인 모델
  • 트랜잭션 스크립트와 도메인 모델은 마틴 파울러가 지은 패턴의 이름이다.

영화 예매 도메인

도메인 개념

  • 영화(Movie)
    • 제목, 상영시간
    • 영화는 메타 데이터라고 볼 수 있다.
  • 상영(Showing)
    • 영화, 상영일시 등
    • 고객이 실제로 예매하는 것은 영화가 아니라 상영이다.
  • 할인 정책(Discount)
    • Amount Discount: 특정한 금액을 빼줌
    • Percent Discount: 현재 금액의 특정한 비율을 빼줌
  • 할인 규칙(Rule)
    • Sequence Rule: 10회 상영인 경우 할인
    • Time Rule: 무슨 요일 몇시인 경우 할인
  • 예매(Reservation)
    • 영화, 상영 정보, 금액, 인원 등
    • 고객이 최종적으로 받을 정보

  • Discount는 있을 수도 있고 없을 수도 있지만, 하나만 사용 가능하다.
  • Rule은 여러 개가 있을 수 있다.
  • Discount가 있으면 반드시 Rule이 존재한다.

  • 결과적으로 예메라는 도메인이 반환된다.

  • 도메인은 개발할 대상이다.

트랜잭션 스크립트(절차 지향)

  • 데이터와 이를 조작하는 프로세스가 따로 동작한다.
  • 데이터와 프로세스를 따로 고민한다.

  • 각 데이블에 필요한 DAO를 구현한다.

절차적인 예제 로직

@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    1) 데이터 베이스로부터 Movie, Showing, Rule 조회
    2) Showing에 적용할 수 있는 Rule이 존재하는지 확인
    3) if(Rule이 존재하면) {
            Discount를 읽어 할인된 요금계산
       } else {
            Movie의 정가를 이용해 요금 계산
       }
    4) Reservation을 생성해 데이터베이스에 저장
}
  • 대부분 위처럼 절차적으로 구현한다.
  • 도메인이 복잡해지면 객체지향이 적합하다.
  1. 데이터 베이스로부터 Movie, Showing, Rule 조회
@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    // 1) 데이터 베이스로부터 Movie, Showing, Rule 조회
    // 필요한 데이터를 모두 준비한다.
    Showing showing = ShowingDAO.selectShowing(showingId);
    Movie movie = MovieDAO.selectMovie(showing.getMovieId());
    List<Rule> rules = ruleDAO.selectRules(movie.getId());

    // ...
}
  1. Showing에 적용할 수 있는 Rule이 존재하는지 확인
@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    Showing showing = ShowingDAO.selectShowing(showingId);
    Movie movie = MovieDAO.selectMovie(showing.getMovieId());
    List<Rule> rules = ruleDAO.selectRules(movie.getId());

    // 2) Showing에 적용할 수 있는 Rule이 존재하는지 확인
    Rule rule = findRule(showing, rules);

    // ...
}

private Rule findRule(Showing showing, List<Rule> rules) {
  for (Rule rule : rules) {
    if (rule.isTimeOfDayRule()) {  // 시간 할인 규칙
      if (showing.isDayOfWeek(rule.getDayOfWeek()) &&
        showing.isDurationBetween(rule.getStartTime(), rule.getEndTime())) {
          return rule;
        }
    } else {                       // 횟수 할인 규
      if (rule.getSequence() == showing.getSequence()) {
        return rule;
      }
    }
  }
}
  1. Rule 존재 여부
@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    Showing showing = ShowingDAO.selectShowing(showingId);
    Movie movie = MovieDAO.selectMovie(showing.getMovieId());
    List<Rule> rules = ruleDAO.selectRules(movie.getId());

    Rule rule = findRule(showing, rules);

    /*
    3) if(Rule이 존재하면) {
            Discount를 읽어 할인된 요금계산
       } else {
            Movie의 정가를 이용해 요금 계산
       }
    */
    Money fee = movie.getFee();
    if (rule != null) {   // Rule이 null이 아니면 할인
        fee = calculateFee(movie);
    }

    // ...
}

private Money calculateFee(Movie movie) {
  Discount discount = DiscountDAO.selectDiscount(movie.getId());

  Money discountFee = Money.ZERO;
  if (discount != null) {
    if (discount.isAmountType()) {
      discountFee = Money.wons(discount.getFee());
    } else if (discount.isPercentType()) {
      discountFee = movie.getFee().times(discount.getPercent());
    }
  }

  return movie.getFee().minus(discountFee);
}
  1. Reservation을 생성해 데이터베이스에 저장
@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    Showing showing = ShowingDAO.selectShowing(showingId);
    Movie movie = MovieDAO.selectMovie(showing.getMovieId());
    List<Rule> rules = ruleDAO.selectRules(movie.getId());

    Rule rule = findRule(showing, rules);

    Money fee = movie.getFee();
    if (rule != null) {   // Rule이 null이 아니면 할인
        fee = calculateFee(movie);
    }

    // 4) Reservation을 생성해 데이터베이스에 저장
    Reservation result = makeReservation(customerId, showingId, audienceCount, fee);
    reservationDAO.insert(result);

    return result;
}

private Reservation makeReservation(int customerId, int showingId, int audienceCount, Money payment) {
  Reservation result = new Reservation();
  result.setCustomerId(customerId);
  result.setShowingId(showingId);
  result.setAudienceCount(audienceCount);
  result.setFee(payment);

  return result;
}
  • 데이터를 가져오고 로직을 실행하는 것이 서비스 하나에서 모두 수행된다.
  • 수 많은 if-else 로직이 존재한다.

  • 절차지향적 시퀀스 다이어그램은 서비스에서 모든 로직이 이루어진다.

도메인 모델(객체 지향)

  • 프로세스와 데이터를 하나의 덩어리로 묶어서 생각한다.
  • 객체지향 기반의 도메인 레이어 설계
  • 객체지향은 협력하는 객체들의 공동체이다.
    • 역할을 나누고 책임을 부여한다. 그리고 여러 객체들이 협력하여 로직을 수행한다.

  • 객체 하나만으로는 아무것도 하지 못한다.
  • 객체 사이 협력을 하는 매개체는 메시지 이다.

  • 실제로 만질 수 있는 물리적인 물체를 사용하여 객체들의 상호작용을 설계하였다.(실제로는 종이를 사용했다고 한다.)
  • 객체를 나타내는 종이들을 실제로 옮기고 수정하면서 설계를 하였다고 한다.
  • 객체지향은 데이터에 대한 이야기를 하지 않는다. 어떤 역할이 필요한지 어떤 객체가 필요한지 이야기한다.
  • 객체는 상태와 행동을 같이 가지고 있다.
    • 그 상태를 가장 밀접하게 사용하는 행동을 그 객체에 배치해야 한다. 아니면 캡슐화가 깨진다.

  • 관련된 데이터를 가장 많이 가지고 있는 객체에 행동을 할당한다.
  • 객체를 만드는 순서는 애플리케이션의 요구사항을 분석한 후 적절한 객체를 선정한다.
  • 예매 생성은 상영에게 책임을 할당한다. 왜냐하면 가장 많은 데이터를 가지고 있기 때문이다.
    • 이런 생성 패턴이 애매하면 팩토리를 만들수도 있다.

  • 영화 가격을 알고 있는 영화 객체에 할당한다.

  • Discount Strategy 객체를 만들어서 할당해준다.

  • Rule에 할당한다.
  • Rule은 Discount와 협력한다.

  • 애플리케이션 요구사항을 바탕으로 객체들을 생성하고 책임을 할당하고 협력하도록 한다.
  • 객체간의 협력을 통해 애플리케이션을 구현하는 것이 객체지향이다.

객체 지향 구현

public class Showing {
    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, audienceCount);
    }

    public Money calculateFee() {
        return movie.calculateFee(this);
    }
}

public class Reservation {
    Reservation(Customer customer, Showing showing, int audienceCount) {
        this.customer = customer;
        this.showing = showing;
        this.fee = showing.calculateFee().times(audienceCount);
        this.audienceCount = audienceCount;
    }
}

public class Movie {
    public Money calculateFee(Showing showing) {
        return fee.minus(discountStrategy.calculateDiscountFee(showing));
    }
}

할인 전략

public abstract class DiscountStrategy {
    public Money calculateDiscountFee (Showing showing) {
        for(Rule each : rules) {
            if(each.isStatisfiedBy(showing)) {
                return getDiscountFee(showing);
            }
        }

        return Money.ZERO;
    }

    abstract protected Money getDiscountFee(Showing showing);
}

public class AmountDiscountStrategy extends DiscountStrategy {
    protected Money getDiscountFee (Showing showing) {
        return discountAmount;
    }
}

public class NonDiscountStrategy extends DiscountStrategy {
    protected Money getDiscountFee (Showing showing) {
        return Money.ZERO;
    }
}

public class PercentDiscountStrategy extends DiscountStrategy {
    protected Money getDiscountFee (Showing showing) {
        return showing.getFixedFee().times(percent);
    }
}

할인 규칙

public interface Rule {
    boolean isStatisfiedBy(Showing showing);
}

public class SequenceRule implements Rule {
    public boolean isStatisfiedBy(Showing showing) {
        return showing.isSequence(sequence);
    }
}

public class TimeOfDayRule implements Rule {
    public boolean isStatisfiedBy(Showing showing) {
        return showing.isPlayingOn(dayOfWeek) && Interval.closed(startTime, endTime).includes(showing.getPlayingInterval());
    }
}
  • Reservation을 보면 가격을 계산하는 것을 Showing에게 할당한다.
  • Showing은 Movie에 대한 정보를 가지고 있지 않기 때문에 Movie에게 메시지를 보낸다.
  • Movie는 Discount를 가지고 있고, 이 객체에게 계산해달라고 부탁한다.
  • Discount Strategy는 Rule이 있는지 찾는다.
  • Rule은 인터페이스이며, 두가지로 나뉜다. (시퀀스, 타임)
  • Discount Strategy를 상속받은 객체가 할인을 직접 적용한다.

  • 객체지향으로 구현한다. -> 도메인 개념과 비슷한 객체 관계를 가진다.

  • 트랜잭션 스크립트와는 다른 시퀀스 다이어그램을 볼 수 있다.

도메인 레이어와 아키텍처

  • 도메인 모델을 사용할 때
    • 할인 정보는 실제로 DB에 있다.
    • DB 정보를 객체에 밀어넣을 필요가 있다.
  • 위처럼 도메인 로직을 처리할 때 필요한 전후 작업이 있다.
    • 이러한 전후 작업을 애플리케이션 로직이라 한다.(애플리케이션 flow)
  • 애플리케이션 로직은 도메인에 넣지 않는다.
    • 의존성이 생기기 때문이다. 이러면 응집도가 떨어진다.
    • 결과적으로 도메인 레이어를 캡슐화해야 한다. 숨겨야 한다.
  • 기본적으로 도메인과 무관한 로직이 도메인에 침투하지 못하도록 막아야한다.
  • 이러한 애플리케이션 로직을 모아놓은 곳이 서비스 레이어 이다.

  • 도메인 모델 패턴으로 만들면 서비스 레이어가 생긴다.
  • 서비스 레이어는 애플리케이션 로직을 처리한다.
  • 이렇게 분리하면 하나의 도메인 레이어를 다양한 서비스 레이어에서 재사용이 가능하다.

  • 트랜잭션도 애플리케이션 로직이라고 볼 수 있다.
  • 서비스 레이어를 만들면서 트랜잭션 경계가 생긴다.
  • 서비스 레이어에서 트랜잭션 경계를 결정한다.
// 도메인 모델에서 서비스 레이어 로직
@Transactional
public Reservation reserveShowing(int reserverId, int showingId, int audienceCount) {
  Customer reserver = customerRepository.find(reserverId);
  Showing showing = showingRepository.find(showingId);

  Reservation reservation = showing.reserve(reserver, audienceCount);

  reservationRepository.save(reservation);

  return reservation;
}
  • 서비스 레이어는 도메인을 준비하는 데 필요한 작업을 처리하는 전처리와 도메인 예외 등을 처리하는 후처리가 있다.
  • 결국 관심사의 분리이다.

트랜잭션 스크립트를 사용할 떄

@Transactional
public Reservation reserveShowing(int customerId, int showingId, int audienceCount) {
    Showing showing = ShowingDAO.selectShowing(showingId);
    Movie movie = MovieDAO.selectMovie(showing.getMovieId());
    List<Rule> rules = ruleDAO.selectRules(movie.getId());

    Rule rule = findRule(showing, rules);

    Money fee = movie.getFee();
    if (rule != null) {
        fee = calculateFee(movie);
    }

    Reservation result = makeReservation(customerId, showingId, audienceCount, fee);
    reservationDAO.insert(result);

    return result;
}
  • 트랜잭션 스크립트는 트랜잭션, 도메인, 애플리케이션 로직이 한 곳에 있다.
  • 별도의 서비스 레이어가 필요없다.
  • 본질적으로 위 코드는 도메인 레이어이다.

  • 대부분 관계형 데이터베이스를 사용한다.
  • 도메인 모델을 사용할 때 테이블 설계는 어렵다.

  • 객체 모델과 DB 스키마 사이의 불일치를 말한다.
  • 객체 패러다임과 관계 패러다임의 불일치
    • 객체 패러다임: 객체들의 행위 중심, 객체는 유연성, 재사용성 중점
    • 관계 패러다임: 데이터 관점은 중복 제거 중점
  • 객체 지향적으로 구현할수록 관계 패러다임과 멀어진다.

  • 해결 방법으로는 Data Mapper를 둔다.(ORM)
    • 객체와 DB(관계) 관계를 끊는다.
    • Data Mapper를 직접 구현하기 어려우므로 이미 만들어진 ORM을 쓴다.
  • 대표적으로 JPA가 있다.
    • JPA를 사용한다고 해도 순수 객체 지향으로 구현하기는 불가능하다.

  • 트랜잭션 스크립트
    • getter/setter를 가진 Anemic Domain Model을 사이에 둔다.
    • 각 테이블마다 개별 DAO를 만든다.(테이블 데이터 게이트웨이)

선택의 기로에서

  • 변하지 않는 것은 없다.

만약 위 영화 예매 시스템에서 할인을 중복해주고 싶다면?

트랜잭션 스크립트

private Money calculateFee(Movie movie) {
  List<Discount> discounts = DiscountDAO.selectDiscounts(movie.getId());

  Money discountFee = Money.ZERO;
  for (Discount discount : discounts) {
    if (discount != null) {
      if (discount.isAmountType()) {
        discountFee = Money.wons(discount.getFee());
      } else if (discount.isPercentType()) {
        discountFee = movie.getFee().times(discount.getPercent());
      }
    }
  }

  return movie.getFee().minus(discountFee);
}
  • 트랜잭션 스크립트는 기존의 코드를 수정해야한다.
    • for문이 추가되었다.
  • 기존 코드를 수정한다는 것에 대한 두려움이 크다.
    • 특히, 테스트 코드가 없다면 더욱 심화된다.
  • 개념이 암묵적으로 숨겨진다.
    • 중복할인 로직을 코드에서 찾을 수 없다.
    • 전체 로직을 모두 읽어봐야 된다.
    • What이 아닌 How를 판단해야 한다.

도메인 모델

public class OverlappedDiscountStrategy extends DiscountStrategy {
  // ...
}
  • 도메인 모델은 기존의 코드를 수정하지 않고, Composite 디자인 패턴으로 해결할 수 있다.
    • 기존의 코드를 수정하지 않고 기능을 확장할 수 있으므로, 이는 OCP 를 준수했다.
  • 개념을 명시적으로 표현할 수 있다.
    • OverlappedDiscountStrategy라는 이름의 클래스를 추가했으므로, 이름만 보고 알 수 있다.

  • 상위 레벨이 하위 레벨의 의존성을 가지지 않는다.
  • 의존성 방향이 추상화(하이 레벨)로 간다.
  • 의존성 역전 원칙(DIP)

거짓말

  • 영화 예매 시스템을 통해 도메인 모델의 장점을 쉽게 볼 수 있었다.
  • 하지만 이는 요구사항이 어떻게 변경되는지 알 수 있기 때문에 위와 같은 설계가 가능하다.
  • 대부분의 애플리케이션에서 요구사항은 어떻게 변화될지 아무도 모른다.
  • 코드를 최대한 간단하게 구현해야 한다.
    • 요구사항이 어떻게 변경될지 알 수 없으므로
  • 변경이 필요할 때마다 항상 리팩토링이 필요하다.
  • 하지만, 변경의 방향을 알 수 있다면 객체지향 설계는 변경에 매우 유연한 코드를 만들어준다.
    • 변경의 방향을 알기 위해서는 경험을 쌓아야 한다.

참고자료

profile
https://parker1609.github.io/ 블로그 이전

0개의 댓글