TIL-25.12.04

이준연·2025년 12월 4일

학습 키워드

  • 객체지향
  • 실습과제
  • 트러블슈팅
  • 자바개념확장

객체지향

캡슐화

  • 객체의 정보를 외부에서 직접 접근하지 못하게 보호하는 개념입니다.
  • 캡슐처럼 감싸서 내부를 보호하고 외부로부터 내용물을 숨기고 있는 모습에서 유래됐습니다.
  • 클래스 혹은 객체의 캡슐화는 접근 제어자를 통해서 구현할 수 있습니다.

접근제어자

  • 클래스, 변수, 메서드, 생성자의 접근 범위를 제한하는 키워드입니다.
  • 캡슐화 구현을 위해 사용됩니다.
접근제어자클래스 내부패키지 내부상속한 클래스전체공개
public
protected
default
private

접근제어자 활용

public class Person { // ✅ 외부에서 접근 가능
 	
    public String name; // ✅ 외부에서 접근 가능
 	private String secret; // ❌ 외부에서 접근 불가
 
 	public Person() {} // ✅ 외부에서 접근 가능
 	
    public void methodA() {} // ✅ 외부에서 접근 가능
 	private void methodB() {} // ❌ 외부에서 접근 불가
}
Person person = new Person(); // ✅ 접근가능 생성자가 public
person.name; // ✅ 접근가능 변수가 public
person.secret; // ❌ 접근불가능 변수가 private
person.methodA(); // ✅ 접근가능 메서드가 public
person.methodB(); // ❌ 접근불가능 메서드가 private

데이터 접근

  • 캡슐화가 잘 적용된 클래스는 내부 데이터를 private 로 보호하고 있습니다.
  • 데이터 조회나 변경이 필요한 경우 안전한 접근방법이 필요합니다.
  • 그 역할을 수행하는 메서드가 바로 게터와 세터입니다.

게터 활용법

  • 데이터를 안전하게 접근하기 위해 사용됩니다.
public class Person { 
 	private String secret;
 
 	public String getSecret() {
 		return this.secret; // ✅ 객체의 secret 속성 반환
 	}
}
public class Main {
 	public static void main(String[] args) {
 		Person p1 = new Person();
 		p1.secret; // ❌ 직접 접근 불가능
 		String newSecret = p1.getSecret(); // ✅ 게터를 활용해 접근가능
 	}
}

세터 활용법

  • 데이터를 안전하게 설정/변경하기 위해 사용됩니다.
public class Person { 
 	private String secret;
 
 	public void setSecret(String secret) {
 		this.secret = secret; // ✅ secret 속성 설정 및 변경
 	}
}
public class Main {
	public static void main(String[] args) {
 		Person p1 = new Person();
 		p1.secret = "password"; // ❌ 직접접근, 변경 불가능
 		p1.setSecret("newPassword"); // ✅ 세터를 활용해 접근, 변경가능
 	}
}

무분별한 세터를 주의
객체의 일관성을 유지하기 힘들어지고 어디서 어떤 상태가 변경되는지 파악하기 힘들어진다. 즉, 코드의 가독성이 떨어질 수 있다.

상속

  • 클래스간의 관계를 부모(상위), 자식(하위)로 바라보는 개념입니다.
  • 이 구조를 통해 상속에서는 재사용성, 확장이 가능합니다.
  • extends 키워드를 사용해서 상속관계를 구현합니다.
  • 부모의 속성과 기능을 자식이 물려받습니다.
  • 코드 중복을 줄이고 유지보수성을 높일 수 있습니다.
  • 추상화, 다형성을 구현하는데 잘 활용됩니다.

재사용성

public class Parent {
 	
    public String familyName = "스파르탄";
 	public int honor = 10;
 
 	public void introduceFamily() {
 		System.out.println("우리 " + this.familyName + " 가문은 대대로 명성을 이
 	}
}
class Child extends Parent { // ✅ extends 키워드 활용
}
public class Main {
 
 	public static void main(String[] args) {
 	
    	Child child = new Child();
 		System.out.println(child.honor); // ✅ 부모의 속성을 물려받아 사용
 		System.out.println(child.familyName); // ✅ 부모의 속성을 물려받아 사용
 		child.introduceFamily(); // ✅ 부모의 메서드를 물려받아 사용
 	}
}

super

  • 부모클래스의 멤버(변수, 메서드)에 접근할 때 사용하는 키워드입니다.
  • 자식클래스에서 부모의 변수나 메서드를 명확하게 호출할 때 사용합니다.
public class Child extends Parent {
	
    private String familyName = "gim"
 
 	public void superExample() {
 		
        System.out.println("우리 " + this.familyName + " 가문은 ...");
 		System.out.println("원래 가문의 이름은 " + super.familyName);
 	}
}

생성 순서

  • 부모가 먼저 생성되고 자식이 생성됩니다.
  • 부모가 먼저 생성되어야 하므로 super() 는 항상 생성자의 첫 줄에 위치해야 합니다.
public class Child extends Parent {
		
        ...
 	
    public Child() {
 		
        super(); // ✅ (1)부모클래스 생성자를 먼저 호출
 		// 추가로직은 여기에 작성
 	}
}
public class Parent {
 
 	public Parent() {} // ✅ 부모 생성자
}

확장

  • 부모클래스의 기능을 유지하면서 자식클래스에서 기능을 확장할 수 있습니다.
  • 자식클래스에서 새로운 메서드를 추가하면 됩니다.
public class Child extends Parent {

		...

	// ✅ 부모에는 없지만 자식에만 있는 기능
 	public void showSocialMedia() {
 		
        System.out.println("우리 가문은 이제 SNS도 합니다. 팔로우 부탁드려요!");
 	}
}
public class Main {

	public static void main(String[] args) {
 
 		...
 
 	child.showSocialMedia(); // ✅ 부모에는 없지만 자식에만 있는 기능
 }
}

재정의

  • 부모 메서드를 자식클래스에서 변경하여 재정의하는 것을 의미합니다.
  • 재정의된 메서드에는 @Override 키워드를 붙이는 것을 권장합니다.
  • 메서드 이름, 매개변수, 반환타입이 완전히 동일해야합니다.
  • 접근 제어자는 부모보다 더 강한 수준으로만 변경 가능합니다.
public class Parent {

	// 기존 기능
 	public void introduceFamily() {
 
 		System.out.println("우리 " + familyName + " 가문은 대대로 명성을 이어온 집안입니다.");
 }
}
class Child extends Parent {

		...
 
		@Override
 	void introduceFamily() { // ✅ 자식클래스에서 재정의
 
 		System.out.println("오버라이드");
 	}
}
public class Main {
 
 	public static void main(String[] args) {
 
 		Child child = new Child();
        
 		child.introduceFamily(); // ✅ 출력 "오버라이드"
 	}
}

추상클래스

  • 공통 기능을 제공하면서 하위 클래스에 특정 메서드 구현을 강제하기 위해 사용됩니다.
  • 객체를 생성할 목적이 아니라 설계도 역할을 할 때 적합합니다.
  • abstract 키워드로 클래스를 선언하면 추상클래스입니다.
  • abstract 키워드로 메서드를 선언하면 자식클래스에서 강제로 구현해야합니다.
  • 추상클래스로 객체를 생성할 수 없습니다.
  • 일반 클래스처럼 변수와 메서드를 가질 수 있습니다.
abstract class Animal {
 
 	private String name; // ✅ 변수선언가능
 	
    abstract void eat(); // ⚠️ 추상메서드: 상속 받은 자식은 강제 구현해야합니다.
 
 	public void sleep() { // ✅ 자식클래스에서 재사용가능합니다.
 		
        System.out.println("쿨쿨");
 }
}
public class Cat extends Animal {
 
 		@Override
 	void eat() {
 
 		System.out.println("냠냠"); // ⚠️ 자식클래스에서 강제 구현해야합니다.
 	}
}
public class Main {
 
 	public static void main(String[] args) {
 
 		Animal animal = new Animal(); // ❌ 추상클래스는 구현할 수 없습니다.
 
 		Cat cat = new Cat();
	 	cat.eat(); // ⚠️ 강제 구현한 메서드 사용
 		cat.sleep(); // ✅ 부모클래스의 매서드 사용
 	}
}

추상클래스와 인터페이스 차이점

  • 상속이 계층적 구조를 선언하기 적합합니다.
  • 인터페이스는 표준을 제공하는 데 적합합니다.
  • 인터페이스는 객체 변수를 선언할 수 없습니다.
  • 계층적 구조를 표현하면서 공통 속성과 기능을 재사용할 때 추상클래스를 사용하는 것이 적합합니다.

추상화

  • 불필요한 정보를 제거하고 본질적인 특징만 남기는 것을 의미합니다.
  • 객체지향 프로그래밍에서는 추상화의 계층적 특징을 활용해서 유지보수성이 좋은 프로그램을 만들 수 있습니다.

인터페이스 상속을 활용한 추상 계층 표현

public interface LifeForm {
 
 	void exist(); // ✅ 공통: 모든 생명체는 존재한다.
}
public interface Animal extends LifeForm {
 
 	void makeSound(); //✅ 공통: 모든 동물은 소리를 냅니다.
}
public class Cat implements Animal {
 
 		@Override
 	public void exist() {
 
 		System.out.println("고양이가 존재합니다.");
 	}
 
 		@Override
 	public void makeSound() {
 
 		System.out.println("야옹");
 	}
 
 	public void scratch() {
 		
        System.out.println("스크래치");
 	}
}
public class Main {
 
 	public static void main(String[] args) {
 
 		Cat cat = new Cat();
 
 		cat.exist();
 		cat.makeSound();
 		cat.scratch();
 	}
}

클래스 상속을 활용한 추상 계층 표현

public class LifeForm {
 
 	public void exist() {
 		
        System.out.println("존재합니다2"); // ✅ 공통: 모든 객체는 존재한다.
	}
}
public class Animal extends LifeForm {
 
 	public void makeSound() {
 
 		System.out.println("소리를 냅니다2"); // ✅ 공통: 모든 생명체는 성장한다.
 	}
}
public class Cat extends Animal {
 
 		@Override
 	public void makeSound() {
 
 		System.out.println("야옹2");
 	}
 
 	public void scratch() {
 	
    	System.out.println("스크래치!");
 	}
}
public class Main {
 
 	public static void main(String[] args) {
 
 		Cat cat = new Cat();
 
 		cat.exist();
 		cat.makeSound();
 		cat.scratch();
 	}
}

다형성

  • 다형성은 하나의 타입으로 여러 객체를 다룰 수 있는 객체지향의 특징입니다.

인터페이스를 활용한 다형성 구현

  • LifeForm.java
public interface LifeForm {

	void exist();
}
  • Animal.java
public interface Animal extends LifeForm {
 
 	void makeSound();
}
  • Cat.java
public class Cat implements Animal {
 
 		@Override
	public void makeSound() {
 
 		System.out.println("야옹");
 	}
 
 		@Override
 	public void exist() {
 
 		System.out.println("고양이가 존재합니다.");
 	}
 
 	public void scratch() {
 
 		System.out.println("스크래치!");
 	}
}
  • Dog.java
public class Dog implements Animal {
 
 		@Override
 	public void makeSound() {
 
 		System.out.println("멍멍");
 	}
 
 		@Override
 	public void exist() {
 		
        System.out.println("강아지가 존재합니다.");
 	}
 
 	public void wag() {
 
 		System.out.println("꼬리 흔들");
 	}
}
public class Main {
 
 	public static void main(String[] args) {
 
 		// 다형성 활용
 		Animal animal = new Cat();
 
 		animal.exist(); // ✅
		animal.makeSound(); // ✅
 	}
}

형변환

  • 부모타입으로 자식타입을 다룰 수 있는 이유는 자동으로 형변환이 발생했기 때문입니다.
  • 자식타입 → 부모타입: 업캐스팅
  • 부모타입 → 자식타입: 다운캐스팅

업캐스팅

public class Main {
 
 	public static void main(String[] args) {
 
 		// 다형성 활용
 		Animal animal = new Cat(); // ✅ 자동 형 변환
 
 		animal.exist(); // ✅
 		animal.makeSound(); // ✅
 	}
}

업캐스팅 주의사항

  • 업캐스팅은 부모의 타입으로 데이터를 다룰 수 있지만 자식클래스의 고유기능을 활용할 수 없습니다.
  • 자식클래스의 고유 기능을 사용하려면 다운캐스팅이 필요합니다.
public class Main {

	public static void main(String[] args) {
 
 		// 다형성 활용
 		Animal animal = new Cat(); // ✅ 자동 형 변환
 	
    	animal.exist();
 		animal.makeSound();
 		animal.scratch(); // ❌ 사용 불가
 	}
}

다운캐스팅

public class Main {
 
 	public static void main(String[] args) {
 
 		// 다형성 활용
 		Animal animal = new Cat();
 
 		animal.exist();
 		animal.makeSound(); 
 		Cat cat = (Cat) animal; // ✅ 다운캐스팅(부모Animal -> 자식Cat)
 		cat.scratch(); // ✅ 자식 클래스의 기능 활용 가능
 	}
}

다운캐스팅 주의사항

  • 컴파일러는 다운캐스팅이 문법적으로 올바른지 여부만 검사해주기 때문에 런타임시에 실제 어떤 객체가 변수에 할당되는지 검사해주지 않습니다.
  • 컴파일 시점에는 오류 없이 통과되지만 런타임시점에 ClassCastException가 발생할 가능성이 있습니다.
public class Main {
 
 	public static void main(String[] args) {
 
 		// 다운 캐스팅
 		Animal dog = new Dog();
 
 		// 문법적으로 잘못된건 아니라서 에러가 발생하지 않습니다.
 		Cat cat1 = (Cat) dog; // ⚠️
 		cat1.scratch(); // ❌ 해당 라인이 실행할때만 에러 여부를 확인할 수 있습니다
 	}
}

그래서 다운캐스팅을 사용할 때 instanceof 를 활용해야합니다.

  • instanceof 는 객체가 특정 클래스나 인터페이스의 인스턴스인지 확인해주는 역할을 합니다.
  • 주로 다운캐스팅 하기 전에 타입을 검사해서 ClassCastException을 예방하는데 활용됩니다.
public class Main {

	public static void main(String[] args) {
 
 		Animal animal2 = new Dog();
 
 		// ✅ 안전한 다운캐스팅(animal2 가 Cat 의 인스턴스 유형인지 확인합니다.)
 		if (animal2 instanceof Cat) {
 		
        	Cat cat = (Cat) animal2;
 			
            cat.scratch();
 		} else {
 			
            System.out.println("객체가 고양이가 아닙니다.");
 		}
 	}
}

한 가지 타입으로 유연한 프로그래밍 처리

public class Main {
 
 	public static void main(String[] args) {
 
 		Animal[] animals = {new Cat(), new Dog()};
 
 		for (Animal animal : animals) {
 
 			animal.makeSound();
 		}
 	}
}

참조 타입을 Animal[] 배열로 선언하고 배열안에 Cat, Dog의 객체를 모두 담았습니다. animal[0]부터 순차실행합니다.


실습과제

인터페이스

  • 인터페이스
    • 다양한 객체를 일관된 방식으로 동작하는 인터페이스를 설계
      • 객체의 종류: Tv, 에어컨, PC
      • turnOn() → 기기를 켜는 기능
      • turnOff() → 기기를 끄는 기능
      • 각 객체의 기능 확장
package com.example.interfacetest;

interface ElectronicDevices {

    void turnOn();
    void turnOff();
}

class Computer implements ElectronicDevices {

    @Override
    public void turnOn() {
        System.out.println("Computer turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("Computer turned off");
    }

    void calculation() {
        System.out.println("Computer calculation");
    }
}

class Television implements ElectronicDevices {

    @Override
    public void turnOn() {
        System.out.println("Television turned on");
    }

    @Override
    public void turnOff() {
        System.out.println("Television turned off");
    }

    void watchingTv() {
        System.out.println("Watching tv");
    }
}

class AirConditioner implements ElectronicDevices {

    @Override
    public void turnOn() {
        System.out.println("Air conditioner turned on");
    }

    @Override
    public void turnOff(){
        System.out.println("Air conditioner turned off");
    }

    void keppCool() {
        System.out.println("Air conditioner kepp cool");
    }
}



public class Interfacetest {

    public static void main(String[] args) {

        // 객체 선언
        Computer computer = new Computer();
        Television television = new Television();
        AirConditioner airConditioner = new AirConditioner();
        
        // 기능 출력
        computer.turnOn();
        computer.turnOff();
        computer.calculation();

        television.turnOn();
        television.turnOff();
        television.watchingTv();

        airConditioner.turnOn();
        airConditioner.turnOff();
        airConditioner.keppCool();
    }
}

위는 내가 실습한 코드이다. 코드는 이상없이 작동하지만 main클래스의 작성이 효율적이지 못하다. 이를 해결하기 위해 상위 형으로 객체를 선언하여 인터페이스 메소드를 작성하고, if문에 instnceof 를 사용하여 형 확인 후 새 변수에 할당, 고유의 메서드를 호출하면 아래와 같은 코드가 완성된다.

public class Interfacetest {

    public static void main(String[] args) {

        // 배열 선언
        ElectronicDevices[] devices = {
            new Computer(),
            new Television(),
            new AirConditioner()
        };

        // 공통 기능
        for (ElectronicDevices device : devices) {
            device.turnOn();
            device.turnOff();
            System.out.println();
        }

        // 고유 기능
        for (ElectronicDevices device : devices) {
            if (device instanceof Computer computer) {
                computer.calculation();
            } else if (device instanceof Television television) {
                television.watchingTv();
            } else if (device instanceof AirConditioner airConditioner) {
                airConditioner.keppCool();
            }
        }
    }
}

객체지향

  • 추상화
    • 스스로 주제를 정하여 인터페이스와 클래스를 활용하여 3단계 이상의 추상 계층 구현
package com.example.abstraction;

interface Book {
    void storyTelling();
}

interface Novel extends Book {
    void onlyText();
}

class HarryPotter implements Novel {

    @Override
    public void storyTelling() {
        System.out.println("HarryPotter is story telling");
    }

    @Override
    public void onlyText() {
        System.out.println("HarryPotter is only text");
    }

    public void popular() {
        System.out.println("HarryPotter is popular");
    }
}

class Pilgrim implements Novel {

    @Override
    public void storyTelling() {
        System.out.println("Pilgrim is story telling");
    }

    @Override
    public void onlyText() {
        System.out.println("Pilgrim is only text");
    }

    public void outOfDate() {
        System.out.println("Pilgrim is out of date");
    }
}

class Game {
    public void fun() {
        System.out.println("Game is fun");
    }
}

class OnlineGame extends Game {
    public void multiPlaying() {
        System.out.println("OnlineGame is multi playing");
    }
}

class LeagueOfLeagues extends OnlineGame {

    @Override
    public void multiPlaying() {
        System.out.println("LeagueOfLeagues is multi playing");
    }

    public void rank() {
        System.out.println("Silver");
    }
}



public class abstraction {

    public static void main(String[] args) {

        Novel[] novels = {new HarryPotter(), new Pilgrim()};
        LeagueOfLeagues lol = new LeagueOfLeagues();

        lol.fun();
        lol.multiPlaying();
        lol.rank();

        for(Novel novel : novels) {
            novel.storyTelling();
            novel.onlyText();
        }

        for(Novel novel : novels) {
            if(novel instanceof HarryPotter) {
                ((HarryPotter) novel).popular();
            } else if(novel instanceof Pilgrim) {
                ((Pilgrim) novel).outOfDate();
            }
        }

    }
}

  • 다형성
    • 클래스를 활용해서 3단계 계층 구조 설계
      • 다형성 활용
      • 업캐스팅&다운캐스팅 구현
      • 다운캐스팅시 instanceof 를 사용

추상화 실습을 진행하다 보니 다형성을 만족하는 코드를 완성하여 따로 실습을 진행하지 않음.


트러블슈팅

인터페이스
업로드중..

  • 인터페이스 메서드는 public 으로 선언하였지만 구현화 메서드에서는 default 로 선언하여 접근권한의 범위가 좁아졌습니다.
  • 오버라이드 시에는 부모와 같거나 더 넓은 접근제어자만을 사용해야하는 규칙이 있기에 이를 준수하여야 합니다.
  • 구현화 메서드를 public 으로 선언하여 해결하였습니다.

자바개념확장

예외처리

예외란

  • 프로그램 실행 중 예상하지 못한 상황의 발생입니다.

예외의 구조와 종류

  • RuntimeException - UncheckedException
    • RuntimeException 을 상속받는 모든 예외를 UncheckedException 이라고 합니다.
    • 예외처리를 컴파일러가 확인하지 않습니다.
    • 예외처리가 강제되지는 않지만 처리되지 않으면 프로그램이 비정상적으로 종료됩니다.
  • Exception - CheckedException
    • Exception 클래스를 직접 상속받는 모든 예외를 CheckedException 이라고합니다. RuntimeExceptionRuntimeException 을 상속받은 예외는 제외합니다.
    • 예외처리를 컴파일러가 확인해 줍니다.
    • 예외처리가 강제됩니다.

예외처리

  • try-catch 활용
public class ExceptionPractice {

    public void callCheckedException() {
		    // ✅ try-catch 로 예외 처리
        try { 
            if (true) {
                System.out.println("체크예외 발생");
                throw new Exception();
            }
        } catch (Exception e) {
            System.out.println("예외 처리");
        }
    }
}
  • throws 활용
public class ExceptionPractice {

    public void callCheckedException() throws Exception { // ✅ throws 예외를 상위로 전파
        if (true) {
            System.out.println("체크예외 발생");
            throw new Exception();
        }
    }
}

Optional

Optional이란

  • null 을 안전하게 다루게 해주는 객체입니다.

Optional 사용 이유

  • null 을 반환할 수 있는 메서드의 값이 채워지지 않아 null 을 반환하면 NPE(NullPointerException) 가 발생합니다.
  • NPE(NullPointerException) 은 런타임 예외이고 컴파일러가 잡아주지 못합니다.
  • 예외처리를 하지 못하면 프로그램이 비정상적으로 종료됩니다.

Optional 활용

  • null 이 반환될 가능성을 전달할 수 있습니다.
  • isPresent() 같은 Optional API로 안전한 처리를 할 수 있습니다.

isPresent 활용

  • Optional 내부의 값이 존재하면 true 아니면 false 를 반환합니다.
import java.util.Optional;

public class Camp {

    // 속성
    private Student student;

    // 생성자

    // 기능
    // ✅ null 이 반환될 수 있음을 명확하게 표시
    public Optional<Student> getStudent() {
        return Optional.ofNullable(student);
    }
    
    public void setStudent(Student student) {
		    this.student = student;    
    }
}
public class Main {

    public static void main(String[] args) {

        Camp camp = new Camp();
        
        // isPresent() 활용시 true 를 반환하고 싶을때 활용
        // Student newStudent = new Student();
        // camp.setStudent(newStudent);

        //  Optional 객체 반환받음
        Optional<Student> studentOptional = camp.getStudent();

        // Optional 객체의 기능 활용
        boolean flag = studentOptional.isPresent(); // false 반환
        if (flag) {
            // 존재할 경우
            Student student = studentOptional.get(); // ✅ 안전하게 Student 객체 가져오기
            String studentName = student.getName();
            System.out.println("studentName = " + studentName);

        } else {
            // null 일 경우
            System.out.println("학생이 없습니다.");
        }
    }
}

orElseGet 활용

  • 값이 없을 때 기본값을 제공하는 로직을 실행하는 메서드입니다.
  • 메서드를 매개변수로 전달합니다.
import java.util.Optional;

public class Camp {

    // 속성
    private Student student;

    // 생성자

    // 기능
    // ✅ null 이 반환될 수 있음을 명확하게 표시
    public Optional<Student> getStudent() {
        return Optional.ofNullable(student);
    }
}
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Camp camp = new Camp();

        // ✅ Optional 객체의 기능 활용 (orElseGet 사용)
        Student student = camp.getStudent()
                              .orElseGet(() -> new Student("미등록 학생"));

        System.out.println("studentName = " + student.getName());
    }
}
profile
반갑습니다!

0개의 댓글