프로그램을 실행하면 JVM은 OS에 알맞도록 CPU에 프로세스를 할당해준다고 이야기하였다

그렇다면 어떤 방식으로 이 작업이 이뤄지는것일까?

CPU는 주소값을 통하여 메모리를 접근하는데 메모리공간을 효율적으로 사용하기위해 적용되는 개념인 바인딩에 관하여 설명하겠다.

 

주소공간

하단의 사진은 주소값이 어떤식으로 배열되어있는지 나타낸 사진으로 메모리의 주소공간은 0번지부터 시작하며 0번지 주소의 대응하는 명령어는 0100 1111임을 확인할수 있다 또한 메모리의 단위는 byte로 이뤄진다.

레지스터는 CPU가 계산할 데이터는 램에 쌓여있는데 이것들은 CPU 안에서 연산되어야하기에 CPU 내부에 임시로 값을 저장할 공간이 필요하고 이 공간을 뜻한다.

레지스터 크기에 따라 32비트, 64비트로 구분된다. (윈도우의 32(x84),64비트는 레지스터의 크기가 2^32 또는 2^64임을 의미한다.)

주소 바인딩 (메모리 할당과정)

링커 (Linker) - 컴파일러가 원시코드를 파일로 생성하면 이 파일에 라이브러리와 다른 파일들을 결합 (exe 같은 파일을 생성시킴

로더 (Loader) - 사용자 프로그램을 메모리에 적재시키는 프로그램 (지정 위치에서 시작해 메모리에 프로그램을 배치 -> 로딩을 해주는 역할)

 

프로그램이 실행을 위해 메모리에 적재되면(Symbolic Address) 그 프로세스를 위한 주소공간이 생성된다. 

이것을 논리적 주소(logical address) 또는 가상 주소(vritual addres)라고 칭한다.

논리적 주소는 각 프로세스마다 독립적으로 0번지부터 시작되며(가상주소 이므로 "여기서부터 0이다" 라고 선언하면 거기가 0이 된다고 이해하면 된다) CPU는 이 논리적 주소를 바탕으로 명령을 실행한다.

그리고 이 논리적 주소(가상주소)를 새로운 물리적 주소(실제 하드웨어)로 주소를 할당시키는것(Mapping)을 주소바인딩이라 표현한다. (가상에 임의로 부여했던 주소를 실제 메모리에 할당하며 이 작업은 Mapping은 MMU라는 메모리 관리장치가 담당한다.)

요약하자면 CPU가 기계어 명령을 수행하기 위해선

1. 프로그램이 물리적 메모리에 올라가야한다. (가상이 아닌 실제 메모리상의 주소값을 가져야한다)

2. CPU가 기계어 명령을 수행하기 위해선 논리적 주소를 통해 메모리를 참고하며 논리적 주소가 물리적 메모리에 매핑되어야한다.

 

물리적 주소 (Physical Memory Address) -> 실제 메모리

실제 하드웨어에 올라가는 주소로 물리적 메모리의 낮은주소(메모리가 낮다 -> 접근이 더 빠르며 이것이 우선 실행된다) 크기가 굉장히 큰 배열을 갖는다 생각하면 되며 각 배열을 구분하는 인덱스 값을 갖는데 이 값을 물리적 주소라고 이야기한다. (메모리 자체의 인덱스 (0x0001, 0x0002)를 의미)

모든 프로세스는 논리적 주소가 0번지부터 시작해서 차례대로 증가하기에 물리적 주소도 시작주소만 다르지 연속적으로 배치되어있다. (OS는 메모리를 최대한 활용하기 위해서 0부터(낮고 가져오기 쉬운 순서부터) 시작하고 이를 연속적으로 배치한다)

 

논리적 주소 (Logical Memory Address) -> 가상 메모리

동일한 공간을 공유할때 서로 침범하지 않기 위해선 사람들이 자신의 공간을 벽으로 나눴듯이 컴퓨터에서도 자신의 공간을 표시한것이 논리적 주소라고 생각하면 된다. (이것은 Base Register, Limit Register를 활용해 물리적 주소를 정한다)

논리적 주소는 CPU 입장에서의 메모리 주소로 프로그램이 실행중에 CPU가 생성하는 주소이다. (CPU가 실행시 실제 메모리가 아닌 가상에 할당하기에 가상주소라고 이야기 하기도 한다.)

프로그램이 1MB라면 0x00000 ~ 0xfffff (2^20 -> 1M)이다.

 

모든 프로세스는 논리적 주소가 0번지부터 시작해서 차례대로 증가하기에 물리적 주소도 시작주소만 다르지 연속적으로 배치되어있다.

OS는 메모리를 최대한 활용하기 위해서 0부터(낮고 가져오기 쉬운 순서 -> 주로 기본적인 프로그램들) 배치를 시작하고 이를 연속적으로 배치한다.

 

 

Symbolic Address 

프로그래머들이 특정 이름을 통해 변수를 지정하고 값을 저장할때 이 변수를 하드웨어 어디에 저장할지 정하지 않는데 이것을 변수의 이름을 통하여 그 값에 접근하는것이며 이 변수를 Symbolic Address라고 칭한다

이 주소가 컴파일되어 숫자 주소가 만들어지고 이것이 물리적인 메모리와 매핑되는것이다. -> int a = 5; 에서 a의 주소

(여담으로 심볼릭 링크라는것도 있는데 이는 윈도우의 바로가기 아이콘이라고 생각하면 된다)

 

 

 


주소를 확정하는 방식

컴퓨터에선 이를 Base Register에 Limit Register 를 더하여 주소를 확정한다.

또한 한 프로세스의 모든 논리적 주소에 동일한 Base Register를 더하는 주소할당을 "연속할당"이라고 표현한다.-> 연속적으로 나열된것 0x002가 끝인경우 다음 프로세스의 주소는 0x003이 되는것 (고정분할, 가변분할 2가지 방식이 있다)

불연속 할당

 

Base Register - 메모리(RAM)에 프로그램이 할당될때(논리적 주소) 프로그램의 시작주소를 의미한다. (재배치 레지스터라고 표현하기도 한다)

Limit Register - Base Register에서 현재 프로그램이 사용할수 있는 레지스터(논리적 주소)의 끝을 의미하며 CPU가 논리적 주소를 요청할 때 마다 한계 레지스터 값보다 작은 값인지를 검사 (논리적 주소에서 끝주소를 의미한다)

(Relocation Register와 Base Register는 동일한 것이라 생각하면 된다.)

Relocation Register - 접근할수 있는 물리적 메모리 주소의 최소(시작)값

 

조금 더 자세히 설명하면 상단 2개의 레지스터는 각 프로그램이 고유값으로 A프로세스의 논리적 주소는 Base Register + Limit Register로 Base Register가 0.00005이며 Limit Register가 10이면 A프로세스의 논리적 주소는 0.00005~0.00015까지 사용된다고 생각하면 된다.

만일 Limit Register가 10인데 주소를 0.00016까지 사용할경우 CPU는 trap(software interrupt)을 발생하고 프로그램을 에러를 뿜어내며 강제종료된다,

 

여기서 프로세스가 물리적 주소에 접근하려면 논리적 주소를 Mapping하는 과정이 필요한데 이것은 MMU(Memory Management Unit)가 이 역할을 수행한다.

주소 매핑 과정은 MMU가 하드웨어적으로 구현되어있으며 MMU는 논리적 주소에 Base Register값을 더해주면 된다.

 

프로세스(Process) - 컴퓨터 내에서 프로그램을 수행하는 컴퓨터에서 실행하는 하드웨어 유닛으로 1개 이상의 ALU 및 처리 레지스터 내장 (특정 목적을 수행하기 위해 메모리에 적재되어 프로세서에 의해 실행중인 프로그램(작업목록))

프로세서(Processor) -> 명령어를 해석하는 컴퓨터의 한부분으로 데이터 포맷을 변환하는 역할을 수행하는 프로세싱 시스템 (데이터 처리 시스템)을 의미하며 인쇄물을 출력하는 워드프로세서도 프로세서라 불리운다. (CPU, 어셈블러 등)

MMU기법 (MMU scheme)

MMU는 CPU가 논리적 주소를 참조하고 싶을때 그 주소값에 기준 레지스터값(base register)을 더해 물리적 주소값을 얻어낸다 (주소를 유추하고 싶을때는 MMU 논리적 주소에 base 레지스터값을 더해주면 된다)

기준레지스터(Relocation Register)는 재배치 레지스터라고 표현하기도 하며 프로세스의 물리적 메모리 시작주소(를 갖고있다.

MMU는 프로그램 주소공간이 연속적으로 적재되있음을 가정하에 진행한다.

멀티 프로세싱을 위해 가상주소를 사용한다.

 

논리적 주소를 물리적 주소로 매핑해주는 하드웨어 디바이스

CPU가 프로세스의 논리적주소와 기준 레지스터의 값을 더하여 물리적 주소 값을 얻는다 -> 이것은 곧 물리적 주소의 시작주소만 알아낸다면 주소변환이 가능하다는 의미이다.

 

 

마이크로 프로세서(microprocessor) - 기계어 코드를실행하기 위해 실행과정을 단계적으로 나누어 처리를 위한 마이크로 코드를 작성하고 이것을 단계적으로 처리하는 논리회로

 

 

 

MMU scheme - 사용자 프로세스가 CPU에서 수행되며 생성해내는 모든 주소값에 대해 Base Register의 값을 더해준다.

user program - Logical address만 다루며 실제 물리적 주소를 볼수 없다.

 

주소의 매핑은 

Relocation Register과 base Register는 같은 것으로 생각하면 된다?

(주소 매핑 과정은 빈번하게 이뤄지므로 MMU는 하드웨어적으로 구성되어있다)

 

여기서 굳이 Mapping하지말고 물리적 주소만 쓰면 속도가 더 빠르지 않냐는 궁금증이 생기는데 만일 논리적 주소를 사용하지 않고 물리적 주소 그러니까 실제 HW의 주소만 사용하면 같은 프로그램이 존재할때 한개의 프로그램은 실행되지 않는다 (크롬 여러창을 띄울수 없다고 생각하면 된다 -> 게임 클라이언트가 실행중인데 또 실행하면 이미 실행중이기에 다른 클라이언트가 안켜진다)

반면 논리적 주소는 컴파일할때 주소가 확정되는것이 아닌 실제 프로그램을 실행시킬때 메모리 빈공간에 논리적 주소(가상주소)를 만들고 물리적 주소와 같이 바인딩 하기에 물리적 주소가 같은 프로그램을 여러개 띄울수 있는것이다.

 

 

여기서 바인딩은 어느시점에서 논리적주소(가상주소)가 물리적주소(실제주소)로 언제 Mapping할것이가? 궁금증이 생길것이다.

바인딩의 방식은 물리적 메모리의 주소가 결정되는 시점에 따라 컴파일 타임 바인딩,로드 타임 바인딩, 실행시간 바인딩 3종류로 구분된다.

컴파일 타임 바인딩(Compile time binding)

프로그램을 컴파일할때 물리적 메모리 주소가 결정되는 방식 (프로그램 전체가 메모리에 올라가야한다)

즉, 처음부터 프로그램 내부에서 사용하는 주소(논리적 주소)와 물리적 메모리 주소가 동일

물리적 메모리 위치 변경을 하려면 재컴파일을 사용하며 기점유 하고있는 프로세스가 있는경우도 있다.

하나의 프로세스만 사용할때 사용하며 현재 범용적으로 사용중인 멀티 프로세싱에 부적합한 로드방법이다.

(이미 실제 메모리에 사용중인 크롬A의 주소가 있는데 크롬B를 그 사이에 호출하고 주소를 넣을순 없다 -> 우리는 앞서 메모리는 연속적으로 나열된다고 했는데 컴파일 타임 바인딩은 실행시점만 주소에 값을 넣을수 있기에 추가적인 실행은 불가능하다.)

 

로드 타임 바인딩(Load time binding)

프로그램 시작시(메모리에 로드,적재될때) 물리적 메모리 주소가 결정되며 로더의 책임하에 물리적 메모리 주소가 부여되며 프로그램이 종료될 때까지 물리적 메모리상의 위치 고정

초기에 부여되는 논리적 주소와 물리적 주소가 다르기때문에 멀티 프로세싱 환경에 용이하다 (물리적 메모리 주소와 논리적 메모리 주소를 분리하여 처음엔 가상주소로 들고있다가 프로그램 실행시 물리적 주소로 변경, 프로그램 종료전까지 물리적 메모리상의 위치가 고정)

프로그램 내부에서 사용하는 주소(논리적 주소)와 물리적 메모리 주소는 다른방식이다.

메모리를 참조하는 명령어를 다 변경해야하는 단점으로 메모리 로딩시간이 엄청 오래걸리는 단점으로 잘 사용되진 않는다.

 

실행시간 바인딩 (execution time binding 또는 run time binding)

프로그램 실행 이후로 프로그램이 위치한 물리적 메모리상의 주소의 변경이 가능한 바인딩방식 (실시간 물리적  주소의 변경을 의미하며 이것은 프로세스가 메모리에 연속적(Continuous)으로 적재된것을 가정하에 진행한다)

CPU가 주소를 참조할때마다 해당 데이터의 물리적 메모리가 어디에 있어야하는지 주소매핑테이블(address mapping table)을 통해 바인딩을 점검한다.

실행시간에 바인딩이 이뤄지기에 기준 레지스터(Base Register)한계 레지스터(Limit Register)를 포함한 MMU라는 하드웨어적인 자원이 필요하다

 

 

동적적재 (Dynamic Loading)

디스크에 프로그램을 여러 루틴(Function)으로 나눠 저장

해당 루틴이 실제 호출되기 전까지는 루틴을 메모리에 미적재, 메인 프로그램만 메모리에 적재하여 수행

루틴이 호출되면 바인딩 수행

미사용할 루틴은 메모리에 미적재하여 메모리의 효율적 사용이 가능

 

 

요약

1. 프로세스는 논리적 주소(가상주소)에 배치된다.

2. 논리적 주소를 물리적 주소(실제 하드웨어 주소)로 변환하는걸 바인딩이라 한다

3. 바인딩은 Base Register + Limit Register를 통해 주소값을 확정짓는다.

'CS' 카테고리의 다른 글

운영체제란  (0) 2023.03.30
프로세서, 프로세스  (0) 2023.03.21
제네릭(Generic)  (0) 2023.03.14
객체 지향 프로그래밍 (OOP) - 2 (5대원칙)  (0) 2023.03.02
스택트레이스(Stack Trace)  (0) 2023.02.26

제네릭이란 데이터의 타입을 일반화(generalize) 하는것을 의미하며 클래스 및 메서드에서 사용할 내부 데이터 타입을 컴파일시에 미리 지정하는것을 의미한다.

조금더 상세하게 설명하면 1개의 값이 여러 타입의 데이터를 갖도록 하는 방법이다

우리가 메뉴판 프로그램을 만든다고 하면 이를 사용하는 가게주인은 가격란에 30,000을 입력할수도, 삼만원을 입력할수도 있는고 만일 데이터 타입을 int나 String 으로 정한다면 둘 중의 1개는 무조건 버그가 나타날것이다. 

이런식이다..

이런 문제를 해결하기 위해 제네릭이 사용되며 처음 작성할때는 어떤타입이든 올수 있도록 해놨다가 값(파라미터)를 넣을때 데이터의 형식을 정해주는게 사용하는게 제네릭이다.

 

제네릭을 사용하는 이유

1. 컴파일시 강한 타입체크이 가능하다.

2. 타입변환(Casting)을 제거

이것을 하단의 코드와 함께 설명하겠다.

class Studentinfo{
	public int grade;
	Studentinfo(int rank) { this.rank = rank; }
}

class Employeeinfo{
	public int rank;
	Employeeinfo(int rank) { this.rank = rank; }
}

class Person{	//(A)
	//Objcet타입은 추상적인 타입으로 어떤 타입이든 들어갈수 있다.
    public Object info;		//int, char등 모든 타입이 들어갈수 있다
    Person(Object info) { this.info = info; }
}

public class test {	//(B)
	public static void main(String[] args) {
    	Person p1 = new Person("개발자");	//Object형으로 되어있다.
        //Object가 최상위인건 알겠는데 지금 사용하려는게 어떤타입(int?String?)이냐?...
    	Employeeinfo ei = (Employeeinfo) p1.info;
    	System.out.println(ei.rank);
    }
}

여기서 (A)를 본다면 Object로 선언되있는데 Object는 각 파라미터 타입의 최상위객체이므로 어떤 타입(int,String,float...)을 넣어도 문제가 없게 만든다. -> 이것을 "타입이 불안전하다"고 표현한다.

(컴파일 단계에선 (문법적)에러가 없으나 코드의 원래목적과 다른 결과값이 나오기때문이다.)

이것은 (B)처럼 int값이 들어갈 부분인데 String값이 들어가도 문법상 오류는 없어 컴파일러는 캐치할수 없기에 찾기 어려운 버그가 생기게 되는것이다. (치킨 가격에 "15000"이 출력되야 하는데 "국내산 닭" 이라고 출력되는 버그..)

 

이것을 제네릭방식으로 바꾼 코드를 보겠다. (제네릭 예시 코드 참조)

//제네릭 예시 
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank) { this.rank = rank; }
}
//T와S는 와일드 카드이다
//제네릭에서 복수의 파라미터를 받는경우 앞부분(T)은 참조 데이터타입만 가능
//복수의 파라미터를 받는경우 뒷부분(S)는 기본데이터 타입인 Integer, Char만 올수있다.
class Person<T, S>{	//멀티타입 파라미터
    public T info;
    public S id;
    Person(T info,s id) {
    	this.info = info;
        this.id = id;
    }
}
public class test2 {
	public static void main(String[] args) {
    	Integer id = new Integer(1);
    	Person<EmployeeInfo, Integer> p1 = new person<EmployeeInfo, Integer>(new EmployeeInfo(1), id);
    	System.out.println(p1.id.intValue());	//레퍼클래스에서 갖고있는 숫자를 int타입숫자1로 형변환
    }
}

//제네릭은 상속과 구현도 가능하다.

//부모클래스,자식클래스가 될수있다.
public class 자식클래스명<T,M> extends 부모클래스<T,M> {...}
//파라미터의 추가도 가능하다.
public class 자식클래스명<T,M,C> extends 부모클래스<T,M> {...}

 (와일드카드 - 어떤값이든 올수 있다는 의미로 여기서는 T나 S에 int, String등 어떤타입이든 올수있음을 의미한다)

 

상단의 코드를 어느정도 이해했다면 하단의 코드를 살펴보자

여기서 제네릭 타입은 타입(int,String)을 파라미터로 갖는 클래스와 인터페이스를 뜻한다.

//class<T>, interface<T> -> <>제네릭 의미, T -> 파라미터의 타입을 의미
//제네릭타입 기본적인 양식
public class test1<Sting> { ... }	//string형 제네릭 클래스
public class test2<Integer> { ... }	//int형 제네릭 클래스
public interface test3<T>{ ... }	//타입 파라미터의 이름은 T이며 제네릭 인터페이스

//CASE A
List<String> list = new ArrayList<String>();	//String만 받도록 생성
list.add("help");			//리스트에 글자삽입
String a = list.get(0);		//String으로의 타입변환 없이 출력됨

//CASE B
List list = new ArrayList();
list.add("hello");
String b = (String)list.get(0)	//String으로 Casting 필요

 

CASE A는 제네릭을 활용하여 캐스팅이 필요없는 코드이고

CASE B는 제네릭 없이 사용한 일반적인 코드이다.

 

B에 대하여 설명하자면 이전의 포스팅에서 우리는 자료형을 선언할때 아무런 조치를 하지 않는경우 JVM에서 Object를상속받는다 이야기했는데 여기서도 이개념이 동일하게 적용된다.

List를 저장할때 앞에 자료형선언이 없었기에 Object를 상속받았는데 Object는 최상위클래스지만 위에서 이야기한 Object는 타입이 불안전한 이유로 인하여 Casting을 진행한 이유다.

 

추가로 제네릭 타입은 static변수를 사용할수 없는데 이것은 클래스가 인스턴스가 되기전 static은 메모리에 먼저 올라감과 동시에 T의 타입이 정해지는데 이때 T는 타입이 결정되지 않았기때문에 에러가 나타난다. (후술할 제네릭 메서드에서 부연설명 예정)

멀티타입 파라미터

//제네릭에서 복수의 파라미터를 받는경우 앞부분(T)은 참조 데이터타입만 가능
//복수의 파라미터를 받는경우 뒷부분(S)는 기본데이터 타입인 Integer, Char만 올수있다.
class Person<T, S>{	//T,S는 임의로 붙인 이름
    public T info;
    public S id;
    Person(T info,s id) {
    	this.info = info;
        this.id = id;
    }
}
public class test2 {
	public static void main(String[] args) {
    	Integer id = new Integer(1);
    	Person<EmployeeInfo, Integer> p1 = new person<EmployeeInfo, Integer>(new EmployeeInfo(1), id);
      //자바7 이상은 타입 파라미터 부분을 유추하여 자동으로 설정해준다. (코드양이 줄었다!)
      //Person<EmployeeInfo, Integer> p1 = new person<>(new EmployeeInfo(1), id);
        System.out.println(p1.id.intValue());	//레퍼클래스에서 갖고있는 숫자를 int타입숫자1로 형변환
    }
}

해당 코드는 상단의 코드를 가져온것으로  복수의 파라미터를 사용할수 있는데 각 파라미터(T,S)를 콤마로 구분한다.

 

제네릭메서드

제네릭메서드는 매개타입, 리턴타입으로 타입(int,string)파라미터를 갖는 메서드를  의미한다.

상단의 제너릭타입은 static이 불가능했지만 제네릭 메서드는 static이 가능하다

이것은 호출시 매개타입을 지정하기때문인데 하단의 코드를 확인해보자

//public <타임파라미터,..> 리턴타입 메서드명(매개변수, ...) { ... }
//public      <T> 			Box<T>  boxing      (T t)	   { ... }
public <T> Box<T> boxing(T t) { ... }

//제네릭 메서드 호출방식 1
//리턴타입 변수 = <구체적타입> 메서드명 (매개값);
Box<Integer> box = <Integer>boxing(100);	//타입 파라미터를 명시적으로 Integer로 지정

//제네릭 메서드 호출방식 2
//리턴타입 변수 = 메서드명 (매개값);
Box<Integer> box = boxing(100);	//타입 파라미터를 Integer로 지정

 

 상단에서 제네릭 타입은 static이 사용 불가능하다고 이야기했지만 제네릭 메서드는 static의 사용이 가능하다

그 이유는 static이 갖는 특징과 타입이 결정되는 시점을 생각하면 된다.

제네릭 타입은 인스턴스가 생성될때 타입이 결정된다 ( 인스턴스 생성 예시로 "cat = new Animal()" 에서 Animal에 들어가는 데이터 타입(int,String)에 따라 제네릭타입이 갖는 타입(int,String)이 정해진다.)

그에 반해 static은 인스턴스 생성과 별도로 이미 메모리에 올라가있다. (어떤타입인진 몰라도 Object형으로 이미 올라가있어 제네릭타입을 사용이 불가능한것이다.)

즉, static으로 선언된 메서드는 인스턴스가 생성되는 시점에 결정되는 제네릭 타입을 매개변수로 받을수 없는데 반해

제네릭 메서드는 생성과 별도로 메모리에 올라가있는데 제네릭으로 시그니처된 메서드는 호출된 시점에 타입이 결정되기 때문에 에러가 발생하지 않는다 (메서드를 호출시 여기에 들어갈 값(파라미터)가 정해지거나 값이 넣어진 상태로 호출되기 때문에 static을 사용해도 문제없다)

 

제한된 타입 파라미터 (T extends 최상위타입)

public <T extends 상위타입> 리턴타입 메서드(매개변수, ...) { ... }

타입파라미터에서 구체적인 타입을 제한할 필요가 종종 있다.(숫자만 들어가는경우 int,float의 상위타입인 Number타입만 가능하도록 지정 (EX : 가격표)) 

이것을 제한된 타입 파라미터(bounded type parameter)라고 표현한다.

//제한된 타입 파라미터 예시
public <T extends 상위타입> 리턴타입 메서드명(매개변수,...) { ... }

//사용예시 (A)
public class Util {
	public <T extends Number> int compare(T t1, T t2) {
		double v1 = t1.doubleValue();	//Number의 하위타입은 double로 변형
		double v2 = t2.doubleValue();	
		return Double.campare(v1,v2);
            //compare() => 좌우가 동일할경우 = 0, 좌측크면 1, 우측이 크면 -1
	}
}

//사용예시
public class BoundedTypeParameterExample {
	public static void main(String[] args) {
    //타입 파라미터로 배열을 생성하려면 "(T[]) (new Object[n])"형태로 생성한다.
    	//String str = Util.compare("a","b");
        //"a","b"는 Number의 하위타입이 아니기에 에러가 출력된다
    	
        int result1 = Util.compare(10,20);	//20은 int형에서 Integer 형으로 Boxing된다.
        System.out.println(result1);		//-1 출력
        
       int result2 = Util.compare(4.5,3);	//4.5는 double형에서 Double 형으로 Boxing된다.
       System.out.println(result2);			//1 출력
    }
}

(int형과 Integer의 차이점)

int는 변수의 타입(data type)을 의미한다. (변수는 값을 저장할수 있는 메모리상의 공간을 의미하며 4바이트를 의미한다.)

Integer는 wrapper class를 의미한다.(매개변수를 객체로 필요할때, 기본형값(int)이 아닌 객체로 저장해야할때, 객체간 비교가 필요할때 사용된다.) 

int to Integer -> boxing한다는 표현을 사용한다.

boxing - 기본타입 데이터를 래퍼클래스 인스턴스로 변환하는것

int - 산술연산이 가능하며 null로 초기화가 불가능하다.

Integer - Unboxing하지 않는경우 산술연산이 불가능하며 null값이 가능하다.

래퍼클래스 (wrapper class)는 기본타입 데이터를 객체로 다루기 위하여 사용하는 클래스

 

와일드카드 타입

코드에서 일반적으로 제네릭 타입을 

(와일드카드 - "?"로 어떤값이든 올수 있다는 뜻이다.)

제네릭<?> - 제한없음(모든 클래스, 인터페이스가 올수있다)
제네릭타입<? extends 상위타입> - 상위클래스 제한 (상위타입, 하위타입만 올수있다)
제네릭타입<? super 하위타입> - 하위클래스 제한 (하위타입이나 상위타입만 올수있다)
//상위타입 <-> 하위타입 가능한건 OOP의 특징에서 다형성 참조

 

마지막으로 범용적으로 쓰이는 제네릭 타입변수는 하단과 같은 방식으로 쓰인다고 한다

타입 설명
<T> Type
<E> Element
<K> Key
<V> Value
<N> Number

 

참고사이트 : https://vvshinevv.tistory.com/55

 

'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
객체 지향 프로그래밍 (OOP) - 2 (5대원칙)  (0) 2023.03.02
스택트레이스(Stack Trace)  (0) 2023.02.26
객체 지향 프로그래밍 (OOP) - 1  (0) 2022.12.25

이전 포스팅에서 객체지향이 추구하는 4가지 특징에 관하여 설명하였다.

이번에는 4가지 특징에 동반되는 5가지 원칙에 대하여 설명하겠다.

OOP의 5대원칙은 각각의 앞글자를 따서(SOLID)라고 불리며 여러 디자인패턴은 5가지 원칙을 준수하며 만들어졌단걸 기억하면 이해하기 더욱 쉬울것이다.

 

SRP(Single Responsibility Principle): 단일 책임 원칙 - 객체는 1개의 책임만 가져야한다

OCP(Open Closed Priciple): 개방 폐쇄 원칙 - 확장에 열려있어야하며 수정엔 닫혀있어야 한다.

LSP(Listov Substitution Priciple): 리스코프 치환 원칙 - 서브타입은 언제나 부모타입으로 교체할수 있어야한다.

ISP(Interface Segregation Principle): 인터페이스 분리 원칙 - 적재적소에 맞도록 인터페이스를 설계해야한다.

DIP(Dependency Inversion Principle): 의존 역전 원칙 - (구현된)클래스를 참조하는게 아닌 그 상위요소로 참조하라


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

책임은 기능이라고 이해하면 쉽다.

1개의 클래스는 1개의 책임(기능)만 갖고 있어야 한다는 원칙이다.

그렇다면 어째서 이런 원칙을 지켜야하는가?

 

유지보수 VS 성능

개발자 입장에서만 생각한다면 코드 전체를 여러 클래스가 아닌 한 클래스에 전부 몰아넣어서 확인한다면 외부참조 등 여러가지 문제에서 해방될수 있을것이다 (성능을 우선시한 결과이며 코드의 가독성, 유지보수등의 문제는 논외로 치자..)

하지만 단순한 계산프로그램이 아닌 100개의 기능을 구현하였고 이것이 얽히고 섥혀 난잡하게된 상황에서 1개의 기능을 수정한 경우는 큰 에러가 발생할것이다.

 

한가지 예시를 들어보겠다. 

상단의 사진은 한 게임의 대미지 계산공식으로 해당 공식에선 여러 변수들이 계산되는걸 볼수있는데(캐릭터 스텟, 몬스터 방어력으로 2개이상 참조) 만일 일정시간동안 입는 대미지별로 몬스터의 방어력이 달라지는 기능을 넣는다고 가정해보자.(기능변경으로 단일책임 원칙 위배) -> 레이드시 보스의 순삭을 막기위한 기능이다

 

이 기능이 정상적으로 작동이 된다면 문제가 없겠지만 에러가 발생하는경우 어디서 에러가 발생하였는지 찾을때 굉장히 어려움이 발생할것이고 이러한 기능을 방지하고자 만들어진게 SRP이다. (유지보수 우선)

 

또한 이전의 카카오 화재사태처럼 코드가 소실된경우 이러한 에러발생시 이와 관련된 에러를찾을때 굉장한 시간이 걸리는데 SRP를 지켜가며 설계한경우 문제되는 부분만 캐치하여 코드만 기입하면 되기에 DR(Disaster Recovery) 에 대응하는 시간또한 줄어든다고 볼수있다.

 

(SRP는 모듈화를 최대한 지향한다고 생각하면 된다.)

(모듈화 - SW설계에서 기능단위로 분해, 추상화하여 재사용 및 공유가능한 수준으로 만들어진 단위로 모듈화가 강해질수록 다른 객체와의 의존,연관성이 줄어든다.)

 

장점

클래스의 책임영역이 확실하다 -> 책임 변경(기능수정,추가 등)에 따른 연쇄변경에서 자유롭다 (한개만 바꿔도 다른곳 신경 안써도 된다)

응집도(cohesion)강화, 결합도(coupling)약화, 가독성 향상, 유지보수 용이

(응집도 -  모듈 내부의 기능적인 집중정도 // 결합도 - 모듈 상호간 의존하는 정도)

 

요점정리

1. 클래스는 1개의 책임(1개의 기능)만 갖는다

2. 클래스는 캡슐화를 하여야한다.

3. 클래스를 변경하려는 이유는 단 하나(필요할때마다 구현) 해야한다.

4. 유지보수 VS 성능에서 유지보수가 이긴결과이다.


 

OCP(Open Closed Priciple) - 개방 폐쇄 원칙

SW개발작업중 1개의 모듈에 수정을 가할경우 그 모듈을 이용하는 다른 모듈을 전부 고쳐야한다면 수정하기에 굉장히 어려운 작업일것이다.

이러한 불편함을 해소하고자 세운 원칙이 OCP이다.

확장에 열려있어야하며 수정엔 닫혀있어야 한다. (추상화라고 생각하고 이해하면 쉽다)

 

먼저 확장과 변경에 대한 설명부터 시작하겠다.

확장 -> 모듈의 확장성(파생)을 보장하는것 (새로운 변경사항 발생시 코드를 기능을 추가할수 있는것 -> 추상메서드 구현)

변경 -> 객체를 직접적으로 수정하는건 제한해야한다(새로운 변경사항이 발생했을때 객체를 직접적으로 수정하지 않게 해야한다 -> 추상메서드를 수정하지말고 구현메서드를 수정해라)

 

위의 요약을 해석하자면 원래코드(추상메서드)를 변경없이 기존 코드에 새로운 코드를 추가함 또는 상속받음으로써 기능의 추가 및 변경이 가능하다. (추상메서드의 구현)

즉, 다형성을 활용하여 생성하는 클래스를 최소한으로 하되 상속 및 추상클래스를 구현하는 방식으로 설계하라 

 

이것을 가장 잘 확인해볼수 있는것이 자바의 기본API이다. (개인적인 의견입니다!...)

우리는 필요할때마다 자바에 내장된 각종 API를 가져와서 사용하고 필요하면 수정Override하여 API 자체를 수정하는 작업은 기피하고있는데 이것이 가장 OCP를 잘지키고있는 모습이라 생각한다.

 

여담으로 하단에 서술할 DIP는 OCP를 기반으로 작성 된다고 한다.

 

요점정리

1. 다형성과 확장을 최대한 지향하는것이다.

2. 기능이 필요할경우 추상화 및 상속을 활용하여 필요할때마다 구현하되 필요한 기능은 상위클래스가 아닌 하위클래스를 변경할것 

 


LSP(Listov Substitution Priciple): 리스코프 치환 원칙

상위객체를 하위객체로 치환하여도 상위타입을 사용하는 프로그램은 정상동작해야한다.

즉, 언제든 자식객체가 부모타입으로 교체하여도 문제없이 돌아가야한다.(다형성)

 

이를 조금더 자세히 설명하자면 다음과 같다.

 

정사각형 클래스가 직사각형 클래스를 상속하면 정사각형의 특징인 "네 변의 길이는 동일하다" 라는 특징과 그렇지 않은 직사각형의 차이로 인하여 직사각형을 정사각형 클래스로 치환해서 사용할 때, 두 클래스의 특징 차이 때문에 오류가 날 수 있다는 의미이다.

 

부모객체와 이를 상속한 자식객체가 잇을떄 부모객체를 호출하는 동작에서 자식객체가 부모객체를 완전히 대체할수 있다는 원칙이다. -> 자식객체의 확장이 부모객체의 방향을 온전히 따르도록 권고하는 원칙

 

요점정리

부모클래스의 인스턴스를 사용하는 위치에 자식클래스의 인스턴스를 대신 사용해도 정상작동해야 한다. (부모클래스의 구현조건사항을 자식클래스도 따라야한다. -> 오버라이딩할때 다형성을 지켜지도록 설계하자)


ISP(Interface Segregation Principle) - 인터페이스 분리 원칙

큰덩어리의 인터페이스를 작은단위로 분리시켜 필요한 메서드만 사용하게 해야한다. (적재적소에 맞도록 인터페이스를 설계해야한다.)

 

예를들어 OS는 버전이 올라가면서 여러가지 기능을 지원하기 시작했는데 현재 window 98에 없던 블루투스 기능이 현재(window 22)는 구현되어 있다 가정해보자. (이하 A 인터페이스 -> 1998버전, B 인터페이스 -> 2022버전)

업데이트 방식을 인터페이스로 구현한다고 가정해보면 A 인터페이스를 사용할경우 22버전,98버전 모두 사용에 문제가 없으나 (2022의 블루투스 기능은 추가로 구현하면 되기에 문제가 되지않음) B 인터페이스를 사용할경우 98버전에선 사용할수도 없는 기능인 블루투스기능을 구현해야하는 불편함이 생긴다. (불필요한 코드가 늘어난다)

 

 

즉, 인터페이스를 너무 크게 만들면 나중에 이를 사용할때 사용안하는 코드를 구현하는데 불필요한 코드가 늘어나니 가능하면 최소단위로 만들어라 이것이 ISP의 주요목적이다. (인터페이스는 다중상속을 지원한다)

 

요점정리

1. 인터페이스를 너무 크게(기능이 많게) 만들면 나중에 이를 구현할때 사용안하는 코드를 구현하는데 불필요한 코드가 늘어나니 가능하면 최소단위로 만들어라

2. 한번 분리한 인터페이스를 추가 수정사항이 생겨서 또 이것을 분리하는행위는 하지말아라(한번 구성하였는데 또 분리한다면 기존에 사용되던 인터페이스를 구현한 객체들도 전부 수정이 필요하기 때문이다)


DIP(Dependency Inversion Principle) - 의존 역전 원칙

(구현된)클래스를 참조하는게 아닌 그 상위요소로 참조하라

추상클래스, 추상메서드를 우선적으로 사용할것이며 구현클래스는 가능하면 건드리지 말아라.

의존 - 객체끼리 서로 참조등으로 A를 실행하기 위해선 B가 필요한경우 A는 B에 의존한다고 한다 (내가 메세지를 보내기 위해서는 카카오톡이 필요하다 -> 내가 메세지를 보내는 행동을 위해선 카카오톡이 필요(의존)한다)

의존성주입 - 클래스 외부에서 의존되는것을 대상 객체의 IV에 주입하는것

 

이것을 조금더 풀어쓰면 상위모듈(부모개체), 하위모듈(자식개체) 모두 추상화에 의존해야하며 상위모듈이 하위모듈에 의존하면 안된다.

즉, 의존관계를 형성할때 구체적인것(변하기 쉬운 구현객체)보단 추상적인것(변화하기 어려운 추상객체)에 의존해야하며(상위모듈(부모개체), 하위모듈(자식개체) 모두 추상화에 의존해야하며) 저수준의 모듈이 변경되어도 고수준 모듈은 타격을 입지 않는 형태가 좋다. (상위모듈이 하위모듈에 의존하면 안된다)

 

요점정리

참조가 필요할경우 가능하다면 구현된 클래스가 아닌 그것의 상위요소 (추상클래스 및 인터페이스)를 우선적으로 참조하라 (구현된 클래스가 수정이 되는경우 이것을 참조하는 객체도 수정이 필요하기 때문이다)


'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
제네릭(Generic)  (0) 2023.03.14
스택트레이스(Stack Trace)  (0) 2023.02.26
객체 지향 프로그래밍 (OOP) - 1  (0) 2022.12.25

스택트레이스란 프로그램이 시작도니 시점부터 현재 위치까지의 메서드 호출목록을 뜻한다.

쉽게말해서...요거다..

java.lang.reflect.InvocationTargetException
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at com.mylibrary.ap.xul.builder.handler.MethodCallback.invokeCallback(MethodCallback.java:32)
     at com.mylibrary.ap.xul.builder.handler.ViewHandler.invokeActionResult(ViewHandler.java:581)
     at com.mylibrary.ap.xul.context.impl.ViewContextImpl$2.onActionResult(ViewContextImpl.java:283)
     at com.mylibrary.ap.xul.context.impl.ActionContextImpl.setResult(ActionContextImpl.java:90)
     at com.mylibrary.ap.xul.action.stock.DialogAction.handleDialogClose(DialogAction.java:156)
     at com.mylibrary.ap.xul.action.stock.DialogAction$1.windowClosed(DialogAction.java:142)
     at com.mylibrary.api.Window.fireWindowClosedEvent(Unknown Source)
     at com.mylibrary.api.Window.onClose(Unknown Source)
     at com.mylibrary.api.platform.WindowBase.BaseClose(Native Method)
     at com.mylibrary.api.platform.WindowBase.BaseClose(Unknown Source)
     at com.mylibrary.api.Window.close(Unknown Source)
     at com.mylibrary.ap.xul.ui.MessageDialog.handleButtonClick(MessageDialog.java:145)
     at com.mylibrary.ap.xul.ui.MessageDialog$1.handleClick(MessageDialog.java:134)
     at com.mylibrary.api.ClickableSupport.fireClickEvent(Unknown Source)
     at com.mylibrary.api.ClickableSupport.handleAction(Unknown Source)
     at com.mylibrary.api.Button.actionHook(Unknown Source)
     at com.mylibrary.api.Component.action(Unknown Source)
Caused by: java.lang.NullPointerException
     at com.mycompany.service.impl.PortalManagerImpl.deleteMenuItem(PortalManagerImpl.java:603)
     at com.mycompany.service.impl.PortalManagerImpl.deletePortal(PortalManagerImpl.java:358)
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
     at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
     at org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:66)
     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
     at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
     at $Proxy54.deletePortal(Unknown Source)
     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
     at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
     at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
     at java.lang.reflect.Method.invoke(Unknown Source)
     at com.mycompany.util.SpringSecurityContextInvocationHandler.invoke(SpringSecurityContextInvocationHandler.java:62)
     at $Proxy84.deletePortal(Unknown Source)
     at com.mycompany.ui.binding.PortalDataProvider.doDelete(PortalDataProvider.java:81)
     at com.mycompany.ui.binding.PortalDataProvider.doDelete(PortalDataProvider.java:12)
     at com.mycompany.ui.binding.AbstractEISDataProvider.delete(AbstractEISDataProvider.java:105)
     at com.mylibrary.ap.xul.binding.dataset.impl.DatasetImpl.doCommit(DatasetImpl.java:90)
     at com.mylibrary.ap.xul.binding.dataset.impl.AbstractDataset.commit(AbstractDataset.java:251)
     at com.mylibrary.ap.xul.binding.dataset.impl.AbstractDataset.deleteRow(AbstractDataset.java:201)
     at com.mylibrary.ap.xul.action.dataset.DeleteDataRowAction.execute(DeleteDataRowAction.java:22)
     at com.mylibrary.ap.xul.context.impl.ViewContextImpl.execute(ViewContextImpl.java:294)
     at com.mycompany.ui.portal.PortalInfoHandler.onPostConfirmDeleteAction(PortalInfoHandler.java:192)
     ... 21 more

상단 코드에서 유심히 볼건 해당코드로

Caused by: java.lang.NullPointerException
     at com.mycompany.service.impl.PortalManagerImpl.deleteMenuItem(PortalManagerImpl.java:603)
     at com.mycompany.service.impl.PortalManagerImpl.deletePortal(PortalManagerImpl.java:358)

603번째줄 또는 358번째 줄에서 에러가 나타나는것을 의미한다

여기서 나타난 에러는 NullPointerException으로 Null값을 가진 객체나 변수를 참조할떄 에러가 나타난다는 의미이다.

if (item == null) {
    throw new NullArgumentException("item");
}

//중간 생략
List<PortalMenu> children = getMenuItems(item.getPortal().getId(), item.getId()); // 603번째 줄

for (PortalMenu child : children) {
    deleteMenuItem(child);
 }

 

출처 : https://jaehoney.tistory.com/51

'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
제네릭(Generic)  (0) 2023.03.14
객체 지향 프로그래밍 (OOP) - 2 (5대원칙)  (0) 2023.03.02
객체 지향 프로그래밍 (OOP) - 1  (0) 2022.12.25

객체 지향 프로그래밍 (Object Oriented Programing) 이란?

프로그래밍에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만들고 그 객체들 간의 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍 방법

 

- 장점

코드 재사용이 용이

남이 만든 클래스를 가져와서 이용할 수 있고 상속을 통해 확장해서 사용할 수 있다.

유지보수가 쉬움

절차 지향 프로그래밍에서는 코드를 수정해야할 때 일일이 찾아 수정해야하는 반면 객체 지향 프로그래밍에서는 수정해야 할 부분이 클래스 내부에 멤버 변수혹은 메서드로 존재하기 때문에 해당 부분만 수정하면 된다. 

대형 프로젝트에 적합

클래스 단위로 모듈화시켜서 개발할 수 있으므로 대형 프로젝트처럼 여러 명, 여러 회사에서 프로젝트를 개발할 때 업무 분담하기 쉽다.

 

- 단점

처리 속도가 상대적으로 느림

객체가 많으면 용량이 커질 수 있음

설계시 많은 시간과 노력이 필요

 

OOP의 4가지 특징

1. 캡슐화

2. 추상화

3. 다형성

4. 상속

 

오버라이딩 : 부모클래스의 메서드와 같은 이름, 매개변수를 재정의 하는것.

오버로딩 : 같은 이름의 함수를 여러개 정의하고, 매개변수의 타입과 개수를 다르게 하여 매개변수에 따라 다르게 호출할 수 있게 하는 것.


1. 캡슐화

원래의 목적은 객체의 속성(data fields)과 행위(methods)를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉한다.

즉, 사용하려는 메서드에 접근제어자를 활용하여 기존의 데이터에 손상이 가지 않도록(접근 못하도록) 하는것으로 주요목적은

1. 메서드를 통한 간접접근이 가능하도록 하는것

2. 내부적으로만 사용되는 부분을 감추기 위하여 사용

이것을 조금더 상세하게 설명하자면 

//접근 가능 코드
public class Time{
	public int hour = 12;		//0~23
	public int minute = 35;		//0~60
	public int second = 27;		//0~60
    //출력시 12:35:27
}

public class TimeTest{
	Time t = new Time(); 		//생성
	t.hour = 50;			//Time 맴버변수에 직접 접근
	System.out.println(t.hour);	//출력시 50:35:27이 출력된다...
	}
}

한가지 예시로 실시간 반영 서비스를 위한 코드를 작성하고 있다고 가정했을때 public으로 구현된 hour에 직접 접근하여 50을 넣는다면 코드상으론 문제가 없지만 실제 서비스를 구현한다고 했을때 사용자는 이로인해 여러 애로사항이 발생할것이다

 

여기서 발생하는 문제점은

문제점1. 접근제어자가 public이라 데이터의 원형이 바뀔 우려가 있다.

문제점2. 시간,분,초가 기준범위를 넘어선 값이 입력될 우려가 있다(현재시간 50시입니다..)

//수정한 코드

public class Time2{				//접근제어자 변경으로 문제점1 해결
	private int hour = 12;			//0~23
	private int minute = 35;		//0~60
	private int second = 27;		//0~60
    //출력시 12:35:27

    public int getHour() {return hour;}		//다른 메서드에서 호출은 가능해야 하기 때문에 public으로 선언
    public void setHour() (int hour) {		//조건문을 활용해 문제점 2 해결
	if (hour < 0 || hour >23) return;	
        this.hour = hour;
	}
}

public class TimeTest {
	public static void main(String[] args){
    	Time t - new Time();
        t.setHour (21);
        System.out.println(t.getHour));
	}	
}

이것이 캡슐화가 진행되도록 수정된 코드이다.

 

문제점1은 public에서 private로 접근제어자를 변경하여 데이터의 원형을 참조할수 없도록 하였다 

문제점2는 조건문을 활용하여 정해진 범위만 들어가도록 하였다

추가로 hour은 private로 선언하여 외부에선 접근이 불가능 한데 이를 활용하기 위하여 public을 사용하여 외부의 다른 클래스에서 사용이 가능하도록 하였다.

 

객체의 속성과 행위를 하나로 묶고 -> 시간과 관련된 객체, 행위(유효한지 확인)를 한곳에("Time2"메서드) 묶고

실체 구현 내용 일부를 외부에 감추어 은닉한다 -> private를 활용하여 외부에서 접근할수 없고 참조하는 형식으로만 진행된다.

 

접근제어자

public - 외부 클래스가 자유롭게 사용가능

protected - 같은 패키지, 자식클래스(상속)만 사용가능

default - 같은 패키지에 소속된 클래스만 사용가능

private - 외부에서 사용 불가(캡슐화!)

 


2. 추상화(Abstraciton)

공통된 속성, 기능을 묶어 이름을 붙이는것

 

좀더 자세히 설명하기 위해 코드를 작성하기로 하자

//추상클래스
abstract class Player{		//(A)
    abstract void Unit_attack (Sting Unitname);	//공격
    abstract void Unit_Idle();			//대기
    abstract void Unit_Run(Sting Unitname);	//후퇴
    
}

상단의 코드는 3개의 메서드가 있지만 어떠한 기능을 하는것이 아닌 그저 선언만 되있는 반쪽짜리 메서드이다.

선언만 해놨기에 작동하지는 않고 그저 사람이 어떤기능을 하겠거니 하고 추측할수 있는 추상화된 메서드를 작성한것일 뿐이다. (이하 추상메서드) 라고 칭한다.

그리고 이것을 실제로 사용하는 코드를 작성하였다.

public class Main extends Player{
	
    public static void main(String[] args){
    	String Player_name = "marine";
    	Main main = new Main();    
    	main.Unit_attack("Zergling");
    	main.Unit_Idle();
        main.Unit_Run("Zergling");
    }

    @Override
    void Unit_attack (Sting Unitname){	//공격
    System.out.println(Player_name+"이 "+ Unitname + "을 공격합니다"); 
    }
    
    @Override
    void Unit_Idle(){			//대기
    System.out.println(Player_name+"이 대기중입니다"); 
    }
    
    @Override
    void Unit_Run(Sting Unitname){	//후퇴
    System.out.println(Player_name+"이 "+ Unitname + " 에게서 달아납니다."); 
    
    }

상단의 코드는 추상메서드의 선언에 지나지 않은것을 Override하여 구체화한 코드이다.

공통된 속성, 기능을 묶어(플레이어가 갖고있는 기본적인 기능)이름을 붙이는것

 

추상화를 사용하는 이유가 무엇이냐?

설계자가 특정 메서드를 각 클래스 별로 재 구현을 원하지만 부모 클래스에서 일반 메서드로 구현하면 자식 클래스에서 구현을 하지 않는 경우가 발생할 수 있다. 이런 메서드를 추상 메서드로 선언하면 자식 클래스는 재구현을 강요받는다.

조금더 상세히 설명하자면 마린과 탱크가 있다고 가정을 해보자. 2개의 유닛은 기본적인 기능(A)외에 추가적인 기능(ex:스팀팩, 시즈모드) 등이 있을때 해당 기능을 넣지 않고 개발해버린다면 큰 오류이다. (시즈모드 못하는 탱크...)

이것을 추상메서드로 선언해놓고 구체화를 진행할경우 구체화된 메서드에서 빠진부분이 없는지 컴퓨터가 점검해주는것 + Override를 통해 부가기능을 추가하는것이 장점이다.


3. 상속(Inheritance)

 

부모클래스의 속성과 기능을 그대로 이어받아 사용할 수 있게하고 기능의 일부분을 변경해야 할 경우 상속받은 자식클래스에서 해당 기능만 추가로 작성하여 사용하면 되는것이다.

하단의 코드를 참조하길 바란다.

//부모클래스
class Tv{
    boolean power;
    boolean now_touch;
    
}

class Phone{
    boolean power;
    boolean now_touch;
}

class nintendo{
    boolean power;
    boolean now_touch;

}

간단한 예시로 TV, 핸드폰, 닌텐도 3기기 모두 공통적으로 power,touch 맴버를 선언하였다.

상단의 코드는 3개밖에 안되지만 예를들어 스타 유닛마다 체력,공격력,방어력을 저장할 변수를 만든다고 생각해봐라..... 굉장히 번거로운 작업일것이다.

이러한 문제점을 없애주는 기능이 상속이다.

쉽게 말해서 공통적으로 사용되는것을 만들어놓고(부모클래스) 필요할때마다 가져와서(상속해서) 사용하는것

//자식클래스 상속 예시
//class 자식클래스 extends 부모클래스{...}

//총 맴버는 power,now_touch,Tv_Channel 3개
class Tv_attribute extends Tv{		
    int Tv_Channel;
}

//총 맴버는 power,now_touch,app_name 3개
class Phone_attribute extends Phone{	
    string app_name;
}

//총 맴버는 power,now_touch,game_name 3개
class Nintendo_attribute extends Nintendo{	
    string game_name;
}

//총 맴버는 power,now_touch,game_name,stage_level 4개
class Nintendo_play_gmae extends Nintendo_attribute{	
    int stage_level;

}

상속의 특징

1. 자식클래스는 부모의 생성자, 초기화 블럭을 제외한 모든 맴버를 상속받는다. (부모가 아닌 부부모의 맴버도 상속이 된다)

2. 자바에서는 다중상속이 불가능 하다.(extends Nintendo,Phone 등은 불가능하다 -> 이것은 자바의 일반클래스가 부모 클래스를 1개만 가져야 하기 때문인데 클래스가 아닌 인터페이스를 이용한다면 다중 상속이 가능하긴 하다)

3. 접근제어자가 private을 갖는 필드나 메소드는 상속이 불가하고, 패키지가 다를 경우 접근제어자가 default인 경우도 상속이 불가하다.(접근제어자에 대한 내용은 별도 포스팅 예정)

4. 접근제한을 더 강하게 오버라이딩 할수없다.

5. 부모의 메서드와 동일한 시그너처(동일한 리턴타입, 동일한 메소드이름, 동일한 매개변수 리스트)를 가져야한다.

6. 상속으로 부모클래스의 메서드를 오버라이딩 해버린경우 부모클래스의 메서드는 숨겨지고 오버라이딩된 자식메서드만 사용되며 부모클래스의 메서드를 호출하려면 super키워드로 호출할수 있다..(만일 그렇지 않으면 ambiguous 오류 발생)

 


4. 다형성(polymorphism)

여러가지 형태를가질수 있는 능력 -> 가장 범용적인 예시로 오버로딩, 오버라이딩, 함수형 인터페이스 3가지 종류이다.

다형성을 한줄로 요약하자면 "조상타입 참조변수로 자손타입 객체를 다루는것"이다.

 

하단의 예시를 참조하길 바란다.

//다형성 설명용 코드
class Controller {	//인스턴스 5개 (power,channel,power(),channelUp(),channelDown())
    boolean power;
    int channel;

    void power()	{ power =!power;}
    void channelUp()	{ ++channel;	}
    void channelDown()  { --channel;	}

}

class SmartTv extends Controller{	//부모(Controller)가 자식(smarTv)
    String text;			//인스턴스 2개 (text,caption())
    void caption() {...}

}

// 기존 생성자를 활용한 코드
// 앞뒤의 타입이 일치함을 확인할수 있다.
Controller TvController = new Controller();
SmartTv STV = new SmartTv();	//(A)

//타입의 불일치 - 다형성 (B)
Controller TvController2 = new SmartTv();

 

Controller 타입(참조타입) TvController 변수는 Controller객체를 생성자로 받고

SmartTv 타입(참조타입) STV변수는 SmartTv생성자로 받고있다.

 

다형성이 적용된 코드(B)를 확인하면

Controller타입 TvController2 변수에 SmartTv를 생성자로 받고있는것이다.

두 코드의 차이점은 타입이 불일치 한다는것을 확인할 수 있는데 이것을 조금더 자세히 설명해보겠다.

 

상단에서 다형성이란 조상타입참조변수(Controller)가 자손타입 객체를 다루는것(SmartTv, TvController)이라고 설명을 해놨는데 일단 타입이 일치할때(A)와 일치하지 않을경우(B)의 차이부터 알아보자

 

(A)는 동일한 타입을 생성하기에(SmartTv-SmartTv) (A)가 사용가능한 맴버의 숫자는 7개이다. (Controller + SmartTv)

(B)의 타입이 다르기 때문에 맴버는 7개이나 사용가능한 맴버는 5개이다. (Controller)

 

그러면 제약이 더 많은것 같은데 다형성의 장점이 무엇이냐?

1. 유지보수가 쉽다. (상단 코드의 예시로 Controller만 건드리면 자식클래스도 모두 변경되기 때문이다.)

2. 느슨한 결합 (클래스간 의존성이 줄어들며 확장성이 늘어나고 결합도가 낮아 안정성이 높아진다. -> 참조타입이 달라도 사용이 가능하기 때문)

3. 재사용성 증가 (한번 선언해두면 필요할때마다 호출해서 사용이 가능하기때문 참조타입이 달라도 된다!)

 

조건

자손타입 참조변수로 조상타입 객체를 가리킬순 없다. (업캐스팅만 가능하다)

한가지 예시를 들면 갤럭시10(이하 "10v")에서 20(이하 "20v")으로 넘어갈때 터치기능 외에 스와이프 기능이 추가되었다 가정을해보자

그리고 10v에서 작성된 프로그램 (A)는 터치기능만 있으면 작동이 되지만

20v에서 작성된 프로그램이(B)는 터치기능과 스와이프가 필수로 필요하다고 가정을 했을때

10v에서 사용하던(A)는 20v에선 사용이 가능하다. -> 20v도 터치기능은 내장되있기 때문이다!

그러나 20v에서 사용하던(B)는 10v에선 사용이 불가능하다. -> 스와이프 기능이 없기 때문이다!

class phone10V{
	boolean touch;	//터치기능
}

class phone20V extends phone10V{
	boolean swape;	//스와이프기능
}


phone10V A = new phone20v();	//가능 - A는 터치만 있으면 사용가능
phone20V B = new phone10v();	//불가능 -	B는 스와이프까지 있어야 사용가능

'CS' 카테고리의 다른 글

프로세서, 프로세스  (0) 2023.03.21
주소 바인딩  (0) 2023.03.18
제네릭(Generic)  (0) 2023.03.14
객체 지향 프로그래밍 (OOP) - 2 (5대원칙)  (0) 2023.03.02
스택트레이스(Stack Trace)  (0) 2023.02.26

+ Recent posts