[Java] Review

주영·2026년 9월 13일

객체지향 프로그래밍 : OOP

Object-Oriented Programming Language

  • 객체를 기반으로 프로그램을 작성하는 언어
  • 부품 객체를 먼저 만들고 하나씩 조립해 가는 프로그램 기법
    → 사용 목적 : 소프트웨어 생산성 향상, 실세계에 대한 쉬운 모델링

✓ 객체란?

  • 데이터(변수, 속성, 필드) + 기능(메소드, 함수)
  • 객체 지향 기법의 3R : Readability, Reusability, Reliability

객체 지향의 특성

#1 캡슐화 → 숨기자!

  • 약속되지 않은 부분 / 보여줄 필요가 없는 부분을 감싸서 숨겨버리는 것
  • 객체의 필드, 메소드를 하나로 묶고, 실제 구현 내용 감춤
  • 외부 객체는 객체 내부 구조 알 수 없고 객체가 노출한 필드와 메소드만 이용 가능
    • 접근 제한자(Access Modifier) 사용: public / private / protected
    • private 속성을 공유할 때는 getter / setter 메소드 구현
  • 캡슐화 사용 이유 : 외부의 잘못된 사용으로 인해 객체가 손상되지 않고, 캡슐화된 멤버를 노출시킬 것인지 숨길 것인지 결정하기 위해서

#2 상속성 → 공통 부분을 재사용하자!

  • 여러 객체를 설계하면서 일반화 개념이 발견되었거나 새로운 객체를 정의할 때, 기존 설계도를 재사용함
  • 하위 객체는 상위에서 허용한 모든 멤버를 물려받아 사용하며 상위에서 정의되지 않은 새로운 멤버를 추가하거나 확장하고 수정함 / 필요 시 상위에서 물려받은 메소드 재정의 가능
  • 상속의 조건 : 일반화
    • 물리적으로 개념적으로 Is-a, Is-kind of 관계가 성립되고 타당할 때만 사용
    • 단순히 공통 부분이 있다고 사용하면 안 됨
    • 상속 대상 : 상위 개체에서 허용한 멤버(속성+일반메소드)만 가능
  • 상속 : 멤버 속성과 메소드를 모두 포함 / extends 사용
  • 인터페이스 : 메소드만 포함 / implements 사용
  • 클래스는 단일 상속만 허용하지만, 기능을 분리하여 인터페이스를 다중 상속할 수 있음
  • 상속의 장점: 클래스 간결화, 관리 용이, 소프트웨어 생산성 향상
  • 상속의 단점: 강한 결합 및 취약한 기반 클래스 문제, 캡슐화 약화, 불필요한 클래스 증가, 유지보수 어려움
    → 클래스 상속보다는 인터페이스, 합성 구조를 권장함 (= 상속회피)

#3 다형성

  • 같은 이름의 메소드가 클래스나 객체에 따라 다르게 동작하도록 구현
  • 어떤 메소드가 실행될지는 실행 중 동적 바인딩
  • 먼저 하위 클래스에서 method를 찾고, 없으면 상위 클래스로 올라오면서 찾아 결정함
  • 메소드 오버로딩(overloading) : 같은 이름이지만 인자의 형태, 수에 따라 optional하게 동작하도록 함
  • 메소드 오버라이딩(overriding) : 슈퍼클래스에서 상속받은 메소드를 서브 클래스마다 각자 다르게 구현함
  • 인터페이스(interface) : 메소드만 정의한 클래스, 기능을 명시함
    • 각 객체가 반드시 구현해야 할 동작을 명시해야 할 때, 객체 전체가 아니라 객체의 기능으로 접근해야 한다면 interface 사용



클래스와 객체의 구조와 관계

  • 클래스 : 멤버(필드+메소드) 설계도

  • 객체 : 클래스 모양대로 찍어낸 실체

    객체의 상호 작용

    • 객체 지향 프로그램에서 객체는 다른 객체와 관계를 맺음
    • 객체들은 서로 간에 기능(동작)을 이용하고 데이터를 주고 받음

    주요 관계 종류

    • 집합(Has-a): 완성품과 부품의 관계
    • 사용(Use-a): 객체가 다른 객체를 사용하는 관계
    • 상속(Is-a): 종류 객체와 구체적인 사물 객체 관계

    UML : Unified Markup Language

    • 클래스와 객체를 모델링하기 위한 markup 언어



생성자(Constructor) 기본과 호출 순서

생성자 : 객체가 생성될 때 초기화를 위해 실행되는 메소드

생성자의 특징

  • 리턴타입이 없는 메소드
  • 생성자는 new를 통해 객체를 생성할 때, 한 번 호출됨 → 반드시 호출
    • 강아지 a = new 강아지(); // 생성자 호출
    • 강아지 b = new 강아지("댕댕이"); // 이름을 "댕댕이"로 설정할 수 있는 생성자 오버로딩
    • 개발자가 생성자를 작성하지 않으면 자동으로 기본 생성자 삽입됨
  • 생성자 이름은 클래스 이름과 반드시 동일해야 함
  • 생성자는 필요에 따라 여러 개를 작성할 수 있음(메소드 오버로딩)

Lab #1 : 두 개의 생성자를 가진 Circle 클래스

public class Circle {
	
	int radius;
	String name;
	
	public Circle() { // 매개변수 없는 생성자, 기본 생성자 
		radius = 1; 
		name = "";
	}
	
	public Circle(int r, String n) { // 매개변수를 가진 생성자 
		radius = r; 
		name = n;
	}
	
	public double getArea() {
		return 3.14*radius*radius;
	}

	public static void main(String[] args) {

		Circle pizza = new Circle(10, "자바피자"); // Circle 객체 생성, 반지름 10
		
		double area = pizza.getArea();
		System.out.println(pizza.name + "의 면적은 " + area);
		
		Circle donut = new Circle(); // Circle 객체 생성, 반지름 1(초기값)
		
		donut.name = "도넛피자";
		area = donut.getArea();
		System.out.println(donut.name + "의 면적은 " + area);
	}

}


기본 생성자 : 매개변수 없고 아무 작업 없이 단순 리턴하는 생성자

  • 클래스에 생성자가 하나도 선언되지 않은 경우, 컴파일러가 자동으로 생성함
  • 인자가 없는 기본 생성자는 넣어두는 것이 좋음!

Lab #2 생성자 선언 및 활용 연습


public class Book {
	
	String title;
	String author;
	
	public Book() { // 기본 생성자 
		
	}
	
	public Book(String t) { // 제목으로 초기화하는 생성자 
		this.title = t;
		this.author = "작가미상";
	}
	
	public Book(String t, String a) { // 제목과 저자 이름으로 초기화하는 생성자 
		this.title = t;
		author = a;
	}

	public static void main(String[] args) {
		Book book = new Book();
		Book littlePrince = new Book("어린왕자", "생텍쥐페리");
		Book loveStory = new Book("춘향전");
		
		System.out.println(littlePrince.title + " " + littlePrince.author);
		System.out.println(loveStory.title + " " + loveStory.author);
	}
	
}

Q. 왜 인자가 하나인 생성자에서는 this.author을 사용하고 인자 두 개인 생성자에서는 author을 사용한 거지?
this. ⇒ 현재 객체에 속한 것이라는 걸 알려주는데, 여기서는 멤버 변수랑 매개변수가 같으니까 굳이 사용을 안 해도 됨


서브 클래스의 슈퍼 클래스 생성자 호출 및 실행

  • new에 서브 클래스의 객체가 생성될 때
    • 슈퍼 클래스에 의해 생성자와 서브 클래스 생성자가 순차적으로 모두 실행됨
    • 호출 순서 : 서브 클래스 생성자 먼저 호출
    • 실행 순서 : 슈퍼 클래스 생성자 먼저 실행
      → 서브 클래스 생성자에서 슈퍼 클래스 생성자가 먼저 호출되어서 슈퍼 클래스 생성자가 먼저 실행되는 것이다
  • 서브 클래스에서 슈퍼 클래스 생성자를 선택하는 방법
    • super() 사용
    • 사용자가 명시하지 않으면 기본 생성자 자동 생성 - 생성자에서 상위 생성자 자동 호출 - 맨 윗줄에 super()가 생김

슈퍼 클래스와 서브 클래스의 생성자 간의 호출 및 실행 관계

super(), super(parameter)

  • 객체 생성 시 필요한 일을 생성자에 명시
  • 인자의 형태로 오버로딩 시켜두는 것이 좋음
  • 서브 클래스에서는 명시적으로 슈퍼 클래스의 생성자를 선택하여 호출 → 반드시 코드의 제일 첫 라인에 오도록
  • 상속 관계에서 여러 개의 생성자를 갖는 경우 주의할 것!
  • 서브 클래스의 생성자에서 명시적으로 슈퍼 클래스의 생성자를 선택하지 않은 경우
    → 기본 생성자가 호출됨, 컴파일러는 서브 클래스의 기본 생성자에 대해 자동으로 슈퍼 클래스의 기본 생성자와 짝을 맺음



메소드 오버로딩(Overloading)과 오버라이딩(Overriding)

메소드 : 접근 지정자 + 리턴 타입 + 메소드 이름 + 메소드 인자들 + 메소드 코드
자바의 모든 메소드는 반드시 클래스 안에 있어야 함 ⇒ 캡슐화 원칙


메소드 오버로딩(Overloading)

같은 클래스나 상속 관계에서 동일한 이름의 메소드를 중복으로 작성하는 것

  • 이름이 같은 여러 개의 메소드를 사용해 편리성 향상, 다형성 실현
  • 매개변수의 개수나 타입, 순서가 서로 다름
  • 리턴 타입은 상관 없음
  • 매개변수는 자동 형변환 규칙이 적용

메소드 오버라이딩(Overriding)

서브 클래스에서 슈퍼 클래스에 있는 메소드와 동일한 이름의 메소드 재작성

  • 슈퍼 클래스에 구현된 메소드를 무시하고 서브 클래스에서 새로운 기능의 메소드를 재정의, 다형성 실현

  • 메소드 이름, 매개변수 타입과 개수, 리턴 타입 모두 동일

  • 서브 클래스에 오버라이딩된 메소드가 실행되는 동적 바인딩 발생

  • 접근 제한을 더 강하게 오버라이딩 하는 것은 불가

    • public을 default / private으로 수정 불가
    • default을 public으로는 수정 가능



접근 지정자

클래스나 멤버의 공개 범위 설정
→ 클래스나 일부 멤버를 공개/비공개하여 다른 클래스에서 접근하는 것을 제한할 수 있음

  • private : 같은 클래스 내에서만 접근 가능, 외부로부터 완벽 차단
  • default : 명시하지 않으면 동일 패키지 내에서만 접근 가능
  • protected : 동일 패키지와 자식 클래스에서 접근 가능
  • public : 모든 클래스에서 접근 가능

앞으로 주요 변수는 private으로 설정하고 auto-generation으로 getter, setter을 만들어두자!

  • getter : 값을 가져오는 메소드
    리턴 타입 getFieldName() {}
  • setter : 값을 세팅하는 메소드
    void setFieldName(타입 변수) {}



static / final / 싱글톤

static 멤버와 non-static 멤버

static 멤버

  • 객체를 생성하지 않고 바로 사용 가능
  • 객체마다 생기는 것이 아님
  • 클래스 당 하나만 생성됨
  • 클래스가 로딩될 때 static 공간에 할당됨
  • 동일한 클래스의 모든 객체에 의해 공유
  • this 사용 불가

non-static 멤버

  • 멤버들은 객체마다 독립적으로 별도 존재
  • 필드와 메소드는 객체 생성 후 사용 가능
  • 멤버들은 다른 객체에 의해 공유되지 않음

static의 활용
① 전역 변수와 전역 함수를 만들 때
② 공유 멤버를 작성할 때

싱글톤(Singleton)

하나의 어플리케이션 내에서 단 하나만 생성되도록 만든 객체

  • 외부에서 new 연산자로 생성자를 호출할 수 없도록 막기 : private 접근 제한자를 생성자 앞에 붙임
  • 클래스 자신의 타입으로 정적 필드 선언 : private 접근 제한자를 붙여 외부에서 필드 값 변경할 수 없도록
  • 외부에서 호출할 수 있는 정적 메소드인 getInstance()를 선언하여 접근하도록 함

Lab #3 싱글톤 객체 만들기 - 하늘

public class 하늘 {
	
	private static 하늘 instance = null;
	
	private 하늘() {
		System.out.println("하늘 객체를 만듭니다!");
	}
	
	public static 하늘 getInstance() {
		
		if (instance == null) instance = new 하늘();
		
		return instance;
	}
}
public class Main {

	public static void main(String[] args) {
		
		//하늘 sky1 = new 하늘();
		하늘 sky1 = 하늘.getInstance();
		하늘 sky2 = 하늘.getInstance();
		하늘 sky3 = 하늘.getInstance();
		
		System.out.println(sky1);
		System.out.println(sky2);
		System.out.println(sky3);

	}

}

final 클래스와 메소드

final 클래스 : 클래스 상속 불가
final 메소드 : 오버라이딩 불가

final 상수 선언 - 선언 시 초기값 지정해야 하며, 실행 중 값 변경 불가




추상 클래스와 인터페이스

추상 클래스

실체 클래스 생성 목적이 아닌, 상속용/참조용 클래스
실체 클래스들의 공통되는 필드와 메소드만 정의해둔 것
→ 구체적인 행위는 상속받은 클래스마다 다를 수 있으므로 추상적인 개체를 표현해야 함

  • 추상 클래스는 추상 메소드를 포함할 수 있음
    • 추상 메소드 : body는 없고 prototype만 있는 메소드
  • 추상 메소드가 있으면 추상 클래스로 선언되어야 하고, 추상 메소드가 없는 클래스를 추상 클래스로 만들면 생성은 문법적으로 불가능함

추상 클래스와 인터페이스 비교

비슷한 점

  • 설계와 구현 분리
  • 클래스 다형성 실현
  • 객체 생성 용도로 사용하지 않음

다른 점

  • 추상 클래스
    • 추상 메소드와 일반 메소드 모두 포함
    • 상수, 변수 필드 모두 포함
  • 인터페이스
    • 변수 필드는 포함하지 않음
    • 상수, 추상 메소드, 일반 메소드, default 메소드, static 메소드 모두 포함
    • protected 접근 지정 선언 불가
    • 다중 상속 지원



인터페이스 Review

Lab #4 인터페이스 상속과 구현

interface A{
	public void a();
}

interface B{
	public void b();
}

interface C extends A,B { // 인터페이스 다중 상속 가능 
	public default void c() {
		System.out.println("인터페이스에서 바디가 있는 디폴트 메소드를 가질 수 있다");
	}
}

abstract class E {
	public abstract void e();
}

class D extends E implements C {
	
	// 위에서 상속받은 모든 것들이 구현되어 있어야 함 
	public void a() {}	
	public void b() {}	
	public void c() {}	
	public void e() {}
	
}

public class Main {

	public static void main(String[] args) {
		
		A a = new D();
		D d = new D();

	}

}

→ 인터페이스는 클래스에게 어떤 기능을 반드시 구현해야 한다는 책임을 부여함
→ 인터페이스는 인터페이스를 상속(extends)한다고 하지만, 클래스는 인터페이스를 구현(inplements)한다고 함
→ 일반 클래스는 추상 메소드를 가질 수 없기 때문에 모두 구현해야 함
→ main에서는 두 경우 모두 D 객체를 생성하는 것은 맞지만, 앞에 있는 참조 타입(A, D)에 따라 접근할 수 있는 멤버가 달라짐

Lab #5 리모컨 만들기

interface RemoteRole {
	
	// 인터페이스는 상수와 static만 가질 수 있음
	public static final int MAX_VOL = 10;
	public static final int MIN_VOL = 0;
	
	public void turnOn();
	public void turnOff();
	public void set_vol(int vol);
	
	//어떤 객체든 같은 동작을 한다면 default로 바디가 있는 메소드를 정의 
	public default int get_vol() {
		return 0;
	}
	
	public default void set_MUTE() {
		System.out.println("무음처리합니다.");
		this.set_vol(0);
	}
	
}
public class Audio implements RemoteRole{
	
	private int volume; 

	@Override
	public void turnOn() {
		// TODO Auto-generated method stub
		System.out.println("AUDIO를 켭니다.");
	}

	@Override
	public void turnOff() {
		// TODO Auto-generated method stub
		System.out.println("AUDIO를 끕니다.");
	}

	@Override
	public void set_vol(int vol) {
		// TODO Auto-generated method stub
		this.volume = vol;
		System.out.println("AUDIO의 volume을 " + vol + "로 setting합니다.");
	}

	@Override
	public int get_vol() {
		// TODO Auto-generated method stub
		return this.volume;
	}

}

→ 다른 클래스에서 private인 volume 자체에 접근할 수는 없지만, public인 메소드를 통해 접근 가능


public class Car implements RemoteRole{
	
	private int volume;

	@Override
	public void turnOn() {
		// TODO Auto-generated method stub
		this.시동걸기();
	}

	@Override
	public void turnOff() {
		// TODO Auto-generated method stub
		this.시동끄기();
	}

	@Override
	public void set_vol(int vol) {
		// TODO Auto-generated method stub
		this.volume = vol;
	}
	
	public void 시동걸기() {
		System.out.println("시동을 켭니다.");
	}

	public void 시동끄기() {
		System.out.println("시동을 끕니다.");
	}
	
	public void move() {
		System.out.println("부릉부릉~");
	}
}

→ RemoteRole의 turnOn 요청은 Car에서 시동걸기 동작으로 수행되고, turnOff 요청은 Car에서 시동끄기 동작으로 수행됨


public class TV implements RemoteRole{
	
	private int volume;

	@Override
	public int get_vol() {
		// TODO Auto-generated method stub
		return this.volume;
	}

	@Override
	public void set_MUTE() {
		// TODO Auto-generated method stub
		System.out.println("TV를 무음처리합니다.");
		set_vol(0);
	}

	@Override
	public void turnOn() {
		// TODO Auto-generated method stub
		System.out.println("TV를 켭니다.");
	}

	@Override
	public void turnOff() {
		// TODO Auto-generated method stub
		System.out.println("TV를 끕니다.");
	}

	@Override
	public void set_vol(int vol) {
		// TODO Auto-generated method stub
		this.volume = vol;
		System.out.println("TV의 volume을 " + vol + "로 setting합니다.");
	}

}
public class MyRemoteControl implements RemoteRole{
	
	RemoteRole rc = null;
	
	public MyRemoteControl(TV tv) {
		this.rc = tv;
	}
	
	public MyRemoteControl(Audio au) {
		this.rc = au;
	}
	
	public MyRemoteControl(RemoteRole rc) {
		this.rc = rc;
	}
	
	// 장치 변경 메소드 방법 1 
	// 하나의 메소드에서 여러 객체를 구분하는 동작 
	public void changeMode(Object o) {
		System.out.println("인자를 Object로 받아 변경합니다.");
		if (o instanceof Audio) {
			System.out.println("Audio Mode로 바꿉니다!");
			this.rc = (Audio) o;
		} else if(o instanceof TV) {
			System.out.println("TV Mode로 바꿉니다!");
			this.rc = (TV) o;
		} else System.out.println("Car Mode로 바꿉니다!")
	}
	
	// 장치 변경 메소드 방법 2
	// 메소드 인자로 구분된 메소드마다 각각의 동작
	
	public void changeMode(Audio au) {
		System.out.println("Audio Mode로 바꿉니다!");
		this.rc = au;
	}
	
	public void changeMode(TV tv) {
		System.out.println("TV Mode로 바꿉니다!");
		this.rc = tv;
	}
	
	public void changeMode(Car car) {
		System.out.println("Car Mode로 바꿉니다!");
		this.rc = car;
	}

	@Override
	public void turnOn() {
		// TODO Auto-generated method stub
		this.rc.turnOn();
	}

	@Override
	public void turnOff() {
		// TODO Auto-generated method stub
		this.rc.turnOff();
	}

	@Override
	public void set_vol(int vol) {
		// TODO Auto-generated method stub
		this.rc.set_vol(vol);
	}

	@Override
	public int get_vol() {
		// TODO Auto-generated method stub
		return this.rc.get_vol();
	}
	
}

→ 방법2가 모듈화+OOP에 더 적합함, 새로운 장치가 추가되면 메소드만 추가하면 됨

public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		// test1: 리모컨 인터페이스로 연결한 각 객체들의 동작 

		RemoteRole rm; // 인터페이스 rm 선언
        TV tv = new TV();
		Audio au = new Audio();
		Car car = new Car();
        
		rm = tv; // rm을 이용해 TV 동작 
		rm.turnOn();
		rm.set_MUTE();
		rm.set_vol(3);
		System.out.println("-------------------------------");
		
		rm = au; // rm을 이용해 Audio 동작 
		rm.turnOn();
		rm.set_vol(5);
		rm.set_MUTE();
		System.out.println("-------------------------------");

		// test2: 리모컨 객체를 통해 연결된 객체들의 동작 
		
		MyRemoteControl my_rc = new MyRemoteControl(au); // 리모컨 객체 my_rc 생
		
		System.out.println("audio sound : " + my_rc.get_vol());
		my_rc.set_vol(9);
		
		System.out.println("audio sound : " + my_rc.get_vol());
		
		my_rc.changeMode(car);
		my_rc.turnOn();		
		System.out.println("-------------------------------");

		my_rc.changeMode((Object)tv);
		my_rc.turnOn();
		
	}

}




다형성 동작 매커니즘 : 업캐스팅과 오버라이딩

업캐스팅

자식 클래스의 객체를 부모 클래스 타입으로 바라보는 것


짱구테스트


→ 오버라이딩은 덮어쓴 거라서 원래 내용을 쓰려면 super 사용해야 함
→ super을 사용하면 바로 상위 클래스로만 갈 수 있어서 할머니처럼요리하기()를 해도 바로 할머니한테 가는 게 아니라 엄마한테 감, 엄마의 할머니처럼요리하기() 안의 super을 사용해서 할머니의 요리하기()가 실행됨

→ 요리하기()는 이름도 같고 매개변수도 같은 걸 자식이 또 쓰니까 오버라이딩
→ 핸드폰뺏기(짱구), 핸드폰뺏기(엄마)는 이름은 같지만 매개변수가 다르니까 오버로딩

Q. 짱구는 엄마를 상속받았는데 핸드폰뺏기(짱구)는 어떻게 되는 거지?
내가 코드에 꼭 적지는 않았어도, 짱구는 핸드폰뺏기(짱구)도 엄마로부터 상속받음 그래서 지금 짱구 코드에 이 메소드를 적지는 않았지만 테스트코드에서 실행하려고 하면 할 수는 있음




람다식(λ-Expression)

이름 없는 함수를 간단히 표현하는 문법

  • 함수형 인터페이스와 함께 사용
  • 코드 간결화, 직관성 및 가독성 향상 ⇒ 매우 효율적
  • 일회성 동작의 표현, 이벤트 처리, Stream API 등에 활용

표현 방법
① 인자가 없을 때 : () -> {...}
② 인자가 하나일 때 : (매개변수) -> {...}
③ 인자가 두 개 이상일 때 : (매개변수1, 매개변수2) -> {...}

람다식을 이용한 함수의 구현과 호출
1. 인터페이스에서 메소드 '틀'만 정의 → 추상 메소드
2. 람다식으로 메소드 구현
3. 메소드 호출

interface Hello {
	void sayHello(); // 메소드 정의
    default void sayBye() {
		System.out.println("이제 집에 가요~");
	} 
}

interface Square {
	int calc(int x); // 메소드 정의
}

interface Calculator {
	int operate(int a, int b); // 메소드 정의 
}

public class LambdaTest {

	public static void main(String[] args) {
		
		Hello h = () -> {
			System.out.println("안녕하세요!");
		};
		
		h.sayHello();
		h.sayBye();
		
		Square s = (x) -> x*x; // 메소드 구현 
		System.out.println(s.calc(10)); // 메소드 호출 
		
		Calculator add = (x,y) -> x+y; 
		System.out.println(add.operate(3, 2)); 
		
		Calculator sub = (x,y) -> x-y;
		System.out.println(sub.operate(3, 2));
	}

}

→ 인터페이스 안에서 인자가 없는 것을 두 개 만드려면 위에서처럼 default를 사용해야 함, 그렇지 않으면 읽지 못함
→ 핵심 동작만 표현함으로써 객체 생성 없이 유연하게 함수를 정의할 수 있게 됨

0개의 댓글