객체 지향 프로그래밍

SINHOLEE·2020년 6월 27일
1

1. 객체지향 프로그래밍

  • 기능을 제공한다.

  • 절차지향은 데이터 중심, 객체지향은 기능 중심

  • 객체 = 데이터 + 프로시져(오퍼레이션, 메소드, 함수)

  • 가장 중요한 키워드 1. 정보은닉 2. 유연성

  • 객체지향의 정의 한마디로: 유지보수성이 뛰어난 프로그래밍 설계 방식 3요소 5규칙(java)을 지키면서 오는 잇점을 바탕으로...

  • 정환이라면 -> 추상화 -> , 관계

  • 캡슐화 정보은닉과 무결성을 위한, 상속 기능의 확장을 위해 사용, 다형성이라는 특성을 오버라이딩, 오버로딩을 이용하여 유연하게 변경이 가능하도록 프로그래밍 하는 방법론
  • 다형성은 왜 있는가?

1.1. 인터페이스

  • 개념적 인터페이스

    : 객체가 제공하는 모든 오퍼레이션(기능)의 집합을 객체의 인터페이스라고 부른다. 객체를 사용하기 위한 일종의 명세 혹은 규칙이라고 생각하자. 한객체가 갖는 책임을 정의한 것

  • 각각 언어에서 지원하는 인터페이스 -> 자바에서는 interface => implement로 인터페이스의 목적을 달성하게 하는 기능이 있다. 인터페이스는 오퍼레이션의 정의, implement에서 구현

1.2. 메시지

  • 오퍼레이션의 실행을 요청하는 것을 메시지라고 한다.
  • 자바의 경우, 메서드를 호출하는 것이 메시지를 보내는 과정에 해당된다.

1.3. 객체지향의 규칙(책임)

  • 객체가 갖는 책임의 크기가 작을수록 좋다 === 객체가 제공하는 기능의 개수가 적다 => 단일 책임원칙 : single responsibility pinciple (SRP)
  • 기능의 단위를 쪼개는 것
  • => solid 설계규칙에서 더 자세히 알아보자

1. 4. 의존

  • 한 객체가 다른 객체를 생성하거나 다른 객체의 메서드를 호출할때 의존이라고 표현한다.
  • 객체를 생성하거나 메서드를 호출하는 것뿐만 아니라 파라미터로 전달받은 경우에도 의존이다/
  • 순환의존이 발생하지 않도록 하는 원칙중의 하나가 의존 역전 원칙이다 DIP(dependency inverseion principle) => solid의 하나

1.4.1. UML 다이어그램

  • unified modeling language 다이어그램의 약자.
  • 객체 관리 그룹으로 관리하기 위한 다이어그램

1.4.2. 의존의 양면성 ( 이해가 잘 가지 않는 부분)

  • 클래스간의 의존성이 존재하면, 하나의 클래스의 변경은 의존성 관계를 가진 클래스의 변경을 야기한다. 그럼 어떻게 관리해?? 어렵네 즉 약간 양방향 링크드 리스트의 연결을 고려하는거랑 비슷?
    • 내가 변경되면 나에게 의존하고 있는 코드에 영향을 준다
    • 나의 요구가 변경되면 내가 의존하고 있는 타입에 영향을 준다.

캡슐화와 의존의 양면성은 서로 충돌하는데 이걸 어떻게 설명해야하지?

1.5. 캡슐화( 3요소 중 하나)

  • 객체지향의 장점: 한 곳의 구현 변경이 다른 곳에 변경을 가하지 않도록 하는데 있다.

  • 캡슐화란: 객체가 내부적으로 기능을 어떻게 구현하는지 감추는 것 즉, 내부 구현이 변경되더라도 그 기능을 사용하는 코드는 영향을 받지 않는다.

  • 캡슐화를 위한 두개의 규칙

    • Tell Don't Ask : 데이터를 묻지 말고, 기능을 호출해라
    // 절차지향
    if (member.getExpiryDate()!=null && member.getExpiryDate().getDate() < System.currentTimeMillis()){
        // 처리
    }
    
    // 객체지향
    if (member.isExpired()){
        // 처리
    }
  • law of memeter(데미테르의 법칙)

    • 위의 규칙을 따를 수 있도록 만들어 주는 또 다른 규칙
      1. 메서드에서 생성한 객체의 메서드만 호출
      2. 파라미터로 받은 객체의 메서드만 호출
      3. 필드로 참조하는 객체의 메서드만 호출
    // ex
    if (member.getDate().getTime()< ...){ // 데미테르 법칙 위반
    }
    
    // 그럼 어떻게해야함?
    
    class member {
    	private Datetime datetime;
    	
    	public getDate(){
    	return this.datetime;
    	}
    	public getTimeByDate() {
    	return this.getDate().getTime();
    	}
    }
    
    if (member.isOverTime()){
    	// 데미테르 법칙 지킴
    }
    • 소트워크 앤솔로지에서...
    디미터의 법칙("친구하고만 대화하라")이 좋은 출발점이긴 하지만, 이런 식으로 생각하자. 자기 소유의 장난감, 자기가 만든 장난감, 그리고 누군가 자기에게 준 장난감하고만 놀 수 있다. 하지만 절대 장난감의 장난감과 놀면 안 된다.
    • 데미테르의 법칙 == 메소드 체이닝을 사용하지 마라 이뜻은 아니다.
    // 이렇게 써도 문제 없음
    IntStream.of(1, 15, 20, 3, 9)
        .filter(x -> x > 10)
        .distinct()
        .count();
    
    • 위 코드에서 of, filter, distinct 메서드는 모두 IntStream이라는 동일한 클래스의 인스턴스를 반환한다.
  • 즉, 이들은 IntStream의 인스턴스를 또다른 IntStream의 인스턴스로 변환한다. 따라서 이 코드는 디미터 법칙을 위반하지 않는다.

  • 정보은닉에 대한 글: http://egloos.zum.com/aeternum/v/1232020

  • 객체지향 정보은닉: https://effectiveprogramming.tistory.com/entry/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%A0%95%EB%B3%B4-%EC%9D%80%EB%8B%89information-hiding%EC%97%90-%EB%8C%80%ED%95%9C-%EC%98%AC%EB%B0%94%EB%A5%B8-%EC%9D%B4%ED%95%B4

  • 정보은닉에 대한 내 정리:

    • 정보은닉은 객체지향적인 설계를 위해 고려해야하는 조건 그리고 목표이다. 왜? 소프트웨어에서 유연성을 가지게 하는 방법은 "객체간에 서로를 최대한 모르게 하는 것"이다. 이것이 정보은닉의 목표인 것
    • 캡슐화는 정보은닉의 목표를 달성하기 위한 수단 중에 하나.

    슐화라는 것은 간단히 말하면 오브젝트 내부의 성질을 오브젝트 외부에서 직접 관여하지 못하게 하는 것이고 오직 오브젝트가 제공하는 루트를 통해서만 관여할 수 있게끔 하는 것입니다.

    출처:https://okky.kr/article/472722

1.6. 객체 지향 설계 과정

  1. 필요한 기능이 무엇인지 정의하고, 세분화하여, 그 기능에 맞게 객체에 할당한다.
    • 그 기능에 필요한 데이터를 객체에 추가한다. 혹은 데이터를 먼저 객체에 추가하고 기능을 나중에 넣을 수 도 있다.
    • 기능은 최대한 캡슐화하여 구현한다.
  2. 객체간에 어떻게 메시지를 주고받을지 결정한다.
  3. 1과 2의 과정을 반복한다.

1.7. 캡슐화 정리.

  • 갭슐화를 통해서 객체를 사용하는 다른코드에 영향을 최소화하면서 객체 내부 구현을 변경할 수 있는 유연함을 얻었다.

2. 다형성과 추상타입

  • 간단정리: 객체지향의 특성인 구조변경의 유연함을 가능하게하는 또 다른 특징

2.1. 상속이란?

  • 한 타입을 그대로 사용할 수 있으면서, 구현을 추가해주는 방법.

  • 확장에는 열려있고, 변화에 유연하게 대처 가능하다.(유연성의 목적을 달성하는 또다른 방법)

  • 한마디로 정의: 한 타입을 그대로 사용하면서, 추가적인 구현을 가능하도록 하는 방법 혹은 기능

    // 쿠폰이 있고, 그 쿠폰이 할인해 줄 수 있는 돈이 정해져있다.
    // 사용자가 이 쿠폰을 사용할때, 쿠폰으로 할인받고 나서의 가격을 반환받는 기능이 있다.
    public class Coupon{
    	private int discountAmount;
    	public Coupon(int discountAmount){
    		this.discountAmount = discountAmount;
    	}
    	public int getDiscountAmount(){
    		return this.discountAmount;
    	}
    	public int calculateDiscountedPrice(int price){
    		if (this.discountAmount <= price) {
    			return price - dicscountAmount;
    		}
    		return 0;
    	}
    }
    
    // 쿠폰중에서, 지정한 가격보다 큰 상품을 구매할때만 쿠폰을 적용할 수 있는 기능을 추가하고 싶다.
    public class LimitPriceCoupon extends Coupon {
    	private int limitPrice;
    	
    	public LimitedPriceCoupon(int limitPrice, int discountAmount){
    	super(discpuntAmount);
    	this.limitPrice = limitPrice;
    	}
        public int getLimitPrice(){
            return this.limitPrice;
        }
        @override
        public int claculateDiscountedPrice(int price){
            if (price < this.limitPrice){
    			return price;
            }
            return super.calculateDiscountedPrice(int price);
            
        }
        
    }
    
    
  • 상속을 통해서, 다형성이라는 특징을 갖게 됩니다. 다형성의 뜻은 한 객체가 여러가지 타입을 가질 수 있다는 성질인데, 이러한 다형성을 갖게 할 수 있는 방법이 바로 상속입니다.

  • 상속은 두가지로 나뉩니다. 인터페이스 상속과, 구현상속이 있습니다.

2.2. 상속의 세부내용

  • 자바 8버전을 썻다...
  • 3가지 추가 되었다 ㅠㅠ
  • abstract와 interface의 차이점
    • 목적: 인터페이스 다중상속
    • 인터페이스는 매개변수를 받지 못한다.
2.2.1. 구현상속
  • 부모클래스를 상속받을때 구현상속이라 표현한다.
  • 자식클래스는 하나의 부모클래스로부터만 상속받는것이 가능하다. (다중상속 불가)
  • 구현상속은 추상메소드가 있을 수도 있고, 없을 수도 있다.
  • 자식 클래스에서 부모클래스가 가지고 있는 메소드를 재정의(override)를 통해 자식클래스만의 메소드를 구현할 수 있다.
  • 하지만 인터페이스 상속과 다르게 추상메소드가 없으면 재정의를 할 필요가 없다.
  • 이것이 다중상속기능을 제거하는 이유가 되기도 한다. 왜 ? 다중상속의 모호성을 제거하기 위해
2.2.2. 인터페이스 상속
  • 다중상속을 원할경우, 인터페이스를 이용해야 한다.
  • 인터페이스를 상속하려면 implement를 사용해야한다.
  • 인터페이스 상속은 추상 메소드만가진 추상클래스를 상속받을 때를 뜻한다.
  • 인터페이스라는 타입이 자바에 존재한다.
  • 인터페이스에 존재하는 추상메소드는 구현된 기능이 없으므로 상속받은 구현체에서 해당 기능을 작성해야한다.

2.3. 추상화

  • 상속의 기능을 사용하기 위해서는 각 객체의 논리적 공통점을 추출해서 관리하는 것이 객체지향 설계의 방법중 하나이다.
  • 그렇다면 왜 추상화가 중요한 것일까? 바로 구현에 있어서 교체의 유연함을 가져오기 때문이다.
  • 어떻게? 책임을 분리하는 방법으로.. 아래의 코드를 살펴보자/
public class FlowController {
	// 소켓 데이터 요청을 받기 전
	public void process() {
        // 데이터 읽기 객체 직접 생성
		FileDataReader reader = new FileDataReader();
		
        // 흐름제어: 1 읽기
        byte[] data = reader.read();
		
		Encryptor encryptor = new Encryptor();
		// 흐름제어: 2 암호화
        byte[] encryptedData = encryptor.encrypt(data);
		
		FileDataWriter writer = new FileDataWriter();
		// 흐름제어: 3 쓰기
        writer.write(encryptedData);
	}
}
  • 해당 코드를 보면, 데이터를 읽어오는 객체를 생성하는 책임흐름을 제어하는 책임을 동시에 가지고 있다. 이를 분리하기 위해 추상화작업을 통하여, ByteSourceFactory라는 클래스를 생성하고, 그 팩토리 클래스에서 데이터를 읽어오는 책임을 부여하는 것이다.
// byteSourceFactory 클래스 코드

public class ByteSourceFactory {
	public ByteSource create() {
		if (useFile()) {
			return new FileDataReader();
		} else if {
			return new SocketDataReader();
		}
	}
	
	private boolean useFile() {
		String useFileVal = System.getProperty("useFile");
		return useFileVal != null && Boolean.valueOf(useFileVal);
	}
	
	// lazyHolder 방법의 싱글톤 패턴
	private ByteSourceFactory() {}
	public static ByteSourceFactory getInstance() {
		return LazyHolder.INSTANCE;
	}
	private static class LazyHolder{
		private static final ByteSourceFactory INSTANCE = new ByteSourceFactory();
	}
}
// flowController 클래스 수정
public class FlowController {
	// 소켓 데이터 요청을 받기 전
	public void process() {
		ByteSource source = ByteSourceFactory.getInstance().create();
		byte[] data = source.read();
		 		
		Encryptor encryptor = new Encryptor();
		byte[] encryptedData = encryptor.encrypt(data);
		
		FileDataWriter writer = new FileDataWriter();
		writer.write(encryptedData);
   	}
}
  • 해당 팩토리 클래스를 추출하기위해 글쓴이는 다음과 같은 과정을 진행했다.
    1. FileDataReader와 SocketDataReader라는 객체의 공통점인, ByteType의 데이터를 read한다라는 속성을 찾는다.
    2. ByteSource라는 인터페이스를 만들고, read()하는 추상메소드를 추출한다.
    3. 추출한 추상 메소드를 상속받아 각각의 dataReader라는 구현체를 만들고, 조건에 맞는 구현체를 factory를 통해 인스턴스를 생성하도록 설계하였다.
  • 추상화는 다음과 같은 역할을 한다.
    1. 공통된 개념을 출해서 추상 타입을 정의
    2. 많은 책임을 가진 객체로부터 책임을 분리하는 역할

3. 재사용: 상속보단 조립

  • 목표: 상속을 사용할경우 생기는 문제점을 알아보고, 객체 조립을 통해 재사용의 단점을 극복하는 법을 알아본다.

3.1. 상속의 문제점

  1. 상위클래스 변경의 어려움

    • 상위클래스의 시그니처를 변경하면 하위클래스에도 그 변화가 반영됨으로 결집도가 높아 서로종속적인 관계가 객체에 영향을 끼친다.
    • 그렇기 때문에 클래스 상속의 거대화는 단일구조처럼 만들어 변화의 유연함을 잃게한다.
  2. 클래스의 불필요한증가

    • 유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다.
  3. 상속의 오용

    • 상속 자체를 잘못사용할 수 도 있다. 책에서 보여주는 예시를 보도록 한다.

    컨테이너의 수화물 목록을 관리하는 클래스가 필요하다고 할때, 이 클래스는 세가지 기능을 제공한다고 하자.

    • 수화물을 넣는다.
    • 수화물을 뺀다.
    • 수화물을 넣을 수 있는지 확인한다.

    ArrayList를 상속하여 Contatiner클래스를 만들어 사용하면, ArrayList의 메서드와 Container에서 정의한 사용자 함수를 구분하지 못하고, 사용자 입장에서 혼용하여 쓸 수 있다. 이를 방지하기 위하여 조립이라는 방법으로 해결하고자 한다.

3.2. 조립

  • 객체지향 언어에서 객체조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현한다.
// 예를들어 FlowController클래스의 경우,

public class FlowController {
	private Encryptor encryptor = new Encryptor();
	
	public void process(){
	...
	byte[] encryptedData = enryptor.encrypt(data); 
	...
	}
}
  • 조립의 단점은 상속보다 상대적으로 복잡한 구조를 가지게 돤다는 점이다.

3.3. 위임

  • 내가 할 일을 다른 객체에게 넘긴다는 의미, 조립방법으로 위임을 구현한다.
  • 위임을 사용하면, 내가 바로 만들어 실행할 수 있는 것을, 다른 객체의 인스턴스를 생성하고, 그 인스턴스에게 해당 메소드를 부탁하기 때문에 실행시간이 다소 증가한다는 단점이 있다.
  • 하지만 그럼에도 불구하고 유연함과 재사용성의 장점으로 위임의 방법으로 설계하는 것이 좋다.

3.4. 그렇다면 상속은 언제 사용해야 할까?

  • 재사용성에 목적을 두기 보다는, 기능의 확장이라는 관점으로 접근해야 한다.
  • 추가적으로 명확한 IS A관계가 성립해야한다.
  • 예를들어안드로이드 ui 위젯 클래스의 계층은 모두 is a 관계를 만족한다. ex) 버튼은 ui 위젯이다.'

4. SOLID 제 5원칙

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

  • 객체를 객체로 존재하게 하는 이유가 책임이다.
  • 클래스는 단 한 개의 책임을 가져야 한다. == 클래스를 변경하는 이유는 단 한 개여야 한다.
  • 책임은 다시 말하면, 기능 혹은 역할이라 생각하자.
  • 책임을 구분하는 것은 초보 개발자로서 쉽지 않은 일이다. 그렇지만 메소드의 사용처가 하나일 경우, 그 메소드는 그 사용자에게 하나의 책임(기능)을 제공한다고 생각하면 된다고 한다.
  • 단일책임원칙을 위반했을 경우, 하나의 기능을 추가 혹은 변경할때, 수정할 곳이 많아진다. 유지 보수성이 떨어진다.
  • 하나의 클래스에 너무 많은 기능을 주지 말자. 초콜릿 공장..예시 굿
  • 강한 응집력 약한 결합력을 위해

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

  • 확장에는 열려있어야 하고, 변경에는 닫혀있어야 한다.

    1. 기능을 변경하거나, 확장할 수 있다.
    2. 그 기능을 사용하는 코드는 기능의 변경 혹은 확장을 하더라도 (변경시) 수정사항이 없어야 한다.
  • 추상화와 다형성을 잘 이용하면 개방 폐쇄 원칙을 지키게 된다. 일 예로 펙토리 패턴을 보면 이해하기 쉽다.

  • 또 다른 방법은 상속을 이용하는 것이다.

    • 템플릿 패턴을 이용해서 일련의 과정을 정의하고, 세부 과정은 각 세부 클래스를 구현하여 각각의 기능을 수행하도록 하는 방법 -> 뜬구름 잡는 모습인데, 그냥 템플릿 패턴을 이용하면 상속을 통해 개방 폐쇄 원칙을 지킨다고 생각하면 된다.
  • 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다.

4.3. 리스코프 치환 법칙(Liskov Substitution Principle)

  • 개방 폐쇄 원칙을 받쳐주는 다형성에 관한 규칙
  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야한다.
  • 리스코프 치환법칙을 위반하는 상황: 직사각형- 정사각형 문제, 상위 타입에서 지정한 리턴 값의 범위에 해당하지 않는 값을 리턴할 때
  • 한 메소드에서 instanceof 연산자를 이용해 분기를 만들면 확장에 열려있지 않다는 뜻이다.(아직 잘 이해가 안감) 리스코프 치환 법칙을 위반할 때 자주 발생하는 증상임
  • 이러한 증상은 사용하는 객체의 추상화가 덜 이루어 졌다고 진단한다.
  • 재정의를 하지 않으면 문제 없다.
  • 쿠폰의 예시를 보자.
public class CouPon{
	// 할인에 적용될 가격
    private float discoutRate = 0.5;
	public int calculateDiscountAmount(Item item){
		return item.getPrice() * discountRate;
	}
}

// 추가 기능 요청 -> 특정 클래스의 아이템일 경우, 할인에 적용되지 않게 한다.
public class CouPon{
	// 할인에 적용될 가격
	public int calculateDiscountAmount(Item item){
		if(item instanceof SpecialItem){
			return 0;
		}
		return item.getPrice() * discountRate;
	}
}
	
  • nstanceof 를 제거하기 위해 다음과 같이 변형한다.
public Class Item {
	public boolean isDiscountAvailable(){
		return true;
	}
}

public class SpecialItem extends Item {
	@Override
	public boolean isDiscountAvailable(){
		return false;
	}
}

public class CouPon{
	// 할인에 적용될 가격
	public int calculateDiscountAmount(Item item){
		if(!item.isDiscountAvailable()){
			return 0;
		}
		return item.getPrice() * discountRate;
	}
}


4.4. 인터페이스 분리 원칙(Interface Segregation principle)

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다. == 클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.
  • 쉽게 말해 스프링 부트에서 컨트롤러에서 서비스를 사용할때, 각각의 컨트롤러를 각각의 서비스 인터페이스를 분리하여 관리하는 것
  • 이 원칙을 지키지 않을 경우 재컴파일을 해야한다는데 이부분을 이해하지 못했다..
  • 정환쓰 예 -> 하나의 거대한 인터페이스보다 자잘한 여러개의 인터페이스가 더 좋다. 구현이 강제되는게 크다.. 아하 ->>왜? 낭비되는 리소스카 크기때문에 인터페이스가 크다면..->

4.5. 의존 역전 원칙(Dependency Inversion Principle)

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저 수준 모듈이 고 수준 모듈에서 정의한 추상타입에 의존해야한다.

  • 고수준 모듈: 어떤 의미있는 단일 기능을 제공하는 모듈

  • 저수준 모듈: 고 수준 모듈 기능을 구현하기 위해 필요한 하위 기능의 실제 구현

  • 저수준의 모듈이 고수준의 모듈을 의존하도록 한다.

  • 어떻게? byteSourceFactory예제를 보면 추상화가 이루어질 수 있다.

  • 정환 ->> 추상성이 낮은 클래스보다 추상성이 높은 클래스에 의존성을 맺어야 한다.

    child라는 클래스가 있고, robot, rccar, doll이 라는 인터페이스가 있으면 이 모든 인터페이스들은 child이 playWith(Toy toy)하는 메소드를 쓸때는 has a 관계이고 이는 child가 toy에 의존하게 되며 toy인터페이스를 상속하는 robot, rccar, doll의 변화가 있더라도 toy에는 아무런 영향을 끼치지 않으므로 유지보수성을 보장할 수 있다.

  • 소스코드상에서 의존성의 역전을 지키는 것이지 런타임상의 역전을 원하는 것은 아니다.

  • 의존 역전 원칙은 개방폐쇄 원칙을 클래스 수준뿐 아니라 패키지 수준에서까지 확장시켜준다.

5. 참조

  • 개발자가 반드시 정복해야 할 객체지향과 디자인 패턴
profile
엔지니어로 거듭나기

0개의 댓글