본문 바로가기

Life Style/IT

Clean Architect (1) 프로그래밍 패러다임

반응형

소프트웨어 아키텍처의 목표

-필요한 시스템을 만들고 유지보수하는 데 투입되는 인력의 최소화

아키텍처와 설계

-고수준의 구조(결정사항)과 저수준의 세부사항

아키텍처는 구현과 측정을 통해 증명해야 가는 가설이다.

-톰 길브

행위와 구조

-프로그래머가 지켜야할 두 가지 가치

소프트웨어 존재 가치

-기계의 행위를 쉽게 변경하기 위함

-필요조건 :

(1) 변경 범위에 비례

- 모듈화, 캡슐화(정보은닉)

(2) 변경사항의 행태에는 영향을 받지 않아야함

- TDD

개발자가 이해관계자로서 지켜야할 가치

-긴급하지 않지만 중요한

-긴급하지만 중요하지 않은

의 투쟁에서 아키텍처의 중요성이라는 가치를 지키는 것


프로그래밍 패러다임

1. 구조적 프로그래밍

- 제어흐름의 직접적인 전환에 대한 규칙 부과

-> 조건에 따라 실행흐름 직접 제어

2. 객체지향 프로그래밍

- 제어흐름의 간접적인 전환에 대한 규칙 부과

-> 모듈화, 캡슐화

-> 객체의 상태변화에 따른 행동 제어

3. 함수형 프로그래밍

- 할당문에 대해 규칙 부과

-> 불변성의 람다 계산법 (자바8 람다와 스트림으로 함수형 프로그래밍 지원)

  • 함수와 불변성: 함수는 값을 입력받아 새로운 값을 반환합니다. 입력값은 변경되지 않고, 새로운 값이 생성됩니다. 함수형 프로그래밍에서는 데이터와 상태의 변경을 피하고, 불변성을 유지합니다.
  • 함수 조합: 함수는 메서드를 사용하여 사용자정의함수를 적용합니다. 함수를 조합하여 데이터를 변환합니다.
  • 순수 함수: 함수는 순수 함수입니다. 즉, 함수는 입력값에만 의존하며 외부 상태를 변경하지 않고, 같은 입력값에 대해 항상 같은 결과를 반환합니다.
  • 모듈화: 함수형 프로그래밍에서는 작은 함수를 조합하여 복잡한 작업을 수행합니다. 함수는 독립적이며, 다른 함수와의 조합을 통해 다양한 작업을 수행합니다.

객체지향 프로그래밍과 함수형 프로그래밍의 구조적 차이

  1. 상태와 행동:
    • 객체지향 프로그래밍: 객체는 상태와 행동을 함께 캡슐화하여 관리합니다. 상태는 객체의 속성으로 저장되며, 행동은 메서드를 통해 정의됩니다.
    • 함수형 프로그래밍: 상태를 변경하지 않고, 함수의 입력값에 기반하여 새로운 값을 생성합니다. 함수는 입력에만 의존하며, 상태를 외부로부터 받지 않습니다.
  2. 제어 흐름:
    • 객체지향 프로그래밍: 객체의 메서드가 객체의 상태를 기반으로 제어 흐름을 결정합니다.
    • 함수형 프로그래밍: 함수를 조합하여 데이터를 변환하고 제어 흐름을 처리합니다. 데이터는 불변성을 유지하며, 함수의 조합으로 새로운 데이터를 생성합니다.
  3. 캡슐화와 모듈화:
    • 객체지향 프로그래밍: 데이터와 행동이 객체 내부에 캡슐화되어 있으며, 객체 간의 상호작용은 메서드를 통해 이루어집니다.
    • 함수형 프로그래밍: 함수가 데이터를 변환하며, 함수는 독립적으로 정의되고 조합되어 사용됩니다.

구조적 프로그래밍

: 제어흐름을 직접 관리

객체지향 프로그래밍

: 제어흐름을 간접 관리, 객체의 상태 변경해 행위를 수행

함수형 프로그래밍

: 상태를 변경하지 않고, 문제를 해결

-> 세 가지 관심사 (함수, 컴포넌트 분리, 데이터 관리)

-> 패러다임의 전환마다 권한의 축소


구조적 프로그래밍

증명 기법인 분할정복법의 사용 목적

-> 순차, 분기와 반복이라는 단순한 제어 구조로 변경

기능적 분해

-> 모듈을 증명 가능한 작은 단위로 재귀적 분해

참이라는 건 증명 불가능

-> 반증이 가능하도록 구조화 하는 것이 필요

-> 모듈, 컴포넌트, 서비스를 만들 때 테스트하기 쉽도록 설계

테스트

-> 프로그램이 맞다고 증명하는 것이 테스트가 아니라, 프로그램이 잘못되었음을 증명하는 것이 테스트

-> 충분한 테스트를 통해 얻을 수 있는 것은 충분히 참이라고 여길 수 있도록 하는 것


객체지향 프로그래밍

객체지향 프로그래밍의 역사

-> 함수 호출 스택 프레임을 힙으로 옮겨서 데이터 구조를 함수에 전달하는 것에서 시작

OO (Object-oriented)의 조건

-> 캡슐화, 상속, 다형성을 적절하게 조합하거나 세 가지 요소를 반드시 지원해야한다.


캡슐화

-> 데이터와 함수를 쉽고 효과적으로 캡슐화

->응집력있는 묶음의 단위

-> 데이터는 은닉되고 일부 함수만 외부에 노출

# 자바와 C#에서 헤더와 구현체를 분리하는 방식을 제거

-> 클래스 선언과 정의를 구분하는게 불가능

-> 캡슐화의 훼손

OO 언어가 완벽한 캡슐화를 지원한다. (X)

-> 사용자가 쉽게 캡슐화를 사용할 수 있게 기능을 제공하는 것.

-> 캡슐화를 강제하지 않는다.


상속

-> 단순히 어떤 변수와 함수를 하나의 유효 범위로 묶어서 재정의 하는 것

-> 눈속임으로 사용하던 기법을 상속이라는 편리한 방식으로 만듦

-> 강제로 타입을 변환하던 것을 OO 언어에서 업캐스팅으로 구현


다형성

다형성의 표현

-> 행위가 인터페이스의 타입에 의존하는 함수

입출력 인터페이스

-> 다양한 데이터 소스와 싱크를 일관된 방식으로 처리할 수 있다

유닉스 운영체제에서 입출력 장치 드라이버가 표준 함수를 제공해야 하는 요구

->다형성의 원칙에 따라 다양한 장치와 소프트웨어 간의 일관된 상호작용을 보장하기 위한 것

다형성의 기초

-> 다형적 행위를 수행하기 위한 함수를 가리키는 포인터의 응용

-> 함수 포인터 사용의 위험성을 감소시키고, 사용하기 쉽도록 만든 OO 언어

다형성의 필요성

-> 입출력 드라이버는 프로그램의 플러그인 형탸

-> 프로그램은 장치 독립적이어야함

장치 의존적인 프로그램의 문제점

-> 장치의 변경에 따라서 프로그램의 변화가 연속적으로 발생


의존성 역전 원칙 (DIP)

-> 일반적인 의존성의 방향은 반드시 제어흐름을 따르지만, 상속관계에서는 특별하다.

  • 상위 모듈은 하위 모듈에 의존하지 말고, 하위 모듈이 상위 모듈에 의존해야 한다.
  • 추상화는 구체화에 의존하지 말고, 구체화가 추상화에 의존해야 한다.

다형성으로 인한 제어 흐름의 변화

-> 모듈과 인터페이스 사이의 상속 관계가 존재할 경우 제어흐름이 역전된다.

-> 따라서 소스코드 의존성과 제어흐름의 방향이 일치하도록 제한되지 않는다.

-> 호출하던 호출당하던, 소스 코드 의존성의 방향을 결정할 수 있다.

DIP 구현 방법
DIP (Dependency Inversion Principle)를 구현하는 방법에는 여러 가지가 있으며, 참조 변수 타입을 부모 타입으로 설정하는 것 외에도 다양한 접근 방식이 있습니다. 이 원칙을 구현하는 주된 방법들을 설명하겠습니다:

1. 인터페이스 기반 설계

인터페이스 기반 설계는 DIP의 핵심 원칙 중 하나로, 구체적인 구현체에 의존하지 않고 인터페이스를 통해 의존성을 주입하는 방법입니다. 이는 상위 모듈이 하위 모듈의 구체적인 세부 사항에 대해 알 필요 없이, 인터페이스를 통해 상호작용하도록 합니다.

  • 인터페이스 정의: 기능을 정의하는 인터페이스를 작성합니다.
  • 구현체 작성: 인터페이스를 구현하는 구체적인 클래스를 작성합니다.
  • 의존성 주입: 상위 모듈이 인터페이스를 통해 하위 모듈과 상호작용합니다.

예시

public interface PaymentProcessor {
    void processPayment(double amount);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class OrderService {
    private PaymentProcessor paymentProcessor;

    public OrderService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }

    public void placeOrder(double amount) {
        paymentProcessor.processPayment(amount);
    }
}

2. 의존성 주입 (Dependency Injection)

의존성 주입은 객체가 자신의 의존성을 직접 생성하거나 찾는 것이 아니라, 외부에서 주입받는 방법입니다. 이는 DIP를 준수하는 일반적인 방식입니다. 의존성 주입은 크게 생성자 주입, 세터 주입, 인터페이스 주입으로 나뉩니다.

생성자 주입

객체 생성 시 의존성을 주입합니다.

public class OrderService {
    private final PaymentProcessor paymentProcessor;

    public OrderService(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
}

세터 주입

객체 생성 후 세터 메서드를 통해 의존성을 주입합니다.

public class OrderService {
    private PaymentProcessor paymentProcessor;

    public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
}

인터페이스 주입

인터페이스를 통해 의존성을 주입합니다.

public interface PaymentProcessorAware {
    void setPaymentProcessor(PaymentProcessor paymentProcessor);
}

public class OrderService implements PaymentProcessorAware {
    private PaymentProcessor paymentProcessor;

    @Override
    public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
        this.paymentProcessor = paymentProcessor;
    }
}

3. 서비스 로케이터 패턴 (Service Locator Pattern)

서비스 로케이터 패턴은 객체의 의존성을 런타임에 찾는 방식입니다. 이는 의존성을 직접 주입하는 대신, 서비스 로케이터를 통해 필요한 객체를 조회합니다. 이 패턴은 DIP를 적용하는 데 유용하지만, 의존성 주입에 비해 테스트하기 어렵고 코드의 명시성이 떨어질 수 있습니다.

예시

public class ServiceLocator {
    private static final Map<Class<?>, Object> services = new HashMap<>();

    public static <T> void registerService(Class<T> clazz, T service) {
        services.put(clazz, service);
    }

    public static <T> T getService(Class<T> clazz) {
        return clazz.cast(services.get(clazz));
    }
}

public class OrderService {
    private PaymentProcessor paymentProcessor;

    public OrderService() {
        this.paymentProcessor = ServiceLocator.getService(PaymentProcessor.class);
    }
}

4. 팩토리 패턴 (Factory Pattern)

팩토리 패턴은 객체의 생성 로직을 별도의 팩토리 클래스로 분리하여 DIP를 적용하는 방법입니다. 팩토리 클래스가 객체 생성의 책임을 가지며, 클라이언트 코드는 팩토리를 통해 객체를 얻습니다.

예시

public interface PaymentProcessorFactory {
    PaymentProcessor createPaymentProcessor();
}

public class CreditCardProcessorFactory implements PaymentProcessorFactory {
    @Override
    public PaymentProcessor createPaymentProcessor() {
        return new CreditCardProcessor();
    }
}

public class OrderService {
    private PaymentProcessor paymentProcessor;

    public OrderService(PaymentProcessorFactory factory) {
        this.paymentProcessor = factory.createPaymentProcessor();
    }
}

5. 모듈화와 추상화

모듈화와 추상화를 통해 소프트웨어를 계층화하고, 상위 모듈이 하위 모듈의 세부 사항에 의존하지 않도록 합니다. 모듈화는 코드의 유지보수성과 확장성을 높이는 데 도움을 줍니다.

요약

DIP를 구현하는 방법은 다양하며, 참조 변수의 타입을 부모 타입으로 설정하는 것 외에도 여러 가지 방법이 있습니다:

  • 인터페이스 기반 설계: 인터페이스를 통해 의존성을 주입합니다.
  • 의존성 주입: 생성자, 세터, 또는 인터페이스를 통해 의존성을 주입합니다.
  • 서비스 로케이터 패턴: 런타임에 서비스 로케이터를 통해 의존성을 조회합니다.
  • 팩토리 패턴: 팩토리 클래스를 통해 객체를 생성하고 의존성을 주입합니다.
  • 모듈화와 추상화: 코드의 계층화와 추상화를 통해 DIP를 구현합니다.

이러한 원칙과 패턴을 통해 소프트웨어의 유연성과 유지보수성을 향상

 

DIP의 활용

-> 업무 규칙이 DB와 UI에 의존하는 의존성 방향

-> DB와 UI가 업무 규칙에 의존하도록 의존성 방향 역전

: 배포 독립성, 개발 독립성

 

 


객체지향 프로그래밍의 결론

-> 다형성을 이용해 전체 시스템의 소스 코드 의존성의 절대적인 제어 권한 획득

-> 이로 인한 모듈 간의 독립성 보장


함수형 프로그래밍

-> 변수의 상태를 바꾸지 않는 특징

-> 변수의 불변성

 

일반적으로 변수는 가변 변수

-> 경합 조건, 교착상태 조건, 동시 업데이트 문제 발생 가능성 존재

-> 동시성 제어의 어려움

 

불변성의 실현 가능여부를 판단하기 위한 기준 : 가변성의 분리

-> 가변 컴포넌트와 불변 컴포넌트로의 분리

 

트랜잭션 메모리

-> DB가 디스크의 레코드 다루는 방식으로 메모리 변수를 처리

-> 변수를 보호하는 역할 수행

 

이벤트 소싱

전통적인 트랜잭션과 이벤트 소싱의 비교

  • 전통적인 트랜잭션:
    • 주문을 생성하면 데이터베이스의 주문 테이블에 새로운 주문 레코드가 삽입됩니다. 이 과정에서 트랜잭션이 시작되고, 주문 생성 작업이 완료되면 트랜잭션이 커밋됩니다. 현재 상태는 데이터베이스에 저장됩니다.
  • 이벤트 소싱:
    • 주문을 생성하면 "주문 생성" 이벤트가 발생하여 이벤트 저장소에 저장됩니다. 이 이벤트는 주문 생성의 변화를 기록하며, 현재 상태는 이벤트를 재생하여 도출됩니다. 이벤트 소싱에서는 상태를 직접 저장하지 않고, 상태 변경 이력(이벤트)을 저장합니다.

요약

  • 전통적인 상태 관리에서는 현재 상태를 직접 저장하고 트랜잭션을 사용하여 상태 변경을 관리합니다.
  • 이벤트 소싱에서는 상태 변경을 나타내는 이벤트를 저장하고, 이 이벤트들을 통해 시스템의 상태를 재구성합니다.
  • 이벤트 소싱은 상태 변경 이력을 보존하고, 과거 상태를 쉽게 조회할 수 있도록 하며, 시스템의 비동기성과 확장성을 높이는 데 유용합니다.

-> 애플리케이션은  CRUD가 아닌 CR만 수행하므로 동시성 제어 문제에서 해방

 

EX) 소스 코드 버전 관리 시스템 -> git -> 모든 변경을 기록

 

 

반응형