* 해당 글은 백엔드 자바 강의이후 회고 글입니다.
https://bootcamp.likelion.net/school/kdt-backendj-21th
백엔드 부트캠프 21기: Java : 멋사 부트캠프
실전 스킬 기반 백엔드 개발자 취업 완벽 대비 교육
bootcamp.likelion.net
인터페이스(interface)
인터페이스는 클래스 혹은 프로그램이 제공하는 기능을 명시적으로 선언하는 역할을 한다.
인터페이스는 추상 메서드와 상수로만 이루어져 있다.
구현된 코드가 없기 때문에 당연히 인터페이스로 객체 인스턴스를 생성할 수도 없다.
public interface Vehicle {
void start(); // 추상 메서드
void stop(); // 추상 메서드
}
인터페이스의 주요 특징
- 추상 클래스와 달리 인터페이스를 구현하는 클래스에 대해 다중 상속이 가능하다.
- 추상클래스의 경우 어제 정리한 글의 내용과 같이 다이아몬드 문제, 즉 추상 클래스를 상속받는 서로 다른 클래스 2가지를 다중 상속 받을때 내부에 구현되어 있는 반환타입과 매개변수, 이름이 모두 똑같은 두 메서드들 중 어느 메서드를 오버라이드 받아와야 하는지 모호해진다.
- 그러나 인터페이스의 경우 추상 클래스와 달리 내부의 구현코드가 존재하지 않기 때문에 다중 상속으로 인해 모든 부분이 완전히 같은 두 메서드가 존재한다고 해도 인터페이스를 구현하는 클래스에서 해당 메서드의 내용을 구현해주면 되기 때문에 다중 상속이 문제가되지 않는다.
- 즉, 다이아몬드 문제(메서드 충돌 문제) 가 발생하지 않는다는 뜻이다.
- 모든 메서드가 기본적으로 추상 메서드로 선언된다.
- default, static 메서드를 사용 가능하다.
- private 메서드의 경우 구현 클래스에서 상속은 불가능한 대신, 인터페이스 내부에서 default 메서드에 의해 호출될 수 있다.
- 필드는 상수만 선언 가능하다.
- 구현코드가 없으므로 new 키워드를 통해 객체를 생성할 수 없다.
- 인터페이스의 타입을 이용해 다형성의 특징을 활용할 수 있다. 즉, 클라이언트 프로그램을 많이 수정하지 않고 기능을 추가하거나 다른 기능을 사용할 수 있다.
- 인터페이스를 구현하는 클래스는 각 인터페이스의 타입으로 형 변환(타입 캐스팅) 할 수 있기 때문에 이를 통해 프로그램을 수정하지 않고 기능을 추가하거나 다른 기능을 사용하는 등의 다형성의 특징이 극대화된다.
- 인터페이스를 사용하면 클래스간의 결합도를 낮추고 시스템의 유지보수성을 높일 수 있다
인터페이스가 결합도를 낮추는 원리
- 구현과 분리 : 개발 코드는 인터페이스의 메서드만 알면 되며, 실제 구현 객체가 무엇인지 몰라도 된다.
- 유연한 교체 : 인터페이스를 구현한 객체는 언제든 변경 가능하며, 이는 OCP(Open-Closed-Principle, 개방-폐쇄 원칙) 를 준수하여 확장에는 열려있고 수정에는 닫혀있게된다. (이와같은 특징이 가장 잘 드러나는것이 스프링에서 인터페이스 타입을 이용해 스프링 컨테이너에 있는 스프링 빈 객체에 대해 의존성을 주입받는 경우이다.)
- 의존성 감소 : 클래스들이 구체적인 클래스에 종속되지 않기 때문에, 한 클래스의 변경이 다른 클래스들의 연쇄적인 코드 수정을 유발하지 않는다.
결론적으로 인터페이스는 구체적인 객체에 대한 의존도를 최소화 하여 자료 결합도(Data Coupling)에 가까운, 가장 낮은 단계의 결합을 형성하는데 기여한다.
(자료 결합도 : 클래스간 파라미터로 필요한 데이터만 전달하는 경우)
추상 클래스와 인터페이스의 차이점
위의 인터페이스에 대한 내용과 추상 클래스간의 차이점은 다음과 같다.
| 특징 | 추상 클래스 | 인터페이스 |
| 메서드 구현 | 가능(추상 + 일반 메서드 혼합) | JDK 8 이후 default,static 메서드 가능, 일반 메서드 불가능 |
| 다중 상속 | 불가능(다이아몬드 문제) | 가능 |
| 필드 | 일반 변수선언 가능 | 상수만 가능(final) |
| 목적 | 공통 기능 자식 클래스들과 공유 | 구현 기능 강제 |
다형성(Polymorphism)
다형성이란 하나의 코드가 여러 자료형으로 구현되어 실행되는 것을 말한다. 즉, 같은 코드에서 여러가지의 실행 결과가 나오는 것이다.
아래의 코드를 한번 보자.
class Animal {
public void move() {
System.out.println("동물이 움직입니다.");
}
}
class Human extends Animal {
public void move() {
System.out.println("사람이 두 발로 걷습니다.");
}
}
class Tiger extends Animal {
public void move() {
System.out.println("호랑이가 네 발로 뜁니다.");
}
}
class Eagle extends Animal {
public void move() {
System.out.println("독수리가 하늘을 날아다닙니다.");
}
}
public class AnimalTest01{
public static void main(String[] args){
AnimalTest01 aTest = new AnimalTest01();
aTest.moveAnimal(new Human());
aTest.moveAnimal(new Tiger());
aTest.moveAnimal(new Eagle());
}
public void moveAnimal(Animal animal) { // 매개변수의 자료형이 부모 클래스
animal.move(); // animal 인스턴스는 자식 클래스의 객체 인스턴스 이므로 재정의된 메서드가 호출됨
}
}
위의 코드를 보면 moveAnimal(0 메서드가 호출될 때 내부에서 실행되는 animal.move() 메서드는 변함이 없지만 어떤 매개변수가 넘어왔냐에 따라 출력문이 달라지게 된다.
이것이 바로 다형성의 특징이 잘 드러난 경우이다.
다형성의 장점
- 부모-자식 상속 관계의 클래스에서 부모 클래스에서 공통 부분의 메서드를 제공하고, 자식 클래스에서는 그것을 기반으로 추가 요소를 덧붙여 구현하면 각 자료형에 따라 코드를 일일히 다르게 구현해줄때와 달리 코드의 양도 줄어들고 유지보수도 편리해진다.
- 또한 필요에 따라 연쇄적으로 상속받은 모든 클래스를 한 가지의 부모클래스로 처리할 수 있어 재사용성이 증가하고 다형성에 의해 각 클래스의 여러가지 구현을 실행할 수 있으므로 프로그램을 쉽게 확장할 수 있다.
- 이처럼 다형성을 잘 활용하면 유연하면서도 구조화된 코드를 구현하여 확장성있고 유지보수 하기 좋은 프로그램을 개발할 수 있다.
다형성의 실현 조건
- 부모 클래스와 자식 클래스간의 상속이 있어야 한다.(상속)
- 다형성을 위해 메서드 오버라이딩이 되어있어야 한다.(메서드 오버라이딩)
- 부모타입으로 자식 클래스에 있는 오버라이딩된 메서드 접근을 위해 자식 타입 객체를 부모 타입으로 변환해야 한다.(업캐스팅)
다형성에서 선조의 주소를 선언할 경우
- 인터페이스와 추상클래스가 모두 최상위에 존재하더라도, '공통 상태' 나 '공통 로직', 그리고 '계층 구조' 의 의미가 중심이 되는 경우에는 인터페이스보다 추상클래스를 다형성의 참조 타입으로 사용한다.
- 다형성의 참조 타입은 단순한 행위 규약일때는 인터페이스를, 공통 상태와 구현을 포함한 구조적 개념일 때는 추상 클래스를 사용한다.
- 인터페이스 VS 추상클래스 기준점 판단
- 공통 필드가 필요한가? YES -> 추상 클래스
- 공통 로직(템플릿 메서드)이 있는가? YES -> 추상 클래스 (무엇이다 (is-a) 관계)
- 여러 계층에서 횡단적으로 쓰이는 기능인가? YES -> 인터페이스 (무엇을 한다. (can-do))
IDraw d = new Circle(); // IDraw is a interface
Runnable r = new Thread(); // Runnable 인터페이스는 스레드 = method 를 실행한다는 뜻을 가지고있다.
Comparable c = new Person(); // name, age -> 특정 필드를 기준으로 비교 가능한 객체를 만든다.
* java.lang 에서 실행 관련 메서드들
- Process, ProcessBuilder, ProcessBuilder.Redirect -> exe 단위 실행
- Thread, ThreadGroup, ThreadLocal -> method 단위 실행
- Runtime, RuntimePermission -> 대상을 실행시킨다.
* Comparable 인터페이스의 간단한 활용방법
class Person implments Comparable<Person> {
public int compareTo(T o) {
return this.age - o.age;
}
}
Person p1 = new Person(20);
Person p1 = new Person(30);
p1.compareTo(p2);
다운 캐스팅과 instance of
부모 클래스가 자식 클래스로 형 변환이 되는 과정을 확인해보자.
Animal ani = new Human(); // 자식 클래스 -> 부모 클래스로 업캐스팅
- 위의 코드에서 생성된 인스턴스 Human 은 Animal 형이다. 이렇게 부모 클래스인 Animal 형으로 형변환이 이루어진 경우에는 Animal 클래스에서 선언한 메서드와 멤버 변수만 사용할 수 있다.
- 다시 말해, Human 클래스에 부모 클래스인 Animal 클래스보다 더 많은 메서드가 구현되어 있고 다양한 멤버 변수가 있다고 하더라도 자료형이 Animal 형인 상태에서는 사용할 수 없다.
- 따라서 필요에 따라 다시 원래 인스턴스의 자료형으로 되돌아가야 하는 경우가 있다.(여기서는 Human 형)
- 이렇게 상위 클래스로 형 변환(업캐스팅)되었던 하위클래스를 다시 원래 자료형으로 형 변환하는 것을 '다운 캐스팅' 이라고한다.
instanceof 키워드
상속관계를 생각해보면 모든 인간은 동물이지만, 모든 동물이 인간은 아니다.
따라서 다운캐스팅을 하기 전에 업캐스팅으로 상위 클래스로 형 변환된 하위 인스턴스의 원래 자료형을 확인해야 변환할 때 오류를 막을 수 있다.
이를 확인하는 예약어가 바로 instanceof 이다.
- 사용예시
Animal hAnimal = new Human();
if(hAnimal instanceof Human) { // hAnimal 인스턴스 자료형이 Human 형 이라면
Human human = (Human)hAnimal; // 인스턴스 hAnimal 을 Human 형으로 다운캐스팅
}
- instanceof 예약어는 왼쪽에 있는 변수의 원래 인스턴스형이 오른쪽 클래스 자료형인지를 확인한다.
- instanceof 의 반환값이 true 이면 다운캐스팅을 하는데, 이때는 Human human = (Human)hAnimal; 과 같이 명시적으로 자료형을 작성해줘야 한다.
- 상위 클래스로는 묵시적으로 형 변환이 되지만, 하위 클래스로 형 변환을 할 때는 명시적으로 해야하기 때문이다.
JDK 16+ 버전 에서의 instanceof 활용
public static void main(String[] args){
Employee m = new Manager();
Test(m);
}
// jdk 16+
public static void Test(Employee e){
if(e instanceof Manager m){ // 조건문을 통해 명시적 형변환
// Manager m = (Manager)e;
m.prn();
m.disp();
}
}
바인딩(Binding)
출처 :
[JAVA] 정적(Static) 바인딩 vs 동적(Dynamic) 바인딩
바인딩 (binding)바인딩이란 컴퓨터 프로그램에서 각종 값들이 더 이상 변경되지 않는 값으로 구속 되는 것입니다. 풀어서 설명해보자면, 변수(식별자, identifier)가 각종 타입에 의해 데이터형이
hyunsb.tistory.com
바인딩이란 컴퓨터 프로그램에서 각종 값들이 더 이상 변경되지 않는 값으로 구속되는 것이다.
좀 더 풀어서 설명하자면 변수가 각종 타입에 의해 데이터 자료형이 확정되는것, 변수가 메모리 주소를 가리키거나 값을 가지는것, 혹은 호출될 함수를 결정하는 것을 바인딩이라고 한다.
바인딩은 컴파일 시에도 수행되고 런타임 시에도 수행된다. 컴파일 과정에서 수행되는 바인딩을 정적(static) 바인딩이라고 하며, 런타임 과정에서 수행되는 바인딩을 동적(dynamic) 바인딩이라고 한다.
따라서 정적 바인딩, 동적 바인딩 여부는 컴파일이나 런타임 과정에서 결정된다라고 표현할 수 있다.
정적 바인딩(Static Binding)
컴파일 시에 결정되는 바인딩을 정적 바인딩이라고 한다.
메서드 오버로딩(method overloading) 이 대표적이다.
public class OverLoading {
public void print() {
System.out.println("매개변수 없는 print");
}
public void print(String param){
System.out.println("매개변수가 있는 print");
}
}
public static void main(String[] args){
OverLoading over = new OverLoading();
over.print();
over.print("매개변수");
}
위의 코드에서 OverLoading 클래스의 어떤 print() 메서드를 호출할 지는 컴파일러에 의해 결정된다. 오버로딩된 메서드는 매개변수의 유무, 종류, 갯수가 다르기 때문에 컴파일 과정에서 이를 구분 할 수 있기 때문이다.
또 다른 예시를 들어보자.
- 상속관계에서 static 메서드 활용의 경우
class School {
public static void ringBell(){
System.out.println("Ringing the school bell...");
}
}
class Classroom extends School {
public static void ringBell(){
System.out.println("Ringing the classroom bell...");
}
}
public static void main(String[] args){
School school1 = new School();
school1.ringBell(); // 정적 바인딩이기 때문에 School.ringBell() 을 자동으로 호출
Classroom classroom = new Classroom();
classroom.ringBell(); // 정적 바인딩이기 때문에 Classroom.ringBell() 을 자동으로 호출
School school2 = new Classroom(); // 업캐스팅
school2.ringBell(); // 정적 바인딩이기 때문에 School 클래스의 ringBell() 이 호출
}
- 위의 코드에서 School 클래스, Classroom 클래스 모두 ringBell() 메서드를 static 으로 정의하고 있다.
- 여기서 부모 클래스에 있는 static method 는 자식 클래스에게 상속되지 않는다.(static 메서드는 상속 불가능)
- static 메서드는 컴파일 시 클래스와 함께 JVM 의 Method area 에 로드된다. static 키워드가 붙은 멤버들은 인스턴스에 소속된 멤버가 아닌, 클래스에 소속된 멤버이기 때문에 클래스 변수 혹은 클래스 메서드라고 부른다.
- new 를 통해 객체의 여러 인스턴스를 생성하더라도 생성된 인스턴스는 서로 독립적이지만 static 키워드가 붙은 멤버들은 모든 인스턴스가 같은 메모리 영역을 공유하기 때문에 모두 같은 멤버가 호출된다. (같은 클래스 타입으로 만들어진 객체 인스턴스들의 경우)
- 결국 School 타입 변수에 Classroom 의 생성자를 호출하여 객체 인스턴스를 저장하더라도 컴파일러에 의해 논리적으로 school2 변수는 School 타입이기 때문에 School 클래스에 선언되어 있는 ringBell() 메서드가 실행된다.
동적 바인딩(Dynamic Binding)
컴파일 시 각종 자료형이나 메모리 참조 주소, 호출될 함수 등이 결정되는 정적 바인딩과 달리 각종 변수 및 메서드에 대해 런타임 시 시 수행되는 바인딩을 동적 바인딩이라고 한다.
부모 - 자식 상속 관계에서 구현 할 수 있는 메서드 오버라이딩이 동적 바인딩의 대표적인 예시이다.
- 메서드 호출의 경우 참조 변수 타입이 아니라 메모리에 적재되는 '실제 객체 타입' 을 기준으로 실행될 메서드가 결정된다.
- 동적 바인딩의 대상 : 인스턴스 메서드, 재정의 메서드
- 동적 바인딩 대상이 안되는 것 : static, final, private, Field 변수
- 동적 바인딩의 장점 : 코드 절약, 메모리 절약, 실행시간 단축
아래의 코드를 한번 보자.
class Exam{
public void total(){
return kor + eng + math;
}
}
class FinalExam extends Exam {
@Override
public void total(){
return super.total() + com;
}
}
- FinalExam 클래스는 Exam 클래스를 is-a 방식으로 상속받고, total() 메서드를 오버라이드 하여 기능을 추가로 구현했다.
여기서 임의의 클래스에서 아래처럼 메서드를 호출하게 된다면 어떤 객체의 메서드가 호출되게 될까?
public void print(Exam exam){
System.out.println(exam.total());
}
위의 메서드에서 파라미터로 전달된 객체에 따라 호출되는 메서드가 달라지게 된다. 파라미터로 전달되는 인스턴스가 Exam 일 수도, FinalExam 일 수도 있기 때문이다.
위의 메서드를 가지는 클래스는 자신이 어떤 인스턴스를 전달받을지 모른다. 프로그램이 실행되고 위의 메서드가 호출되면 그 때 알 수 있다.
이와 같이 런타임 시 호출될 메서드가 결정되는 것을 동적 바인딩이라고 한다.
조금 다른 예시를 들어보자.
class School{
public void ringBell(){
System.out.println("Ringing the school bell...");
}
}
class Classroom extends School {
@Override
public void ringBell(){
System.out.println("Ringing the classroom bell...");
}
}
public static void main(String[] args){
School school = new Classroom();
school.ringBell();
// new 키워드를 통해 실제 메모리에 적재되는 Classroom 객체의 ringBell 메서드가 실행된다.
}
위의 코드에서 main 메서드를 실행시켜 보면 정적 바인딩과는 다르게 참조 형식은 School 인데 실행 메서드는 자식 클래스인 Classroom 의 ringBell 메서드가 실행된 것을 확인할 수 있다.
그렇다면 왜 이렇게 동작하는 것일까?
자바에서 객체는 데이터 저장을 위한 메모리 외에 테이블 주소를 저장하기 위한 메모리 4Byte 를 추가로 할당받는다.(가상 메서드 테이블)
테이블 주소를 저장하기 위한 메모리에는 저장되는 객체의 클래스에 대한 각종 정보가 저장된다.(자신의 클래스 정보, 상속받은 부모 클래스의 정보 등)
오버라이딩을 통해 메서드를 재정의 했을 때, 가상 테이블에서 부모 클래스의 메서드 주소를 재정의한 메서드 주소로 매핑한다.
각 클래스가 생성되면 가장 최근에 오버라이드 된 메서드를 가리키게 된다. 만약 A a = new B(); 처럼 A 참조타입 변수에 B 인스턴스를 대입한다면 A를 기준으로 인스턴스가 생성되지만, B에서 오버라이드된 메서드도 모두 함께 생성되기 때문에 결국 a 인스턴스의 메서드는 가장 최근에 오버라이드된 B 의 메서드를 참조하게 되는것이다.
이러한 동적 바인딩을 통해 객체의 다형성을 형성할 수 있다.(실행 코드는 같아도 변수의 타입, 갯수에 따라 서로 다른 실행결과를 돌려주는 객체지향 프로그래밍의 가장 대표적인 특징)
아래의 코드들은 동적 바인딩을 구현하는 다른 예시 코드들이다.
코드를 자세히 보면 switch - case 문을 통해 매개변수로 넘어온 객체의 타입에 따라 서로 다른 타입의 객체를 생성시킴으로서 동적 바인딩을 구현하고 있는것을 알 수 있다.
public class DynamicBind {
public static void main(String[] args) {
System.out.println(" inpunt No :[강아지 1 , 야옹이 2, 오리 3, Exit et]");
Scanner sc = new Scanner(System.in);
Base base = null;
while (true) {
System.out.print("\n Choice no :");
int no = sc.nextInt();
switch (no) {
case 1:
base = new Puppy();
break;
case 2:
base = new Cat();
break;
case 3:
base = new Duck();
break;
default:
System.exit(0);
}
base.Start();
base.Stop();
}
}
}
public class DynamicBind2 {
public static void main(String[] args) {
System.out.println(" inpunt No :[강아지 1 , 야옹이 2, 오리 3, Exit et]");
Scanner sc = new Scanner(System.in);
while(true) {
System.out.println("\n Choice no : ");
int no = sc.nextInt();
Base base = switch(no) {
case 1 -> new Puppy();
case 2 -> new Cat();
case 3 -> new Duck();
default -> null;
};
if(base == null) {
System.out.println("프로그램 종료");
break;
} else
Base_Test(base);
}
}
public static void Base_Test(Base base) {
switch (base) {
case Puppy p -> {
System.out.println("Puppy 객체 생성");
}
case Cat c -> {
System.out.println("Cat 객체 생성");
}
case Duck d -> {
System.out.println("Duck 객체 생성");
}
default ->
System.out.println("해당 타입이 없습니다.");
}
// 동적 바인딩 실행
base.Start();
base.Stop();
}
}'부트캠프 > 후기 챌린지' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프] 백엔드 자바 21기 (2026.01.22) (0) | 2026.03.23 |
|---|---|
| [멋쟁이사자처럼부트캠프] 백엔드 자바 21기 (2026.01.21) (0) | 2026.03.09 |
| [멋쟁이사자처럼부트캠프] 백엔드 자바 21기 (2026.01.19) (0) | 2026.02.09 |
| [멋쟁이사자처럼부트캠프] 백엔드 자바 21기 (2026.01.16) (0) | 2026.02.08 |
| [멋쟁이사자처럼부트캠] 백엔드 자바 21기 (2026.01.15) (0) | 2026.01.22 |