객체지향과 SOLID

이진우·2024년 7월 25일

객체지향 프로그래밍 (Object-Oriented Programming, OOP)

캡슐화

변수와 메서드를 클래스로 묶어 독립적으로 동작하지않도록 하거나 불필요한 정보를 노출시키지않습니다.

    1. 코드의 유지보수성을 향상
    1. 객체의 내부구현을 외부로부터 숨김
    1. 객체의 내부상태를 제어하고, 잘못된 접근으로부터 보호함

    캡슐화 예시 - DTO
    대표적인 예시중 하나로, 필드를 private 로 접근을 제어하고 그필드에 사용하기위한 getter, setter를 만들어 필드에 접근하는 방식

상속

extends
부모클래스가 가지고있는것을 자식클래스가 확장하는 개념

    1. 코드의 재사용을 높이고, 중복을 최소화(코드의 재사용성 증가)
    1. 계층적인 구조를 통해 객체간의 관계를 나타낼 수 있음

추상화

추상 메서드, 인터페이스, 추상 클래스
구체적인 사실들을 일반화시켜 기술하는 개념으로 필요한 공통점을 추출하고 불필요한 공통점을 제거하는 과정

    1. 복잡한 시스템을 단순화 하고 모델링이 가능합니다.

추상화 vs 상속

다형성

오버로딩, 오버라이딩,...

    1. 코드의 재사용성을 높이고, 중복을 최소화
    1. 계층적인 구조를 통해 객체간의 관계를 나타낼 수 있음

오버로딩(Overloading)

같은 이름의 메서드를 여러개의 정의 하되, 매개변수의 타입 또는 개수가 다르도록 정의하는 개념

 public class Test {

    public static void main(String[] args) {

        Test calc = new Test();
        System.out.println("add(int a, int b) = " + calc.add(1,2));
        System.out.println("add(double a, double b) = " + calc.add(1.5,2.5));
        System.out.println("add(int a, int b, int c) = " + calc.add(1,2,3));
    }

    public int add(int a, int b) {
        return a + b;
    }
    public double add(double a, double b) {
        return a + b;
    }
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

오버라이딩 (Overriding)

자식 클래스가 부모클래스의 메서드를 재정의 하는 행위

@Overriding 어노테이션

부모 클래스의 메서드를 재정의(오버라이딩) 하고 있음을 컴파일러에게 알려주는 역할을 수행하는 어노테이션

  • 사용 이유

    1. 명확성 제공 : 해당 메서드가 재정의 되고 있음을 명확하게 표시하기 위해
    2. 컴파일 검사 : 실제로 부모클래스의 메서드를 재정의하고 있는지 컴파일러가 검사하여 개발자의 실수 등의 의한 오류를 사전에 방지하기 위해

SOLID

단일 책임의 원칙 (SRP, Single Responsibility Principle)

하나의 객체는 반드시 하나의 책임(기능, 동작) 을 가져야합니다.

- 단일책임의 원칙 위반 예시

// 단일 책임의 원칙 위반
public Employee(double salary) {
        this.salary += salary;
        generatePromotionLetter();
    }

    public void generatePromotionLetter() {
        System.out.println("Promotion lefter for" + name) ;
        System.out.println("with new salary : " + salary);
    }

Employee 클래스는 직원과 관련된 데이터 및 기능을 담당하여야 하며 프로모션 레터를 생성하는 기능도 포함하고 있기 때문에 SRP에 위반함

- 단일 책임의 원칙 적용 후

class PromotionLetterGenerator {
    public void generatorPromotionLetter(Employee employee) {
        System.out.println("Promotion lefter for" + employee.name) ;
        System.out.println("with new salary : " + employee.salary);
    }
}

프로모션 레터 생성 기능을 담당하는 별도의 클래스로 분리하여 각각의 클래스는
하나의 책임(기능)을 가지도록 적용

개방 폐쇄의 원칙 (OCP, Open-Closed Principle)

객체의 확장은 개방적으로 열려 있어야 하며, 객체의 수정은 폐쇄적으로 닫혀 있어야 합니다
-> 기능을 추가할 때 기존 코드를 수정하지 않고, 새로운 코드를 추가하여 확장할 수 있는 확장성을 가져야 합니다.

- 개방 폐쇄의 원칙 위반 예시

class PromotionLetterGenerator {
    public void generatePromotionLetter(Employee employee) {
        System.out.println("Promotion lefter for" + employee.name) ;
        System.out.println("with new salary : " + employee.salary);
    }
}

정규직과 계약직 직원에 대하여 아래와 같이 프로모션 레터를 따로 생성해야할 경우
어떻게 코드를 수정해야할 것인가?

  1. 정규직 직원 : “promotion letter 이름 with new salary : 연봉”
  2. 계약직 직원 : “Contract promotion letter 이름 with new contract salary : 연봉”

- 개방 폐쇠의 원칙 적용 후

class  FullTimeEmployee extends Employee {

    public FullTimeEmployee(String name, double salary) {
        super(name, salary);
    }

    @Override
    public void generatePromotionLetter() {
        System.out.println("Promotion lefter for" + name) ;
        System.out.println("with new salary : " + salary);
    }
}

기존 Employee 클래스의 generatorPromotionLetter의 기존 코드를 수정하지 않고
상속받음으로써 새로운 코드를 추가하여 확장을 할 수 있도록 적용

인터페이스 분리의 원칙 (ISP, Interface segregation principle)

객체는 자신이 호출하지 않는 메소드의 의존하지 않아야 한다는 원칙입니다.
-> 인터페이스를 상속 받을 때 사용되지 않는 메소드가 없어야 합니다.

- 인터페이스 분리의 원칙 위반 예시

interface EmployeeOperation {
    void promote(double increase);
    void generatePromotionLetter();
    double calculateBounds();
}

EmployeeOperations 인터페이스를 상속 받아야 하고, 아래와 같이 메서드를 사용해야 할 경우 사용되지 않는 메서드가 있으니 ISP에 위반함

1. Employee 클래스 : promote()
2. FullTimeEmployee 클래스 : generatePromotionLetter(), calculateBonus()
3. ContractEmployee 클래스 : generatePromotionLetter()
4. Intern 클래스 : generatePromotionLetter()

즉, 안쓰는 메서드는 분리 시켜야 한다.

- 인터페이스 분리 원칙 적용 후

interface promotable {
    void promote(double increase);
}
interface PromotionLetterGeneraTable {
    void generatePromotionLetter();
}
interface  BoundsCalculateTable {
    double calculateBounds();
}

이처럼 각 기능별로 인터페이스를 분리하여 필요한 인터페이스만 상속받아 사용합니다.

리스코프 치환의 원칙 (LSP, Liskov substitution principle)

자식 클래스는 언제나 부모 클래스 타입으로 교체할 수 있어야 함
-> 자식 객체는 부모 객체를 완전하게 대체할 수 있어야 함

다음 슬라이스에서 리스코프 치환의 원칙을 지키기 위해 인터페이스 상속을
Employee에서 받은 후 추상 메서드들을 오버라이딩하여 기본 값 설정

public class Employee implements Promotable, BoundsCalculateTable {


    public String name;
    public double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public void promote(double increase) {
        this.salary += increase;
    }

    @Override
    public double calculateBounds() {
        return 0;
    }

인터페이스 분리 원칙에서 분리해둔 인터페이스 두개를 상속받아
Employee에 추상 메서드를 적습니다.

의존 역전의 원칙 (DIP, Dependency Inversion Principle)

고수준 모듈이 저수준 모듈에 의존하지 않도록 해야 함
단, 의존 역전의 원칙은 구현체(세부 사항)가 아닌 추상화에 기반을 두기 때문에
고수준 모듈과 저수준 모듈은 모두 추상화에 의존해야 함

- 고수준 모듈

정책(특정 기능)을 정의하는 모듈로써 주로 비즈니스 로직을 담당하게
되며 MVC 패턴에서는 Service 계층의 인터페이스를 고수준 모듈이라고 할 수 있음
실습 코드에서는 HRDepartment 클래스가 고수준 모듈로써의 역할을 수행함

class HRDepartment {
    private Promotable promotable;
    public void processPromotion(Promotable promotable) {
        this.promotable = promotable;
    }
    
    public void processPromotion(double increase) {
        promotable.promote(increase);
    }
}

- 저수준 모듈

정책의 세부적인 기능들을 구현하여 구체적인 작업을 수행하는 모듈
실습 코드에서는 HRDepartment 클래스를 제외한 나머지 클래스들이 저수준 모듈

@Override
    public void promote(double increase) {
        this.salary += increase;
    }

    @Override
    public double calculateBounds() {
        return 0;
    }

- 의존 역전 원칙 적용 위반

class HRDepartment {
    public void processPromotion(Employee employee, double increase) {
        employee.promote(increase);
    }
}

고수준 모듈이 저수준 모듈에 의존하지 않으며 추상화 계층에 의존하고있습니다.
만약 적용을 하게 된 정상적인 의존역전 원칙 방식은 고수준 모듈에 자료처럼 고수준 모듈이 저수준 모듈에 의존하지 않으며 추상화 계층에 의존합니다.

profile
개발자 응애입니다

0개의 댓글