ABAP Clean Code

ABAP 클린 코드 - Class 클래스 (객체 지향 클래스) [9-1]

Dev Do 2024. 8. 21. 22:04
반응형
“정적인 클래스보다 오브젝트를 사용하라”는 지침은 객체지향 프로그래밍(OOP)에서 정적인(static) 클래스나 정적 메서드를 사용하는 것보다는 객체를 생성하고 인스턴스 메서드를 활용하는 것이 더 바람직하다는 의미입니다. 객체지향 원칙을 잘 활용하면 코드의 유연성과 재사용성을 높일 수 있으며, 유지보수하기 쉬운 구조를 만들 수 있습니다.

 

정적 클래스와 인스턴스 클래스의 차이점

 

정적 클래스

  • 정적 클래스나 정적 메서드는 객체를 생성하지 않고도 클래스 레벨에서 직접 호출할 수 있습니다.
  • 일반적으로 정적 클래스는 상태를 가지지 않고, 유틸리티 메서드나 공통 함수 모음에 자주 사용됩니다.
* 정적 클래스
CLASS lcl_util DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS: calculate_sum
      IMPORTING iv_num1 TYPE i
                iv_num2 TYPE i
      RETURNING VALUE(rv_sum) TYPE i.
ENDCLASS.

CLASS lcl_util IMPLEMENTATION.
  METHOD calculate_sum.
    rv_sum = iv_num1 + iv_num2.
  ENDMETHOD.
ENDCLASS.

DATA(lv_sum) = lcl_util=>calculate_sum( 5, 10 ).

 

인스턴스 클래스

  • 인스턴스 클래스는 객체를 생성하여 인스턴스 메서드와 속성에 접근할 수 있습니다.
  • 객체는 상태를 유지하며, 객체의 데이터를 처리하는 메서드를 가집니다. 이를 통해 보다 유연한 구조를 구현할 수 있습니다.
* 인스턴스 클래스
CLASS lcl_calculator DEFINITION.
  PUBLIC SECTION.
    METHODS: constructor
      IMPORTING iv_num1 TYPE i
                iv_num2 TYPE i,
             calculate_sum RETURNING VALUE(rv_sum) TYPE i.
  PRIVATE SECTION.
    DATA: num1 TYPE i,
          num2 TYPE i.
ENDCLASS.

CLASS lcl_calculator IMPLEMENTATION.
  METHOD constructor.
    num1 = iv_num1.
    num2 = iv_num2.
  ENDMETHOD.

  METHOD calculate_sum.
    rv_sum = num1 + num2.
  ENDMETHOD.
ENDCLASS.

DATA: lo_calculator TYPE REF TO lcl_calculator,
      lv_sum TYPE i.

CREATE OBJECT lo_calculator
  EXPORTING iv_num1 = 5 iv_num2 = 10.

lv_sum = lo_calculator->calculate_sum( ).

 

왜 오브젝트를 사용하는 것이 좋은가?

 

1. 유연성

  • 객체는 상태를 유지할 수 있습니다. 이로 인해 동일한 클래스에서 여러 인스턴스를 생성하여 각각의 상태를 독립적으로 관리할 수 있습니다.
  • 정적 클래스는 상태를 관리할 수 없으므로, 유연한 구조를 만들기 어렵습니다.

2. 재사용성

  • 객체는 재사용 가능성이 높습니다. 클래스에서 객체를 생성함으로써 다양한 상황에 맞게 객체를 활용할 수 있으며, 객체의 메서드와 속성을 다양하게 재사용할 수 있습니다.
  • 정적 클래스는 특정 작업을 수행하는 유틸리티 형태로 주로 사용되며, 코드의 재사용성 측면에서는 제한적입니다.

3. 확장성

  • 객체지향 원칙을 따르는 인스턴스 클래스는 상속, 다형성, 인터페이스 등을 통해 기능을 확장할 수 있습니다. 새로운 요구사항이 생길 때 클래스 구조를 확장하여 처리할 수 있습니다.
  • 정적 클래스는 상속을 통한 확장이 불가능하며, 기능을 확장하려면 새로운 정적 메서드를 추가해야 합니다.

4. 캡슐화

  • 객체는 상태와 동작을 캡슐화하여 외부에 노출하지 않을 수 있습니다. 이를 통해 객체 내부의 상태를 보호하고, 안전하게 데이터를 처리할 수 있습니다.
  • 정적 클래스는 클래스 레벨에서 동작하므로, 상태 관리가 어렵고 캡슐화의 이점을 활용하기 어렵습니다.

5. 테스트 용이성

  • 객체를 사용하면 각 인스턴스를 독립적으로 테스트할 수 있어, 유닛 테스트가 쉬워집니다. 객체의 메서드와 속성을 다양한 시나리오로 테스트할 수 있습니다.
  • 정적 클래스는 상태를 가지지 않기 때문에 테스트가 어렵고, 테스트 시 독립적인 객체를 생성할 수 없습니다.

 

언제 정적 클래스를 사용해야 할까?

  • 유틸리티 클래스: 상태를 유지하지 않고, 단순히 기능적인 작업(예: 문자열 처리, 수학 연산 등)을 수행하는 유틸리티 클래스를 만들 때는 정적 메서드가 적합할 수 있습니다.
  • 상태가 불필요한 상황: 특정 작업을 수행하는 데 상태나 객체 관리가 필요하지 않다면, 정적 메서드로 처리하는 것이 더 간단할 수 있습니다.
“정적인 클래스보다 오브젝트를 사용하라”는 상태 관리와 유연성이 필요한 경우, 정적 클래스보다 객체를 생성하고 인스턴스 메서드를 사용하는 것이 더 바람직하다는 의미입니다. 객체를 사용하면 코드의 유연성, 재사용성, 확장성, 캡슐화, 테스트 용이성이 향상됩니다. 단순한 유틸리티 작업을 처리할 때는 정적 클래스가 유용할 수 있지만, 복잡한 로직이나 상태 관리를 필요로 하는 경우 객체 지향적인 접근이 더 효과적입니다.

 

“클린 상속은 리스코프 대체 원리(Liskov Substitution Principle)“라는 표현은 객체지향 설계 원칙 중 하나인 **리스코프 대체 원리(LSP)**와 상속 관계에서의 설계를 올바르게 적용하라는 의미입니다. 이 원칙은 SOLID 원칙 중 하나로, 클래스 상속이 올바르게 사용되었는지를 평가하는 중요한 기준입니다.

 

 

리스코프 대체 원리(LSP)란?

 

**리스코프 대체 원리(Liskov Substitution Principle)**는 1987년 바바라 리스코프(Barbara Liskov)에 의해 제안된 객체지향 설계 원칙으로, 상위 타입(부모 클래스)이 사용되는 모든 곳에서 하위 타입(자식 클래스)도 문제없이 사용될 수 있어야 한다는 원칙입니다. 쉽게 말해, 자식 클래스는 부모 클래스의 행동을 대체할 수 있어야 하며, 클래스 간의 상속 관계는 기대한 대로 동작해야 한다는 것입니다.

 

이 원칙을 위반하면, 상속 관계에서 예기치 않은 오류가 발생할 수 있고, 유지보수와 확장성이 떨어지는 코드를 작성하게 됩니다.

 

리스코프 대체 원리의 핵심 개념

 

1. 자식 클래스는 부모 클래스의 기능을 깨지 않아야 한다.

  • 자식 클래스가 부모 클래스의 메서드를 재정의하거나 확장할 때, 부모 클래스의 예상 동작을 변경하거나 깨지 않도록 해야 합니다.
  • 자식 클래스는 부모 클래스의 계약(contract)을 준수해야 하며, 이를 위반하면 LSP 원칙을 위배한 것입니다.

2. 클라이언트는 부모 클래스와 자식 클래스를 구분하지 않아야 한다.

  • 클라이언트 코드(사용하는 쪽)는 부모 클래스와 자식 클래스를 구분할 필요가 없어야 합니다. 즉, 부모 클래스를 사용하는 코드가 자식 클래스에서도 동일하게 동작해야 합니다.
  • 자식 클래스는 부모 클래스의 모든 기능을 대체할 수 있어야 하므로, 부모 클래스를 참조하는 코드가 자식 클래스를 사용할 때도 동일하게 동작해야 합니다.

3. 하위 클래스는 상위 클래스의 사양을 확장할 수 있지만, 제한해서는 안 된다.

  • 자식 클래스는 부모 클래스의 기능을 확장할 수 있지만, 그 기능을 제한하거나 제거해서는 안 됩니다. 예를 들어, 부모 클래스에서 지원하는 입력 범위를 자식 클래스가 제한하거나, 부모 클래스에서 가능했던 행동을 자식 클래스가 불가능하게 만들어서는 안 됩니다.

 

리스코프 대체 원리를 준수하는 클린 상속

 

클린 상속은 리스코프 대체 원리를 준수하여, 상속 관계에서 자식 클래스가 부모 클래스를 완전히 대체할 수 있도록 설계하는 것을 의미합니다. 이를 달성하려면 다음과 같은 원칙을 지켜야 합니다.

 

1. 부모 클래스의 계약을 준수하라

  • 자식 클래스가 부모 클래스의 모든 행동을 올바르게 구현해야 합니다. 부모 클래스의 메서드나 속성을 오버라이딩할 때, 부모 클래스의 예상 동작을 위반하지 않도록 주의해야 합니다.

2. 상속보다는 인터페이스와 구성을 고려하라

  • 상속은 강한 결합을 초래할 수 있으므로, 가능한 경우 상속 대신 인터페이스를 구현하거나 구성(composition)을 활용하여 유연한 설계를 유지하는 것이 좋습니다. 인터페이스를 사용하면 클래스 간 결합도를 낮추고, 상위 클래스의 동작을 더 자유롭게 정의할 수 있습니다.

3. 사양 확장 시 예상하지 못한 부작용을 피하라

  • 자식 클래스는 부모 클래스의 기능을 제한하지 않고, 확장해야 합니다. 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 때, 부모 클래스의 계약을 위반하지 않도록 주의해야 합니다.

4. 테스트 기반 설계

  • 부모 클래스와 자식 클래스 모두가 동일한 방식으로 동작하도록 보장하려면 테스트 기반 설계를 고려할 수 있습니다. 부모 클래스에 대한 테스트를 작성하고, 자식 클래스가 부모 클래스의 테스트를 통과하도록 해야 합니다. 이렇게 하면 상속 구조가 예상대로 동작하는지 확인할 수 있습니다.
“클린 상속은 리스코프 대체 원리”라는 말은 상속 관계에서 **리스코프 대체 원리(LSP)**를 준수해야 한다는 것을 의미합니다. 즉, 자식 클래스는 부모 클래스를 완전히 대체할 수 있어야 하며, 부모 클래스와 자식 클래스 간의 기능이 일관되도록 설계해야 합니다. 상속이 필요할 때는 이 원칙을 지켜야 코드의 유지보수성과 확장성을 확보할 수 있으며, 상속을 남용하지 않고 인터페이스와 구성을 통해 더 유연한 설계를 고려하는 것이 좋습니다.

 

 

“상속 대신에 구성을 사용하라”는 객체지향 설계 원칙 중 하나로, 객체의 기능을 확장하거나 재사용할 때 상속(inheritance)을 사용하는 것보다 구성을 통해 유연하게 처리하라는 의미입니다. 이 원칙은 객체지향 설계에서 상속이 가지는 단점을 보완하고, 더 나은 구조와 재사용성을 보장하기 위한 방법으로 사용됩니다.

 

상속과 구성의 차이점

 

1. 상속(Inheritance)

  • 상속은 클래스 간의 부모-자식 관계를 정의하는 것으로, 자식 클래스가 부모 클래스의 속성 및 메서드를 상속받아 사용하는 방식입니다.
  • 자식 클래스는 부모 클래스의 기능을 상속받아 그대로 사용하거나, 오버라이딩을 통해 일부 기능을 수정할 수 있습니다.
  • 상속은 계층 구조를 형성하며, 한 번 상속받은 클래스는 부모 클래스에 강하게 결합됩니다.

2. 구성(Composition)

  • 구성은 다른 객체를 참조하여 객체를 구성하는 방식입니다. 즉, 객체는 다른 객체를 자신의 속성으로 포함하고, 해당 객체의 기능을 호출합니다.
  • 구성을 사용하면 객체 간의 결합도가 낮아지며, 서로 독립적인 객체들이 협력하여 동작할 수 있습니다.
  • 구성은 상속보다 더 유연하게 객체 간 관계를 정의할 수 있으며, 객체의 재사용성과 확장성을 높일 수 있습니다.

왜 구성을 사용하는 것이 더 나을까?

 

1. 유연성

  • 상속은 부모 클래스와 자식 클래스 간에 강한 결합을 만듭니다. 부모 클래스의 변경이 자식 클래스에 영향을 미칠 수 있으며, 자식 클래스는 부모 클래스의 구조에 종속됩니다. 반면, 구성은 결합도를 낮춰주며, 객체 간의 관계를 더 유연하게 정의할 수 있습니다.
  • 구성을 사용하면 필요한 객체를 쉽게 교체하거나 확장할 수 있습니다. 예를 들어, 다른 객체를 대체하거나 추가 기능을 추가할 때 객체 간 관계만 변경하면 되므로 상속 구조보다 더 유연합니다.

2. 재사용성

  • 구성을 사용하면 코드 재사용성이 높아집니다. 같은 기능을 가진 여러 객체를 구성으로 사용하여 중복 코드를 줄일 수 있으며, 기능이 변경될 때 특정 객체만 수정하면 됩니다.
  • 상속 구조는 재사용성을 제한할 수 있습니다. 상속 계층이 깊어질수록, 부모 클래스의 변경이 자식 클래스에 영향을 미치므로, 재사용이 어려워질 수 있습니다.

3. 단일 책임 원칙(SRP)

  • 구성은 단일 책임 원칙을 더 잘 지원합니다. 각 클래스가 자신의 역할과 책임을 가지며, 객체 간 협력을 통해 기능을 구현하기 때문에 클래스가 특정 역할에 집중할 수 있습니다.
  • 상속을 사용하면 자식 클래스가 부모 클래스의 기능을 상속받아 불필요한 책임을 가지게 될 수 있습니다. 이는 클래스가 너무 많은 책임을 가지게 되어 단일 책임 원칙을 위반하게 만들 수 있습니다.

4. 테스트 용이성

  • 구성을 사용하면 개별 객체를 독립적으로 테스트할 수 있습니다. 객체 간 결합도가 낮기 때문에, 특정 객체만 테스트하거나 목(mock) 객체를 사용하여 테스트를 쉽게 수행할 수 있습니다.
  • 상속 구조에서는 부모 클래스의 변경이 자식 클래스의 동작에 영향을 미치므로, 테스트하기가 더 어려워질 수 있습니다.

5. 상속의 한계

  • 상속은 단일 상속만 허용되는 경우가 많습니다. 따라서 여러 부모 클래스로부터 기능을 상속받는 것이 어렵습니다. 반면, 구성은 여러 객체를 포함하여 각각의 기능을 조합할 수 있습니다.
  • 상속은 자식 클래스가 부모 클래스의 구현 세부 사항에 의존할 수 있으므로, 부모 클래스의 수정이 자식 클래스의 동작에 영향을 미치는 문제가 발생할 수 있습니다.
* 상속을 사용하는 경우 클래스
CLASS lcl_animal DEFINITION.
  PUBLIC SECTION.
    METHODS: make_sound.
ENDCLASS.

CLASS lcl_animal IMPLEMENTATION.
  METHOD make_sound.
    WRITE: / 'Animal sound'.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_dog DEFINITION INHERITING FROM lcl_animal.
  PUBLIC SECTION.
    METHODS: make_sound REDEFINITION.
ENDCLASS.

CLASS lcl_dog IMPLEMENTATION.
  METHOD make_sound.
    WRITE: / 'Bark'.
  ENDMETHOD.
ENDCLASS.

 

* 구성을 사용하는 클래스
CLASS lcl_animal DEFINITION.
  PUBLIC SECTION.
    METHODS: make_sound.
ENDCLASS.

CLASS lcl_animal IMPLEMENTATION.
  METHOD make_sound.
    WRITE: / 'Animal sound'.
  ENDMETHOD.
ENDCLASS.

CLASS lcl_dog DEFINITION.
  PUBLIC SECTION.
    METHODS: constructor,
             make_sound.
  PRIVATE SECTION.
    DATA: lo_animal TYPE REF TO lcl_animal.
ENDCLASS.

CLASS lcl_dog IMPLEMENTATION.
  METHOD constructor.
    CREATE OBJECT lo_animal.
  ENDMETHOD.

  METHOD make_sound.
    lo_animal->make_sound( ).
    WRITE: / 'Bark'.
  ENDMETHOD.
ENDCLASS.

 

“상속 대신에 구성을 사용하라”는 객체 간의 관계를 설정할 때 상속보다는 구성을 사용하는 것이 더 유연하고 재사용 가능하다는 의미입니다. 구성을 사용하면 객체 간 결합도가 낮아지며, 코드의 유지보수성과 테스트 용이성이 향상됩니다. 상속은 특정 상황에서 유용할 수 있지만, 복잡한 상속 계층 구조를 피하고, 구성을 통해 객체지향 원칙을 잘 활용하는 것이 더 좋은 설계 방법이 될 수 있습니다.

 

“동일한 클래스에서 상태 저장과 비저장을 혼재하지 마라”는 객체지향 프로그래밍에서 클래스 설계 시 상태를 저장하는 메서드와 상태를 저장하지 않는 메서드를 혼합하지 말라는 의미입니다. 이 지침은 클래스의 일관성과 가독성을 높이고, 유지보수성을 향상시키기 위한 원칙입니다. 클래스는 상태를 유지하는지, 아니면 순수하게 작업만 수행하는지 명확히 해야 합니다.

 

상태 저장 클래스와 비저장 클래스의 차이점

 

상태 저장 클래스

  • 클래스가 상태(즉, 인스턴스 변수의 값)를 저장하고, 그 상태를 기반으로 메서드가 동작하는 클래스입니다.
  • 이러한 클래스는 객체가 생성된 이후에도 상태를 유지하며, 다른 메서드가 이 상태를 읽거나 수정할 수 있습니다.
* 상태 저장 클래스
CLASS lcl_counter DEFINITION.
  PUBLIC SECTION.
    METHODS: increment,
             get_value RETURNING VALUE(rv_value) TYPE i.
  PRIVATE SECTION.
    DATA: counter TYPE i.
ENDCLASS.

CLASS lcl_counter IMPLEMENTATION.
  METHOD increment.
    counter = counter + 1.
  ENDMETHOD.

  METHOD get_value.
    rv_value = counter.
  ENDMETHOD.
ENDCLASS.

 

비저장 클래스

  • 클래스가 상태를 저장하지 않고, 주로 입력을 받아 작업을 수행하는 클래스입니다. 이러한 클래스는 상태에 의존하지 않고, 각 메서드 호출 시 필요한 값을 매개변수로 받아 처리합니다.
* 상태 비저장 클래스
CLASS lcl_math DEFINITION.
  PUBLIC SECTION.
    METHODS: add IMPORTING iv_num1 TYPE i
                        iv_num2 TYPE i
              RETURNING VALUE(rv_sum) TYPE i.
ENDCLASS.

CLASS lcl_math IMPLEMENTATION.
  METHOD add.
    rv_sum = iv_num1 + iv_num2.
  ENDMETHOD.
ENDCLASS.

 

왜 상태 저장과 비저장을 혼재하면 안 되는가?

 

1. 클래스의 일관성 저하

  • 상태 저장과 비저장 로직이 혼재된 클래스는 그 동작을 예측하기 어려워집니다. 클래스의 메서드 중 일부는 상태를 유지하고, 일부는 그렇지 않다면, 클래스가 어떻게 작동하는지 직관적으로 파악하기 어려워집니다.
  • 이는 객체의 사용 방식에 대한 혼란을 초래할 수 있으며, 객체의 의도를 명확하게 전달하지 못합니다.

2. 유지보수의 어려움

  • 상태를 저장하는 메서드와 비저장 메서드가 혼합되면, 유지보수가 어려워집니다. 예를 들어, 상태 저장 메서드가 객체의 상태를 변경한 후 비저장 메서드가 호출될 때, 예기치 않은 동작이 발생할 수 있습니다.
  • 이러한 구조는 디버깅이 복잡해지고, 오류 발생 시 원인을 찾기 어려워집니다.

3. 예측 불가능한 동작

  • 상태를 저장하는 메서드가 클래스를 사용하는 동안 객체의 상태를 변경하고, 다른 메서드가 이 상태에 의존하면, 코드가 예측하기 어려운 동작을 하게 됩니다. 이는 테스트가 어렵고, 버그 발생 가능성을 높입니다.

4. 단일 책임 원칙(SRP) 위반

  • 상태 저장과 비저장을 혼합하면 클래스가 여러 책임을 가지게 됩니다. 상태를 관리하면서 동시에 작업을 수행하는 것은 단일 책임 원칙을 위반하게 되며, 클래스를 복잡하게 만듭니다.

상태 저장과 비저장을 분리하는 방법

 

1. 클래스를 분리하라

  • 상태 저장이 필요한 로직과 비저장 로직을 별도의 클래스로 분리하여, 각각의 클래스가 명확한 역할을 가지도록 해야 합니다. 상태를 저장하는 클래스는 상태 관리에 집중하고, 비저장 클래스는 순수하게 작업만 처리하도록 설계합니다.

2. 인터페이스 활용

  • 상태를 저장하거나 비저장 메서드를 다루는 인터페이스를 정의하여, 특정 객체가 상태 저장 클래스인지 비저장 클래스인지를 명확히 구분할 수 있습니다. 인터페이스를 통해 역할을 분리하면, 클래스가 특정 역할에 집중할 수 있습니다.

3. 팩토리 패턴 사용

  • 상태 저장 객체와 비저장 객체를 생성하는 팩토리 패턴을 사용하여, 객체 생성 시점에서 상태 관리 여부를 명확히 결정할 수 있습니다. 이를 통해 객체 생성 과정에서 상태와 비저장을 분리할 수 있습니다.
“동일한 클래스에서 상태 저장과 비저장을 혼재하지 마라”는 클래스 설계 시 상태를 관리하는 메서드와 비저장 메서드를 한 클래스에 혼합하지 말고, 각각의 역할에 맞게 클래스를 분리하라는 의미입니다. 이렇게 하면 클래스가 더 명확한 책임을 가지게 되며, 코드의 가독성과 유지보수성이 향상됩니다. 객체지향 설계 원칙을 잘 활용하여 클래스의 역할과 책임을 명확히 하고, 예측 가능한 동작을 구현하는 것이 중요합니다.
반응형