[도서] 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 정리

Junseo Kim·2021년 2월 26일
0

[도서]

목록 보기
3/5

객체지향

요구 사항이 바뀔 때, 그 변화를 좀 더 빠르고, 수월하게 적용할 수 있다는 유연함을 얻을 수 있다는 장점을 얻기 위해 사용되는 것.

소프트웨어는 사용자가 요구하는 기능을 올바르게 제공해야하지만, 기능만 제공되도록 구현하면 안되고, 변화를 적용할 수 있어야한다.

절차 지향과 객체 지향

절차 지향: 데이터를 조작하는 코드를 별도로 분리해서 함수나 프로시저와 같은 형태로 만들고, 각 프로시저들이 데이터를 조작하는 방식으로 코드를 작성하는 방법. 데이터 중심

절차지향은 2가지 문제점을 가진다.

  • 데이터 타입이나 의미를 변경해야 할 때, 함께 수정해야 하는 프로시저가 증가.
  • 같은 데이터를 프로시저들이 서로 다른 의미로 사용하는 경우가 발생

객체 지향: 데이터 및 데이터와 관련된 프로시저를 객체로 묶어서 사용한다. 객체는 프로시저를 실행하는데 필요한 만큼의 데이터를 가지고, 객체들이 모여 프로그램을 구성한다.

데이터가 변경되더라도 다른 객체에 영향을 주지 않는다.

객체

객체가 제공해야 할 기능에 따라 객체가 정의된다. 객체마다 자신만의 책임을 가지고 있다.

객체가 제공하는 기능들을 사용하기 위해 사용법이 존재한다.

  • 기능 식별 이름
  • 파라미터 및 파라미터 타입
  • 기능 실행 결과 값

객체가 제공하는 모든 기능 집합을 객체의 인터페이스라고 부르며, 객체를 사용하기 위한 일종의 명세나 규칙이다. 실제 구현은 포함되어있지 않고, 실제 구현은 클래스에서 이뤄진다.(여기서 인터페이스는 개념적인 의미이며 자바 언어가 제공하는 인터페이스를 뜻하는 것은 아니다.)

책임

객체는 자신만의 책임을 가지고 있는데 이 말은 객체가 역할을 수행한다는 의미를 가진다.

객체에 책임을 결정하는 것은 아래와 같이 진행된다.
1. 프로그램을 만들기 위해 필요한 기능 목록 정리
2. 객체마다 기능 할당

객체가 갖는 책임의 크기는 작을수록 좋다. 즉, 객체가 제공하는 기능의 수가 적을수록 좋다. 한 객체가 많은 책임을 가지면 절차 지향적으로 구조가 바뀐다.(단일 책임 원칙. 변경의 유연함을 얻을 수 있다.)

의존

객체는 다른 객체에 메세지를 보내면서 서로 협력한다. 메세지는 어떤 기능을 실행하라고 요청하는 것이다. 즉 메서드의 호출을 뜻한다.

한 객체가 다른 객체를 생성하거나 다른 객체의 메서드를 호출할 때, 파라미터로 전달받는 경우 이를 그 객체에 의존한다고 표현한다.

의존한다는 것은 의존하는 타입에 변경이 발생할 때 같이 변경될 가능성이 높다는 것을 뜻한다. 여러 클래스가 서로 의존 관계일 때, 순환 의존이 생길 수 있는데 이를 방지하기 위해 의존 역전 원칙이 있다.

  • 내가 변경되면 나에게 의존하고 있는 코드에 영향을 준다.
  • 나의 요구가 변경되면 내가 의존하고 있는 타입에 영향을 준다.

캡슐화

객체 지향의 장점은 한 곳의 구현 변경이 다른 곳에 변경을 가하지 않도록 해준다는데 있다. 즉, 수정을 좀 더 원할하게 할 수 있도록 하는 것이 객체 지향적으로 프로그래밍을 하는 이유이다.

객체 지향은 기본적으로 캡슐화를 통해서 한 곳의 변화가 다른 곳에 미치는 영향을 최소화한다.

캡슐화는 객체가 내부적으로 기능을 어떻게 구현하는지를 감추는 것이다. 따라서 내부적으로 기능 구현방법이 변경되더라도 그 기능을 사용하는 코드는 영향을 받지 않게 만드는 것이다.

캡슐화를 위한 2가지 법칙

  • Tell, Don't Ask: 데이터를 물어보지 않고(가져오지 말고 ex. getter), 기능을 실행해 달라고 말해라
  • 데미테르의 법칙(Law of Demeter)
    -메서드에서 생성한 객체의 메서드만 호출
    -파라미터로 받은 객체의 메서드만 호출
    -필드로 참조하는 객체의 메서드만 호출

데미테르의 법칙을 지키지 않고 있는 가능성이 높은 경우

  • 연속된 get 메서드 호출
  • 임시 변수의 get 호출이 많음

객체 지향 설계 과정

  1. 제공해야 할 기능을 찾고 또는 세분화하고, 그 기능을 알맞은 객체에 할당
    • 기능을 구현하는데 필요한 데이터를 객체에 추가.
    • 기능은 최대한 캡슐화해서 구현
  2. 객체 간에 어떻게 메시지를 주고받을 지 결정
  3. 1번과 2번을 반복

다형성과 추상 타입

객체 지향이 주는 장점은 구현 변경의 유연함.

상속

한 타입을 그대로 사용하면서 구현을 추가할 수 있도록 해주는 방법. 자식 클래스는 부모 클래스에 정의된 구현을 물려받는다. private이 아닌 메서드나 필드에 접근할 수 있다. 자식 클래스는 부모 클래스에 정의된 메서드를 재정의 할 수 있다.

다형성과 상속

다형성이란 한 객체가 여러 가지 모습(타입)을 갖는다는 것을 의미한다. 즉 한 객체가 여러 타입을 가질 수 있다는 것이다. 클래스를 상속받거나, 인터페이스를 상속받는 클래스는 부모 클래스나 인터페이스 타입으로 캐스팅 될 수 있다.

인터페이스 상속과 구현 상속

인터페이스 상속이란 타입 정의만을 상속받는 것이다. 인터페이스나 추상 메서드만 가진 추상 클래스를 상속받는 경우이다. 실제 구현 코드는 상속받는 클래스에서 구현한다.

구현 상속은 클래스 상속을 의미한다. 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용된다. 부모 클래스의 구현을 재사용하면서 다형성도 함께 재공해준다. 부모 클래스의 메서드를 자식 클래스에서 재정의해서 사용할수도 있다. 만약 부모 클래스타입으로 캐스팅을 해서 메서드를 실행한다고 해도 객체의 실제 타입에 따른 메서드가 호출된다.(하위 타입에서 상위 타입 메서드 재정의 시 하위 타입의 메서드를 호출)

추상 타입과 유연함

추상화는 데이터나 프로세스 등을 의미가 비슷한 개념이나 표현으로 정의하는 과정으로 타입도 추상화의 대상이다. 추상화된 타입은 오퍼레이션의 시그니처만 정의할 뿐 실제 구현을 제공하지는 못한다. 추상 타입은 구현을 제공할 수 없으므로 주로 인터페이스로 정의한다.

추상화는 상세 구현을 제공하는 여러 클래스들을 통해 하나의 타입을 만들어내기도 하지만 추상화가 반드시 추상 타입을 만들어야하는 것은 아니다.

추상 타입과 실제 구현의 연결

추상 타입과 실제 구현 클래스는 상속을 통해서 연결한다. 구현 클래스(콘크리트 클래스)가 추상 타입을 상속받는 방법으로 연결시킨다. 구현 클래스의 인스턴스를 추상 타입으로 받을 수 있다.

추상 타입을 이용한 구현 교체의 유연함

추상 타입을 이용하면, 구현 클래스의 종류가 달라지거나 추가되더라도 사용하는 쪽의 코드의 변경이 없어지게된다.

변화되는 부분을 추상화하기

요구 사항이 바뀔 때 변화되는 부분은 이후에도 변경될 소지가 많다. 이런 부분을 추상 타입으로 교체하면 향후 변경에 유연하게 대처할 수 있다. 추상화 되지 않은 코드는 주로 동일 구조를 가지는 if-else 블록으로 드러난다.

인터페이스에 대고 프로그래밍하기

실제 구현을 제공하는 콘크리트 클래스를 사용해서 프로그래밍하지 말고, 기능을 정의한 인터페이스를 사용해서 프로그래밍하라는 뜻이다. 인터페이스는 최초 설계에서 도출되기 보다는 요구 사항의 변화와 함께 점진적으로 도출이 된다. 즉, 새롭게 발견된 추상 개념을 통해서 도출되는 것이다. 하지만 모든 곳에서 인터페이스를 사용하라는 것은 아니다. 모든 곳에서 인터페이스를 사용하게 되면 타입이 많아지고 구조도 복잡해져 프로그램의 복잡도만 증가시킬 수 있다. 변화 가능성이 높은 경우에만 사용해야한다.

인터페이스는 인터페이스 사용자 입장에서 만들기

인터페이스를 작성할 때는 그 인터페이스를 사용하는 코드 입장에서 작성해야 한다.

인터페이스와 테스트

추상 타입을 사용하게 되면 테스트가 용이해진다. 실제 구현 코드의 구현이 덜 끝났더라도 임의의 구현체를 만들어 테스트할수 있기 때문이다. 실제 콘크리트 클래스 대신에 진짜 처럼 행동하는 객체를 Mock 객체라고 한다. Mock 객체를 사용하여 실제 사용할 콘크리트 클래스 구현 없이 테스트 할 수 있다.

재사용: 상속보단 조립

상속을 사용하면 쉽게 다른 클래스의 기능을 재사용하면서 추가 기능을 확장할 수 있다. 하지만 상속은 변경의 유연함이라는 측면에서 치명적인 단점을 갖는다.

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

상속은 상위 클래스의 변경을 어렵게 만든다. 어떤 클래스를 상속받는다는 것은 그 클래스에 의존한다는 것이기 때문에 의존하는 클래스가 변경되면 영향을 받을 수 있다.

2. 클래스의 불필요한 증가

유사한 기능을 확장하는 과정에서 클래스의 개수가 불필요하게 증가할 수 있다. 다중 상속을 할 수 없는 자바에서는 한 개의 클래스만 상속받고 다른 기능은 별도로 구현해야 한다. 필요한 기능의 조합이 증가할수록 상속을 통한 기능 재사용을 하게 되면 클래스의 개수가 증가하게된다.

3. 상속의 오용

상속 자체를 잘못 사용할 수 있다. 서로 다른 책임을 갖는 클래스를 상속받으면 원하는 방식으로 동작하지 않을 수 있다.

조립을 이용한 재사용

객체 조립은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어내는 것이다. 필드에서 다른 객체를 참조하는 방식으로 구현된다. 한 객체가 다른 객체를 조립해서 필드로 갖는다는 것은 다른 객체의 기능을 사용한다는 의미이다. 조립을 이용하면 상속으로 인한 문제점들을 해결할 수 있다. 또 조립 방식은 런타임에 조립 대상 객체를 교체할 수 있다.

위임

내가 할 일을 다른 객체에게 넘긴다는 의미이다. 보통 조립 방식을 이용해서 위임을 구현한다. 요청을 위임할 객체를 필드로 연결해서 사용하거나 객체를 새로 생성해서 사용한다. 위임은 내가 바로 실행할 수 있는 것을 다른 객체에 한 번 더 요청하게되므로 실행 시간은 증가하지만 이로 인해 얻는 유연함/재사용의 이점이 더 크다.

상속은 언제 사용하나?

상속은 재사용이라는 관점이 아닌 기능의 확장이라는 관점에서 적용해야한다. 명확한 IS-A 관계일 때 사용해야한다. 하위로 내려갈수록 상위 클래스의 기본적인 기능을 그대로 유지하면서 기능을 확장해나간다.

설계 원칙: SOLID

[도서] 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - SOLID

DI(Dependency Injection)와 서비스 로케이터

어플리케이션 영역과 메인 영역

어플리케이션 영역

고수준 정책 및 저수준 구현을 포함하는 영역

메인 영역

어플리케이션 영역의 객체를 생성, 설정, 실행하는 책임(어플리케이션 영역에서 사용할 하위 수준 모듈을 변경하고 싶다면 메인 영역만 수정) 모든 의존은 메인 영역에서 어플리케이션으로 나간다. 반대 방향의 의존은 없어야한다.(메인이 바껴도 어플리케이션 영역은 바뀌지 않음)

  • 어플리케이션 영역에서 사용될 객체를 생성
  • 각 객체 간의 의존 관계를 설정
  • 어플리케이션 실행

DI를 이용한 의존 객체 사용

DI는 필요한 객체를 직접 생성하거나 찾지 않고 외부에서 넣어 주는 방식. DI를 통해 의존 객체를 관리할 때는 객체를 생성하고 각 객체들을 의존 관계에 따라 연결해 주는 조립 기능이 필요하다.

생성자 방식과 설정 메서드 방식

  • 생성자 방식
    생성자를 통해 의존 객체를 전달 받는 방식. 객체를 생성하는 시점에 필요한 모든 의존 객체를 준비할 수 있어서 객체 생성 시점에 의존 객체가 정상인지 확인할 수 있다.(검증 작업) 의존 객체가 먼저 생성되어있어야 사용 가능한 방법이다.
  • 설정 메서드 방식
    메서드를 이용해서 의존 객체를 전달받는다. 객체를 생성한 뒤에 의존 객체를 주입하게 된다. 의존할 객체가 나중에 생성된다면 이 방법을 사용해야한다. 의존 객체를 설정하지 못한 상태에서 객체를 사용하게 되면 NullPointerException이 발생하게된다.

DI와 테스트

DI는 의존 객체를 Mock 객체로 쉽게 대체할 수 있도록 함으로써 단위 테스트를 할 수 있게 해준다.(구현되지 않은 클래스를 사용해야할 때 Mock객체로 생성 후 주입해준다.) 기존의 다른 코드를 변경할 필요가 없어진다.

서비스 로케이터를 이용한 의존 객체 사용

프로그램 개발 환경이나 사용하는 프레임워크의 제약으로 인해 DI를 적용할 수 없는 경우가 있다. 이때 사용할 수 있는 방법 중 하나가 서비스 로케이터를 사용하는 것인데 단점도 존재한다.

서비스 로케이터의 구현

서비스 로케이터는 어플리케이션에서 필요로 하는 객체를 제공하는 책임을 갖는다. 따라서 의존 대상이 되는 객체의 getter를 제공해준다. 메인 영역에서 서비스 로케이터가 제공할 객체를 초기화 해준다.

  • 객체 등록 방식의 서비스 로케이터 구현
    서비스 로케이터를 생성할 때 사용할 객체를 전달하고 서비스 로케이터 인스턴스를 지정하고 참조하기 위한 static 메서드 제공.

  • 상속을 통한 서비스 로케이터 구현
    객체를 구하는 추상 메서드를 제공하는 상위 타입을 구현하고, 상위 타입을 상속받은 하위 타입에서 사용할 객체를 설정한다.

  • 지네릭/템플릿을 이용한 서비스 로케이터 구현
    서비스 로케이터 사용시 인터페이스 분리 원칙을 위반하게된다.(사용하지 않는 타입까지 의존이 생김) 이를 해결하기 위해 지네릭을 사용할 수 있다.

서비스 로케이터의 단점

가장 큰 단점은 동일 타입의 객체가 다수 필요할 경우, 각 객체 별로 제공 메서드(getter)를 만들어 주어야 한다는 점이다. 이 경우 로케이터를 사용하는 클래스에서 get할 객체에 의존하게 된다. 다른 구현으로 바꿔줘야할때 로케이터를 사용하는 클래스도 수정되어야하므로 개방 폐쇄 원칙을 어기게 된다.

DI를 사용하면 구현 객체가 변경되더라도 사용하는 쪽 객체에는 영향이 가지 않는다.

두 번째로는 인터페이스 분리 원칙을 어긴다. 필요하지 않은 타입의 객체까지 의존관계가 생기기 때문에 다른 의존 객체에 의해 발생되는 수정에 의해 영향을 받게 된다.

가능하면 DI를 사용하자

주요 디자인 패턴

[도서] 개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 - 주요 디자인 패턴

0개의 댓글