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 위배 사례
- A 택시 회사의 택시 파견 URI는 aaa.com/driver/Bob/pickupAddress/24/pickupTime/153/destination/ORD 
- 새로운 팀이 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