객체지향

박진선·2022년 11월 7일

객체란?

객체(object)는 정체성(Identity), 상태(State), 행위(Behavior)를 가진, 내부 상태를 캡슐화하고 메시지(메서드 호출)로만 상호작용하는 추상화된 개체

객체지향 설계원칙

의존 관계 역전 원칙 (Dependency Inversion Principle, DIP)

전통적인 객체지향 설계에서는 고수준(High-level) 모듈이 저수준(Low-level) 모듈에 의존하는 것이 일반적이다. 여기서 '고수준'은 비즈니스 로직과 같은 상위 개념을, '저수준'은 데이터 저장, 파일 입출력과 같은 구체적인 구현을 의미한다.

즉, 전통적인 방식의 의존 관계 방향(고수준 -> 저수준)이 역전되어, 고수준 모델은 저수준 모델의 추상화에 의존하고, 저수준 모듈은 고수준 모듈이 필요로 하는 추상화에 의존하게 된다.

Inversion의 의미
원래 기대되는 자연스러운 방향인 고수준 → 저수준(구체)라는 의존 방향이 당연하다

단순히 고수준이 저수준을 의존 → 저수준이 고수준을 의존의 반전이 아니라,
고수준이 저수준 구체에 직접적으로 끌려가던 구조 → 고수준이 추상화에만 의존하고, 저수준이 그 추상화에 맞춰 따라가는 구조로 의존의 중심축이 뒤집힌 것을 의미한다.

DIP에서 의존성 역전을 하는 이유는 애플리케이션의 중심이 되는 도메인/비즈니스 로직을 가진 가장 상위 모듈이 기술적인 메카니즘을 다루는 변경 가능성이 높은 하위모듈에 의존하지 않게 만드는 것이 목적이다.

의존성 역전 원칙(DIP)과 인터페이스 소유권의 역전(Interface Ownership Inversion)

설명했지만 A가 B를 의존하여 사용한다면 A가 상위 모듈 B가 하위 모듈이고, 이때 B가 바뀌면 A도 수정해야 하는 결과를 낳기 때문에 결국 '추상화'. 즉, Interface를 이용하여 의존하라는 것이라고 했다.

근데 일반적인 흐름으로 하위 모듈에 Interface를 만들고 상위모듈이 하위 모듈 추상화에 의존하도록 한다면 결국 모듈인 패키지 수준으로 보자면 여전히 상위 모듈이 하위 모듈 추상화에 의존하기 때문에 하위 모듈을 의존하고 있다고 볼 수 있다.

이때 '인터페이스 소유권의 역전'과 '분리 인터페이스 패턴(Separated Interface Pattern)'이 필요하다.
이 개념은, 인터페이스는 어느 패키지에 들어 있어야 되는가에 대한 개념이다.

보통 인터페이스를 만들면, 이것을 구현한 클래스와 같은 패키지에 있는 것이 맞아 보일 수 있지만, 많은 경우에 Interface는 자신을 구현한 클래스 쪽이 아니라 자신을 사용하는 쪽에 있는 게 더 자연스러운 경우가 많다고 한다. 그래서 '인터페이스의 소유권을 역전'시켜야 한다는 것이다.

소프트웨어 설계에서 "상위 계층이 하위 계층에 의존한다"라는 전통적인 의존성 방향이 역전(Invert)됐다. 따라서 하위 모듈(구현체)이 변경되더라도 상위 모듈은 영향을 받지 않는 구조가 됨을 그림으로 알 수 있다.

의존성 역전 원칙을 잘 따르는 코드를 만들 때의 작업은 다음과 같다.
1) 첫 번째 작업: Interface를 만들어내고 추상화를 한 다음에 모든 코드가 추상화에만 의존하도록 만듦.
2) 두 번째 작업: Interface를 구현한 클래스가 있는 하위모듈에 위치시키는 것이 아니라 이를 사용하는 클라이언트(코드)가 있는 모듈(패키지)로 이전시키는 것.

왜 Web Controller와 Service 계층 간에는 DIP를 적용하지 않는가?

보통 Spring Web MVC의 구조는 아래와 같다. 웹 계층(Web Controller)과 서비스 계층(Service Layer) 간의 의존성으로 보아 DIP를 적용한다면, 서비스 계층의 interface도 Controller 패키지에 위치해야 하지 않을까 싶다. 하지만 왜 그렇게 설계하지 않는 걸까?

com.sample.app
    ├── controller
    │   └── PaymentController.java
    ├── service
    │   ├── PaymentService.java (인터페이스)
    │   └── impl
    │       └── PaymentServiceImpl.java
    └── mapper
        └── PaymentMapper.java

의존성 역전을 하는 이유는 애플리케이션의 중심이 되는 도메인/비즈니스 로직을 가진 상위 모듈이 기술적인 메커니즘을 다루는 변경 가능성이 높은 하위 모듈에 의존하지 않게 만드는 것이 목적이다.

그리고 Web MVC 구조에서는 변경 가능성이 높은 Web과 UI, Data 그리고 각종 Infra 기술들이 하위 모듈로 취급되며, 비즈니스 로직을 다루는 서비스 계층과 이 안에서 다루는 도메인 오브젝트가 가장 중심이 되는 상위 모듈이라고 한다.

생각해보면 Controller는 단순히 Web 요청을 받아서 서비스 계층에 전달하고, 그 결과를 UI에 반환하는 역할에 초점이 맞춰져 있기도 하다. 따라서, 상위 모듈인 서비스 계층을 중심으로 모든 의존성이 형성되는 것이다. 이렇게 되면 중요한 애플리케이션 로직이 웹 요청을 어떤 식으로 받아서 처리하는지? 혹은 뒤에서 데이터를 어떻게 읽어오는지? 혹은 어떠한 인프라 기술을 사용하는지에 영향을 받지 않는 안정적인 구조가 된다.

상속(inheritance) 이란?

  • 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미하며 상속은 캡슐화, 추상화와 더불어 객체 지향 프로그래밍을 구성하는 중요한 특징 중 하나이다. extends 예약어를 통해 상속을 받게 되면 부모 클래스에서 정의한 모든 필드와 메소드를 물려받아 자식 클래스에서 사용이 가능하다.

  • 상속을 하더라도 예외가 존재하는데 부모 클래스의 private 접근 제한을 갖는 필드 및 메소드는 자식이 물려받을 수 없다.
    부모와 자식 클래스가 서로 다른 패키지에 있다면, 부모의 default 접근 제한을 갖는 필드 및 메소드도 자식이 물려받을 수 없다 default 접근 제한은 ‘같은 패키지에 있는 클래스’만 접근이 가능하게 제한하기 때문이다. 이외의 경우는 모두 상속의 대상이 된다.

  • 상속은 단일 상속만 가능하다, 즉 extend 부모 클래스1, 부모 클래스2 이런 식으로 다중 상속이 불가능하다. 부모 클래스가 하나일 필요는 없다 부모 클래스의 부모 클래스가 있다면 자식 클래스는 모든 부모 클래스의 필드와 메소드를 전부 상속 받는다. 만약 부모 클래스가 없을 경우 모든 클래스는 상속계층도 최상위 클래스인 Object 클래스를 컴파일러에 의해 자동으로 상속받는다, 부모 클래스가 있을 경우에는 부모 클래스가 Object 클래스를 상속 받기 때문에 자식 클래스에서도 Object 클래스를 상속 받는다.

  • 자바에서는 자식 객체를 생성하면, 부모 객체를 먼저 생성한 후, 자식 객체가 그 다음에 생성 한다. 아래의 코드 main클래스에서 부모 객체를 생성하지 않았지만 내부적으로는 Parent 객체를 생성하고 Child 객체를 생성한다. 객체는 생성자를 호출해야만 생성되는데,부모 객체를 생성할 때 부모 생성자를 어디서 호출할까? 부모 클래스(Parent)는 명시적 생성자 선언이 없고, 자식 클래스(Child)는 명시적 생성자 선언이 존재한다, 그러면 부모 클래스의 기본 생성자 선언은 자식클래스의 생성자 첫줄에 super(); 라고 자동으로 생성되어 호출한다. 또한 부모 클래스 에서 명시적 생성자가 있을 경우 super()를 통하여 호출하지 않으면 컴파일 에러가 발생하기에 자식 생성자 첫줄에 super() 를 써서 호출 해주어야 한다.

    super : 부모 클래스 객체를 지칭, 부모 클래스의 멤버나 메소드에 접근시 사용
    super() : 부모 클래스의 생성자를 호출

  • 부모 클래스에서 정의한 메소드를 자식 클래스에서 재정의 하는 것을 오버라이딩 이라 하며 몇가지 조건이 존재한다.

  1. 메소드의 동작만을 재정의하는 것이므로, 메소드의 선언부는 기존 메소드와 완전히 같아야 한다.
  2. 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다.
  3. 부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없다.
public class Parent{
  String name;
  int age;

public void Print(){ 
  System.out.println("이름과 나이 : "+name+" "+age);
}
public class Child extends Parent{
  Child (String name, int age){
    this.name = name;
    this.age = age;
}

public static void main (String[] args){
  Child child = new Child("홍길동", 20);
  Child.Print();
}

캡슐화

  • 연관된 목적을 가지는 변수와 함수를 하나의 클래스로 묶어 외부에서 쉽게 접근하지 못하도록 은닉하는 것을 뜻하며, 중요한 데이터를 쉽게 바꾸지 못하도록 정보 은닉을 위해 사용한다.
    캡슐화를 통해 외부에서 내부의 정보에 접근하거나 변경할 수 없게 막고 객체가 제공하는 필드와 메소드를 통해서만 접근이 가능하다. 접근을 제한함으로써 유지보수나 확장 시 오류의 범위를 최소화할 수 있고 객체 내의 정보 손상과 오용을 방지하고 데이터가 변경되어도 다른 객체에 영향을 주지 않아 독립성이 좋다. 캡슐화는 접근제어자를 통해 이루어진다.

    접근 제어자
    public : 접근 제한 없음
    protected: 동일한 패키지 내에서 또는 다른 패키지의 자식 클래스에서만 접근이 가능
    default : 접근 제한자를 명시하지 않으면 default 값이 되며, 동일한 패키지 내에서만 접근 가능
    private: 자기 자신의 클래스 내에서만 접근 가능

  • 아래 코드를 보면 필드의 변수에 private 접근제어자를 선언하여 member 클래스 내에서가 아닌외부에서는 직접적인 접근이 불가능하며 getter와 setter를 통해서만 접근이 가능하다

public class member {

	private String id;
	private String pw;
	private int age;

	//getter
	public String getId() {
		return id;
	}
	public String getPw() {
		return pw;
	}
	public int getAge() {
		return age;
	}

	//setter
	public void setId(String id) {
		this.id = id;
	}
	public void setPw(String pw) {
		this.pw = pw;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

다형성

  • 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다, 클래스의 상속이나 인터페이스 구현 관계에서 부모 클래스 타입 또는 인터페이스의 참조 변수로 자식 클래스 타입 또는 구현 클래스 타입의 인스턴스를 참조할 수 있다. 부모 타입의 참조변수는 부모 자식 간의 공통적으로 가지고 있는 필드 및 메소드에만 접근할 수 있고 자식 클래스에서 별도로 정의한 필드와 메소드는 접근할 수 없다. 부모 클래스의 메소드를 각 자식 클래스에서 오버라이딩한 경우 오버라이딩한 메소드가 호출된다.

캐스팅(casting)이란 타입을 변환하는 것을 말하며 형변환이라고도 한다. 상속 관계에 있는 부모 자식 클래스 간에는 서로 간의 형변환이 가능하다.
업캐스팅 : 자식 클래스 -> 부모 클래스 (형변환 연산자 생략 가능)
다운캐스팅 : 부모 클래스 -> 자식 클래스 (형변환 연산자 생략 불가)

  • 다형성의 이점
    여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이함
    객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높아짐
    클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 안전성이 높아짐

  • 다형성 필수 조건
    상속 관계
    다형성을 활용하기 위해서는 필수로 부모-자식 간 클래스 상속이 이루어져야 한다.
    오버라이딩 필수 (자식 클래스에서 메소드 재정의)
    다형성이 보장되기 위해서는 하위 클래스 메소드가 반드시 재정의되어 있어야 한다.
    업캐스팅 (자식 클래스의 객체가 부모 클래스 타입으로 형변환 되는 것)
    부모 타입으로 자식클래스를 업캐스팅하여 객체를 생성해야 한다.

public class Person{
  public void Print(){ 
    System.out.println("hi~");
  }
}
public class Singer extends Parent{
  @Override
  public void Print(){ 
    System.out.println("La La~");
  }
}
public class Dancer extends Parent{
  @Override
  public void Print(){ 
    System.out.println("둠칫 둠칫~");
  }
}
public class Main{
  public static void main (String[] args){
    Person p1 = new Singer(); //자식 클래스 업캐스팅
    p1.print(); // Singer 클래스에서 오버라이딩한 La La~ 출력
    p1 = new Dancer();
    p1.print(); // Dancer 클래스에서 오버라이딩한 둠칫 둠칫~ 출력
  }
}

추상화

  • 공통된 특징을 묶어 하나의 클래스로 정의하는 것을 말하며 추상 클래스와 인터페이스를 사용해서 추상화를 구현한다.

추상클래스

  • 하나 이상의 추상 메소드를 포함하는 클래스를 가리켜 추상 클래스라고 한다. 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면 이 클래스를 상속받는 모든 클래스에서는 해당 추상 메소드를 반드시 오버라이딩 해야하는 강제성을 부여한다.

  • 추상 클래스는 인스턴스를 생성할 수 없고 반드시 상속받아 구현한다. 추상 메소드는 선언부만 작성하고 구현부는 구현하는 클래스에서 작성하도록 비워둔다. 생성자, 필드, 일반 메소드도 작성이 가능하다.

abstract class Animal { abstract void cry(); }

class Cat extends Animal { void cry() { System.out.println("냐옹냐옹~"); } }

class Dog extends Animal { void cry() { System.out.println("멍멍!"); } }

public class Main {
  public static void main(String[] args) {
      // Animal a = new Animal(); // 추상 클래스는 인스턴스를 생성할 수 없음.
      Cat c = new Cat();
      Dog d = new Dog();
      c.cry();
      d.cry();
  }
}

인터페이스

  • 극단적으로 동일한 목적 하에 동일한 기능을 수행하게끔 강제하는 것이 바로 인터페이스의 역할이자 개념이다. 다형성을 극대화하여 개발코드 수정을 줄이고, 구현 클래스보다 인터페이스에 의존하게 하여 클래스간의 결합도를 낮추어 프로그램 유지보수성을 높이기 위해 사용한다.

  • 모든 필드가 public static final로 정의되고, 모든 메서드가 public abstract 로 정의된다.(static과 default 메서드 제외) 오로지 추상 메소드와 상수만을 포함할 수 있다.
    인터페이스 끼리 상속이 가능하며 상속관계가 있는 인터페이스를 구현하는 클래스에서는 부모 자식 인터페이스의 추상 메소드를 모두 오버라이딩 해야한다.

interface Hunting { public abstract void hunt(); }
interface Animal extends Hunting { public abstract void cry(); }

class Cat implements Animal {
  public void cry() {
      System.out.println("냐옹냐옹~");
  }
  public void hunt() {
      System.out.println("물고기 사냥!");
  }
}
class Dog implements Animal {
  public void cry() {
      System.out.println("멍멍!");
  }
  public void hunt() {
      System.out.println("토끼 사냥!");
  }
}

public class Main {
  public static void main(String[] args) {
    Animal animal = new Cat();
    animal.cry(); // 냐옹냐옹~ 출력
    animal.hunt(); // 물고기 사냥! 출력 
    
    animal = new Dog();
    animal.cry(); // 멍멍! 출력
    animal.hunt(); // 토끼 사냥! 출력 
  }
}
profile
주니어 개발자 입니다

0개의 댓글