안녕하세요~ 개발을 잘하고싶은 개발자입니다.
1편에 이어서 2편 이야기를 진행합니다.
[1편 참조]
- 객체지향 프로그래밍 기본 지식 두번째 이야기
abstract - 추상 메서드와 추상 클래스
: 추상 메서드(Abstract Method)는 선언부는 있는데 구현부가 없는 메서드를 말한다. 추상 메서드를 하나라도 갖고 있는 클래스는 반드시 추상 클래스(Abstract Class)로 선언해야 한다.
- abstract Class, Method 예제
public abstract class Animal {
public abstract void eat();
public abstract void sleep();
}
public class Dog extends Animal {
// 추상화클래스 상속시, 추상 클래스 미선언시 컴파일 에러 발생
@Override
public void eat() {
System.out.println("개사료를 먹는다");
}
@Override
public void sleep() {
System.out.println("주인옆에서 코~ 잔다");
}
}
- 도대체 왜 추상화 클래스를 쓰는것일까?
자체적으로 인스턴스를 생성할 수도 없고, 왜 상속을 받아서 강제로 오버라이딩을 해야하는 것일까? 보통 소규모 프로젝트에서는 사용할 필요가 거의 없다. 하지만 프로젝트 규모가 커지게 되면 당연히 여러 개발자들과 협업을 하게 될 것이다. 이때 공통적으로 작성되어야 할 부분들이 분명히 생겨날 것이다. 공통적인 부분이 개발자들마다 메소드명 필드명 등 여러가지 내용들로 다르게 정의된다면 유지보수 및 관리 문제와 같은 부분들이 발생할 것이다. 따라서 이녀석을 쓰는이유는.. 공통된 내용(필드, 메소드 등)들을 추출하여 통일된 내용으로 작성하도록 규격화 하는 것이다.
- 정리
1] 추상화 클래스는 인스턴스, 즉 객체를 만들 수 없다. (new 사용불가)
2] 추상 메서드는 하위 클래스에게 메서드의 구현을 강제한다. 오버라이딩 강제(미구현시 컴파일 에러)
3] 추상 메서드를 포함하는 클래스는 반드시 추상 클래스여야 한다.
- 생성자 (new 키워드)
: 객체(=인스턴스)를 만들 때 사용한다.
public class Animal() {
}
public class MainClass() {
public static void main(String[] args) {
Animal animal = new Animal(); // 객체 생성
}
}
객체 생성시에 기본적으로 반환값이 없고, 클래스명과 같은 이름을 가진 메서드를 객체를 생성하는 메서드를 생성자(=생성자 메서드)라고 한다. 보통은 자바가 알아서 인자가 없는 기본 생성자를 자동으로 만들어준다. 단, 인자가 있는 생성자를 하나라도 만든다면 자바는 기본 생성자를 만들지 않는다!!
- static (정적 멤버)
: 메모리의 static 영역에 적재하여 사용하도록 선언하는 예약어. 정적 멤버라고도 한다. static이 선언되면, 클래스 생성자를 만들 필요가 없어진다. 그리고 모든 객체가 공유하여 하나의 정적 멤버를 어디에서든지 참조가 가능해진다. 단, GC의 관리영역에서는 벗어나며, 프로그램 종료시까지 메모리에 적재되어있기때문에, 너무 남용하게 되면 성능이슈가 발생할 가능성도 있을 것이다.
(!) static관련해서 하나 짚고 넘어가야할 것이 있다. 프로그램이 시작될때 바로 메모리에 할당되는 것은 아니고, 해당 클래스가 처음 사용될 때 로딩된다. 메모리는 최대한 늦게 사용을 시작하고 빨리 반환하는 것이 좋기 때문이다. (똑똑한 자바..)
- final
: 변경이나 상속을 허락하지 않겠다는 의미를 가진 예약어.
1] 상수 : 최초로 한번 할당. 이후에 변경은 불가능하다.
2] 메소드 : 메소드를 오버라이딩할 수 없다.
3] 클래스 : 상속(extends)할 수 없다.
- instanceof
: 인스턴스는 클래스를 통해 만들어진 객체이다. instanceof 연산자는 만들어진 객체가 특정 클래스의 인스턴스인지를 확인하는 연산자이다. 따라서 이 연산자는 boolean 타입으로 return한다.
class Animal {
}
// Animal을 상속받은 Dog
class Dog extends Animal {
}
// Person interface
interface Person {
}
// Person을 구현한 KoreaPeople
class KoreaPeople implements Person {
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Dog dog = new Dog();
KoreaPeople koreaPeople = new KoreaPeople();
System.out.println(animal instanceof Animal); // true
System.out.println(dog instanceof Animal); // true
System.out.println(animal instanceof Dog); // false
System.out.println(koreaPeople instanceof Person); // true (인터페이스 구현체)
}
}
결과: 상속을 받은 객체는 부모객체와 같을 수가 없지만, 인터페이스를 구현한 구현체는 해당 클래스의 구현체로 판단한다.
- interface와 implements
- interface란?
: 클래스를 작성할 때 구현체(로직)가 없는 일정 틀을 제공해주는 추상화 클래스. 추상메소드와 정적(static)상수만을 포함할 수 있으며, 추상메소드에 코드가 구현되어있는 경우에는 컴파일 오류가 발생한다.
(JAVA 8부터는 default 메소드와 static 메소드도 구현이 가능하도록 추가되었다 - 실제 적용되어있는 코드중, 인터페이스 구현체로부터 추가요건이 있을시에, 조금더 능동적으로 대처하기 위해 추가가 된듯하다)
- interface를 왜사용하는가?
: 1부에서 소개한 자바가 가지는 특성중, [다형성]을 극대화할 수 있는 방법이다. 소규모 프로젝트에서는 굳이 잘 사용하지 않지만, 대형 프로젝트에서는 interface를 사용하므로써 일관되고 정형화된 개발을 위한 표준화가 가능해진다. 이게 개발 시간 단축까지 연결될 수 있다.
- 이게 끝? 아니다. 자바 언어의 특징중 하나가 다중상속이 안된다는 것인데, interface를 활용하여 다중상속을 가능하게 한다. 예제를 통해 확인해보자.
// -----------다중상속이 가능하다고 가정-----------
class Animal {
public void cry() {
System.out.println("동물이 울어요");
}
}
class Cat extends Animal {
public void cry() {
System.out.println("야옹~");
}
}
class Dog extends Animal {
public void cry() {
System.out.println("멍멍!");
}
}
class MyAnimal extends Cat, Dog { // 물론, 컴파일 오류가 나겠지만, 된다고 가정했을때..
}
public class Main {
public static void main(String[] args) {
MyAnimal myAnimal = new MyAnimal();
myAnimal.cry(); // ???? 애매해진다.
}
}
// -----------interface를 통한 다중상속 구현 예제-----------
interface Animal {
public abstract void cry();
}
interface Cat extends Animal {
public abstract void cry();
}
interface Dog extends Animal {
public abstract void cry();
}
class MyAnimal implements Cat, Dog {
public void cry() {
System.out.println("멍멍!");
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
MyAnimal myAnimal = new MyAnimal();
myAnimal.cry(); // 애매한 상속문제를 해결하여 구현
}
}
- implements
: 위에서 설명한 interface를 구현하겠다고 선언하는 키워드.
- this
: 객체가 자기 자신을 지칭할 때 쓰는 키워드.
class Test {
int var = 10;
public void test() {
int var = 20;
System.out.println(var); // 20
System.out.println(this.var); // 10
// => 같은 변수명, 같은타입으로 전역변수와 지역변수가 선언되는 경우, 지역변수가 우선
}
}
- 지역 변수와 속성(객체 변수, 정적 변수)의 이름이 같은 경우 지역 변수가 우선한다.
- 객체 변수와 이름이 같은 지역 변수가 있는 경우, 객체 변수를 사용하려면 this를 사용한다.
- 정적 변수와 이름이 같은 지역 변수가 있는 경우 정적 변수를 사용하려면 클래스명을 접두사로 사용한다.
- super
: 단일 상속만을 지원하는 자바에서 super는 바로 위 상위(부모) 클래스의 인스턴스를 지칭할 때 쓰는 키워드.
(!) 상위 클래스가 또 다른 클래스를 상속받더라도 super.super 이런식으로 사용은 불가능하다..
- 객체 지향 설계 5원칙 - SOLID
- SOLID??
Robert C. Martin 이라는 사람이 초반 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙으로 제시한 것을 Michael Feathers가 두문자어로 소개한 것이라고 합니다..
SRP(Single Responsibility Principle): 단일 책임 원칙
: 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.
OCP(Open Closed Principle): 개방 폐쇄 원칙
: 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.
LSP(Liskov Substitution Principle): 리스코프 치환 원칙
: 서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.
ISP(Interface Segregation Principle): 인터페이스 분리 원칙
: 클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.
DIP(Dependency Inversion Principle): 의존 역전 원칙
: 자신보다 변하기 쉬운 것에 의존하지 마라.
이 원칙들은 응집도는 높이고, 결합도는 낮추는 고전 원칙을 객체 지향의 관점에서 재정립한 것이라고 할 수 있다.
- 결합도는 모듈(클래스)간의 상호 의존정도로서 결합도가 낮으면 모듈간의상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이하다.
- 응집도는 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로 응집도가 높은 모듈은하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다.
SOLID는 객체 지향 4대 특성을 발판으로 하고 있으며, 디자인 패턴의 뼈대이며 스프링 프레임워크의 근간이 된다.
(상세내용 참고: m.blog.naver.com/1ilsang/221105781167)
'JAVA' 카테고리의 다른 글
Java - Enum (0) | 2021.09.15 |
---|---|
Java - 로컬환경 자바프로젝트 세팅 (with Docker) (0) | 2021.09.15 |
Java - 자바 객체 지향의 원리와 이해(개념정리) - 1편 (0) | 2021.02.08 |
Java - lambda와 Stream (0) | 2021.02.04 |
Java - JVM(Java Virtual Machine)의 메모리 영역 - 간단 정리 (0) | 2021.02.04 |