JAVA 기본편
JAVA는 버전이 매우 다양하다. JAVA 17을 통해서 주로 공부를 하긴 했지만 11, 8처럼 보다 예전 버전도 자주 쓰이고 있는 것 같다.
우선적으로 자바의 기본적인 특징
- JVM 기반의 객체지향(OOP)언어
- C/C++의 가장 큰 특징인 메모리 관리와 책임이슈를 구조적으로 제거 = GC(Garbage Collector) 자동으로 사용하지 않는 객체를 제거
- OS(나 Platform)에 대한 의존성이 없다 - JVM = C++ (OS, CPU), JAVA (OS, CPU, JVM)
- 컴파일러, 인터플리터 특징을 모두 가진다.
- 문법적으로 C/C++와 매우 유사하다.

객체와 클래스
public class Dog {
// 필드
public String name;
public int age;
//true면 대형, false면 소형
public boolean kind;
public int hungry=50;
// 생성자
public Dog(String name) {
this.name = name;
}
// 메서드 1
public void eat(String food) {
System.out.println(this.name+"이/가 "+food+" 을/를 먹습니다");
this.hungry = hungry+10;
if(hungry<=30) {
bark();
}
}
// 메서드 1-1 (메서드 오버로딩)
public void eat() {
System.out.println(this.name+"이/가 뭔가 을/를 먹습니다");
this.hungry = hungry+10;
if(hungry<=30) {
bark();
}
}
// 메서드 2
public void play() {
System.out.println(name+" 이/가 놀고있습니다");
this.hungry = hungry-20;
if(hungry<=30) {
bark();
}
}
// 메서드 3
public void walk() {
System.out.println(name+" 이/가 산책하고 있습니다");
this.hungry = hungry-10;
if(hungry<=30) {
bark();
}
}
// 메서드 4
public void bark() {
if(kind == true) {
System.out.println(this.name+"이가 월월 하고 짖습니다");
}else {
System.out.println(this.name+"이가 왈왈 하고 짖습니다");
}
}
}
public class DogExample {
public static void main(String[] args) {
// 메서드 호출
Dog dog1 = new Dog("구름");
dog1.walk();
dog1.play();
Dog dog2 = new Dog("두부");
dog2.walk();
dog2.eat("간식");
}
}
싱글톤
프로그램에서 단 하나의 객체만 만들도록 보장된 객체
package ch06.sec05.exam04;
public class Singleton {
// 정적 필드
private static Singleton singleton = new Singleton();
// 생성자
private Singleton() {}
// 정적 메서드
static Singleton getInstance() {
return singleton;
}
}
package ch06.sec05.exam04;
public class SingletonEx {
public static void main(String[] args) {
/*
* // 컴파일 에러
* Singleton st1 = new Singleton();
* Singleton st2 = new Singleton();
*/
Singleton st1 = Singleton.getInstance();
Singleton st2 = Singleton.getInstance();
if(st1 == st2) {
System.out.println("같은 Singleton 객체 입니다.");
} else {
System.out.println("다른 Singleton 객체 입니다.");
}
}
}
객체 지향 4대 특성
캡슐화 (Encapsulation)
캡슐화란 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것을 말한다.
아래 그림은 캡슐화의 기본 내용을 잘 표현하고 있습니다. 즉 서로 관련 있는 데이터와 이를 처리할 수 있는 기능들을 한곳에 모아 관리하는 것입니다. 자바 객체 지향 프로그래밍에서 이렇게 캡슐화를 하는 이유로 크게 두 가지를 언급할 수 있습니다.
- 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
- 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출

외부로부터 클래스에 정의된 속성과 기능들을 보호하고, 필요한부분만 외부로 노출될 수 있도록 하여 각 객체 고유의 독립성과 책임 영역을 안전하게 지키고자 하는 목적이 있다.
자바 객체 지향 프로그래밍에서 캡슐화를 구현하기 위한 방법은 크게 두 가지가 있다. 먼저는 접근제어자(access modifiers)를 활용하는 것이다.
접근제어자는 클래스 또는 클래스의 내부의 멤버들에 사용되어 해당 클래스나 멤버들을 외부에서 접근하지 못하도록 접근을 제한하는 역할을 한다.
접근제한자
접근제한자는 클래스와 클래스 변수, 메서드, 생성자 등의 접근을 제어할 수 있음
(클래스의 경우엔 public과 공백만 가능. 단,내부클래스는 4가지 전부 가능)
- public, private, protected와 접근제한자를 붙이지 않았을 경우에는 기본값인 package friendly

| 접근제한자 | 클래스 내부 | 패키지 내부 | 패키지 외부 & 하위 클래스 | 패키지 외부 |
|---|---|---|---|---|
| public | O | O | O | O |
| protected | O | O | O | X |
| default | O | O | X | X |
| private | O | X | X | X |
int one;
private int two;
private int three;
==> private을 붙인 변수 two와 three는 클래스 내부에서만 사용가능
==> 접근제한자를 생략한 변수 one은 pakage friendly로 클래스내부, 같은 패키지내부에서만 사용가능
getter/setter
두 번째 방법으로는 getter/setter 메서드가 있다.
public class Car {
private String model;
private String color;
private int wheels;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getColor() {
return model;
}
public void setColor(String color) {
this.color = color;
}
}
모든 속성값들이 private 접근 제어자로 선언, getter/setter 메서드의 접근제어자만 public. 선택적으로 외부에 접근을 허용할 속성과 그렇지 않을 속성을 설정가능하다.
public class Car {
private String model;
private String color;
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public void startEngine() {
System.out.println("시동을 겁니다.");
}
public void moveForward() {
System.out.println("자동차가 앞으로 전진합니다.");
}
public void openWindow() {
System.out.println("모든 창문을 개방합니다.");
}
}
public class Driver {
private String name;
private Car car;
public Driver(String name, Car car) {
this.name = name;
this.car = car;
}
public void drive() {
car.startEngine();
car.moveForward();
car.openWindow();
}
}
public class Main{
public static void main(String[] args) {
Car car = new Car("벤츠", "블랙");
Driver driver = new driver("홍길동", car);
driver.drive();
}
}
// 출력값
시동을 겁니다.
자동차가 앞으로 전진합니다.
모든 창문을 개방합니다.
위의 코드는 아무 문제없이 작동하고 있지만 drive() 메서드가 호출될 때 Car의 메서드들이 순차적으로 실행되고 있는 것을 알 수 있다.
만약에 Car 클래스의 3가지 메서드들에 어떤 변경이 생겼다고 가정해봅시다. 그러면 해당 메서드들을 사용하고 있는 Driver 클래스의 driver() 메서드의 수정이 불가피해진다. >> 객체 간의 결합도가 높은 상태
이걸 완화 시키는 방법을 아래에서 확인해보도록 하자.
public class Car {
private String model;
private String color;
private int wheels;
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public String getColor() {
return model;
}
public void setColor(String color) {
this.color = color;
}
}
모든 속성값들이 private 접근 제어자로 선언, getter/setter 메서드의 접근제어자만 public. 선택적으로 외부에 접근을 허용할 속성과 그렇지 않을 속성을 설정가능하다.
public class Car {
private String model;
private String color;
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public void startEngine() {
System.out.println("시동을 겁니다.");
}
public void moveForward() {
System.out.println("자동차가 앞으로 전진합니다.");
}
public void openWindow() {
System.out.println("모든 창문을 개방합니다.");
}
public void operate() { // 앞서 Driver 클래스에 정의된 메서드를 이동해 메서드 추출
startEngine();
moveForward();
openWindow();
}
}
public class Driver {
private String name;
private Car car;
public Driver(String name, Car car) {
this.name = name;
this.car = car;
}
public String getName() {
return name;
}
public void drive() {
car.operate();
}
}
public class Main{
public static void main(String[] args) {
Car car = new Car("벤츠", "블랙");
Driver driver = new driver("홍길동", car);
driver.drive();
}
}
// 출력값
시동을 겁니다.
자동차가 앞으로 전진합니다.
모든 창문을 개방합니다.
아까와 출력값은 동일하지만, 기존의 Driver 클래스가 하나하나 호출해줬던 메서드들을 모두 operate() 메서드로 묶어 Car 클래스로 옮겨두었고, Driver 클래스에서는 내부 동작을 전혀 신경쓰지 않아도 단순히 operate() 메서드를 호출하여 사용하고 있습니다.
또한, operate() 메서드 내부의 메서드들은 외부에서 호출되어 사용할 일이 없으므로 접근 제어자를 모두 private으로 변경해주었습니다. 정리하면, Car 클래스와 관련된 기능들은 온전히 Car 에서만 관리되도록 하였고, 불필요한 내부 동작의 노출을 최소화하였습니다. 이제 Driver 클래스의 입장에서는 더 이상 Car 클래스의 내부 로직을 알지 못하고, 알 필요도 없어졌습니다.
이렇게 캡슐화를 활용하면, 객체 내부의 동작의 외부로의 노출을 최소화하여 각 객체의 자율성을 높이고, 이를 통해 객체 간 결합도를 낮추어 앞서 설명한 객체 지향의 핵심적인 이점을 잘 살리는 방법으로 프로그램을 설계하는 일이 가능합니다.
상속 (Inheritance) ⭐
생성자, final 클래스는 상속 불가, 단일 상속만 가능 (다중 상속은 interface를 사용하면 가능)
extends 키워드 이용하여 상속을 표현한다. abstract 클래스는 반드시 상속해서 사용
부모클래스
public class Person {
public String name;
public int age;
public Person(String name, int age) {
super(); //object - 생략가능, 부모의 기본 생성자 호출
this.name = name;
this.age = age;
}
public String getDetails() {
return "이름: "+name+", 나이 : "+age;
}
}
자식클래스
public class Student extends Person {
public int studentId;
public String getDetails() {
return super.getDetails()+", 학번 : "+studentId;
}
public Student(String name, int age, int studentId) {
super(name,age); //Person
this.studentId = studentId;
}
}
※this와 super
| this는 현재 객체(자기 자신) | super는 현재 객체의 부모 객체, 한 단계 |
|---|---|
| * 변수 또는 메서드를 참조 | * 부모의 변수 또는 메서드를 참조 |
| * 자기 자신 클래스 내의 다른 생성자를 참조 | * 부모의 생성자를 참조 |
public class InheritanceExample {
public static void main(String[] args) {
Person p1 = new Person("송민지",24);
Student s1 = new Student("송민지",24,2017);
Teacher t1 = new Teacher("송민지",24,"JAVA");
Employee e1 = new Employee("송민지",24,2020);
System.out.println(p1.getDetails());
System.out.println(s1.getDetails());
System.out.println(t1.getDetails());
System.out.println(e1.getDetails());
//다형적 객체. 부모 = 자식
//자식은 부모에게 (물려받은 코드+자신의 코드)의 형태이기 때문에
//부모의 형태로 변환 가능 - 대신 명시적으로 부모 상태이기 때문에 부모의 것만,
//자신(자식)의 것은 사용 불가.
//이런 부모 = 자식 형태의 객체를 다형적 객체라 부름
Person p2 = s1;
//메서드 재정의(메서드 오버라이딩) - 부모에게 물려받은 메서드를 이름과 리턴타입, 매개변수 등의
//선언 형태는 그대로 and 내용만 바꾸는 형태
//BUT 그렇게만 실행된다면 다형성의 이유가 없어짐 - 부모의 것만 사용할 수 있다면
//전부 부모로 만들고 부모만 사용하면 될 것
//때문에 컴파일 시 부모의 메서드로 컴파일 하고 실행 시
//자식에 있는 재정의된 메서드로 호출 - 자식이면서 부모인 형태(다형성)를 가질 수 있음
System.out.println(p2.getDetails());
Person[] perArray = new Person[4];
//Person이 될 수 있는 객체들은 모두 가능
perArray[0] = p1;
perArray[1] = s1;
perArray[2] = t1;
perArray[3] = e1;
for(Person p : perArray) {
System.out.println(p.getDetails());
}
}
}
재정의 불가능한 final 메서드 & 클래스
Class
public final class Member {
}
public class Person extend Member { // 재정의 불가 클래스
}
Method
public class Car {
public int speed;
public void speedUp() {speed += 1;}
public final void stop() {
System.out.println("STOP");
speed = 0;
}
}
public class SportsCar extends Car {
@Override
public void speedUp() {speed += 10;}
// @Override // 재정의 불가 메서드
// public void stop() {
// System.out.println("스포츠카 정지");
// speed = 0;
// }
}
상속과 합성
| 상속(Inheritance) | 합성(Composition) |
|---|---|
| 부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결 | 두 객체 사이의 의존성은 런타임에 해결 |
| is-a 관계 | has-a 관계 |
| 부모클래스의 구현에 의존 결합도가 높음. | 구현에 의존하지 않음.내부에 포함되는 객체의 구현이 아닌 인터페이스에 의존. |
| 클래스 사이의 정적인 관계 | 객체 사이의 동적인 관계 |
| 부모 클래스 안에 구현된 코드 자체를 물려 받아 재사용 | 포함되는 객체의 퍼블릭 인터페이스를 재사용 |
-
Is a (상속관계) : 자식 클래스는 (하나의) 부모 클래스이다.
-
Has a (연관관계) : 한 클래스 멤버변수로 다른 클래스 타입의 참조변수를 선언한다. (객체 내 객체)
상속(Inheritance)이란?
클래스 상속을 통해 자식 클래스는 부모 클래스의 자원을 물려 받게 되며, 부모 클래스와 다른 부분만 추가하거나 재정의함으로써 기존 코드를 쉽게 확장할 수 있다.
그래서 상속 관계를 is-a 관계라도 표현하기도 한다.
class Mobile {
// ...
}
class Apple extends Mobile {
// ...
}
[ 상속을 통한 코드의 재사용 ]
'코드의 재사용' 이라는 단어가 머릿속에 그려지지 않아 정확히 어떤 것을 말하는지 모를수 있을텐데 이렇게 생각해보면 된다.
애초에 우리가 함수(function)을 만들어 쓰는 이유가 공통적으로 사용되는 코드를 묶어 재사용을 통해 코드 중복을 줄이기 위해서이다.
이런 관점에서, 객체 지향 프로그래밍에서 공통적으로 사용되는 코드가 있다면, 일일히 클래스마다 메서드를 만들어 사용하는게 아닌, 부모 클래스에 메서드 하나 만들어놓고 상속을 통해 부모의 것을 가져와 사용한다는 기법으로 코드의 재사용이라고 말하는 것이다.
다만, 엄밀히 말하면 상속은 그저 코드 재사용을 위한 기법이 아니다. 일반적인 클래스가 이미 구현이 되어 있는 상태에서 그보다 좀 더 구체적인 클래스를 구현하기 위해 사용되는 기법이며, 그로 인해 상위 클래스의 코드를 하위 클래스가 재사용 할 수 있을 뿐이다.
세부정보
예를들어 백화점의 고객을 클래스로 표현하려고 할때, 고객도 백화점 매출의 기여도에 따라 VIP, Gold, Silver 등급으로 나누어 각 등급마다 차별된 서비스(할인 쿠폰, 포인트)를 제공할 수 있다. 이러한 경우 Customer 라는 최상위 클래스를 만들고, 이를 각각 상속 받아 VIPCustomer, GoldCustomer 등으로 자식 클래스를 구현하게 된다.만일 고객 상속 구조에서 Cooper 등급 클래스를 새로이 추가한다고 하였을때, 다른 클래스를 건드리지 않고 그냥 상위 클래스 Customer를 상속(extends)만 하면 무리없이 구조화된 클래스를 만들 수 있게 된다. 이처럼 ‘고객’ 이라는 객체 주제는 같지만, 서로 다른 속성이나 기능들을 가지고 있을때, 이러한 구조를 상속 관계를 통해 논리적으로 개념적으로 연관 시키는 것을 상속이라 한다.
따라서 상속을 사용하는 경우는 명확한 is - a 관계에 있는 경우, 그리고 상위 클래스가 확장할 목적으로 설게되었고 문서화도 잘되어 있는 경우에 사용하면 좋다. 그러나 상속을 제대로 활용하기 위해서는 부모 클래스의 내부 구현에 대해 상세하게 알아야 하기 때문에 자식 클래스와 부모 클래스 사이의 결합도가 높아질 수 밖에 없다.
또한 상속 관계는 컴파일 타임에 결정되고 고정되기 때문에 코드를 실행하는 도중에 변경할 수 없다. 따라서 여러 기능을 조합해야 하는 설계에 상속을 이용하게 된다면 모든 조합별로 클래스를 하나하나 추가해주어야 한다. 이것을 클래스 폭발 문제라 한다.
더군다나 Java8부터는 인터페이스의 디폴트 메서드 기능이 나오면서 인터페이스내에서 로직 구현이 가능하여 상속의 장점이 약화되었다고 할 수 있다. 그래서 더 클래스 상속보다는 인터페이스 구현을 이용하라는 풍문을 한번쯤 들어봤을 것이다. 결과적으로 상속은 클래스간의 관계를 한눈에 파악할 수 있고 코드를 재사용할 수 있는 쉽고 간단한 방법일지는 몰라도 우아한 방법이라고 할 수는 없다.
합성(Composition)이란?
합성(Composition)은 또다른 말로 조합이나 그냥 콩글리쉬로 컴포지션이라고 불린다. 합성 기법은 기존 클래스를 상속을 통한 확장하는 대신에, 필드로 클래스의 인스턴스를 참조하게 만드는 설계이다.
예를들어 서로 관련없는 이질적인 클래스의 관계에서, 한 클래스가 다른 클래스의 기능을 사용하여 구현해야 한다면합성의 방식을 사용한다고 보면 된다.가령 학생(Student)이 수강하는 과목(Subject)들이나, 자동차(Car)와 엔진종류(Engine) 간의 관계같이 아주 연관이 없지는 않지만 상속 관계로 맺기에는 애매한 것들을 다루는 것으로 볼 수도 있다.
class Car {
Engine engine; // 필드로 Engine 클래스 변수를 갖는다(has)
Car(Engine engine) {
this.engine = engine; // 생성자 초기화 할때 클래스 필드의 값을 정하게 됨
}
void drive() {
System.out.printf("%s엔진으로 드라이브~\n", engine.EngineType);
}
void breaks() {
System.out.printf("%s엔진으로 브레이크~\n", engine.EngineType);
}
}
class Engine {
String EngineType; // 디젤, 가솔린, 전기
Engine(String type) {
EngineType = type;
}
}
public class Main {
public static void main(String[] args) {
Car digelCar = new Car(new Engine("디젤"));
digelCar.drive(); // 디젤엔진으로 드라이브~
Car electroCar = new Car(new Engine("전기"));
electroCar.drive(); // 전기엔진으로 드라이브~
}
}
위의 초기화 코드에서 볼수 있듯이, 마치 new 생성자에 new 생성자를 받는 형식 new Car(new Engine(“디젤”)) 으로 쓰여진다. 즉, Car 클래스가 Engine 클래스의 기능이 필요하다고 해서 무조건 상속하지말고, 따로 클래스 인스턴스 변수에 저장하여 가져다 쓴다는 원리이다. 이 방식을 포워딩(forwarding)이라고 하며 필드의 인스턴스를 참조해 사용하는 메서드를 포워딩 메서드(forwarding method) 라고 부른다. 그래서 클래스간의 합성 관계를 사용하는데 다른 말로 Has-A 관계라고도 한다. 객체 지향에서 다른 클래스를 활용하는 기본적인 방법이 바로 합성을 활용하는 것이다.
합성을 이용할때 꼭 클래스 뿐만 아니라 추상 클래스, 인터페이스로도 가능하다.
추상화 (Abstraction)
객체의 공통적인 속성과 기능을 추출하여 정의

코드 예제
Vehicle 인터페이스
public interface Vehicle {
public abstract void start();
void moveForward(); // public abstract 생략가능
void moveBackward();
}
MotorBike 클래스
public class MotorBike implements Vehicle {
@Override
void moveForward() {
System.out.println("오토바이 앞으로 전진 중");
}
@Override
void moveBackward() {
System.out.println("오토바이 뒤으로 후진 중");
}
}
Car 클래스
public class Car implements Vehicle {
@Override
void moveForward() {
System.out.println("자동차 앞으로 전진 중");
}
@Override
void moveBackward() {
System.out.println("자동차 뒤으로 후진 중");
}
}
위에서 확인할 수 있듯이, Vehicle 인터페이스를 구현한 구현체, Car 와 MotorBike 클래스에서 앞서 우리가 인터페이스에 정의한 역할을 각각의 클래스의 맥락에 맞게 구현했다. 각각 클래스 모두 전진과 후진의 기능을 공통적으로 가지지만, 차는 차의 시동을 걸어야 하고, 오토바이는 오토바이의 시동을 걸어야 하기 때문에 그 구현은 각 클래스에 따라 달라야 할 것이다.
객체 지향 프로그래밍에서는 이를 역할과 구현의 분리라고 하며, 이 부분이 아래에서 살펴볼 다형성과 함께 유연하고 변경이 용이한 프로그램을 설계하는 데 가장 핵심적인 부분이라 할 수 있다.
다형성 (Polymorphism) ⭐
여러가지 형태를 가지는 성질
-
다형성은 상속을 전제조건으로 함
-
다형성을 적용하는 방법은 부모클래스의 객체를 선언하고 자식클래스의 객체를 할당하는 것. 반대의 경우는 불가능
=> 다형적 객체 - 부모 = 자식 ex) Person p2 = s1; (자식은 부모에게 물려받은 코드 + 자신의 코드로 이루어져 있기 때문에 부모의 형태로 변환가능)
-
메서드 재정의 : 부모에게 물려받은 메서드를 이름과 리턴타입, 매개변수 등의 선언형태는 그대로, 내용만 바꾸는 형태
컴파일 시에는 부모의 메서드를, 실행 시는 자식의 메서드를 사용 => 자식이면서 부모인 형태(다형성)을 가질 수 있음 (때문에 부모클래스에 있는 해당 메서드를 지우면 안됨)
public class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getDetails() {
return "이름: "+name+", 나이 : "+age;
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}