웹개발지식

2.9 인터페이스를 짧게 유지하고 스마트를 사용하세요 ~ 3.2.2

1son 2024. 5. 6. 23:34
2.9 인터페이스를 짧게 유지하고 스마트를 사용하세요

 

클래스를 작게 만드는 것이 중요하다면

인터페이스를 작게 만드는 것은 훨씬 중요

 

클래스가 다수의 인터페이스를 구현하기 때문, 

예시 - 두개의 인터페이스가 각각 5개씩 메서드를 선언하고 있다면,

두 인터페이스를 모두 구현하는 클래스는 10개의 public 메서드를 가져야 함

 

인터페이스는 구현 클래스가 준수해야하는 계약임

그러니까 인터페이스는 구현자에게 너무 많은 것을 요구하면

단일책임원칙을 위반하는 클래스 만들도록 부추길 수 있음 

 

이를 해결하기 위해서는 인터페이스 안에 '스마트(smart)' 클래스 추가

 

# AS-IS
interface Exchange {
	float rate(String target);
    float rate(String source, String target);
}

#to-be
interface Exchange{
	float rate(String source, String target);
    final class Smart{
    	private final Exchange origin;
        public float toUsd(String source) {
        	return this.origin.rate(source, "USD");
        }
     }
}

 

 

새롭게 추가한 smart 클래스는 공통적인 작업을 수행하는 많은 메서드들 포함 가능

smart 클래스는 인터페이스 위에 특별한 기능을 적용

이 기능은 Exchage(인터페이스)의 서로 다른 구현 사이에 공유될 수 있음 

 

- '스마트' 클래스를 인터페이스와 함께 제공해야 하는 또 다른 이유는 

=> 인터페이스를 구현하는 서로 다른 클래스 안에 동일한 기능을 반복해서 구현하고 싶지 않아서 

 

-ex) 뉴욕 증권 거래소로부터 환율을 조회하는 일은 

NYSE 클래스에 특화된 기능임. 

하지만 환율이 제공되지 않았을 때 'USD' 통화를 적용하는 것은 공유 가능한 공통 기능임 

 

 

다음은 NYSE 클래스를 중첩 클래스 Exchange.Smart와 함께 사용하는 방법을 보여줌 

float rate = new Exchage.Smart(new NYSE()).toUsd("EUR");

 

 

 

-가정) NYSE 뿐만 아니라 모든 Exchage 구현체에 동시에 더 많은 기능을 추가해야 함 

1. USD를 EUR로 빈번하게 변환해야 하고 코드 중복도 피해야함 

(그리고 EUR이라는 문자열 리터럴을 반복적으로 사용하고 싶지 않음 )

 

이에 우리에게 필요한 것은 USD를 EUR로 변환해주는 eurToUsd() 메서드임 

이 메서드는 Exchage 인터페이스보다는 '스마트' 클래스에 추가하는 것이 나음 

이제 '스마트' 클래스는 두 개의 메서드를 포함함 

 

#AS-IS
interface Exchange{
	float rate(String source, String target);
    final class Smart{
    	private final Exchange origin;
        public float toUsd(String source) {
        	return this.origin.rate(source, "USD");
        }
     }
}

#TO-BE
interface Exchange{
	float rate(String source, String target);
    final class Smart{
    	private final Exchange origin;
        public float toUsd(String source) {
        	return this.origin.rate(source, "USD");
        }
        #새롭게 생긴 메서드 
        public float eurToUsd() {
        	return this.toUsd("EUR");
     }
}

 

 

이제부터는 단 한번의 메서드 호출만으로 EUR에서 USD로의 환율을 얻을 수 있습니다 .

float rate = new Exchage.Smart(new NYSE()).eurToUsd();

 

 

'스마트' 클래스의 크기는 점점 더 커지겠지만, 

Exchage 인터페이스는 작고, 높은 응집도를 유지할 수 있음 

 

'smart' 클래스 안에 추가하는 기능은 모든 거래소들이 기능을 공유할 수 있기 때문에 각 거래소들이 해당 기능을 개별적으로 구현할 필요가 없음 => Exchage 인터페이스를 구현하는 일을 번거롭게 만들 필요가 없다는 것 

 

 

이것이 바로 이번 섹션의 제목과 주제를 "인터페이스를 짧게 유지하세요"로 정한 이유임

인터페이스가 커질수록, NYSE를 구현하기 위해 더 많은 것을 요구하고, 더 많은 문제가 발생함

(클래스의 응집도와 견고함이 손상됨)

 

그래서 ! 기본적으로 인터페이스를 짧게 만들고

'스마트' 클래스를 인터페이스와 함께 배포함으로써 공통 기능을 추출하고 코드 중복을 피할 수 있음 

 

이 접근 방법은 3.2.6절에서 다룰 조합 가능한 데코레이터(composable decorator)와 매우 유사합니다. 

데코레이터 vs. '스마트' 클래스 다른점은?

- 스마트 클래스가 객체에 새로운 메서드를 추가하는데 비해 데코레이터는 이미 존재하는 메서드를 좀 더 강력하게 만든다는 점 

 

- 예제) 

interface Exchange{
	float rate(String source, String target);
    final class FAST implements Exchage{
    	private final Exchange origin;
        @override
        public float rate(String source, String target)	{
        	final float rate;
            if (source.equals(target)){
            	rate=1.0f;
            }else {
            	rate=this.origin.rate(source, target);
            }
            return rate;
        }
        public float toUsd(String source) {
        	return this.origin.rate(source, "USD");
        }
     }
}

 

'스마트' 클래스가 있던 곳에 조합가능한 데코레이터가 등장

중첩클래스인 Exchage.Fast는 데코레이터인 동시에 '스마트 클래스'

1. Exchage.Fast는 rate() 메서드를 오버라이드해서 더 강력하게 만듬

2. Exchage.Fast는 새로운 메서드인 toUsd()를 추가해서 USD로 쉽게 환율을 변환할 수 있도록 해줌 

 

 

 

 


 

 

Chapter 3. Employment

 

 

3장. 취업 

 

절차적인 프로그래밍과 OOP의 중요한 차이점?

- 책임을 지는 주체가 무엇인가

 

절차적인 프로그래밍에서는 문장, 연산자, 명령문으로 구성된 코드가 가장 중요한 지위 가짐 

- 데이터는 코드가 다가와서 자신을 수정해 주기를 기다리는 수동적인 존재일뿐

 

객체지향 프로그래밍에서는 데이터를 대체하는 객체가 가장 중요한 지위 차지 

 

이번장의 주제를 한 문장으로 요약하면 

거대한 객체, 정적 메서드, NULL 참조, getter, setter, new 연산자에 반대한다는 것입니다. 

 

 

 

3.1 5개 이하의 public 메서드만 노출하세요

 

클래스의 크기를 정하는 기준으로 public 메서드(protected 메서드 포함)의 개수를 사용권장

=> public 메서드가 많을 수록 클래스가 커짐 

- 적절하다고 생각하는 public 메서드의 수는 5개 

 

- 클래스를 작게 만들어서 얻는 이득?

: 우아함, 유지보수성, 응집도, 테스트 용이성 향상

 

 

3.2 정적 메서드를 사용하지 마세요 

 

정적 메서드는 순수악입니다. 

 

- 정적 메서드는 무엇이고, 정적 메서드가 아직도 OOP에 남아있는 이유는 무엇인지 

예시) HTTP 요청을 전송해서 웹 페이지를 로드하는 기능을 구현한 '클래스'

class WebPage {
	public static String read(String uri){
    	//HTTP 요청을 만들고
        //UTF-8 문자열로 변환
    }
}

//간편한 사용
String html = WebPage.read("http://www.java.com");

 

read() 메서드는 정적 메서드의 일종 

더 나은 방식 => 정적 메서드 대신 객체를 사용 

 

class WebPage{
	private final String uri;
    public String content(){
    	//HTTP 요청을 만들고
        //UTF-8 문자열로 변환한다
    }
}

//위 클래스 사용
String html = new WebPage("http://www.java.com")
  .content();

 

큰 차이가 없어 보임 

오히려 정적 메서드는 더 빠르고 객체 생성과 가비지 컬렉션에 신경쓸 필요 없고 매우 직관적이기 까지. 

하지만 정적 메서드는 소프트웨어를 유지보수하기 어렵게 만듬 

 

 

3.2.1 객체 대 컴퓨터 사고(object vs. computer thinking)

 

초창기 프로그래밍 언어( 어셈블리어, C, COBOL 등) 들의 패러다임은 컴퓨터가 우리를 위해 일하고 

우리는 명령어를 제공해서 지시를 내린다는 것 

 

이 방식의 장점

- 프로그래머가 CPU와 유사한 방식으로 CPU에게 직접 지시할 수 있다는 점 

프로그램의 흐름은 항상 순차적, 스크립트의 위에서 아래로 흐름 

 

이런 순차적인 사고 방식을 가리켜 '컴퓨터 입장에서 생각하기'라고 부름

작은 소프트웨어에서는 문제 없지만 규모 커지면 한계에 직면 

 

 

(def x (max 5 9))

 

이 최대값을 계산하는 것은 프로그래머의 통제 밖에 있음 

x는 최댓값'이다(is a)'라고 정의하는 것이 핵심 

OOP가 절차적 프밍과 차별화 되는 점이 'is a' 입니다. 

 

컴퓨터처럼 생각하기에서 : 명령을 제어할 책임이 프로그래머에게 있음 

OOP에서 : 그저 누가 누구인지만 정의, 객체들이 필요할 때 스스로 상호작용하도록 제어를 위임 

 


 

OOP 관점 최댓값 계산 코드 : 

class Max implements Number { 
	private final Number a;
    private final Number b;
    public Max(Number left, Number right) {
    	this.a = left;
        this.b = right;
    }
}

//Max 클래스 사용방법
Number x = new Max(5, 9);

 

위 코드는 최댓값을 계산하지 않음 

그저 x가 5와 9의 최댓값이라는  (is a) 사실을 정의할 뿐 

CPU에게 계산과 관련된 어떤 지시도 내리지 않고, 단순히 객체 생성 

이런 측면에서 OOP는 함수형 프로그래밍과 매우 유사 

 

반대로, OOP의 정적 메서드는 정확하게 C와 어셈블리어의 서브루틴과 동일 

다음은 정적 메서드를 이용해서 최댓값을 구하는 Java 코드

int x = Math.max(5, 9);

 

형편없는 코드

정적 메서드 사용금지 

 

 

 

 

3.2.2 선언형 스타일 대 명령형 스타일 (declarative vs. imperative style)

 

명령형 프로그래밍 : '프로그램의 상태를 변경하는 문장을 사용해서 계산방식 서술'

선언형 프로그래밍 : '제어 흐름 서술하지 않고 계산로직 표현' 

 

명령형 프로그래밍 : 연산을 차례대로 실행

선언형 프로그래밍 : '엔티티'와 엔티티 사이의 '관계'로 구성되는 자연스러운 패러다임 

 

선언형이 더 강력하지만 명령형 프로그래밍이 쉬움 

 


정적 메서드와 무슨 상관?

- 정적 메서드 방식과 객체 방식 둘 다 a와 b를 비교하는 실제 if문을 감싸고 있을 뿐임

- 둘 사이의 차이점은 다른 클래스, 객체, 메서드가 이 기능을 사용하는 방법에 있음 

 

예시) 두 정수 사이 간격이 있고 그 간격 안에 존재해야 하는 또 다른 정수가 있음 그 정수가 간격 안에 포함되는 지 여부 확인해야함 

 

max()가 정적 메서드라면 

public static int between(int l, int r, int x){
	return Math.min(Math.max(l,x),r);
}

int y = Math.between(5,9,13); // 9를 반환

 

 

기존의 정적 메서드인 Math.min()과 Math.max()를 사용하는 또 다른 정적 메서드인 between()을 추가해야함 

between() 메서드는 호출된 즉시 결과 반환

=> 메서드 호출한 시점에 CPU가 즉시 결과 계산함 => 명령형 스타일 

 

선언형 스타일 

class Between implements Number { 
	private final Number num;
    Between(Number left, Number right, Number x) { 
    	this.num = new Min(new Max(left, x), right);
    }
    @Override
    public int intValue() {
    	return this.num.intValue();
    }
}

//사용예제
Number y = new Between(5,9,13); //아직!

 

이 두 방식의 차이점: 

- 선언형 스타일 : 아직까지는 CPU에게 숫자를 계산하라고 말하지 않음 

: 프로그래서믄 Between이 무엇인지만 정의하고, 변수 y의 사용자가 intValue()의 값을 계산하는 시점을 결정함

 

선언형 방식이 명령형 방식보다 좋은 이유?

- 선언형 방식이 더 빠름 

: 우리가 직접 성능 최적화를 제어할 수 있기 때문

: 인스턴스를 생성하는 데 걸리는 시간이 정적 메서드 호출하는데 걸리는 시간보다 오래걸림 

: 실행경로가 직선적이고 단순한 경우라면 사실 

: 하지만 다수의 정적 메서드를 호출해야 하는 경우 필요한 메서드만 호출할 수 있는 객체 방식 vs.

작업완료 위한 모든 정적 메서드 순차적으로 호출해야하는 방식 

 

//정적메서드 사용 
pulblic void doIt() {
	int x = Math.between(5,9,13);
    if (/* x가 필요한가? */){
    	System.out.println("x=" + x);
    }
}

//선언형 방식 
public void doIt(){
	Integer x = new Between(5,9,13);
    if (/* x가 필요한가? */) {
    	System.out.println("x=" + x);
    }
}

 

- 정적메서드 사용 예제에서는 나중에 x의 값이 필요한지 여부와 무관하게 무조건 x의 값을 계산함 

_CPU는 어떤 경우에도 x의 값이 9라는 사실을 알게됨

 

 

선언형 방식이 더 빠르다

- 두 번째 코드에서는 CPU에게 결과가 실제로 필요한 시점과 위치를 결정하도록 위임하고 CPU는 요청이 있을 경우에만 계산을 실행 

 

OOP에서 선언형 스타일을 선호하는 이유

=> 실행 관점에서 선언형 방식이 더 최적화 되기 때문에 더 빠르다

=> 다형성(코드 블록 사이의 의존성을 끊을 수 있는 능력)

 

예시) min과 max 클래스를 사용하는 대신 if-then-else 구문을 사용하도록 변경 (선언형 방식)

class Between implements Number {
	private final Number num;
    Between(int left, int right, int x){
    	this(new Min(new Max(left, x), right));
    }
    Between(Number number){
    	this.num = number;
    }
}

//다른 알고리즘과 조합
Integer x = new Between(
	new IntegerWithMyOwnAlogrithm(5,9,13)
);

 

새롭게 추가한 ctor을 제외하면 앞에 있던 클래스와 거의 동일 

다른 알고리즘과 조합해서 사용가능 

Between과 Max, Min은 모두 클래스 이기 때문에 Max와 Min으로부터 Between을 쉽게 분리가능 

 

 

객체를 다른 객체로부터 완전히 분리하기 위해서는

메서드나 주ctor 어디에서도 new 연산자를 사용하지 말아야함.

예시) 명령형 코드

int y = Math.between(5,9,13);

위와 동일한 방법으로 연산을 분리하고 리팩토링 불가 

=> 정적 메서드 between()은 두개의 정적 메서드 min()과 max()를 사용하고 있기 때문에, 

앞서 짠 메서드를 전체적으로 다시 구현하는 방법뿐

 

 

 

선언형 프로그래밍 스타일을 선호하는 두번째 이유 

:선언형 프로그래밍을 이용하면 객체 사이의 결합도를 낮출 수 있음, 우아한 처리 + 유지보수성 

 

선언형 프로그래밍 스타일을 선호하는 세번째 이유 

: 표현력 때문 

선언형 방식은 결과를 얘기함 

명령형 방식은 수행 가능한 한 가지 방법을 얘기함 

 

명령형 방식에서 결과를 예상하기 위해선 머릿속에서 코드를 '실행' 해야함

이에 명령형 방식이 선언형 방식보다 덜 직관적 

 


 

//명령형 스타일
Collection<Integer> evens = new LinkedList<>();
for (int  number : numbers) {
	if (number %2==0){
    	evens.add(number);
    }
}

//선언형 스타일
Collection<Integer> evens = new Filtered(
	numbers,
    new Predicate<Integer>() {
    	@Override
        public boolean suitable(Integer number){
        	return number %2== 0;
        }
	}
);

 

명령형 스타일 코드를 이해하기 위해서는 코드 안의 루프를 '마음 속으로 시각화'해야 함 

CPU가 수행해야 하는 일을 코드를 읽는 사람도 동일하게 수행해야함 

 

선언형 스타일 코드에서 알아야 하는 것은 컬렉션이 '필터링' 되었다는 사실 뿐 

코드에는 구현과 관련된 제수 사항은 감춰져 있고, 오직 행동만 표현됨

 

첫번째 코드가 더 읽기 편하다? => 습관의 문제

알고리즘과 실행 대신 객체와 행동의 관점에서 사고한다면 무엇이 올바른지 느껴질 것 

 


 

선언형 프로그래밍 스타일을 선호하는 네번째 이유 

: 코드 응집도 

선언형 스타일 코드에서 evens = new Filterd(...)라는 문장을 통해 evens를 한 줄에 선언했음 

이 경우 '계산'을 책임지는 모든 코드들은 한 곳에 뭉쳐 있으므로 실수로 분리할 수 없음 

그에 비해 명령형 코드는 실수로 코드의 순서를 쉽게 변경할 수 있음_ 오류 가능성 향상 

 

 

정적 메서드는 객체지향 소프트웨어의 암적인 존재 

 

 

 

 

 

 

#스터디 준비 끝