2025. 1. 6. 20:49ㆍ개발 회고/TIL
😊오늘 배운 내용
자료구조(컬렉션 - 리스트,스택,Set,Map), 객체와 클래스, 객체 생성 방법, 필드와 메서드, 오버로딩, 메서드에서 기본형변수와 참조형 변수, 인스턴스멤버와 클래스멤버, 지역변수, final, 생성자(기본생성자, 필드초기화, 생성자오버로딩), this와 this(), 접근제어자와 getter, setter, 패키지와 import, 상속, 오버라이딩
[어떤 문제가 있었는지 + 어떻게 해결하였는지]
1.먼저 오늘 공부한 개념들을 정리해보았다.
[ 자료구조(컬렉션 - 리스트,스택,Set,Map)]
컬렉션할 선언할 때 꼭 임포트 해주어야 함.
//list
//순서가 있는 데이터의 집합 (중복허용)
//배열과 다른 점 -> 최초길이를 몰라도 만들 수 있음. 가변적임
//Array -> 정적배열
//List(==ArrayList) -> 동적배열(크기가 가변적으로 늘어남)
//생성 시점에 작은 연속된 공간을 요청해서 참조형 변수들을 담아놓는다
//더 큰 공간이 필요하면 더 큰 공간을 받아서 저장함
//꺽쇠안에는 래퍼클래스가 들어가아함
ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1);
intList.add(2);
intList.add(3);
System.out.println(intList.get(2));
//2번째 인덱스에 있는 값을 15로 바꿈
intList.set(2,15);
intList.remove(2);
//list 전체 값 삭제
intList.clear();
//list 값 확인
System.out.println(intList.toString());
//링크드 리스트
//메모리에 남는 공간을 요청해서 여기저기 담아놓는다
//그래서 실제값이 있는 주소값으로 목록을 구성하고 저장하는 자료구조
//기본적 기능은 ArrayList와 동일
//링크드 리스트는 값을 여기저기 나누어 담아놓은 것을 화살표로 이어놓았기 때문에 값을 조회하는 속도가
//인덱스 방식보다 느림
//대신에 리스트 중간에 값을 추가하거나 삭제할 때 용이함
LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println(linkedList.get(0));
System.out.println(linkedList.toString());
//2번 인덱스에 4라는 값을 추가
linkedList.add(2,4);
System.out.println(linkedList.toString());
linkedList.set(0,30);
System.out.println(linkedList.toString());
linkedList.remove(2);
System.out.println(linkedList.toString());
linkedList.clear();
System.out.println(linkedList.toString());
//stack
//push, peek, pop
//최근 저장된 데이터를 나열하고 싶거나, 데이터의 중복처리를 막고 싶을 때
//선언 및 생성
Stack<Integer> intStack = new Stack<>();
intStack.push(10);
intStack.push(15);
intStack.push(1);
//다 지워질떄까지 출력
//isEmpty()는 비워져있니? 라는 뜻 -> 값이 있으면 false 없으면 true
while(!intStack.isEmpty()) {
System.out.println(intStack.pop());
}
//다시 추가
intStack.push(10);
intStack.push(15);
intStack.push(1);
//peek
//값을 꺼내지않고 가장 위에 있는 element를 반환하는 함수
System.out.println(intStack.peek()); //1
System.out.println(intStack.size());
//Queue
//add, peek, poll
//큐는 생성자가 없는 인터페이스
//선언 및 생성
Queue<Integer> intQueue = new LinkedList<Integer>();
intQueue.add(10);
intQueue.add(20);
intQueue.add(30);
while (!intQueue.isEmpty()) {
System.out.println(intQueue.poll());
}
//다시추가
intQueue.add(10);
intQueue.add(20);
intQueue.add(30);
//peek
System.out.println(intQueue.peek());
System.out.println(intQueue.size());
//set
//집합과 닮음
//순서가 없고 중복 없음!
//순서가 없는 대신 중복을 허용하지 않는 프로그램에서 사용하는 자료구조
//Set은 HashSet, TreeSet 등으로 응용해서 사용가능
//Set은 생성자가 없는 껍데기라서 바로 생성할 수 없음
//생성자가 존재하는 HashSet을 이용해서 Set 구현 가능
//선언 및 생성
HashSet<Integer> intHashSet = new HashSet<>();
intHashSet.add(10);
intHashSet.add(8);
intHashSet.add(24);
intHashSet.add(9);
intHashSet.add(24);
intHashSet.add(8);
for(Integer val : intHashSet) {
System.out.println(val);
}
//contains
System.out.println(intHashSet.contains(10)); //true
System.out.println(intHashSet.contains(7)); //false
//Map
//키 밸류 쌍으로 구성되어 있음
//key라는 값으로 unique하게 보장되어야 함!
//HashMap , TreeMap으로 응용해서 사용 가능
//선언 및 생성
Map<String, Integer> intMap = new HashMap<>();
intMap.put("일", 1);
intMap.put("이", 2);
intMap.put("삼", 3);
intMap.put("사", 4);
intMap.put("일", 5);
intMap.put("일", 6);
//key값 전체 출력
//keySet() -> Map에서 key만 빼서 배열로 만드는 함수
for(String key: intMap.keySet()) {
System.out.println(key);
}
//value값 전체 출력
//values() -> Map에서 value만 빼서 배열로 만드는 함수
for(Integer val: intMap.values()) {
System.out.println(val);
}
//key를 가지고 value를 찾아오는 방법
System.out.println(intMap.get("삼")); //3
[객체와 클래스]
Car 클래스
public class Car {
//필드
//고유데이터
String company;
String model = "GV80";
String color;
double price;
//상태데이터
double speed;
char gear;
boolean lights = true;
//객체데이터(부품)
Tire tire = new Tire();
Door door;
Handle handle;
//생성자
//처음 객체가 생성될 때 어떤 로직 수행, 어떤 값이 필수인지 강제하는 것
public Car() {
//아무 것도 없는 상태 -> 기본생성자 (디폴트)
System.out.println("생성자가 호출되었습니다. 객체가 생성됩니다.");
}
//메서드
//input : kmh
//output : speed
double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
};
//input : 없음
//output : speed
double brakePedal() {
speed = 0;
return speed;
};
//input : gear (gear type)
//output : gear
char changeGear(char type) {
gear = type;
return gear;
};
//input : 없음
//output : lights
boolean onOffLight() {
lights = !lights;
return lights;
};
//input : 없음
//output : 없음
void horn() {
System.out.println("빵애에요");
};
//자동차의 속도
//가변길이 매개변수
void carSpeeds(double ... speeds) {
for(double v : speeds) {
System.out.println("v = " + v);
}
}
public static void main(String[] args) {
}
}
Main 클래스
//객체생성
//new 키워드 이용
Car car1 = new Car();
Car car2 = new Car();
//car1이 가지고 있는 주소
System.out.println(car1); //week03.Car@b4c966a
//배열 생성
Car[] carArray = new Car[3];
car1.changeGear('P');
carArray[0] = car1;
car2.changeGear('N');
carArray[1] = car2;
Car car3 = new Car();
car3.changeGear('D');
carArray[2] = car3;
for(Car car:carArray) {
System.out.println("car.gear = " + car.gear);
}
[객체지향프로그래밍]
- 캡슐화
- 캡슐화란 속성(필드)와 행위(메서드)를 하나로 묶어 객체로 만든 후 실제 내부 구현 내용은 외부에서 알 수 없게 감추는 것을 의미합니다.
- 외부 객체에서는 캡슐화된 객체의 내부 구조를 알 수 없기 때문에 노출시켜 준 필드 혹은 메서드를 통해 접근할 수 있습니다.
- 필드와 메서드를 캡슐화하여 숨기는 이유는 외부 객체에서 해당 필드와 메서드를 잘못 사용하여 객체가 변화하지 않게 하는 데 있습니다.
- Java에서는 캡슐화된 객체의 필드와 메서드를 노출시킬지 감출지 결정하기 위해 접근 제어자를 사용합니다.
- 상속
- 객체지향 프로그래밍에는 부모 객체와 자식 객체가 존재합니다.
- 부모 객체는 가지고 있는 필드와 메서드를 자식 객체에 물려주어 자식 객체가 이를 사용할 수 있도록 만들 수 있습니다.
- 위 같은 행위를 상속이라고 할 수 있는데 상속을 하는 이유는 다음과 같습니다.
- 각각의 객체들을 상속 관계로 묶음으로써 객체 간의 구조를 파악하기 쉬워집니다.
- 필드와 메서드를 변경하는 경우 부모 객체에 있는 것만 수정하게 되면 자식 객체 전부 반영이 되기 때문에 일관성을 유지하기 좋습니다.
- 자식 객체가 부모 객체의 필드와 메서드를 물려받아 사용할 수 있기 때문에 코드의 중복이 줄어들며 코드의 재사용성이 증가됩니다.
- 다형성
- 객체가 연산을 수행할 때 하나의 행위에 대해 각 객체가 가지고 있는 고유한 특성에 따라 다른 여러 가지 형태로 재구성되는 것을 의미합니다.
- Car 클래스를 토대로 자동차 객체를 만들 때 A자동차 객체와 B자동차 객체의 경적 소리가 다르다면 ‘경적을 울리다’라는 행위 즉, horn(); 메서드의 구현을 다르게 재정의 하여 사용할 수 있습니다.
- 추상화
- 객체에서 공통된 부분들을 모아 상위 개념으로 새롭게 선언하는 것을 추상화라고 합니다.
- 공통적이고 중요한 것들을 모아 객체를 모델링 합니다.
- 현실 세계의 여러 종류의 자동차들이 공통적으로 가지고 있는 가속, 브레이크, 속도와 같은 것들을 모아 자동차라는 객체를 모델링 할 수 있습니다.
[클래스 생성]
- 만들려고 하는 설계도를 선언합니다.(클래스 선언)
- 객체가 가지고 있어야 할 속성(필드)을 정의합니다.
- 객체를 생성하는 방식을 정의합니다.(생성자)
- 객체가 가지고 있어야 할 행위(메서드)를 정의합니다.
[객체 생성]
객체 생성 연산자인 ‘new’를 사용하면 클래스로부터 객체를 생성할 수 있다.
//car1이 가지고 있는 주소
System.out.println(car1); //week03.Car@b4c966a
new 연산자를 통해서 객체가 생성되면 해당 인스턴스의 주소가 반환되기 때문에 해당 클래스의 참조형 변수를 사용하여 받아줄 수 있다.
[객체 배열]
객체는 참조형 변수와 동일하게 취급되기 때문에 배열 또는 컬렉션에도 저장하여 관리할 수 있다.
//배열 생성
Car[] carArray = new Car[3];
car1.changeGear('P');
carArray[0] = car1;
car2.changeGear('N');
carArray[1] = car2;
Car car3 = new Car();
car3.changeGear('D');
carArray[2] = car3;
for(Car car:carArray) {
System.out.println("car.gear = " + car.gear);
}
[필드]
필드는 객체의 데이터를 저장하는 역할을 합니다. 객체의 필드는 크게 고유한 데이터, 상태 데이터, 객체 데이터로 분류할 수 있습니다.

우리가 정의하여 선언한 클래스의 필드들은 기본적으로 초기값을 제공하지 않을 경우 객체가 생성될 때 자동으로 기본값으로 초기화됩니다.
‘필드를 사용한다’라는 의미는 필드의 값을 변경하거나 읽는 것을 의미합니다.
- 우리가 클래스에 필드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
- 클래스는 설계도일 뿐 실제로 필드의 데이터를 가지고 있는 것은 객체입니다.
- 따라서 객체를 생성한 후에 필드를 사용할 수 있습니다.
[메서드]
메서드는 객체의 행위를 뜻하며 객체 간의 협력을 위해 사용됩니다.
가변 길이의 매개변수도 선언할 수 있습니다.
void carSpeeds(double ... speeds) {
for (double v : speeds) {
System.out.println("v = " + v);
}
}
double … speeds 이렇게 … 을 사용하면 아래처럼 매개값을 , 로 구분하여 개수 상관없이 전달 가능합니다.
carSpeeds(100, 80);
carSpeeds(110, 120, 150);
‘메서드를 호출한다’라는 의미는 메서드의 블록 내부에 작성된 코드를 실행한다는 의미입니다.
- 필드와 마찬가지로 클래스의 메서드를 정의하여 선언했다고 해서 바로 사용할 수 있는 것은 아닙니다.
- 클래스는 설계도일 뿐 메서드는 객체의 행위를 정의한 것입니다.
- 따라서 객체를 생성한 후에 메서드를 사용할 수 있습니다.
외부 접근 -> 도트연산자 사용 ex) car.brakePedal();
내부 접근 ->
double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
[오버로딩]
오버로딩 은 함수가 하나의 기능만을 구현하는 것이 아니라 하나의 메서드 이름으로 여러 기능을 구현하도록 하는 Java의 기능입니다.
오버로딩의 조건
- 메서드의 이름이 같고, 매개변수의 개수, 타입, 순서가 달라야 합니다.
- '응답 값만' 다른 것은 오버로딩을 할 수 없습니다.
- 접근 제어자만 다른 것도 오버로딩을 할 수 없습니다.
- 결론, 오버로딩은 매개변수의 차이로만 구현할 수 있습니다.
[메서드에서 기본형 변수와 참조형 변수]
- 기본형 매개변수
- 메서드를 호출할 때 전달할 매개값으로 지정한 값을 메서드의 매개변수에 복사해서 전달합니다.
- 참조형 매개변수
- 메서드를 호출할 때 전달할 매개값으로 지정한 값의 주소를 매개변수에 복사해서 전달합니다.
- 매개변수를 참조형으로 선언하면 값이 저장된 곳의 원본 주소를 알 수 있기 때문에 값을 읽어 오는 것은 물론 값을 변경하는 것도 가능합니다.
- 메서드의 매개변수뿐만 아니라 반환 타입도 참조형이 될 수 있습니다.
//Car 클래스의 메서드
double brakePedal(char type) {
speed = 0;
type = 'P'; // 정지 후 매개변수 type을 어떤 타입으로 전달 받았는지 상관없이 'P'로 고정시키기
changeGear(type);
return speed;
}
Car car = new Car(); // 객체 생성
// 기본형 매개변수
char type = 'D';
car.brakePedal(type);
// 메서드 실행 완료 후 전달할 매개값으로 지정된 type 값 확인
System.out.println("type = " + type); // 기존에 선언한 값 'D' 출력, 원본 값 변경되지 않음
// 메서드 실행 완료 후 반환된 car 인스턴스의 gear 타입 확인
System.out.println("gear = " + car.gear); // 객체 내부에서 type을 변경하여 수정했기 때문에 'P' 출력
//Tire 클래스
public class Tire {
String company; // 타이어 회사
public Tire() {}
}
//Car 클래스의 setTire 메서드
Tire setTire(Tire tireCompany) {
tireCompany.company = "KIA"; // 금호 타이어를 전달 받았지만 강제로 KIA 타이어로 교체
tire = tireCompany;
return tire;
}
// 참조형 매개변수
Tire tire = new Tire();
tire.company = "금호"; // 금호 타이어 객체 생성
// 차 객체의 타이어를 등록하는 메서드 호출한 후 반환값으로 차 객체의 타이어 객체 반환
Tire carInstanceTire = car.setTire(tire);
// 메서드 실행 완료 후 전달할 매개값으로 지정된 참조형 변수 tire의 company 값 확인
System.out.println("tire.company = " + tire.company); // "KIA" 출력
// 전달할 매개값으로 지정된 tire 인스턴스의 주소값이 전달되었기 때문에 호출된 메서드에 의해 값이 변경됨.
// 메서드 실행 완료 후 반환된 car 인스턴스의 tire 객체 값이 반환되어 저장된 참조형 변수 carInstanceTire의 company 값 확인
System.out.println("carInstanceTire.company = " + carInstanceTire.company); // "KIA" 출력
company가 금호인 tire이지만 setTire 함수에 의해서 KIA로 변경되었다.
[인스턴스 멤버와 클래스 멤버]
멤버 = 필드 + 메서드
인스턴스 멤버 = 인스턴스 필드 + 인스턴스 메서드
클래스 멤버 = 클래스 필드 + 클래스 메서드
인스턴스 멤버는 객체 생성 후에 사용할 수 있고 클래스 멤버는 객체 생성 없이도 사용할 수 있습니다.
지금까지 학습하면서 선언한 필드와 메서드는 전부 인스턴스 멤버였습니다.
- 인스턴스 멤버는 객체를 생성해야 사용할 수 있다고 했습니다.
- 또한 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있습니다.
- 그렇다면 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 될까요?
- 그렇지 않습니다. 매번 저장한다면 중복 저장으로 인해 메모리 효율이 매우 떨어지기 때문에 메서드는 메서드 영역에 두고서 모든 인스턴스들이 공유해서 사용합니다.
- 대신 무조건 객체를 생성 즉, 인스턴스를 통해서만 메서드가 사용될 수 있도록 제한을 걸어둔 것입니다.
클래스는 Java의 클래스 로더에 의해 메서드 영역에 저장되고 사용됩니다.
- 이때 클래스 멤버란 메서드 영역의 클래스와 같은 위치에 고정적으로 위치하고 있는 멤버를 의미합니다.
- 따라서 클래스 멤버는 객체의 생성 필요 없이 바로 사용이 가능합니다.
클래스 멤버 선언
- 필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 됩니다.
- 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언하는 것이 좋습니다.
- 또한 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋습니다.
- 클래스 멤버로 선언된 메서드는 인스턴스 멤버를 사용할 수 없습니다.
- 반대로 인스턴스 멤버로 선언된 메서드는 클래스 멤버를 사용할 수 있습니다.
- 클래스 멤버는 객체 생성 없이 바로 사용 가능하기 때문에 객체가 생성되어야 존재할 수 있는 인스턴스 멤버를 사용할 수 없습니다.
static String company = "GENESIS"; // 자동차 회사 : GENESIS
String getCompany() {
return "(주)" + company;
}
모든 Car 클래스의 객체마다 company 인스턴스 필드를 가지고 있을 필요 없이 클래스 필드로 만들어 공유하게 만든다면 메모리를 효율적으로 사용할 수 있습니다.
static String setCompany(String companyName) {
// System.out.println("자동차 모델 확인: " + model); // 인스턴스 사용 불가
company = companyName;
return company;
}
Car.company = "Audi";
String companyName = Car.setCompany("Benz");
클래스 멤버를 사용하려면 클래스의 이름과 함께 도트(.) 연산자를 사용한다.
public class Main {
public static void main(String[] args) {
// 클래스 필드 company 확인
System.out.println(Car.company + "\n");
// 클래스 필드 변경 및 확인
Car.company = "Audi";
System.out.println(Car.company + "\n");
// 클래스 메서드 호출
String companyName = Car.setCompany("Benz");
System.out.println("companyName = " + companyName);
System.out.println();
// 참조형 변수 사용
Car car = new Car(); // 객체 생성
car.company = "Ferrari";
System.out.println(car.company + "\n");
String companyName2 = car.setCompany("Lamborghini");
System.out.println("companyName2 = " + companyName2);
}
}
[지역변수]
public class Main {
public int getNumber() {
//지역변수
//해당 메서드가 실행될따마다 독립적인 값을 저장하고 관리
//이 지역 변수는 메서드 내부에서 정의될 때 생성된다.
//그리고 이 메서드가 종료될 때 소멸된다.
int number = 1;
number += 1;
return number;
}
public static void main(String[] args) {
Main main = new Main();
System.out.println(main.getNumber()); //2
System.out.println(main.getNumber()); //2
}
}
[final]
final은 ‘최종적’ 이라는 의미입니다.
- final 필드는 초기값이 저장되면 해당값을 프로그램이 실행하는 도중에는 절대로 수정할 수 없습니다.
- 또한 final 필드는 반드시 초기값을 지정해야 합니다.
final String company = "GENESIS";
[상수]
상수의 특징은 값이 반드시 한 개이며 불변의 값을 의미합니다.
따라서 인스턴스마다 상수를 저장할 필요가 없습니다.
static final String COMPANY = "GENESIS"; //static 키워드 추가
일반적으로 모두 대문자로 적는 것이 관례이다.
[생성자]
기본 생성자는 선언할 때 괄호( ) 안에 아무것도 넣지 않는 생성자를 의미합니다.
- 모든 클래스는 반드시 생성자가 하나 이상 존재합니다.
- 만약 클래스에 생성자를 하나도 선언하지 않았다면 컴파일러는 기본 생성자를 바이트코드 파일에 자동으로 추가시켜줍니다. 따라서 이러한 경우는 기본 생성자 생략이 가능합니다.
- 반대로 단 하나라도 생성자가 선언되어 있다면 컴파일러는 기본 생성자를 추가하지 않습니다.
생성자는 객체를 초기화하는 역할을 수행합니다.
- 객체를 만들 때 인스턴스마다 다른 값을 가져야 한다면 생성자를 통해서 필드를 초기화할 수 있습니다.
- 예를 들어 만들어지는 자동차마다 모델, 색상, 가격이 다르다면 생성자를 사용하여 필드의 값을 초기화하는 것이 좋습니다.
- 반대로 인스턴스마다 동일한 데이터를 가지는 필드는 초기값을 대입하는 것이 좋습니다.
- 예를 들어 자동차가 만들어질 때마다 기어의 상태를 ‘P’로 고정해야 한다면 초기값을 직접 대입하는 것이 좋습니다.
생성자를 통해 필드를 초기화할 때 오버로딩을 적용할 수 있습니다.
public Car(String modelName) {
model = modelName;
}
public Car(String modelName, String colorName) {
model = modelName;
color = colorName;
}
public Car(String modelName, String colorName, double priceValue) {
model = modelName;
color = colorName;
price = priceValue;
}
[this와 this()]
this는 객체 즉, 인스턴스 자신을 표현하는 키워드입니다.
- 객체 내부 생성자 및 메서드에서 객체 내부 멤버에 접근하기 위해 사용될 수 있습니다.
- 객체 내부 멤버에 접근할 때 this 키워드가 필수는 아니지만 상황에 따라 필수가 될 수 있습니다.
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
또한 this는 인스턴스 자신을 뜻하기 때문에 객체의 메서드에서 리턴 타입이 인스턴스 자신의 클래스 타입이라면 this를 사용하여 인스턴스 자신의 주소를 반환할 수도 있습니다.
Car returnInstance() {
return this;
}
this(…)는 객체 즉, 인스턴스 자신의 생성자를 호출하는 키워드입니다.
public Car(String model) {
this.model = model;
this.color = "Blue";
this.price = 50000000;
}
public Car(String model, String color) {
this.model = model;
this.color = color;
this.price = 50000000;
}
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
이렇게 생성자를 선언한다고 했을 때 코드의 중복이 발생합니다.
this()를 이용해서 코드 중복을 처리할 수 있습니다.
public Car(String model) {
this(model, "Blue", 50000000);
}
public Car(String model, String color) {
this(model, color, 100000000);
}
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
[접근 제어자]
멤버 또는 클래스에 사용, 외부에서 접근하지 못하도록 제한합니다.
클래스, 멤버 변수, 메서드, 생성자에 사용되고, 지정되어 있지 않다면 default입니다.
- public : 접근 제한이 전혀 없습니다.
- protected : 같은 패키지 내에서, 다른 패키지의 자손 클래스에서 접근이 가능합니다
- default : 같은 패키지 내에서만 접근이 가능합니다.
- private : 같은 클래스 내에서만 접근이 가능합니다.
생성자의 접근 제어자
- 생성자에 접근 제어자를 사용함으로 인스턴스의 생성을 제한할 수 있습니다.
- 일반적으로 생성자의 접근 제어자는 클래스의 접근 제어자와 일치합니다.
[getter 와 setter]
객체의 무결성 즉, 변경이 없는 상태를 유지하기 위해 접근 제어자를 사용합니다.
- 이때 외부에서 필드에 직접 접근하는 것을 막기 위해 필드에 private, default 등의 접근 제어자를 사용할 수 있습니다.
- 그렇다면 우리는 어떻게 객체의 private 필드를 읽어오거나 저장할 수 있을까요?
- 우리는 Getter 와 Setter를 사용하여 이를 해결할 수 있습니다.
외부에서 객체의 private 한 필드를 읽을 필요가 있을 때 Getter 메서드를 사용합니다.
public String getModel() {
return model;
}
public String getColor() {
return color;
}
public double getPrice() {
return price;
}
외부에서 객체의 private 한 필드를 저장/수정할 필요가 있을 때 Setter 메서드를 사용합니다.
자동차 클래스의 필드에 이처럼 private 접근 제어자로 지정한 필드가 있을 때 Setter 메서드를 통해 값을 저장하거나 수정할 수 있습니다.
public void setModel(String model) {
this.model = model;
}
public void setColor(String color) {
this.color = color;
}
public void setPrice(double price) {
this.price = price;
}
[패키지와 import]
패키지 == 폴더
package oop.pk1;
public class Car {
public void horn() {
System.out.println("pk1 빵빵");
}
}
package oop.pk2;
public class Car {
public void horn() {
System.out.println("pk2 빵빵");
}
}
//import를 안할 시
package oop.main;
public class Main {
public static void main(String[] args) {
oop.pk1.Car car = new oop.pk1.Car();
car.horn(); // pk1 빵빵
oop.pk2.Car car2 = new oop.pk2.Car();
car2.horn(); // pk2 빵빵
}
}
//import 할 시
package oop.main;
import oop.pk1.Car;
public class Main {
public static void main(String[] args) {
Car car = new Car();
car.horn(); // pk1 빵빵
oop.pk2.Car car2 = new oop.pk2.Car();
car2.horn(); // pk2 빵빵
}
}
[상속]
클래스 간의 상속은 extends 키워드를 사용하여 정의할 수 있습니다.
- 우리는 상속의 개념을 확장의 개념으로 이해해야 합니다.
- 자칫하여 부모 클래스, 자식 클래스라는 용어에 현혹되어 ‘부모가 자식보다 큰 사람이니까 부모 클래스도 마찬가지로 자식 클래스 보다 큰 범위겠지?’라고 생각하는 순간 헷갈리기 시작합니다…

- 부모 클래스에 새로운 필드와 메서드가 추가되면 자식 클래스는 이를 상속받아 사용할 수 있다.
- 자식 클래스에 새로운 필드와 메서드가 추가되어도 부모 클래스는 어떠한 영향도 받지 않는다.
- 따라서 자식 클래스의 멤버 개수는 부모 클래스보다 항상 같거나 많다.
클래스 간의 관계를 분석하여 관계 설정을 해줄 수 있습니다.
- 상속관계 : is - a (”~은 ~(이)다”)
- 포함관계 : has - a (”~은 ~을(를) 가지고 있다”)
- 상속관계는 위에서 예제로 살펴본 스포츠카와 자동차도 있지만 한 가지 더 예를 들자면 고래와 포유류가 있습니다.
- 위 공식에 대입해 보겠습니다.
- 상속관계 : 고래는 포유류다 👍
- 포함관계 : 고래는 포유류를 가지고 있다…? 🤔
포함관계는 우리가 일전에 봤던 자동차와 타이어, 차 문, 핸들과의 관계라고 볼 수 있습니다.
- 한번 위 공식에 대입해 보겠습니다.
- 자동차는 타이어를 가지고 있다. 👍
- 자동차는 차 문을 가지고 있다. 👍
- 자동차는 핸들을 가지고 있다. 👍
[단일상속과 다중상속]
Java는 다중 상속을 허용하지 않습니다…
final 키워드를 클래스와 메서드에 선언하면 어떻게 될까요?
public final class Car {}
...
public class SportsCar extends Car{} // 오류가 발생합니다.
클래스에 final 키워드를 지정하여 선언하면 최종적인 클래스가 됨으로 더 이상 상속할 수 없는 클래스가 됩니다.
public class Car {
public final void horn() {
System.out.println("빵빵");
}
}
...
public class SportsCar extends Car{
public void horn() { // 오류가 발생합니다.
super.horn();
}
}
메서드에 final 키워드를 지정하여 선언하면 최종적인 메서드가 됨으로 더 이상 오버라이딩할 수 없는 메서드가 됩니다.
[Object]
Object는 말 그대로 “객체”를 의미하는 단어이며 보통, Object 클래스를 의미합니다.
- Object 클래스는 Java 내 모든 클래스들의 최상위 부모 클래스입니다.
- 따라서, 모든 클래스는 Object의 메서드를 사용할 수 있습니다.
- 또한 부모 클래스가 없는 자식 클래스는 컴파일러에 의해 자동으로 Object 클래스를 상속받게 됩니다.
Object 클래스의 메서드를 몇 가지 소개해 드리겠습니다.
- Object clone() : 해당 객체의 복제본을 생성하여 반환함.
- boolean equals(Object object) : 해당 객체와 전달받은 객체가 같은지 여부를 반환함.
- Class getClass() : 해당 객체의 클래스 타입을 반환함.
- int hashCode() : 자바에서 객체를 식별하는 정수값인 해시 코드를 반환함.
- String toString() : 해당 객체의 정보를 문자열로 반환함. & Object 클래스에서는 클래스이름 @해쉬코드값 리턴함.
- …
[오버라이딩]
부모 클래스로부터 상속받은 메서드의 내용을 재정의 하는 것을 오버라이딩이라고 합니다.
- 부모 클래스의 메서드를 그대로 사용 가능하지만 자식 클래스의 상황에 맞게 변경을 해야 하는 경우 오버라이딩을 사용합니다.
- 오버라이딩을 하기 위해서는 아래 조건들을 만족해야 합니다.
- 선언부가 부모 클래스의 메서드와 일치해야 합니다.
- 접근 제어자를 부모 클래스의 메서드 보다 좁은 범위로 변경할 수 없습니다.
- 예외는 부모 클래스의 메서드 보다 많이 선언할 수 없습니다.
//부모클래스 Car 클래스
public class Car {
String company; // 자동차 회사
private String model; // 자동차 모델
private String color; // 자동차 색상
private double price; // 자동차 가격
double speed; // 자동차 속도 , km/h
char gear = 'P'; // 기어의 상태, P,R,N,D
boolean lights; // 자동차 조명의 상태
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public double gasPedal(double kmh, char type) {
changeGear(type);
speed = kmh;
return speed;
}
public double brakePedal() {
speed = 0;
return speed;
}
public char changeGear(char type) {
gear = type;
return gear;
}
public boolean onOffLights() {
lights = !lights;
return lights;
}
public void horn() {
System.out.println("빵빵");
}
}
//자식 클래스 SportsCar 클래스
public class SportsCar extends Car{
String engine;
public void booster() {
System.out.println("엔진 " + engine + " 부앙~\n");
}
public SportsCar(String engine) {
this.engine = engine;
}
@Override
public double brakePedal() {
speed = 100;
System.out.println("스포츠카에 브레이크란 없다");
return speed;
}
@Override
public void horn() {
booster();
}
}
public class Main {
public static void main(String[] args) {
// 부모 클래스 자동차 객체 생성
Car car = new Car();
car.horn(); // 경적, 부모클래스의 horn()호출
System.out.println();
// 자식 클래스 스포츠카 객체 생성
SportsCar sportsCar = new SportsCar("Orion");
// 오버라이딩한 brakePedal(), horn() 메서드 호출
sportsCar.brakePedal(); //부모클래스의 brakePedal()호출
sportsCar.horn(); //자식클래스에서 오버라이딩한 horn()호출
}
}
[super와 super()]
super는 부모 클래스의 멤버를 참조할 수 있는 키워드입니다.
// 부모 클래스 Car
String model; // 자동차 모델
String color; // 자동차 색상
double price; // 자동차 가격
// 자식 클래스 SportsCar
String model = "Ferrari"; // 자동차 모델
String color = "Red"; // 자동차 색상
double price = 300000000; // 자동차 가격
public void setCarInfo(String model, String color, double price) {
super.model = model; // model은 부모 필드에 set
super.color = color; // color는 부모 필드에 set
this.price = price; // price는 자식 필드에 set
}
super(…)는 부모 클래스의 생성자를 호출할 수 있는 키워드입니다.
- 객체 내부 생성자 및 메서드에서 해당 객체의 부모 클래스의 생성자를 호출하기 위해 사용될 수 있습니다.
- 자식 클래스의 객체가 생성될 때 부모 클래스들이 모두 합쳐져서 하나의 인스턴스가 생성됩니다.
- 이때 부모 클래스의 멤버들의 초기화 작업이 먼저 수행이 되어야 합니다.
- 따라서 자식 클래스의 생성자에서는 부모 클래스의 생성자가 호출됩니다.
- 또한 부모 클래스의 생성자는 가장 첫 줄에서 호출이 되어야 합니다.
// 부모 클래스 Car 생성자
public Car(String model, String color, double price) {
this.model = model;
this.color = color;
this.price = price;
}
// 자식 클래스 SportsCar 생성자
public SportsCar(String model, String color, double price, String engine) {
// this.engine = engine; // 오류 발생
super(model, color, price);
this.engine = engine;
}
- 자식 클래스 객체를 생성할 때 생성자 매개변수에 매개값을 받아와 super(…)를 사용해 부모 생성자의 매개변수에 매개값을 전달하여 호출하면서 부모 클래스의 멤버를 먼저 초기화합니다.
- 오버로딩된 부모 클래스의 생성자가 없다고 하더라도 부모 클래스의 기본 생성자를 호출해야 합니다.
- 따라서 눈에 보이지는 않지만 컴파일러가 super();를 자식 클래스 생성자 첫 줄에 자동으로 추가해 줍니다.
2. 계산기 과제 트러블 슈팅
문제상황
App.java
Calculator2 doCalculate = new Calculator2();
doCalculate.calculate(firstNumber, secondNumber, operator);
Calculator2.java
private ArrayList<Integer> resultArray = new ArrayList<Integer>();
App에서 매번 계산기 객체를 생성해서 리스트에 저장하고 있었다. 그래서 리스트에 값이 쌓이지 않고 서로 객체마다 각자의 리스트에 각자 저장하고 있었다.
나는 결과 값을 담을 리스트를 인스턴스 멤버로 저장하고 있었던 셈이다.
<인스턴스 멤버>
- 인스턴스 멤버는 객체를 생성해야 사용할 수 있다고 했습니다.
- 또한 객체의 인스턴스 필드는 각각의 인스턴스마다 고유하게 값을 가질 수 있습니다.
- 그렇다면 객체가 인스턴스화할 때마다 객체의 메서드들은 인스턴스에 포함되어 매번 생성이 될까요?
- 그렇지 않습니다. 매번 저장한다면 중복 저장으로 인해 메모리 효율이 매우 떨어지기 때문에 메서드는 메서드 영역에 두고서 모든 인스턴스들이 공유해서 사용합니다.
- 대신 무조건 객체를 생성 즉, 인스턴스를 통해서만 메서드가 사용될 수 있도록 제한을 걸어둔 것입니다.
<클래스 멤버>
클래스는 Java의 클래스 로더에 의해 메서드 영역에 저장되고 사용됩니다.
- 이때 클래스 멤버란 메서드 영역의 클래스와 같은 위치에 고정적으로 위치하고 있는 멤버를 의미합니다.
- 따라서 클래스 멤버는 객체의 생성 필요 없이 바로 사용이 가능합니다.
클래스 멤버 선언
- 필드와 메서드를 클래스 멤버로 만들기 위해서는 static 키워드를 사용하면 됩니다.
- 일반적으로 인스턴스마다 모두 가지고 있을 필요 없는 공용적인 데이터를 저장하는 필드는 클래스 멤버로 선언하는 것이 좋습니다.
- 또한 인스턴스 필드를 사용하지 않고 실행되는 메서드가 존재한다면 static 키워드를 사용하여 클래스 메서드로 선언하는 것이 좋습니다.
해결과정
따라서 리스트 자료구조를 다음과 같이 변경하였다.
static private ArrayList<Integer> resultArray = new ArrayList<Integer>();
그리고 다음과 같은 메서드를 이용해 리스트에 값이 쌓이는지 확인하였다.
public void fullGetter() {
System.out.println(resultArray.toString());
};
또한 결과 값 출력을 다음 메서드로 따로 구현하였다.
public void printResult() {
System.out.println("result is " + resultArray.get(resultArray.size()-1));
}
그래서 메서드를 다음과 같이 정리하고,
//method
public void calculate(int firstNumber, int secondNumber, String operator) {
Scanner sc = new Scanner(System.in);
if(operator.equals("+")){
this.result = firstNumber + secondNumber;
} else if (operator.equals("-")) {
this.result = firstNumber - secondNumber;
} else if (operator.equals("*")) {
this.result = firstNumber * secondNumber;
} else if (operator.equals("/")) {
if(secondNumber == 0){
System.out.println("Division by zero is impossible");
System.out.println("first number is: " + firstNumber);
System.out.println("rewrite the second number: ");
secondNumber = sc.nextInt();
}
this.result = firstNumber / secondNumber;
}
resultArray.add(this.result);
};
public int getter(int index) {
return resultArray.get(index);
};
public void setter(int index, int value) {
resultArray.set(index, value);
};
public void printResult() {
System.out.println("result is " + resultArray.get(resultArray.size()-1));
}
public void removeResult() {
resultArray.remove(resultArray.remove(0));
}
public void fullGetter() {
System.out.println(resultArray.toString());
};
public static void main(String[] args) {
}
메인 클래스에서 다음과 같이 사용하였다.
Calculator2 doCalculate = new Calculator2();
doCalculate.calculate(firstNumber, secondNumber, operator);
doCalculate.fullGetter();
doCalculate.printResult();
doCalculate.removeResult();

로그를 보면 fullGetter 함수를 이용해 현재 리스트에 어떻게 담겨있는지 확이하였고 printResult함수를 이용해 현재 리스트에서 가장 최신값을 출력하도록 하였다. 그 다음에는 removeResult를 이용해 0번째 인덱스에 있는 값을 삭제하도록 하였다. 왜냐하면 이 로직대로라면 항상 리스트에는 한개의 값만 담길 것이기 때문이다.(무조건 계산한번하면 리스트에 한번 담고 한번 삭제하기 때문) 사용자에게 값을 선택하여 삭제할 수 있도록 한다면,
리스트에 값이 계속 쌓이게 하여서 출력하는 함수를 fullGetter를 이용해 리스트에 담긴 값을 모두를 보여주게 한 다음 거기서 값을 선택해서 삭제할 수 있도록 구현할 수도 있다.
혹시 몰라 내가 클래스멤버와 static 키워드에 이해하고 있는 부분이 맞는지 질문 하였다.
각 계산기 객체가 생성될때마다 새로운 리스트를 생성하는게 아닌 공용적으로 데이터를 저장하기 위해서 static 키워드 사용 > 내가 이해한 static 개념이 맞는가? > 맞았다고 해주셨다.
[오늘 회고]
오늘 배운 부분이 중요한 부분이고 상당히 많은걸 배운것 같은데 그래도 전부 이해하고 넘어갈 수 있어서 다행이다. 특히 계산기 과제에서 클래스멤버 문제를 스스로 해결한 부분이 뿌듯했다.
오늘도 앉아서 공부하기 힘들었지만, 노력에는 지름길이 없다!
'개발 회고 > TIL' 카테고리의 다른 글
| [1/13] TIL - 콘솔 출력 형식 지정, 계산기 코드 개선, 앞으로의 공부 다짐 😊 (4) | 2025.01.13 |
|---|---|
| [1/7] TIL - 다형성, 상속, 추상클래스, 인터페이스, 형변환의 필요성 정리 (2) | 2025.01.07 |
| [1/2] TIL - 📚 오늘의 학습 정리: 연산자, 제어문, 배열, 콜렉션 자료구조, 객체와 생성자 (1) | 2025.01.02 |
| [12/31] TIL - Java 버퍼비우기, 동적배열 , 참조형 변수, 래퍼클래스 & Git branch 협업 (4) | 2024.12.31 |
| [12/30] TIL - 프로젝트 발표회에서 얻은 인사이트와 앞으로의 다짐 (4) | 2024.12.30 |