다형성(polymorphism)

조상 클래스 타입 참조 변수로 자손 타입 객체를 가리키는

실제 생성된 인스턴스가 무엇인지가 가장 중요하다.

 

public class Test {
	public static void main(String[] args) {
		Tv tv = new SmartTv();
		tv.down();
		tv.up();
//		tv.search(); 사용 못한다. Tv클래스를 보면 search()없기 때문
		//실제 생성된 객체는 SmartTv이기에 타입을 변경하면 호출할 수 있다.
		((SmartTv)tv).search();
	}
}

class Tv{
	void up() {}//채널 업
	void down() {}//채널 다운
}
class SmartTv extends Tv{
	void search() {}//인터넷 검색기능
}

원래 생성된 객체가 SmartTv니까 문제가 없다. 만약 Tv라면 어떻게 될까?

컴파일러는 오류를 못잡는다. 즉, 런타임 예외가 발생하게 된다. (실행 중 예외) 

형변완 예외 발생

이 때문에 형변환 시에 무조건 instanceof 연산자로 형변환가능한 경우만 형변환되도록 하는 것이 중요하다.

 

위까지가 다형성의 사전 지식

public class Test {
	public static void main(String[] args) {
		Tv tv = new SmartTv();
		tv.down();
		tv.up();
	}
}
class Tv{
	void up() {
		System.out.println("일반 TV 채널 업");
	}//채널 업
	void down() {
		System.out.println("일반 TV 채널 다운");
	}//채널 다운
}
class SmartTv extends Tv{
	void up() {
		System.out.println("스마트 TV 채널 업");
	}//채널 업
	void down() {
		System.out.println("스마트 TV 채널 다운");
	}//채널 다운
	void search() {}//인터넷 검색기능
}
///////////////////////////////////////
스마트 TV 채널 다운
스마트 TV 채널 업

실제 동작은 생성된 객체 구현부로 동작되는 것을 알 수 있다. 이를 통한 다형성 사례

public class Test {
	public static void main(String[] args) {
		SmartTv tv1 = new LGSmartTv();
		SmartTv tv2 = new SamsungSmartTv();
		tvTest(tv1);
		tvTest(tv2);
	}
	//앞으로 새로운 스마트 TV가 추가되어도 기존 아래 코드는 수정할 일이 없다.
	static void tvTest(SmartTv smartTv) {
		smartTv.down();
		smartTv.up();
		smartTv.search();
	}
}
class Tv{
	void up() {
		System.out.println("일반 TV 채널 업");
	}//채널 업
	void down() {
		System.out.println("일반 TV 채널 다운");
	}//채널 다운
}
//객체 생성을 막음
abstract class SmartTv extends Tv{
	void up() {
		System.out.println("스마트 TV 채널 업");
	}//채널 업
	void down() {
		System.out.println("스마트 TV 채널 다운");
	}//채널 다운
	void search() {
		System.out.println("스마트한 검색");
	}//인터넷 검색기능
}

class LGSmartTv extends SmartTv{
	void up() {
		System.out.print("LG");
		super.up();
	}
	void down() {
		System.out.print("LG");
		super.down();
	}
	void search() {
		System.out.print("LG의 ");
		super.search();
	}
}
class SamsungSmartTv extends SmartTv{
	void up() {
		System.out.print("삼성");
		super.up();
	}
	void down() {
		System.out.print("삼성");
		super.down();
	}
	void search() {
		System.out.print("삼성의 ");
		super.search();
	}
}
////////////////////////////////
LG스마트 TV 채널 다운
LG스마트 TV 채널 업
LG의 스마트한 검색
삼성스마트 TV 채널 다운
삼성스마트 TV 채널 업
삼성의 스마트한 검색

객체지향 원칙 캡슐화, 추상화, 상속, 다형성을 기반으로 다양한 디자인 패턴들이 존재한다.

 

인터페이스(interface)

일종의 추상클래스

추상클래스 처럼 추상메서드를 갖지만 추상클래스보다 추상화 정도가 높다

 

개발 코드가 객체에 종속되지 않게 하여 객체를 교체할 수 있도록 한다. (OCP, 변경엔 닫혀있고, 확장엔 열려있어야한다)

객체 간 느슨한 결합을 위한 핵심

객체지향 원칙에 구현보다는 인터페이스에 맞춰서 프로그래밍한다는 원칙도 있다.

 

인터페이스 모든 멤버변수는 public static final이다. 그렇기에 생략해도 컴파일러가 일괄적으로 붙여준다.

모든 메서드는 public abstract다.

 

인터페이스 간 상속도 당연히 된다. expands 키워드, 인터페이스를 클래스가 구현하면 implements

 

느슨한 결합 예시

public class Test {
	public static void main(String[] args) {
		LgTv lgTv = new LgTv();
		tvTest(lgTv);
	}
	//지금 구조에서 기존 코드(tvTest)를 절대 건드리지 말고 SamSungTv로 바꿔 동작하게 할 수 있나? 
	public static void tvTest(LgTv lgTv) {
		lgTv.volDown();
		lgTv.volUp();
	}
}
class LgTv{
	void volUp() {}
	void volDown() {}
}

class SamSungTv{
	void volUp() {}
	void volDown() {}
}

절대 불가능하다.

public class Test {
	public static void main(String[] args) {
		LgTv lgTv = new LgTv();
		SamSungTv samSungTv = new SamSungTv();
		//자동 형변환된다.
		tvTest(lgTv);
		tvTest(samSungTv);
	}
	//지금 구조에서 기존 코드(tvTest)를 절대 건드리지 말고 SamSungTv로 바꿔 동작하게 할 수 있나? 
	public static void tvTest(Tv tv) {
		tv.volDown();
		tv.volUp();
	}
}
interface Tv{
	void volUp();
	/*public abstract*/void volDown();
}
class LgTv implements Tv{
	public void volUp() {}
	public void volDown() {}
}

class SamSungTv implements Tv{
	public void volUp() {}
	public void volDown() {}
}

객체지향을 시작으로 다양한 디자인 원칙, 더 나아가 디자인 패턴에서 중요한 핵심은 기존코드를 수정하지 않고 새로운 기능을 손쉽게 추가하는 것이다.

익명 클래스(anonymous class)

이름 없는 클래스, 이유는 선언과 객체 생성을 동시에 하기 때문이다.

일회용으로 쓰거나, 오직 하나의 객체만을 생성한다.

 

public class Test {
	public static void main(String[] args) {
		new AnonymousTest();
	}
}
class AnonymousTest {
	public AnonymousTest() {
		System.out.println("객체 선언과 동시에 생성");
	}
}
////////////////////
객체 선언과 동시에 생성
public class Test {
	public static void main(String[] args) {
		Runnable runnable = ()->Test.call("1");
		Thread thread = new Thread(runnable);
		thread.start();
		call("0");
	}
	static void call(String msg) {
		for(int i = 0; i< 10000 ; i++) {
			System.out.print(msg);
		}
	}
}

람다식은 익명 구현 객체이다.

 

 

 

 

 

 

 

 

 

'개발 > 자바(JAVA)' 카테고리의 다른 글

컴포지트 패턴 연습 - 1  (0) 2023.04.13
기본형 배열 List로 변환하기  (0) 2023.03.10
객체 지향 - 5  (0) 2023.02.01
객체 지향 - 4  (0) 2023.01.31
객체 지향 - 3  (0) 2023.01.29

클래스 메서드(static메서드) 인스턴스 메서드

static 붙으면 static 메서드

클래스는 '데이터와 데이터에 관련된 메서드 집합'

따라서 클내스 내에 있는 메서드와 멤버변수는 아주 밀접한 관계가 있다.

 

1. 클래스를 설계할 때. 멤버변수 중 모든 인스턴스에 공통으로 사용하는 것에 static을 붙인다. 
생성된 각 인스턴스는 서로 독립적이기 때문에 각 인스턴스의 변수는 서로 다른 값을 유지한다. 그러나 모든 인스턴스에서 같은 값이 유지되어야 하는 변수는 static을 붙여서 클래스변수로 정의해야 한다. 

2. 클래스 변수(static변수)는 인스턴스를 생성하지 않아도 사용할 수 있다. 
static이 붙은 변수(클래스변수)는 클래스가 메모리에 올라갈 때 이미 자동적으로 생성되기 때문이다. 

3. 클래스 메서드 (static 메서드)는 인스턴스 변수를 사용할 수 없다. 
클래스 메서드는 JVM로딩과 동시에 생성된다. 반면에 인스턴스 변수는 객체를 생성해야만 접근할 수 있다.

즉, 클래스 메서드가 존재할 때 반드시 인스턴스 변수가 존재할 거란 보장이 없다.

위와 같은 이유로 인스턴스 멤버는 반드시 클래스 멤버가 존재할 거란 보장이 되기에 아무런 문제가 없다.


4. 메서드 내에서 인스턴스 변수를 사용하지 않는다면, static을 붙이는 것을 고려한다 
메서드의 작업내용 중에서 인스턴스변수를 필요로 한다면, static을 붙일 수 없다. 반대로 인스턴스변수를 필요로 하지 않는다면 static을 붙이자. 메서드 호출시간이 짧아지므로 성능이 향상된다. static을 안 불인 메서드(인스턴스메서드)는 실행 시 호출되어야할 메서드를 찾는 과정이 추가적으로 필요하기 때문에 시간이 더 걸린다. 

 

오버로딩(overloading)

같은 메서드 이름으로 메서드를 만드는 것, 구분 방법은 매개변수 뿐이다.

매개변수라 함은 매개변수 타입과, 개수이다.

가변인자

내부적으로 배열로 처리

가급적 가변인자는 사용하지 말 것, 명확히 배열 타입이나 컬렉션을 사용할 것

생성자(Constructor)

인스턴스가 생성될 때마다 호출되는 인스턴스 초기화 메서드, 메서드 이름은 클래스 이름과 같다.

 

초기화

초기화를 흔히 깨끗함, 0으로 만들기, 문자열이면 "" 같이 공백으로 넣기 등으로 생각하는 사람이 있다.

아니다. 바로 사용가능한 상태를 만드는 것이 초기화이다.

 

생성자도 근본적으로 메서드다. (반환 값이 없는 메서드)

메서드는 일련의 동작들이다. 즉, 복잡한 초기화를 할 수 있다. 

 

기본 생성자(default constructor)

매개변수가 없는 생성자

생성자가 클래스 안에 하나도 없을 자동으로 컴파일러가 추가해줌

하지만 무조건 직접 만들어주는 것을 권장

매개변수가 있는 생성자를 만들 경우 기본 생성자 추가가 자동으로 안된다. 따라서 무조건 기본 생성자는 수동으로 넣는 것이 좋다.

생성자에서 다른 생성자 호출하기 - this()

생성자 이름 대신 this를 사용하는 것

this는 객체 자신의 주소가 저장되어 있다. 

public class Constructor {
	public Constructor() {
		this(1);
		//반드시 한 개만 호출 가능
//		this(1,2);
	}
	public Constructor(int a) {
		//순환 구조가 생기면 안된다.
//		this();
		this(a,2);
	}
	public Constructor(int a,int b) {
//		super();
	}
}

변수 초기화

기본형은 초기화를 하지 않아도 기본 값이 존재한다.

참조형은 객체 주소이므로 초기값이 존재할 수가 없다. new 연산자로 힙영역에 객체를 생성해야 그 주소가 참조형 변수에 담기기 때문이다. 따라서 null 상태

char는 공백이 초기값이다.  \u0000

 

 

멤버변수의 초기화 방법

  • 명시적 초기화
    선언과 동시에 값을 대입하는 것
  • 생성자
    생성자 호출
  • 초기화 블록
    아무 이름 없이 { } 중괄호만 있는 것

public class Constructor2 {
	int a = 1;
	static {
		System.out.println("JVM에 이 클래스 적재 시 단 한번만 호출, 즉 모든 인스턴스 초기화보다 먼저 호출 됨");
//		System.out.println(a);당연히 사용 못함
	}
	{
		System.out.println(a);
		System.out.println("생성자보다 먼저 호출");
		a = 10;
	}
	public Constructor2() {
		System.out.println(a);
		System.out.println("생성자 호출");
		a = 100;
	}
	public static void main(String[] args) {
		Constructor2 c = new Constructor2();
		System.out.println(c.a);
	}
}

static 초기화 블록이 있는 이유

public class Constructor3 {
	static int a = 1;
	static {
		//클래스 멤버에 대해 복잡한 초기화가 필요할 수 있기 때문
		System.out.println("무언가 복잡한 방법으로 static a를 초기화하고 싶음");
	}
}
클래스 초기화 인스턴스 초기화
기본값 명시적 초기화블록 기본값 명시적 초기화블록 생성자

클래스 초기화는 JVM에 해당 클래스가 적재될 때 단 한 번만 수행 됨

 

 

 

 

 

 

 

 

 

 

 

 

'개발 > 자바(JAVA)' 카테고리의 다른 글

객체 지향 - 5  (0) 2023.02.01
객체 지향 - 4  (0) 2023.01.31
객체 지향 - 2  (0) 2023.01.27
객체 지향 - 1  (0) 2023.01.25
파일 옮기기  (0) 2022.07.25

자바 객체지향 언어

  • 코드 재사용성이 높다. 
  • 코드 관리가용이하다.
  • 신뢰성이 높은 프로그래밍을 가능하게 한다.

 

객체 지향 프로그래밍

부품에 해당하는 객체를 먼저 만들고, 이걱들을 하나 씩 조립해서 완성된 프로그램을 만드는 기법

 

객체 간의 관계

  • 상속관계
  • 사용관계
  • 집합관계

객체 지향 프로그래밍 특징

    • 캡슐화
      객체의 필드, 메서드를 하나로 묶고, 실제 구현내용을 감추는 것
      접근 제한자를 통해 구현
      감추는(보호하는) 이유는 외부의 잘못된 사용으로 손상을 막기 위함
    • 상속
      상위 객체가 가진 필드와 메서드를 하위 객체에게 물려주는 또한 상위 객체 변경이 하위 객체에게 영향을 미치므로 유지보수성도 상승 한다.
      , 상위 객체를 재사용하여 빠른 설계와 코드 재사용성 상승으로 개발 효율이 올라간다.
    • 다형성
      같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질
      부모 클래스(인터페이스) 타입으로 자식 객체를 다루는 것
    • 추상화
      공통된 성질을 뽑아 상위 타입으로 만든다.

 

클래스와 객체

클래스는 객체를 정의해 놓은 것

클래스는 객체를 생성하는데 사용된다.

 

new 연산자

new 연산자 뒤엔 반드시 생성자를 호출한다.

클래스에 new 연산자를 사용해 객체를 생성, 생성된 객체는 자바 heap 영역에 존재하며, 그 주소 값를 리턴한다.

 

객체의 구성요소

속성 - 멤버변수, 특성, 필드, 상태

기능 - 메서드 , 함수, 행위

 

 

클래스의 구성 멤버

필드

객체가 가져야할 데이터

상태, 정보, 다른 객체의 참조변수

 

생성자

new 연산자와 같이 사용되어 클래스로부터 객체를 생성할 호출되는 메서드

객체 초기화 담당

heap영역에 생성 주소를 리턴

아무 것도 없을 컴파일러가 기본 생성자를 추가해준다.

, 명시적으로 다른 생성자를 만들었으면 기본생성자는 추가 안해준다

명시적 초기화 이유는 복잡 초기화를 위함이다.

 

메소드

객체에 동작에 해당

객체간 데이터 전달

필드 수정 등등

메소드 선언부 - 리턴타입, 메소드이름, 매개변수(파라미터) 선언

이를 시그니처(signature)라고도 한다.

 

클래스의 다른 정의

객체지향의 관점에서 클래스는 객체를 생성하기 위한 , 클래스는 속성과 기능으로 정의된다.

 

클래스를 바라보는 3가지 관점

  • 설계도 
  • 데이터와 함수의 결합
  • 사용자 정의 데이터 타입

변수와 메서드

선언위치 변수종류

클래스 변수, 인스턴스 변수, 지역변수 종류가 있다.

구분 방법은 선언 위치이다.

 

멤버 변수를 제외하고는 전부 지역 변수이며

그중 static 붙은 것을 클래스 변수라 한다.

public class Variables {
	int iv; //인스턴스 변수, 객체와 같은 생명주기 , 힙영역에 위치
	static int cv;// 클래스 변수 JVM과 같은 생명 주기, 메서드 영역에 위치
	
	void method() {
		int lv = 0; //지역변수.  스택 영역에 위치
		//지역변수는 선언과 동시에 초기화를 해줘야 한다.
	}
}

1. 인스턴스 변수(instance variable)

클래스 영역에 선언

클래스의 인스턴스가 생성되었을 생성된다.

각각 독립적인 저장공간을 가진다.(서로 다른 가능)

인스턴스 마다 고유한 상태를 유지해야하는 경우 인스턴스 변수를 사용

객체가 제거 같이 소멸

 

2. 클래스 변수(class variable)

인스턴스 변수 앞에 static 키워드를 붙인

클래스가 메모리에 올라갈 생성되며, 인스턴스 생성 없이 바로 사용 가능

모든 인스턴스가 공통된 저장공간을 공유한다.

JVM 종료될 같이 소멸

 

3. 지역 변수(local variable)

메서드 내에 선언, 메서드 내에서만 사용 가능

메서드 종료 같이 소멸

해당 블록에서만 유효 { }

 

클래스 변수 아무때나 사용가능하다.

프로그램 시작과 동시에 고정으로 생성되기 때문이다.

클래스 변수의 수정은 전역 영향을 미친다.

클래스 변수의 초기화는 묵시적으로 가능하다.

클래스로부터 생성된 객체들이 공유하는 데이터를 클래스 멤버로 만든다.

하나의 메모리를 같이 공유(메서드 영역)

 

인스턴스 변수는 인스턴스가 생성돼야 사용 가능하다.

인스턴스는 힙영역에 동적으로 할당 되기 때문이다.

인스턴스 변수의 변화는 해당 인스턴스만 영향을 미친다.

인스턴스 변수의 초기화는 묵시적으로 가능하다.

필요한 필드나 메서드는 같지만 객체마다 서로 다른 값이 필요한 경우 인스턴스 멤버로 쓴다.

객체마다 별도의 데이터 공간이 할당된다(힙영역)

 

지역 변수는 메서드 구현부 블록 {} 안에서만 유효하다.

스택 영역에 저장되며 블록 {} 벗어나면 소멸한다.

지역 변수의 변화는 해당 메소드에만 영향을 미친다.

지역 변수는 반드시 명시적 초기화 해줘야 한다.

 

메서드

특정 작업을 수행하는 문장을 작업단위로 묶어서 이름을 붙인

 

값을 받아서 처리하고, 결과를 반환

중요한 것은 메서드가 어떤 작업을 수행하는 필요가 없다는 것이다.

, 메서드에 넣을 (입력) 결과(출력) 알면 된다.

그래서 메서드를 내부가 보이지 않는 '블랙박스'라고도 한다.

 

 

메서드를 사용하는 이유 가지

높은 재사용성(reusability)

Java API에서 제공하는 메서드들을 생각해보자

만들어 놓으면 번이고 다른 프로그램에서 사용이 가능하다.

 

중복된 코드 제거

반복되는 코드를 묶어서 하나의 메서드로 작성해 놓으면

반복되는 코드 대신 메서드 호출 번으로 대체가 가능하다.

, 코드량과 코드 유지보수성이 증가한다.

코드 변경에도 유리하다.

 

프로그램 구조화

main()메서드 안에 어플리케이션 코드를 집어넣을 수는 없다.

작업 단위로 나눠서 여러 개의 메서드에 담아 프로그램의 구조를 단순화 시키는 것이 필수적이다.

 

메서드의 선언과 구현

메서드는 크게 부분, 선언부와 구현부로 이뤄져 있다.

void method(){ // 이외 선언부, 선언부의 다른 이름 시그니처
/*{}속이 구현부*/
}

반복적으로 수행되는 여러 문장을 하나의 메서드로 작성

하나의 메서드는 가지 기능만을 수행하도록 작성(재사용, 이식성)

 

반환 값은 있을 수도 있고 없을 수도 있지만

반환 값이 있을 경우 반드시 1개만 반환한다.

만약 여러 개를 값을 반환하고 싶다면 배열이나 객체로 반환한다

리턴 값은 당연히 반환 타입과 일치해야한다.

모든 메서드는 반드시 하나의 return문이 있어야한다.

반환 값이 없는 void경우 컴파일러가 자동으로 추가해 준다.

 

메서드 선언부는 후에 변경사항이 발생하지 않도록 신중히 작성해야한다. 메서드를 호출하는 부분이 전부 변경되기 때문이다.

 

 

 

 

 

 

'개발 > 자바(JAVA)' 카테고리의 다른 글

객체 지향 - 3  (0) 2023.01.29
객체 지향 - 2  (0) 2023.01.27
파일 옮기기  (0) 2022.07.25
변수의 타입  (0) 2022.07.07
변수  (0) 2022.07.03

디자인 패턴의 중요성

프로그래밍 자주 발생하는 문제를 이미 누군가 좋은 해답으로 해결한

그것을 우리는 배워서 재사용하는 것이다.

디자인 패턴은 코드가 아닌 경험을 재사용하는 것이다.

 

 

디자인 원칙
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.(캡슐화)

달라지는 부분을 캡슐화하면 향후 다시 변경 시 나머지 안 달라지는 부분에 대한 영향도가 감소한다.

 

 

디자인 원칙
구현보다는 인터페이스에 맞춰서 프로그래밍한다.(다형성)

구현부가 존재하는 구체적인 클래스에 맞춰 프로그래밍하면 유연하지 못하다.

 

디자인 원칙
상속보다는 구성을 활용한다

자바는 단일 단일 상속만 지원한다. 따라서 더욱 적극적으로 구성을 활용하는 것이 좋다.

 

전략패턴(Strategy Pattern)

알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 쓸 수 있게 해준다.

알고리즘은 일련의 행동을 정의해둔 행동이다

 

public interface Movable {
	void move();
}

public class Fly implements Movable{

	@Override
	public void move() {
		System.out.println("날아갑니다...");
	}

}

public class Riding implements Movable{

	@Override
	public void move() {
		System.out.println("말타고 갑니다...");
	}

}
public class Walk implements Movable{

	@Override
	public void move() {
		System.out.println("걸어가기...");
	}

}

public interface Weapon {
	void attack();
}

public class BowWeapon implements Weapon{

	@Override
	public void attack() {
		System.out.println("화살 쏘기!");
	}
	
}
public class SpearWeapon implements Weapon{

	@Override
	public void attack() {
		System.out.println("찌르기!");
	}
	
}

public class SwordWeapon implements Weapon{

	@Override
	public void attack() {
		System.out.println("베기!");
	}
	
}



public abstract class Character {
	String type;
	//상속보단 일반적으로 구성과 위임이 더 유연하다(특히 자바는 단일 상속만 되므로)
	Movable movable;
	Weapon weapon;
	
	public void setMovable(Movable movable) {
		this.movable = movable;
	}
	public void setWeapon(Weapon weapon) {
		this.weapon = weapon;
	}
	
	public void attack() {
		weapon.attack();
	}
	
	public void move() {
		movable.move();
	}
}
public class Angel extends Character{
	
	public Angel() {
		super.type = "천사";
	}
	
	@Override
	public void attack() {
		System.out.print("신성한 ");
		super.attack();
	}
	
}
public class Devil extends Character{
	
	public Devil() {
		super.type = "악마";
	}
	
	@Override
	public void attack() {
		System.out.print("타락한 ");
		super.attack();
	}
}


public class Ex {
	public static void main(String[] args) {
		
		//인터페이스에 맞춰서 프로그래밍 한다.
		Character angel = new Angel();
		Character devil = new Devil();
		
		//전략 객체 
		//인터페이스에 맞춰서 프로그래밍 한다.
		Movable fly = new Fly();
		Movable riding = new Riding();
		Movable walk = new Walk();
		Weapon sword = new SwordWeapon();
		Weapon spear = new SpearWeapon();
		Weapon bow = new BowWeapon();
		
		//실세 런타임 환경이라면 버튼이던 이벤트로 설정자 주입으로 객체 행동을 마음것 바꿀 수 있다.
		angel.setMovable(fly);
		angel.setWeapon(sword);
		behavior(angel);
		angel.setMovable(riding);
		angel.setWeapon(spear);
		behavior(angel);
		angel.setMovable(walk);
		angel.setWeapon(bow);
		behavior(angel);
		
		//람다식을 사용하면 전략 객체를 일급함수처럼(변수처럼) 다룰 수 있다.
		devil.setMovable(()->System.out.println("뛰기!"));
		devil.setWeapon(()->System.out.println("침뱉기!"));
		behavior(devil);
	}
	
	static void behavior(Character character) {
		character.move();
		character.attack();
	}
	
}
날아갑니다...
신성한 베기!
말타고 갑니다...
신성한 찌르기!
걸어가기...
신성한 화살 쏘기!
뛰기!
타락한 침뱉기!

mark4.zip
0.00MB

 

 

패턴과 전문 용어

서로 알고 있는 패턴은 막강하다. 불필요한 설명이 필요없이 짧은 대화로 모든 의사소통된다.

 

예시, 우리는 여기 부분을 전략 패턴으로 설계했습니다. 

속뜻, 자주 변경되는 부분을 캡슐화하여 쉽게 확장하고 런타임 환경에서 객체 행동을 자유롭게 변경할 수 있게 설계했다.

 

 

 

 

핵심 정리

패턴의 밑바탕에는 객체지향 기초(추상화, 캡슐화, 다형성, 상속)가 있다.

프로그램 생애주기를 보면 개발보다 유지보수가 훨씬 더 많은 비중을 차지한다. 따라서 재사용성, 확장성, 관리 용이성을 갖추는 것이 중요하다.

패턴은 검증받은 객체지향 경험이다.

대부분 패턴과 원칙은 변경에 관련된 것이다.

변경 가능성 있는 부분은 캡슐화해야 한다.

 

 

 

 

 

 

 

 

 

협력 속에 사는 객체

역할, 책임, 협력

실제로 협력에 참여하는 주체는 객체.

실행 중 인 어플리케이션을 보면 객체들간에 메시지를 주고받으면서 협력하는 것을 있다. 이렇듯 객체가 없으면 객체지향 세계 역시 아무런 의미가 없다. 객체지향이라 부르는 이유는 패러다임의 중심에 객체가 있기 때문이다.

 

객체는 애플리케이션의 기능을 구현하기 위해 존재한다. 작은 기능조차 객체 혼자 감당하기에는 버거울 정도로 복잡 거대하다. 때문에 객체는 다른 객체와의 협력을 통해 기능을 구현하게 된다.

따라서 협력이 얼마나 조화를 이루는지 결정하는 것은 객체다.

결국 협력의 품질을 결정하는 것은 객체의 품질이다.

 

협력 공동체의 일원으로 객체는 가지 덕문을 갖춰야 한다.

첫째, 객체는 충분히 협력적이어야 한다.

모든 것을 스스로 처리하려고 하는 전지전능한 객체는 내부적인 복잡도에 의해 자멸하고 만다

 

둘째, 객체가 충분히 자율적이어야 한다.

자기 스스로의 원칙에 따라 어떤 일을 하거나 자기 스스로를 통제하여 절제하는 것을 의미한다.

객체는 다른 객체에 명력에 복종하는 것이 아니라 요청에 응답할 뿐이다. 어떤방식으로 응답할지는 객체 스스로 판단하고 결정한다.

심지어 요청에 응할지 여부도 객체 스스로 결정할 있다.

 

객체 공동체에 속한 객체들은 공동의 목표를 달성하기 위해 협력에 참여하지만 스스로의 결정과 판단에 따라 행동하는 자율적인 존재.

 

 

상태와 행동을 함께 지닌 자율적인 객체

객체를 상태(state) 행동(behavior) 함께 지닌 실체라고 정의한다.

이 말은 객체가 협력에 참여하기 위해 어떤 행동을 한다면 행동을 하는 필요한 상태도 함께 지니고 있어야 한다는 것을 의미한다.

 

객체가 협력에 참여하는 과정 속에서 스스로 판단하고 스스로 결정하는 자율적인 존재로 남기 위해서는 필요한 행동과 상태를 함께 지니고 있어야 한다.

 

객체의 자율성은 객체의 내부와 외부를 명확하게 구분하는 것으로부터 나온다.

객체의 사적인 부분은 객체 스스로 관리하고 외부에서 일체 간섭할 없도록 차단해야 하며, 객체의 외부에서는 접근이 허락된 수단을 통해서만 객체와 의사소통해야 한다.

객체는 다른 객체가 무엇(What) 수행하는지는 있지만 어떻게(how) 수행하는지에 대해서는 없다.

 

커피를 주문하는 협력 과정에 참여한 손님과 캐시어, 바리스타는 외부의 간섭을 받지 않고 스스로 생각하고 스스로 판단하는 자율적인 존재였다.

객체의 관점에서 자율성이란 자신의 상태를 직접 관리하고 상태를 기반으로 스스로 판단하고 행동할 있음을 의미한다.

객체는 행동을 위해 필요한 상태를 포함하는 동시에(바리스타는 커피 제조 방법을 기억하고 있다) 특정한 행동을 수행하는 방법을 스스로 결정할 있어야 한다.(바리스타는 자신이 알고 있는 방법에 따라 커피를 제조한다)

따라서 객체는 상태와 행위를 하나의 단위로 묶은 자율적인 존재다.

 

과거 전통적인 개발 방법은 데이터와 프로세스를 엄격하게 구분한다

이에 반해 객체지향에서는 데이터와 프로세스를 객체라는 하나의 안에 함께 묶어 놓음으로써 객체의 자율성을 보장한다.

자율적인 객체로 구성된 공동체는 유지보수가 쉽고 재사용이 용이한 시스템을 구축할 있는 가능성을 제시한다.

 

 

 

협력과 메시지

커피를 주문하기 위해 협력하는 사람들은 자신의 책임을 다하기 위해 다른 사람들에게 도움을 요청한다.

다양한 요청 방법이 있을 것이다.

 

객체지향의 세계에서는 오직 가지 의사소통 수단만이 존재한다. 이를 메시지라고 한다.

객체가 다른 객체에게 요청하는 것을 메시지를 전송한다고 다른 객체로부터 요청을 받는 것을 메시지를 수신한다고 말한다.

결과적으로 객체는 협력을 위해 다른 객체에게 메시지를 전송하고 다른 객체로부터 메시지를 수신한다.

 

 

메서드와 자율성

객체는 다른 객체와 협력하기 위해 메시지를 전송한다.

수신자는 먼저 수신된 메시지를 이해할 있는지 여부를 판단한 미리 정해진 자신만의 방법에 따라 메시지를 처리한다. 이처럼 체가 수신된 메시지를 처리하는 방법을 메서드라고 부른다.

 

객체지향 프로그래밍 언어에서 메서드는 클래스 안에 포함된 함수 또는 프로시저를 통해 구현된다.

다른 프로그래밍 언어와 객체지향 프로그래밍 언어를 구분 짓는 핵심적인 특징 하나다.

 

메시지(요청) 메서드의 분리는 객체의 협력에 참여하는 객체들 간의 자율성을 증진시킨다.

커피를 주문하는 협력과정에서 커피 제조를 요청받은 바리스타는 커피머신, 수제 커피 등등 제조 방법을 자율적으로 선택할 있다.

 

객체지향에 대입하면 커피 주문이 요청, 커피 제작하는 방법은방법은 메서드다

커피 제조를 요청한 사람은 어떻게 커피를 제조하는지 구체적인 방법을 모른다.(자율성 보장)

 

외부의 요청이 무엇인지 표현하는 메시지와 요청을 처리하기 위한 구체적인 방법인 메서드를 분리하는 것은 객체의 자율성을 높이는 핵심 메커니즘이다. 이것은 캡슐화라는 개념과도 깊이 관련돼 있다.

 

 

 

 

객체지향의 본질

객체지향이란 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고 객체를 이용해 시스템을 분할하는 방법

자율적인 객체란 상태와 행위를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미한다.

객체는 시스템의 행위를 구현하기 위해 다른 객체와 협력한다. 각 객체는 협력 내에서 정해진 역할을 수행하며 역할은 관련된 책임의 집합이다.

객체는 다른 객체와 협력하기 위해 메시지를 전송하고, 메시지를 수신한 객체는 메시지를 처리하는 데 적합한 메서드를 자율적으로 선택한다.

 

 

객체를 지향하라

클래스가 객체지향 프로그래밍 언어의 관점에서 매우 중요한 구성요소인 것은 분명하다

하지만 객체지향의 핵심을 이루는 중심 개념이라고 말하긴 무리가 있다.

자바스크립트 같은 프로토타입 기반의 객체지향 언어에서는 클래스가 존재하지 않으며 오직 객체만이 존재한다. 프로토타입 기반의 객체지향객체지향 언어에서는 상속 역시 클래스가 아닌 객체 간의 위임 메커니즘을 기반으로 한다.

지나치게 클래스를 강조하는 프로그래밍 언어적인 관점은 객체의 캡슐화를 저해하고 클래스를 서로 강하게 결합시킨다.

 

객체지향 설계의 관점을 코드를 담는 클래스의 관점에서 메시지를 주고받는 객체의 관점으로 변환해야 한다.

중요한 것은 어떤 클래스가 필요한가 가 아니라 어떤 객체들이 어떤 메시지를 주고받으며 협력하는 가다. 클래스는 객체들의 협력 관계를 코드로 옮기는 도구에 불과하다.

 

클래스의 구조와 메서드가 아니라 객체의 역할, 책임, 협력에 집중하라. 객체지향은 객체를 지향하는 것이지 클래스를 지향하는 것이 아니다.

 

지금까지 깨끗한

깨끗한 코드 블록

올바른 함수

이제 높은 차원인 깨끗한 클래스에 대해 다룬다

클래스 체계

관례상 변수 목록이

변수들 중에서도 static public final 이 맨위

그다음 static private

그다음 private

 

그다음 공개 메서드가 나온다

비공개 함수는 자신을 호출하는 공개 함수 직후에 위치

 

따라서 추상화 단계 순서대로 내려간다.

캡슐화

변수와 유틸리티 함수는 가능한 비공개로 두는 것이 좋다.

때로는 protected 선언해 테스트 코드에 접근을 허용하게 한다.

테스트 코드가 함수를 호출하거나 변수를 사용해야 한다면 함수나 변수는 protected public으로 공개한다.

하지만 그 전에 비공개 상태를 유지할 온갖 방법을 강구한다.

캡슐화 풀어주는 결정은 최후의 수단이다

 

클래스는 작아야 한다.

클래스를 만들 번째 규칙은 크기다. 번째도 크기다.

작아야 한다. 작아야 한다.

 

함수와 마찬가지로 작게가 기본규칙이다.

 

얼마나 작게?

함수는 물리적 수로 크기를 측정한다면 클래스는 맡은 책임으로 크기 수준 가늠한다.

맡은 책임은 클래스에 속한 "공개 메서드 수"라고 생각하기 쉽다. 틀린 말은 아니지만 완전히 맞는 말도 아니다.

적은 메서드 수라도 책임이 클 수 있기 때문이다.

 

클래스 작명은 크기를 줄이는 첫 번 단계이다.

만약 작명이 어렵거나 너무 길어진다면 해당 클래스 책임이 너무 많은 것이다.

마찬가지로 클래스 이름이 모호하다면 역시 클래스 책임이 많은 것이다.

Processor, Manager, Super 등과 같은 모호단 단어가 붙는다면 여러 책임을 떠안았다는 표시다.

클래스 설명은 if, and, or, but을사용하지 않고서 25 단어내외로 가능해야한다.

단어들이 붙었다는 것은 여러 책임이 존재한다는 증거이다.

 

단일 책임 원칙 (Single Responsibility Principle : SRP)

클래스나 모듈을 변경할 이유가 하나, 하나뿐이어야 한다는 원칙

SRP 책임이라는 개념을 정의하며 적절한 클래스 크기를 제시한다. 

클래스는 책임, 변경할 이유가 하나여야 한다는 의미다.

책임, 변경할 이유를 파악하려 애쓰다 보면 코드를 추상화하기도 쉬워진다.

 

SRP 객체 지향설계에서 더욱 중요한 개념이다. 또한 이해하고 지키기 수월한 개념이다

그러나 개발자가 가장 무시하는 규칙 하나다.

우리는 수많은 책임을 떠안은 클래스를 꾸준하게 접한다.

 

소프트웨어를 돌아가게 하는 것과 깨끗하게 만드는 것은 완전 별개다

깨끗하고 체계적인 소프트웨어에 초점은 두는게 아니라 프로그램이 돌아가게 하는 것이 나쁜 자세는 아니다.

문제는 거기서 끝이라고 생각하는데 있다. 깨끗하고 체계적인 소프트웨어라는 다음 관심사로 전환하지 않는다.

우리는 반드시 프로그램으로 되돌아가 여러 책임(기능) 가진 클래스를 단일 책임 클래스로 나누어야한다.

 

많은 개발자들이 자잘한 단일 책임 클래스가 많아지면 그림을 이해하기 어려워진다고 우려한다.

그림을 이해하려면 클래스 클래스를 넘나들어야 한다고.

하지만 실제로 작은 클래스가 많은 시스템이던 클래스가 개뿐인 시스템이든 돌아가는 부품은 수가 비슷하다.

그러므로 우리가 고민할 것은 다음과 같다.

도구 상자를 어떻게 관리하고 싶은가?

    작은 서럽을 많이 두고 기능과 이름이 명확하게!!

    아니면 서랍 개를 두고 모두 집어 던져버리기

 

프로젝트는 시스템 논리가 많고 복잡하다. 이런 복잡성을 다루려면 체계적인 정리를 통해 제어하는 것이 중요하다.

그래야 무엇이 어디에 위치한지 알 있다.

또한 변경을 직접 영향이 미치는 컴포넌트만 이해해도 충분하다

큼직한 다목적 클래스로 이뤄진 시스템은 변경을 가할 당장 알필요가 없는 사실까지 들이밀어 방해한다

 

강조한다. 클래스 보다 작은 클래스 여럿으로 이뤄진 시스템이 바람직하다

작은 클래스는 각자 맡은 책임이 하나며, 변경할 이유가 하나며, 다른 작은 클래스와 협력해 시스템에 필요한 동작을 수행한다.

 

응집도(Cohesion)

클래스는 인스턴스 변수 수가 작아야 한다.

클래스 메서드는 클래스 인스턴스 변수 하나 이상 사용해야 한다.

일반적으로 메서드가 변수를 많이 사용할 수록 메서드와 클래스는 응집도가 높다.

특히 모든 인스턴스 변수를 메서드마다 사용하는 클래스는 응집도가 가장 높다.

 

위처럼 응집도가 가장 높은 클래스는 가능하지도 바람직하지도 않다.

우리는 응집도 높든 클래스를 선호한다.

응집도가 높다는 것은 클래스에 속한 메서드와 변수가 서로 의존하며 논리적인 단위로 묶인다

 

'함수를 작게, 매개변수 목록을 짧게' 라는 전략을 따르다 보면 때때로 몇몇 메서드만이 사용하는 인스턴스 변수가 아주 많아진다.

우리는 이제 알고 있다. 이것이 새로운 클래스로 쪼개야 한다는 신호라는 것을

응집도가 높은 클래스를 유지하도록 서로 응집력이 높은 메서드와 변수들을 그룹지어 여러 클래스로 분리해준다.

 

응집도를 유지하면 작은 클래스 여럿이 나온다

함수를 작은 함수 여럿으로 나누기만 해도 클래스 수가 많아진다.

예를 들어, 변수가 많은 함수 하나에서 일부를 작은 함수 하나로 빼내고 싶다.

빼내려는 코드가 함수에 변수 4개를 사용한다. 그러면 작은 함수 인자에 변수 4개를넣어야 하나? 아니다. 이럴 함수에 로컬 변수를 클래스 인스턴스 변수로 만들면 함수는 인수가 필요 없다. 그만큼 함수를 쪼개기 쉬워진다.

다만 이과정에서 불행이도 응집력을 잃는다. 몇몇 함수만 이용하는 인스턴스 변수가 점점 늘어나기 때문이다.

, 이런 상황에서 클래스가 응집력을 잃는다면 새 클래스를 만들어 분리하는 것이다.

그래서 함수를 작은 함수 여럿으로 쪼개다 보면 종종 작은 클래스 여럿으로 쪼갤 기회가 생긴다.

 

리팩터링 과정

  • 원래 프로그램의 정확한 동작을 검증하는 테스트 슈트를 작성
  • 그다음 한 번에 하나씩 수 차례 걸쳐 조금씩 코드를 변경
  • 코드 변경마다 테스트를 수행해 원래 프로그램과 동일하게 동작하는지 확인
  • 이 과정을 반복 정리한 결과로 최종 프로그램 산출

변경하기 쉬운 클래스

대다수 시스템은 지속적인 변경이 가해진다. 과정에서 의도적으로 동작하지 않을 위험이 생긴다. 깨끗한 시스템은 클래스를 체계적으로 정리해 변경에 수반되는 위험을 낮춘다.

 

어떤 변경이던 클래스에 손대면 다른 코드를 망가뜨릴 잠정적인 위험이 있다.

이는 SRP를 위반하는 것이다.

이럴 땐 공통되는 기능을 추상화해 상위 클래스로 만들어 이를 상속하는 작은 클래스로 쪼갠다. 

작은 클래스들은 각기 다른 작은 책임을 수행할 메서드를 구현할 것이다.

추후 변경(수정, 추가)이 발생해도 그 작은 클래스에 국한된다. 

 

위 과정은 객체 지향 설계에서 다른 핵심 원칙인 OCP(Open-Closed Principle)을 지키게 된다.

OCP 클래스는 확장에 개방적이고 수정에 폐쇄적이어야 한다는 원칙이다.

파생 클래스를 생성하는 방식으로 기능에 개방적인 동시에 다른 클래스를 닫아놓는 방식으로 수정에 폐쇄적

 

새 기능을 수정하거나기존 기능을 변경할 건드릴 코드가 최소인 시스템 구조가 바람직하다. 이상적인 시스템이라면 기능을 추가할 시스템을 확장할 기존 코드를 변경하지는 않는다.

 

변경으로부터 격리

요구사항은 변하기 마련이다. 따라서 코드도 변한다.

객체지향 프로그래밍에서 클래스는 구체적인(concreate) 클래스와 추상(abstract) 클래스가 있다

구체적인 클래스는 구현부가 존재하며 추상 클래스는 선언부만 존재한다.

상세한 구현에 의존하는 클라이언트 클래스는 구현이 바뀌면 위험에 빠진다. 따라서 우리는 인터페이스와 추상 클래스를 사용해 구현에 미치는 영향을 격리한다.

 

상세한 구현에 의존하는 코드는 테스트가 어렵다.

 

 시스템의 결합도를 낮추면 유연성과 재사용성도 더욱 높아진다. 결합도가 낮다는 소리는 시스템 요소가 다른 요소로부터 그리고 변경으로부터 격리되어 있다는 의미다. 시스템 요소가 서로 격리되어 있다면 요소를 이해하기도 쉽다.

결합도를 최소로 낮추면 자연스럽게 DIP(Dependency Inversion Principle) 따르는 클래스가 나온다.

DIP 본질은 클래스가 상세한 구현이 아니라 추상화에 의존해야 한다는 원칙이다.

 

'IT책, 강의 > 클린코드(Clean Code)' 카테고리의 다른 글

12장 창발성  (0) 2022.11.05
11장 시스템  (0) 2022.11.01
9장 단위 테스트  (0) 2022.10.21
8장 경계  (0) 2022.10.18
7장 오류 처리  (0) 2022.10.12

+ Recent posts