OOP란?

Kuno17·2023년 5월 8일
0

CS공부

목록 보기
4/17
post-thumbnail

OOP란? (Object Oriented Programming)

객체 지향 프로그래밍을 의미한다.

  • OOP란 인간 중심적 패러다임(사고)이라고 할 수 있다.
  • 즉 프로그래밍에서 필요한 데이터를 추상화 시켜 상태(속성,어트리뷰트)와 행위(동작, 메소드)를 가진 객체로 만들고 객체간의 상호 작용을 통해 로직을 구상하는 방법을 의미.

객체

객체란 동작의 주체가 되는 요소를 의미.
모든 객체는 상태와 동작을 가진다. TV를 객체라고 한다면 특정 TV만이 가지는 색,크기,가격 등은 상태이며, TV채널 이동, 다시보기, 넷플릭스 연결 등은 기능(동작)이라고 볼 수 있다.

JAVA에서는 다음과 같이 볼 수 있다.

  • 객체 → 클래스
  • 상태 → 맴버 변수
  • 동작 → 메소드(함수)

장점

  • 다른 클래스를 가져와서 사용가능 → 코드의 재사용성 증가
  • 객체를 수정할 경우 모든 로직에 일괄적으로 적용, 중복코드 관리 간단 → 코드의 유지보수성 증가
  • 객체, 모듈 단위로 구분되는 특징으로 업무분장이 쉽고, 각 모듈의 연관성 도식용이 → 큰 규모의 프로그래밍에 유리함.

단점

  • 절차지향과 달리 각각의 의존관계로 인해 속도가 느리다.
  • 복잡한 상호작용으로 이루어진 방식이라 설계에있어 많은 고민과 역량을 요구한다.
  • 추상 객체, 상속, 인터페이스 등의 복잡한 개념과 활용이 코드의 구조를 파악하기에 어려움을 만든다.

객체 지향의 특성

  • 캡슐화
    코드를 수정없이 재활용하는 것을 목적으로 함.
    클래스라는 캡슐에 기능과 특성을 담아 묶는다.

  • 상속
    클래스로부터 속성과 메소드를 물려받는 것을 말한다.
    다른 클래스를 가져와서 수정할 일이 있다면, 그 클래스를 직접 수정하는 대신 상속을 받아 변경하고자 하는 부분만 변경.

  • 추상화
    객체 지향 관점에서 클래스를 정의하는 것.
    불필요한 정보 외 중요한 정보만 표현함으로써 공톡의 속성과 기능을을 묶어서 이름을 붙이는 것.

  • 다형성
    하나의 변수명이나 함수명이 상황에 따라 다르게 해석될 수 있음.
    대표적인 다형성이 오버 라이딩, 오버 로딩이다.


SOLD원칙

1. SRP : 단일 책임 원칙

  • 클래스는 단 하나의 책임(목적)을 가지고, 그에 대한 책임을 져야 한다.

Example

Employee.class에는 4가지 메소드가 존재한다.


calculatePay() : 회계팀에서 급여를 계산하는 메서드
reportHours() : 인사팀에서 근무시간을 계산하는 메서드
saveDababase() : 기술팀에서 변경된 정보를 DB에 저장하는 메서드
calculateExtraHour() : 초과 근무 시간을 계산하는 메서드 (회계팀과 인사팀에서 공유하여 사용)


그런데 회계팀에서 급여를 계산하는 방식을 새로 변경하여 코드에서 초과 근무 시간을 계산하는 메서드 calculateExtraHour()의 알고리즘 업데이트가 필요해졌다.

그런대 calculateExtraHour()매소드를 변경헀는데, 변경에 의한 파급 효과로 인해 수정 내용이 의도치 않게 reportHours()메소드에도 영향을 주게 되어버린다.(공유하기 때문)

그리고 인사팀에서는 이러한 변경사실을 알지 못하고 메소드 반환 결과가 잘못되었다고 개발팀에 요청을 보내게 된다.
이러한 상황이 바로 SRP에 위배되는 상황이다.

Employee클래스에서 회계팀,인사팀,기술팀 이렇게 3개의 엑터에 대한 책임을 한번에 가지고 있기 때문이다.
즉 이런경우 회계팀, 인사팀, 기술팀의 클래스를 별도로 분리하고 각 클래스의 메소드를 사용하는 방식으로 해결할 수 있다.


2. OCP : 개방-폐쇄 원칙

  • 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되어야 한다는 원칙.
  • 확장에 대해서는 개방적(open)이고 수정에 대해서는 폐쇄적(closed)이어야 한다는 의미.

확장에 열려있다.

  • 모듈의 확장성을 보장하는 것
  • 새로운 변경 사항 발생시 유연하게 코드를 추가함으로써 애플리케이션의 기능을 큰 힘을 들이지 않고 확장할 수 있다.

변경에 닫혀있다.

  • 객체를 직접 수정하는것은 제한해야 한다는 것을 의마한다.
  • 객체를 직접 수정하지 않고도 변경사항을 적용할 수 있도록 설계해야 한다. 그래서 변경에 닫혀있다고 표현.

Example

메인 메소드에서 cat과 dog 동물 객체를 만들고 HelloAnimal 클래스의 hello() 메소드를 통해 실행해보면 오류없이 잘 동작됨을 확인 할 수 있다.

class Animal {
	String type;
    
    Animal(String type) {
    	this.type = type;
    }
}

// 동물 타입을 받아 각 동물에 맞춰 울음소리를 내게 하는 클래스 모듈
class HelloAnimal {
    void hello(Animal animal) {
        if(animal.type.equals("Cat")) {
            System.out.println("냐옹");
        } else if(animal.type.equals("Dog")) {
            System.out.println("멍멍");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        HelloAnimal hello = new HelloAnimal();
        
        Animal cat = new Animal("Cat");
        Animal dog = new Animal("Dog");

        hello.hello(cat); // 냐옹
        hello.hello(dog); // 멍멍
    }
}

문제는 기능을 추가 이다.
만일 고양이와 개 외에 사자와 양을 추가한다면? 당연하게 HelloAnimal 클래스를 수정해 주어야 한다.

class HelloAnimal {
	// 기능을 확장하기 위해서는 클래스 내부 구성을 일일히 수정해야 하는 번거로움이 생긴다.
    void hello(Animal animal) {
        if (animal.type.equals("Cat")) {
            System.out.println("냐옹");
        } else if (animal.type.equals("Dog")) {
            System.out.println("멍멍");
        } else if (animal.type.equals("Sheep")) {
            System.out.println("메에에");
        } else if (animal.type.equals("Lion")) {
            System.out.println("어흥");
        }
        // ...
    }
}

이런식으로 동물이 추가될때마다 계속 코드를 변경해줘야하는 번거로움이 생기게 된다.
이는 처음 설계에서부터 잘못되었기 때문에 발상하는 현상이다.

이제 이를 올바른 OCP대로 설계하려면 다음을 고려해야한다.
1. 먼저 변경(확장)될 것과 변하지 않을 것을 엄격히 구분
2. 이 두 모듈이 만나는 지점이 추상화(추상 클래스 or 인터페이스)를 정의
3. 구현체에 의존하기 보다 정의한 추상화에 의존하도록 코드를 작성.

다음과 같이 클래스를 분리하고 추상화후 구체화 한다면 일일이 코드를 변경하지 않아도 된다.

사자나 양클래스를 추가할때도 동일하게 추가(확장)만 해주면된다.


3. LSP 리스코프 치환 원칙

  • 서브 타입은 언제나 기반타입으로 교체할 수 있어야 한다는 것을 의미.
  • 부모 클래스를 사용하는 위치에 자식 클래스의 인스턴스를 대신 사용한 경우 코드가 원래의 의도대로 작동해야 한다는 의미이다.
  • 즉 다형성 원리를 이야기한다.

Example

리스코프 원칙을 지키지 않는 대표적인 예가 직사각형 - 정사각형 문제이다.
직사각형은 정사각형이 아니지만 정사각형은 직사각형이라는 사실에 기반한다.

public class Rectangle {
    private int width;
    private int height;

    public void setWidth(final int width) {
        this.width = width;
    }

    public void setHeight(final int height) {
        this.height = height;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}

따라서 위의 직사각형 객체를 상속받아서 아래처럼 정사각형 객체를 정의할 수 있다.

public class Square extends Rectangle {

    @Override
    public void setWidth(final int width) {
        super.setWidth(width);
        super.setHeight(width);
    }

    @Override
    public void setHeight(final int height) {
        super.setWidth(height);
        super.setHeight(height);
    }
}

다만 정사격형은 가로와 세로의 길이가 같으므로 setWidth()나 setHeight()를 호출하면 가로와 세로값을 모두 바꿔줘야 하므로 메소드를 재정의 했다.

이제, 다른 클래스에서 Retangle클래스를 이용해 보자.

public void increaseHeight(final Rectangle rectangle) {
	if(rectangle.getHeight() <= rectangle.getWidth()) {
    	rectangle.setHeight(rectangle.getWidth()+1);
        }
    }

해당 메소드는 직사각형의 가로와 세로를 비교한 다음에, 세로가 가로보다 짧거나 같다면 가로의 길이에 1을 더한 만큼의 길이를 갖게 만드는 역할을 한다. 정사각형이 아닌 직사각형에 대해서는 위 메소드가 올바르게 작동한다.

하지만 정사각형의 경우는 다르다. 정사각형은 항상 가로,세로의 길이가 같으므로 위 메소드를 실행하면 가로와 세로의 길이가 모두 1씩 증가한다.
즉 우리가 원하는 메소드 실행 후, 직사각형의 길이는 가로보다 세로가 길어야 한다는 가정이 꺠진다.
따라서 instanceof를 통해 타입 비교를 해야한다.

public void increaseHeight(final Rectangle rectangle) {
        if (rectangle instanceof Square) {
            throw new IllegalStateException();
        }

        if (rectangle.getHeight() <= rectangle.getWidth()) {
            rectangle.setHeight(rectangle.getWidth() + 1);
        }
    }

해당 도형이 정사각형일 경우 예외를 발생시키는 방식으로 코드를 변경할 수 있다.
하지만 이것은 OCP원칙에 어긋나는 코드이다. increaseHeight()가 확장에는 열려있지 않기 때문이다.
따라서 Square 클래스는 Rectangle클래스를 상속받으면 안된다.
아무리 우리가 정사각형은 직사각형이다 라고 이야기해도 위처럼 문제가 발생한다.


4. ISP 인터페이스 분리 원칙

클라이언트가 자신이 이용하지 않는 메서드에 의존하지 않아야 한다는 원칙.

Example

ISP를 위반하는 예를 확인하자

Vechicle은 go(), fly() 메서드를 가진 추상 클래스이다.
AirCraft는 Vechicle을 상속받는다.
Car클래스는 Vechicle을 상속받는다 단 Car는 fly할 수 없기 때문에 fly에서 예외를 발생시킨다.

public class Car extends Vehicle {
    public void go() {
        System.out.println("Go");
    }

    public void fly() {
        throw new IllegalArgumentException("can not fly");
    }
}

Car 클래스는 Vehicle 클래스를 상속받기 때문에 반드시 fly를 구현해야 한다.
하지만 Car class는 fly를 사용하지 않습니다. 이것은 ISP를 위반하는 것이다.

ISP를 적용한다면 다음과 같이 만들 수 있다.

Vehicle 인터페이스를 Movable, Flyable 인터페이스로 나누고 Flyable 클래스에 Movable 클래스를 상속받도록 한다.
Aircraft 클래스가 Flyable 클래스를 상속받도록 한다.
Car 클래스는 Movable 클래스를 상속받도록 한다.
이렇게 되면 Car는 go() 메서드만 구현하면 되고 fly메서드는 구현할 필요가 없어 ISP를 만족하게된다.


5. DIP 의존성 역전 원칙

  • 상위 모듈은 하위 모듈에 의존해서는 안 되고 둘 다 추상화에 의존해야 한다.
  • 추상화는 세부 사항에 의존해서는 안 되고 세부사항(구체적인 구현)은 추상화에 의존해야 한다.

Calculator 클래스가 Add클래스를 사용하여 덧셈을 하는 예시.
여기서 Calculator 클래스는 상위 모듈이고 Add클래스는 하위 모듈

public class Add {
    public int calculate(int num1, int num2) {
        int ret = num1 + num2;
        System.out.printf("%d + %d = %d%n", num1, num2, ret);
        return ret;
    }
}

public class Calculator {
    public void start(int num1, int num2) {
        Add operation = new Add();
        operation.calculate(num1, num2);
    }

    public static void main(String[] args) {
        Calculator cal = new Calculator();
        cal.start(1, 2);
    }
}

여기서 뺄샘을 추가하고 싶다면 Calculator를 수정해야 한다.

DIP를 적용한다면 다음과 같은 구조를 생각할 수 있다.

Calculator는 Add클래스 대신 인터페이스 역할을 하는 CurrentOperation 추상 클래스에 의존하고 있고 구현체인 Add 및 Sub 클래스 또한 CurrentOperation 추상 클래스에 의존하게 되어 DIP를 만족한다.

이렇게 되면 Calculator는 구현체에 의존하지 않기 때문엑 구현 내용이 변경되어도 수정할 필요가 없다.


interface CurrentOperation {
    int calculate(int num1, int num2);
}

class Add implements CurrentOperation {
    public int calculate(int num1, int num2) {
        int ret = num1 + num2;
        System.out.printf("%d + %d = %d%n", num1, num2, ret);
        return ret;
    }
}

class Sub implements CurrentOperation {
    public int calculate(int num1, int num2) {
        int ret = num1 - num2;
        System.out.printf("%d - %d = %d%n", num1, num2, ret);
        return ret;
    }
}

class Calculator {
    private CurrentOperation operation;

    public Calculator(CurrentOperation operation) {
        this.operation = operation;
    }

    public void start(int num1, int num2) {
        this.operation.calculate(num1, num2);
    }
}

public class Main {
    public static void main(String[] args) {
        CurrentOperation operation = new Add();
        Calculator cal = new Calculator(operation);
        cal.start(1, 2);
    }
}
profile
자바 스터디 정리 - 하단 홈 버튼 참조.

0개의 댓글