안녕하세요! 자바 프로그래밍 입문 아홉번째입니다~ 다음 포스팅까지 해서 자바 입문 단계를 마치고 그 다음에는 DB관련 포스팅을 올리려고 합니다.
아마 메인은 Oracle DB가 될것같고, 자바와 연계해서 어떻게 사용하는지 다룰 것으로 예상합니다.
자, 그러면 오늘 살펴볼 내용은 객체 지향 프로그래밍의 특징인 Abstract(추상화), 인터페이스에 대해 정리해보고 변수의 종류에 대해 알아보도록 하겠습니다!
그러면 늘 하던것처럼 이전 포스팅 내용 간단하게 살펴보고 가겠습니다!
이전 포스팅에서 객체 지향 프로그래밍의 특징 중 하나인 상속이라는 개념에 대해 다뤘습니다.
상속이라는 것은 자식 클래스가 부모 클래스의 특성들을 물려 받아서 사용하는 개념이었죠.
자식 클래스를 부모 클래스에 extends
해서 부모 클래스에 있는 것들을 가져와서 쓸 수가 있습니다.
클래스의 상속은 다음과 같이 이루어 졌습니다.
우선 부모 클래스가 있으면
public class Parent {
private int number;
protected String name;
public void parentMethod () {
System.out.println("ParentClass parentMethod()");
}
}
자식 클래스는 extends
라는 키워드로 부모 클래스로부터 상속을 받았습니다.
public class Child extends Parents {
private double height;
public void childMethod () {
number = 1;
name = "홍길동";
System.out.println("childMethod()");
}
}
그래서 우리 눈에 보이진 않지만 부모 클래스에 있는 것들이 자식 클래스로 다 넘어와 있습니다.
그리고 부모 생성자를 호출할 때, super()
메서드를 사용한다는 것도 있었죠
// 부모 클래스
public class People {
private String name;
private String ssn;
public People(String name, String ssn) {
this.name = name;
this.ssn = ssn;
}
}
// 자식 클래스
public class Student extends People {
private int studentNo;
public Student(String name, ssn, int studentNo) {
super(name, ssn); // 부모 생성자를 호출
this.studentNo = studentNo;
}
}
이렇게 부모 클래스에 있는 메서드를 가져올 수 있었죠.
오버라이드라는 개념도 있었습니다. 오버라이드는 부모 클래스로부터 상속받은 메서드를 재정의 하는 것입니다.
// 부모 클래스
public class ParentClass {
public void pMethod() {
System.out.println("ParentClass pMethod()");
}
}
// 자식 클래스
public class ChildClass extends ParentClass{
@Override // 오버라이딩, 부모 클래스의 메서드를 재정의
public void pMethod() {
System.out.println("ChildClass pMethod()");
}
public void func() {
pMethod(); // 이 클래스 내부의 오버라이딩 된 메서드
super.pMethod(); // 부모 클래스의 메서드
}
}
그리고 다형성의 개념은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질이었습니다. 자바에서는 하나의 타입에 여러가지 객체를 넣어서 다양한 기능을 이용하게 해주는 것이었죠.
그래서 우리가 중요하게 살펴봤던 것은 객체의 타입변환이었죠. 타입변환이라는 것은 자료형변환과 비슷한 개념이었고 상속된 객체끼리만 가능하다는 특징이 있었습니다.
부모 객체를 인스턴스로 해서 자식 객체를 만들 수 있었습니다.
// Parent class
public class ParentClass {
public void Method() {
System.out.println("ParentClass Method()");
}
public void func() {
System.out.println("ParentClass func()");
}
}
// Child class
public class ChildOneClass extends ParentClass{
@Override
public void Method() { // OverRide
System.out.println("ChildOneClass Method()");
}
public void function() {
System.out.println("ChildOneClass function()");
}
}
이렇게 부모 클래스와 자식 클래스가 있다고 가정했을 때, 메인 함수에서 어떻게 활용을 했냐하면
import cls.ChildOneClass;
import cls.ParentClass;
public class Main {
public static void main(String[] args) {
ParentClass pc = new ChildOneClass();
pc.Method(); // "ChildOneClass Method()"
pc.func();
pc.function(); // 이렇게는 작동하지 않습니다.
}
}
이 때, pc.Method()
는 오버라이딩된 메서드이기 때문에 해당 메서드가 오버라이딩된 메서드로 저장이 되고 이를 호출합니다.
그러나 자식 클래스에서 오버라이딩 되지 않은 메서드는 사용할 수가 없죠.
그래서 오버라이딩 되지 않은 메서드는 캐스터 변환을 해서 사용해야 했죠.
import cls.ChildOneClass;
import cls.ParentClass;
public class Main {
public static void main(String[] args) {
ParentClass pc = new ChildOneClass();
pc.Method(); // "ChildOneClass Method()"
pc.func();
pc.function(); // 이렇게는 작동하지 않습니다.
ChildClass cc = (ChildCalss) pc; // 부모 객체를 자식객체로 넣어주고
cc.function(); // 적용이 됩니다.
}
}
그리고 instanceof
는 부모 변수가 참조하는 객체가 부모 객체인지 자식 객체인지 확인하는 것으로 boolean
값을 반환해줍니다.
여기까지 복습 마치고 이번 포스팅의 본 주제로 들어가 보겠습니다.
Abstract Class
추상 클래스가 무엇일까요? 말 그대로 추상적인 클래스 입니다. 이렇게 하면 잘 와닿지 않으니까 클래스의 틀이라고 생각하면 좋을 것 같습니다.
일반적인 클래스에서는 멤버변수나 멤버 메서드가 클래스 안에 선언되었고 이를 바탕으로 상속을 해서 확장시키거나 기타 다양한 활용을 할 수 있었습니다.
추상 클래스도 마찬가지로 일반 메서드를 포함시킬 수 있고, 멤버 변수도 가질 수 있습니다.
추상 클래스 안에는 추상 메서드(Abstract Method)라고 하는 특이한 메서드를 선언할 수 있는데 이것은 처리를 다루는 내용은 없고 매개변수나 return
값, 즉 prototype
만 선언된 메서드가 들어와야 합니다.
추상 메서드의 형식은 보통의 메서드에 abstract
를 붙여주면 추상 메서드가 됩니다. 물론 중괄호 블록은 포함되지 않습니다.
이러한 형식의 메서드를 한 개 이상 가지고 있는 클래스를 추상 클래스라고 부릅니다.
// AbstractClass.java
// 일반적인 클래스의 형태 : 접근 제어자 + class + class name
public class MyClass { ... }
// 추상 클래스의 형태 : 접근 제어자 + abstract + class name
public abstract class AbstractClass {
private String name; // 선언되지 않은 일반적인 변수, 문제 없습니다.
public AbstractClass() {} // 생성자
public AbstractClass(String name) { // 매개변수가 있는 생성자, 문제 없습니다.
this.name = name;
}
public void method() { // 일반 메서드, 문제 없습니다
System.out.println("AbstractClass method()");
}
public abstract void abstractMethod(); // 추상 메서드
}
이 클래스는 단독적으로는 사용하기 어렵기 때문에 부모 클래스로서 다른 자식 클래스를 두고 상속관계에서 사용을 해야 합니다.
자 그러면 다른 클래스를 만들어서 확장을 시켜주겠습니다.
// MyClass.java
package cls;
import abstractCls.AbstractClass;
public class MyClass extends AbstractClass { // 상속
public MyClass() {
super();
}
@Override
public void abstractMethod() {
System.out.println("MyClass abstractMethod()");
}
public void func() {
System.out.println("MyClass func()");
}
}
자 우선은 상속을 위해서 extends
를 사용했고, 블럭 안으로 들어가보면 부모 생성자를 호출하기 위해서 super()
메서드를 사용했습니다.
그리고 부모 클래스에 있는 abstractMethod()
를 사용하기 위해서 오버 라이딩 해줬습니다.
상속의 과정을 통해서 부모 클래스에 공병(빈병)느낌으로 만들어준 abstractMethod()
를 가져와서 내용을 재정의 해줬죠.
그다음에 나오는 멤버 메서드는 자식 클래스만의 메서드죠.
그리고 두번째 자식 클래스도 만들어줍니다.
// YouClass.java
package cls;
import abstractCls.AbstractClass;
public class YouClass extends AbstractClass {
@Override
public void abstractMethod() {
System.out.println("YouClass abstractMethod()");
}
}
마찬가지로 AbstractClass
의 상속을 받아서 abstractMethod()
를 오버라이딩하여 재정의했습니다.
여기에서 알 수 있는 사실은 MyClass
와 YourClass
가 공통적으로 가지고 있는 그릇이 abstractMethod()
인데, 이를 하나의 추상 클래스로 묶어서 오버라이딩 방식으로 끌어오는 것입니다.
다시 말해서 두개의 공통된 특징을 추상 클래스에 넣어두고 이를 각각의 자식 클래스에서 가져와 사용하는 것이죠.
자 그러면 추상 클래스의 성질을 파악해보겠습니다. 메인 클래스로 들어가서
// Main.java
public class Main {
public static void main (String[], args) {
// 추상클래스를 객체로 만들어주기
AbstractClass ac = new AbstractClass();
}
}
이렇게 작성해주면 에러가 발생합니다. 추상 클래스는 상속을 통해서만 사용해야 한다는 이유입니다.
그러면 상속을 받은 자식 클래스로 객체를 만들면 어떨까요?
// Main.java
public class Main {
public static void main (String[], args) {
MyClass mycls = new MyClass();
mycls.method(); // "AbstractClass method()"
mycls.abstractMethod();
}
}
이렇게 해주면 자식 클래스로 객체를 만드는 것은 가능합니다. 그리고 그 안에 있는 메서드들도 잘 호출되는 것을 확인할 수 있죠.
mycls.method();
의 경우에는 mycls
가 가리키는 MyClass
에는 method
라고 하는것이 우선 없습니다. 그런데 부모 클래스인 AbstractClass
에는 존재하죠.
상속을 받으면 부모 클래스에 있는 요소들이 자식 클래스로 모두 떨이지기 때문에 mycls.method();
라고 부모 클래스에 있는 것과 동일한 method();
가 출력됩니다.
그리고 mycls.abstractMethod();
는 자식 클래스에서 오버라이딩 되어있기 때문에 메서드가 재정의되어 오버라이딩된 메서드가 출력이 되는것이죠.
그렇지만 부모 클래스의 인스턴스로 자식 클래스를 객체로 생성하는 것은 가능합니다.
// Main.java
public class Main {
public static void main (String[], args) {
AbstractClass ac = new MyClass();
ac.method();
ac.abstractMethod();
}
}
왜냐하면 상속관계에 있는 객체들이니까요!!!
이렇게 해서 ac.method()
, ac.abstractMethod()
를 호출하면 어떤 결과가 나올까요?
ac.method()
를 호출하면 "AbstractClass method()"가 출력되고, ac.abstractMethod()
를 호출하면 오버라이딩된 abstractMethod()
가 호출되어 "MyClass abstractMethod()"가 출력됩니다.
그 다음은 YouClass
에 관한 내용입니다.
// Main.java
public class Main {
public static void main (String[], args) {
YouClass you = new YouClass();
you.abstractMethod();
}
}
이렇게 호출을 하면 YouClass
에 오버라이딩 되어 있는 abstractMethod()
가 호출되어 "YouClass abstractMethod()"가 출력됩니다.
// Main.java
public class Main {
public static void main (String[], args) {
AbstractClass ac2 = new AbstractClass() {
@Override
public void abstractMethod() {
System.out.println("AbstractClass abstractMethod()");
}
};
ac2.abstractMethod();
ac2.method();
}
}
이렇게 하는 경우도 있는데 잠깐 사용하는 용도라면 메인에서 바로 오버라이딩 해줄 수도 있습니다.
그래서 ac2.abstractMethod()
를 호출하면 메인 클래스에서 오버라이딩된 abstractMethod()
가 호출되어 "AbstractClass abstractMethod()"를 출력해 줍니다.
그리고 ac.method()
는 기존의 AbstractClass에 있던 "AbstractClass method()"가 출력이 되겠죠.
Interface
인터페이스는 추상클래스와 비슷한 개념이라고 착각할 수 있지만 다소 다른 특징을 갖습니다.
우선 인터페이스는 추상 메서드만을 포함하는 형태이고, 멤버 변수나 일반 메서드를 포함할 수 없습니다.
추상 클래스에 비해서 완전히 빈 깡통같은 그런 느낌이죠?
자 그러면 추상 클래스를 쓰면 되는거 아닌가, 인터페이스는 왜 쓰나 생각하실 수 있습니다.
인터페이스를 부모객체로 둔다면 자식객체는 인터페이스로부터 상속을 받은 후에 메서드를 정의하여 사용할 수 있게 해줍니다.
그리고 클래스의 사양을 파악할 용도로 사용하죠. 프로젝트의 범위가 커진다면 모든 코드를 전부 분석하는데 시간이 걸립니다. 그래서 각 클래스가 어떤 사양을 가지고 있는지 쉽게 파악하게 해주는 도구로써 사용할 수 있는 것이죠.
그리고 다형성의 측면에서도 사용을 합니다. 상속 받은 클래스를 통해서 또 다른 것을 만들어 줄 수가 있죠.
그리고 가장 중요한 사실은 다중 상속이 가능하다는 것입니다.
우선 인터페이스와 추상 클래스를 잠시 표로 정리해서 비교해 보겠습니다.
구분 | 인터페이스 | 추상 클래스 |
---|---|---|
제한사항 | 멤버변수나 일반 메서드의 포함 불가능 추상 메서드만 포함 | 일반 메서드 포함 가능 멤버 변수 포함 가능 |
비유를 들자면? | 요리의 재료 | 밀키트 |
제가 이렇게 비유를 든 까닭은 인터페이스는 말 그대로 각 클래스의 공통되는 것만 깡통으로 짜집기 해놓은 말 그대로 매우 추상적인 개념이고, 추상 클래스는 재료는 주되 요리는 입맛에 맞게 알아서 하라는 취지로서 이해할 수 있기 때문입니다.
// MyInterface.java
public interface MyInterface {
public void method();
}
이렇게 인터페이스 클래스는 일반적인 클래스의 선언 규칙에 interface
만 포함되는 형태이고, 블록 내부에는 깡통만 들어가 됩니다. 최소 메서드라면 중괄호정도 치는게 추상 클래스라면 인터페이스에서는 중괄호 조차 없는 빈깡통이라는 것이죠.
자 그러면 다른 클래스에 인터페이스를 상속시켜 보겠습니다.
// MyClass.java
import inter.MyInterface; // 다른 패키지에 있으므로 import
public class MyClass implements MyInterface {
}
자 이렇게 상속은 일반 객체나 추상 객체의 상속과 다르게 implements
라는 키워드를 사용 했습니다.
자 그러면 이 클래스 안에서 인터페이스에 선언된 추상 클래스를 가져와서 사용해 보겠습니다.
// MyClass.java
import inter.MyInterface; // 다른 패키지에 있으므로 import
public class MyClass implements MyInterface {
@Override
public void method() {
System.out.println("MyClass method()");
}
}
추상 메서드를 응용하는 방법이기 때문에 마찬가지로 오버라이딩 해서 재정의 해주면 되겠죠!
자 그러면 조금 더 연습을 해볼게요.
YouInterface를 만들어주고 아래와 같이 추상 메서드를 선언해 둡니다.
// YouInterface.java
package inter;
public interface YouInterface {
public void func();
}
그리고 YouClass를 만들어주고 YouInterface에 있는 func()
라는 추상 메서드를 오버라이딩 해봅시다.
// YouInterface.java
package cls;
import inter.YouInterface;
public class YouClass implements YouInterface {
@Override
public void func() {
System.out.println("YouClass func()");
}
}
자 이렇게하면 YouInterface 안에 있던 func()
가 오버라이딩 되어 재정의 되겠죠?
그러면 YouClass안에 MyInterface의 method
까지 오버라이딩 해봅시다. 위에서 인터페이스는 다중 상속이 된다고 했습니다.
그래서 implements
뒤에 MyInterface를 추가해 주시면 되겠죠?
// YouInterface.java
package cls;
import inter.MyInterface;
import inter.YouInterface;
public class YouClass implements MyInterface, YouInterface { // 이름하여 다중상속
@Override
public void method() {
System.out.println("YouClass method()");
}
@Override
public void func() {
System.out.println("YouClass func()");
}
}
Static
과 상수 Final
정적 변수라는걸 보기전에 우리는 자바에서 그리고 프로그래밍에서 사용되는 변수의 종류에 대해서 살펴보아야 합니다.
우선 변수라는 것은 변할 수 있는 수를 말하죠.
우리가 이제까지 살펴봤던 변수들의 종류를 보면 멤버변수, 매개변수, 지역변수, 전역변수가 있습니다. 멤버변수는 클래스 안에 있는 변수이고, 매개변수는 메서드나 함수의 parameter 즉 들어가는 값이라고 불는 것이었죠.
그리고 지역변수는 해당 스코프 안에서만 사용할 수 있는게 지역변수였습니다. 전역변수는 모든 영역에서 다 참조할 수 있는 변수였죠.
자 그러면 우선 MyClass
라는 클래스파일안에서 각각의 변수들의 특징에 대해 보겠습니다.
// MyClass.java
public class MyClass {
private int number;
public static staticNumber;
public void method(int number) {
int localNumber;
{
int localNum;
}
}
}
이렇게 클래스 안에 각각의 변수들이 있는데 어떤 역할을 하는지 살펴보겠습니다.
우선 접근 제어자 private
으로 선언된 4바이트 정수형의 number
는 MyClass
라는 클래스의 멤버변수입니다.
그리고 static
이 붙은 staticNumber
는 정적 변수입니다. 여기에 붙은 static
이 "정적 변수"를 선언해주는 키워드입니다.
그리고 클래스안의 멤버 메서드를 보면 int number
라는 4바이트 정수형의 number
변수가 있는데 이는 메서드의 매개변수(parameter)가 되는 것이죠.
그리고 이 메서드 안에 있는 localNumber
는 지역변수이고 이 메서드 안의 또 다른 블럭에 위치한 localNum
도 지역변수에 해당하죠.
우선 멤버변수는 클래스 내부에서 접근하거나 외부에서는 public
일 경우 어디서든 참조가 가능하고, private
일 때는 getter
나 setter
를 사용해서 값을 조회, 변경할 수 있었습니다. 상속관계에서만 참조할때는 protected
를 사용했죠.
정적변수는 프로그램 실행 전반에 영향을 미치는 변수입니다. 그래서 전역변수로 활용을 많이하죠. 이 정적변수의 개수는 많을수록 좋지 않습니다. 왜냐하면 프로그램이 실행될 때 변수가 선언되고 종료될 때 사라지는 그러니까 프로그램 실행중일때 계속 메모리에 저장되기 때문에 자원낭비 + 속도저하가 될 수 있죠.
매개변수는 우리가 잘 알고 있는것처럼 메서드나 함수에 들어가는 값입니다. 파라미터라고도 하죠.
마지막으로 지역변수는 블록 안에서만 사용할 수 있는 변수를 말합니다.
예시에 method
의 중괄호 안에 선언된 변수가 지역변수였는데, 중괄호 밖을 나가는 순간 그 변수를 끌어서 사용할 수가 없습니다.
이를 바탕으로 MyClass
안에 메서드를 선언해서 각 변수들의 특징을 살펴보겠습니다.
// MyClass.java
public class MyClass {
private int number;
public static staticNumber;
public void method(int number) {
int localNumber;
{
int localNum;
}
}
public void func() {
int localNumber = 0;
number++;
staticNumber++;
localNumber++;
System.out.println("local variable: " + localNumber);
System.out.println("member variable: " + number);
System.out.println("static variable: " + staticNumber);
}
}
우선 멤버변수와 정적변수는 선언 해주는것 만으로도 초기화가 됩니다. 0이 된다는거죠.
그렇지만 지역변수는 초기화를 해주지 않으면 오류가 발생합니다.
메인 클래스로 가서 다음과 같이 입력해주고 실행을 해보면
package main;
import cls.MyClass;
public class Main {
public static void main(String[] args) {
MyClass cls = new MyClass();
cls.func();
cls.func();
cls.func();
}
}
세번 호출했을 때 다음과 같은 결과를 얻습니다.
local variable: 1
member variable: 1
static variable: 1
local variable: 1
member variable: 2
static variable: 2
local variable: 1
member variable: 3
static variable: 3
우선 로컬 변수가 계속 1로 나오는 이유는 메서드 내부에서 0이라고 초기화를 해줬기 때문에 함수가 호출될 때마다 0으로 초기화되어 1씩 더해주기 때문입니다.
그리고 멤버변수와 정적변수는 선언해줄 당시에 0이었고, 여기에 1씩 더한 값이 계속 저장되기 때문에 1로 바뀌지 않는 것이죠.
그리고 또 한가지 중요한 사실은 클래스에 있는 static
이 붙은 메서드(정적 메서드, 정적 함수)를 호출해줄 때는 인스턴스 없이 호출이 가능하다는 것입니다.
예를 들어서 YouClass라는 파일을 만들고 다음과 같이 작성해주겠습니다.
// YouClass.java
package cls;
public class YouClass {
int number;
// 멤버 메서드
public void memberMethod() {
System.out.println("YouClass memberMethod()");
}
// 정적 메서드
public static void staticMethod() {
System.out.println("YouClass staticMethod()");
}
}
그리고 나서 이것을 메인 클래스에서 호출해보겠습니다.
// Main.java
public class Main {
public static void main (String[] args) {
// 멤버 메서드를 호출할 때
YouClass yc = new YouClass();
yc.memberMethod();
// 스태틱 메서드를 호출할 때
YouClass.staticMethod();
}
}
이렇게 멤버 메서드는 객체를 생성해주고 생성된 객체로부터 불러오는 방식이라면 static 메서드(정적 메서드)는 인스턴스가 없어도 바로 호출할 수 있습니다.
final
자바스크립트에서 변수를 선언할 때 var
를 사용했었습니다. 그런데 이것은 이제 더 이상 사용하지 말라는 권고가 있고 대신에 let
이나 const
를 사용하죠. 우리가 변수 즉 값이 유동적인 숫자를 선언할 때는 let
을 써주고, 값이 바뀌지 않는 숫자는 const
로 선언을 해줍니다.
자바에서도 마찬가지로 상수라는 개념 즉 바뀌지 않는 숫자는 final
이라는 선언자를 붙여서 변수를 선언해줍니다. final
은 변수의 자료형 앞에 붙여줍니다.
자바스크립트의 const
와 같은 개념이라고 보시면 되겠죠?
그럼 진짜 값이 바뀌지 않는지 실험을 해보겠습니다.
// Main.java
public class Main {
public static void main (String[] args) {
int number = 1;
number = 2;
}
}
우선 우리가 알고 있는 상식선에서 변수는 언제든지 값을 바꿔 대입할 수가 있습니다.
// Main.java
public class Main {
public static void main (String[] args) {
final int number = 1;
number = 2; // 에러발생
}
}
이렇게 자료형 앞에 final
을 붙이고 다른 곳에서 이 변수를 바꿔 대입하려고 시도하면 에러가 발생합니다. 이렇게 바뀌지 않을 값을 상수라고 하고, 자료형 앞에 final
을 붙여주면 상수로 선언됩니다.
이것을 변수로 바꿔주려면 final
을 떼주면 되겠죠?
뿐만 아니라 클래스에 final
이 선언되면 상속금지를 나타내고, 메서드에 final
이 선언되면 오버라이딩 금지를 나타냅니다.
// Main.java
/* final */ class Parent { // 클래스에 final이 추가되면 상속금지
public /* final */ void method() { // 메서드에 final이 추가되면 오버라이드 금지
}
}
class Child extends Parent {
@Override
public void method() {
}
}
final
을 붙였을 때, 지웠을 때를 각각 비교해보세요.
오늘 정리할 내용은 여기까지입니다.
연습문제는 이어지는 포스팅에서 다루도록 하겠습니다!