ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 상속은 인터페이스 상속을 위해 사용 되어야 한다.[오브젝트 chpt 2]
    ProgrammingTheory/OOP 2023. 3. 24. 17:29

    합성을 이해 하고 , 상속은 인터페이스 상속을 위해 사용되어야 한다. 

     

    (마지막 수정일  :  23/03/25)

     

    이글은  오브젝트 2장과 각종 블로그의 정리글 + 인프런 김영한님의 스프링 핵심 원리의 내용을 다루며 글 말에는 어떻게 프로젝트의 적용을 했는지 서술 한다.

     

    오브젝트 2장에서 다룬 개념을 먼저 보자, 전반적인 이해를 위해서는 객체 지향의 사실과 오해의 내용을 읽었으면 한다.

     

     

    [다형성]

    메시지와 메서드는 다른 개념이다.

     

    하나의 객체는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라지며 이를 다형성이라 한다.

     

    [코드의 의존성과 실행 시점의 의존성은 다를 수 있다]

    - Lazy Binding ,Dynamic Binding

     

    public class Movie(){
        public Money calculateMovieFee(Screening screening){
            return fee.minus(discountPolicy.calculateDiscountAmount(screening))
        }
    }

     

    abstract class 

    public abstract class DiscountPolicy{
        public DiscountPolicy(DiscountCondition ... conditions){
            this.conditions = Arrays.asList(conditions);
        }
    }

     

     

     

    코드상으로는 DiscountPolicy에 의존하고 있지만, 실행시점에 Movie 인스턴스는 DiscountPolicy의 구현체인 AmountDiscountPolicy, PercentDiscountPolicy의 인스턴스에 의존하게 된다.

     

    이렇게 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 이해하기 어려워 지지만, 코드는 더 유연해지고 확장에 유리해진다.  SOLID 의 DIP 이다. 추상화에 의존해야지 구체화에 의존하면 안된다.

     

    메시지에 응답하기 위하여 실행되는 메서드를 "컴파일" 시점이 아닌 "실행 시점(RunTime)"시점에 결정하는 것을 

    지연 바인딩(Lazy Binding) 또는 동적 바인딩(Dynamic Binding)이라고 한다.

     

    강조한다 다형성은 이러한 사실을 기반으로 동일한 객체의 메시지에 다르게 응답할 수 있는 능력을 의미한다

     

     

    추가적으로 의존관계를 설계하는데 있어서 DIP 를 생각을 하는것이 좋다.

    예를 들어 보자

    public class MemberServiceImpl implements MemberService {
    
        private final MemberRepository memberRepository = new MemoryMemberRepository();
    	//private final MemberRepository memberRepository = new DBMemberRepository();
    ....
    
    }

     

    MemberSeriveImpl 이라는 MeberService 인터페이스의 구현체의 코드에서 필드 주입을 할때.

     

    MemberRepository 라는 인터페이스에 의존함과 동시에 MemoryMemberRepository 구현체와도 의존하는걸 볼 수있다.

     

    SOLID 원칙의 DIP(Dependency Inversion Principle) : 의존 역전 원칙 을 지켰는가 ? 답은 아니가.

    위의 설명인 인터페이스인 추상화에만 의존해야지 구체화의 의존하면 안된다. 구현체의 의존을 하게되면 변경이 어려워진다.

     

    다음과 같이 수정해주자.

     

    public class MemberServiceImpl implements MemberService {
        
    	private final MemberRepository memberRepository; // 인터페이스만 의존.
        
        public MemberServiceImpl(MemberRepository memberRepository){
            this.memberRepository = memberRepository;
        }
    
    }

     위의, 코도를 보면 구현체의 관한 코드는 없다, 아래의 AppConfig 를 통해 생성자 주입을 한다.

     

    public class AppConfig {
        public MemberService memberService(){
            return new MemberServiceImpl(new MemoryMemberRepository());
        }
        public OrderService orderService(){
            return new OrderServiceImpl(
                    new MemoryMemberRepository(),
                    new FixDiscountPolicy());
        }
    }

     

     

    위와 같은 코드로 구조를 변경시 클라이언트의 (MemberServiceImpl)변경은 최소화 하면서 기능 확장이 가능하다.

     

    -----------------------------------------------------------------------------------------------------------

    -----------------------------------------------------------------------------------------------------------

     

    [구현 상속 VS 인터페이스 상속]

    구현 상속 : implementaion inheritance = sub classing

     

    목적 : 코드를 재사용하기 위한 목적

     

    인터페이스 상속 : interface inheritance. = sub typing

     

    목적 : 부모와 자식이 인터페이스를 공유할 수 있도록 상속을 이용한다.

     

    동일한 코드를 재사용 하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 둘다 사용하는 것이 좋다. 

     

    여기서 합성이란

     

    1. 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함하여 재사용 하는것이다.

     

    2. 인터페이스에 정의된 메시지를 통해 코드를 재사용하는 방법이다.

     

    "상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다.

    클래스를 상속받는 것만이 다형성을 수현할 수 있는 유일한 방법은 아니다."

     

    하나의 타입계층으로 묶음으로서 응집성은 높이고 결합도를 낮출 수 가 있다.

     

    Java 의 추상 클라스인 abstract  클라스와의 차이점으로는 , 추상 메서드를 가지는것은 인터페이스와 동일 하지만,

    필드와 일반 메서드를 가진다는 점에서 인터페이스와 차이가 난다.

     

    예시 :

     

    BaseTimeEntity 는 추상 클라스 이며 시간을 리셋 하는 메서드와 필드를 가진다.

    public abstract class BaseTimeEntity implements Serializable {
    
        @CreatedDate
        @Column(updatable = false)
        private LocalDateTime createdDateTime;
    
        @LastModifiedDate
        private LocalDateTime updatedDateTime;
    
        protected BaseTimeEntity(){}
    
        public void resetTimeNow(){
            this.createdDateTime = LocalDateTime.now();
        }
    
    }

     

     

     

     

    -----------------------------------------------------------------------------------------------------------

     

     

    [상속은 캡슐화를 위반한다]

    상속은 인터페이스 상속을 위해 사용 되어야 한다.

     

    구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드이다. 그 이유로는 다음과 같다.

     상속을 이용하기 위해서는 부모클래스의 구조를 잘 알고 있어야 하며, 결과적으로 부모 클래스의 구현을 자식클래스에 노출하기 때문에 캡슐화가 약화된다.  → 이를 부모와 자식 클라스 간의 강한 결합이 된것이라고 이해를 해도 좋다.

     

    구현 상속은 컴파일 시점에 부모 클래스와 자식 클래스의 관계가 결정되기 때문에 유연한 설계가 불가능하다.

     

    반면 합성과 같이 인스턴스 변수로 관계를 설정하면 객체가 서로 내부 구현에 대해서 알 필요가 없고, 설계가 유연해진다.

     

     

    + 부내용

    [객체를 이용하여 의미를 명시적으로 분명하게 표현 하자.]

    1장에서는 단순하게 long 으로 돈을 표현했다면 2 장에서는 Money class 를 사용했다 

     

    적용하는 예시를 보자 . 예시는 todo list 의 entity 즉 도메인 모델 부분이며  string imageUrl 을 보면 된다.

     

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class PrivateTodo {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "private_todo_id") //pk
        private Long id ;
    
        private String task;
        private LocalDate date;
    
        @JsonIgnore
        private String imageUrl;
        private Boolean complete;
    
        @ManyToOne(fetch = LAZY)
        @JoinColumn(name = "member_id")
        private Member member;
    
        private PrivateTodo(String task,LocalDate date,String imageUrl,Boolean complete,Member member)
        {
            this.task=task;
            this.date=date;
            this.imageUrl = imageUrl;
            this.complete = complete;
            this.member = member;
        }

    imageUrl 에는 todo 관련 사진을 넣을 수 있지만,  validation 처리와 의미의 불분명함 logic 을 모을 수 가 없는 구조이다.

     

     

     

    변경후 코드 이다. 

    TodImage class 를 넣은 모습이다.

    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    @Entity
    public class EventTodo extends BaseTimeEntity {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name="event_todo_id")
        private Long id;
    
        @Embedded
        private TodoImage todoImage;
        .....
        
        
        
        }

     

    Class TodoImage

    package greendar.domain.eventtodo.domain;
    
    import java.util.Objects;
    import javax.persistence.Column;
    import javax.persistence.Embeddable;
    
    
    @Embeddable
    public class TodoImage {
    
        private  static final int MAX_IMAGE_LENGTH = 10_000;
    
        @Column(name = "todo_image_url",nullable = false)
        private  String todoImageUrl;
    
        protected TodoImage(){
    
        }
        public TodoImage(String todoImageUrl){
            validate(todoImageUrl);
            this.todoImageUrl = todoImageUrl;
        }
        private void validate(String todoImageUrl){
            if(todoImageUrl.length() > MAX_IMAGE_LENGTH){
    //            throw  new InvalidImageException();  Need Invalid class in domain for  Exception
                throw  new RuntimeException("Wrong Length");
            }
        }
        @Override
        public boolean equals(Object o){
            if(this == o ){
                return true;
            }
            if(!(o instanceof  TodoImage)){
                return false;
            }
            TodoImage todoImage = (TodoImage) o;
            return Objects.equals(todoImageUrl, todoImage.todoImageUrl);
        }
        @Override
        public int hashCode(){
            return Objects.hashCode(todoImageUrl);
        }
    }

     

    -----------------------------------------------------------------------------------------------------------

     

     

    실무 고민 

     

    인터페이스를 사용하면 오는 장점이 있다.

     

    1. 오브젝트 2장의 예시처럼 , 할인에 관한 정책을 만드는 중에 세부 사항이 확정이 나지 않아도 개발이 가능하다.

     

    2. 데이터 베이스가 정해지지 않은 상황에서 어떤 데이터 베이스로 사용할지 최대한 미루면서 개발이 가능하다.

    ->  인프런 김영한님 스프링 입문 강의 참고

     

    허나, 추상 클라스를 이용하면 다시 한번 런타임에 어떤 객체가 매서드를 실행하는지 다시 한번 봐야하며 , 추상화를 구현하는 비용이 발생한다.  LSP 를 지켜, 인터페이스의 규액을 다 지켰는지 인터페이스를 구현한 구현체는 믿고 사용해도 되는지 테스트 코드가 없다면 확인하기 어렵다.

     

    해결책 : 기능을 확장할 가능성이 없다면, 구현체 클래스를 직접 사용하고 , 향후 꼭 필요할때 리펙터링을 진행해서 인터페이스를 도입하는 것이 좋다.

Designed by Tistory.