Cooper's devlog

[TIL] 2021.01.15 본문

TIL

[TIL] 2021.01.15

cooper_dev 2021. 1. 17. 05:06

 [좌표 계산기 완성]

 좌표 계산기는 객체지향적으로 구현하기 좋은 예제인 것 같다. 다각형이라는 공통된 특징을 분류하고 각각의 요소들로 분리해서 결과를 분리해서 구현하는 방식으로 진행한다. 아래에는 내가 구현한 클래스 다이어그램이다.

 

 

기존의 설계와 일치하는 구현을 진행하지는 않았지만 이전에 설계없이 프로그래밍을 진행했었다. 그러다 보니 변경 사항이 잦아졌고 최소한의 기준도 없이 구현에 초점을 맞춰 프로그램을 작성했다. 반면에 설계 기반으로 프로그램을 작성하고 나니 최소한의 기준을 두고 프로그램을 구현할 수 있었다. 그러다 보니 각각의 역할을 할당하여 기존의 설계의 틀에서 많이 벗어나지 않는 프로그램을 작성할 수 있었던 것 같다.

 


[클래스와 오브젝트, 인스턴스]

  • 오브젝트(Object) : 속성(property) + 행위(behavior)을 통해 사물을 정의.
  • 클래스(class) : 상태(field)와 행동(method)을 기반으로 객체를 정의하는 설계도
  • 인스턴스(isntance) : 클래스로 부터 객체를 선언하는 과정
class Main{
	public static void main(String[] args) {
    	//1. 인스턴스를 생성자를 통해서 생성된다.
    	Point p = new Point();
        
        //2. Capsulation
        // - 클래스는 최대한 외부에 자신의 상태(filed)를 공개하지 않는 것이 좋으며,
        // - 행위(Method)의 내부 로직을 감춘 상태로 결과를 반환한다.
        //(+ 캡슐화가 잘되있다면 변경에 용이한 코드를 작성할 수 있는 장점이 있다.)
        p.moveRight();
    }
}

// class : 객체(instance)를 만들기 위해 상태(filed)와 행위(method)를 정의하는 설계도
class Point {
	private int x;
    private int y;
    
    public Point(int x, int y) {
    	this.x = x;
        this.y = y;
    }
    
    public void moveRight() {
    	this.x += 1;
    }
    
    publicvoid moveUp() {
    	this.y += 1;
    }
}

 

[상속과 다형성]

[상속(Inheritance)] : 기존의 클래스에서 기능이 확장된 버전의 클래스의 행동을 제공한다.

 

상속의 장점

  • 기존의 클래스를 재사용
  • 적은 양의 코드로 새로운 클래스를 작성할 수 있음
  • 코드를 공통적으로 관리 → 생산성과 유지보수에 크게 기여

 

 

[this와 super 키워드 차이점]

  • this : 자기 자신의 주소 번지를 참조하는 키워드
  • super : 조상의 주소 번지를 참조하는 키워드

[this, super 사용 예시]

class Main {
    public static void main(String[] args) {
        B b_test = new B();
        b_test.printTest();
        System.out.println("--------------------");
        B b_test_1 = new B();
        b_test_1.printTest();
    }
}

class A {
    int x;
    int y;

    public A(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class B extends A {
    public B () {
        super(0, 0);
        this.x = 3;
        this.y = 2;
    }

    public void printTest() {
        System.out.printf("super.x = %d, super.y = %d \n", super.x, super.y);
        System.out.printf("super.reference address: %s \n", super.toString());

        System.out.printf("this.x = %d, this.y = %d \n", this.x, this.y);
        System.out.printf("this.reference address: %s \n", this.toString());
    }
}

//super.x = 3, super.y = 2 
//super.reference address: oop.this_test.B@7ea987ac 
//this.x = 3, this.y = 2 
//this.reference address: oop.this_test.B@7ea987ac 
//--------------------
//super.x = 3, super.y = 2 
//super.reference address: oop.this_test.B@12a3a380 
//this.x = 3, this.y = 2 
//this.reference address: oop.this_test.B@12a3a380 

 

[Q. 상위 클래스의 변수를 참조해서 하위 객체를 초기화한다면 같은 인스턴스 변수를 공유하는 건가요?]

ans. NO : 각자의 인스턴스를 생성해서 만들어지는 것이며 위 결과를 보면 각자 같은 주소번지를 가지는 것을 확인할 수 있다. 

 

 

[this 사용]

class pointTest{
	public static void main(String[] args) {
    	//1. point 객체를 생성하면서 주소 번지가 point(16진수 형태)에 저장됨
        //2. 객체 타입은 Reference type이므로 해당 정보의 주소는 힙영역에 저장
        //3. 객체 자신이 자신의 인스턴스 변수를 사용하기 위해서는 주소번지를 알고 있어야 함.
        //4. 이를 나타내는 keyword가 this 이다.(this를 생략하고 변수명만 사용해서 가능하지만 비추천)
        
        Point point = new Point(3, 5); //point : 주소번지 저장/ new ~ : 힙 영역에 저장
    }
}

class Point{
	private int x;
    private int y;
    
    public Point(int x, int y) {
    	this.x = x;
        this.y = y;
    }
    
    public void moveRight() {
    	this.x += 1;
        // 객체 주소 자신의 x 변수의 위치를 1만틈 이동한다.
    }
    
    public boic moveLeft() {
    	this.x -= 1;
        // 객체 주소 자신의 x변수의 위치를 1만큼 뒤로 이동한다.
    }
}

 

 그렇다면 this를 사용하는 이유는?

  • method를 구현하다보면 객체 자신을 사용해야 하는 경우가 발생한다.
  • 이 경우 this라는 keyword를 이용해서 인스턴스 자신을 참조할 수 있도록 한다. 
  • 아래는 equals 재정의 시, this를 사용한 예시이다.

> this예시 : equals 재정의

class Point {
	int x;
    int y;
    
    
    public boolean equals (Object o) {
   		if(this != o) { //자시 자신의 주소와 일치할 경우
        	return true;
        }
        
        if(!(o instanceof Point) { //Point 객체 계열의 경우
        	return false;
        }
        
        Point p = (Point) o;
        return this.x == o.x && this.y == o.y;
    }
}

 

 

[다형성(Polymorphism)] : 같은 자료형에 여러 가지 객체를 대입하여 다양한 결과를 얻어내는 성질

  • 장점
    • 여러 타입의 객체하나의 타입으로 관리가 가능하다.
    • interface로 선언 시, 다중 상속이 가능해서 다양한 타입을 받아올 수 있음.
    • 적은 결합도로 코딩이 가능해 재사용성이 높아짐
  • 캡슐화(Capsulation) 
    • 객체의 내부 구현을 외부로부터 감추기 위해서이다.
    • 변경 가능성이 높은 부분객체 내부로 숨기는 추상화 기법
    • 불안정한 부분과 안정적인 부분을 분리해서 변경의영향을 통제할 수 있다.
  • 응집도(Cohension)
    • 모듈에 포함된 내부 요소들의 연관돼 있는 정도
      • 높은 응집도 : 모듈 내에 요소들이 하나의 목적을 위해 긴밀하게 협력하는 상태
      • 낮은 응집도 : 모듈 내에 요소들이 서로 다른 목적을 추구하는 상태
      • 높은 응집도 일수록 유지보수에 좋은 코드이다.
  • 결합도(Coupling)
    • 의존성의 정도는 나타냄.
      • 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지는 나타내는 정도.
      • 높은 결합도 : 다른 모듈에 대해 너무 자세한 부분까지 알고 있는 경우
      • 낮은 결합도 : 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있는 경우
    • 결합도가 낮을수록, 유지보수에 좋은 코드이다.

 

public class Morphism_Test {
    public static void main(String[] args) {
        Triangle t = new Triangle();
        Rectangle r = new Rectangle();

        printResult(t); //Triangle type임에도 작동
        printResult(r); //Rectangle type임에도 작동
    }

    public static void printResult(Shape shape){
        System.out.printf("넓이는 %.1f입니다.\n", shape.calculateArea());
    }
}

interface Shape {
    double calculateArea();
}

class Triangle implements Shape {

    @Override
    public double calculateArea() {
        return 3;
    }
}

class Rectangle implements Shape {

    @Override
    public double calculateArea() {
        return 4;
    }
}

 

 

[객체 인스턴스 비교 : equals 재정의, hashcode 재정의]

equals : 객체가 같은지를 확인하기 위한 메서드

  • equals 재정의 하지 않는 경우 : 기본적으로 객체의 주소 값을 비교 객체 자신만 같은 것으로 취급.
  • equals 재정의를 하는 경우 : 현실 세계에서의 동일한 기준을 적용시키기 위해서

 

[ex : xy좌표가 각각 같은 것으로 판별시키고 싶을 때]

  1. 재정의 안할 시
  2. equals 재정의
  3. hashCode 재정의

 

[1. 재정의 안할 시]

public class None_Equals_Test {
    public static void main(String[] args) {
        Point p1 = new Point(3, 4);
        Point p2 = new Point(3, 4);

        System.out.println(p1.equals(p2));
        System.out.printf("p1 주소: %s \n",p1.toString());
        System.out.printf("p2 주소: %s \n", p2.toString());
    }
}

//false
//p1 주소: oop.this_test.Point@1b6d3586 
//p2 주소: oop.this_test.Point@12a3a380 

class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

이전에 말한 것처럼 재정의를 하지 않고 결과값을 비교하는 경우 각각의 주소번지를 확인해서 비교하기 떄문에

두 주소번지는 다르므로 false를 반환한다.

 

그렇다면 x, y의 좌표만을 가지고 두 객체가 같다는 것을 어떻게 표현할 수 있을까?

그 방법이 equals 재정의이다.

 

 

[2. equals 재정의]

// Point 객체 안에 해당 내용을 재정의하자.
    @Override
    public boolean equals(Object o) {
        //같은 주소번지 일 때면, true를 반환한다.
        if (this == o){ 
            return true;
        }
        
        //Point type 계열이 아니라면 false 반환
        if (!(o instanceof Point)){
            return false;
        }
        
        Point point = (Point) o;
        return x == point.x && y == point.y; //두 값이 같으면 true!
    }
}

 

위 코드에서 설명한 내용들을 기반으로 두 값을 비교한다면

위 코드를 확인해보면 알 수 있듯이, 주소 값이 다르더라도 일치하는 것을 확인할 수 있다.

 

그리고 추가적으로 진행해야 할 일이 있다.

Hashcode 재정의도 진행해야 한다.

 

 

[3. hashCode 재정의]

 

  • hashCode Method : 해당 객체를 해쉬코드 값으로 변환해주는 함수 
    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }
  • Hashcode 재정의 시, 장점
    • Collection Framework에서 Hashcode를 이용한 자료 구조의 논리적 동치성(현실세계 비교) 가능.

1. Hashcode 재정의하지 않고 HashMap에 추가 후 결과.

재정의 이전, HashSet에 추가한 결과

HashMap은 <Key, Value>형태의 자료 구조이며 Key는 중복을 허용하지 않지만,

중복을 허용한 값을 추가한 것을 볼 수 있다.

원인은 HashCode를 재정의해야 한다.

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

 

 

 

해당 코드를 작성하면 두 객체가 중복된 값이라고 인지하게 되고 값이 하나만 추가되는 것을 확인할 수 있다.

재정의 후, HashSet에 추가한 결과

 


[SOLID원칙]

 

[SRP : Single Response Principle]

  • 모든 클래스는 각각 하나의 책임을 가져야 한다.
  • = 계산 클래스는 사칙연산만 작업 수행

 

[OCP: Open Closed Principle]

  • 확장에는 열려있고 수정에는 닫혀있는 설계를 해야한다.
  • = 기존의 코드를 변경하지 않으면서 (closed) 기능을 추가할 수 있는(open) 설계를 해야 한다.

 

[LSP : Liskov Substitution Principle]

  • 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있다.
  • 부모 클래스가 들어갈 자리자식 클래스를 넣어도 계획대로 잘 작동해야 함.
  • 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않고 확장을 해야 LSP를 만족한다.

 

[ISP : Interface Segregation Principle]

  • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야 한다.
  • 일반적인 하나의 인터페이스보다 여러개의 구체적인 인터페이스가 낫다.

 

[DIP : Dependency Inversion Princilple]

  • 의존관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다는 변화하기 어려운 것, 거의 변화가 없는 것에 의존해야 한다.
  • = 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺도록 한다.

 

'TIL' 카테고리의 다른 글

[TIL] 2021.01.19 - Closure, Pure Function, HOF  (1) 2021.01.19
[TIL] 2021.01.18  (0) 2021.01.18
[TIL]2021.01.14  (0) 2021.01.16
[TIL] 2021.01.13  (0) 2021.01.13
[TIL] 2021.01.12  (0) 2021.01.12
Comments