약점 - 자바 & 객체 지향

Kwon Donghyun·2024년 4월 22일
0
post-thumbnail

객체 지향 프로그래밍(OOP)에 대해서는 10년 이상 보고 듣고 배웠는데 과연 나는 OOP에 대해 얼마나 잘 알까? 라는 의문으로 책 - 스프링 입문을 위한 자바 객체 지향의 원리와 이해을 읽으며 부족했던 부분을 채운 내용을 정리하는 글입니다.

스프링을 잘하기 위해서 자바의 기본을 다시 돌아보는 좋은 계기가 될 수 있을 것 같다.


자바와 객체 지향

객체 지향은 현실 세계를 반영한다.
기존 구조적 프로그래밍 언어에서 가장 중요한 것은 함수였는데 이후 나온 개념이 객체 지향이고, 주변에서 사물을 인지하는 방식대로 프로그래밍할 수 있기 때문에 직관적이다.

객체 지향의 4대 특성

캡슐화(Encapsulation) - 정보 은닉
상속(Inheritance) - 재사용
추상화(Abstraction) - 모델링
다형성(Polymorphism) - 사용 편의

클래스와 객체

기존에 우리는 클래스와 객체를 설명할 때 붕어빵틀과 붕어빵으로 대부분 설명했다. 하지만 이 책에선 다른 시각으로 바라본다.

클래스 객체명 = new 클래스();
붕어빵틀 붕어빵 = new 붕어빵틀(); // = 새로운 붕어빵틀을 만들면 붕어빵이 만들어진다.

특정 사물 틀로 원하는 사물을 만든다.를 클래스와 객체로 설명하기 위해 "틀"은 빠지지 않고 나왔다.
클래스는 분류, 객체는 사물이라는 관계로 설명하면 클래스 : 객체 = 펭귄 : 뽀로로 = 사람 : 김연아 처럼 나오게 된다. 즉, 어떤 개념이 클래스라면 실체가 되는게 객체라고 생각하면 된다.

더 자세한 내용은 해당 책의 추상화: 모델링을 통해 공부하는 것을 추천한다.

객체 지향의 4대 특성 중 추상화는 모델링이다.

  • OOP의 추상화는 모델링이다.
  • 클래스 설계에서 추상화가 사용된다.
  • 클래스 설계를 위해서는 애플리케이션 경계부터 정해야 한다.
  • 객체 지향에서 추상화의 결과는 클래스다.

추상화의 개념을 넓게 보면 상속을 통한 추상화 & 구체화, 인터페이스를 통한 추상화, 다형성을 통한 추상화 등이 포함될 수 있다.
자바는 객체 지향의 추상화를 class 키워드를 통해 지원한다. (추상화 = 모델링 = 자바의 class 키워드)

클래스 객체_참조_변수 = new 클래스();

  • 클래스 : 객체_참조_변수의 자료형(Type)
  • 객체_참조_변수 : 생성된 객체를 참조할 수 있는 변수
  • 클래스의 인스턴스 : 객체 생성자를 호출해 생성된 객체

추상화와 T 메모리

이 책을 읽고 정리가 필요하다. 라는 생각을 하게 된 가장 중요한 부분인 메모리 영역이다.

클래스를 설계하기 위해 객체의 공동 특성을 뽑아야 한다.
ex) 쥐의 공동 특성 : 성명, 나이, 꼬리수 / 행위 : 울다()

public class Mouse {
	public String name;
    public int age;
    public int countOfTail;
    
    public void sing() {}
}

public class MouseDriver {
	public static void main(String[] args) {
    	Mouse mickey = new Mouse();
        mickey.name = "미키";
        mickey.age = 85;
        mickey.countOfTail = 1;
        
        mickey.sing();
        
        Mouse jerry = new Mouse();
        jerry.name = "제리";
        jerry.age = 73;
        jerry.countOfTail = 1;
        
        jerry.sing();
    }
}

객체 지향을 통해 Mouse라는 클래스(개념)가 있고, 개념들에 해당하는 mickey, jerry 객체(사물)들이 존재한다.

객체에 대해 이해했으면 메모리에서 어떻게 존재하는지 아는 것이 중요하다고 생각한다.

  • 스태틱 영역 : 클래스의 장소
  • 스택 영역 : 메서드의 장소
  • 힙 영역 : 객체의 장소
  1. 스태틱 영역에 클래스 - Mouse와 MouseDriver가 존재하게 된다.
  2. 스택 영역에 시작점의 main 메서드가 생기며 mickey와 jerry가 포함된다.
  3. 힙 영역에 스택 영역을 통해 생성된 객체들이 생기며 정보가 저장되고 주소를 통해 연결된다.

이 3가지 순서는 가장 기본이지만 처음에는 제대로 이해하지 못한 채로 메서드와 객체들이 존재한다라고만 이해한 채로 개발을 진행하게 되는 것 같다. 지금까지 대략적으로만 이해하고 이번에야 정확하게 공부하면서 이해할 수 있었다.

또한 Mouse의 countOfTail이 항상 1이라면 불필요한 메모리를 사용하게 되므로 별도로 입력하기보다는 static을 통해 스태틱 영역에 저장하고 Mouse.countOfTail을 통해 접근할 수 있다.

상속: 재사용 + 확장

상속이 부모 - 자식으로만 연결시킬 수 없다.
상위 클래스의 특성을 하위 클래스에서 상속하고 더 필요한 특성을 추가(확장)해서 사용할 수 있다.
우리는 클래스에서 상속을 inheritance가 아닌 확장의 extends를 사용한다.

클래스를 상속하는 뜻(부모-자식)보다는 클래스의 특성을 상속하므로 상위 클래스-하위 클래스 or 슈퍼 클래스-서브 클래스 로 표현하는 것이 더 좋다.

public class 동물 {
	String myClass;
    
    동물() { myClass = "동물" }
    
    void showMe() { System.out.println(myClass); }
}

public class 포유류 extends 동물 {
	포유류() { myClass = "포유류"; }
}
public class 조류 extends 동물 {
	조류() { myClass = "조류"; }
}
public class 고래 extends 동물 {
	고래() { myClass = "고래"; }
}
public class 박쥐 extends 동물 {
	박쥐() { myClass = "박쥐"; }
}
public class 참새 extends 동물 {
	참새() { myClass = "참새"; }
}
public class 펭귄 extends 동물 {
	펭귄() { myClass = "펭귄"; }
}

// (1)
동물 animal = new 동물(); // animal.showMe();
포유류 mammalia = new 포유류(); // mammalia.showMe();
조류 bird = new 조류(); // bird.showMe();
고래 whale = new 고래(); // whale.showMe();
박쥐 bat = new 박쥐(); // bat.showMe();
참새 sparrow = new 참새(); // sparrow.showMe();
펭귄 penguin = new 펭귄(); // penguin.showMe();

// (2)
동물 animal = new 동물(); //  animal.showMe();
포유류 mammalia = new 포유류(); // mammalia.showMe();
조류 bird = new 조류(); // bird.showMe();
고래 whale = new 고래(); // whale.showMe();
박쥐 bat = new 박쥐(); // bat.showMe();
참새 sparrow = new 참새(); // sparrow.showMe();
펭귄 penguin = new 펭귄(); // penguin.showMe();

// (3)
동물[] animals = new 동물[7];
animals[0] = new 동물();
animals[1] = new 포유류();
animals[2] = new 조류();
animals[3] = new 고래();
animals[4] = new 박쥐();
animals[5] = new 참새();
animals[6] = new 펭귄();

for (int index = 0; index < animals.length; index++) {
	animals[index].showMe();
}

(1), (2), (3) 모두 동일한 결과가 나오는데 주로 (1)로만 쓰게 되다보니 지금까지 제대로 활용하지 못 했다는 생각이 들었다.

  • 상속은 is a 관계보다는 is a kind of 관계이다.
    • 객체 is 클래스 => 펭귄 is a 동물 (펭귄은 한 마리의 동물이다.)
      • 한 마리의 동물은 객체가 될 수 있기 때문에 is a 보다는 is a kind of 가 더 적합하다.
    • 하위 클래스 is a kind of 상위 클래스 => 펭귄 is a kind of 동물 (펭귄은 동물의 한 분류이다.)
  • 객체 지향의 상속은 상위 클래스의 특성을 재사용하고 확장하는 것이다.

상속과 인터페이스

자바는 다중 상속 대신 인터페이스를 도입했다.

  • 인터페이스는 is a kind of 보다는 is able to가 적합하다.
    • 구현 클래스 is able to 인터페이스
    • Serializable 인터페이스: 직렬화할 수 있는
    • Cloneable 인터페이스: 복제할 수 있는
    • Comparable 인터페이스: 비교할 수 있는
    • Runnable 인터페이스: 실행할 수 있는
public class Animal {
	public String name;
    
    public void showName() { ... }
}

public class Penguin extends Animal {
	public String habitat;
    
    public void showHabitat() { ... }
}

public class Driver {
	public static void main(String[] args) {
    	Penguin pororo = new Penguin();
        
        pororo.name = "뽀로로";
        pororo.habitat = "남극";
        
        pororo.showName();
        pororo.showHabitat();
        
        Animal pingu = new Penguin();
        
        pingu.name = "핑구";
        // pingu.showHabitat();
    }
}

  • 스태틱 영역에 클래스 Driver, Animal, Penguin 3개가 존재
  • 스택(LIFO) 영역에 main 메서드에 사용하는 변수 pingu, pororo, args 존재
  • 힙 영역에 인스턴스 2개 중 pororo는 Penguin을, pingu는 Animal로 연결되어 존재
    • pororo는 Penguin과 Animal 속성, 메서드 모두 사용 가능
    • pingu는 상위 클래스 Animal이 가진 속성과 메서드만 사용 가능
      • pingu는 펭귄이 new Penguin()을 통해 만들어지지만 Animal인 정보만 알 수 있다.

다형성: 사용편의성

오버라이딩(overriding)
같은 메서드 이름, 같은 인자 목록으로 상위 클래스의 메서드 재정의
오버로딩(overloading)
같은 메서드 이름, 다른 인자 목록으로 다수의 메서드 중복 정의

오버라이딩은 기존 메서드 위에 새로 쓰기 때문에 라이딩 = 재정의, 오버로딩은 이름은 같지만 다른 인자 목록으로 여러 개를 불러올 수 있어 로딩 = 중복으로 생각하면 헷갈리지 않는다.

public class Animal {
	public String name;
    
    public void showName() { ... }
}

public class Penguin extends Animal {
	public String habitat;
    
    public void showHabitat() { ... }
    
    // 오버라이딩
    public void showName() { ... }
    
    // 오버로딩
    public void showName(String yourName) { ... }
}

public class Driver {
	public static void main(String[] args) {
    	Penguin pororo = new Penguin();
        
        pororo.name = "뽀로로";
        pororo.habitat = "남극";
        
        pororo.showName();
        pororo.showName("친구");
        pororo.showHabitat();
        
        Animal pingu = new Penguin();
        
        pingu.name = "핑구";
        pingu.showName();
    }
}

  • 여기서 잘 봐야하는건 Animal의 showName()과 Penguin의 showName() 메서드이다.
    • 두 개는 동일한 메서드 = 오버라이딩을 통해 재정의한 메서드인데 Penguin에서 showName()을 사용하면 재정의한 메서드가 호출된다.
    • Animal의 showName()이 중요한데 상속한 경우 Animal 객체 = new Penguin();의 경우 객체는 Animal이 가진 정보만 사용할 수 있다. 근데 이 경우에 Animal.showName()이 실행되는 것이 아닌 Penguin에서 재정의한 메서드가 실행되게 된다.
  • 즉, 오버라이딩의 경우 하위 클래스가 재정의한 메서드를 알아서 호출해주기 때문에 하위 클래스가 뭔지 알 필요가 없다.
동물[] 동물들 = new 동물[5];

동물들[0] = new();
동물들[1] = new 고양이();
동물들[2] = new 강아지();
동물들[3] = new 송아지();

for (int i = 0; i < 동물들.length; i++) {
	동물들[i].울기();
}

상위 클래스에 해당하는 동물의 메서드 울기()를 실행하면 하위 클래스 쥐, 고양이, 강아지, 송아지에서 재정의된 울기()가 자동으로 실행되게 된다.

상속과 다형성을 잘 이용하면 우린 상위 클래스를 통해 필요한 하위 클래스의 역할을 잘 실행할 수 있고 이것으로 Spring의 PSA를 잘 활용할 수 있을 것이다.


캡슐화: 정보 은닉

접근 제어자 : private, [default], protected, public

접근 제어자가 객체(인스턴스) 멤버와 쓰일 때 / 정적(클래스) 멤버와 함께 쓰일 때 비교해봐야 한다.

  • 기본적으로 각 클래스에 맞게 상속된 경우와 패키지가 어디에 속했는지에 따라 접근할 수 있는 경우가 다르다.
    • 기본 접근제어자부터 static 접근제어자에 접근할 수 있는 경우가 어떻게 되는지 정확하게 아는 것은 매우 중요하다.
    • 일단 메서드와 static 메서드일 때와 클래스명.static변수 & static 변수 & this.static변수 접근 등에 대해 잘 아는 것이 중요하게 생각됐다.
      • 정적 멤버에 접근할 때는 클래스명.정적멤버 형태로 접근하는 것을 권장한다.
      • 클래스명.static변수 접근 가능 부분과 static 변수 접근 가능 부분, this.static변수 접근 가능 부분을 패키지별, 상속별로 꼭 한 번씩 체크해보면 좋을 것 같습니다. (해당 책 코드의 chapter 3 참고)

정리

  • 이 책은 저자의 생각이 강하게 들어가 있고, 개인적으로 저자의 생각은 참고 정도 하면 좋을 것 같습니다.
  • 자바를 공부하셨던 분들 or 주니어 개발자 중에서 다시 한 번 돌아보면 부족했던 개념을 채우는데 큰 도움이 될 수 있는 분들이 계실 것 같습니다.
    • 개인적으로 메모리에서 어떻게 저장되고 사용되는지에 대한 이해가 부족했었는데 이번에 확실하게 이해를 하면서 객체를 사용하는데 있어 상속 & 다형성을 좀 더 의미있게 쓸 수 있을 것 같습니다.

0개의 댓글

관련 채용 정보