프로세스란

 

프로세스는 메모리에 적재되어 실행중인 프로그램을 말합니다.

 

프로세스의 메모리 구조는 다음과 같습니다.

 

Stack: 여기에는 메서드 호출 시 생성되는 지역 변수, 매개변수 및 자바의 기본 데이터 유형(primitive type) 값들이 저장됩니다.

재귀 함수 호출과 같이 런타임에 크기가 변경될 수 있습니다.

 

Heap: 참조 타입(Reference Type, Non-Primitive Type)의 객체들이 여기에 할당됩니다.
런타임에 크기가 동적으로 결정되며 Garbage Collector에 의해 관리됩니다.

 

Data: BSS 영역과 Data 영역으로 나뉩니다. 정적(static) 변수, 상수 등이 이 영역에 저장됩니다.

 

Code: 프로그램의 소스 코드가 컴파일되어 생성된 기계어 코드가 저장되는 영역입니다. 이 영역은 읽기 전용이며

프로그램이 실행될 때 수정되지 않습니다.

 

Stack , Heap 은 Dynamic한 특징을 Data , Code 는 Static한 특징을 갖습니다.

 

프로세스는 고유한 메모리를 갖습니다. 따라서 N개의 프로세스가 실행중이라면 실제로 N개의 Stack,Heap,Data,Code 영역을 갖습니다.

또한 프로세스가 실행되면 OS는 프로세스 관리를 위해 PCB(Process Control Block)을 생성합니다.

(이것 역시 N개의 PCB가 생성됩니다.)

 

PCB(Process Control Block)

 

아래 그림은 PCB의 구조입니다.

PCB는 운영체제가 프로세스를 관리하고 제어하기 위해 사용되는 자료구조이며 

Process를 설명하는 일종의 메타데이터입니다.

 

PCB의 주요 역할은 다음과 같습니다.

  1. 프로세스 상태(대기,실행)
  2. 프로세스 번호(프로세스의 고유 식별 번호 , PID)
  3. 프로그램 카운터(PC, 다음 실행될 명령의 주소를 나타냄)
  4. 레지스터

 

Context Switching

 

프로세스의 컨텍스트 스위칭(Context Switching)은 하나의 프로세스가 실행 중인 상태에서 다른 프로세스로 전환하는 과정을 말합니다.

 

이는 운영체제가 다중 프로세스를 실행하기 위해 필요한 기술로 컨텍스트는 프로세스가 현재 실행 중인

상태를 나타내는 정보의 집합(환경)을 나타냅니다

 

컨텍스트 스위칭은 PCB를 기반으로 동작합니다.(스레드 또한 가능)

 

Process1 실행중 -> 인터럽트 발생 -> PCB1저장 -> PCB2리로드 -> Process2실행 의 순서를 나타내며

 

이때 프로세스의 실행환경이 전환됩니다.

Context Switching 하기 위해서는 Interrupt가 필요하며 이는 CPU사용률을 낮춥니다.

(스레드의 경우 Stack영역을 제외한 모든 부분을 공유하기 떄문에 Switching에 대한 자원소모가 훨씬 적습니다.)

 

IPC(Inter Process Communication)

 

앞서 말했듯이 프로세스는 각각 고유한 메모리와 실행환경을 가집니다.
따라서 프로세스끼리 통신하기 위해서는 특정한 메커니즘이 필요합니다.

공유메모리(Shared Memory)

IPC 중에서 가장 빠른 통신 방법으로 여러 프로세스가 통신할 수 있도록 메모리를 공유하는 것을 말합니다.
메모리 자체를 매핑하여 공유하기 떄문에 불필요한데이터 복사의 오버헤드가 발생하지 않습니다.

전체 데이터를 메모리에 로드하지않고 필요한 부분을 선택적으로 로드할 수 있다는 장점또한 존재합니다.

 

 

파일

디스크에 저장된 데이터를 기반으로 통신하는 것을 의미합니다.

프로세스의 메모리에 데이터를 로드시키지 않기 떄문에 I/O관련 오버헤드가 발생합니다.

Socket

네트워크 인터페이스 (TCP , UDP , HTTP 등)를 기반으로 통신하는 것을 의미합니다.

예를들어 크롬브라우저에서(프로세스) www.naver.com 검색하면 네이버 서버에서 프로세스 띄워서 응답을 주게됩니다.

 

채팅프로그램의 경우에도 클라이언트 프로세스 , 서버 프로세스끼리의 통신이 필요합니다.

 

 

파이프

파이프 기법은 익명파이프(Anonymous Pipe) , 명명파이프(Named Pipe)로 나뉩니다.

익명파이프(Anonymous Pipe)

프로세스 사이에 FIFO기반 통신채널을 만들어 통신하는 것입니다.

파이프 하나당 단방향 통신이기 때문에 양방향 통신을 하려면 2개의 익명파이프를 만들어야 합니다.

부모-자식 프로세스 간에만 사용 가능하며 다른 네트워크상에서는 사용이 불가능합니다.

 

명명파이프(Named Pipe)

익명파이프의 확장된 개념으로 부모-자식 뿐 아니라 다른 네트워크 상에서도 통신할 수 있는 파이프입니다.

 

익명파이프의 경우 부를 이름이 없어 다른 네트워크에서 해당 파이프를 불러 통신하는게 불가능했지만    

명명파이프에는 이름이있어 다른 네트워크와 통신이 가능합니다.

 

보통 서버,클라이언트용 파이프를 구분해서 동작합니다.

 

메시지 큐

프로세스나 스레드 간에 비동기적으로 데이터를 주고받을 수 있도록 하는 통신 방식 중 하나로

메시지를 큐(Queue) 자료구조 형태로 관리하는 버퍼를 만들어 통신하는 것을 말합니다.

  1. 프로세스가 메시지를 보내거나 받기 전에 큐를 초기화합니다.
  2. 보내는 프로세스(sender)의 메시지는 큐에 복사되어 받는 프로세스(receiver)에 전달됩니다.

 

 

IPC기법을 자바로 나타낸 간단한 예제입니다.
https://github.com/masiljangajji/IPC_Example

 

GitHub - masiljangajji/IPC_Example

Contribute to masiljangajji/IPC_Example development by creating an account on GitHub.

github.com

도움이 되셨으면 좋겠습니다.

'프로그래밍 기초 > 운영체제' 카테고리의 다른 글

가상메모리(Virtual Memory)란  (0) 2023.12.28
운영체제(Operating System)란  (1) 2023.12.28

가상메모리(Virtual Memory)란

OS에서 사용하는 메모리 관리 기법중 하나 입니다.
컴퓨터가 실제 이용가능한 메모리 자원을 추상화하여 이를 사용하는 사용자들에게 더 큰 메모리로 보이게 만드는 것으로
HDD의 일부 영역을 RAM처럼 사용함으로써 구현됩니다.

 

포토샵과 유투브를 실행해 RAM이 꽉 찬 상황을 가정하겠습니다.
기존의 방법으로는 RAM이 꽉 찬 상태에서 Word를 실행시키는 것은 불가능합니다.

 

하지만 RAM에 적재돼 있는 포토샵,유투브 중 사용하지 않는 프로세스를 가상메모리(HDD)로 보내 공간을 만들고

그 공간에 Word를 할당시킨다면 더 많은 프로세스를 실행 가능하게 만들고 더 큰 메모리를 사용하는것과 같은 효과를 나타내게 됩니다.

 

이는 프로세스들끼리 메모리 침범이 일어날 여지를 크게 줄이며
가상메모리라는 통일된 주소 공간을 배정할 수 있음으로 메모리 관리의 단순화를 돕습니다.

 

이것이 가상메모리를 사용하는 이유이며 동작방법에 대해 알아보겠습니다.

Page Table

Page Table은 RAM에 존재하는 실제 주소(Physical Address)와 HDD에 존재하는 가상 주소(Virtual Address)를

매핑한 테이블 입니다.

 

Page Table은 다음을 포함합니다.

  1. 페이지를 식별하는 번호인 Virtual Page Number
  2. 가상페이지가 현재 RAM에 어느 프레임에 매핑되어 있는지를 나타내는 Physical Frame Number
  3. 해당 페이지가 현재 RAM에 존재하는지 여부를 나타내는 Valid Bit

 

페이지와 프레임의 상태 관리를 위해 사용하며

이 가상주소는 사실 RAM에서 이런 주소야~ 라는 것을 알려줍니다.

 

Page는 가상 메모리의 최소 단위를 의미하며
Frame은 물리 메모리의 최소 단위를 의미합니다.
(Page Table은 MMU(Memory Management Unit)에 의해 관리됩니다.)


또 자주 사용되는 주소는 TLB(Translation lookaside buffer)라는 캐싱계층을 둡니다.

 

Page Fault , Swapping

Page Falut란 Page에 접근했으나 해당 페이지가 RAM에 존재하지 않는 경우를 의미합니다.

모든 페이지를 RAM에 적재해놓지 않고 필요한 페이지만 RAM에 적재하는 방식을 사용하기 떄문에 발생합니다.

실제 동작 순서는 다음과 같습니다.

TLB 확인 -> Page Table 확인 -> RAM에 Page 존재안함 -> Page Fault 발생

 

이를 해결하기위해 Page Replacement Algorithm에 따라 RAM에 로드된 페이지를 HDD로 보내게되고(Swap Out)
필요한 Page를 RAM에 로드하게 됩니다(Swap In).

 

이러한 과정을 Swapping이라 부릅니다.

 

하지만 이 방법도 메모리 부족을 완벽하게 해결할 순 없습니다.

너무 많은 프로세스를 실행시키게 되면 Swapping이 빈번하게 발생하게 되고 CPU 사용률이 줄어들게 됩니다.

OS는 CPU사용률이 줄어 더 많은 일을 주게되고 이는 더 많은 Page Fault , 더 적은 CPU 사용률을 만듭니다.

이런현상을 Thrashing이라 부릅니다.

 

따라서 가장 명확한 해결법은 메모리를 늘리는 것입니다.

 

Page Replacement Algorithm

LFD (Longest Forward Distance)

가장 좋은 알고리즘은 오프라인 알고리즘의 일종인 LFD(Longest Forward Distance)입니다.

LFD는 가장 먼 미래에 참조되는 페이지와 현재의 페이지를 바꾸는 알고리즘이며 가장 적은 스와핑이 일어납니다.

이는 매우 효율적이지만 현실에서는 미래에 사용될 것을 알 수 없기 떄문에 구현이 불가능합니다.

따라서 이 알고리즘은 성능에 대한 Upper Bound를 제공합니다.

ex) 내 알고리즘은 LFD에 비하면 이정도야~

 

FIFO (First In First Out)

가장 먼저 메모리에 적재된 페이지부터 교체하는 알고리즘입니다.

 

LRU (Least Recently Used)

최근에 사용되지 않은 페이지를 교체하는 알고리즘입니다.

최근에 사용되지 않음 = 가장 오랫동안 사용되지 않은 페이지 교체

 

 

도움이 되셨으면 좋겠습니다.

'프로그래밍 기초 > 운영체제' 카테고리의 다른 글

프로세스(Process)와 통신기법  (1) 2023.12.29
운영체제(Operating System)란  (1) 2023.12.28

운영체제란 (Operating System)

컴퓨터 시스템을 관리하고 제어하는 소프트웨어를 말합니다.

OS 구성요소

OS의 구성 요소는 다음과 같습니다.

  1. Interface
  2. System Call
  3. Kernel (I/O 드라이버, 파일시스템)

 

Interface

Interface의 경우 크게 2가지 로 나뉩니다

 

GUI (Graphical User Interface)
그래픽으로 구성된 화면을 말하며 , 키보드와 마우스를 조합하여 상호작용 합니다. ex(Window , Mac)

 

CUI (Chracter User Interface)
텍스트로 구성된 화면을 말하며 , 키보드를 중심으로 상호작용 합니다. ex(MS-DOS, chatGPT)

 

초기에는 CUI를 사용했지만 현재는 대부분의 OS가 GUI(Graphical User Interface) 방식을 채택하고 있습니다.


System Call

System Call이란 OS가 Kernel에 접근하기 위한 인터페이스이며

유저 프로그램이 커널 함수를 호출할 때 System Call을 거쳐서 호출하도록 설계되어있습니다.

프로세스 관리(생성,삭제), 파일관리 , 디바이스 관리 , 프로세스간 통신 등에 동작에서 커널함수를 호출합니다.

public class Main {

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; ; i++) {
            Thread.sleep(1000);
            System.out.println(i);
        }

    }

}

이 코드를 실행시키면 , 하나의 매 초마다 숫자를 출력하는 자바 프로세스가 생성 됩니다.

 

이 프로세스를 kill 명령어를 통해 삭제할 수 있습니다.



이 경우 System Call을 거쳐 Kernel Function 을 호출하게 됩니다.

 

조금 더 절차를 자세히 하면 다음과 같습니다.

  1. 유저 프로그램이 trap을 발생
  2. 올바른 요청인지 확인
  3. 유저모드가 시스템콜을 통해서 커널모드로 변환
  4. 커널함수 실행

System Call 동작 사진

유저모드 , 커널모드

유저모드는 운영체제 서비스를 제공받을 수 없는 실행 모드입니다. 즉 권한이 없는 상태로 커널에 접근이 불가능 합니다.
커널모드는 운영체제 서비스를 제공받을 수 있는 실행 모드입니다. 즉 권한이 있는 상태로 커널에 접근이 가능 합니다.

 

일반적인 응용프로그램은 유저모드에서 , 특정 자원에 대한 접근은 커널모드에서 이루어집니다.
(권한 차이를 부여하는 이유는 아래에서 다루겠습니다.)

 

Kernel

OS의 뇌를 담당하며 , OS가 하는 일은 Kernel이 관리합니다.

  1. CPU 스케쥴링과 프로세스 상태관리
  2. 메모리관리
  3. 디스크파일 관리
  4. I/O 디바이스 관리

 

 

키보드 드라이버가 깔려있지 않아 키보드를 사용할 수 없는 경우를 종종 볼 수 있습니다.

이러한 드라이버 또한 커널이 관리하며 추상화를 통해 하드웨어 장치를 제어하고 동작을 관리 합니다.

 

 

우리는 앞서 배운 개념으로 System Call을 사용해 유저 모드를 커널 모드로 변경 후 커널에 접근함을 알고 있습니다.

유저 모드, 커널 모드 구분 없이 커널에 접근할 수 있게 만들면 더 빠르고 효율적이겠지만
이는 안전성과 보안성에 큰 문제가 야기할 수 있습니다.

만약 프로그램이 CPU, RAM, HDD 등에 마음대로 접근하고 조작할 수 있다면 자원이 무질서하게 관리될 것이고
프로그램이 조금만 실수해도 컴퓨터 전체에 문제가 생길 수 있습니다.

이러한 문제점들을 유저 모드와 커널 모드를 분리함으로써, 또 System Call이라는 Interface를 둠으로써

사용자와 OS 간의 경계를 설정하고 OS의 핵심 부분을 안전하게 보호하고 있습니다.

 

Kernel은 다양한 운영체제에서 Linux 커널, Windows NT 커널, macOS의 XNU 커널 등이 존재합니다.

 

도움이 되셨으면 좋겠습니다.

'프로그래밍 기초 > 운영체제' 카테고리의 다른 글

프로세스(Process)와 통신기법  (1) 2023.12.29
가상메모리(Virtual Memory)란  (0) 2023.12.28

[Immutable Vs Mutable]

프로그램의 오류는 예기치 못한 동작으로 인해 발생하게 됩니다.
이 예기치못한 동작을 줄이고 예측 가능하게 동작시키려면 어떻게 설계해야 할까요??
 
자바는 생성된 이후 상태를 변경할 수 있는 Mutable Type과 그렇지 않은 Immutable Type이 존재합니다.
 
지금부터 각 타입의 특징과 장단점에 대해 알아보겠습니다.

Immutable Type (불변 타입)

Immutable Type은 생성된 후 상태를 유지하는 타입을 의미합니다.
대표적인 예시로 String 이 존재합니다.

String Class

 
 
value , coder 등등의 field가 final로 선언된 것을 볼 수 있습니다.

이를 통해 String Type을 불변객체로 만드는 것이죠
 
하지만 우리는 한가지 의문에 빠집니다.
값이 안바껴?? += 연산으로 값 바뀌던데??

String str="1234";
str+="5678";

불변객체 임에도 다음의 연산이 가능한 이유는 "1234" 값을 갖고있는 str의 값이 변하는게 아닌
"12345678" 값을 갖고있는 새로운 str 객체를 만들어 내는 것이기 떄문입니다.
 
사실상 아래와 같은 연산을 하는 것이죠

String str = new String("12345678");

(이러한 연산또한 막고싶다면 final String 으로 선언하면 됩니다)
 

Constant Value

static final int MAX_SIZE = 10;

상수또한 불변타입에 속합니다.
불변타입이기 떄문에 값을 변경하려고 시도하면 에러가 발생하는 것을 볼 수 있습니다.

MAX_SIZE=20; // 에러발생

불변타입을 사용한다면 상태변화를 막을수 있기 때문에 객체가 초기에 설정한 상태를 유지하게 되고
상태변화로 인한 버그를 막을 수 있게 됩니다.
 
또한 다른 메서드의 파라미터로 전달될 떄에도 다른 코드에 의해 수정될 염려가 없기 떄문에 안전하게 전달가능합니다.
뿐만 아니라 불변값임을 명시함으로서 더욱 이해하기 쉬운 코드를 작성할 수 있습니다.

public static int rangeSum(final int start, final int end) {

        int result = 0;

        for (int i = start; i <= end; i++) {
            result += i;
        }
        return result;
    }

시작지점부터 끝나는 지점까지의 정수합을 반환하는 메서드입니다.

시작지점과 끝나는 지점을 불변하게 만듦으로서 이해를 돕고 버그를 방지합니다.
 
불변타입은 변경할 수 없기 떄문에 호환성이 떨어지고 코드의 작성을 어렵게 만들지만 그로인한 버그또한 줄여줍니다.
따라서 견고한 프로그램을 작성하고 싶다면 불변타입을 사용하는 것이 좋습니다.
 
 

Mutable Type(가변 타입)

StringBuilder는 String과 비슷한 기능을 하지만 가변객체 입니다.

StringBuilder sb = new StringBuilder();
sb.append("1234");
sb.append("5678");
System.out.println(sb);

이 코드는 실제로 sb가 가지고 있는 값이 변경되는 것이죠
 

String str = sb.toString();
System.out.println(str);

StringBuilder는 toString을 통해서 String Type으로도 사용가능하고
객체를 새로 만들어 내는 것이 아니기 떄문에 성능측면에서도 String보다 뛰어납니다.
 
하지만 이러한 호환성이 견고한 프로그램 작성을 어렵게 만듭니다.

Risks Of Mutaion (가변위험성)

Mutable Type은 위에서 명시한 것 처럼 장점이 많습니다.
상태 변경이 가능하기 떄문에 호환성이 뛰어나며 이로인해 쉬운 코드를 작성할 수 있는것이죠
 
하지만 똑같은 이유로 단점이됩니다.
상태 변경이 가능하기 떄문에 프로그램 시작에서의 상태와 프로그램의 특정 시점에서의 상태가 다를 수 있기 떄문이죠
 
따라서 객체가 어떤 상태인지 이해하기 어려워지고 Contract를 강제하는데 어려움이 생깁니다.
예시를 통해 알아보겠습니다.
 

Passing Mutable values

 public static int sum(List<Integer> list) {

        int sum = 0;
        for (int num : list) {
            sum += num;
        }

        return sum;
    }

List요소의 합을 반환하는 메서드

  public static int sumAbsolute(List<Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            list.set(i, Math.abs(list.get(i)));
        }
        return sum(list);
    }

List 요소의 절대값 합을 반환하는 메서드
 
Mutable Type을 인자로 받았기 떄문에 상태변경이 가능합니다.
또한 sumAbsolute 메서드는 기존에 작성한 sum메서드를 재사용하기 떄문에 효율적으로 보이기까지 합니다.
 
하지만 이 코드는 치명적인 버그를 발생시킬 것입니다.
sumAbsolute() 메서드를 호출한 후에는 List요소 값이 변하기 떄문입니다.

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,-1,-2,-3));

System.out.println(sum(list));
System.out.println(sumAbsolute(list));
System.out.println(sum(list));

이 코드의 결과 값은 다음과 같습니다.
 

list가 가변객체이기 때문에 처음 정의한 list와 sumAbsolute를 호출한 뒤의 list의 정의가 달라집니다.
이런식으로 재사용이나 성능에서 장점을 가질 수 있지만 추적하기 어려운 버그를 일으킬 수 있습니다.

 

Aliasing Risk

Alias(동일한 참조를 갖는 변수)가 존재할때 문제가 생길 수 있습니다.

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, -1, -2, -3));
List<Integer> list2 = list;

list2.add(5);
list2.add(6);
list2.add(7);

System.out.println(sum(list) + " " + sum(list2));

list2는 list와 동일한 참조를 갖기 떄문에 list2의 상태변화가 list에게도 영향을 주어 버그를 일으킬 수 있습니다.
 
 

Mutation undermines an iterator

 public static void removeUnderZero(List<Integer> list) {
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) < 0) {
                list.remove(i);
            }
        }
    }

0보다 작은 요소를 삭제하는 메서드입니다.

문제될 것이 없어보이지만 list가 가변적으로 변함으로서 요소가 삭제될시 size() 또한 가변적으로 변하게 됩니다.
따라서 결과값이 예상과 다르게 반환됩니다.

 
 

Mutable objects reduce changeability

가변객체의 사용은 코드를 변경하기 어렵게 만듭니다.
list의 sum에 대해서 불필요한 연산을 막고자 cache를 도입하는 상황을 가정하겠습니다.
 

public static Map<List<Integer>, Integer> cache = new HashMap<>();

    public static void main(String[] args) {

        List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));

        addToCache(list);

        System.out.println(cache.get(list));
    }

public static void addToCache(List<Integer> list) {
        if (cache.get(list) == null) {
            cache.put(list, sum(list));
        }
    }

문제없이 동작하는 것을 볼 수 있습니다.
 
하지만 코드가 추가된다면 어떨까요??

list.add(4);
list.add(5);

System.out.println(cache.get(list));

list의 상태가 변경되면서 cache가 정상적으로 동작하지 않게됩니다.
 
이렇게 가변객체는 논리적인 버그를 발생시킬 수 있습니다. 따라서 보다 안전한 방법으로 사용되야 합니다.
 

Use Producer

Producer는 기존객체에서 새로운 객체를 생성하는 것을 의미합니다.

  public static void sortList(List<Integer> list) {

        for (int i = 0; i < list.size(); i++) {
            for (int j = 0; j < list.size(); j++) {
                if (list.get(i) < list.get(j)) {
                    int temp = list.get(i);
                    list.set(i, list.get(j));
                    list.set(j, temp);
                }
            }
        }
    }

다음은 list를 오름차순으로 정렬하는 메서드입니다.
 
이 연산은 문제없이 동작하지만 list의 요소를 변화시킵니다. 이로인해 생기는 버그를 막기 위해 메서드를 변경하겠습니다.

 public static List<Integer> sortList(List<Integer> list) {

        List<Integer> result = new ArrayList<>(list);

        for (int i = 0; i < result.size(); i++) {
            for (int j = 0; j < result.size(); j++) {
                if (result.get(i) < result.get(j)) {
                    int temp = result.get(i);
                    result.set(i, result.get(j));
                    result.set(j, temp);
                }
            }
        }

        return result;
    }

새로운 List 객체를 반환하도록 만듦으로서 기존 list를 변화시키지 않아 버그발생을 줄일 수 있습니다.
(이런 방식을Defensive Copy라 합니다.)
 

Localize Value

변수가 사용되는 범위를 줄여야 합니다. 가변객체가 넓은 범위에서 사용될 수 있다면 (Static 이거나 메서드 자체가 거대하다면)
상태변화로 생기는 버그를 잡기 어려울 것입니다.
 
 
이 글에서는 Immutable Type 과 Mutable Type의 이해와 장단점에 대해 다뤘습니다.
도움이 되셨다면 좋겠습니다.

Specification

프로그램에서 터지는 버그는 대부분 동작에 대한 오해로 발생합니다.

이러한 동작의 오해를 줄이기 위한 대표적인 방법으로 Spec(명세)이 존재합니다.

또한 완성된 프로그램은 필수적으로 Spec을 갖고있어야 합니다.

 

그렇다면 Spec은 무엇을 의미하는 걸까요??

다음은 실제로 사용되는 BigInteger 클래스 Spec입니다.

Class BigInteger

public BigInteger add(BigInteger val)
Returns a BigInteger whose value is (this + val).
Parameters:
val - value to be added to this BigInteger.
Returns:
this + val

add 메서드의 Signature와 실행되는 동작에 대한 간단한 설명이 존재합니다.

 

이러한 명세를 통틀어 Spce이라 말합니다.

인터페이스에 존재하는 Abstract Method도  Signature를 명시해놓은 Spce의 일부로 볼 수 있습니다.

 

만약 Spec이 없다면 이 메서드가 어떤연산을 하는지 알 수 없으며 어떤 연산을 하는지 정해지지 않았기 떄문에
테스트코드 작성도 불가능해집니다.

 

또한 구현이 바뀔때마다 해당 메서드가 어떤 기능을 하는지 내부를 확인해야 알 수 있음으로 Spec 명시는 필수적입니다.

 

Contract

Contract(계약)은 클라이언트와 개발자의 책임을 의미합니다.

  • 개발자의 책임 : 구현을 완료하고 Spec을 명시합니다. 또 Spec만 따르면 해당 기능은 정상적으로 동작함을 책임집니다.
  • 클라이언트의 책임 : 개발자가 구현한 기능을 사용할때 Spec에 맞게 사용해야 한다.

이런 책임을 묶어 Contract라 지칭합니다. 또한 Contract의 개념을 적용시켜 PreCondition , PostCondition 이 만들어집니다.

 

PreCondition(전제조건) 은 클라이언트의 책임입니다.

만약 Spec이 다음처럼 명시돼있다면

Requirements : 중복이없는 배열 
Effects : 중복없는 배열을 오름차순 정렬함

사용자는 Spec에 맞게 중복이없는 배열을 인자로 넘겨주어야 합니다.

그렇지않으면 Spec을 위반하는것으로 결과값을 올바르지 않을 수 있습니다. (이 경우에는 보통 Exception을 발생시켜 줍니다)
이러한 조건을 PreCondition 이라 합니다.

 

반면에 PostCondition(사후조건)은 구현자의 책임입니다.

전제조건이 맞으면 올바른 값을 반환해야 한다는 것이죠

이러한 조건을 이용해 테스트코드 작성이 가능합니다.

 

Test Code

우리는 Spec이 왜 필요한지 어떤 역할을 하는지 알아봤습니다.

이를 이용해 Test Code를 작성해 보겠습니다.

Test Code에는 크게 2가지 가 존재합니다.

  1. BlackBox Test
    • 연산내부가 어떻게 구현됐는지 모름 , 오직 Spec에 대한 명시만으로 테스트 작성
  2. WhiteBox Test
    • 연산내부 구현의 지식으로 테스트 작성

각각의 테스트는 장단점이 존재합니다.

BlackBox Test는 코드의 내부를 몰라도 된다는 장점이 있지만

테스트 케이스를 설계할 때 누락된 기능이 있을 수 있습니다.

또한 주로 입력과 출력에 중점을 두는 테스트 방법입니다.

 

WhiteBox Test는 코드의 내부 구조와 논리를 이해하고 검증하는 방법입니다.

따라서 논리적 결함을 발견하기 용이하지만 구조를 이해하고 있어야 하기 때문에 코드에 대한 지식이 필요합니다.

BlackBox Test

  public static boolean inputPassword(String passWord) {

    String regex = "^[0-9]*";

        if (passWord.equals("")||!Pattern.matches(regex,passWord)) {
            return false;
        }
        return true;
    }

다음은 매우 간단한 연산이며 Spec은 다음과 같습니다.

Requirments : String Value
Effects :  비어있지 않은 숫자로만 이루어진 경우 True , 그외는 False 반환 

우리는 이 코드의 내부를 전혀 모르지만 Spec을 통해 테스트 가능합니다.

System.out.println(inputPassword("1234")); //  숫자로만 이루어짐 , true
System.out.println(inputPassword("asd")); // 영어로만 이루어짐 , false
System.out.println(inputPassword("a123")); // 문자 영어 섞임 , false
System.out.println(inputPassword("")); // 빈 문자열 , false


(지금은 출력을통해서 메서드의 반환값이 올바른지 확인했지만 실제로는 JUnit5 혹은 Assert를 사용하면 됩니다.)

WhiteBox Test

 public static int factorial(int n) {

     if (n < 0) {
            throw new IllegalArgumentException();
        }

        int result = 1;

        for (int i = 1; i <= n; i++) {
            result *= i;
        }
        return result;
    }

다음은 팩토리얼 연산을 하는 메서드입니다.

이 메서드의 Spec은 다음과 같습니다.

Requirments : 계산할 팩토리얼의 대상이 되는 0이상의 정수 
Effects : 정수의 팩토리얼 값 

Spec에는 정수값을 넣었을때

이 코드의 내부의 구현에 대한 지식이 없다면 다음과 같은 테스트만 작성 가능합니다.

try {
            factorial(-1);
        } catch (Exception e) {
            System.out.println("에러 체크");
}

매우 한정적인 테스트만 작성 가능함으로 WhiteBox Testing이 필요합니다.

assert factorial(5) == 120; // n이 양수인 경우 
assert factorial(0) == 0; // n이 0인 경우 

이 경우에 PreCondition 은 0이상의 정수를 의미하며
PostCondition은 올바른 팩토리얼 값을 의미합니다.

 

 

이 글에서는 Spec에 필요성 및 간단한 기초 활용법을 알아봤습니다.

 

이 글이 도움이 되셨으면 좋겠습니다.

Collection FramWork

자바는 Collection FramWork를 제공합니다

이 안에는 대부분의 자료구조가 존재합니다. List, Map, Set, Tree...

또 기본적으로 Collection은 Parameterized Type을 사용합니다.

 

Collection을 한마디로 정리하면 자료를 다루는 방법과 저장하는 구조가 다른 Abstract Data Type의 집합입니다.
따라서 용도에 따라 어떤 자료구조를 사용할 것인지 잘 이해했다면 Collection FrameWork를 이해했다고 볼 수 있습니다.

 

 

List

List Interface를 상속받는 구현체는 대표적으로 ArrayList, LinkedList 가 존재합니다.

둘다 같은 List를 상속받아 구현체로써 사용되지만 , 구현할떄 사용되는 Representation이 다릅니다.

이런식으로 Collection Interface를 상속받은 구현체는 구현시 사용되는 자료구조가 다르거나 정렬의 유무에서 차이가 나는 경우게 대부분입니다.

 

데이터 구조

  1. ArrayList 배열 사용
  2. LinkedList node구조 사용

이렇게 구조에서 차이가 나지만 실제 동작에서는 ArrayList가 할 수 있는 일은 LinkedList가 전부 할 수 있으며

LinkedList 가 할 수 있는 일은 ArrayList가 전부 수행 가능합니다.

 

그럼 왜 나눠서 쓰는 걸까요??
그냥 하나로 통일해서 쓰면 안될까요??

 

ArrayList는 배열을 사용합니다. 따라서 주소가 sequential하게 연결돼있습니다.

따라서 특정 index에 접근하는게 매우 빠릅니다 시작주소 + 3 하면 3번쨰 index에 접근 가능하게 되는 것이죠

반면에 원소를 더하거나 삭제하는 경우에는 배열의 요소들을 한 칸씩 뒤로 이동시키거나 앞으로 당겨야 하기 때문에 상당히 느린 연산을 수행하게됩니다.

 

LinkedList는 node 구조를 사용합니다 배열이 아님으로 주소가 sequential하게 연결돼있지 않습니다.
따라서 특정 index에 접근하는게 ArrayList에 비해 느립니다.

 

하지만 원소를 더하거나 삭제하는 연산을 할때 node들 사이에 연결만 이어주고 끊어주면 되기때문에 ArrayList보다 연산이 빠릅니다.

정리하자면 다음과 같습니다.

 

  • ArrayList : 리스트 크기 변경 안될때 , 데이터 접근이 빈번할 때
  • LinkedList : 리스트 크기 변경이 자주 일어날떄 , 요소의 삽입/삭제가 빈번할 때

이렇게 같은 List자료구조지만 구현돼있는 내부 구조가 다르기 떄문에 용도에 맞게 사용할 수 있어야 합니다.

 

 

Set

Set은 중복을 허용하지 않는 대표적인 자료구조입니다.

크게 HashSet , TreeSet 으로 나뉩니다.

Hash는 이름그대로 HashCode를 사용하며 Tree는 내부적으로 BinarySearchTree 구조를 사용합니다.

 

이점은 중복을 어떻게 확인할 것인지에 큰 차이가 생깁니다.

  • HashSet
    1. HashCode 비교
    2. Equals 비교
    3. 중복없음

 

  • TreeSet
    1. 요소들이 이미 정렬돼있음
    2. Tree를 순회하면서 직접 Element 비교 (Comparable Implements 필요)
    3. 중복없음

HashSet은 HashCode와 Equals만 이용하면 되기 떄문에 원소를 추가하는 연산이 빠릅니다.

반면에 TreeSet은 직접적인 순회를 해야하기 떄문에 HashSet에 비해 느립니다. 하지만 정렬된 상태를 유지하기 떄문에

 

정렬된 상태가 필요한 경우에는 TreeSet을 그렇지 않다면 HashSet쓰는게 일반적으로 유리합니다.

public class Main {

    public static void main(String[] args) {

        Set<Person> set = new HashSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(20, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }
}

class Person {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;

        return age == person.age && Objects.equals(name, person.name);
    }

//    @Override
//    public int hashCode() {
//        return Objects.hash(age, name);
//    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

}

Equals 구현 , HashCode 주석처리

따라서 중복체크를 똑바로 못합니다.

 

 

HashCode의 주석처리를 지워주면 중복체크가 정상적으로 작동합니다.

public class Main {

    public static void main(String[] args) {

        Set<Person> set = new TreeSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(20, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }
}

class Person implements Comparable<Person> {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public int compareTo(Person o) {

        return this.age - o.age;

    }
}

TreeSet은 해시코드와 Equals를 쓰지 않습니다.


대신 Comparable의 compareTo 를 사용합니다

지금은 age를 기준으로 compareTo가 구현이 돼있습니다.

public static void main(String[] args) {

        Set<Person> set = new TreeSet<>();

        set.add(new Person(20, "홍길동"));
        set.add(new Person(21, "홍길동"));

        for (Person p : set) {
            System.out.println(p);
        }

    }

이 코드에서는 나이가 다르면 이름이 같아도 다른 객체기 떄문에 2개가 들어갑니다.

 

Map

Map은 Key : Value 쌍으로 이루어져 있습니다.

여기서는 Key가 해시코드 역할을 합니다.

Map 자료구조도 대표적으로 HashMap , TreeMap 이 존재합니다.

또 내부적으로 HashMap은 배열과 연결리스트 기반 , Tree는 노드기반 구조를 사용합니다

기존과 똑같이 Hash는 HashCode , Equals 를 사용합니다.

public class Main {

    public static void main(String[] args) {

        Map<Person, Integer> map = new HashMap();

        map.put(new Person(20, "홍길동"), 1);
        map.put(new Person(20, "홍길동"), 2);


        for (Person p : map.keySet()) {
            System.out.println(p);
        }

    }
}

class Person {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Person person = (Person) o;

        return age == person.age && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(age, name);
    }


}

 

 

마찬가지로 TreeMap은 compareTo를 구현해 써야 합니다.

public class Main {

    public static void main(String[] args) {

        Map<Person, Integer> map = new TreeMap<>();

        map.put(new Person(20, "홍길동"), 1);
        map.put(new Person(20, "홍길동"), 2);


        for (Person p : map.keySet()) {
            System.out.println(p);
        }

    }
}

class Person implements Comparable<Person> {


    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }


    @Override
    public int compareTo(Person o) {
        return this.age - o.age;
    }
}

 

Collection FramWork를 이해하고 응용한다는 것은

자료구조를 용도에 맞게 문제 해결에 더 적합한 것으로 선택해서 사용할 수 있음을 의미합니다.

 

도움이 되셨다면 좋겠습니다.

Abstract Class Vs Interface

Abstract Class(추상클래스) 와 Interface(인터페이스)는 어떤 차이가 있을까요?

기능적인 차이점 부터 알아보겠습니다.

추상클래스

  1. 타입에 제한없는 변수
  2. 메서드 제한 없음
  3. extend KeyWord로 상속
  4. 다중 상속 불가능

인터페이스

  1. static final 상수 변수
  2. abstract method , default method
  3. implements KeyWord로 상속
  4. 다중 상속 가능

보다 다양한 차이점이 있지만 크게 4가지만 간추렸습니다.

 

여기서 중요한것은 단순히 기능적인 차이점이 아니라 어쨰서? 입니다

 

왜 이런 기능적인 차이가 있는걸까요?

지금부터 왜? 에 대해서 알아보겠습니다.

 

(이 글은 클래스의 Implementation과 Abstraction 개념을 알고있다는전제하에 작성된 글입니다.

따라서 관련된 내용이 궁금하신 분들은 다음을 참고해 주세요)

 

Abstraction(추상화) 기본 개념 - 1편

Abstraction(추상화)란? 자바에서는 추상클래스 , 추상메서드 , 추상화 등 "추상"이라는 말이 자주 쓰입니다. 또한 Abstract으로 선언된 클래스 , 메서드 등을 보고 추상화시켰다 합니다. 그렇다면 추상

masiljangajji-coding.tistory.com

 

클래스 설계와 Abstraction Barrier

클래스란 클래스란 특정한 타입을 지정하는것을 의미합니다. 우리는 이미 자바에서 제공하는 Primitive Type을 사용하고있습니다. (byte,short,int,long,folat,double,boolean,char) 이 Type은 각각 특정한 목적을

masiljangajji-coding.tistory.com

 

추상클래스 Vs 클래스

추상클래스는 기본적으로 클래스이기 떄문에 Implementation을 갖습니다.

또한 일반적인 클래스와 같이 특정한 타입을 정의하기 위해서 사용됩니다.

그럼 일반 클래스와 어떤 점이 다를까요?

일반 클래스와 차이점은 단 하나입니다. 이 타입이 필요한 연산을 구현하지는 않고 명시만 하여 간추린 것이죠

자바에서는 이를 Abstract method 라고 지칭합니다.

 

좀더 포괄적으로 이해하면 다음과 같습니다.

  • 일반클래스 : 이 타입은 Implementation을 갖고 이런 연산을 수행해 !
  • 추상클래스 : 이 타입은 Implementation을 갖고 이런 연산을 할수는 있어야 해 !

추상클래스는 미완성 설계도 , 미완성된 클래스 라고 이해하면 됩니다.

구현하지 않고 명시만 해놓은 연산을 extends받는 클래스에서 구현하여 추상클래스의 설계도를 완성시키는 것이죠

이점 외에는 클래스와 동일하기 때문에 당연히 변수와 메서드에 제한이 없습니다.

 

하지만 이것만으로 해결이 안되는 부분이 존재합니다 . 바로 다중상속이 불가능한 것입니다.

왜 클래스는 다중상속이 불가능 할까요??

이를 알아보겠습니다.

class Sparrow {
    int wing;
    int beak;
    int feather;
}

class Pigeon {
    int wing;
    int beak;
    int feather;
}

참새와 비둘기 타입을 정의했습니다.

참새와 비둘기 타입 모두 날개,부리,깃털을 가집니다.

또한 이것들은 각각의 타입에 대한 Implementation에 속합니다.

참새,비둘기 모두 Implementation을 갖기 떄문에 이 둘을 상속받는다면

두 클래스의 Implementation을 지켜줘야 합니다.

그렇다면 상속받은 클래스에서 사용되는 날개는 어떤 클래스의 날개일까요?


마찬가지로 상속받은 클래스에서 사용되는 깃털은 어떤 클래스의 깃털인가요??

또 부리를 이용해 불을 내뿜는 Dragon타입을 추가적으로 상속받는다면 어떤 클래스의 부리를 이용해 불을 내뿜을 건가요??

 

이런식으로 Implementation끼리의 충돌이 발생할 수 있습니다.

따라서 클래스는 원천적으로 다중상속이 불가능합니다.

그렇기 떄문에 상속할때도 extends(확장)라는 KeyWord를 사용합니다.

기존 클래스의 Implementation은 지키면서 추가적으로 살을 덧붙여 확장시켜 사용하라는 의미입니다.

동일한 논리로 미완성이기 댸문에 new를 통한 객체생성도 불가능 합니다.

 

인터페이스 Vs 추상클래스

앞서 말했듯 추상클래스는 Implementation과 연산에 대한 명시가 돼있는 클래스를 의미합니다.

그런데 이런 경우에는 어떨까요?

추상클래스의 추상화 정도가 점점 높아져 어떠한 필드변수도 갖지않고 연산에대한 명시만 돼있는 것입니다.

abstract class Dragon {

    abstract void fly(int wing);

    abstract void breath(int beak);

}

이 Dragon 추상클래스는 날수있는 기능과 불을뿜는 기능을 명시해놓은 명세서입니다.

별도의 Implementation을 갖지않는 클래스기 떄문에 다중상속이 되어도 문제가 발생하지 않습니다.

하지만 여전히 클래스의 범주에 속하기 떄문에 다중상속이 불가능합니다.

 

그래서 나온것이 바로 Interface입니다.

 

추상클래스의 추상화정도가 계속해서 증가하여 필드변수를 갖지않고 기능에대한 명시만 있을때는
다중상속의 문제점이 사라지기 떄문에 Interface라 지칭하고 다중상속이 가능하게끔 만든 것 입니다.

 

따라서 인터페이스는 Implementation을 갖지 않고 다중상속이 되어도 문제가없는 상수값과 기능에대한 명시만 가집니다.

 

같은 논리로 상속의 KeyWord도 extends가 아닌 implements(구현)을 사용합니다. 그럼 default method는 무엇인가요??

이것또한 인터페이스의 등장과 동일합니다.

인터페이스를 사용하다보니 어라?? 이런 연산은 다중상속되도 문제가 없을 것 같은데?? 라고 하는것을

default method로 만들어 놓은 것입니다.

 

이렇게 되면 해당 인터페이스를 구현한 클래스들이 그 메서드를 구현하지 않아도 되는 장점이 있습니다.

 

자바는 언어가 설계될떄 모든 연산을 클래스를 통해 해결이 가능하다 믿었습니다.
하지만 프로그래밍이 고도화 됨에 따라 요구사항이 증가했고 기존에 사용하던 클래스만으로는 극복불가능한 문제점들이 생기게 됐습니다.

 

따라서 클래스라는 타입이 갖는 태생적인 한계를 극복하기 위해 여러가지 기능을 덧붙여서 만들었다고 보면 될 것 같습니다.

 

도움이 되셨다면 좋겠습니다.

클래스란

클래스란 특정한 타입을 지정하는것을 의미합니다.

우리는 이미 자바에서 제공하는 Primitive Type을 사용하고있습니다. (byte,short,int,long,folat,double,boolean,char)

 

이 Type은 각각 특정한 목적을 가집니다.

  1. 정수를 표현하기 위한 byte,short,int ,long (1,2,4,8 byte로 표현가능한 크기가 다름)
  2. 실수를 표현하기 위한 float , double (4,8 byte로 표현가능한 크기가 다름)
  3. 참,거짓을 표현하기 위한 boolean
  4. 문자를 표현하기 위한 char

이렇게 타입은 특정한 목적을 가지고 설계됩니다.

 

우리가 만드는 클래스또한 타입을 정의하는 것으로 , 특정한 목적을 가지고 연산을 수행하게 됩니다.

따라서 "클래스를 작성한다"는 것은 '특정한 목적을 갖는 타입을 정의한다" 는 의미입니다.

클래스 설계

클래스를 만들떄 이 클래스를 정의하는 , 즉 근간이되는 것을 Implementation이라 합니다.

 

또한 Implementation은  Abstraction Barrier로 보호되어야 합니다. 유리수를 예시로 들겠습니다.

public class Main {

    public static void main(String[] args) {

        RationalNumber rationalNumber = new RationalNumber(1, 2);

        System.out.println(rationalNumber.numerator + "/" + rationalNumber.denominator);

    }
}

class RationalNumber {

    int numerator; // 분자
    int denominator; // 분모

    public RationalNumber(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

}

이 클래스는 유리수라는 타입을 지정한 것입니다.

 

유리수의 정의는 정수들의 비로 나타낼수 있는 수를 의미합니다.

 

따라서 2개의 정수를 받아야 하며 이 정수가 유리수를 정의하기 떄문에 Implementation에 속합니다.

하지만 이 클래스는 치명적인 문제점을 갖습니다.

rationalNumber.denominator=5;
System.out.println(rationalNumber.numerator + "/" + rationalNumber.denominator);

이런식으로 언제든지 rationalNumber의 필드변수에 접근하여 값을 바꿀수 있습니다.

 

이렇게 된다면 기존에 정의했던 유리수에서 다른 유리수로 변경되게 됩니다.

 

이렇게 Implementation에 속하는 것들은 의도치 않게 변경된다면 기존의 정의와는 아예 달라지기 떄문에

Abstraction Barrier로 보호할 필요가 있습니다.

public class Main {

    public static void main(String[] args) {

        RationalNumber rationalNumber = new RationalNumber(1, 2);

        System.out.println(rationalNumber.getNumerator() + "/" + rationalNumber.getDenominator());

        //rationalNumber.denominator = 5; 오류발생 !

    }
}

class RationalNumber {

    private int numerator; // 분자
    private int denominator; // 분모

    public RationalNumber(int numerator, int denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public int getDenominator() {
        return denominator;
    }

}

이 코드는 유리수를 정의하는 정수를 Abstraction Barrier로 감싼것으로
클래스의 필드변수에 바로 접근하는 것이 아니라 getter를 사용해 우회적으로 접근하게 합니다.

(getter또한 Implementation 에 속합니다.)

 

 

하지만 여전히 치명적인 문제점을 갖습니다. 유리수에 정의에 대해서 누락된 것들이 있기 떄문입니다.

  1. 유리수는 분모에 0이 들어올수 없다.
  2. 분모 분자의 비가 같다면 동일한 유리수다.
  3. 정수가 음수로 들어왔을때의 처리

이 조건들 또한 충족시킬수 있도록 보완하겠습니다.

 

1. 유리수는 분모에 0이 들어올수 없다.

RationalNumber rationalNumber = new RationalNumber(1, 0);
System.out.println(rationalNumber.getNumerator() + "/" + rationalNumber.getDenominator());

유리수는 분모에 0이 들어올수 없음에도 다음과 같이 동작하는 것을 볼 수 있습니다.

따라서 타입 정의에 맞게 바꿔주겠습니다.

public RationalNumber(int numerator, int denominator) {

        if (denominator == 0) {
            throw new IllegalArgumentException("올바른 분모 입력이 아닙니다");
        }

        this.numerator = numerator;
        this.denominator = denominator;
    }

생성자를 통해 객체가 만들어질떄 분모에 대한 제한조건을 거는 것 입니다.

이는 PreCondition을 체크하는 것과 동일합니다.

 

2. 분모 분자의 비가 같다면 동일한 유리수다.

RationalNumber rationalNumber = new RationalNumber(1, 2);
RationalNumber rationalNumber2 = new RationalNumber(2, 4);

System.out.println(rationalNumber.equals(rationalNumber2));

유리수에서 1/2와 2/4는 동일한 비를 같기떄문에 같은유리수지만 실행결과는 false인 것을 알 수 있습니다.

따라서 다음과 같이 수정하겠습니다.

 

public RationalNumber(int numerator, int denominator) {

        if (denominator == 0) {
            throw new IllegalArgumentException("올바른 분모 입력이 아닙니다");
        }

        this.numerator = numerator;
        this.denominator = denominator;

        nomarize();
    }


private void nomarize() {
        int number = gcd(this.numerator, this.denominator);

        this.numerator = this.numerator / number;
        this.denominator = this.denominator / number;
    }

    private int gcd(int number1, int number2) {

        if (number2 == 0) {
            return number1;
        }

        return gcd(number2, number1 % number2);
    }

@Override
public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        RationalNumber that = (RationalNumber) o;
        return numerator == that.numerator && denominator == that.denominator;
    }

2번의 조건을 만족하기 위해 유리수를 기약분수의 형태로 만들어줬습니다.

 

기약분수형태로 만들기 위해 최대공약수를 찾는 gcd 알고리즘을 추가한후 nomarize를 통해 기약분수 꼴로 변경합니다.

이제 1/2 와 2/4 를 비교시 true 값이 나옵니다.

 

 

3. 정수가 음수로 들어왔을때의 처리

RationalNumber rationalNumber3 = new RationalNumber(-1, 2);
System.out.println(rationalNumber3.getNumerator() + "/" + rationalNumber3.getDenominator());

음수가 들어왔을때 출력되는 형식이 다른것을 알 수 있습니다.

따라서 이 양식을 맞춰주도록 하겠습니다.

private void nomarize() {

        int number = gcd(this.numerator, this.denominator);

        this.numerator = this.numerator / number;
        this.denominator = this.denominator / number;

        if (this.denominator < 0) {
            this.denominator *= -1;
            this.numerator *= -1;
        }

    }


@Override
    public String toString() {
        return numerator + "/" + denominator;
    }

이제부터는 음수가 들어갔을떄 모두 동일한 형식을 갖게 됩니다.

 

추가적으로 toString 메서드를 Override해 출력에 편의성을 증가시켰습니다.

지금은 getter를 이용한 다른 연산이 없기 떄문에 getter또한 삭제 가능합니다.

  public static void main(String[] args) {

        RationalNumber rationalNumber3 = new RationalNumber(-1, 2);
        RationalNumber rationalNumber4 = new RationalNumber(1, -2);
        RationalNumber rationalNumber5 = new RationalNumber(-16, -32);

        System.out.println(rationalNumber3);
        System.out.println(rationalNumber4);
        System.out.println(rationalNumber5);
        System.out.println(rationalNumber3.equals(rationalNumber4));
    }

이렇게 유리수 클래스를 설계해봤습니다.

 

클래스를 만든다는 것은 특정한 타입을 정의하는 것으로  이 타입을 만든 목적에 맞게 설계해줘야 합니다.

또한 이 타입의 가져서는 안되는 값들에 대해서도 충분한 고민이 필요합니다.

 

이외에도 간략하게 자동차 클래스를 설계한다면

 

자동차를 정의내리는 바퀴,핸들,이름 뿐만 아니라
자동차라는 타입이 갖는 필수적인 연산들 (가속 , 브레이크 등등)을 Implementation으로 두고 

타입을 설계해 나가면 될 것입니다. 

 

도움이 되셨다면 좋겠습니다.

+ Recent posts