WEEK 3-2: Java 객체지향(2)

ensalada.de.pollo·2025년 5월 2일

be

목록 보기
6/44

인터페이스(Interface)

인터페이스란 쉽게 말하자면 프로그램 설계에서 지켜야 할 최소한의 규칙 입니다. 클래스들이 어떤 기능을 반드시 구현해야 하는지 명확하게 정해주는 설계도 역할을 합니다. 실제 구현은 신경 쓰지 않고, 어떤 기능이 무조건 있어야 하는지만 정해주는 역할을 합니다.

왜 사용하는가?

개발하는 사람마다, 작성하는 메서드 이름을 다르게 작성할 수도 있고, 메서드를 작성하는 방식이 달라질 수도 있습니다. 이렇게 된다면 코드가 많이 복잡해질 것입니다. 이를 예방하기 위해 최소한의 규칙을 정하여 일관성을 지킵니다.

인터페이스의 적용

클래스에서 implements라는 키워드로 인터페이스를 활용할 수 있습니다. 여기서 인터페이스를 구현한 클래스를 구현체라고 부르기도 합니다.

인터페이스의 기능

다중 구현(Multi-Implementation)

클래스는 여러 가지의 인터페이스를 한 번에 구현할 수 있습니다.

interface Animal {
	void eat();
}

interface Flyable {
	void fly();
}

class Bird implements Animal, Flyable {
	/* 새는 동물이면서 날 수 있음을 표현 */
	public void eat() {
    	/* 실제 구현 코드 작성 */
    }
    public void fly() {
    	/* 실제 구현 코드 작성 */
    }
}

다중 상속(Multi-Inheritance)

인터페이스는 인터페이스끼리 상속(extends) 또한 가능합니다.

interface FlyableAnimal extends Animal, Flyable {
	void land();
    /* FlyableAnimal을 구현하려면,
    Animal, Flyable에 있는 메서드 또한 구현해야 함 */
}

인터페이스 변수

인터페이스에서는 public static final 변수, 즉, 상수만 선언할 수 있습니다.

왜 인터페이스에서 선언하는 변수는 public static final이어야만 할까?

인터페이스는 앞서 설명했듯이, 객체의 설계만을 정의하는 곳입니다. 이런 기능을 구현하라는 정도만 알려주는 역할이지, 실제로 값을 저장해주거나 상태를 관리하지는 않습니다.
따라서 클래스에서 사용하는 인스턴스 변수는 가질 수 없습니다. 변수가 객체의 소유가 아니므로 static이어야 합니다.
그리고, 규칙을 이야기하는 것이 인터페이스의 역할이기 때문에, 누구나 접근이 가능해야하므로 public이어야 합니다.
마지막으로, 여러 클래스가 이 값을 공유해야 하다 보니, 혼란을 없애기 위해서 값은 변경 불가능한 final이어야 합니다.

public static final을 생략하더라도 오류는 나지 않지만 컴파일러가 자동으로 public static final을 붙여줍니다.

물론, 변수의 선언이 가능하지만, 규칙을 정의하는 역할이기 때문에 변수를 선언하는 것은 최소화하는 것이 가장 좋을 것입니다.

캡슐화(Encapsulation)

캡슐화는 객체의 정보를 외부에서 직접적으로 접근하지 못하게 보호하는 것을 의미합니다. 캡슐화를 한다면, 객체의 내부 데이터에 접근을 쉽게 하지 못하니까 데이터에 문제가 생기는 것을 막을 수 있습니다. 즉, 중요한 데이터를 숨기면서 꼭 필요할 때에만 접근할 수 있도록 하는 것이 캡슐화를 하는 이유입니다.
여기서, 접근 제어자(Access Modifier)를 이용하여 캡슐화를 구현할 수 있습니다.

접근 제어자(Access Modifier)

접근제어자클래스 내부패키지 내부상속한 클래스전체
publicOOOO
protectedOOOX
defaultOOXX
privateOXXX

보통 아무것도 작성하지 않았을 때에는 default가 적용된다고 생각하면 됩니다.

public class Person {
	public String name; // 외부에서 접근 가능
    private String phoneNumber; // 외부에서 접근 불가능
    
    public void methodA() { ... } // 외부에서 접근 가능
    private void methodB() { ... } // 외부에서 접근 불가능
}

public class Main {
	public static void main(String[] args) {
    	Person person = new Person();
        person.name; // 가능
        person.phoneNumber; // 불가능
        person.methodA(); // 가능
        person.methodB(); // 불가능
    }
}

Getter와 Setter

이전 게시글에서 다루었던 내용입니다.
private으로 지정한 요소들은 외부에서 직접적으로 접근할 수 없기 때문에, 이를 간접적으로 읽거나(Getter) 쓸 수 있게(Setter) 해주는 것이 Getter와 Setter의 역할입니다.
위 예시에서 다음과 같은 메서드를 작성해보겠습니다.

public class Person {
	...
    
    public String getPhoneNumber() {
    	// private 변수 phoneNumber에 대한 Getter
    	return this.phoneNumber;
    }
    public void setPhoneNumber(String phoneNumber) {
    	// private 변수 phoenNumber에 대한 Setter
    	this.phoneNumber = phoneNumber;
    }
}

이런 식으로 작성을 하면 다음과 같은 작업이 가능합니다.

public class Main {
	public static void main(String[] args) {
    	Person person = new Person();
        
        System.out.println(person.phoneNumber); // 불가능
        System.out.println(person.getPhoneNumber); // 가능
        
        person.phoneNumber = "01012345678"; // 불가능
        person.setPhoneNumber("01012345678"); // 가능
    }
}

무분별한 Setter 사용?

그냥 setter를 열어버리면, setter를 사용하는 목적이 사라질 것입니다. 들어오면 안 되는 데이터가 들어온다는 등, 아무런 검증도 없이 값을 바꿀 수 있으면 큰 문제가 생길 수 있습니다.
이에 대한 해결법으로, setter에서 가질 수 있는 값에 대한 검증 로직을 추가하거나 setter 대신 유의미한 메서드로 값을 바꿀 수 있도록 할 수 있습니다.

상속(Inheritance)

상속은 기존 클래스의 변수와 메서드를 새로운 클래스가 물려받아 사용할 수 있게 하는 구조입니다. 여기서 기존 클래스를 부모 클래스, 새로운 클래스를 자식 클래스 라고 합니다. 자식 클래스는 부모의 속성과 기능을 그대로 재사용할 수 있고, 필요하다면, 기능을 확장하거나 부모의 메서드를 오버라이딩 할 수도 있습니다.
인터페이스는 implements 키워드를 이용하여 구현을 하였고, 상속은 extends 키워드를 사용하여 구현합니다.

상속의 장점

  • 재사용성: 부모 클래스의 코드를 자식 클래스에서 그대로 사용할 수 있어서 코드의 중복을 막을 수 있고 효율적으로 코드를 작성할 수 있습니다.
  • 확장성: 자식 클래스에서는 부모 클래스의 기능을 확장할 수 있다고 하였습니다.
  • 유지보수성: 공통된 기능을 부모 클래스에 두고, 수정이 필요할 때에는 부모 클래스에 있는 기능을 수정하면 됩니다.
  • 추상화, 다형성: 상속을 통해서 추상 클래스, 다형성과 같은 특징 또한 활용할 수 있습니다.

super 키워드

클래스에서 객체 자신의 멤버에 접근할 때 this라는 키워드를 사용한다고 하였습니다. 자식 클래스는 부모 클래스의 변수나 메서드를 가져와서 사용하기 때문에, 변수명이나 메서드 이름에서 혼선이 발생할 수 있습니다(어떤 변수를 부를 때, 자식 클래스의 변수인지, 부모 클래스의 변수인지 모름).
이를 해결하기 위해서 자식 클래스에서 부모 클래스의 멤버에 접근을 할 때에 super 키워드를 붙입니다.
특히, super() 는 부모 클래스의 생성자를 명시적으로 호출할 때 사용하며, 항상 생성자의 첫 번째 줄에 위치해야 합니다.

class Parents {
	private String name = "A";

class Child extends Parents {
	private String name = "B";
    
    public void example() {
    	System.out.println(this.name); // "B"
        System.out.println(super.name); // "A"
    }
    
    public Child {
    	super(); // 부모의 생성자 먼저 호출
        ...
    }
}

오버라이딩(Overriding)

부모로부터 상속받은 메서드를 자식 클래스에서 재정의하는 것을 오버라이딩(overriding)이라고 합니다. 메서드의 이름, 매개변수, 반환타입이 완전히 동일해야 오버라이딩이라고 부를 수 있습니다.
접근 제어자는 부모에서보다 더 강한 수준의 접근 제어자만 사용할 수 있습니다.
여기서, 오버라이딩을 했을 때 실수를 줄이기 위해서 @Override라는 어노테이션을 사용하여 컴파일러에게 확인을 받을 수 있습니다.

class Parents {
	public void helloFamily() {
    	System.out.println("Parents");
    }
}

class Child extends Parents {
	@Override // 컴파일 시 부모 클래스에 해당 메서드가 존재하는지 확인
    protected void helloFamily() { // 더 강한 수준의 접근 제어자로 수정 가능
    	System.out.println("Child");
    } // 자식 객체에서 helloFamily를 호출하면, 이 메서드가 호출됨
    
    @Override
    public String helloFamily() {
    	return "Child";
    } // Error! 부모 클래스에 있는 helloFamily 메서드의 반환 타입은 void임
}

추상 클래스(Abstract Class)

추상클래스는 공통 기능을 제공하며, 하위 클래스에 특정 메서드 구현을 강제합니다.
abstract 키워드로 선언하고, 추상 메서드는 반드시 자식 클래스에서 구현해야 합니다.
그리고, 직접 객체로 생성할 수 없습니다.

abstract class Animal {
	private String name;
    abstract void eat();
    public void sleep() {
    	/* 구현 내용 */
    }
}

class Cat extends Animal {
	@Override
    void eat() {
    	/* 구현 내용 */
    }
}

추상 클래스 vs. 인터페이스

  • 추상클래스: 변수기능을 물려주면서, 계층적인 구조를 형성할 때 사용합니다.
  • 인터페이스: 서로 다른 계층의 클래스가 특정 기능을 보장해야할 때 사용합니다.

추상화(Abstraction)

특정 계층에서 필요한 본질적인 특성만 유지하고, 불필요한 세부사항을 숨기는 것을 추상화라고 합니다.
현실세계는 복잡하지만, 여기서 사물이나 어떤 개념의 공통적이고 중요한 속성과 기능만 추출해서 프로그래밍에 활용되는 것을 의미합니다.

인터페이스 또는 추상 클래스를 통해서 구현하며, 각각의 특징에 맞게 상황에 맞추어 사용할 수 있습니다.

다형성(Polymorphism)

하나의 타입(부모 타입, 인터페이스 등)으로 여러 가지 객체(서브 클래스, 구현체)를 다룰 수 있는 것을 다형성이라고 합니다.
코드에서는 같은 타입으로 보이지만, 실제로는 다양한 객체가 동작할 수 있습니다.
말로 설명하면 이게 뭐지? 싶습니다.

Animal animal = new Cat(); // Animal 타입으로 Cat 객체를 다룬다.

이런 식으로, 타입은 부모 타입으로 두되, 자식 객체를 다루는 것을 의미합니다.

다형성의 핵심

  • 상속으로 추상 계층을 만들고.
  • 부모 타입으로 여러 자식 객체를 다룬다.

형변환(Casting)

다형성에서 중요한 개념은 형변환입니다.

업캐스팅(UpCasting)

자식에서 부모 타입으로 변환되는 것을 의미합니다. 이는, 자동적으로 수행이 됩니다.

Animal animal = new Cat(); // cat 객체가 Animal로 형변환
animal.eat(); // 가능
animal.scratch(); // 불가능(cat 객체의 고유 기능)

다운캐스팅(DownCasting)

부모에서 자식 타입으로 변환되는 것입니다. 업캐스팅과 다르게 명시적으로 캐스팅을 해야합니다.

Animal animal = new Cat();
Cat cat = (Cat) animal; // 명시적인 다운캐스팅
cat.scratch(); // 가능

다운 캐스팅이 잘못 되었을 때는, 컴파일러가 잡아주지 못하고 런타임 에러(ClassCastException)이 발생할 수 있습니다.
이를 방지하기 위해 다운 캐스팅 전에는 타입 체킹이 필요합니다.

if (animal instanceof Cat) { // instanceof라는 연산자 활용!
	Cat cat = (Cat) animal;
   	cat.scratch();
} else {
	System.out.println("고양이가 아닙니다.");
}

다형성의 장점

  • 코드 유연성: 부모 또는 인터페이스라는 한 가지 타입으롱 여러 가지 객체를 다룰 수 있습니다. 특히, 배열이나 컬렉션 같은 자료구조를 사용할 때, 다양한 객체를 담아 처리할 수 있습니다.
  • 확장성: 새로운 자식 클래스가 추가되어도 기존 코드를 변경하지 않아도 됩니다.
Animal[] animals = {new Cat(), new Dog()}; // 모두 Animal의 자식 클래스이므로 가능

for (Animal animal : animals) {
	animal.makeSound(); // 각 객체에 맞는 기능이 실행됨
}

자료 및 코드 출처: 스파르타 코딩클럽

0개의 댓글