[Programming Paradigm] 객체지향프로그래밍이란?

dyomi·2024년 7월 3일
post-thumbnail

객체지향 프로그래밍이란?

객체지향 프로그래밍 Object-Oriented Progamming(OOP) 이란, 프로그래밍의 패러다임 중 하나로 여러개의 객체를 구성해 설계하고 구현하는 방식을 말한다.

여기서 객체란, 실세계에 존재하거나 생각할 수 있는 모든 것을 말하며, 프로그래밍에서 필요한 데이터를 추상화시켜 그 데이터가 가진 상태와 행위를 포함하는 독립적인 단위를 말한다.

왜 객체지향프로그래밍이란 패러다임이 나오게 되었을까?

우선 객체지향 프로그래밍은 인간 친화적이라고 볼 수 있다.
실세계에 존재하는 모든 것을 객체라는 단위로 표현해서 부품을 조립하듯이 구성할 수 있기 때문에 굉장히 직관적이고, 실생활에 사용되는 개념을 그대로 코드에 적용해서 작성할 수 있다.

다른 프로그래밍 패러다임과의 차이

객체지향 언어는 객체라는 개념을 중심으로 데이터를 구조화하고 기능을 구현하는 방식으로, 코드의 재사용성과 유지보수를 향상시킨다.

반면 절차지향 프로그래밍은 말 그대로 실행 순서와 절차가 더 중점이며, 코드의 순서가 바뀌면 동일한 결과를 보장하기가 어렵고 유지보수가 어렵다.

또한 함수형 프로그래밍은 함수 조합과 불변성을 중시하는 등 각기 프로그래밍 패러다임마다 다른 접근 방식을 취하고 있다.

절차지향 프로그래밍이라고 해서 객체를 다루지 않는 것이 아니고, 객체지향 프로그래밍이라고 해서 절차가 없는 것도 아니다.

즉, 서로 반대되는 개념이 아니라, 각 프로그래밍 패러다임들이 초점을 맞추고 있는 부분이 다를 뿐이다.

객체지향 프로그래밍의 장점은 다양하다.

  1. 코드의 재사용성이 용이하다. 미리 만들어 놓은 클래스를 가져와 사용 할 수 있고, 상속을 통해서 개념을 확장할 수도 있다.

  2. 코드가 객체 단위로 나뉘어져있어 수정이 용이하고 유지보수가 쉽다.
    캡슐화라는 개념을 사용해서 내부 구현을 숨기고 인터페이스를 통해 객체에 접근한다면, 외부 코드에 영향을 주지 않고 내부 구현을 수정할 수도 있다.

  3. 새로운 기능을 추가할때 기존 코드의 변경 없이 새로운 클래스를 추가하는 방식으로 시스템 확장이 가능하다.

  4. 클래스 단위로 모듈화시켜 개발이 가능하고, 프로그램을 독립적인 객체로 분할해서 논리적인 단위로 구성할 수 있다.

반면 객체지향 프로그래밍의 단점도 존재한다.

  1. 초기 설계 단계에서 객체간의 상호작용과 상속 관계등을 설계하는데 복잡하고 시간이 많이 소요된다.

  2. 메모리와 CPU자원을 더 많이 소모할 수 있고, 객체의 생성과 소멸, 메서드 호출 등으로 성능 저하가 발생할 수 있다.

그렇다면 이러한 객체 지향적인 설계를 잘하기 위해선 어떻게 해야할까?

객체지향 설계를 하면서 지켜야 하는 원칙 5가지가 존재한다.

SOLID 원칙이라고도 불리며, 이 이름은 각각의 원칙들의 앞글자를 따서 만든 이름이다.

1. 단일 책임 원칙 Single Responsibility Principle(SRP)

단일 책임 원칙은 하나의 모듈이 변경되어야 하는 이유는 한가지여야 한다는 의미이다. 즉, 오직 하나의 액터에 대해서만 책임을 져야한다는 뜻이다.
이 원칙을 지키면 변경이 필요할때 수정할 대상이 명확해질 수 있다.

수정 전,

// SRP 위반: 두 가지 책임 (파일 읽기, 데이터 처리)을 가진 클래스
public class DataHandler {
    public String readFile(String filePath) {
        // 파일 읽기 로직
    }

    public void processData(String data) {
        // 데이터 처리 로직
    }
}

수정 후,

// SRP 준수: 각각의 책임을 별도의 클래스로 분리
public class FileReader {
    public String readFile(String filePath) {
        // 파일 읽기 로직
    }
}

public class DataProcessor {
    public void processData(String data) {
        // 데이터 처리 로직
    }
}

2. 개방-폐쇄 원칙 Open-Closed Principle(OCP)

개방 폐쇄 원칙은 확장에는 열려있고, 수정에는 닫혀있어야 한다는 원칙이다. 이를 위해 구체적인 구현체보단 추상화된 객체에 의존함으로써 기존 코드의 변경 없이 시스템 확장을 가능케한다.

수정 전,

// OCP 위반: 새로운 할인 정책 추가 시 기존 코드를 수정해야 함
public class DiscountService {
    public double calculateDiscount(String type, double amount) {
        if (type.equals("seasonal")) {
            return amount * 0.1;
        } else if (type.equals("clearance")) {
            return amount * 0.5;
        }
        return 0;
    }
}

수정 후,

// OCP 준수: 인터페이스를 통해 확장 가능하게 설계
public interface DiscountPolicy {
    double calculateDiscount(double amount);
}

public class SeasonalDiscount implements DiscountPolicy {
    public double calculateDiscount(double amount) {
        return amount * 0.1;
    }
}

public class ClearanceDiscount implements DiscountPolicy {
    public double calculateDiscount(double amount) {
        return amount * 0.5;
    }
}

public class DiscountService {
    private DiscountPolicy discountPolicy;

    public DiscountService(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    public double calculateDiscount(double amount) {
        return discountPolicy.calculateDiscount(amount);
    }
}

3. 리스코프 치환 원칙 Liskov Substitution Principle(LSP)

리스코프치환 원칙은 하위 타입은 상위 타입을 대체할 수 있어야 한다는 의미이다. 자식 클래스는 최소한 부모 클래스에서 가능한 행위는 수행이 보장되어야 한다는 의미로 볼 수 있다.
또한 부모의 의도와 다르게 자식 클래스가 구현되는 일을 방지할 수 있다.

수정 전,

// LSP 위반: 서브 클래스가 부모 클래스의 동작을 제대로 수행하지 못함
public class Rectangle {
    protected int width;
    protected int height;

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

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

    public int getArea() {
        return width * height;
    }
}

public class Square extends Rectangle {
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 사각형 규칙을 깨트림
    }

    public void setHeight(int height) {
        this.width = height;
        this.height = height; // 사각형 규칙을 깨트림
    }
}

수정 후,

// LSP 준수: 서브 클래스가 부모 클래스의 동작을 온전히 수행
public abstract class Shape {
    public abstract int getArea();
}

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

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public int getArea() {
        return width * height;
    }
}

public class Square extends Shape {
    private int side;

    public Square(int side) {
        this.side = side;
    }

    public int getArea() {
        return side * side;
    }
}

4. 인터페이스 분리 원칙 Interface Segregation Principle(ISP)

인터페이스 분리 원칙은 클라이언트의 목적과 용도에 적합한 인터페이스만을 제공하는 것을 의미한다.
이렇게 되면 다른 클라이언트로부터 불필요한 간섭을 최소화하고 변경에 대한 영향을 더욱 세밀하게 제어할 수 있다.

수정 전,

// ISP 위반: 인터페이스가 너무 많은 책임을 가짐
public interface Worker {
    void work();
    void eat();
}

public class OfficeWorker implements Worker {
    public void work() {
        // 작업 수행
    }

    public void eat() {
        // 식사
    }
}

public class RobotWorker implements Worker {
    public void work() {
        // 작업 수행
    }

    public void eat() {
        // 로봇은 먹지 않음, 불필요한 메서드
    }
}

수정 후,

// ISP 준수: 인터페이스를 분리
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public class OfficeWorker implements Workable, Eatable {
    public void work() {
        // 작업 수행
    }

    public void eat() {
        // 식사
    }
}

public class RobotWorker implements Workable {
    public void work() {
        // 작업 수행
    }
}

5. 의존성 역전 원칙 Dependency Inversion Principle(DIP)

의존 역전 원칙은 고수준의 모듈은 저수준의 모듈 구현에 의존해서는 안되며, 저수준 모듈은 고수준 모듈에 의존해야 한다는 것이다.
여기서 고수준 모듈은 입력과 출력으로부터 먼 추상화 된 모듈이며, 저수준 모듈은 입출력으로부터 가까운 (HTTP, 데이터베이스, 캐시 등) 구현 모듈을 의미한다.
즉, 비즈니스와 관련된 부분이 세부 사항에는 의존하지 않는 설계 원칙을 뜻한다.

수정 전,

// DIP 위반: 구체적인 클래스에 의존
public class LightBulb {
    public void turnOn() {
        // 전구 켜기
    }

    public void turnOff() {
        // 전구 끄기
    }
}

public class Switch {
    private LightBulb bulb;

    public Switch(LightBulb bulb) {
        this.bulb = bulb;
    }

    public void operate() {
        // 스위치 작동
        bulb.turnOn();
    }
}

수정 후,

// DIP 준수: 추상화에 의존
public interface Switchable {
    void turnOn();
    void turnOff();
}

public class LightBulb implements Switchable {
    public void turnOn() {
        // 전구 켜기
    }

    public void turnOff() {
        // 전구 끄기
    }
}

public class Switch {
    private Switchable device;

    public Switch(Switchable device) {
        this.device = device;
    }

    public void operate() {
        // 스위치 작동
        device.turnOn();
    }
}

모든 프로젝트에 이 5가지 원칙을 반드시 적용할 필요는 없다. 프로젝트에 적용할 원칙의 수는 코드의 구성에 따라 다르다고 보면 된다. 각 원칙은 특정 문제를 해결하기 위한 지침일 뿐이며, 만일 코드에 해당 문제가 없으면 원칙을 적용할 이유가 없다.

정리하자면,

이러한 5가지 원칙의 핵심은 결국 추상화와 다형성이라고 볼 수 있다.
구체적인 클래스에 의존하지 않고 추상 클래스 혹은 인터페이스에 의존함으로써 유연하고 확장가능한 객체 지향적인 애플리케이션을 만들 수 있는 것이다.

그 외 소프트웨어 개발 3대 원칙 - KISS, YAGNI, DRY

SOLID 외에도 다른 개발 원칙들이 존재한다.

그 외 유명한 개발 원칙으로 KISS(키스), DRY(드라이), YAGNI(야그니)가 존재한다.

KISS는 keep it simple stupid의 약자로 단순함에 중점을 둔 원칙이다.

DRY는 don't repeat yourself의 약자로 중복을 최소화한다는 원칙이다.

YAGNI는 you aren't gnna need it으로 현재 필요하지 않는 기능은 추가하지 않는다는 원칙이다.



🌟 추가 질문

📍 SOLID 등 개발 원칙들이 나오게 된 이유는?

솔리드라는 원칙이 없었을 때 개발자들이 여러 방식으로 코딩을 했을 것이다. 그러면서 공통적으로 반복돼서 생기는 문제들이 발생하고 이 문제를 해결하는 방법으로 이러한 원칙들이 생겨나게 될 것이다.


📍 SOLID 원칙을 잘 지키는 방법?

개발을 다 끝낸 뒤, S, N, D 하나씩 확인하는 방법은 비효율적일 것이다.
하지만 한번에 모든 원칙을 지켜가며 코드를 작성하는 것은 어렵다.

우선 하나씩 S면 단일원칙만 생각하면서 코드를 짠다. 그럼 습관화가 될 것이고 어느 순간 이 SRP 원칙을 생각하지 않더라도 그런 방향으로 코드를 짤 것이다. 그리고 하나씩 늘려가는 방법이 좋지 않을까?

그리고 이 모든 사항은 권유 사항이지 반드시 답은 아니다. 모든 원칙이 모든 상황을 커버해주진 않는다.

(하지만 처음에는 솔리드 원칙을 항상 지키며 코드를 짜는 것이 오히려 좋을 수 있다.)



참고 자료
[OOP] 객체지향 프로그래밍의 5가지 설계 원칙, 실무 코드로 살펴보는 SOLID
객체 지향 프로그래밍이 뭔가요? (꼬리에 꼬리를 무는 질문 1순위, 그놈의 OOP)
절차지향(Procedural Programming), 객체지향(Object Oriented Programming) 장단점 및 차이점

profile
기록하는 습관

0개의 댓글