ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [오브젝트] 객체지향 프로그래밍
    개발/프로그래밍 2023. 2. 20. 23:19

    객체 지향

    객체 지향은 말 그대로 객체를 지향하는 것이다.

    우리가 OOP를 통해서 개발할 때 클래스를 우선적으로 고려하지만 OOP의 본질은 "객체"가 주된 요소가 되는 것이다.

    이 프로그래밍 패러다임을 위해서는 객체와 객체 사이의 관계를 파악해야 한다.

    이를 기반으로 객체를 추상화하여 클래스를 만들고 그 사이 관계를 정의하는 것이 시작이다.

     


    도메인

    우리는 SW를 개발할 때 기능 등을 통해서 비즈니스 로직을 분류한다.

    이때 로직이 특정 역할을 기준으로 나뉘게 되는데 이 로직을 위해 데이터가 어떠한 역할을 수행하고 그 범위는 어떻게 되는지를 결정하는 것이 도메인이다.

     

    객체지향 프로그래밍 패러다임에서 도메인 또한 마찬가지로 객체, 클래스와 그 관계로 표현된다.

    객체지향이 가지는 장점은 SW를 설계할 때 객체의 추상화를 통해서 도메인을 고려하고 이를 실제 개발 단계까지 원활하게 연결할 수 있다는 것이다.

     


    도메인 예시 - 영화 예매

    영화 예매 다이어그램

    영화 예매를 생각하자

    우리는 영화를 예매해서 관람하기까지의 과정을 절차적인 단계로 생각한다.

    하지만 이를 단순하게 절차적인 단계가 아닌 객체 간의 관계로 생각해볼 수 있다.

    우리는 다음과 같은 객체를 생각해볼 수 있다.

    • 영화관
    • 영화
    • 상영/관람
    • 예매
    • 할인

     

    위 다이어그램의 예시에서는 다음과 같은 객체와 그 관계를 표현했다.

    • 영화
    • 상영
    • 예매
    • 할인
    • 비율 할인
    • 금액 할인
    • 할인 조건
    • 순서 조건
    • 기간 조건

     

    간단하게 분석해보자.

    1. 영화는 그 자체로는 아무것도 할 수 없다.

    2. 관람객이 영화 티켓을 가격, 상영 시간에 맞게 선택하고 예매해야 상영관에서 상영할 수 있다.

    3. 또한, 영화는 1회 상영으로 전부 소비되지 않는다. 여러 번 상영될 수 있다.

     

    이처럼 하나의 영화에 대해 발생할 수 있는 상황과 조건은 여러가지가 있다.

     


    클래스 구현

    /* 상영 */
    public class Screening {
        private Movie movie;
        private int sequence;
        private LocalDateTime whenScreened;
    
        public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
            this.movie = movie;
            this.sequence = sequence;
            this.whenScreened = whenScreened;
        }
    
        public LocalDateTime getStartTime() {
            return whenScreened;
        }
    
        public boolean isSequence(int sequence) {
            return this.sequence == sequence;
        }
    
        public Money getMovieFee() {
            return movie.getFee();
        }
    
        public Reservation reserve(Customer customer, int audienceCount) {
    
            return new Reservation(customer, 
                                    this, 
                                    calculateFee(audienceCount),
                                    audienceCount);
        }
    
        private Money calculateFee(int audienceCount) {
            return movie.calculateMovieFee(this)
                    .times(audienceCount);
        }
    }

     

    상영 클래스를 확인해보자.

    상영할 영화에 대한 정보, 언제, 몇 번째 상영되는 영화인지 등에 관한 속성을 가지고 있다.

    하지만 이에 대한 접근 지정자는 private이므로 직접적으로 접근할 수 없다.

    그저 상영 클래스를 통해 생성한 객체에서 제공하는 메소드만을 이용할 수 있다.

     

    영화는 영화 자체의 정보를 가질 것이다.

    상영 객체 역시 영화 객체를 직접 접근하여 수정할 수 없고 public 지정자로 공개된 부분을 통해서만 접근할 수 있다.

     

    객체 사이에 관계는 있으나 그 독립성이 보장되면 명확한 계층관계가 생겨 개발자가 개발해야하는 기능 역시 명확해진다.

    위 객체를 살펴보면 상영 객체는 결국 상영 정보라는 데이터와 그 데이터를 통한 행위를 가진다.

     

    이렇게 객체가 갖는 정보와 역할을 명시하여 묶는 것이 캡슐화다.

    캡슐화는 접근 제어를 통해 접근을 수정하는 메커니즘을 가진다.

     

    마찬가지로 객체 외부에서 내부 전체를 확인할 수 없고 접근 제어를 통해 공개된 정보만을 확인할 수 있다.

    이것이 구현은닉이다.

     

    이런 개념들이 결국 유지보수성을 늘릴 수 있다.

     


    관계 파악

    public class Screening {
    
        ...
    
        /* 예매 객체 반환 */
        public Reservation reserve(Customer customer, int audienceCount) {
            return new Reservation(customer, this, calculateFee(audienceCount),
                    audienceCount);
        }
    
        /* 요금 계산 */
        private Money calculateFee(int audienceCount) {
            return movie.calculateMovieFee(this).times(audienceCount);
        }
    }

     

    상영 클래스는 내부에서 예매 클래스의 생성자를 통해서 예매 객체를 반환한다.

    또한, calcuateFee 메서드를 통해서 영화의 요금을 계산한다.

     

    public class Money {
        public static final Money ZERO = Money.wons(0);
    
        private final BigDecimal amount;
        ...
    }

     

    요금 객체 역시 내부에서 계산을 위한 메소드를 제공하고 있다.

     

    요금은 일반적으로 정수 값으로 표현될 수 있고 Integer나 Long을 이용할 수 있다.

    다만 이는 객체의 관점에서 단순히 정수를 뜻하고 "요금"의 관점에서 관련 연산을 수행하는 것이 아니다.

    따라서, "요금" 도메인으로 분리하여 관련된 속성과 기능 만을 수행하게 하기 위해 Money 클래스로 분리한다.

    객체 간의 관계와 협력

     

    위의 다이어그램처럼 일련의 동작이 가지는 객체 간의 관계를 표현할 수 있다.

     

    여기서 메시지메소드의 구분할 수 있다.

    메시지는 그저 메시지를 수신자에게 보내어 요청한다.

    메소드는 수신한 요청를 바탕으로 비즈니스 로직의 일부를 처리하고 그에 대한 응답을 주는 것 뿐이다.

     


    바인딩

    동일한 메시지를 수신하더라도 송신자가 결정한 수신 메소드는 다를 수 있다.

    메시지 수신자의 객체에 따라서 메소드의 로직이 달라지고 이는 다형성으로 표현할 수 있다.

    즉, 동일한 인터페이스를 가지는 객체 들이 메시지를 수신받고 그에 따라 다른 응답을 주는 것이다.

     

    런타임 시점에서 메시지와 메소드를 결정하는 것을 동적 바인딩이라고 한다.

    반대로 컴파일 시점에 결정되는 것은 정적 바인딩이다.

     


    상속, 인터페이스

    클래스 다이어그램

     

     

    업캐스팅(Upcasting)

    위 클래스 다이어그램에서 보면 영화 클래스는 할인정책 클래스에 의존관계를 가진다.

     

    컴파일 시점에서는 Movie가 DiscountPolicy에 의존성을 가진다.

    자식들은 런타임 시점에서 결정되지만 결국 부모의 타입을 가지게 된다.

    자식이 부모 타입을 대체하는 것이 업캐스팅(Upcasting)이다.

     

    런타임 시점에서 DiscountPolicy의 상속을 받은 AmountDiscountPolicy나 PercentDiscountPolicy가 결정된다.

    Movie는 생성자를 통해서 AmountDiscountPolicy나 PercentDiscountPolicy를 파라미터로 전달받는다.

    따라서 런타임 시점에서는 Movie는 위 두 클래스에 의존성을 가진다.

     

    여기서 확인할 수 있는 점은 코드 컴파일 단계에서 의존성을 확정지을 수 없다는 것이다.

    이런 의존성의 차이는 코드의 유연성을 높이지만 반대로 이해도는 떨어질 수 있다.

    유연성, 확장성 두 마리 토끼를 모두 잡는 것이 어려운 상황에서는 트레이드 오프를 잘 고려해보자!

     

    인터페이스

    인터페이스는 어떨까?

    인터페이스는 상속과는 엄밀히 다르다.

    상속은 부모의 특징을 그대로 물려받는 것이라면 인터페이스는 부모의 필수 요소를 직접 정의(구현)한다.

     

    인터페이스를 상속 받아 직접 구현한 경우 파라미터로 전달되는 메시지의 타입이나 응답 타입은 동일하다.

    즉, 구현체를 사용하는 로직에 영향을 주지 않는다.

     

    상속을 받아 사용하는 경우에도 발생하는 예외 상황은 조건 분기가 아닌 또 다른 예외처리 자식을 사용하는 것이 좋다.

    이런 확장성을 가지는 것은 Movie 객체가 DiscountPolicy에 의존적이기는 하나 그 자식들에게 종속되지는 않게 할 수 있다.

    이는 컨텍스트 독립성이라고 표현한다.

     


    추상화

    자식이 추상 클래스나 인터페이스 등 부모를 상속 받아 부모의 특징을 가지고 부모에 대한 구현체를 가질 수 있다.

     

    반대로 말하면 부모는 자식이 추상화된 것이라고 할 수 있다.

    추상화를 통해서 특정 도메인이나 개념의 바운더리를 정해놓고 그 세부적인 조건에 따라 다른 자식을 구현할 수 있는 것이 추상화-상속의 장점이다.

     


    객체의 생성

    추상 클래스나 인터페이스를 상속받고 이에 따라 새로운 비즈니스 로직을 오버라이딩하거나 오버로딩하는 등 객체의 의도 변경이 발생한다.

     

    이렇게 클래스에 변동이 올 경우 객체를 생성할 때 매번 올바른 객체를 가질 수 있을까?

    이런 경우 생성자를 강제하면서 의도한 객체를 올바르게 생성하도록 보장할 수 있다.

     


    상속, 추상화가 가지는 단점

    분명 상속이 가지는 장점이 있지만 자식이 부모를 그대로 노출시키거나 부모의 변경에 영향을 받는다는 점에서 캡슐화가 영향을 받게 된다,

    또한 자식은 부모에 의존적이므로 자식 클래스에 의존적인 다른 클래스와는 다르게 부모 클래스와의 관계가 정적 바인딩된다.

     

    public class Movie {
        ...
        private DiscountPolicy discountPolicy;
    
        ...
    
        public void modifyDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    }

     

    생성자를 통해서 강제하더라도 객체가 런타임 시점에 DiscountPolicy를 변경하게 하게 할 수 있다.

    때로는 이런 방식이 보다 유연함을 보여준다.

     


    참고 자료

     

    GitHub - eternity-oop/object: 오브젝트: 코드로 이해하는 객체지향 설계 예제

    오브젝트: 코드로 이해하는 객체지향 설계 예제. Contribute to eternity-oop/object development by creating an account on GitHub.

    github.com

     

    '개발 > 프로그래밍' 카테고리의 다른 글

    [오브젝트] 객체, 설계  (0) 2023.02.12

    댓글

Designed by Tistory.