좋은 객체지향설계의 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받는 클래스들에서 구현한다.

 

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

 

문자열을 뒤집는 방법에는 다음과 같은 방법이 있다.

  • 전부 뒤집기
  • 부분 뒤집기

String은 immutable(불변)하다는 특징이 있다. 스트링을 더하거나 바꿔주거나 할 때는 무조건 새로운 String이 생성된다.

a+b를 콘솔에 찍어보면 Hello World!가 나온다고 해서 단순히 '아, Hello라는 문자열에 World를 더해주었구나' 라고 생각해서는 안된다.

찍어본 HashCode

위의 결과에서 알 수 있듯, a, b, a+b의 hashcode는 다르다. a에다가 b를 붙여준 것이 아니라, "Hello World"라는 스트링을 생성해준 것이다. String을 리터럴로(""의 형태로) 한번 생성하면 Heap 영역에 있는 Constant String Pool에서 같은 String이 있는지 검사한 후, 같은 스트링이 없다면 스트링 풀에 문자열 객체를 생성하고 주소값을 반환한다. 이와 같은 맥락에서 String에서의 equals와 ==의 차이 문제가 발생하기도 한다. 자세한 내용은 나중에 따로 설명하겠다.

 

그렇다면 스트링을 이용하고 싶은데 메모리를 덜 먹는 방법은 없을까?

바로 StringBuilder 클래스를 이용하는 것이다. StringBuffer 클래스도 있지만 이것은 동기화 차이이므로 Builder 클래스가 낫다.

for (String x : str){
	StringBuilder sb = new StringBuilder();
	answer.add(String.valueOf(sb.append(x).reverse()));
}

스트링빌더는 한번 생성한 개체로 필드의 가변 인자들을 받아 append등의 연산을 빠르고 가볍게 할 수 있다.

조금 더 풀이를 깎아서,

for (String x : str){
	String tmp = new StringBuilder(x).reverse();
	answer.add(tmp)
}

로도 나타낼 수 있겠다. 조금 더 명확하다.

 

toCharArray를 이용해서 문자열을 부분적으로 뒤집는 방법도 있다.

for (String x : str){
	char[] arr = x.toCharArray();
	int lt = 0;
	int rt = arr.length-1;
	while (lt<rt){
		char tmp = arr[lt];
		arr[lt] = arr[rt];
		arr[rt] = tmp;
        lt++;
        rt--;
        }
        answer.add(String.valueOf(arr));
}

기술 자체는 어려운게 아니지만, lt와 rt를 설정하는 방법이 핵심이다.

 

lt는 한 칸씩 증가, rt는 한 칸씩 감소하면서 char배열을 읽어낼 수 있다.

마지막에 String.valueOf로 값을 뽑아준다.

 

 

 

 

https://hyeran-story.tistory.com/123#recentEntries

 

[Java] equals()과 == 차이점, String Constant Pool(상수 풀)

자바에서 String의 값을 비교할때 equals()를 쓰시나요 ==을 쓰시나요? 보통 산술연산자에서 값을 비교할때는 ==을 하는데요 인텔리제이에서 String의 값을 비교할때 ==을 쓰면 아래와 같은 메세지를

hyeran-story.tistory.com

 

'알고리즘 > 기타' 카테고리의 다른 글

Two pointers  (0) 2023.04.25
[JAVA]지뢰 찾기 - 델타 배열을 이용한 방법  (0) 2023.02.02
소수 찾기 - 외부 메서드 없이 탐색  (0) 2023.02.02
문자열 중복제거 - indexOf()  (0) 2023.01.29

좋은 객체지향설계의 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단 방식을 이용해 어떻게 해야 하나 정말 고민했는데

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

 

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

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

SRP : 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는것은 모호하지만, 중요한 기준은 변경이다
    • 변경이 있을 때 파급 효과가 적어야 한다.

OCP : 개방/폐쇄 원칙

  • 소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀 있어야 한다.
  • 다형성을 활용해 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현

LSP : 리스코프 치환 원칙

  • 프로그램의 객체는 프로그램의 정확성을 깨트리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 단순히 컴파일에 성공하는 것을 넘어서서, 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 그래서 기능적으로 보장이 되어야 한다는 것을 말한다.
  • 하위 클래스의 인스턴스는 상위형 객체 참조변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다

ISP : 인터페이스 분리 원칙

  • 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
  • 예를 들어 자동차 인터페이스 → 운전 인터페이스, 정비 인터페이스 등으로 분리
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.
  • 인터페이스는 여러개를 상속받을 수 있기 때문에 좋은 코드를 만들 수 있다.
    • 그러나 너무 분리하면 코드를 다 까봐야해서 어느정도 선까지 구체화할지, 추상화할지를 정해야 한다.
  • 클라이언트가 필요하지 않는 기능을 가진 인터페이스에 의존해서는 안되고 최대한 인터페이스를 작게 유지해야 한다.

DIP : 의존관계 역전 원칙

  • “추상화에 의존해야 하고, 구체화에 의존하면 안된다”는 말은 여기서 나온다.
  • 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
  • 역할에 의존해야 한다는 것. 줄리엣은 로미오가 무엇을 하든, 누구든 상관 없어야 하고, 운전자는 K3 자체에 매여서는 안된다는 것이다.

다형성 만으로는 OCP, DIP를 지킬 수 없다.

그럼 뭐가 더 있어야 하는데?

그래서 스프링이 등장했다.

스프링은 DI와 DI 컨테이너로 OCP, DIP를 가능하게 지원한다.

 

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

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

SRP : 단일 책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나 뿐이어야 한다.
로버트 C. 마틴(밥 아저씨)
  • 한 클래스(객체)는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는것은 모호하지만, 중요한 기준은 변경이다.
    • 변경이 있을 때 파급 효과가 적어야 한다. (매우 중요)
class Calculator {

    public int calculate(String operator, int firstNumber, int secondNumber) {
        int answer = 0;

        if(operator.equals("+")){
            answer = firstNumber + secondNumber;
        }else if(operator.equals("-")){
            answer = firstNumber - secondNumber;
        }else if(operator.equals("*")){
            answer = firstNumber * secondNumber;
        }else if(operator.equals("/")){
            answer = firstNumber / secondNumber;
        }

        return answer;
    }
}

다음 코드의 문제점은 뭘까?

  1. 첫 번째로, Calculate의 책임감이 너무 무겁다. 혼자 "+"를 연산하는 메서드, "-"를 연산하는 메서드, "*"를 연산하는 메서드, "/"를 연산하는 메서드를 전부 짊어지고 있다.
  2. 두 번째로, 연산 기호가 바뀌었을 때나 각 연산 과정에서 변화가 생겼을 때 다른 연산 과정에도 영향을 끼칠 수 있다.
  3. "/"에서 예외처리가 되지 않았다.

=> 해결 방안

  1. Calculate에는 사칙연산을 실행하는 메서드만 넣는다.
  2. 상세한 내용은 각 클래스로 구현하되, calculate 클래스에서는 각 클래스를 불러온다.
  3. 예외처리를 해준다.
class Calculator {
    private int firstNumber;
    private int secondNumber;
    public Calculator(){
        this.firstNumber = 0;
        this.secondNumber = 0;
    }

    public Calculator(int firstNumber, int secondNumber) {
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public int addOperation() {
        AddOperation addOperation = new AddOperation(firstNumber,secondNumber);
        return addOperation.operate();
    }
    public int subtractOperation(){
        SubstractOperation substractOperation = new SubstractOperation(firstNumber, secondNumber);
        return substractOperation.operate();
    }

    public int multiplyOperation(){
        MultiplyOperation multiplyOperation = new MultiplyOperation(firstNumber,secondNumber);
        return multiplyOperation.operate();
    }
    public double divideOperation() {
        DivideOperation divideOperation = new DivideOperation(firstNumber, secondNumber);
        return divideOperation.operate();
    }

    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }
}

class DivideOperation{
    private int firstNumber;
    private int secondNumber;

    public DivideOperation(int firstNumber, int secondNumber) {
        this.firstNumber = firstNumber;
        this.secondNumber = secondNumber;
    }

    public void setFirstNumber(int firstNumber) {
        this.firstNumber = firstNumber;
    }

    public void setSecondNumber(int secondNumber) {
        this.secondNumber = secondNumber;
    }

    public double operate(){
        try{
            return firstNumber/secondNumber;
        } catch(ArithmeticException e){
            System.out.println("0으로 나눌수 없습니다. 다시 계산해주세요");
            exit(0);
        }
        return firstNumber/secondNumber;
    }
}

Calculator와 나눗셈 영역만 업로드했다. 나눗셈은 try-catch로 0으로 나눌 때를 대비해 ArithmeticException을 걸렀고,

지금은 아니지만 혹시나 계산기 내의 메서드가 비밀이라고 할 때를 대비해 인자들을 private로 감쌌다.

 

의외로 try-catch문으로 감싸는걸 까먹어서 오래 걸렸다.

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

개괄

호랑이도 무서워하는 곶감

자바를 시작한 사람에게 무엇이 제일 무서우냐 하면 무조건 'NPE, NullPointerException'이라고 할 것이다.

'혈연, 지연, 학연'보다 무서운 인연이 '악연이다'라는 말이 있듯

그만큼 자바와 null은 뗄레야 뗄 수 없는 존재이면서도 무서운 존재이다.

어쩌면 그 null 하나 때문에도 kotlin이 각광을 받으니...

NPE에 대해선 정말 여러곳에서 수만가지 이야기들을 들었다.

Effective Java의 item54에서는

null이 아닌, 빈 컬렉션이나 배열을 반환하라

라고도 쓰여져 있다. 

세상에 누가 null을 반환하고 싶을까 스럽지만, 때로는 null을 반환해야 할 때도 있고, null을 반환하게 된다면 클라이언트가 추가 방어 코드를 구현해야 한다. 또 클라이언트가 방어코드를 빼먹으면 큰 오류가 발생하게 될 수도 있다.

치즈의 개수를 반환하는 코드가 있다고 해보자.

private final List<Chicken> chickenInStock = ...;
public List<Chicken> getChickens(){
	return chickensInStock.isEmpty()? null : new ArrayList<>(cheesesInStock);
}

치킨의 재고를 반환하는 코드에서 null을 반환하지 말고

public List<Chicken> getChicken(){
	return chickensInStock.isEmpty() ? Collections.emptyList() : new ArrayList<>(chickensInStock);
}

이렇게 빈 Arraylist를 반환하거나,  혹은

public Chicken[] getChickens[]{
	return chickensInStock.toArray(new Chicken[0]);
}

와같은 빈 (혹은 빌 수도 있는) 배열을 반환하는 것이 좋다.

자바8 이후로는 optional.ofNullable로 nullcheck를 보다 유연하게 할수 있지만, optional은 많이 쓰면 가독성이 떨어지는 외에 오류가 많을 수 있다.


왜 이 얘기를 했니?

연산자와 관련된 튜터링을 하면서 튜터님이 말씀해주신

if var==null ? null : 11;

이라는 코드를 보고 갑자기 책에서 본 내용이 생각났다.

이런 코드들이 많이 쓰인다니, 프론트에서도 null checking을 열심히 해야 하나보다.

 

늘 혼자서만 공부하다 보니, 프론트와 소통할 수 있는 일이 한번도 없었고,

이에 늘 여기서 데이터를 정리하든 저기서 데이터를 정리하든 똑같지 않나라는 생각을 했는데,

정말 좋은, 프론트엔드들을 만나고 나니 정말 정보를 잘 정리해서 데이터를 던져야겠다는 생각이 들었다.

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

TIL - 좋은 객체지향적 설계원칙, SOLID(3) : LSP  (0) 2023.01.27
TIL - OOP 요약  (0) 2023.01.25
TIL - Math.Random은 정말 난수를 리턴할까?  (0) 2023.01.24
TIL - THE JAVA(2) Flow Chart  (0) 2023.01.20
TIL - THE JAVA(1), JVM  (0) 2023.01.19

많은 수포자들을 열심히 길러냈던 수학적 귀납법

개괄

고등학교때 누구나 수포자를 한 번씩은 꿈꿔봤을 것이다. Dreams come true를 만들어 주는 그 문제...

프로그래밍을 하면서 다시금 한번 마주치게 되었다. 수학과를 갈 줄 알았다면 열심히 해 놓을걸...

플로우차트는 말 그대로 흐름에 관한 차트이며, 비단 알고리즘 문제를 풀 때만이 아니라 추후 설계나 디자인 패턴과도 연관되어 있다.

 

Flowchart

보통 yes는 검은색 화살표로, no는 빈 화살표에 하는 줄 알았는데 상관이 없는 것 같다.

플로우차트는 크게 직선형, 분기형, 반복형으로 나뉘는데 보통 분기형을 많이 사용한다.

직선형이나 반복형만 나타난 것으로는 사실 크게 플로우차트를 그릴 일이 없기 때문이다.

 

오늘의 과제는 https://app.diagrams.net/를 이용한 '이메일/패스워드 검증 플로우차트 그리기'였다.

내가 그린 플로우차트는 다음과 같다. 꽤나 허접하지만, 그래도 처음 플로우차트를 이용해 그림을 그려보았다.

flow차트를 그리니 이런 간단한 문제에서는 필요성을 잘 못 느끼지만, 나중에 의존적인 함수나 패턴이 나올 때에는 굉장히 유용할 것 같다. 다음에는 설계할 때 한번씩 플로우차트를 그려봐야겠다.

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

TIL - 좋은 객체지향적 설계원칙, SOLID(3) : LSP  (0) 2023.01.27
TIL - OOP 요약  (0) 2023.01.25
TIL - Math.Random은 정말 난수를 리턴할까?  (0) 2023.01.24
TIL - THE JAVA(3), NPE와 NULL  (1) 2023.01.20
TIL - THE JAVA(1), JVM  (0) 2023.01.19

개괄

IT 인프라 구조라는 책을 읽다가 문득 웹 서버와 AP 서버에 대한 이야기가 나왔다.

웹 서버의 OS 안에는 '프로세스'라는, 독립된 메모리 공간을 가진 것이 있다.

AP서버에도 '스레드'라는 프로세스가 있지만, 웹 서버의 프로세스와는 차이점을 가진다. 바로 AP서버의 스레드는 한 스레드마다 독립적인 메모리 공간을 가지고, 웹 서버의 프로세스는 그 메모리 공간을 같이 공유한다는 것이다.

'JVM도 AP서버의 프로세스중 하나'라고 적혀 있다. 그럼 대체 JVM이 뭘까?

 

자바를 시작하게 되면 늘 듣게 되는 영단어들이 있다. 그 중 3개를 아마 가장 많이 듣게 될텐데,

자바를 설치할 때 듣는 JDK,

자바를 실행할 때 듣는 JRE,

자바 이론을 공부할 때 든는 JVM이 있다.

(물론 내가 들은 것 기준)

JDK 안에 JRE 안에 JVM

JDK는 Java Development Kit, JRE는 Java Run Environment, JVM은 Java Virtual Machine의 줄임말로, 각각 자바 개발 도구, 자바 실행 환경, 자바 가상 기계라는 뜻을 가지고 있다.

 

근본?

코틀린이 자바에서 쓰이던 어플리케이션 목적의 개발에서 조금 더 나은 길을 걷고자 나왔듯,

자바도 결국은 C에서 나온 언어다. 자바는 왜 C에서 branch를 뻗게 되었을까?

C는 "One Source Multi Object Use Anywhere"를 모토로, 하나의 소스파일로 각 기계에 맞는 목적파일로 만들어 어디든 사용 가능하게 하려는 프로그램이다. 그러나 개발자들은 기계에 맞는 목적 파일(OS를 맞춰줘야 하는 파일)들이 아니라, 어떤 OS환경에서도 사용하고 싶어했다.

그래서 C의 위에서 아래로 흐르는 절차지향적 언어에서 탈피해, JAVA라는 객체지향적 언어를 만들었다.

 

자바의 실행

자바는 개발자가 짜놓은 소스파일을 JDK가 제공하는 'java compiler'를 이용하여 실행한다.

JVM의 클래스 로더는 컴파일로 생성된 바이트코드를 전달받아 JVM의 Runtime data area에 로드하고,

JVM의 Execution에 의해 기계어로 해석되어 실행된다.

 

클래스 로더? Execution?

클래스 로더는 자바로 작성한 모든 클래스, 변수등의 정보를 method area에 배치한다.

이 때 변수 중 하나인, static value라고도 불리는 cv가 생성되고 heap area에 배치된다.

 JVM 내부에 바이트 코드를 로드하고, 링크를 통해 배치하는 작업을 수행한다.

 

인터프리터는 바이트 코드를 한 줄씩 읽고 번역해주는데 매우 느리다.

그래서 JIT 컴파일러가 등장했는데, 컴파일러의 특징은 다음과 같다.

 : 바이트 코드 전체를 컴파일하여 캐싱하기 때문에 실행이 매우 빠르고, 미리 오류를 줄여준다.

근데 엑시큐션 얘기하는데 JIT와 인터프리터가 왜 나오냐고? 실행 엔진(Execution)의 실행 방식에 JIT와 인터프리터가 있기 때문이다.

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

TIL - 좋은 객체지향적 설계원칙, SOLID(3) : LSP  (0) 2023.01.27
TIL - OOP 요약  (0) 2023.01.25
TIL - Math.Random은 정말 난수를 리턴할까?  (0) 2023.01.24
TIL - THE JAVA(3), NPE와 NULL  (1) 2023.01.20
TIL - THE JAVA(2) Flow Chart  (0) 2023.01.20

+ Recent posts