들어가며

엘라스틱서치를 클러스터로 운영하게 되면 다음과 같이 몇가지 문제를 맞이 할 수 있습니다

  1. 디스크 문제
  2. 메모리 문제
  3. 샤드 할당 문제
  4. 기타 등등

클러스터의 경우 마스터노드와 데이터노드 코디네이터 노드를 여러대 운영하기 때문에 노드의 추가와 제거가 가능합니다
다만, 간단한 작업의 경우 서버 재시작보다는 rolling restart 방법을 고려해 보는게 좋습니다.


문제상황 (예시)

  • 여기선, 단순히 heap 메모리 증설의 경우를 들어 설명하겠습니다.
  • 데이터 노드의 heap 메모리 부족
  • 먼저, 물리 메모리가 확보 됐는지 확인하고, 가능하다면 heap 메모리를 늘려주자

해결방안

메모리 증설은 해당 노드만 종료후 재실행시켜주면 되기에
엘라스틱서치 전체 재시작보다는 rolling-restart 방식을 취하는 것이 좋습니다

쿼리는 kibana 기준으로 작성 되어 있습니다

  • Step1 heap 메모리 재할당
    # path /elasticsearch/data/config/jvm.options
    Xms4g
    Xmx4g

샤드는 프라이머리, 레플리카로 구성됩니다.
노드가 종료되면 샤드 재배치라는 고비용 작업이 이루어지게
rolling-restart 작업을 할 시에는 샤드 재할당 기능을 종료시켜 놓습니다

  • Step2 샤드 할당 비활성

    # kibana 쿼리 / ES 6.4.2 # 노드를 중단했을때 샤드들이 재배치 되지 않도록 설정 
    PUT _cluster/settings { 
      "persistent": {     
          "cluster.routing.allocation.enable": "none" 
      } 
    }
  • Step3 세그먼트 동기화

    # kibana 쿼리 / ES 6.4.2 # primary - replica 간의 세그먼트 저장 상태를 동기화 
    POST _flush/synced
  • Step4 ES 재실행
    재실행시 elasticsearch의 권한을 잘 확인해보는 것이 좋습니다!!

  • Step5 샤드 할당 활성

    # kibana 쿼리 / ES 6.4.2 # unassigned 된 샤드 재배치 
    PUT _cluster/settings 
    { 
      "persistent": { 
          "cluster.routing.allocation.enable": null 
      } 
    }
  • step6 헬스 체크

    GET _cat/health

참조

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

14장. 일관성 있는 협력


객체지향 패러다임의 장점은 설계를 재사용할 수 있다는 것이다.
하지만 재사용은 꽁짜로 얻어지지 않는다.
재사용을 위해서는 객체들의 협력 방식을 일관성 있게 만들어야 한다

아래 예제를 통해 일관성 있는 협력 패턴이, 이해하기 쉽고 직관적이라는 것을 알아보자


01. 핸드폰 과금 시스템 변경하기

기본 정책 확장

11장에서 구현한 핸드폰 과금 시스템의 요금 정책을 아래와 같이 수정해보자

  • 고정요금 방식
    • ex) 10초당 18원
  • 시간대별 방식
    • ex) 00 ~ 19시 10초당 18원
    • ex) 19 ~ 24시 10초당 15원
  • 요일별 방식
    • ex) 평일 10초당 38원
    • ex) 공휴일 10초당 19원
  • 구간별 방식
    • ex) 초기 1분 10초당 50원
    • ex) 1분 이후 10초당 20원

조합 가능한 요금 계산 순서


위의 사진처럼 무수히 많은 조합이 나오기에 설계가 중요해졌다.

클래스 구조

고정요금 방식 구현하기

가장 간단한 고정요금이다. 일반요금제와 동일하다.

public class FixedFeePolicy extends BasicRatePolicy {
    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

시간대별 방식 구현하기

시간대별 방식에 따라 요금을 계산하기 위해서는 통화 기간을 정해진 시간대별로 나눈후 시간대별로 서로 다른 계산 규칙을 적용해야 한다.
시간대별 방식의 통화 요금일 계산하기 위해서는 통화의 시작, 종료시간, 시작일자, 종료일자도 함께 고려되어야 한다.

시간대별 통화 시간을 관리하는 클래스 DateTimeInterval

public class DateTimeInterval {
    public static DateTimeInterval of(LocalDateTime from, LocalDateTime to) {
        return new DateTimeInterval(from, to);
    }

    public static DateTimeInterval toMidnight(LocalDateTime from) {
        return new DateTimeInterval(from, LocalDateTime.of(from.toLocalDate(), LocalTime.of(23, 59, 59, 999_999_999)));
    }

    public static DateTimeInterval fromMidnight(LocalDateTime to) {
        return new DateTimeInterval(LocalDateTime.of(to.toLocalDate(), LocalTime.of(0, 0)), to);
    }

    public static DateTimeInterval during(LocalDate date) {
        return new DateTimeInterval(
                LocalDateTime.of(date, LocalTime.of(0, 0)),
                LocalDateTime.of(date, LocalTime.of(23, 59, 59, 999_999_999)));
    }

    private DateTimeInterval(LocalDateTime from, LocalDateTime to) {
        this.from = from;
        this.to = to;
    }

    public Duration duration() {
        return Duration.between(from, to);
    }

    public LocalDateTime getFrom() {
        return from;
    }

    public LocalDateTime getTo() {
        return to;
    }

    public List<DateTimeInterval> splitByDay() {
        if (days() > 0) {
            return split(days());
        }
        return Arrays.asList(this);
    }

    private long days() {
        return Duration.between(from.toLocalDate().atStartOfDay(), to.toLocalDate().atStartOfDay()).toDays();
    }

    private List<DateTimeInterval> split(long days) {
        List<DateTimeInterval> result = new ArrayList<>();
        addFirstDay(result);
        addMiddleDays(result, days);
        addLastDay(result);
        return result;
    }

    private void addFirstDay(List<DateTimeInterval> result) {
        result.add(DateTimeInterval.toMidnight(from));
    }

    private void addMiddleDays(List<DateTimeInterval> result, long days) {
        for(int loop=1; loop < days; loop++) {
result.add(DateTimeInterval.during(from.toLocalDate().plusDays(loop)));
        }
    }

    private void addLastDay(List<DateTimeInterval> result) {
        result.add(DateTimeInterval.fromMidnight(to));
    }

    public String toString() {
        return "[ " + from + " - " + to + " ]";
    }
}

요일별 방식 구현하기

요일별 방식은 요일별로 요금 규칙을 다르세 설정할 수 있다.
각 규칙은 요일의 목록, 단위 시간, 단위 요금이라는 3가지 요소로 구성된다.

public class DayOfWeekDiscountRule {
    public Money calculate(DateTimeInterval interval) {
        if (dayOfWeeks.contains(interval.getFrom().getDayOfWeek())) {
            return amount.times(interval.duration().getSeconds() / duration.getSeconds());
        }
        return Money.ZERO;
    }
}

요일별 방식 역시 통화 기간이 여러 날에 걸쳐 있을 수 있다.
시간대별 방식과 동일하게 통화 기간을 날짜 경계로 분리하고 각 통화 기간을 요일별로 설정된 요금 정책에 따라 적절하게 계산해야 한다.

public class DayOfWeekDiscountPolicy extends BasicRatePolicy {
    @Override
    protected Money calculateCallFee(Call call) {
        Money result = Money.ZERO;
        for(DateTimeInterval interval : call.getInterval().splitByDay()) {
            for(DayOfWeekDiscountRule rule: rules) { result.plus(rule.calculate(interval));
            }
        }
        return result;
    }
}

잠시 지금까지 구현한 고정요금, 시간대별, 요일별 방식의 클래스를 다시 살펴보자.
겉으로 보기에는 문제가 없이 잘 구현된 것 같아 보인다. FixedFeePolicy, TimeOfDayDiscountPolicy, DayOfWeekDiscountPolicy
세 클래스는 통화 요금을 정확하게 계산하고 있고 응집도와 결합도 측면에서도 특별히 문제는 없어 보인다.
그러나 이 클래스들을 함께 모아놓고 보면 문제점이 보인다

문제는 이 클래스들이 유사한 문제를 해결하고 있음에도 설계의 일관성이 없다는 것이다


02. 설계에 일관성 부여하기

설계에 일관성을 부여하기 위해서는 다음과 같은 연습이 필요하다

  • 다양한 설계 경험 익히기
  • 디자인 패턴을 학습하고 변경이라는 문맥안에 적용해보기
  • 협력을 일관성 있게 만들기 위해 다음과 같은 기본 지침을 따르기
    • 변하는 개념을 변하지 않는 개념으로부터 분리
    • 변하는 개념을 캡슐화

조건 로직 대 객체 탐색

먼저, 조건 로직에 대해서 코드를 통해서 살펴보자.
4장에서 나왔던 ReservationAgency 예제이다.

public class ReservationAgency {
    public Reservation reserve(...) {
        for (DiscountCondition condition : movie,getDiscountConditions()) {
            if (condition.getType() == DiscountCondtionType.PERIDOD) {
                // 기간조건
            } else {
                // 회차조건
            }
        }

        if (discountable) {
            switch(movie.getMovieType)) {
                case AMOUNT_DISCOUNT: // 금액 할인 정책
                case PERCENT_DISCOUNT: // 비율 할인 정책
                case NONE_DISCOUNT: // 할인 정책 없음
            }
        } else {
            // 할인 정책이 불가능한 경우
        }
    }
}

위와 같이 조건 로직으로 구현한다면 변경에 취약하고 유지보수의 어려움성이 생긴다.
조건로직이란 위와 같이 'if ~ else' 를 통해 비즈니스 로직에 덕지덕지 붙여 놓은 것이다.

우리는 추상화와 다형성이라는 것을 배웠기에 인터페이스를 이용해 객체 탐색으로 변경할 수 있다.

public class Movie {
    private DiscountPolicy discountPolicy;
    public Money calculteMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

이처럼 다형성은 조건 로직을 객체 사이의 이동으로 바꾸어 준다.
따라서, 할인 조건에 맞는 메시지만 주고 받으면 되기 때문에 변경이 용이해진다


03. 일관성 있는 기본 정책 구현하기

변경 분리하기

일관성 있는 협력을 만들기 위한 첫 단계는 변하는 개념과 변치 않는 개념을 분리하는 것이다
앞에서 본 핸드폰 과금 시스템의 기본 정책에서 변하는 부분과 변하지 않는 부분을 다시 살펴보자


단위요금이 시간당 요금을 계산하는 반면, 적용조건은 형식이 다르다른 것을 알 수 있다.
이것으로 적용조건은 변하는 부분이고, 단위요금은 변치 않는 다는 것으로 분리할 수 있다는 것을 유추할 수 있다.

변경 캡슐화하기

협력을 일관성 있게 만들기 위해서는 변경을 캡슐화해서 파급효과를 줄여야 한다.
여기서 변하지 않는 것은 ‘규칙’이다. 변하는 것은 ‘적용조건’이다.
따라서 규칙으로부터 적용조건을 분리하여 추상화한 후 시간대별, 요일별, 구간별 방식을 이 추상화의 서브타입으로 만들어야 한다.
그 후에 규칙이 적용조건을 표현하는 추상화를 합성관계로 연결하는 것이 객체의 캡슐화이다.

추상화 수준에서 협력 패턴 구현하기

먼저, ‘적용조건’을 표현하는 추상화인 FeeCondtion

public interface FeeCondition {
    List<DateTimeInterval> findTimeIntervals(Call call);
}

다음으로 FeeFule, 단위요금과 적용조건을 인스턴스변수로 사용한다

public class FeeRule {
    private FeeCondition feeCondition;
    private FeePerDuration feePerDuration;

    public Money calculateFee(Call call) {
        return feeCondition.findTimeIntervals(call)
                .stream()
                .map(each -> feePerDuration.calculate(each))
                .reduce(Money.ZERO, (first, second) -> first.plus(second));
    }
}

FeePerDuartion클래스는 단위 시간당 요금이라는 개념을 표현한다.
또한, 이정보를 이용해 일정기간의 요금을 계산하는 calculate 메서드를 구현한다

public class FeePerDuration {
    private Money fee;
    private Duration duration;

    public Money calculate(DateTimeInterval interval) {
        return fee.times(Math.ceil((double)interval.duration().toNanos() / duration.toNanos()));
    }
}

구체적인 협력 구현하기

이제 맨 처음 앞서 구현한 구체적인 협력 클래스를 인터페이스를 이용해 구현해보자
TimeOfDayFeeCondition 시간대별 정책

public class TimeOfDayFeeCondition implements FeeCondition {
    private LocalTime from;
    private LocalTime to;

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        return call.getInterval().splitByDay()
                .stream()
                .filter(each -> from(each).isBefore(to(each)))
                .map(each -> DateTimeInterval.of(LocalDateTime.of(each.getFrom().toLocalDate(), from(each)),
              LocalDateTime.of(each.getTo().toLocalDate(), to(each))))
                .collect(Collectors.toList());
    }

    private LocalTime from(DateTimeInterval interval) {
        return interval.getFrom().toLocalTime().isBefore(from) ?
                from : interval.getFrom().toLocalTime();
    }

    private LocalTime to(DateTimeInterval interval) {
        return interval.getTo().toLocalTime().isAfter(to) ?
                to : interval.getTo().toLocalTime();
    }
}

DayOfWeekFeeCondition, 요일별정책

public class DayOfWeekFeeCondition implements FeeCondition {
    private List<DayOfWeek> dayOfWeeks = new ArrayList<>();

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        return call.getInterval()
                .splitByDay()
                .stream()
                .filter(each ->
 dayOfWeeks.contains(each.getFrom().getDayOfWeek()))
                .collect(Collectors.toList());
    }
}

DurationFeeCondition 구간별정책

public class DurationFeeCondition implements FeeCondition {
    private Duration from;
    private Duration to;

    @Override
    public List<DateTimeInterval> findTimeIntervals(Call call) {
        if (call.getInterval().duration().compareTo(from) < 0) {
            return Collections.emptyList();
        }

        return Arrays.asList(DateTimeInterval.of(
                call.getInterval().getFrom().plus(from),
                call.getInterval().duration().compareTo(to) > 0 ?
                       call.getInterval().getFrom().plus(to) :
                        call.getInterval().getTo()));
    }
}

맨처음 짯던 정책들과 비교를 해보자. 각 클래스마다 일관성이 생기게 되었다.
우리는 변하는 개념과 변치 않는 개념을 분리해서 변경을 캡슐화 하였다.
이처럼 변경을 캡슐화해 협력을 일관성 있게 만들면 어떤 장점을 얻을 수 있는지 명확하게 보여준다


마찬가지로, 추가적인 정책이 필요하다면 FeeCondition으로부터 구현을 해주면 된다.

일관성 있는 협력의 핵심은 변경을 분리하고 캡슐화하는 것이다.
변경을 캡슐화 하는 방법이 협력에 참여하는 객체들의 역할과 책임을 결정하고 이렇게 결정된 협력이 코드의 구조를 결정한다.
따라서, 변경의 방향을 파악할 수 있는 감각을 기르는 것이 중요하다.


참조

  • 조영호님의 오브젝트 '일관성 있는 협력'

'Books > Object' 카테고리의 다른 글

[Object] 일관성 있는 협력  (0) 2021.08.16
[Object] 다형성  (0) 2021.08.12
[OBJECT] 합성과 유연한 설계  (2) 2021.08.08
[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Controlling Step Flow


들어가며

이전에 우리는 Job이 n개의 Step으로 구성되어 있다는 것을 확인하였습니다.
Step은 일반적으로 순차적인 흐름으로 진행되지만, 조건에 따라 흐름을 제어할 수도 있습니다.
이번 포스팅에선 Step의 흐름 제어에 대해 알아보도록 하겠습니다.


Sequential Flow

일반적인 Step의 Flow입니다

@Bean
public Job job() {
    return this.jobBuilderFactory.get("job")
                .start(stepA())
                .next(stepB())
                .next(stepC())
                .build();
}
  • start() 메서드를 이용해 첫번째 Step을 호출합니다
  • next() 메서드를 이용해 다음 Step을 호출합니다
  • 우리는 Step이 A -> B -> C 순서로 진행되는 것을 쉽게 알 수 있습니다.

Conditional Flow

위 예제에서는 두가지 시나리오만 가능합니다

  1. Step이 성공하고 다음 Step이 실행됩니다
  2. Step이 실패하고, job이 실패됩니다

일반적으론 위의 flow로도 충분합니다.
하지만, Step이 실패할경우 job을 실패로 끝내기 보다
다른 Step을 호출한다면 어떻게 될까요???
아래의 Step flow를 확인 해보겠습니다

@Bean
public Job job() {
    return this.jobBuilderFactory.get("job")
                .start(stepA())
                    .on("*")
                    .to(stepB())
                .from(stepA())
                    .on("FAILED")
                    .to(stepC())
                .end()
                .build();
}
  • Step A가 성공할 경우 StepB가 실행됩니다.
  • Step A가 실패할 경우 StepC가 실행됩니다.

메서드가 직관적이기 때문에 무엇을 의미하는지 알 수 있습니다.
좀더 정확한 의미를 알기 위해 위의 코드를 잠시 살펴보겠습니다.

  • start()
    • Job의 Step 구성으로부터 시작할 Step을 정의
  • on()
    • ExitStatus를 Catch
    • '*'는 모든 ExitStatus를 정의
    • Status는 BatchStatus가 아닌 ExitStatus가 온다!!!
  • to()
    • 다음으로 이동할 Step을 정의
  • from()
    • Step의 상태값을 보고 일치할경우 캐치
  • end()
    • FlowBuilder를 종료
      이렇듯, 약간의 코드 추가로 Step의 실패가 Job의 실패로 유도되지 않도록 설계할 수 있습니다.

ExitStatus 설정!!!

Job Flow를 설정하면서 가장 많은 실수를 하는 부분이 이 부분이 아닐까 합니다!!

return stepBuilderFactory.get("stepA")
    .tasklet((contribution, chunkContext) -> {
        //////////////////////////////////////////////
        contribution.setExitStatus(ExitStatus.FAILED);
        //////////////////////////////////////////////
        return RepeatStatus.FINISHED;
    })

on() 메서드가 캐치하는 상태값은 Batch Status가 아닌 Exit Status입니다.
따라서 Step의 Flow를 컨트롤할 경우 setExitStatus() 메서드를 통해 상태값을 정의해야 합니다!!

public void setExitStatus(ExitStatus status) {
    this.exitStatus = status;
}

참조

jojoldu님 블로그 - 4. Spring Batch 가이드 - Spring Batch Job Flow
spring.batch.io - Configuring a Step

#Spring/Batch/Step-flow

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

[Object] 다형성

Books/Object 2021. 8. 12. 00:48

12장 다형성


01. 다형성

다형성(Polymorphism)
‘많은’ - poly
‘형태’ - ‘morph’의 합성어로 많은 형태를 가질 수 있는 능력을 의미

‘Computer Science’ 에서는 다형성을 하나의 추상 인터페이스에 대해 코드를 작성하고,
이 추상 인터페이스에 대해 서로 다른 구현을 연결할 수 있는 능력으로 정의한다.

'객체지향 프로그래밍'에서는 다형성을 4개로 분류하여 정의한다.

  • 오버로딩 다형성
    • 하나의 클래스 안에 동일한 이름의 메서드가 존재하는 경우
  • 강제 다형성
    • 자동적인 타입 변환 방식
  • 매개변수 다형성
    • 제네릭 프로그맹과 관련이 깊다
  • 포함 다형성
    • 메시지가 동일하더라도 수신한 객체의 타입에 따라 실제 수행되는 행동이 달라진다
    • 서브타입 다형성이라고도 부른다

02. 상속의 양면성

객체지향 프로그램을 작성하기 위해서는 항상 데이터와 행동이라는 두가지 관점을 함께 고려해야 한다.
상속의 경우도 마찬가지이다.

  • 부모 클래스에 정의한 모든 데이터를 자식 클래스의 인스터에 포함시킬 수 있다 -> 데이터 관점의 상속
  • 데이터 뿐만 아니라 메서드도 포함 시킬 수 있다 -> 행동 관점의 상속

상속은 부모 클래스에서 정의한 것을 자동적으로
공유하고 재사용 할 수 있는 메커니즘으로 보이지만,
이 관점은 상속을 오해한 것

  • 상속의 목적은 코드 재사용이 아니다
  • 상속은 프로그램을 구성하는 개념들 기반으로 다형성을 가능하게 하는 타입 계층을 구축하기 위한 것
  • 타입 계층에 대한 고민 없이 상속을 이용하면 유지보수가 어려워 진다

상속을 사용한 강의 평가

상속의 메카니즘을 이해하기 위해 예제를 살펴보자. 수강생들의 성적을 계산하는 프로그램

Lecture.java

public class Lecture {
    private int pass; // 이수여부 판단할 기준 점수
    private String title; // 과목명
    private List<Integer> scores = new ArrayList<>(); // 학생들의 성적 리스트

    public Lecture(String title, int pass, List<Integer> scores) {
        this.title = title;
        this.pass = pass;
        this.scores = scores;
    }

    public double average() {
        return scores.stream().mapToInt(Integer::intValue).average().orElse(0);
    }

    public List<Integer> getScores() {
        return Collections.unmodifiableList(scores);
    }

    public String evaluate() {
        return String.format("Pass:%d Fail:%d", passCount(), failCount());
    }

    private long passCount() {
        return scores.stream().filter(score -> score >= pass).count();
    }

    private long failCount() {
        return scores.size() - passCount();
    }
}

이수 기준 70점, 5명의 대한 설정 통계

Lecture lecture = new Lecture("객체지향 프로그래밍", 70, Arrays.asList(81, 95, 75, 50,45));
String evaluration = lecture.evaluate(); // 결과 => "Pass:3, Fail:2"


다음으로 좀더 기능을 확장해보자. Lecture의 출력 결과에 등급별 통계를 추가

GradeLecture.java

public class GradeLecture extends Lecture {
    private List<Grade> grades;

    public GradeLecture(String name, int pass, List<Grade> grades, List<Integer> scores) {
        super(name, pass, scores);
        this.grades = grades;
    }

    @Override
    public String evaluate() {
        return super.evaluate() + ", " + gradesStatistics();
    }

    private String gradesStatistics() {
        return grades.stream().map(grade -> format(grade)).collect(joining(" "));
    }

    private String format(Grade grade) {
        return String.format("%s:%d", grade.getName(), gradeCount(grade));
    }

    private long gradeCount(Grade grade) {
        return getScores().stream().filter(grade::include).count();
    }

    public double average(String gradeName) {
        return grades.stream()
                .filter(each -> each.isName(gradeName))
                .findFirst()
                .map(this::gradeAverage)
                .orElse(0d);
    }

    private double gradeAverage(Grade grade) {
        return getScores().stream()
                .filter(grade::include)
                .mapToInt(Integer::intValue)
                .average()
                .orElse(0);
    }
}


등급의 이름과 각등긥 범위를 정의

Grade.java

public class Grade {
    private String name; // 등급
    private int upper,lower; // 상한선, 하한선

    private Grade(String name, int upper, int lower) {
        this.name = name;
        this.upper = upper;
        this.lower = lower;
    }

    public String getName() {
        return name;
    }

    public boolean isName(String name) {
        return this.name.equals(name);
    }

    public boolean include(int score) {
        return score >= lower && score <= upper;
    }
}

GradeLecture 클래스의 evaluate 메서드를 살펴 보자. 부모의 evaluate를 재정의하여 사용하고 있다.
이처럼 동일한 시그니처의 메서드를 재정의해서 부모 클래스의 구현을 새로운 구현으로 대체하는 것을 메서드 오버라이딩이라 한다

Lecture와 GradeLecture의 average를 살펴보자. 메서드 이름은 같지만 시그니처가 다르다.
둘은 대체되지 않으며 호출하는 방법이 다르므로 공존하게 된다. 이처럼 메서드 이름은 동일하지만 시그니처는 다른 메서드를 메서드 오버로딩이라 한다

데이터 관점의 상속

위에서 구현한 클래스를 인스턴스로 생성해보자

Lecture lecture = new Lecture("객체지향 프로그래밍", 70, Arrays.asList(81, 95, 75, 50,45));
Lecture lecture = new GradeLecture("객체지향 프로그래밍",
    70, Arrays.asList(    new Grade("A", 100, 95),
                        new Grade("B", 94, 80),
                        new Grade("C", 79, 70),
                        new Grade("D", 69, 50),
                        new Grade("F", 49, 0)),
        Arrays.asList(81, 95, 75, 50, 45));

아래 사진과 같이 데이의 관점에서 표현할 수 있다


즉, 자식 클래스의 인스턴스는 자동으로 부모 클래스에서 정의한 모든 인스턴스 변수를 내부에 포함하게 된다

행동 관점의 상속

행동 관점의 상속은 부모 클래스가 정의한 일부 메서드를 자식 클래스의 메서드로 포함시키는 것을 의미한다. 부모 클래스의 모든 퍼블릭 메서드는 자식 클래스의 퍼블릭 인터페이스에 포함된다.


03. 업캐스팅과 동적 바인딩

같은 메시지, 다른 메서드

성적 계산 프로그램에 각 교수별로 강의에 대한 성적 통계 기능을 추가해 본다

Professor.java 통계를 계산

public class Professor {
    private String name;
    private Lecture lecture;

    public Professor(String name, Lecture lecture) {
        this.name = name;
        this.lecture = lecture;
    }

    public String compileStatistics() {
        return String.format("[%s] %s - Avg: %.1f", name,
                lecture.evaluate(), lecture.average());
    }
}
Professore professor = new Profeesor("다익스트라", new Lecture(...));
Professore professor = new Profeesor("다익스트라", new GradeLecture(...));

Professor의 생성자 인자 타입은 Lecture로 선언돼 있지만 Lecture, GradeLecture 모두 올수가 있다.
코드 안에서 선언된 참조 타입과 무관하게 실제로 메시지를 수신하는 객체의 타입에 따라
실행되는 메서드가 달라지는 것은 업캐스팅과 동적 바인딩 메카니즘이 작용하기 때문이다

  • 부모 클래스 타입으로 선언된 변수에서 자식 클래스의 인스턴스를 할당하는 것을 업캐스팅이라 한다
  • 선언된 변수 타입이 아니라도 메시지를 수신하는 객체의 타입에 따라 실행되는 메서드가 결정된다. 이것은 컴파일 시점이 아니라 실행시점에 결정하기 때문인데, 이를 동적 바인딩이라 한다

업캐스팅

상속을 이용하면 부모 클래스의 퍼블릭 인터페이스가 자식 클래스의 퍼빌릭 인터페이스에 합쳐지기 때문에 메시지를 자식 클래스의 인스턴스에게 전송할 수 있다.

Lecture lecture = new GradeLecture(...);

반대의 경우를 다운캐스팅(downcasting)이라 한다

Lecture lecture = new GradeLecture(...);
GradeLecture gradeLecture = (GradeLecture) lecture;

동적 바인딩

컴파일 시점이 아닌 메서드를 런타임에 결정하는 방식을 동적바인딩, 지연바인딩 이라 한다. (앞 chapter에 나와 있는 추상화와 의존성을 같이 봐두면 좋다)


04. 동적 메서드 탐색과 다형성

객체지향 시스템은 다음과 같은 규칙으로 실행할 메서드를 선택한다

  1. 먼저, 자신을 생성한 클래스에 적합한 메서드가 있는지 검사
  2. 없을 경우 부모 클래스에서 메스드를 탐색, 있을때까지 상속 계층을 따라 올라간다
  3. 최상위 계층까지 탐색 하여 없을 경우 예외 발생

자동적인 메시지 위임

상속을 이용할 경우, 메시지 수신을 처리할 대상을 자동으로 찾게 된다.
즉, 명시적으로 코드를 작성할 필요가 없고, 상속 계층에 따라 부모 클래스에게 위임 된다.
이런 관점에서 상속 계층을 정의하는 것은 메서드 탐색 경로를 정의하는 것과 동일하다.

메서드 오버라이딩

Lecture lecture = new GradeLecture(...);
lecture.evalute();

위의 코드를 실행해보자. Lecture.evaluate -> 는 GradeLecture.evaluate에 재정의 되어 있다.
실행시, self 참조에 의해 자기 자신의 클래스를 먼저 탐색하게 된다. 따라서, GradeLecture.evaluate()가 실행되게 되고, 오버라이딩된 부모 클래스의 메서드를 감추게 한다.

동적메서드 탐색이 자식 클래스에서 부모 클래스 방향으로 진행된다는 것을 기억하면 오버라이딩의 결과는 당연하다.

메서드 오버로딩

Lecture lecture = new GradeLecture(...);
lecture.avaerage();

위의 코드를 실행해보자. 오버라이딩과 마찬가지로 selft 참조를 하게 된다.
GradeLecture에는 시그니처가 다르기에 부모 클래스에서 해당 시그니처를 찾게 된다.

이처럼 시그니처가 다르기 때문에 동일한 이름의 메서드가 공존하는 경우를 오버로딩이라 부른다.

동적인 문맥

lecture.evaluate()

위의 메시지 전송만으로 어떤 클래스의 메서드가 실행될지 알 수 없다는 것을 이해하였다. 여기서 중요한 것은 메시지를 수신한 객체가 무엇이냐에 따라 메서드 탐색을 위한 문맥이 동적으로 바뀐다는 것이다. 그리고 이 동적인 문맥을 결정하는 것은 바로 메시지를 수신한 객체를 가리키는 self 참조이다

이해할 수 없는 메시지

동적 메서드 탐색을 통해, 메시지를 수신하면 부모 클래스까지 탐색하여 찾게된다. 만약, 최상위 계층까지 탐색후 원하는 메시지가 없다면 어떻게 될까?

정적 타입 언어와 이해할 수 없는 메시지

정적 타입 언어에서는 상속 계층 전체를 탐색후 메시지를 처리할 메서드를 발견하지 못하면 컴파일 에러를 발생시킨다

Lecture lecture = new GradeLecture(...);
lecture.unknownMessage(); // 컴파일 에러!!

동적 타입 언어와 이해할 수 없는 메시지

동적 타입 언어도 자식으로 부터 부모 방향으로 메서드를 탐색한다. 차이점이라면 컴파일단계가 존재하지 않아 실제 코드를 실행해보기 전엔 메시지 처리 가능 여부를 알 수 없다는 점이다.

super

Self 참조의 가장 큰 특징은 동적이라는 점이다. Self 참조는 메시지를 수신한 객체의 클래스에 따라 메서드 탐색을 위한 문맥을 실행 시점에 결정한다. self의 이런 특성과 대비해서 언급할 만한 가치가 있는 것이 super 참조이다.

자식 클래스에서 부모 클래스의 구현을 재사용해야 하는 경우가 있다. 이럴 경우, 부모 클래스의 변수나 메서드에 접근하기 위해 super 참조라는 내부 변수를 제공한다.

public class GradeLecture extends Lecture {
    @Override
    public String evaluate() {
        return super.evalute() + ", " + gradesStatistics();
    }
}

또한, super 참조를 통해 실행하고자 하는 메서드가 반드시 부모 클래스에 위치하지 않아도 되는 유연성을 제공한다.
그 메서드가 조상 클래스 어딘가에 있기만 하면 성공적으로 탐색이 가능하다


참조

  • 조영호님의 오브젝트 12장 “다형성”

'Books > Object' 카테고리의 다른 글

[Object] 일관성 있는 협력  (0) 2021.08.16
[Object] 다형성  (0) 2021.08.12
[OBJECT] 합성과 유연한 설계  (2) 2021.08.08
[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

Spring Batch chunk-oriented Step

들어가며

Step은 Job에서 독립적으로 실행되는 도메인 객체입니다. 하나의 Job에 N개의 Step이 올수 있습니다.
이전까진 Step의 tasklet을 이용해 간단한 예제를 살펴보았는데요, 이번 포스팅에선 chunk-oriented Step에 대해 알아보도록 하겠습니다.

 


Chunk-Oriented

Step은 tasklet/chunk 두가지 작업 방식을 지원합니다.
두 방식은 컨셉 차이가 있기 때문에 용도의 맞춰 프로그래밍을 하는게 좋습니다.
아래에서 tasklet, chunk 방식에 대해 간략히 알아보도록 하겠습니다.

  • tasklet
    • 단일 작업에 사용합니다
    • 일반적으로 Resource 삭제하거나 단순 쿼리를 질의하는 등 단일 작업을 수행합니다
  • chunk
    • 복잡한 작업세 사용합니다
    • Reader, Processor, Writer 3가지 단계로 구성됩니다
    • Reader, Writer는 필수이며 Processor는 선택적으로 사용됩니다
    • 즉, 데이터 읽기, 가공, 저장의 순으로 처리됩니다

간단히 두 방식의 차이점을 알아보았습니다.
tasklet은 복잡하지 않은 단일 작업에, chunk는 복잡하고 데이터를 읽어서 가공 처리해야 할 경우 사용하는 것이 적합합니다.
우리가 구현하고자 하는 대부분의 batch 작업은 복잡하고 거대하기 때문에 일반적으로 chunk-oriented 방식을 많이 사용합니다.


Chunk란?

Chunk란 데이터 작업의 묶음으로, Items를 묶어 처리되는 row수를 얘기합니다.
즉, Chunk 지향 프로세싱이란 한번에 하나씩 데이터를 읽어 Chunk 묶음을 만들고 Chunk 단위로 트랜젹선을 다루는 것을 말합니다.
Chunk 단위로 트랜잭션을 수행하기 때문에 실패할 경우엔 해당 Chunk만큼 롤백이 되고, 이전에 커밋된 트랜잭션 범위까지 반영이 된다는 것입니다.


Chunk-processing

그림으로, Chunk 지향 프로세싱의 처리를 알아보도록 하겠습니다.
Page Size 5000, Chunk Size 5000이라고 가정

  • page-size: Resource에서 읽어올 데이터 묶음 단위
  • chunk-size: 읽어온 데이터를 처리할 데이터 묶음 단위
    Page size = chunk size *n으로 구성하는 것이 좋고, 일반적으로는 page와 chunk 사이즈를 동일하게 구성합니다.
  1. 먼저, page 수 만큼 db 질의를 하여 데이터를 읽어옵니다
  2. Reader는 5000개를 질의하여 List를 가져오고, 한건씩 Processor에게 전달하여 줍니다
  3. 프로세서는 한건씩 데이터를 가공하고 5000건이 모이면 Writer에게 처리하여 줍니다.
  4. Writer를 청크사이즈만큼 일괄처리합니다.
  5. 1~4번 다시 수행
  6. data가 Null이 나올때까지 반복하여 줍니다.

Chunk-processing example

DB로부터 data를 조회할시에는 Reader로 DatabaseReader를 사용하게 됩니다. PagingItemReader, CursorItemReader 두 종류가 있습니다. 이 부분은 추후 포스팅으로 다시 정리하도록 하겠습니다

이번 포스팅에서는 baeldung 블로그에서 chunk 지향 Step에 대한 예를 가져왔습니다.
간략히 chunk 기반 시스템이 어떻게 구성되고 동작하는지만 알아보도록 하겠습니다.

@Configuration
@EnableBatchProcessing
public class ChunksConfig {

    @Autowired 
    private JobBuilderFactory jobs;

    @Autowired 
    private StepBuilderFactory steps;

    @Bean
    public ItemReader<Line> itemReader() {
        return new LineReader();
    }

    @Bean
    public ItemProcessor<Line, Line> itemProcessor() {
        return new LineProcessor();
    }

    @Bean
    public ItemWriter<Line> itemWriter() {
        return new LinesWriter();
    }

    @Bean
    protected Step processLines(ItemReader<Line> reader,
      ItemProcessor<Line, Line> processor, ItemWriter<Line> writer) {
        return steps.get("processLines").<Line, Line> chunk(2)
          .reader(reader)
          .processor(processor)
          .writer(writer)
          .build();
    }

    @Bean
    public Job job() {
        return jobs
          .get("chunksJob")
          .start(processLines(itemReader(), itemProcessor(), itemWriter()))
          .build();
    }
}
  • Job “chunksJob”이 실행됩니다
  • Job 내부에 start를 보면 독립적인 도메인은 Step을 호출합니다
  • Step “processLines“이 실행됩니다
  • processLines은 chunk 지향 Step으로 itemReader, itemProcessor, itemWriter로 구성되어 단계별로 실행됩니다.

참조

jojoldu님 블로그 - 6. Spring Batch 가이드 - Chunk 지향 처리
baeldung 블로그 tasklet vs chunk - Spring Batch - Tasklets vs Chunks | Baeldung
spring.batch.io - Configuring a Step

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

11장 합성과 유연한 설계


상속과 합성은 객체지향 프로그래밍에서 가장 널리 사용되는 코드 재사용 기법이다.

  • 상속은 부모 클래스를 재사용한다.
  • 합성은 전체를 표현하는 객체가 부분을 표현하는 객체를 포함해서 부분 객체의 코드를 재사용 한다.
  • 상속 관계는 is-a 관계라고 부르고 합성 관계는 has-a 관계라고 부른다.

상속과 합성은 이처럼 재사용 기법으로 많이 쓰이는 방법이지만 대체로 합성을 권하고 있다. 간략하게 이유를 살펴보고 합성에 대해 알아보도록 하자

먼저, 상속은 자식클래스 정의에 부모 클래스의 이름을 덧붙이는 것만으로 코드를 재사용할 수 있다. 부모클래스의 정의를 물려 받으며 코드를 추가하거나 재정의함으로써 기존 코드를 쉽게 확장할 수 있다. 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세히 알아야 하기 때문에 자식과 부모 사이의 결합도가 높아질 수 밖에 없다. 결과적으로 코드를 재사용하기 쉬운 방버이기 하지만 결합도가 높아지는 치명적인 단점이 있다.

합성은 구현에 의존하지 않는다는 점에서 상속과 다르다. 합성은 내부에 포함된 객체의 구현이 아닌 퍼블릭 인터페이스에 의존한다. 따라서, 합성을 이용하면 포함된 객체의 내부 구현이 변경되더라도 영향을 최소화 할 수 있기 때문에 더 안정적인 코드를 얻을 수 있다.

상속 관계는 클래스 사이의 정적인 관계인데 비해 합성 관계는 객체 사이의 동적인 관계이다. 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만 합성 관계는 실행 시점에 동적으로 변경할 수 있다. 따라서, 상속 대신 합성을 사용하면 변경하기 쉽고 유연한 설계를 얻을 수 있다.

01. 상속을 합성으로 변경하기

불필요한 인터페이스 상속 문제

10장에서 알아본 Stack이다.
Stack은 Vector를 상속 받아서 사용했기 때문에 문제가 발생하였다. 상속을 제거하고 Vector를 내부 변수로 바꾸어 주자.

public class Stack<E> {
    private Vector<E> elements = new Vector<>();
    public E push(E item) {
        elements.addElement(item);
        return item;
    }

    public E pop() {
        if (elements.isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() -1);
    }
}

이제 Stack의 public 인터페이스엔 10장에서 문제가 되었던 add와 같은 퍼블릭 인터페이스가 포함되지 않게 되었다.
이렇듯 상속을 합성 관계로 변경함으로써 Stack의 규칙이 어겨졌던 것을 막을수 있게 된다

메서드 오버라이딩의 오작용 문제

마찬가지로 10장에서 보았던 예제를 같이 살펴보자.
InstrumentedHashSet의 경우 부모의 메서드를 호출하여 addCount가 원하는 결과를 같지 못하는 문제가 있었다.
다만, 여기서는 해당 퍼블릭 메서드를 사용해야 하기 때문에 위에서 사용하였던 내부 인스턴스 변수로 사용할 수 없다.
이번에는 인터페이스를 이용하면 해결할 수 있다.

public class InstrumentedHashSet<E> implements Set<E> {
    private int addCount = 0;
    private Set<E> set;

    @Override
    public boolen add(E e) {
        addCount++;
        return set.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return set.addAll(c);
    }
}

HashSet에 대한 구현 결합도는 제거 되었고, 퍼블릭 인터페이스는 그대로 유지할 수 있게 되었다.

부모 클래스와 자식 클래스의 동시 수정 문제

마찬가지로 10장에서 살펴보았던 예를 같이 보도록 하겠다

public class PersonalPlaylist {
    private Playlist playlist = new Playlist();

    public void append(Song song) {
        playlist.append(song);
    }

    public void remove(Song song) {
        playlist.getTracks().remove(song);
        playlist.getSingers().remove(song.getSinger());
    }
}

합성으로 변경하더라도 가수별 노래목록을 유지하기 위해 Playlist와 PersonalPlaylist를 함께 수정해주어야 하는 문제는 해결 되지는 않았다. 그렇다 하더라도 여전히 합성을 사용하는게 좋은데, 향후에 Playlist의 내부 구현을 변경하더라도 파급효과를 최대한 PersonalPlaylist 내부로 캡슐화할 수 있기 때문이다.

간단하게 상속을 합성으로 변경하는 것에 대해 알아보았는데,
여기서 기억해야 할 것은 합성은 상속과 비교해 안정성과 유연성이라는 장점을 제공한다.
또한, 구현이 아니라 인터페이스에 의존하면 설계가 유연해진다는 것이다.


02. 상속으로 인한 조합의 폭발적인 증가

상속으로 인해 결합도가 높아지면 코드를 수정하는데 필요한 작업의 양이 과도하게 늘어나게 된다. 일반적으로 다음과 같은 두 가지 문제점이 발생한다

  • 하나의 기능을 추가하거나 수정할때 불필요하게 많은 수의 클래스를 수정한다
  • 단일 상속만 지원하는 언어에서는 상속으로 인해 오히려 중복 코드의 양이 늘어날 수 있다

기존 정책과 부가 정책 조립하기

10장에 있었던 핸드폰 요금제를 살펴보고, 추가로 부가정책(세금, 기본요금할인)을 추가한다고 가정해보자

  • 기본정책으로 - 일반요금제, 심야요금제가 존재
  • 세금 정책은 기본 정책 계산이 끝난후 계산이 끝난 결과에 세금을 부과
  • 세금 정책은 Optional 하다
  • 부가정책은 임의의 순서로 적용 가능하다

위 가정으로 조합 가능한 요금 계산방법이다. 이것을 상속으로 구현해본다고 가정해보자.


위 조합으로 구성된 클래스가 1번이고, 새로운 부가정책을 추가하게 된다면 2번과 같은 방식으로 추가가 될것이다. 자세한 내용은 생략하였지만 상속으로 인해 조합이 추가 된다면 엄청난 고통이 수반될 것은 뻔한 그림이다.


03. 합성 관계로 변경하기

상속 관계는 컴파일타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다. 따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하면 모든 조합별로 클래스를 추가해주어야 한다. 이것을 클래스 폭발 문제라 한다. 합성을 이용하면 런타임에 객체의 관계를 변경할 수 있기 때문에 유연한 설계가 가능해진다.

기본 정책 합성하기

실행 시점에 합성 관계를 이용해 정책들을 조합할 수 있게 인터페이스를 만들어 줍니다
정책, RatePolicy.java

public interface RatePolicy {
    Money calculateFee(Phone phone);
}

기본 정책, BasicRatePolicy.java

public abstract class BasicRatePolicy implements RatePolicy {
    @Override
    public Money calculateFee(Phone phone) {
        Money result = Money.ZERO;

        for (Call call : phone.getCalls()) {
            result.plus(calculateCallFee(call));
        }
        return result;
    }

    protected abstract Money calculateCallFee(Call call);
}

일반요금제, RegularPolicy.java

public class RegularPolicy extends BasicRatePolicy {
    private Money amount;
    private Duration seconds;

    public RegularPolicy(Money amount, Duration seconds) {
        this.amount = amount;
        thiis.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

심야요금제, NightlyDiscountPolicy.java

public class NightlyDiscountPolicy extends BasicRatePolicy {
    private static final int LAST_NIGHT_HOUR = 22;
    private Money nightlyAmount
    private Money regularAmount;
    private Duration seconds;

    public NightlyDiscountPolicy(Money nightlyAmount, Money regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;    
        this.regularAmount = regularAmount;
        thiis.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        if (call.getFrom().getHour() >= LAST_NIGHT_HOUR) {
            return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }

        return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

기본 정책을 이용해 요금을 계산할 수 있도록 Phone을 수정하자

public class Phone {
    private RatePolicy ratePolicy;
    private List<Call> calls = new ArrayList<>();

    public Phone(RatePolicy ratePolicy) {
        this.ratePolicy = ratePolicy;
    }

    public List<Call> getCalls() {
        return Collections.unmodifiableList(calls);
    }

    public Money calculateFee() {
        return ratePolicy.calculateeFee(this);
    }
}

Phone 내부에 RatePolicy에 대한 참조가 포함돼어 있다는 것에 주목하자. 이것이 바로 합성이다. Phone이 다양한 요금 정책과 협력할 수 있어야 하므로 요금 정책의 타입이 RatePolicy라는 인터페이스로 정의되어 있다는 것에도 주목하자.

Phone regularPhone = new Phone(new RegularPolicy(Money.wons(10), Duration.ofSecons(10)));
Phone nightlyPhone = new Phone(new NightlyDiscountPolicy(Money.wons(5), Money.wons(10), Duration.ofSecons(10)));

부가 정책 적용하기

위에서 만들었던 RatePolicy 인터페이스를 이용하여 정책을 추가해보자
부가정책, AddiionalRatePolicy.java

public abstract class AddiionalRatePolicy implements RatePolicy {
    private RatePolicy next;
    public AddiionalRatePolicy(RatePolicy next) {
        this.next = next;
    }

    @Override
    public Money calculateFee(Phone phone) {
        Money fee = next.calucateFee(phone);
        return afterCalcurated(fee);
    }

    abstract protected Moeny afterCalcurated(Money fee);
}

세금정책, TaxablePolicy.java

public class TaxablePolicy extends AdditionalRatePolicy {
    private double taxRatio;
    public TaxablePolicy(double taxRatio, RatePolicy next) {
        super(next);
        this.taxRatio = taxRatio;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.plus(fee.times(taxRatio));
    }
}

기본요금 할인 정책, RateDiscountablePolicy.java

public class RateDiscountablePolicy extends AdditionalRatePolicy {

    private Money discountAmount;
    public RateDiscountablePolicy(Money discountAmount, RatePolicy next) {
        super(next);
        this.discountAmount = discountAmount;
    }

    @Override
    protected Money afterCalculated(Money fee) {
        return fee.minus(discountAmount);
    }
}


완성된 다이어그램을 살펴보자. 아까보다 좀더 명확하게 정책 구분이 되고, 추가에 대한 클래스 폭발 문제도 개선되었다.

새로운 정책 추가하기

합성의 진가는 정책을 추가하거나 수정할 때 발휘 된다. 아래의 다이어그램처럼 간단히 클래스만 추가해주면 된다

객체 합성이 클래스 상속보다 더 좋은 방법이다

객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법은 상속이다. 하지만 상속은 코드 재사용을 위한 우아한 해결책은 아니다. 부모 클래스의 세부적인 구현에 자식 클래스가 강하게 결합하기 때문에 수정/추가에 대해 번거로움이 발생한다.

코드를 재사용하면서 건던한 결합도를 유지할 수 있는 더 좋은 방법은 합성이다. 상속이 구현을 재사용하는데 비해 합성은 객체의 퍼블릭 인터페이스를 재사용하기 때문이다.


참조

  • 조영호님의 오브젝트 “11장 합성과 유연한 설계”

'Books > Object' 카테고리의 다른 글

[Object] 일관성 있는 협력  (0) 2021.08.16
[Object] 다형성  (0) 2021.08.12
[OBJECT] 합성과 유연한 설계  (2) 2021.08.08
[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
블로그 이미지

사용자 yhmane

댓글을 달아 주세요

들어가며

이 글을 읽고 있다면 아마 높은 확률로 ES 클러스터의 디스크 문제로 발생하지 않았을까 합니다 ^^
색인 배치를 만들게 되면 디스크 full 문제가 발생할 수 있는데요, ES는 이러한 경우 읽기 모드로 전환되게 합니다.
읽기 모드에 들어가게 되면 당연히 인덱스의 변경이나 삭제가 불가능한 문제가 발생합니다

해결 방법

  1. 우선적으로 사용하지 않는 인덱스를 삭제하여 줍니다
  2. 인덱스의 읽기 모드를 false로 변경하여 줍니다 (kibana 쿼리)
    PUT _all/_settings 
    {
     "index": { 
         "blocks": { 
             "read_only_allow_delete": "false" 
         } 
     } 
    }

추가적으로 disk 문제의 경우 주기적으로 발생할 수 있는 문제라 생각되면 디스크 증설도 고려해보는게 좋다고 생각합니다.
그럴 경우 클러스터의 데이터 노드를 추가할 것을 권합니다

블로그 이미지

사용자 yhmane

댓글을 달아 주세요

10장 상속과 코드 재사용


전통적인 상속이란,
클래스안에 정의된 인스턴스 변수와 메서드를
새로운 클래스에 자동으로 추가하는 것이다


01. 상속과 중복 코드

중복 코드의 경우 많은 생각을 하게 만든다.
이미 존재하는데도 새로운 코드를 만든 이유는 무엇일까? 아니면 단순한 실수 일까?

DRY 원칙

앤드류 헌트 & 데이비드 토마스
프로그래머들은 DRY 원칙을 따라야 한다

  • DRY는 ‘반복하지 마라’ Don’t Repeat Yourself의 첫글자들이다
    중복된 코드는 변경을 방해한다. 이것이 중복 코드를 제거해야 하는 가장 큰 이유다

중복과 변경

중복 코드 살펴보기

Call.java (개별 통화기간)

public class Call {
    private LocalDateTime from;
    private LocalDateTime to;

    public Call(LocalDateTime from, LocalDateTime to) {
        this.from = from;
        this.to = to;
    }

    public Duration getDuration() {
        return Duration.between(from, to);
    }

    public LocalDateTime getFrom() {
        return from;
    }
}

Phone.java (일반 요금제 전화기)

public class Phone {
    private Money amount; // 단위요금
    private Duration seconds; // 단위시간
    private List<Call> calls = new ArrayList<>();

    public Phone(Money amount, Duration duration) {
        this.amount = amount;
        this.duration = duration;
    }

    public void call(Call call) {
        calls.add(call);
    }

    public Money calculateFee() {
        Money result = Money.ZERO;

        for (Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }

        return result
    }
}

기존 요금제 외에 심야요금제 요구사항이 추가 되었다고 가정해보자

NightlyDiscountPhone.java(심야 요금제 전화기)

public class NightlyDiscountPhone {
    private static final int LATE_NIGHT_HOUR = 22;

    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds; // 단위시간
    private List<Call> calls = new ArrayList<>();

    public Phone(Money amount, Duration duration) {
        this.amount = amount;
        this.duration = duration;
    }

    public Money calculateFee() {
        Money result = Money.ZERO;

        for (Call call : calls) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                result = result.plus(nightlyAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
            result = result.plus(regularAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
        }

        return result
    }
}

쉽게 요구사항을 반영했지만, 중복이 많이 발생하였다. 당장은 모르겠지만, 많은 중복은 엄청난 비용을 요구하게 된다

중복 코드 수정하기

통화 요금에 세금을 계산하는 로직을 추가해보자, 그러나 요금을 계산하는 로직은 Phone과 NightlyDiscountPhone 양쪽 모두에 구현돼 있기 때문에 두 클래스를 모두 수정해야 한다
Phone.java

public class Phone {
    ...
    private double taxRate;

    public Money calculateFee() {
        Money result = Money.ZERO;

        for (Call call : calls) {
            result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
        }

        return result.plus(result.times(taxRate));
    }
}

NightlyDiscountPhone.java

public class NightlyDiscountPhone {
    ...
    private double taxRate;

    public Money calculateFee() {
        Money result = Money.ZERO;

        for (Call call : calls) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                result = result.plus(nightlyAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
            result = result.plus(regularAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
        }

        return result.minus(result.times(taxRate));
    }
}

문제는, 중복 코드 수정시 모든 중복 코드를 같이 바꿔줘야 한다는 것이다. 만약, Phone만 수정한채 배포가 된다면 심야요금제의 세금이 포함되지 않고 계산될 것이다.

타입 코드 사용하기

두 클래스 사이의 중복코드를 제거하는 한 가지 방법은 클래스를 하나로 합치는 것이다.
다만, 타입 코드를 사용하게 되면 높은 결합도 문제에 직면하게 된다

Phone.java

public class Phone {
    private static final int LATE_NIGHT_HOUR = 22;
    enum PhoneType { REGULAR, NIGHTLY }

    private PhoneType phoneType;
    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds; 
    private List<Call> calls = new ArrayList<>();

    public Phone(Money amount, Duration duration) {
        this(PhoneType.REGULAR, amount, Money.ZERO, Money.ZERO, seconds);
    }

    public Phone(Money nightlyAmount, Money regularAmount, Duration seconds) {
        this(PhoneType.NIGHTLY, Money.ZERO, nightlyAmount , regularAmount, seconds);
    }

    public Phone(PhoneType type, Money amount, nightlyAmount, Money regularAmount, Duration seconds) {
        this.type = type;
        this.amount = amount;
        this.regularAmount = regularAmount;
        this.nightlyAmount = nightlyAmount;
        this.seconds = seconds;
    }

    public Money calculateFee() {
        Money result = Money.ZERO;

        for (Call call : calls) {
            if (type == PhoneType.REGULAR) {
                result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
            } else {
                if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                    result = result.plus(nightlyAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
                    } else {
                        result = result.plus(regularAmount(call.getDuration().getSeconds() / seconds.getSeconds()));
                    }
                }
            }
        }

        return result
    }
}

상속을 이용해서 중복 코드 제거하기

객체지향은 타입 코드를 사용하지 않고도 중복 코드를 관리할 수 있는 효과적인 방법을 제공한다. 그것은 바로 상속이다.

public class NightlyDiscountPhone extends Phone {
    private static final int LATE_NIGHT_HOUR = 22;

    private Money nightlyAmount;

    public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration duration) {
        super(reguarAmount, seconds);
        this.nightlyAmount = nightlyAmount;
    }

    public Money calculateFee() {
        Money result = super.calculateFee();

        Money nightlyFee = Money.ZERO;
        for (Call call : calls) {
            if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
                nightlyFee = nightlyFee.plus(getAmount().minus(nightlyAmount).times(call.getDuration().getSeconds() / seconds.getSeconds()));
            }
        }

        return result.minus(nightlyFee);
    }
}

super 참조를 통해 부모 클래스의 메서드를 호출한다. 10시 이전엔 Phone 요금제를 통해서 계산하고, 10시 이후 심야타임은 NightlyDiscountPhone을 통해 요금을 계산한다

강하게 결합된 Phone과 NightlyDiscountPhone

다만, 상속은 부모 클래스와 자식 클래스 사이의 강결합을 발생시킨다. calcaluateFee 메서드를 확인해보면 super를 참조해 가격을 계산하고 차감하는 방식이다. 여기에서 세금을 부과하는 요구사항이 추가 된다면 어떻게 될까?

자식 클래스의 메서드 안에서 super 참조를 이용해 부모 클래스의 메서드를 직접 호출할 경우 두 클래스는 강하게 결합된다. Super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거해야 한다.


02. 취약한 기반 클래스 문제

부모 클래스의 변경에 의해 자식 클래스가 영향을 받는 현상을 취약한 기반 클래스 문제라고 부른다

불필요한 인터페이스 상속 문제

자바의 초기 버전에서 상속을 잘못 사용한 대표적인 사례는 java.util.Properties와 java.util.Stack이다. 두 클래스의 공통점은 부모 클래스에서 상속받은 메서드를 사용할 경우 자식 클래스의 규칙이 위반 될 수 있다는 것이다.

Stack<String> stack = new Stack<>();
stack.push("1st");
stack.push("2nd");
stack.push("3rd");

stack.add(0, "4th");
assertEquals("4th", stack.pop()); // 에러!

Pop 메서드의 반환값은 “3rd”이기 때문에 에러가 난다. 이유는 vector의 add 메서드를 이용해 스택 맨앞에 “4th”를 추가했고, Stack의 pop은 가장 위에 있는 “3rd”를 꺼냈기 때문이다.
Stack이 규칙을 무너뜨릴 수 있는 여지가 있는 Vector의 퍼블릭 인터페이스까지 함께 상속을 받았다.
상속을 사용할 경우 부모 클래스의 메서드가 자식 클래스의 내부 구조에 대한 규칙을 깨드릴 수 있는 것도 숙지하고 있어야 한다

메서드 오버라이딩의 오작용 문제

이펙티브 자바의 나오는 유명한 예이다.

public class InstrumentedHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override
    public boolen add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }
}

InstrumentedHashSet<String> languages = new InstrumentedHashSet<>();
languages.addAll(Arrays.asList("java", "Ruby", "Scala"));

대부분의 사람들은 addCount의 값이 3이 될 것이라 생각하지만, 실제 값은 6이다. 그 이유는 부모 클래스인 HashSet의 addAll 메서드 안에서 add 메서드를 호출하기 때문이다.

자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.


03. Phone 다시 살펴보기

추상화에 의존하자

NightlyDiscountPhone의 가장 큰 문제점은 Phone에 강하게 결합돼 있기 때문에 Phone이 변경될 경우 함께 변경될 가능성이 높다는 것이다. 이 문제를 해결하는 가장 일반적인 방법은 자식 클래스가 부모 클래스의 구현이 아닌 추상화에 의존하도록 만드는 것이다

차이를 메서드로 추출하라

중복 코드 안에서 차이점을 별도의 메서드로 추출해라

먼저, Phone과 NightlyDiscountPhone의 중복과 차이점을 분리한다

중복 코드를 부모 클래스로 올려라

위의 코드에서 중복된 메서드는 공통화로 빼고, 다른 부분은 추상메서드로 구현해보자

public abstract class Phone {
    private List<Call> calls = new ArrayList<>();
    public Money calculateFee() {
        Money result = Money.ZERO;
        for(Call call : calls) {
            result = result.plus(calculateCallFee(call));
        }
        return result;
    }
    abstract protected Money calculateCallFee(Call call);
}

Phone.java

public class Phone extends RegularPhone {
    private Money amount;
    private Duration seconds;

    public RegularPhone(Money amount, Duration seconds) {
        this.amount = amount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
    }
}

NightlyDiscountPhone.java

public class NightlyDiscountPhone extends AbstractPhone {
    private static final int LATE_NIGHT_HOUR = 22;
    private Money nightlyAmount;
    private Money regularAmount;
    private Duration seconds;

    public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
        this.nightlyAmount = nightlyAmount;
        this.regularAmount = regularAmount;
        this.seconds = seconds;
    }

    @Override
    protected Money calculateCallFee(Call call) {
        if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
            return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        } else {
            return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
        }
    }
}

자식 클래스들 사이의 공통점을 부모 클래스로 옮김으로써 실제 코드를 기반으로 상속 계층을 구성할 수 있었다. 여기서 우리는 추상화가 핵심이라는 것을 알 수 있다

여기서 세금을 추가하게 되면 자식 변수를 추가해야 하는데 이에 따라 자식 클래스의 생성자 로직도 변경이 불가피하다. 다만, 중요한 것은 인스턴스 변수의 추가에 따른 생성자의 수정보다 로직에 대한 변경으로 수정을 최소화 하는 것이 중요하다.

참조

  • 조영호님의 오브젝트 10장 "상속과 코드 재사용"

'Books > Object' 카테고리의 다른 글

[Object] 다형성  (0) 2021.08.12
[OBJECT] 합성과 유연한 설계  (2) 2021.08.08
[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
[Object] 객체 분해  (0) 2021.05.29
블로그 이미지

사용자 yhmane

댓글을 달아 주세요