튜터로 학생들의 알고리즘 코드를 봐 주다가 다음 문제를 마주쳤다.

https://www.acmicpc.net/problem/1072

 

문제만 본다면 평범한 알고리즘 문제이지만, 다음 과정에서 문제가 있었다.

백준은 어떤 부분이 틀렸는가에 대해 제공하지 않기에 학생들은 스스로 반례 또는 틀린 코드를 찾고 있다가, 어느 코드가 틀린지 몰라 나에게 도움을 얻으러 왔다.

나도 분명히 이분탐색 로직에는 문제가 없는것 같아 어느 부분에서 틀렸지 찾다가, 다음을 바꿔보니 맞는 코드가 되었다.

int percentage1 = (int)((double)x * 100 / y);
int percentage2 = (int)((double)x / y * 100);

어떤 수식이 틀렸고, 어떤 수식이 맞을까?

부동소수점 이슈는 소수점 계산할 때 항상 나오는 이슈라 항상 알고는 있지만, 실제로 문제를 복기하며 무엇이 틀렸는지 찾을때는 부동소수점 이슈를 발견하기가 굉장히 어렵다.

마치 long type을 써주지 않아 overflow가 났을 때처럼.

일단, 문제를 해결하기 위해 어느 부분에서 문제가 터졌는지 확인하기 위해 살짝 코드를 짜 보았다.

public class Main {
    public static void main(String[] args) throws IOException {
        for (int x=0; x<100; x++){
            for (int y=1; y<100; y++){
                int z1 = (int)((double)x * 100 / y);
                int z2 = (int)((double)x / y * 100);
                if (z1 != z2 && x < y){
                    System.out.println("z1 = " + z1);
                    System.out.println("z2 = " + z2);
                    System.out.println("x = " + x);
                    System.out.println("y = " + y);
                }
            }
        }
    }
}

x와 y를 변환시켜 주면서, 위의 두 수식이 달라질 때를 찾았다.

그리고, 다음 결과를 발견할 수 있었다.

z1 = 58
z2 = 57
x = 29
y = 50

x = 29, y = 50일 때, z1는 58%를, z2는 57%를 반환했다.

정확하게 보기 위해 z1과 z2를 double로 바꾸었더니, x=29, y=50에 대해 다음을 반환했다.

z1 = 58.0
z2 = 57.99999999999999

상식상 29/50은 58%가 되어야 하기에, z1이 맞는 수식임을 알 수 있다.

따라서, 퍼센테이지를 나타낼 때 분자를 크게 가져가야만 소숫점을 잘 출력하는 것을 알 수 있다.

분자가 더 큰 경우에도 마찬가지다.(x>y)

9900가지 경우 중 39건의 경우에서 다른 결과를 반환했다.

이 경우에도 z1이 z2에 비해 조금 더 정확한 결과를 도출해 냄을 알 수 있다.

따라서 결과는 다음과 같다.

퍼센테이지 등의 결과를 도출해 낼 때에는, 항상 분자를 크게 가져가는것이 조금 더 정확하다.

글을 쓰다보니 사실 어떻게 생각해보면 당연한 결과긴 하지만, 그래도 오랜만에 한번 짚고 넘어갈 문제라 포스팅을 해 보았다.

https://www.acmicpc.net/problem/3009

문제를 풀다 새로운 것을 이해했다.

public class Main {
    public static void main(String[] args){
        Integer A = 3;
        Integer B = 3;
        System.out.println("A == B ? "+(A==B));
        Integer C = 128;
        Integer D = 128;
        System.out.println("C == D ? "+(C==D));
    }
}

두개의 결과는 모두 참일까? 그렇지 않다.

첫 번째의 비교는 True, 두 번째의 비교는 False가 나온다.

늘 자바에서 문자열이 아닐 때에는 ==비교를 했는데, 이상하게도 결과가 false가 나왔다.

 

왜 그럴까?

자바에서 int는 primitive type이고, Integer는 객체다.

그래서 primitive type끼리는 ==를 해도 되지만, 객체끼리의 비교는 ==을 쓰면 안된다.(String에서 ==를 안 쓰고 equals를 쓰는 이유와 동일하다.)

그런데, 자바의 내부 동작 구조 원리상 Integer는 -128 ~ 127까지만 동일 객체의 캐시를 가져다 쓰기 때문에

그 전과 그 후의 값들은 주소값이 달라 ==비교가 불가능한 것이다.

 

 

 

참고)

https://romcanrom.tistory.com/177

 

java/ Integer를 '==' 연산자가 아닌 'equals' 메서드로 비교해야 하는 이유 (Integer Cache)

Integer 값을 비교할 때 'equals' 메서드를 사용하지 않고 '==' 연산자를 사용해 코드를 작성한 경우, 일부 값은 정확하게 비교가 되는 반면 일부 값에 대해서는 같은 값임에도 false를 반환하는 경우가

romcanrom.tistory.com

 

자바를 이용해 프로그래밍을 하거나 PS를 하게 되면

코드 중에 제목의 3가지를 이용해 반복문을 빠져나가거나 행동을 종료하는 때가 있다.

break를 쓰기도 하고, continue를 쓰기도 하고, return을 쓰기도 한다.

갑자기 문득 이건 왜 쓸까 하고 궁금했는데, chat gpt의 도움을 받아 작성한다.

 

1. break

반복문(for, while, do-while) 및 switch문에서 사용된다.

for (int i=0; i<5; i++){
    if (i==3){
          break;
    }
    System.out.println(i);
}

//=======결과=====
//0
//1
//2
//=================
switch(text){
    case "break":
        break;
    default:
        System.out.println(text);
        break;
}

break를 만나면 반복문의 조건을 만족하건 말건 즉시 루프를 탈출한다.

 

2.continue

continue는 반복문 안에서도 사용된다.

반복문을 완전히 빠져나가지 않고, 다음 루프로 이동한다.

for (int i=0; i<5; i++){
    if (i==3){
          continue;
    }
    System.out.println(i);
}

//=======결과=====
//0
//1
//2
//4
//=================

3.return

'메소드 내부'에서 '메소드'가 반환하는 값을 지정하고 메소드 실행을 종료한다.

return을 만나면 '메소드'가 즉시 종료된다.

 

디자인 패턴 꼭 써야 한다

MVC 모델

한 화면에서 모든 것을 처리하는 ASP(Active Server Page : PHP 등)에서 J2EE 패턴을 사용하기 위해서는 MVC모델을 몰라서는 안된다. 각 로직(모델, 뷰, 컨트롤러)별 클래스를 각각 만들어서 개발하는 모델이다.
이를 스윙이나 SWT 기반의 2-tier Application의 관점에서 접근해보자.

2-tier application이란?

  • 같은 역할을 하는 서버들을 같은 tier에 있다고 말한다.
  • 2 tier란 ec2같은 웹 서버가 DB에 연결 되어 있는 형태를 말하며 보통 트래픽이 많지 않은 경우 사용된다.
  • 한 클라이언트에 한 서버가 배분된다.

2-tier vs 3-tier

  • 2-tier에서 was서버가 추가된 형태를 3-tier라고 하며, 백엔드용 서버가 따로 존재한다. 백엔드용 서버가 존재하기에 2-tier보다 무거운 작업을 처리하기에 좋지만, 구조가 복잡한 단점이 있다.

모델 - 뷰 - 컨트롤러의 관계

  • 뷰는 사용자가 결과를 보거나 입력할 수 있는 화면이라고 생각하면 된다. 이벤트를 발생시키고, 이벤트의 결과를 보여주는 역할을 한다.
  • 컨트롤러는 뷰와 모델의 연결자로, 뷰에서 받은 이벤트를 모델로 연결하는 역할을 한다.
  • 모델은 뷰에서 입력된 내용을 저장, 관리, 수정하는 역할을 한다.

JSP는 모델 1과 모델2로 나누어져 있는데,

  • 모델 1은 JSP에서 자바 빈을 호출하고, 데이터베이스에서 정보를 조회, 등록, 수정, 삭제 업무를 한 후 결과를 브라우저로 보내 주는 방식이다. 간단하지만 추후 수정이 굉장히 어렵다.
  • 모델 2는 MVC 모델을 정확히 따른다. 모델 1에서 servlet이 컨트롤러의 역할을 수행하면서, MVC 패턴을 완성시킨다.
    보통 3티어 JSP모델은 모델1, 모델2를 사용하고, 웹이 아닌 2티어 구조에서는 MVC를 사용한다.

J2EE 디자인 패턴이란 무엇인가?

'J2EE 디자인 패턴'이라는 용어가 생소할 수 있다. 한 단어씩 띄워서 생각해보자.
먼저, 패턴(pattern)이란, 무엇인가를 만들기 위한 모델이나 가이드, 설명의 집합을 의미한다.
즉, 시스템을 만들기 위해 전체 중 일부 의미 있는 클래스들을 묶은 각각의 집합을 디자인 패턴(design pattern) 이라고 한다. 그리고 스프링이 있기 전 사용되었던 프레임워크인 J2EE 프레임워크의 이름을 따서 J2EE 디자인 패턴이라고 하는 것이다.

디자인 패턴의 종류

사용자의 요청이 처리되는 순서대로 티어를 나눈다면,
프레젠테이션 - 비즈니스 - 인테그레이션 티어 순으로 나눌 수 있겠다. 프레젠테이션 티어쪽으로 갈 수록 View에 가깝고, 인테그레이션 티어는 DB와 같은 repository에 가깝다고 생각하면 이해하기 쉽다.
Core J2EE patterns를 보면서, 몇 가지 패턴들을 살펴볼 수 있겠으나,
디자인 패턴은 공부하고 실제 사용하기 전까지는 너무 이론적인 느낌이라 지금 포스팅하지는 않겠다.
그 중, 대표적인 패턴 두가지만 알고 가보자.

Transfer Object 패턴

VO(Value Object)라고도 불리는 Transfer Object는 데이터를 전송하기 위한 객체에 대한 패턴이다.
DTO를 잘 만들어 놓으면, 내가 원하는 정보만 넘겨줄수도 있고, 각 source에서 일일히 null체크를 할 필요가 없기 때문에 오히려 더 편해진다.
또한 toString을 오버라이딩하여 객체를 보다 쉽게 구분할 수 있다.

implements Serializable
Serializable을 구현할 때 반드시 구현해야 하는 메서드가 있는 것도 아니고, 변수가 존재하는 것도 아니다. 그러나 Serializable을 구현하면 객체를 직렬화할 수 있다. 다시 말해 서버 사이의 데이터 전송이 가능해지고, 원격지 서버에 데이터를 전송하거나, 파일로 객체를 저장할 경우에는 이 인터페이스를 구현해야 한다.

Service Locator 패턴

Service Locator 패턴은 예전에 많이 사용되었던 EJB의 EJB Home 객체나 DB의 DataSource를 찾을 때 소요되는 응답 속도를 감소시키기 위해 사용된다. cache에 어떤 객체를 찾은 결과를 보관하고 있다가, 누군가 그 객체를 필요ㅎ로 할 때 메모리에서 찾아서 제공하고, 해당 객체가 cache에 없으면 메모리에 들어가서 찾는 패턴이다.

정리

DTO가 왜 필요한지, 무엇인지는 알고 있었지만, 이 또한 디자인 패턴이라는 것은 알지 못했다.
어쩌면 내가 무의식적으로 알고 있는 모든 것이 디자인 패턴의 일종일지도 모르겠다는 생각을 했다.
패턴을 한번씩 더 찾아보면서 Read Only인지, R/W가 가능한지에 따라 VO와 DTO로 나뉜다는 글을 보며 예전의 중구난방식의 개발에서 조금 더 디자인 패턴이 가미된 개발 문화로의 전환이 이루어지고 있음을 느꼈다.
또한 JSP등의 ASP에서 전문적인 뷰단으로의 분화가 이루어졌기 때문에(React, Vue 등) 점점 더 프레젠테이션 티어쪽으로의 협업과 관련된 디자인 패턴보다는, 내부에서 lookup이나 처리속도를 조금이라도 더 빠르게 할 수 있는 디자인 패턴이 중요해진 것 같다.

내가 만든 프로그램의 속도를 알고 싶다.

성능이 느릴 때는 병목 지점이 어디인가?를 파악하는 것이 가장 중요하다. APM이나 프로파일링 툴을 이용하여 분석하는 곳도 있고, 손수 디버깅을 하는 곳도 있다.

프로파일링 툴 vs APM

간단히 설명하자면, 프로파일링 툴은 개발자용 툴이고, APM 툴은 운영 환경용 툴이라고 할 수 있다. 표를 보면서 비교해 보자.

구분 특징
프로파일링 툴 - 소스 레벨의 분석을 위한 툴이다.
- 애플리케이션의 세부 응답 시간까지 분석할 수 있다.
- 메모리 사용량을 객체나 클래스, 소스의 라인 단위까지 분석할 수 있다.
- 가격이 APM툴에 비해 저렴하다.
- 보통 사용자수 기반으로 가격이 정해진다.
- 자바 기반의 클라이언트 프로그램 분석을 할 수 있다.
APM 툴 - 애플리케이션의 장애 상황에 대한 모니터링 및 문제점 진단이 주 목적이다.
- 서버의 사용자 수나 리소스에 대한 모니터링을 할 수 있다.
- 실시간 모니터링을 위한 툴이다.
- 가격이 프로파일링 툴에 비해 비싸다.
- 보통 CPU수를 기반으로 가격이 정해진다.
- 자바 기반 클라이언트 프로그램 분석이 불가능하다.

프로파일링 툴은 느린 메서드나 느린 클래스를 찾는 것을 목적으로 하지만, APM 툴은 목적에 따라 용도가 상이하다.
프로파일링 툴은 다음과 같은 기능을 공통적으로 제공한다.

  • 응답 시간 프로파일링 기능
    하나의 클래스 내에서 사용되는 메서드 단위의 응답 시간을 측정하고, 툴에 따라 소스 라인 단위로 응답 속도를 측정한다.

좋은 객체지향설계의 5가지 원칙 : SOLID

  • SRP : 단일 책임 원칙(single responsibility principle)
  • OCP : 개방-폐쇄 원칙(Open/closed principle)
  • LSP : 리스코프 치환 원칙(Liskov substitution principle)
  • ISP : 인터페이스 분리 원칙(Interface segregation principle)
  • DIP : 의존관계 역전 원칙(Dependency inversion principle)

ISP : 인터페이스 분리 원칙

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
로버트 C. 마틴(밥아저씨)
  • 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
  • 예를 들어 자동차 인터페이스 → 운전 인터페이스, 정비 인터페이스 등으로 분리
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.
  • 인터페이스는 여러개를 상속받을 수 있기 때문에 좋은 코드를 만들 수 있다.
    • 그러나 너무 분리하면 코드를 다 까봐야해서 어느정도 선까지 구체화할지, 추상화할지를 정해야 한다.

 

ISP 원칙을 지키지 못한 계산기를 살펴보자.

public class Calculator {
    public int calculate(AbstractOperation operation, int firstNumber, int secondNumber){
        return operation.operate(firstNumber, secondNumber);
    }
}

계산기 클래스

// 연산 결과만 출력
public class DisplayTypeA extends Calculator implements Display {

    @Override
    public void displayResult(AbstractOperation operation, int firstNumber, int secondNumber) {
        int answer = operation.operate(firstNumber, secondNumber);
        System.out.println(answer);
    }

    @Override
    public void displayResultWithOperator(AbstractOperation operation, int firstNumber, int secondNumber) throws Exception {
        throw new Exception("동작 불가");
    }
}

DisplayA 클래스 : 결과만 출력하고 싶다.

// 연산 과정을 포함한 출력
public class DisplayTypeB extends Calculator implements Display {

    @Override
    public void displayResult(AbstractOperation operation, int firstNumber, int secondNumber) throws Exception {
        throw new Exception("동작 불가");
    }

    @Override
    public void displayResultWithOperator(AbstractOperation operation, int firstNumber, int secondNumber) {
        int answer = operation.operate(firstNumber, secondNumber);
        String operator = operation.getOperator();
        System.out.println(firstNumber + " " + operator + " " + secondNumber + " = " + answer);
    }
}

DisplayB 클래스 : 연산 과정까지 출력하고 싶다.

public class Client {
    public static void main(String[] args) throws Exception {
        // 연산 결과만 출력
        DisplayTypeA displayTypeA = new DisplayTypeA();
        displayTypeA.displayResult(new AddOperation(), firNum, secNum);
        // displayTypeA.displayResultWithOperator(new AddOperation(), firNum, secNum); // Error 발생

        // 연산 과정까지 출력
        DisplayTypeB displayTypeB = new DisplayTypeB();
        displayTypeB.displayResultWithOperator(new AddOperation(), firNum, secNum);
        // displayTypeB.displayResult(new AddOperation(), firNum, secNum); // Error 발생
    }
}

클라이언트 클래스

public abstract class AbstractOperation {
    public abstract int operate(int firstNumber, int secondNumber);
    public abstract String getOperator();
}

AbstractOperation 클래스

public class AddOperation extends AbstractOperation {
    @Override
    public int operate(int firstNumber, int secondNumber) {
        return firstNumber + secondNumber;
    }

    @Override
    public String getOperator() {
        return "+";
    }
}

AddOperation 클래스

 

문제점

1. 필요하지 않은 기능을 강제로 구현해야 한다.

  -> DisplayType 클래스들은 전부 Calculator의 기능을 이용하지 않는데도 Calculator를 상속받고, Display Interface 내부의 두가지 추상 메서드를 구현해야만 한다.

2. 필요하지 않은 혹은 사용 못 하는 기능이 강제로 구현되어 사용하지 못하도록 예외처리를 했다.

 -> Display 인터페이스에 DisplayResult와 DisplayWithoutResult 메서드가 공존하다보니 Display 메서드를 implement하면 쓰고싶지 않은 기능에다 Exception을 추가해줬다.

 

 

다시 돌아가서

ISP를 위반한 계산기에서는 무엇을 해 줘야 할까? 답은 다음과 같다.

  • 필요하지 않은 기능을 강제로 구현하지 않도록 인터페이스를 분리한다.
  • 연산 결과를 보여주는 방법마다 인터페이스를 구현합니다.

 

어떻게?

사용하지 않는 Calculator 클래스는 지운다.

Display 인터페이스를 DisplayWithResult, DisplayWithoutResult로 쪼갠 뒤 그 안에 각각 추상 메서드를 작성하고

WithResult, WithoutResult를 각각 Impl받는 클래스들에서 구현한다.

 

쉬운 예제였지만 예제가 어려웠거나 해답이 근거리에 없었다면 힘들었을만한 예제였다.

 

좋은 객체지향설계의 5가지 원칙 : SOLID

  • SRP : 단일 책임 원칙(single responsibility principle)
  • OCP : 개방-폐쇄 원칙(Open/closed principle)
  • LSP : 리스코프 치환 원칙(Liskov substitution principle)
  • ISP : 인터페이스 분리 원칙(Interface segregation principle)
  • DIP : 의존관계 역전 원칙(Dependency inversion principle)

LSP : 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.
로버트 C. 마틴(밥아저씨)
  • 하위 클래스의 인스턴스는 상위형 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.
  • 프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 단순히 컴파일에 성공하는 것을 넘어서서, 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 그래서 기능적으로 보장이 되어야 한다는 것을 말한다.
  • 하위 클래스의 인스턴스는 상위형 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.

자바에서는 Collection Framework라는게 있다.

Collection 안에는 Map, List, Set이 각각의 장단점과 특징을 가지고 존재하고 있고,

Map 안에는 HashMap, TreeMap 등이

List 안에는 ArrayList, LinkedList 등이

Set 안에는 TreeSet, HashSet 등이 있어 설계의 방향성을 가지고 내가 원하는 대로 구현할 수 있다.

Map, List, Set은 Collection의 공통 메서드들을 일관되게 수행할 수 있어야 하고,

Map map = new HashMap();과 같이 하위 인스턴스는 상위형 인스턴스의 역할을 하는 데 문제가 없다.

 

다시 돌아가서, 이번에는 LSP 원칙을 지키지 못한 계산기를 살펴보자.

public class Calculator {
    public int calculate(AbstractOperation operation, int firstNumber, int secondNumber) {
        if(operation instanceof DivideOperation){
            if(secondNumber == 0){return -99999;}
        }
        return operation.operate(firstNumber, secondNumber);
    }

계산기 클래스

public class DivideOperation extends AbstractOperation {
    public int operate(int firstNumber, int secondNumber) {
        return firstNumber / secondNumber;
    }
}

나누기 연산 클래스

 

문제점

무슨 문제가 있을까?

Calculator 안에는 사칙연산을 할 수 있는 연산이 모두 들어가 있는데, 한가지 문제점이 있다.

'0'으로 나누면 ArithmeticException이 터진다는 것.

그래서 계산기 클래스에서 instanceOf로 일일히 타입을 확인하고 있고, 자식이 완전히 부모클래스의 인스턴스 역할을 수행한다고 볼 수 없는 것이다.

 

해설

9조에 있는 성민이, 선연이, 성재, 영준이가 있다.

사람이니까 무조건 밥은 먹고 살아야 하지만, 그중 제일 악질인 영준이는 편식을 심하게 해서 고기가 아니면 안 먹는다고 한다. 그래서 영준이를 위한 편식 코드를 9조 전체에 넣어 다음과 같이 나타냈다.

이를 DIP를 위반한 코드로 나타내면 다음과 같다.

Abstract class 9조{
	abstract void eatVegetable(Person p){
	if (p instanceOf '영준') System.out.println("현기증 나요, 빨리 고기 주세요");
	else System.out.println("맛있게 먹겠습니다.");
        }
}

애꿎은 편식충 영준이때문에 불행하게도 성민, 선연, 성재는 밥을 먹을때마다 편식을 하는지 검사를 당해야 한다.

혹은 영준이만 고기를 먹는다고 영준이에 맞춰서 코드를 짜버리면(추후 업데이트 예정) 계속 다른 사람들은 의도하지 않았던 예외처리를 당하게 된다.

 

다시 돌아가서

LSP를 위반한 계산기에서는 무엇을 해 줘야 할까? 답은 다음과 같다.

  • 부모 와 자식 클래스 사이의 행위가 일관성이 있도록 추상화를 좀 더 정교하게 구현한다.
  • 연산 기능을 추상화한 부모 클래스에 피연산자 값의 유효성 검사를 진행하는 메서드를 추가해 준다.
  • 계산기 클래스에서는 이 메서드를 사용하여 유효성 검사를 진행하고 이 유효성 검사가 필요한 자식 클래스에서는 이 추가된 유효성 검사 조건을 구체화한다.

TROUBLESHOOTING

불효자 나눗셈 계산기에는 불행하게도 0으로 나누면 ArithmeticException이 터지는 특징이 존재한다.

이를 방지하고자 isValid 메서드를 만들어주기로 했다. 그러나 고민했던 부분은 대체 이 isValid 메서드를 어디서 구현하고, 어디서 사용해야하는지가 문제였다.

1) Calculater 자체에서 isValid 메서드로 구현하기

2) AbstractOperation에서 isValid 메서드 구현하고 하위 클래스에서 isValid 체킹해주기

3) 어짜피 DivideOperation에서만 사용하니 DivideOperation에서 isValid 구현해~버리기

정말 오래 고심한 결과 다음과 같은 해결방법이 나왔다.

  1. AbstractOperation에서 isValid를 true로만 return하기
  2. DivideOperation에서 오버라이딩으로 isValid 구현하기
  3. Calculater에서 isValid 메서드로 유효성 검사하기

그래서 다음과 같은 결과물이 나왔다.

abstract class AbstractOperation {
    public boolean isValid(int firstNumber, int secondNumber){return true;}
    public abstract int operate(int firstNumber, int secondNumber);
}

AbstractOperation에서는 추상메서드인 operate와 true값을 반환하는 isValid메서드를 구현해놓는다.

isValid는 왜 추상화 안시켰죠? 라고 할 수 있는데, isValid를 추상화시키면 AbstractOperation을 상속받는 모든 메서드에서 무조건적으로 오버라이딩 해서 구현해줘야 하기 때문에 번거롭다.

class DivideOperation extends AbstractOperation {
    @Override
    public int operate(int firstNumber, int secondNumber) {
        return firstNumber/secondNumber;
    }
    @Override
    public boolean isValid(int firstNumber, int secondNumber) {
        if (secondNumber==0) return false;
        return true;
    }
}

DivideOperation에서는 isValid를 오버라이딩하여 메서드를 구체화시킨다. 

isValid에서 

class Calculator {
    // 연산 기능을 추상화된 부모클래스에 의존하여 처리한다.
    public int calculate(AbstractOperation operation, int firstNumber, int secondNumber) {
        if (!operation.isValid(firstNumber, secondNumber)) return -99999;
        return operation.operate(firstNumber, secondNumber);
    }
}

마지막으로 Calculator에서는 isValid 조건만을 이용하여 유효성 검사를 진행해준다.

 

이렇게 되면 새로운 연산 클래스를 만들어도 isValid를 구현해서 새로 만들어 주기만 하면 되니 만사OK다.

이 구현방법을 생각하고 너무나 기뻤다. 이래서 개발을 하는가 싶을 정도로.

인터넷에서는 대부분 Interface만을 이용하는 방식을 사용하기도 했고,

Abstract -> Divide -> Calculator가 아닌 2단 방식을 이용해 어떻게 해야 하나 정말 고민했는데

정말 오래 기억나는 순간이 될 것 같다.

 

OOP 핵심정리

  • 캡슐화
  • 상속
  • 추상화
  • 다형성

OOP의 장점

  • 유지보수 Easy
  • 재사용성 ⬆️
  • 중복제거 Easy

OOP 작성 순서

  • 클래스 생성 (설계도 작성)
  • 인스턴스 생성 (제품 생성)
  • 인스턴스 사용 (제품 사용)

클래스 : 변수를 정의한 것

  • 설계도
  • 변수 + 메서드
  • 사용자 정의 타입

변수 : iv의 집합 (속성+기능)

메서드 : 명령문의 집합

선언 위치에 따른 변수의 종류

클래스 영역

  • class variable : 인스턴스 생성 없이 사용가능. 클래스가 메모리에 로드되었을 때 생성.
  • instance variable : 인스턴스 생성 후 사용가능. 인스턴스가 생성되었을 때 생성.

메서드 영역

  • local variable : 메서드 종료시 사라짐. 변수 선언문이 수행되었을 때 생성.

메서드 : 값을 받아서 처리하고 결과를 리턴한다

메서드의 장점

  1. 재사용성 ⬆️
  2. 중복제거 Easy
  3. 관리, 이해 Easy

매개변수의 유효성 검사 : 유효하지 않은 값이 들어오면

  1. 값 보정
  2. return문으로 작업 중단

기본형 매개변수와 참조형 매개변수

기본형 매개변수 : Read Only

참조형 매개변수 : Read & Write

JVM의 메모리 구조

Method Area : 클래스에 대한 정보를 저장하는 공간

Call Stack : 메서드 수행 시 필요한 메모리를 제공하는 공간, 메서드 종료 시 할당된 메모리 반환

Heap : 인스턴스가 생성되는 공간

인스턴스 메서드와 클래스 메서드

인스턴스 메서드 : 인스턴스 생성 후, 인스턴스 멤버(iv, im)와 관련된 작업을 하는 메서드

클래스 메서드 : 인스턴스 생성 없이, 인스턴스 멤버와 관련없는 작업을 하는 메서드. iv 사용 불가

오버로딩 : 이름이 같은 메서드를 여러번 정의한 것

오버로딩의 조건

  1. 이름이 같아야 한다.
  2. 매개변수의 타입 / 개수가 달라야 한다.
  3. 반환 타입은 상관없다.

오버라이딩 : 상속받은 메서드를 자손 타입에 맞게 내용을 변경한 것

오버라이딩의 조건

  1. 선언부가 일치해야 한다
  2. 접근 제어자를 조상보다 좁게 설정할 수 없다.
  3. 예외를 조상보다 많이 선언할 수 없다.

생성자 : 인스턴스 생성시마다 호출되는 인스턴스 초기화 메서드

생성자의 조건

  1. 생성자 이름 == 클래스 이름
  2. 리턴값 존재하지 않는다(그래서 앞에 void 자동 생략됨)
  3. 모든 클래스는 반드시 한 개 이상의 생성자를 갖는다.

생성자 this()

  • 같은 클래스에서 다른 생성자를 호출할 때 클래스 이름대신 this() 선언
  • 반드시 첫 줄에서 호출해야 한다.

참조변수 this

  • lv와 iv의 이름이 같을 때 구별하려고 사용
  • 인스턴스 자신을 가리키는 참조변수
  • 인스턴스 메서드 내에서만 사용가능

변수의 초기화 순서 : cv ⇒ iv

  1. 자동 초기화 (0)
  2. 명시적 초기화(간단 초기화, ())
  3. 초기화 블럭 (복잡 초기화, {})
  4. 생성자 (복잡 초기화, {})

상속 ( ~는 ~이다)

  • 기존 클래스로 새로운 클래스 작성
  • 자손은 조상의 모든 멤버를 상속받는다.
  • 자손의 변경은 조상에 영향을 주지 못한다.

포함( ~는 ~를 가지고 있다)

  • 클래스 멤버로 다른 클래스타입의 참조변수 선언

생성자 super()

  • 조상의 멤버 호출 시 사용. 자손의 생성자는 자신이 선언한 변수만 초기화 가능하다.
  • 반드시 첫 줄에서만 사용해야 한다.

참조변수 super

  • 조상의 멤버와 자신의 멤버를 구별하려고 사용한다.

import문 : 컴파일러에게 패키지가 속할 클래스를 알려준다

static import : 클래스 이름 생략이 가능하다.

제어자(modifier) : 클래스와 클래스 멤버에 부가적인 의미 부여

static : 공통적인, 클래스의

  • 클래스 : 객체 생성 없이 사용 가능

final : 불변의, 마지막의

  • 클래스 : 변경, 확장불가능. 자손 x
  • 메서드 : 오버라이딩이 불가능한 메서드
  • 변수 : 상수

abstract : 추상의, 미완성의

  • 클래스 : 미완성 설계도
  • 메서드 : 몸통이 없는 메서드

접근 제어자(Access Modifier)

  1. 외부로부터 내부 데이터 보호
  2. 내부적으로만 쓰이는, 불필요한 부분 감춤
  3. 한 가지 접근 제어자만 사용 가능
  같은 패키지 같은 클래스 자손 클래스 전체
public O O O O
protected O O O  
default O O    
private O      

다형성

  • 참조변수가 사용할 수 있는 멤버의 개수 ≤ 인스턴스 멤버의 개수
  • 조상 타입의 참조변수로 자손 타입의 객체 사용가능
  • 하나의 배열에 여러 종류의 객체 저장가능

참조변수의 형변환

  1. 사용할 수 있는 멤버의 개수 조절
  2. 조상과 자손 관계만 형변환 가능
  3. 기능 多 ⇒ 少 안전

instanceof 연산자 : 형변환이 가능한 경우 true 반환

추상 클래스

  • 미완성 설계도
  • 미완성 메서드 존재
  • 인스턴스 생성 불가

추상 클래스의 장점

  • 설계도 작성 Easy
  • 유지보수 Easy
  • 재사용성 ⬆️
  • 중복제거 Easy

추상 클래스의 완성

  • 추상 메서드 상속
  • 추상 메서드 구현부 작성

추상 메서드

  • 몸통이 없는 메서드
  • 자손마다 다르게 구현된다.
  • 모두 구현해야 완성된다.
  • 여러 클래스의 공통 부분
  • 충돌해도 문제가 생기지 않는다.

추상화

  • 공통부분 뽑아내기
  • 덜 구체적으로 만드는 것

인터페이스

  1. 추상 메서드의 집합. 껍데기
  2. 모든 멤버가 public
  3. 모든 멤버가 미완성
  4. 조상은 인터페이스만 가능
  5. 다중상속 가능
  6. iv 사용불가

인터페이스의 구현

  • 추상 메서드 모두 구현
  • 일부 구현시 abstract 붙여야 함

인터페이스의 다형성

  • 인터페이스 타입 참조변수 ⇒ 구현한 놈만 들어와라
  • retrun 타입이 인터페이스 ⇒ 구현한 놈 반환

인터페이스의 장점

  • 개발시간 단축
  • 표준화 가능
  • 관계없는 클래스에 관계생성 가능
  • 독립적인 프로그래밍 가능

 

Math.Random()

로또를 만드는 등에서 난수가 필요할 때, 우리는 다음과 같은 메서드를 사용한다.

public class random{
	public static void main(String[] args){
		int num1 = (int)Math.random()*10+1;
		int num2 = new Random(10).nextInt(6);
    }
}

그 중, 우리가 일상적으로 많이 쓰는 Math.random() 클래스는 정말 난수를 생성하는지에 대해서 궁금해졌다.

 

난수

위키피디아에 따르면 난수의 정의는 다음과 같다.

난수(亂數)란 정의된 범위 내에서 무작위로 추출된 수를 일컫는다.
난수는 누구라도 그 다음에 나올 값을 확신할 수 없어야 한다.

난수란 1.정의된 범위 내에서 2.무작위로 추출된 수를 일컫고 3.누구라도 다음에 나올 값을 확신할 수 없어야 한다.

그런데, 컴퓨터가 만드는 난수의 값은 정말 난수일까?

Math.random()의 API

먼저 random함수는 RandomNumberGenerator를 이용한 double값을 리턴한다.

randomNumberGenerator값을 타고 들어가보려 했으나, 나오지 않았다.

이는 악용 가능성 때문에 자바에서 난수를 생성하는 원칙을 보여주지 않으려고 해서 그렇다고 한다.

 

컴퓨터는 자연의 부산물이 아니다.

나중에 '컴퓨터의 1초는 어떻게 측정할까'에 대해 포스팅 하겠지만, 컴퓨터는 절대 자연을 따라올 수 없다.

코드는 인간이 짜고 인간이 만든 컴퓨터라는 결과물에서 그 결과를 출력한다.

난수 생성또한 마찬가지로, 그래서 정확하게는 random number가 아닌, pseudo-random number(유사 난수)라고 한다.

위에서 기술한 Random class의 메소드도, currentMillis를 이용한 시간을 받아 만든 난수 생성이기 때문에 완벽한 난수라고는 할 수 없다.

따라서 보안용으로는 random 메소드를 사용하지 말고, 대신 데이터를 실험하기엔 더없이 좋은 메소드이니 그럴 때 사용하도록 하자! :)

'자바' 카테고리의 다른 글

TIL - 좋은 객체지향적 설계원칙, SOLID(3) : LSP  (0) 2023.01.27
TIL - OOP 요약  (0) 2023.01.25
TIL - THE JAVA(3), NPE와 NULL  (1) 2023.01.20
TIL - THE JAVA(2) Flow Chart  (0) 2023.01.20
TIL - THE JAVA(1), JVM  (0) 2023.01.19

+ Recent posts