패스트캠퍼스 백엔드 부트캠프 3기/JAVA

[JAVA] 객체지향 프로그래밍의 이해 - 2

hail2y 2025. 1. 6. 13:43

클래스의 관계는 크게 상속과 포함 관계가 있다. 

  • 상속은 기존의 클래스로 새로운 클래스를 작성하는 것으로 코드를 재사용한다.
  • 쉽게 생각해 두 클래스에 부모 - 자식 관계를 맺어주는 것이라고 볼 수 있다.
  • 자바는 충돌 위험 때문에 단일 상속만 허용한다 (c++은 다중 상속 허용)
  • Object 클래스는 모든 클래스의 조상이다. 부모가 없는 클래스는 자동으로 Object 클래스를 상속받는다.
    (Object 클래스는 11개의 메서드를 가진다.)

부모(parent) - 자식 (Child) 관계도

예를 들어 Point 클래스와 Point3D 클래스를 만들 때 Point3D 클래스를 어떻게 정의하느냐에 따른 상속 관계를 살펴보자. 

class Point {
	int x;
	int y;
}

class Point3D { // 1. Point 클래스와 독립적인 관계
	int x;
	int y;
	int z;
}

class Point3D extends Point { // 2. Point 클래스와 상속 관계
	int z;
}

 

Point 클래스는 x와 y 필드를 가진 클래스다. 그리고 Point3D 클래스는 x와 y 필드에 더불어서 int 형의 z 필드를 하나 더 가지는 클래스다.이때 Point3D 클래스를 Point 클래스와 같이 독립적인 클래스로 만드느냐, 아니면 상속 관계로 만드느냐에 따라 갈린다. 1번 코드를 보면 Point 클래스와 매우 유사하다는 것을 알 수 있지만 코드 상 Point 클래스와 직접적인 관계는 없다. 왜냐하면 Point3D 클래스에서 Point 클래스를 직접적으로 사용하지 않았으니 접근할 수 있는 방법이 없다. 반면 2번 코드는 extends를 사용해 Point 클래스를 부모 클래스로 하여 상속 관계로 표현하였다. 필드에는 z 밖에 없지만 메모리에는 부모 클래스 정보가 함께 올라가기 때문에 1번 클래스 정의와 메모리 상에서 큰 차이가 없다. 

Point3D p = new Point3D();

 

이처럼 1번 코드나 2번 코드나 Point3D 클래스 객체를 생성하게 되면 아래와 같이 x, y, z 메모리 공간이 함께 생성된다.

객체 생성 시 메모리 구조

하지만 둘의 차이는 부모 클래스를 변경할 때 드러난다. 1번 코드는 앞서 말했듯이 Point 클래스와 직접적으로 관계가 없기 때문에 Point 클래스를 변경하더라도 아무런 일이 나타나지 않는다. 반면 2번 코드는 상속 관계에 놓여 있기 때문에 부모 클래스인 Point 클래스를 변경하게 되면 자식 클래스인 Point3D 클래스에 영향을 주게 된다. 예를 들어 Point 클래스의 필드 중 int y;를 float y; 로 변경하게 되면 Point3D 클래스의 필드도 그대로 바뀐다. 하지만 자식 클래스의 변경은 부모 클래스에 영향을 주지 않는다. 2번처럼 상속 관계를 만들더라도 Point 클래스에서는 여전히 Point3D 클래스를 모르기 때문이다. 

 

cf. 클래스는 필드와 메서드로 구성되어 있고, 이러한 구성 요소들은 클래스 멤버라고 부른다.

 

포함 관계는 다음과 같다. 

  • 포함 관계는 클래스 멤버로 참조변수를 선언하는 것이다.
  • 작은 단위의 클래스를 만들고 이들을 조합해서 클래스를 만든다.
  • 대부분의 관계가 포함관계라고 생각하면 된다.
class Car {
	Engine e = new Engine();
	Door[] d = new Door[4];
	...
}

 

(메서드) 오버라이딩은 상속 받은 조상의 메서드를 자신에 맞게 변경하는 것이다. 선언부 말고 구현부만 변경할 수 있다.

오버라이딩의 조건은 다음과 같다.

  1. 선언부가 조상 클래스의 메서드와 일치해야 한다
  2. 접근 제어자를 조상 클래스의 메서드보다 좁게 해서는 안 된다
  3. 예외는 조상 클래스 메서드보다 많이 선언할 수 없다

super 키워드는 조상의 멤버를 자신의 멤버와 구별할 때 사용한다. 이름이 중복되지 않는 경우 this.~ , super.~ 둘 다 사용 가능하다. this와 super는 참조 변수라고 생각하면 된다. 

  • 주의할 점은 자기가 선언한 것만 초기화하게 한다.
    - 부모와 자식에서 동시 접근이 가능한 상태에서 무분별하게 선언한다면 관리하기가 어려워진다.
    - 생성자는 부모와 자식 각각의 영역에서 정의하고 변수 초기화를 한다.
  • 생성자의 첫 줄에 반드시 다른 생성자를 호출해야 한다.
    - 그렇지 않으면 컴파일러가 생성자의 첫 줄에 super();를 자동으로 삽입한다.
  • 기본 생성자는 되도록 꼭 작성한다.

패키지는 서로 관련된 클래스들의 묶음이다. 클래스 패스는 이름 그대로 클래스 파일의 위치를 알려주는 경로다.

환경 변수로 관리하고 윈도우의 경우 세미콜론으로 경로를 추가한다.

 

import 문은 클래스 사용 시 패키지 이름을 생략할 수 있도록 하는 역할을 한다. java.lang 패키지 클래스는 생략이 가능하다. 일반적으로 컴파일러를 위한 것이고, *로 패키지에 있는 모든 클래스를 import 한다고 해서 프로그램의 성능이 떨어지지는 않는다. 즉 프로그램 성능에 영향이 없다.

 

제어자는 일종의 형용사로서 static, final, abstract 등이 있다. 붙을 수 있는 위치는 다음과 같다.

  • static  
    • 멤버변수 - 공통 속성
    • 메서드 - 인스턴스 멤버(인스턴스 변수, 인스턴스 메서드)를 사용할 수 없다
  • final
    • 클래스 - 확장될 수 없는 클래스, 더 이상의 상속 관계를 가지지 못 하게 한다 
    • 메서드 - 오버라이딩 제한
    • 변수 - 멤버 변수, 지역 변수 - 상수 
  • abstract 
    • 클래스 - 추상 클래스: 추상 메서드를 하나라도 가지고 있는 클래스, 미완성 설계도
    • 메서드 - 추상 메서드: 구현부가 없는 클래스

추상 클래스는 기본적으로 미완성 설계도기 때문에 인스턴스를 생성하지 못 한다. 대신 추상 클래스를 상속 받아서 완전한 클래스를 만든다면 객체 생성이 가능해 진다. 완전한 클래스는 구상 클래스 또는 구체 클래스 (=concrete class)로 불린다. 

 

그 밖의 제어자에는 대표적으로 접근 제어자가 있는데 이름 그대로 접근 범위를 표현한 것이다. 외부로부터의 접근을 허용하느냐 제한하느냐는 캡슐화와 관련이 있다.

  • public - 접근 제한이 없음
  • protected - 같은 패키지 + 상속 관계의 호출 허용
  • (default) - 기본적으로 생략한다, 같은 패키지
  • private - 같은 클래스 

캡슐화(encapsulation)는 외부로부터의 직접 접근을 막고 메서드를 통한 간접 접근만 허용하도록 한다. 이렇게 하는 이유는 크게 두 가지 이유가 있다.

  • 외부로부터 데이터를 보호하기 위해
  • 외부로의 노출이 불필요하고 내부적으로만 사용되는 부분을 감추기 위해

다형성은 조상 타입의 참조 변수로 자손 타입의 객체를 다루는 것이다. ex. Product p = new Tv();

다형성을 이용하면 다음과 같은 장점(2)이 있다.

  1. 다형적 매개변수
    - 매개변수 타입을 조상 타입으로 정의하면 그를 상속한 자손 타입의 객체가 모두 들어올 수 있다.
  2. 배열
    - 원래 배열은 같은 타입의 객체만 여러 개 받을 수 있다. ex. Tv[] tvs = new Tv[4]; 
    Tv뿐만 아니라 다른 객체 타입들도 한꺼번에 받고 싶다면 조상 타입으로 정의해 이를 상속한 클래스 객체들을 모두 박을 수 있게 한다. Product[] ps = {new Tv(), new Microwave(), null } null도 올 수 있다.

기본형 형 변환이 있듯이 참조형에도 형 변환이 있을 수 있다. 정확히 말하자면 참조 변수의 형 변환으로 사용할 수 있는 멤버의 개수를 조절하는 것이다.  

  • 조상 - 자손 관계의 참조 변수만 서로 형 변환이 가능하다
  • 실제 객체가 가진 멤버의 개수를 보고 넘지 않도록 하는 것이 중요하다
  • 참조 변수 간 형 변환이 이루어지는 것이지, 참조 변수가 가리키고 있는 객체에는 변함 없다 
  • 참조 변수의 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다

cf. 모든 참조 변수는 null 또는 4 byte의 주소값이 저장된다.

 

형 변환 하기 전에 먼저 형 변환이 가능한지 확인하는 과정이 필요할 수 있다. 그럴 때 사용하는 연산자가 instanceof 연산자다. 주로 참조 변수가 참조하고 있는 인스턴스가 어떤 타입인지를 확인하기 위해 사용한다.

ex. if(p instanceof Tv) - 참조 변수 p가 Tv 인스턴스를 가리키는지를 확인하는 상황

Product p = new Product();
Tv t = (Tv) p; // 에러 (전제 조건 - Tv 클래스는 Product를 상속, 더 많은 멤버들을 가지고 있음)

  

위 코드 상황은 Product 타입의 참조 변수 p를 Tv 타입으로 형 변환하려는 상황이다. 일반적으로 Tv라는 것은 Product라는 제품이므로 Tv 클래스가 Product 클래스를 상속한다고 생각할 수 있다. 그러면 상속 관계에 놓인 참조 변수들을 형 변환하는 것이니까 괜찮지 않을까 볼 수 있는데, 문제는 Tv 클래스에 Product 클래스보다 더 많은 멤버들이 있다는 것이다. 이게 문제가 왜 발생하냐면 t 참조 변수로 결국 객체에 접근하려는 것이 목적인데 실제 인스턴스(Product)에 Product에는 없는 Tv 멤버를 호출할 수 있기 때문이다. 그 결과 조상 타입의 인스턴스를 자식 타입의 참조 변수에 담으려고 했기 때문에 ClassCastException이 발생한다.

 

그리고 추상 메서드, 추상 클래스, 인터페이스를 차례로 살펴 보자.

  • 추상 메서드 - 꼭 필요하지만 자손마다 다르게 구현될 것으로 예상되는 경우
  • 추상 클래스의 메서드 중 일부 (추상 메서드)만 구현하면 클래스에 abstract를 붙인다, 다른 일부를 생략할 수 있다
  • 추상 클래스 속 인스턴스 메서드에서 추상 메서드를 호출할 수 있다
    • 왜냐하면 결국 추상 클래스는 자손 클래스가 상속을 통해 완성하고 이후에 객체 생성을 하기 때문이다
  • 추상 메서드라는 것은 자손 클래스들에서 공통 부분을 추출한 것으로 볼 수 있다
    • 코드 중복 제거, 간결, 설계도 쉽게 작성, 변경 용이(관리) 효과가 수반된다

추상화는 구체화와 반대되는 개념이다. 직관적으로 생각해 보아도 추상화하여 여러 개념을 적용하므로 유연하고 변경에 유리하다. 인터페이스는 위 추상 메서드들의 집합이라고 생각하면 된다. interface의 뜻은 서로 다른 종류들 사이를 잇고 연결하는 것이다. 그렇기 때문에 중간 역할자라고 볼 수 있고, 이를 프로그래밍에 접목시키면 사람과 기계를 연결하는 기계의 껍데기가 될 수 있다. 그리고 인터페이스 개념은 다형성과 이어진다. 

 

A 클래스에서 B 클래스를 참조한다고 생각해 보자. 이때 B에서 C로 변경하고자 할 때 첫 번째 상황에서는 A에서 직접적으로 코드를 변경해 주어야 한다. A에서 B를 직접적으로 선언하기 때문에 변경이 불가피하며 이때는 강한 결합의 상황을 보여준다. 이에 반해 두 번째 상황에서는 A에서 B를 직접 참조하는 것이 아니라 I를 의존하고 있다. 인터페이스로 추상화된 설계도를 정해 놓고 B와 C처럼 구체 클래스를 만들어 두는 것이다. 이렇게 하면 B에서 C로 변경한다고 해도 A의 코드를 직접적으로 변경하지 않아도 된다. 부모 타입으로 인터페이스 타입을 설정해 두었으니까. 따라서 이는 느슨한 결합의 상황이다.

 

cf. A가 B를 사용한다는 것은 A가 B에 의존한다는 것이다. 

개별 클래스에 직접 의존 vs 인터페이스에 의존

  • JDK1.8부터 인터페이스에 디폴트 메서드, static 메서드를 추가로 선언할 수 있다. 
    (인터페이스를 구현한 클래스들이 인터페이스에 새로운 추상 메서드를 추가할 때 구현하는 책임을 피하도록)

인터페이스의 장점은 다음과 같다.

  • 변경에 유리하고 유연하다 → 에러날 확률이 낮다
    첫 번째 상황처럼 코드 변경을 직접적으로 할 기회가 많을수록 에러 날 확률이 커진다.  
  • 선언과 구현을 분리시킬 수 있다
  • 개발 시간을 단축시킬 수 있다
    (알맹이가 완성되지 않아도 껍데기만으로 코드 작성이 가능하다)
  • 변경에 유리한 유연한 설계를 할 수 있다
  • 표준화가 가능하다
  • 서로 관계 없는 클래스들 간 관계를 맺어줄 수 있다, 공통점을 뽑아낼 수 있다

내부 클래스는 클래스 안의 클래스를 말한다. 

  • (접근성) 내부 클래스에서 외부 클래스의 private 멤버도 접근 가능하다
  • (코드 복잡성) 외부 클래스와 내부 클래스는 둘 간 긴밀한 관계를 맺고 있다
  • 종류 (3개 - 변수처럼 + 익명클래스)

익명 클래스는 이름이 없는 일회용 클래스로 정의와 객체 생성을 동시에 한다.

new 조상 클래스/인터페이스 이름 {
	...
    (메서드의 구현부)
}