유연한 설계 (chapter9)

유연한 설계를 위한 다양한 방법을 알아보자. 여기서는 다음과 같이 알아볼 예정이다.

  • 개방-폐쇄 원칙
  • 생성 사용 분리
  • 의존성 주입
  • 의존성 역전 원칙

개방-폐쇄 원칙

로버트 마틴
“소프트웨어 개채(클래스, 모듈, 함수 등등)는 확장에 열려 있어야 하고,
수정에 대해서는 닫혀 있어야 한다”

키워드는 확장수정이다. 이 키워드를 애플리케이션 관점에서 바라보자

  • 확장에 대해 열려 있다
    • 요구사항이 변경될 때, 변경에 맞게 새로운 동작을 추가해 기능을 확장 할 수 있다
  • 수정에 닫혀 있다
    • 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가/변경 할 수 있다

이렇듯, 개방-폐쇄 원칙은 기존의 코드를 수정하지 않고 애플리케이션의 기능을 확장할 수 있는 설계라 할 수 있다.
개방-폐쇄에 대해 좀더 알고 싶다면 uncle bob의 블로그를 참조해보자
Clean Coder Blog

컴파일 의존성을 고정 시키고 런타임 의존성을 변경하라

개방-폐쇄 원칙은 사실 런타임,컴파일타임 의존성에 관한 이야기이다

  • 런타임 의존성 : 실행시 협락에 참여하는 객체들 사이의 관계이다
  • 컴파일타임 의존성 : 코드에서 드러나는 클래스들 사이의 관계이다

이제 의존성을 위에서 언급한 확장과 수정에 대해서 빗대어 알아보자

  • 기능 추가 : Amount, Percent 이외에 Overlapped, None 등의 정책이 추가 가능하다
  • 수정 : 해당 정책 클래스 내에서만 이루어진다

컴파일타임 의존성을 유지지하며 런타임 의존성을 변경할 경우, 코드의 유연한 설계가 가능해진다

추상화가 핵심이다

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다

추상화란 핵심적인 부분만 남기고 불필요한 부분을 생략하는 기법이다.
추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고,
문맥에 따라 변하는 부분은 생략된다.

아래 코드를 살펴보자

public abstract class DiscountPolicy {
    public Money calculateDiscountAmount(Screening screening) {
        for (DiscountCondition each: conditions) {
            if (each.isSatisfiedBy(screening) {
                return getDiscountAmount(screening);
            }
        }
    }
    abstract protected Money getDiscountAmount(Screening screening);
}

문맥이 바뀌더라도 할인여부를 판단하는 로직은 바뀌지 않는다. 변하지 않는 부분을 고정하고 변하는 부분을 생략하는 추상화 매커니즘이 개방-폐쇄 원칙의 기반이 된다


생성 사용 분리

Movie가 오직 DiscountPolicy라는 추상화에만 의존하기 위해서는 Movie 내부에서 구체 클래스에 의존해서는 안된다. 다음 코드를 살펴보자

public class Movie {
    ...
    private DiscountPolicy discountPolicy;
    public Movie(...) {
        ...
        this.discountPolicy = new AmountDiscountPolicy(...);
    }
}

Movie 내부 생성자에서 AmountDiscountPolicy를 직접 생성하기 때문에 다른 할인 정책을 적용하는 것이 어렵다. 이러한 코드는 확장이나 수정에 유연하게 대처하는 것이 어렵기 때문에 위에서 알아본 개방-폐쇄 원칙을 위반한다

유연하고 재사용 가능한 코드를 사용하려면 생성과 사용을 분리 해야 한다.

public class Client {
    public Money getAvatarFree() {
        Movie avatar = new Movie(..., new AmountDiscountPolicy(...));
        return avatar.getFee();
    }
}

위와 같이 컨텍스트에 대한 결정권을 Client(Movie외부)로 옮김으로써 유연한 설계가 가능해졌다

Factory 추가하기

생성 책임을 Movie -> Client로 옮김으로써 Movie의 특정 컨텍스트에 묶이는 것을 해결하였다. 하지만, Movie를 사용하는 Client에서도 특정 컨텍스트에 묶이질 않길 바란다면 어떻게 해결해야 할까?

아래의 코드를 살펴보자

public class Factory {
    public Movie createAvatarMovie() {
        return new Movie(..., new AmountDiscountPolicy(...));
    }
}

public class Client {
    private Factory factory;
    public Client(Factory factory) {
        this.factory = factory;
    }
    public Money getAvatarFree() {
        Movie avatar =     factory.createAvatarMovie();
        return avatar.getFee();
    }
}

이 문제는 Movie에서 해결한 방법과 마찬가지로 결정권자를 옮김으로 해결할 수 있다
이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 Factory라고 한다

순수한 가공물에게 책임 할당하기

앞서 책임을 할당할 때, 정보 전문가에게 할당하라는 원칙을 배웠습니다. 하지만, 방금 언급한 Factory는 Information Expert와는 거리가 먼 모델이라는 것을 알수 있습니다.

크레이그 라만
“시스템을 객체로 분해하는 데는 크게 두가지 방식이 존재한다.
첫번째는 표현적 분해이고, 다른 하나는 행위적 분해이다”

이처럼 표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것을 말합니다. 하지만, 모든 책임을 도메인 객체에게 할당할 수 없는 경우가 생깁니다. 이럴 경우에는 Factory 처럼 가상의 기계적 개념들이 필요한데, 이처럼 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물) 이라고 부릅니다. 이것을 행위적 분해라고 가르킵니다.

정리하자면, 일차적으로 도메인 모델에 책임을 할당하는 것을 고려하고, 그것이 여의치 않다면 대안으로 삼을 수 있는 것이 FACTORY와 같은 PURE FABRICATION 입니다.


의존성 주입

public class Movie {
    ...
    private DiscountPolicy discountPolicy;
    public Movie(...) {
    }
}

위의 코드를 살펴보자. 생성과 사용을 분리하며 Movie에는 오로지 인스턴스를 사용하는 책임만 남게 되었다. 이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입 이라고 부른다.

의존성을 해결하기 위해 다음과 같은 3가지 메카니즘을 이용한다. 8장에서도 나온 내용이기에 간략히 정리한다.

  • 생성자 주입
    Movie avatar = new Movie(..., new AmountDiscountPolicy(...));
  • setter 주입
    avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
  • 메서드 주입
    avatar.calcalateDiscountAmount(screening, new AmountDiscountPolicy(...));

가장 좋은 방법은 생성자와 수정자를 같이 이용하는 것이다. 시스템의 안전성을 보장해주고 필요에 따라 의존성을 변경할 수 있기 때문에 유연성을 향상시킬 수 있다

숨겨진 의존성은 나쁘다

의존성 주입 이외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그 중에서 가장 널리 사용되는 대표적인 방법은 SERVICE LOCATOR 패턴이다

외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리 SERVICE LOCATOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다

public class Movie {
    private DiscountPolicy discountPolicy;
    public Movie(...) {
        this.discountPolicy = ServiceLocator.discountPolicy();
    }
}

public class ServiceLocator {
    private DiscountPolicy discountPolicyl
    private static ServiceLocator soleInstance = new ServiceLocator();
    public static void provide(DiscountPolicy discountPolicy) {
        soleInstance.discountPolicy = discountPolicy;
    }
}

이제 SERVICE LOCATOR 패턴을 이용해 인스턴스를 생성해보자

ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie(...);
ServiceLocator.provide(new PercentDiscountPolicy(...));
Movie avatar1 = new Movie(...);

위와 같이 쉽게 의존성 문제를 해결할 수 있다. 다만 의존성이 숨겨져 있다는 치명적인 단점이 있다. 아래의 코드를 살펴보자

Movie avatar = new Movie(...);

위의 코드는 NPE 문제가 발생할 것이다. 그 이유는 ServiceLocator로 DiscountPolicy를 주입시켜주지 않았기 때문이다.

결론적으로, SERVICE LOCATOR 패턴을 사용하지 말라는 것은 아니다. 다만, 숨겨진 의존성보다 명시적인 의존성을 먼저 고려하는 것이 시스템의 안정을 위해 좋다는 것이다.


의존성 역전 원칙

추상화와 의존성 역전

구체클래스에 의존하는 Movie 클래스를 살펴보자. 결합도가 높아지고 재사용성이 낮아진 것을 확인할 수 있다.

public class Movie {
    private AmountDiscountPolicy discountPolicy;
}

객체 사이의 협력이 존재할 때, 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다.
여기서의 본질은 영화의 가격을 계산하는 것이다. 할인 금액을 계산하는 것은 협력의 본질이 아니다.

그러나 상위 수준의 클래스가 하위 수준의 클래스에 의존하면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 된다. 즉, 하위 수준의 AmountDiscountPolicy를 PercentCountPolicy로 변경해서 상위 수준의 Movie가 영향을 받아서는 안된다.

이 경우 해결할 수 있는 방법이 위에서 알아본 추상화 기법이다.
위의 언급한 내용들을 정리를 해보면

  1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 둘다 모두 추상화에 의존해야 한다
  2. 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.
    이를 의존성 역전 원칙 이라 한다

결론

우리는 런타임의존성, 캄파일타임 의존성을 배우며 유연하고 재사용 가능한 설계에 대해 알아보았다. 하지만 이러한 설계가 항상 옳고 좋은 것은 아니다.
설계의 미덕은 단순함과 명확함이지만 이러한 설계는 복잡성을 높이기 때문이다.

따러서, 필자(조영호님)는 아래와 같은 융통성을 제시한다

  • 미래에 일어나지 않을 변경에 대비하여 꼭 복잡한 설계를 할 필요는 없다
  • 단순하고 명확한 해법이 만족스럽다면 유연성을 제거해도 된다
  • 의존성이 생기는 이유는 객체의 협력과 책임이 있기 때문이다
  • 객체 생성에 초점을 맞추기 보다 객체의 역할과 책임의 균형을 맞추는 것에 초점을 맞추자

참조

  • object 코드로 이해하는 객체지향 설계 (chapter9 유연한 설계) - 조영호님

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

[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
[Object] 메시지와 인터페이스  (0) 2021.05.25
블로그 이미지

yhmane

댓글을 달아 주세요

의존성 관리하기 (chapter8)

잘 설계된 객체지향 애플리케이션 이란?

  • 작고 응집도 높은 객체들로 구성
  • 책임의 초점이 명확하고 한가지 일을 잘 수행
  • 이러한 객체들이 모여 협력을 함

협력을 위해서 의존성이 필요.
하지만 과도한 협력은 의존성 문제로 애플리케이션의 변경을 힘들게 함
이러한 관점에서 객체지향 설계란 의존성을 관리하고 의존성을 정리하는 기술이라고 할 수 있다


의존성 이해하기

변경과 의존성

객체는 협력을 위해 다른 객체를 필요로 하고 두 객체 사이에 의존성이 생기게 된다
의존성은 실행시점과 구현시점에 따라 서로 다른 의미를 갖는다

  • 실행시점
    • 의존하는 객체가 정상적으로 동작하기 위해서는 실행시 의존 객체가 존재해야함
  • 구현 시점
    • 의존 객체가 변경될 경우 의존하는 객체도 함께 변경
public class PeriodCondition implements DiscountCondition {
    private DayOfWeek dayOfWeek;
    private LocalTime startTime;
    private LocalTime endTime;

    ...
    public boolean isSatisfiedBy(Screening screening) {
        return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
        startTime.compareTo(screening,getStartTime().toLocalTime()) <= 0 &&
        endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

위의 코드를 해당 시점에 경우로 나누어 살펴보자

  • 실행시점
    • isSatisfiedBy() 실행시 Screening 인스턴스가 존재해야 한다. 그렇지 않다면 해당 내부의 로직 수행중에 에러가 날 것이다
  • 구현시점
    • isSatisfiedBy() 내부는 DayOfWeek, Screening, LocalTime, DiscountCondition에 영향을 받는다.
    • 따라서 해당 클래스나 변수가 변경이 생기게 된다면 PeriodCondition에게 당연히 영향을 끼친다

따라서, 두 요소 사이의 의존성은 의존되는 요소가 변경 될때 의존하는 요소도 함께 변경될 수 있음을 의미한다

의존성 전이

PeriodCondition -> Screening (직접 의존)
Screening -> Movie (직접 의존)

PeriodCondition ---> Movie (간접 의존)

Screeing의 내부 코드는 Movie, LocalDateTime, Customer에 의존하고 있다.
위의 그림을 잠깐 살펴보자.

[직접의존] - PeriodCondition은 Screening에, Screeing은 Movie에 의존하고 있다.
이렇듯 객체가 다른 객체에 의존하고 있을 경우 ‘직접 의존한다’라고 한다

[간접의존] - PeriodCondition은 Movie에 직접 의존하지 않고 있다.
코드상에 드러나지는 않지만 PeriodCondition->Screening->Moive로 이어지는 연결고리가 잠재적으로 영향을 미칠 수 있다.
이렇듯, 직접 연관되지 않지만 연관된 객체에 의존된 관계가 맺어질 경우 ‘간접 의존한다’ 라고 한다

런타임 의존성과 컴파일타임 의존성

의존성과 관련해서 또 다뤄야 할 부분은 런타임 의존성과 컴파일 의존성이다

  • 런타임 의존성
    • 애플리케이션이 실행되는 시점에 의존성
  • 컴파일 의존성
    • 컴파일 시점에 작성된 코드에 따라 문맥에 의존

아래의 예제를 살펴보자

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

코드상에서 Movie는 DiscountPolicy에 의존하고 있다.
어느 부분에도 AmountDiscountPolicy, PercentDiscountPolicy이 명시되어 있지 않다.
이렇듯, 코드상에는 구체화된 할인정책이 명시되어 있지 않지만 런타임 의존성을 이용해서 의존 관계를 가질 수 있다.

클래스가 어떠한 인스턴스 타입과 협력하는지 코드상에서 알 수 없게 되면
코드의 재사용이 가능해지고 다양한 협력관계를 가질 수 있는 장점이 있다.

이렇듯 런타임 의존성과 컴파일 의존성은 서로 다를 수 있다. 이러한 다름 때문에 프로그래머는 유연하고 재사용 가능한 코드를 설계할 수 가 있다

컨텍스트 독립성

클래스가 구체적인 클래스를 알게 될수록, 그 클래스가 사용하는 문맥에 강하게 결합된다
따라서, 클래스가 사용될 특정한 문맥에 대해 최소한의 가정만으로 이뤄진다면
다른 문맥에서 재사용하기가 더 수월해진다. 이를 컨텍스트 독립성이라고 부른다

결국 의존하는 관계에서는 실행될 컨텍스트에 대한 구체적 정보를 최대한 적게 알도록 설계해야 한다

의존성 해결하기

결국 컴파일 타임 의존성이 구체적인 런타임 의존성으로 대체되어야 재사용 가능한 코드가 작성될 수 있다.
이처럼 컴파일타임 의존성을 실행 컨텍스에 맞는 런타임 의존성으로 교체하는 것을 의존성 해결이라고 부른다

의존성을 해결하기 위해서는 다음과 같은 세 가지 방법을 사용한다

  1. 객체를 생성하는 시점에 생성자를 통해 의존성 해결
    Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(...));
    생성자를 통해 주입하여 준다
  1. 객체를 생성후 setter 메서드를 통해 의존성 해결
    Movie avatar = new Movie(...);
    avatar.calculateFee(...); // 예외발생
    avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
  • set method를 구현해주어야 한다
  • set을 하기 전까지 객체가 불안정하여 NullPointerException이 발생할 수 있다
  1. 메서드 실행 시 인자를 이용해 의존성 해결
    public class Movie {
     public Money calculateMovieFee(Screening screening, DiscountPolicy discountPolicy) {
         return fee.minus(discountPolicy.calculateDiscountAmount(screening));
     }
    }

가장 좋은 방법은 생성자와 수정자를 같이 이용하는 것이다. 시스템의 안전성을 보장해주고 필요에 따라 의존성을 변경할 수 있기 때문에 유연성을 향상시킬 수 있다


유연한 설계

설계를 유연하고 재사용 가능하게 만들기로 결정했다면 의존성을 관리하는데 유용한 몇가지 원칙을 익힐 필요가 있다. 먼저 의존성과 결합도의 관계를 살표보자

의존성과 결합도

객체지향 패러다임의 근간은 협력이다.

객체들은 협력을 위해 서로의 존재와 책임을 알아야 한다. 이러한 지식들이 객체 사이의 의존성을 낳는다.
의존성은 객체들의 협력을 가능케 하는 매개체이고 바람직한 것이다. 다만, 과하면 문제가 될 수 있다.

다음 코드를 살펴보자

Movie는 PercentDiscountPolicy에 의존한다. 의존한다는 사실 자체는 바람직한 관계이다.
다만, Movie가 PercentDiscountPolicy라는 구체적 클래스에 의존하기 때문에 다른 할인 정책을 문맥에서 사용하기 힘들어졌다.

그렇다면 바람직한 의존성이란 무엇인가? 바람직한 의존성은 재사용과 관련이 있다.
의존성이 다양한 환경에서 클래스를 재사용할 수 없도록 제한한다면 그 의존성은 바람직하지 못한 것이다.

이러한 의존성을 세련된 단어로 표현한 것이 결합도이다.
두 요소가 의존성이 바람직할때를 느스한 결합도, 약한 결합도라 한다.
반대로, 두 요소 사이의 의존성이 바람직하지 못할때 단단한 결합도 또는 강한 결합도라고 표현한다

지식이 결합을 낳는다

더 구체화된 지식일 수록 강한 결합으로 연결된다.
Movie -> PercentDiscountPolicy (강결합)
Movie -> DiscountPolicy (느슨한 결합)

더 많이 알수록 더 많이 결합되는 구조이다. 더 많이 알고 있다는 것은 더 적은 컨텍스트에서 재사용 가능하다는 것을 의미하기 때문이다.
결합도를 느슨하기 만들기 위해서는 협력하는 대상에 대해 필요한 정보 외에 최대한 감추는 것이 중요하다.

이러한 목적을 달성하기 위해 가장 효과적인 방법이 추상화이다

추상화에 의존하라

추상화란 어특정 절차나 물체를 의도적으로 생략하거나 감춤으로써 복잡도를 극복하는 방법이다.
추상화를 이용하면 불필요한 정보를 감추고 지식의 양을 줄일 수 있다

PercentDiscountPolicy보다 DiscountPolicy가 지식의 양이 더 적다.
Movie와 DiscountPolicy 사이의 결합도가 더 느스한 이유는 Movie가 구체적인 대상이 아닌 추상화에 의존하기 때문이다

  • 구체 클래스
  • 추상 클래스
  • 인터페이스

클래스는 3개의 타입에 대하여 의존할 수 있다. 당연히 인터페이스와 의존 관계를 맺는 것이 느스한 결합을 만들어 준다

명시적인 의존성

아래의 코드를 살펴보며 의존성에 대한 문제를 찾아보자

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = new AmountDiscountPolicy(...);
    }
}

New AmountDiscountPolicy가 결합도를 높이고 있다.

public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

명시적인 부분을 제거하여 결합도를 낮추어 주자. 이제 객체를 생성할 때 생성자의 인자로 DiscountPolicy의 어떠한 자식 클래스도 전달할 수 있다.
따라서, 런타임에 해당 인스턴스를 선택적으로 전달할 수 있게 되었다.

이처럼 의존하는 사실을 Public 인터페이스에 드러내는 것을 명시적인 의존성이라 한다.
반면, 내부에 AmountDiscountPolicy를 직접 생성하는 방식을 의존하는 사실은 감춘다고 하여 숨겨진 의존성이라 한다.

숨겨진 의존성의 경우 직접 코드 내부를 살펴보아야 하고, 인스턴스를 생성하는 코드를 훓어 파악해야 하므로 버그의 가능성도 내포하고 있다.
따라서, 우리는 재사용성을 높이고 안정성을 높이는 명시적인 의존성을 선택해야 한다.

new는 해롭다

클래스는 인스턴스를 생성할 수 있게 new 연산자를 제공한다. 하지만 잘못 사용한다면 두 객체 사이의 결합도가 높아질 수 있다.

  1. New 연산자로 구체 클래스 이름을 직접 기술한다.
  2. 구체 클래스 뿐만 아니라, 어떠한 인자를 이용해 클래스를 생성할지 인자 정보도 알고 있어야 한다
public class Movie {
    ...
    private DiscountPolicy discountPolicy;

    public Movie(String title, Duration runningTime, DiscountPolicy discountPolicy) {
    ...
        this.discountPolicy = discountPolicy;
    }
}

두개를 비교해보면 구체 클래스를 생성하는 것이 많은 고통을 유반한다는 것을 볼 수 있다

가끔은 생성해도 무방하다

new가 꼭 나쁜것만은 아니다. 생성되는 비율이 다를 경우 아래와 같이 여러 생성자를 이용하여 구현할 수 있다.
해당 생성자는 아래의 생성자를 호출하는 방식이다. 이런 방식으로 구현함으로써 유연성도 가져갈 수있따.

### 컨텍스트 확장하기

표준 클래스에 대한 의존은 해롭지 않다

의존성이 문제가 되었던 것은 변경 가능성으로 인해 유발되는 파생효과들이다.
다만, 표준 클래스처럼 변화가 거의 없는 것에 해당한다면 크게 문제될 것이 없다.


참조

  • 오브젝트, 코드로 이해하는 객체지향 설계, chapter8 의존성 관리하기 [조영호]

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

[Object] 상속과 코드 재사용  (1) 2021.08.03
[Object] 유연한 설계  (0) 2021.06.28
[Object] 의존성 관리하기  (0) 2021.06.10
[Object] 객체 분해  (0) 2021.05.29
[Object] 메시지와 인터페이스  (0) 2021.05.25
[Object] 책임 할당하기  (0) 2021.05.16
블로그 이미지

yhmane

댓글을 달아 주세요

책임 할당하기 (chapter5)


책임 주도 설계를 향해

  • 데이터보다 행동을 먼저 결정
  • 협력이라는 문맥 안에서 책임을 결정

데이터보다 행동을 먼저 결정하라
“객체지향에 갓 입문한 사람들이 가장 많이 저지르는 실수는
객체의 행동이 아닌 데이터에 초점을 맞추는 것”

  • 데이터 중심 설계
    • 객체가 포함해야 하는 데이터를 먼저 선정
    • 그 후, 데이터를 처리하는 데 필요한 행동을 구현
  • 책임 중심 설계
    • 객체가 수행할 책임을 먼저 정의
    • 그 후, 책임을 처리하는 데 필요한 데이터를 정의

설계 패러다임의 변경

'객체의 데이터 중심 설계' -> '책임 중심 설계'로 설계 방식을 바꿔야 한다

협력이라는 문맥 안에서 책임을 결정하라
“책임은 객체의 입장이 아니라,
객체가 참여하는 협력에 적합해야 한다”

  • 협력에 적합한 책임을 할당하기
    • 먼저, 수행할 메세지를 정의
    • 메시지를 수행할 객체를 선택
    • 즉, 객체가 메시지를 선택하는 게 아니라, 메시지가 객체를 선택하도록 설계

책임 할당을 위한 GRASP 패턴

크레이그 라만
“GRASP - General Responsibility Assignment Software Pattern”
일반적인 책임 할당을 위한 소프트웨어 패턴의 약자로
책임 할당시 지침으로 삼을 수 있는 원칙들의 집합

도메인 개념에서 출발

  • 설계를 시작하기 전 도메인에 대한 개략적인 모습을 그려보자
    • 도메인 개념들을 책임 할당의 대상으로 사용하면 코드의 투영하기 수월해진다

정보 전문가에게 책임을 할당

  • “정보전문가 패턴”
    • 책임을 수행하는데 필요한 정보를 가지고 있는 객체에게 할당하라
  • 메시지를 먼저 정의하고, 그 메시지를 수행할 객체를 선정하라
    • 그 정보를 가장 잘 아는 객체를 선정하고 책임을 할당하라

높은 응집도와 낮은 결합도 (chapter4)

  • 응집도 (Cohesion)
    • 모듈에 포함된 내부 요소들이 하나의 책임/ 목적을 위해 연결되어있는 연관된 정도
  • 결합도
    • 다른 모듈과의 의존성이 정도

창조자에게 객체 생성 책임을 할당

CREATOR 패턴
“객체 A를 생성해야 할때, 그 객체를 가장 잘 아는
객체 B에게 객체 생성 책임을 할당하라”

  • B가 A 객체를 포함하거나 참조
  • B가 A 객체를 기록
  • B가 A 객체를 긴밀하게 사용
  • B가 A 객체를 초기화하는데 필요한 데이터 정보를 포함 (B가 A의 정보 전문가)

구현을 통한 검증

  • 변경에 취약
public class DiscountCondition {

  public boolean isSatisfiedBy(Screening screening) {
     if (type == DiscountConditionType.PERIOD) {
        return isSatisfiedByPeriod(screening);
     }
     return isSatisfiedBySequence(screening);
  }

  private boolean isSatisfiedByPeriod(Screening screening) {
        ...
  }

  private boolean isSatisfiedBySequence(Screening screening) {
        ...
    }
}
  • 새로운 할인 조건 추가
  • 순번 조건 로직의 변경
  • 기간 조건 로직의 변경

    해당 클래스는 하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다.
    응집도가 낮다는 것은 서로 연관성이 없는 기능이나 데이터가 뭉쳐 있다는 것을 의미한다


책임 주도 설계의 대안

클래스는 오직 하나의 작업만 수행하고,
하나의 변경 이유만 가지는 작고 명확하고
응집도 높은 메서드들로 구성되어야 한다.

몬스터 메서드

  • 어떤 일을 수행하는지 한눈에 파악하기 어렵다
  • 하나의 메서드 안에 너무 많은 작업을 처리한다
  • 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다
  • 로직의 일부만 재사용하는 것이 불가능 하다

변경과 유연성 - 개발자로서 변경에 대비하는 방법

  • 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계
  • 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 유연하게 작성

책임주도 설계 방법이 익숙하지 않다면 일단 데이터 중심으로 구현한 뒤 이를 리펙터링을 통해 유사한 결과를 얻을 수 있다는 점이다. 아직, 책임 주도 설계가 익숙하지 않다면 동작하는 코드를 작성후 리팩토링을 하는 것도 좋은 대안이 될 수 있다


참조

  • 오브젝트 코드로 이해하는 객체지향 설계 [조영호], 5.책임 할당하기

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

[Object] 객체 분해  (0) 2021.05.29
[Object] 메시지와 인터페이스  (0) 2021.05.25
[Object] 책임 할당하기  (0) 2021.05.16
[Object] 설계 품질과 트레이드오프  (0) 2021.05.08
[Object] 역할, 책임, 협력  (0) 2021.05.03
[Object] 객체지향 프로그래밍  (0) 2021.04.25
블로그 이미지

yhmane

댓글을 달아 주세요

설계 품질과 트레이드오프 (chapter4)


객체지향 설계
“올바른 객체에게 올바른 책임을 할당하면서
낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다”

더 좋은 객체설계를 위해

  • 객체가 가지는 데이터보다 책임에 초점을 맞추어 설계를 하자
    • 퍼블릭 인터페이스를 제공하여 객체의 자유도를 높이자
    • 메세지를 통해 상호작용하여 결합도 낮은 설계를 하자

설계 트레이드오프

객체지향 설계에 3가지 품질 척도

  • 캡슐화
  • 응집도
  • 결합도

캡슐화

변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법

  • 객체 내부에 무엇을 캡슐화 해야 하는가?
    • 경될 수 있는 어떤 대상이라도 캡슐화 해야 한다!

응집도

모듈 내부에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다
모듈 내의 요소들이 하나의 목적을 위해 협력한다면 높은 응집도를 가진다.
서로 다른 목적을 추구한다면 그 모듈은 낮은 응집도를 가진다.
객체지향 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 있는 책임을 할당했는지를 나타낸다.

  • 응집도 변경의 관점에서 - 변경이 발생할때 모듈 내부에서 발생하는 변경의 정도
  • 응집도가 높을 경우 변경이 발생하면, 오직 하나의 모듈만 수정하면 된다. 반면, 응집도가 낮을 경우 변경이 발생하면 여로 모듈을 수정하게 된다.

결합도

의존성의 정도를 나타낸다. 다른 모듈에 얼마나 많은 지식을 갖고 있는지를 나타내는 척도이다.
객체지향 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다

  • 결합도 변경의 관점에서 - 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
  • 결합도가 낮을 경우 수정해야 하는 모듈이 적지만, 결합도가 높을 경우 변경이 발생하게 되면 다른 모듈까지 같이 수정하게 된다.

데이터 중심 설계의 문제점

캡슐화 위반

오직 메서드만을 통해 내부 상태에 접근한다. 어떠한 점이 캡슐화를 위반한 것일까?

public class Movie {
    private Money fee;
    public Money getFee() { 
        return fee; 
    } 

    public void setFee(Money fee) {
        this.fee = fee; 
    } 
}
  • 접근자와 수정자를 통해 Money타입의 fee라는 상태가 존재한다는 것을 알 수 있다
    • 캡슐화를 위반하는 이유는 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다

높은 결합도

public class ReservationAgency {
    ... if (discountable) { 
        fee = movie.getFee().minus(discountAmount).times(audienceCount); 
    } else { 
        fee = movie.getFee().times(audienceCount); 
    } 
}
  • Movie의 fee는 private한 상태이지만, 구현 로직을 보면 전혀 private하지 않다.

결국 데이터 중심의 설계로, ReservationAgency는 높은 결합도를 가지게 되었다. 따라서, 이 모듈을 변경하게 되면 다른 모듈까지 많은 변경을 요구하게 된다.

낮은 응집도

ReservationAgency에는 할인 가격을 계산한다는 이유로 변경 이유가 서로 다른 코드를 전부 뭉쳐놓는다. 따라서 하나의 요구사항을 반영하기 위해서는 여러 모듈을 수정해야한다. 응집도가 낮은것이다.


자율적인 객체

  • 접근자, 수정자를 통해 객체의 내부상태에 접근을 허용하였다. get/set 메서드를 제거하고 클래스 내부에서 책임를 지도록 변경하자

데이터 중심 설계의 문제점

  • 데이터 중심 설계는 너무 이른 시기에 데이터에 관해 결정하도록 강요한다
  • 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 operation을 결정한다

정리

  • 객체 설계시 캡슐화를 먼저 고려해보자. 그러면 낮은 결합도를 가지며, 높은 결합도있는 모듈로 설계할 수 있다
  • 우리가 알고 있는 캡슐화와 저자가 말하고 있는 캡슐화는 다른 조금 다른 영역이다
    • private로 클래스 변수를 지정한다고 캡슐화는 아니다
    • 접근자(get), 수정자(set)이 꼭 필요할까 의심해보자
    • 접근자와 수정자를 제공한다는 것은 결국 public interface로 접근 가능하다는 뜻이다
  • 결국 객체의 자율화란, 내부로 감추는 것인데 srp 원칙을 고려하여 책임을 할당해보자. 클래스 변수는 내부구현으로 감추도록!!

참조

Object 코드로 이해하는 객체지향 설계 chapter4

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

[Object] 메시지와 인터페이스  (0) 2021.05.25
[Object] 책임 할당하기  (0) 2021.05.16
[Object] 설계 품질과 트레이드오프  (0) 2021.05.08
[Object] 역할, 책임, 협력  (0) 2021.05.03
[Object] 객체지향 프로그래밍  (0) 2021.04.25
[Object] 객체 설계  (0) 2021.04.22
블로그 이미지

yhmane

댓글을 달아 주세요

객체

'어떠한 이름을 선언하였을 때, 하나의 이름에 여러개의 값을 넣어줄 수 있는것'입니다

  • 객체는 여러개의 속성들로 이루어져 있고, 속성은 key(이름)과 value(값)으로 구성됩니다.
const yunho = {
  name: '황윤호',
  age: '20',
  grade: 'A'
}

console.log(yunho)
console.log(yunho.name)
console.log(yunho.age)

비구조화 할당

'구조분해'라고도 하며 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 자바스크립트 표현식입니다.

  • 사용법
const yunho = {
    name: '황윤호',
    age: '20',
    grade: 'A'
}

function printInfo(student) {  
    const { name, age, grade } = student;  
    const str = `${name}는 ${age}이며 ${grade} 성적을 받았습니다`;  
    console.log(str)  
}

function printInfo2({ name, age, grade }) {  
    const str = `${name}는 ${age}이며 ${grade} 성적을 받았습니다`;  
    console.log(str)  
}

printInfo(yunho)  
printInfo2(yunho)


함수 선언

객체안에 속성값 뿐만 아니라 함수도 선언하여 사용할 수 있습니다.

const yunho = {
  name: '황윤호',
  age: '20',
  grade: 'A',
  hello: function() {
    console.log(this.name + '입니다')
  },
  hi() {
    console.log(this.name + '입니다')
  }
}
yunho.hello()
yunho.hi()

getter, setter

javascript도 java와 마찬가지로 getter, setter를 구현할 수 있습니다

  • getter는 get, setter는 set을 명시하여줍니다.
const yunho = {
  _name: '황윤호',
  age: '20',
  grade: 'A',
  get info() {
    console.log(this._name);
    return this._name
  },
  set name(value) {
    this._name = value;
  }
}
블로그 이미지

yhmane

댓글을 달아 주세요

들어가며


  이번 포스팅에선 AWS 파일서버인 S3에 대하여 알아보고, S3의 생성, 연결 및 관리 방법에 대하여 알아보도록 하겠습니다.




S3란?

 

  S3란 Simple Storage Service의 약자입니다. 아마존에서 사용하는 파일 서버로 용량에 관계없이 파일을 저장할 수 잇고 웹에서 HTTP 프로토콜을 이용하여 파일에 접근할 수 있습니다. S3를 써야 하는 이유는 성능과 비용에 있습니다. 대용량의 파일 저장을 EC2와 EBS를 통해 구축한다면 상당히 많은 비용이 청구됩니다. (EC2는 RDS와 함께 AWS에서 많은 비용을 차지하는 부분중에 하나이기 때문에 EC2서버와 파일서버를 분리하는 것을 권장합니다) S3는 저장 용량이 무한대이고 파일저장에 최적화 되어 있습니다. 


 따라서, 동적 웹페이지를 EC2에 구축하고, 이미지 관련 정적 파일 등은 S3에 업로드 하여 구축합니다. S3는 흔히 웹하드와 비교하곤 하지만 HTTP를 이용한 파일 업로드/다운로드를 처리하기에 사용하기에 쉽습니다.


* S3 기본 개념


- 객체 (Object)

: S3에 데이터가 저장되는 최소 단위입니다. 객체는 파일과 메타데이터로 구성됩니다.

: 기본적으로 키(Key)가 객체의 이름이며 값(Value)이 객체의 데이터입니다.

: 객체 하나의 크기는 1 바이트부터 5TB까지 지원됩니다.


-  버킷(bucket)

: S3에서 생성할 수 있는 최상위 폴더입니다.

: 버킷은 리전(지역) 별로 생성해야 합니다. 버킷의 이름은 모든 S3 리전 중에서 유일해야 합니다

: 폴더 생성이 가능하고 버킷안에 객체가 저장됩니다.

: 저장 가능한 객체의 개수와 용량은 무제한입니다.

: 접속 제어 및 권한 관리가 가능합니다.


S3 버킷생성




먼저, S3 대쉬보드에서 버킷을 누른후 '버킷 만들기'를 클릭합니다.




 다음으로 버킷이름과 리전을 선택하여 줍니다. 리전은 지역 위치에 따라 속도가 차이가 많이 나므로, 운영할 서버 EC2에 위치한 리전으로 선택 해주는게 좋습니다. 버킷이름은 유니크 하기 때문에 이미 리전에 생성된 이름이 있으면 사용할 수 없습니다.



 생성된 버킷에 들어가면 다음과 디렉토리를 만들고 업로드, 다운로드를 대쉬보드내에서 진행 할 수 있습니다. 하지만, 이렇게 사용하려고 S3를 구성한 것은 아니니 코드(Java)로 제어하는 방법을 알아보도록 하겠습니다. Java 이외에 python, node.js, kotlin, .net 등의 언어로도 지원 가능합니다.



코드로 S3 다루기 feat Java

 

 먼저, S3를 다루는 몇가지 방식이 있습니다. 


1) IAM 계정 추가 방식

- 계정에는 access_key와 secret_key가 부여됩니다.

- s3 full access라는 정책을 계정에 부여합니다.

- s3 client에 access_key, secret_key를 부여한 credential 정보를 부여하고 bucket에 접근하여 업로드, 변경, 다운로드를 수행합니다.

AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);

AmazonS3 s3 = AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();


2) EC2에 역할 부여

- EC2에 S3에 대한 full access 정책을 할당합니다.

- s3 client를 할당하여 api를 통해 업로드, 변경, 다운로드를 수행합니다.

AmazonS3 s3 = AmazonS3ClientBuilder.standard().withRegion(Regions.DEFAULT_REGION).build();
엑세스 권한이 부여된 1) key 접근 방식은 보안적인 측면에서 여러 문제가 발생할 소지가 있기 때문에, AWS에서는 2번째 방법으로 S3의 파일 데이터를 다루는 것은 추천하고 있습니다!!

간단히 API를 보겠습니다.

- bucket_name : 버킷 이름 (유니크한 이름)
- key_name : 저장될 파일 이름 (해당 경로에 위치될 이름입니다)
- file : 파일

* upload 

try {
s3.putObject(bucket_name, key_name, new File(file_path));
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}

* download

try {
S3Object o = s3.getObject(bucket_name, key_name);
S3ObjectInputStream s3is = o.getObjectContent();
FileOutputStream fos = new FileOutputStream(new File(key_name));
byte[] read_buf = new byte[1024];
int read_len = 0;
while ((read_len = s3is.read(read_buf)) > 0) {
fos.write(read_buf, 0, read_len);
}
s3is.close();
fos.close();
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
} catch (FileNotFoundException e) {
System.err.println(e.getMessage());
} catch (IOException e) {
System.err.println(e.getMessage());
}

* move, copy

try {
s3.copyObject(from_bucket, object_key, to_bucket, object_key);
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}

* delete

try {
s3.deleteObject(bucket_name, object_key);
} catch (AmazonServiceException e) {
System.err.println(e.getErrorMessage());
}


* 정리
s3 Object API 다루기 (bucket도  API 코드로 다룰수 있지만 혹시 모르니 버킷은 대시보드 내에서 작업 하시는걸 추천드립니다)
1) aws 클라우드 내 작업
- s3에 대한 엑세스 권한 부여

2) Java code 내 작업
- credential 부여 (1번, 2번 방법에 따라 optional)
- amazones3 client 주입
- api 수행


출처


AWS는 문서화가 잘 되어 있기에 공식 문서를 많이 찾아 보는걸 추천드립니다!!


AWS 공식 개발 document

AWS s3 Object Java API

블로그 이미지

yhmane

댓글을 달아 주세요