byungil

3장. 패러다임 개요

구조적 프로그래밍

  • 구조적 프로그래밍은 제어흐름의 직접적인 전환에 대해 규칙을 부과한다.

객체지향 프로그래밍

  • 함수 포인터를 특정 규칙에 따라 사용하는 과정을 통해 필연적으로 다형성이 등장하게 된다.

  • 객체 지향 프로그래밍은 제어흐름의 간접적인 전환에 대해 규칙을 부과한다.

함수형 프로그래밍

  • 할당문에 대해 규칙을 부과하는 패러다임

생각할 거리

  • 각 패러다임은 프로그래머에게서 권한을 박탈한다.

  • 어느 패러다임도 새로운 권한을 부여하지 않고, 부정적인 의도를 갖는 일종의 추가적인 규칙을 부과한다.

  • 패러다임은 무엇을 해서는 안 되는지를 말해준다.

4장. 구조적 프로그래밍

증명

  • 다익스트라는 goto 문장이 모듈을 더 작은 단위로 재귀적으로 분해하는 과정에 방해가 되는 경우가 있음을 발견했다.

  • 실제로 뵘과 야코피니는 다익스트라보다 2년 전에 프로그램이 순차, 분기, 반복이라는 3가지 구조만으로 표현할 수 있음을 증명함

기능적 분해

  • 구조적 프로그래밍을 통해 모듈을 증명 가능한 더 작은 단위로 즉, 기능적으로 분해할 수 있게 되었음

  • 거대한 문제 기술서를 받더라도 문제를 고수준의 기능들로 분해할 수 있음. 그리고 이들 각 기능은 다시 저수준의 함수들로 분해할 수 있고, 이러한 분해 과정을 끝없이 반복할 수 있음

  • 게다가 이렇게 분해한 기능들은 구조적 프로그래밍의 제한된 제어 구조를 이용하여 표현할 수 있음

테스트

  • 다익스트라는 “테스트는 버그가 있음을 보여줄 분, 버그가 없음을 보여줄 수는 없다"고 말한 적이 있음

  • 다시 말해 프로그램이 잘못되었음은 증명 가능하지만, 프로그램이 맞다고는 증명할 수 없음

결론

  • 소프트웨어 아키텍터는 모듈, 컴포넌트, 서비스가 쉽게 반증 가능하도록(테스트하기 쉽도록) 만들기 위해 분주히 노력해야 하며, 이를 위해 구조적 프로그래밍과 유사한 제한적인 규칙들을 받아들여 활용해야 함

5장. 객체 지향 프로그래밍

캡슐화

  • 데이터와 함수가 응집력 있게 구성된 집단에서 데이터는 구분선 바깥에서 데이터는 은닉되고 일부 함수만이 외부에 노출된다.

  • OO가 아닌 언어에서도 충분히 가능하며, OO 언어에서 오히려 깨진다.

  • 실제로 많은 OO 언어가 캡슐화를 강제하지 않고, 단지 프로그래머가 캡슐화된 데이터를 우회해서 사용하지 않을 거라는 믿음을 기반으로 함

상속

  • OO 언어가 더 나은 캡슐화를 제공하지는 못했지만, 상속만큼은 확실히 제공했다.

다형성

  • 다형성 역시 함수를 가리키는 포인터를 응용한 것일 뿐이며 OO가 새롭게 만든 것은 없다.

  • 다형성을 좀 더 안전하고 편리하게 사용할 수 있게 해줌

6장. 함수형 프로그래밍

정수를 제곱하기

  • 일반적으로 자바와 같은 프로그램은 가변 변수를 사용하는데, 이는 프로그램 실행 중에 상태가 변할 수 있음을 의미함

  • 하지만 함수형 프로그램에서는 가변 변수가 없음

  • 변수가 한번 초기화되면 절대로 변하지 않는다는 것

예시: 리스트에서 짝수만 필터링하고 제곱한 후 출력하기

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main{
    public static void main(String[] args) {
        // 1. 정수 리스트 생성
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 2. 짝수 필터링 후 제곱하여 새로운 리스트 생성
        List<Integer> squaredEvens = numbers.stream() // 스트림 생성
                .filter(n -> n % 2 == 0) // 람다 표현식을 사용하여 짝수 필터링
                .map(n -> n * n) // 짝수의 제곱을 계산
                .collect(Collectors.toList()); // 최종 결과 리스트로

        // 3. 결과 출력
        System.out.println("Squared Even Numbers: " + squaredEvens);
        Squared Even Numbers: [4, 16, 36, 64, 100]
    }
}

불변성과 아키텍처

  • 아키텍트는 왜 변수의 가변성을 염려하는가?

  • 경합 조건(race condition), 교착 상태, 동시 갱신 문제 모두 가변 변수로 인해 발생함

  • 어떠한 변수도 갱신되지 않는다면 경합 조건이나 동시 업데이트 문제가 일어나지 않는다.

가변성의 분리

  • 가변 컴포넌트와 불변 컴포넌트를 분리하는 것

  • 불변 컴포넌트에서는 함수형 방식으로만 작업이 처리, 어떠한 가변 변수도 사용되지 않음

  • 현명한 아키텍트라면 가능한 많은 처리를 불변 컴포넌트로 옮기고, 가변 컴포넌트에서는 가능한 많은 코드를 빼내야 함

7장. SRP: 단일 책임 원칙

  • 하나의 모듈은 하나의, 오직 하나의 사용자 또는 이해관계자에 대해서만 책임져아 한다.

징후1: 우발적 중복

  • 세 가지 메서드가 서로 매우 다른 세 명의 액터를 책임지기 때문이다.

징후2: 병합(Merge)

  • 소스 파일에 다양하고 많은 메서드를 포함하면 병합이 발생하기 쉬운데, 서로 다른 액터를 책임진다면 그 가능성이 더욱 높음

  • 이 문제를 벗어나기 위해 서로 다른 액터를 뒷받침하는 코드를 서로 분리해야 한다.

8장. OCP: 개방-폐쇄 원칙

  • 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.는 원칙임

  • 행위를 확장할 수 있지만, 개체를 변경해서는 안 된다.

9장. LSP: 리스코프 치환 원칙

  • S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면 S는 T의 하위 타입이다.

  • 상속 구조에서, 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환할 수 있어야 한다.

    • 자식 클래스는 부모 클래스의 책임을 준수하며, 부모 클래스의 행동을 변경하지 않아야 한다.

  • LSP를 위반하면, 상속 클래스를 사용할 때 오동작, 예상 밖의 예외가 발생하거나, 이를 방지하기 위한 불필요한 타입 체크가 동반될 수 있다.

class Parent {
	public Result doSomething() { ... }
}

class Child extends Parent {}

Result r = new Parent().doSomething();
Result r = new Child().doSomething();

LSP 위배 사례

  • 새로운 팀이 B 택시 회사 파견 URI를 일부 다르게 설계함 ex) destination -> dest

  • REST 서비스들이 서로 호환되지 않음

  • 아키텍처에서는 if로 분기 처리가 아니라, 치환되지 않는 REST 서비스들의 인터페이스를 처리하는 매커니즘이 필요함

결론

  • LSP는 아키텍처 수준까지 확자알 수 있고, 반드시 확장해야만 한다.

  • 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문임

10장. ISP: 인터페이스 분리 원칙

  • 클라이언트는 자신이 사용하지 않는 인터페이스에 의존하면 안 된다.

  • 인터페이스를 잘게 쪼개라.

  • ISP를 위반하면, 불필요한 의존성으로 인해 결합도가 높아지고, 특정 기능의 변경이 여러 클래스에 영향을 미칠 수 있다.

public interface Game {
	
	void init();
	
	void run();
	
}

public class A implements Game {

	@Override
	public void init() { ... }
	
	@Override
	public void runt() { ... }
}

public class B implements Game {

	// 초기화 과정이 필요 없는데 Override를 해야 한다. -> ISP 위반.
	@Override
	public void init() { ... }
	
	@Override
	public void runt() { ... }
}

// ISP 원칙에 따라서 인터페이스를 분리하자.

public interface GameInit {
	
	void init();
	
}

public interface GameRun {
	
	void run();
}

public class A implements GameInit, GameRun { ... }

public class B implements GameRun { ... }

11장. DIP: 의존성 역전 원칙

  • 의존성이 극대화된 시스템이란 소스 코드 의존성이 추상에 의존하며 구체에는 의존하지 않는 시스템임

  • 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.

    • 고수준: 추상화 레벨이 높은 쪽

    • 저수준: 추상화 레벨이 낮은 쪽 (구현체, 구체쪽에 가깝다.)

    • 추상화 레벨이 높은 쪽 모듈이 구체쪽에 가까운 모듈에 직접적으로 의존해서는 안 된다.

      • 둘 모두 추상화에 의존해야 한다.

  • 의존성의 순방향: 고수준 모듈이 저수준 모듈을 참조하는 것 의존성의 역방향: 고수준, 저수준 모듈이 모두 추상화에 의존하는 것

    • 저수준 모듈이 번경되어도, 고수준 모듈에는 영향이 가지 않는다.

안정된 추상화

  • 인터페이스에 변경이 생기면 구현체들도 수정해야 하지만, 구현체들에 변경이 생기더라도 인터페이스는 대부분 변경될 필요가 없음

  • 인터페이스는 구현체보다 변동성이 낮으며, 뛰어난 소프트웨어 설계자와 아키텍트라면 인터페이스의 변동성을 낮추려고 노력함

  • 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기 위해 노력하며, 이는 소프트웨어 설계의 기본임

  • 즉, 안정된 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처라는 뜻임

  • 변동성이 큰 구체 클래스를 참조하지 말라

    • 대신 추상 인터페이스를 참조하라

    • 이 규칙은 객체 생성 방식을 강하게 제약하며, 일반적으로 추상 팩토리를 사용하도록 강제함

  • 변동성이 큰 구체 클래스로부터 파생하지 말라

    • 따라서 상속은 아주 신중히 사용해야 한다.

  • 구체 함수를 오버라이드하지 말라.

    • 대체로 구체 함수는 소스 코드 의존성을 필요로 한다.

    • 따라서 구체 함수를 오버라이드 하면 의존성을 제거할 수 없게 되며, 실제로는 그 의존성을 상속하게 된다.

    • 이러한 의존성을 제거하려면, 차라리 추상 함수로 선언하고 구현체들에서 각자의 용도에 맞게 구현해야 한다.

  • 구체적이며 변동성이 크다면 절대로 그 이름을 언급하지 말라.

Last updated