2023.01.25 - 안드로이드 앱개발자 과정

CHA·2023년 1월 27일
0

JAVA



객체지향 프로그래밍(Object Oriented Programming)

Class : 객체의 종류

객체 : 고유의 기능과 값을 가진 녀석(변수 + 함수)


Class


사용자 정의 class

OOP 는 객체가 필요할 때 만들고, 만든 기능들을 사용하여 프로그래밍 하는 방식입니다. 그리고 자바에서는 자주 사용될 기능들을 미리 만들어 설계해놓았습니다. 이러한 설계도 파일들을 우리는 class 라고 부르기로 했습니다. 그리고 그 갯수는 2000여개 정도이며, 이러한 class 들을 Java System Library API 라고 합니다.
그런데, 아무리 2000개의 클래스가 있다고 하더래도 내가 원하는 모든 기능을 미리 가지고 있다는것은 불가능합니다. 그래서 개발자는 본인이 원하는 기능을 스스로 만들어야 하는 경우도 많습니다. 즉, 기능을 가지고 있는 class 를 생성해야하며 이러한 클래스를 사용자 정의 클래스 라고 합니다.

생성 위치에 따른 class 생성법 4가지

  • 1. 별도의 .java 문서에 만드는 방법

    별도의 . java 문서를 만들면 문서의 이름과 동일한 클래스 생성 반드시 . java 의 이름과 클래스 이름이 동일해야함.

  • 2. 하나의 .java 문서 안에 여러개의 클래스를 설계하는 방법

    또 다른 클래스를 하나의 . java 파일안에 생성할 수 있음. 단, public 을 쓸 수 있는 클래스는 하나의 . java 파일에 한개. (파일의 이름이 어떤 클래스 인지 알아야하기 때문!)

  • 3. 클래스 안에 또 다른 클래스를 선언하는 방법

    inner class 라고 부르는 클래스. 숨겨놓은 듯한 클래스 생성법. 이너 클래스는 객체 생성을 하지 못한다! 이유는 추후에 알아보자!

  • 4. 메소드 안에 클래스를 설계하는 방법

    지역 클래스 라고 부르는 클래스. 이 메소드를 실행했을 때만 사용이 가능하다! 그리고 메소드가 끝나면 클래스도 사용할 수 없다. 즉, 이 메소드 내부에서만 사용할 수 있는 일시적인 클래스!


Package


패키지의 개념

클래스를 만들고 사용하다가 보면 비슷한 기능과 속성을 가지게 되는 클래스를 만들때도 있습니다. 그럴때는 사실 다른 이름으로 명명하기 쉽지 않은 경우도 있습니다. 예를 들어, 이름, 나이, 주소 의 정보를 갖는 Student 클래스와 이름, 국어성적, 영어성적의 정보를 갖는 Student 클래스를 만들어야 한다고 생각해봅시다. 그러면 클래스의 이름이 동일하기 때문에 구별할 수 없습니다. 그래서 사용하는것이 패키지 입니다.

패키지의 사용

aaa 라는 이름의 패키지 하나와 bbb 라는 이름의 패키지 하나를 만들겠습니다. 그리고 각각의 패키지에 aaa 에는 이름,나이,주소 의 정보를 갖는 Student 클래스를, bbb 에는 이름,국어성적, 영어성적을 갖는 클래스를 만들었습니다.

aaa 패키지의 Student 클래스

package aaa;

public class Student {
	String name;
    int age;
    String address;
}

bbb 패키지의 Student 클래스

package bbb;

public class Student {
	String name;
    int kor;
    int eng;
}

Main 클래스

public class Main {
	public static void main(String[] args){
    // Student stu = new Student(); error!
    aaa.Student stu = new aaa.Student();
    stu.name = "sam";
    }
}

aaa , bbb 패키지를 만들고, 각각의 패키지를 Main 클래스 내에서 사용하기 위해서는 위 코드와 같이 . 연산자를 이용해 어느 패키지인지를 명시해준 뒤 각 패키지에 맞는 클래스의 객체를 생성할 수 있습니다. 그리고 그 객체를 통해 멤버에 접근하는 형태입니다. 그리고 당연하게 같은 패키지 내에서는 굳이 . 연산자를 이용해 어느 패키지 인지 명시할 필요는 없습니다. 추가로, 패키지명은 항상 소문자로만 작성되어야 한다는 사실도 기억해야겠습니다.

import

자 그런데, 패키지를 만들고 사용하다보니 여간 귀찮은게 아닙니다. 다른 패키지에 있는 클래스를 사용하기 위해서 패키지명을 작성하고 . 연산자를 이용해서 클래스에 접근하려니 코드도 길어지고 가독성도 물론 안좋아집니다. 그래서 사용할 수 있는것이 import 입니다. 위 코드 중 Main 클래스만 다시 보도록 합시다.

import aaa.Student;

public class Main {
	public static void main(String[] args){
    Student stu = new Student();
    stu.name = "sam";
    }
}

import aaa.Student; 를 통해 aaa 패키지에 있는 Student 클래스를 import 했습니다. 그로 인해 Main 클래스가 있는 Main.java 파일안에서는 . 연산자를 통해 aaa 패키지에 대한 접근에 대한 명시가 필요없어졌습니다. 즉, 우리가 지금까지 사용했던 import java.util.Scanner 혹은 import java.util.Random 등은 모두 다른 패키지에 있는 클래스를 사용하기 위해 작성했던 내용들입니다.

java.lang

앞서, Scanner 혹은 Random 클래스를 사용하기 위해서는 import 가 필요하다고 했습니다. 그런데 우리는 이러한 클래스 이외에도 String 혹은 Integer 와 같은 클래스들도 알고 있습니다. 이러한 클래스들은 어떻게 import 없이 사용할 수 있는걸까요?
사실, 이러한 클래스들은 모두 java.lang 이라는 패키지에 들어있는 클래스들입니다. java.lang 패키지는 워낙 기본적인 클래스들만 모아놓은 패키지 이기 때문에 자바 언어 자체에서 자동으로 import 해주는 특성을 가진 패키지 입니다. 그래서 우리는 String 이나 Integer 등을 사용할 때 import 해야하는 수고를 덜었습니다.

도메인과 Package

자, 우리가 사용했던 패키지명을 다시 한번 살펴봅시다. java.lang 이나 java.util 처럼, 보통 패키지명이 xxx . xxx 의 형태의 두 단어 이상으로 구성됨을 알 수 있습니다. 패키지명은 마치 카테고리와 같기 때문에 가급적이면 유의미한 단어를 사용해야 하며 두 단어 이상으로 사용하기를 권장합니다. 그래서 보통 클래스를 만든 회사명의 도메인을 역으로 작성하는 형태를 취합니다. 예를들어 Map 클래스를 만든 회사가 Google, Naver, Kakao 가 있다고 합시다. 그러면 패키지는 다음 3개 처럼 만들어질 수 있습니다.

com.google.Map
com.naver.Map
com.kakao.Map

접근 제한자 (Access Modifier)


접근제한자란?

객체지향은 객체의 멤버들을 외부에서 마음대로 사용하는것을 제한하고자 합니다. 그래서 존재하는 개념이 바로 접근 제한자입니다.

이 접근제한자는 4가지의 종류로 구분됩니다.

  1. private : 본인 클래스 안에서 접근을 가능하게 해주는 제한자
  2. default : 같은 패키지 안에서 . 연산자로 접근을 가능하게 해주는 제한자
  3. protected : 같은 패키지 안에서 . 연산자로 접근을 가능하게 해주는 제한자. 단, 상속관계라면 다른 패키지라도 접근 가능.
  4. public : 어디서든 . 연산자만 있으면 접근을 가능하게 해주는 제한자

그러면, 이 4가지의 접근제한자에 대한 예제를 하나씩 보도록 합시다.

public class First {
	private int a;
	int b;
	protected int c;
	public int d;

	void aaa() {
		a = 10;
		b = 20;
		c = 30;
		d = 40;
	}
}

First 클래스는 Main 클래스와 같은 패키지에 생성했습니다.

package aaa;

public class Second {
	private int a;
	int b;
	protected int c;
	public int d;

	void show() {
		a = 100;
		b = 200;
		c = 300;
		d = 400;
	}
	public void output() {
		a = 100;
		b = 200;
		c = 300;
		d = 400;
	}
}

Second 클래스는 Main 클래스와 다른 패키지, aaa 패키지에 생성했습니다.

일단 여기까지 생성하고 보니, 각각의 클래스들에서 만든 멤버변수들을 각 클래스 내부에서 사용하는것은 아무 문제 없었습니다. 즉, 본인 클래스 내부에서 사용한다면 접근제한자는 무의미해집니다.

그러면 Main 클래스에서 한번 살펴보겠습니다.

import aaa.Second;

public class Main {
	public static void main(String[] args) {
		First f = new First();
		// f.a = 10;	// error! 
		f.b = 20;		// OK!
		f.c = 30;		// OK!
		f.d = 40;		// OK!
        
		f.aaa();		// OK!
	
		Second s = new Second();
		// s.a = 10; 	// error!
		// s.b = 20; 	// error!
		// s.c = 30; 	// error!
		s.d = 40; 		// OK!
		
		// s.show(); 	// error! 
		s.output();		// OK!
	}
}

같은 패키지내에서 만든 First 클래스를 Main 클래스내부에서 사용하려고 보니, private 접근제한자를 가진 a 변수만 사용이 불가했습니다. 또한 다른 패키지에서 생성한 Second 클래스를 사용하려고 보니, public 접근제한자를 가진 변수 d와 멤버함수 output() 만 사용이 가능했습니다. 이처럼 접근제한자를 통해 정보의 변경을 막아 오류를 예방할 수 있게됩니다. 상속과 관련한 protected 는 추후에 다시 알아보겠습니다.


왜? 사용해야하는가?


지금까지 클래스에 대한 문법적인 개념들을 배웠습니다. 그런데 왜? 클래스는 왜 사용해야 할까요? 그리고 메서드는 왜 사용해야 하는것일까요? 필요성을 느끼기 위해 먼저 불편함을 느껴봅시다. 그리고 한 사람의 여행기를 읽듯, 흐름을 타고 느껴봅시다. 먼저, 우리는 학생관리 프로그램을 만들고 싶습니다. 여기가 출발점 입니다. 이 프로그램은 학생의 이름, 국어성적, 영어성적, 평균의 정보를 가지고 있어야 합니다. 다음 코드를 봅시다.

String name;
int kor;
int eng;
double aver;

name = "sam";
kor = 80;
eng = 70;
aver = (double) (kor + eng) / 2;

학생 1명의 정보를 담은 코드입니다. 그런데 만약 학생이 여러명이라면 어떻게 해야할까요? 학생이 10명이라면 위 코드 처럼 하면 참 쉽지 않을것 같습니다. 변수부터 40개를 만들고 시작해야하니까요. 불편함이 느껴지실까요? 그래서 우리는 배열을 사용하기로 합니다. 그러면 원래 40개 만들 변수를 4개의 배열로 처리할 수 있으니까요. 다음처럼 만들어봅시다.

String[] arrName = new String[10];
int[] arrKor = new int[10];
int[] arrEng = new int[10];
double[] arrAver = new double[10];

arrName[0] = "sam";
arrKor[0] = 80;
arrEng[0] = 70;
arrAver[0] = (double) (arrKor[0] + arrEng[0]) / 2;

이런식으로 자료형별로 묶어 배열로 사용을 해봤을 때, 앞서 40개의 변수를 만드는것 보단 편리해졌습니다. 그러나 이또한 만족스런 결과는 아닙니다. 우리가 생각했던 관리방식은 이러한 방식은 아니기 때문입니다. 배열로 관리를 하게된다면 이름따로, 국어성적 따로 다 따로따로 관리를 해주어야 합니다. 배열이기 때문이죠. 저는 연관이 있는 데이터끼리, 즉 학생 1명의 이름과 성적, 평균의 데이터를 한묶음으로 보고싶습니다. 그래서 나온 개념이 class 라는 개념입니다. 자, 다음과 같이 Student 라는 이름의 class 를 정의해봅시다.

public class Student {
	String name;
    int kor;
    int eng;
    double aver;
}

학생의 이름과 국어,영어 성적, 평균의 변수를 가진 클래스를 생성했고, 이 클래스를 사용하기 위해 Main 클래스에서 Student 클래스의 객체를 생성해봅시다. 그리고 학생 2명의 정보를 넣고 출력하는 코드입니다.

Student stu = new Student();
stu.name = "sam";
stu.kor = 80;
stu.eng = 70;
stu.aver = (double) (stu.kor + stu.eng) / 2;

Student stu2 = new Student();
stu2.name = "robin";
stu2.kor = 90;
stu2.eng = 95;
stu2.aver = (double) (stu2.kor + stu2.eng) / 2;


System.out.println(stu); 
System.out.println("name : " + stu.name);
System.out.println("kor : " + stu.kor);
System.out.println("eng : " + stu.eng);
System.out.println("aver : " + stu.aver);
System.out.println();
System.out.println("name : " + stu2.name);
System.out.println("kor : " + stu2.kor);
System.out.println("eng : " + stu2.eng);
System.out.println("aver : " + stu2.aver);
System.out.println();

벌써 지저분합니다. 학생 정보를 넣는 부분은 그렇다 치고, 출력을 하는 쪽의 코드가 너무 길고 반복적인 느낌을 줍니다. 그러면 이 부분을 메서드로 만들어서 출력하면 어떨까요? 괜찮아보입니다.

다음은 Student 클래스 내부에 학생의 정보를 출력하는 메서드 코드입니다.

void show(){
	System.out.println("이름 : " + name);
	System.out.println("국어 : " + kor);
	System.out.println("영어 : " + eng);
	System.out.println("평균 : " + aver);
	System.out.println();
}

그리고 다음 코드는 위의 학생 2명 정보를 넣고 출력하는 코드를 메서드를 활용하여 바꿔보았습니다.

Student stu = new Student();
stu.name = "sam";
stu.kor = 80;
stu.eng = 70;
stu.aver = (double) (stu.kor + stu.eng) / 2;
stu.show();

Student stu2 = new Student();
stu2.name = "robin";
stu2.kor = 90;
stu2.eng = 95;
stu2.aver = (double) (stu2.kor + stu2.eng) / 2;
stu2.show();

코드가 훨씬 간결해진게 느껴지시나요? 앞 코드보다 간결할 뿐 아니라 훨씬 직관적인 느낌입니다. 메서드를 활용해서 코드를 작성하니 이렇게 간편하고 직관적인 코드를 짤 수 있었습니다. 좀만 더 활용해봅시다. 위 코드에서 어디가 찝찝해보일까요? 평균을 구하는 부분이 좀 찝찝합니다. 가독성도 떨어져보이고요. 그래서 이 부분도 메서드로 교체합니다.

--- in Student class
void calAver(){
	aver = (double) (kor + eng) / 2;
}

--- in Main class
Student stu = new Student();
stu.name = "sam";
stu.kor = 80;
stu.eng = 70;
stu.calAver();
stu.show();

Student stu2 = new Student();
stu2.name = "robin";
stu2.kor = 90;
stu2.eng = 95;
stu2.calAver();
stu2.show();

이 코드가 또 한번 진화했습니다. calAver() 메서드를 이용해서 간결하고 직관성 좋게 만들어졌습니다.
자자 더 가봅시다. 이번에는 데이터를 입력하는 부분이 맘에 들지 않습니다. 이 부분도 따로 메서드로 만들어볼까요?

--- in Student class
	void setMembers(String name, int kor, int eng) {
		this.name = name;
		this.kor = kor;
		this.eng = eng;
	}

--- in Main class
Student stu = new Student();
stu.setMembers("sam",80,70);
stu.calAver();
stu.show();

Student stu2 = new Student();
stu2.setMembers("robin",90,95);
stu2.calAver();
stu2.show();

자 파라미터를 통해 데이터들을 setMembers() 메서드로 넘겨주었고, 이 메서드 내부에서 각각의 멤버변수들에 데이터를 넣어주었습니다. 여기서 알아둬야할 부분은 2가지 인데, 멤버변수와 매개변수는 다르다는 점 한가지와 클래스의 멤버변수를 지칭하는 특별한 키워드 this 입니다.

멤버변수는 매개변수와 다른 변수입니다. 그렇기 때문에 매개변수로 받은 데이터를 멤버변수에 넣어주는 로직이 따로 필요합니다. 그런데 , 우리는 어떠한 데이터를 매개변수로 받았을까요? 바로 학생의 이름과 성적 입니다. 그런데 매개변수의 이름을 정할때, name 으로 하지않으면 추후에 헷갈릴 여지가 너무나 많아집니다. 그래서 매개변수의 이름으로 이름을 받을 때는 name, 성적은 kor, eng 등으로 해주는것이 좋습니다.

그런데 이렇게 하려니 문제가 발생합니다. 바로 멤버변수의 이름과 겹치는 문제입니다. 그래서 만들어진 키워드(사실은 참조변수입니다.) 가 this 입니다. 위와 같이 this 키워드를 이용하면 본인 클래스의 멤버변수에 접근할 수 있게됩니다. 그러면, 이름이 같아도 매개변수를 멤버변수에 대입할 수 있게됩니다.

자 이렇게 해서 처음 20줄 가까이 되던 코드가 8줄로 줄어들었습니다. 이것은 클래스의 힘이고, 메서드의 힘입니다. 즉 메소드의 존재 이유는 코드를 보다 간결하게 하고 객체의 멤버를 객체 스스로 제어하도록 하기 위함임을 알아둡시다!


Method


Method 의 문법적 모습 4가지

메소드의 필요성에 대해 알아보았으니, 메소드를 정의하는 4가지의 문법적 모습들을 알아보겠습니다.

1) 리턴타입 X , 매개변수 X
2) 리턴타입 X , 매개변수 O
3) 리턴타입 O , 매개변수 X
4) 리턴타입 O , 매개변수 O

위와 같이 4가지의 패턴이 있으며, 이를 토대로 계산기 프로그램을 하나 만들어봅시다. 먼저, Calculator 클래스를 하나 만들어봅시다. 그리고 Main 클래스에서 Calculator 객체를 하나 생성하고 계산기능을 수행해봅시다.

------- Calculator.java
public class Calculator {
	// 더하기 기능
	int sum(int x, int y) {
		return x + y;
	}

	// 뺄셈 기능
	int subtract(int x, int y) {
		return x - y;
	}

	// 곱셈 기능
	int multiply(int x, int y) {
		return x * y;
	}

	// 나눗셈 기능
	int divide(int x, int y) {
		return x / y;
	}

------- Main.java
public class Main {
	public static void main(String[] args) {
    	Calculator cal = new Calculator();
        int a = 10, b = 5;
        int sum = cal.sum(a, b);
		int sub = cal.subtract(a, b);
		int mul = cal.multiply(a, b);
		int div = cal.divide(a, b);
    }
}
출력결과 : 15 / 5 / 50 / 2

계산이 잘 됩니다. 그런데 잘 보면 우리가 만든 계산기는 정수일 때만 작동합니다. Calculator 클래스의 메소드들의 반환타입이 int 이기 때문입니다. 그래서 우리는 실수형 숫자들의 사칙연산 기능도 추가하고 싶습니다.

메소드 오버로딩(Overloading)

그래서 아래 코드를 Calculator 클래스에 추가해봅시다.

double sum2(double x, double y) {
	return x + y;
}

double subtract2(double x, double y) {
	return x - y;
}

double multiply2(double x, double y) {
	return x * y;
}

double divide2(double x, double y) {
	return x / y;
}

이제 잘 될까요? 계산은 잘 하는것 같습니다. 다만, 메소드의 이름이 다르다보니 사용할 때 불편합니다. 실수형을 덧셈을 해야 하는데 실수로 sum() 메서드를 호출할 수도 있고 그 반대가 될 수도 있습니다. 그래서 나온 개념이 오버로딩 입니다. 오버로딩은 메서드의 이름은 똑같이 하되, 파라미터를 다르게 설정해서 다른 기능을 하게 도와주는 문법입니다. 오버로딩을 활용하면 이제 사용자는 실수형이든 정수형이든 sum() 메서드를 호출하면 덧셈기능을 사용할 수 있게되었습니다.

좀 더 확장해서 생각해봅시다. GUI 환경에서는 모든 데이터가 문자열 타입으로 이루어집니다. 그래서 덧셈계산을 하고자 할때, 문자열로 숫자 데이터가 입력되어도, 산술 덧셈결과가 나오도록 하고싶습니다. 그러면 앞서 만들었던 Calculator 클래스에 메소드를 오버로딩해서 아래와 같이 작성해주면 됩니다.

int sum(String x, String y) {
	return Integer.parseInt(x) + Integer.parseInt(y);
}

가변인자

. . . 연산자 : 메소드를 호출할 때 파라미터의 개수가 가변적으로 사용하고 싶을 때!


예를 들어 전달받은 값의 누적된 덧셈결과를 계산하는 기능을 만들고싶다고 해봅시다. 즉, 2개의 값이 전달되면 2개 , 3개의 값이 전달되면 3개의 합을 구하고싶습니다. 어떻게 만들 수 있을까요? 파라미터의 개수가 변할 수 있을때 사용하는 특별한 매개변수 연산자 ... 를 이용하면 됩니다. 다음 코드를 봅시다.

void add(int... ns) {
	System.out.println("전달받은 데이터 개수 : " + ns.length);
	for(int t: ns) {
		System.out.print(t + " ");
			
	}
	System.out.println();
}

가변인자는 내부적으로는 배열을 생성합니다. 하지만 가변인자가 배열인것은 아닙니다. 그래서 파라미터로 값을 전달할 때는 , 연산자를 이용해 데이터 값을 넘겨줍니다. 그러면 내부적으로 값들을 배열에 저장해서 연산처리를 하게 됩니다.

profile
Developer

0개의 댓글