JAVA/메타코딩

4-객체지향

브리오 2025. 4. 8. 17:09

클래스

클래스=class=설계도

//클래스 = 여러가지 상태(특징)을 가지고 있다.
public class Dog {
    int age = 20;
    String name = "a";
    final String color = "b";   //변경 불가능한 상수
}// =   상태  =   필드  =   전역변수
public class DogApp {
    public static void main(String[] args) {
        Dog d = new Dog();
        System.out.println(d.age);
        System.out.println(d.name);
        System.out.println(d.color);
        d.age+=1;
        d.name="aa";
        //d.color"bb";  불가
    }
}

생성자

//값을 초기화 하지 않는 이유는 new 할때마다 새로운 값을 주기 위해서다.
//초기화 안하면 null
public class Cat {
    //여기서 초기화 하지말고, 생성자를 통해서 초기화 하자
    String name;
    String color;

    public Cat() {
        //디폴트 생성자, 원래는 개발자가 생략해도 된다
        //만약 생성자를 임의로 추가하면 디폴트 생성자는 필수다.!!
        System.out.println("디폴트 생성자");
    }
    public Cat(String name,String color) {
        //매개변수는 stack에 존재
        //개발자가 생성자 구현함 = 디폴트 생성자 추가해줘야 한다.
        System.out.println(name+color);
        //stack에 있는 값을 heap으로 옮겨 오랫동안 보관
        this.name = name;
        this.color=color;
    }
}
public class CatApp {
    public static void main(String[] args) {
        Cat c1 = new Cat("n1","n2");
        System.out.println(c1.name);
        System.out.println(c1.color);
        Cat c2= new Cat();
    }
}

this

public class People {
    String name;
    int age;
    public People(String name, int age){
        System.out.println("People 메서드 스택 name :"+name);
        System.out.println("People 메서드 스택 age :"+age);
        this.name=name;
        this.age=age;
    };
}
public class PeopleApp {
    public static void main(String[] args) {
        People p1 = new People("a",1);
    }
}

클래스,오브젝트,인스턴스

클래스 : 신이 가지고 있는 설계도 = .java 파일

오브젝트 : new가 가능한것

클래스 -new-> heap에 존재 = 인스턴스

 

Person a = new Person(); -> 객체 생성 동시에 인스턴스화

Person a; -> 객체 생성

a = new Person(); ->인스턴스

클래스의 상태와 행위

oop의 원칙 : 클래스의 필드는 메서드에 의해서 변한다

실수 방지를 위해서 클래스 필드에는 private를 꼭 붙여주자

class Player {
    String name;
    private int thirsty;
    public Player(String name, int thirsty) {
        this.name = name;
        this.thirsty = thirsty;
    }
    void 물마시기(){    //행위 = 메서드 = 값을 변경
        this.thirsty-=50;
    }
    int 목마름상태확인(){
        return this.thirsty;
    }
}
public class OOPEx01 {
    public static void main(String[] args) {
        Player p1 = new Player("a", 100);
        System.out.println(p1.name);
        // 마법 : 원인없이 변화 발생
        // p1.name="b";
        // 실수 가능한 코드
        // p1.물마시기();
        // p1.thirsty=50
        p1.물마시기();
        System.out.println(p1.목마름상태확인());
    }
}

상속

1. 추상화

2. 상태, 행위를 물려 받을 수 있다. <=> import : 상태,행위 가져와서 쓴다.

 

자동차는 엔진을 상속 할 수 없다 = 타입 일치 불가

치즈햄버거는 햄버거(추상화된 존재)를 상속 받는다 = 타입 일치 가능

치킨햄버거는 햄버거(추상화된 존재)를 상속 받는다 = 타입 일치 가능

 

=>치즈,치킨 햄버거는 햄버거냐? ok

=>자동차는 엔진이냐? x

class Engine{
    int power =2000;
}
class Car{ // 자동차는 엔진이 아니니까 상속 안된다.
    //컴포지션
    Engine e;

    public Car(Engine e) {
        this.e = e;
    }
}
class Hamburger{
    String name ="기본 햄버거";
    String 재료1 ="양상추";
}
// 상속은 상태와 행위를 물려 받을 수 있지만 꼭 타입이 일치 되어야 한다.!!
class CheeseHamburger extends Hamburger{    //치즈 햄버거는 햄버거이다.!!
    //겹치지 않은 상태(필드)만 물려 받는다.
    String name ="치즈 햄버거";
}
class ChickenHamburger{
    String name = "치킨햄버거";
    //이건 컴포지션
    Hamburger h;
    public ChickenHamburger(Hamburger h) {
        this.h = h;
    }
}
public class OOPEx02 {
    public static void main(String[] args) {
        Engine e = new Engine();
        Car c1 = new Car(e);
        System.out.println(c1.e.power);

        Hamburger h1 = new CheeseHamburger();
        System.out.println(h1.name);

        Hamburger h2 = new Hamburger();
        ChickenHamburger ck1 = new ChickenHamburger(h2);
        System.out.println(ck1.name);
    }
}

 

다형성

class 요리사{
    String name ="요리사";
}
class 홍길동 extends 요리사{
    String name="홍길동";
}
public class OOPEx03 {
    public static void main(String[] args) {
        홍길동 h1 = new 홍길동(); //홍길동, 요리사 동시에 heap에 존재, 바라보는건 홍길동
        System.out.println(h1.name);
        
        요리사 y1 = new 홍길동(); //홍길동, 요리사 동시에 heap에 존재, 바라보는건 요리사
        System.out.println(y1.name);
        
        //홍길동 h2 = new 요리사(); -> 이건 안된다, 요리사를 heap에 띄우면 요리사만 존재하기 때문에
    }
}

오버로딩, 오버로딩의 한계

Overloading = 과적재, 함수이름이 같아도 다른 함수로 인식한다, 매개변수가 다르니까

class 임꺽정{
    void 달리기(){
        System.out.println("달리기1");
    }
    //과적재=overloading 사용
    void 달리기(int speed){
        System.out.println("달리기2");
    }
}
public class OOPEx04 {
    public static void main(String[] args) {
        임꺽정 e = new 임꺽정();
        e.달리기();
        e.달리기(1);
    }
}

오버로딩 안하면 함수 이름, 매개변수 계속 기억해야한다.

오버로딩 하면 그냥 동일한 함수 사용하고 매개변수만 변경하면 된다.

그러나 오버로딩은 갯수가 적을때는 되지만 많은 수 앞에서는 한계가 명확하다 - 매번 매개변수 변경하며 함수 생성해야한다

package ch05;
class 전사{//검
    String name ="전사";
    void 기본공격(궁수 e1){
        System.out.println(e1.name+"검으로 공격");
    }
    void 기본공격2(광전사 e1){
        System.out.println(e1.name+"검으로 공격");
    }
}
class 궁수{//활
    String name ="궁수";
    void 기본공격(광전사 e1){
        System.out.println(e1.name+"활로 공격");
    }
    void 기본공격(전사 e1){
        System.out.println(e1.name+"활로 공격");
    }
}
class 광전사{//도끼
    String name ="광전사";
}

public class OOPEx05 {
    public static void main(String[] args) {
        전사 u1 = new 전사();
        궁수 u2 = new 궁수();
        광전사 u3 = new 광전사();

        //오버로딩 안하면 할때마다 함수 이름 기억해야한다
        u1.기본공격(u2);
        u1.기본공격2(u3);

        //오버로딩 하면 기억해야할게 매개변수만 기억하면 된다.
        u2.기본공격(u3);
    }
}

오버라이딩

동적바인딩을 이용해서 내가 원하는 메소드(자식 메소드)를 실행하는 전략!!

class 프로{
    String name ="프로";
    void 공격(프로 e){
        System.out.println("프로 메서드");
    }
    String 이름확인(){
        return "프로";
    }

}
class 질럿 extends 프로{
    String name ="질럿";
    //오버 라이드 = 무효화, 부모의 메서드를 무효화 한다
    void 공격(프로 e){
        System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
    }
    //오버 라이드 = 무효화, 부모의 메서드를 무효화 한다
    String 이름확인(){
        return name;
    }
}
class 드라 extends 프로{
    String name ="드라";
    void 공격(프로 e){
        System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
    }
    String 이름확인(){
        return name;
    }
}
class 다크 extends 프로{
    String name ="다크";
    void 공격(프로 e){
        System.out.println("타깃 :"+e.이름확인()+"실행 : "+this.name);
    }
    //🔥함수명 다르게하면 제대로 오버라이딩 안된다🔥
    String 이름체크(){
        return name;
    }
}
public class OOPEx06 {
    public static void main(String[] args) {
        프로 u1 = new 질럿();
        프로 u2 = new 드라();
        프로 u3 = new 다크();

        u1.공격(u2);
        u2.공격(u3);
        u3.공격(u1);
    }
}

동적 바인딩 

부모 name = new 자식(); -> 자식, 부모 둘다 heap에 생성, 가리키는건 부모

name.method -> 부모에 있는 메서드 실행할려다가, 자식에 동일한 메서드 있으면 자식 메서드 실행

===>>>그게 바로 동적 바인

추상 클래스

추상적이다 = new 할 수 없다 = 메모리에 띄울 수 없다 = 미완성 설계

추상 클래스 = 추상 메서드, 일반 메서드 둘다 가능

추상 메서드 = 자식은 반드시 구현해야한다, 실수를 줄일 수 있다

abstract class Animal{
    //추상 메서드는 중괄호 필요없다. 어차피 안쓰이기도 하니까
    abstract void speak();
    //추상클래스 안에 추상 메서드만 있어야 하는건 아니다
    void hello(){
        System.out.println("hi");
    }
}
class Dog extends Animal{
    void speak(){
        System.out.println("멍");
    }
}
class Cat extends Animal{
    //부모가 추상메서드를 가지고 있으면 자식은 추상메서드를 반드시 상속해야한다
    //강제성이 있기 때문에 실수 할 수가 없다!!!!!!
    @Override
    void speak() {
        System.out.println("옹");
    }
}
public class OOPEx07 {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        a1.hello();
        
        //동적 바인딩!!
        a1.speak();
        //Animal a3 = new Animal(); -> 불가, 추상클래스는 new 할 수 없다.
    }
}

인터페이스

인터페이스 : 일방적인 약속(갑과 을이 존재하는 약속)

vs

프로토콜 : 상호 협의 약속(동등한 관계)

자바의 인터페이스 : 행위에 대한 제약을 준다

 

기본틀!!

인터페이스 : 다중 구현 가능! 기능 명세 제공! -> 이런 기능은 필수야!! = 설계 계약(Contract)

추상클래스 : 단일 상속 가능! 공통 기능,일부 구현! -> 공통은 내가주고 나머진 너가 채워!! = 공통 기능의 재사용

 

심화!!!! 인터페이스,추상메서드 : 같은거 아니냐??

더보기
  • 눈, 코, 날개는 기능(능력) → 인터페이스로 표현하면 딱 좋음
  • 동물은 **공통 속성(예: 이름, 종족)**과 공통 동작(예: 먹다) → 추상 클래스로 표현
  • 강아지는 날개 없음 → 날다 기능을 구현하면 안 됨
  • 그래서 인터페이스를 써야만 강아지에게 '없는 기능'을 강요하지 않게 됨

"모든 동물이 날 수는 없지만, 날 수 있는 동물은 '날 수 있다'는 능력이 있어야 하니까 인터페이스로 분리해야 한다!"

 

❌ 잘못된 구조 (비추천: 추상 클래스만 쓸 경우)

abstract class Animal {
    abstract void 뜨다();      // 눈 관련
    abstract void 숨쉬다();    // 코 관련
    abstract void 앞으로날다(); // 날개 관련
}
class Dog extends Animal {
    void 뜨다() { ... }
    void 숨쉬다() { ... }
    void 앞으로날다() { ... } // ❌ 날개 없어도 구현해야 함
}

 

✅ 올바른 구조 (인터페이스 활용)

interface Eye {
    void 뜨다();
    void 감다();
    void 깜빡이다();
}

interface Nose {
    void 숨쉬다();
    void 숨참다();
    void 냄새맡다();
}

interface Wing {
    void 앞으로날다();
    void 뒤로날다();
}

abstract class Animal {
    String name;
    abstract void 먹다();
}

🐶 강아지 클래스

class Dog extends Animal implements Eye, Nose {

    @Override
    public void 뜨다() { System.out.println("강아지가 눈을 떴다"); }

    @Override
    public void 감다() { System.out.println("강아지가 눈을 감았다"); }

    @Override
    public void 깜빡이다() { System.out.println("강아지가 눈을 깜빡인다"); }

    @Override
    public void 숨쉬다() { System.out.println("강아지가 숨쉰다"); }

    @Override
    public void 숨참다() { System.out.println("강아지가 숨참는다"); }

    @Override
    public void 냄새맡다() { System.out.println("강아지가 냄새를 맡는다"); }

    @Override
    void 먹다() { System.out.println("강아지가 먹는다"); }
}

🦅 독수리 클래스

class Eagle extends Animal implements Eye, Nose, Wing {

    @Override
    public void 뜨다() { System.out.println("독수리가 눈을 떴다"); }

    @Override
    public void 감다() { System.out.println("독수리가 눈을 감았다"); }

    @Override
    public void 깜빡이다() { System.out.println("독수리가 눈을 깜빡인다"); }

    @Override
    public void 숨쉬다() { System.out.println("독수리가 숨쉰다"); }

    @Override
    public void 숨참다() { System.out.println("독수리가 숨참는다"); }

    @Override
    public void 냄새맡다() { System.out.println("독수리가 냄새를 맡는다"); }

    @Override
    public void 앞으로날다() { System.out.println("독수리가 앞으로 난다"); }

    @Override
    public void 뒤로날다() { System.out.println("독수리가 뒤로 난다"); }

    @Override
    void 먹다() { System.out.println("독수리가 먹는다"); }
}

 

interface MoveAble {
    //제어자 안적어주면 public, abstract 2개가 생략되어 있다.
    void 위();
}
interface MoveAble2 {
    //제어자 안적어주면 public, abstract 2개가 생략되어 있다.
    void 위();
    void 땅();
}
/*
만약 추상 클래스가 아닌 일반 클래스가 implement 하면 오류발생한다
interface의 메서드는 반드시 구현해줘야 하는데
추상 클래스와 달리 일반 클래스 이미 완성되어 있기 때문에 implement 할 수 없다.

따라서 추상클래스가 interface를 impelments하면
자식 클래스가 interface의 메서드를 구현해야 하는데,
추상클래스가 대신해서 구현하면 상관없다.
 */
//추상 클래스가 자식 클래스 대신 interface 메서드를 구현한 예시
abstract class 사나운 implements MoveAble {
    abstract void 공격();
    @Override
    public void 위() {
        System.out.println("사나운 위로");
    }
}
class 사자 extends 사나운 {
    @Override//어노테이션 : JVM이 실행시에 분석해서 확인 = JVM의 힌트
    void 공격() {
        System.out.println("사자 공격");
    }
    public void 위(){
        System.out.println("사자 위로");
    }
}
abstract class 온순한 implements MoveAble2 {
    abstract void 채집();
}
//자식 클래스가 interface의 메서드를 구현한 예시
class 소 extends 온순한 {
    @Override
    void 채집() {}
    @Override
    public void 위() {}
    @Override
    public void 땅() {}
}
public class OOPEx09 {
    /*
        이 메서드의 매개변수 타입은 사나운입니다.
        이 함수는 사나운 타입이거나 그 하위 클래스 객체를 받을 수 있습니다.
        ✅ 즉, 컴파일러는 매개변수를 받을 때 "사자냐?"가 아니라 "사나운이냐, 혹은 그 자식이냐?"를 기준으로 봅니다.
     */
    static void 조이스틱(사나운 u) {}
    public static void main(String[] args) {
        /*
            👉 기능(행위)에만 관심이 있다면, 상위 타입(추상 클래스나 인터페이스)으로 선언하세요.
            👉 사자의 고유한 기능(오직 사자만의 메서드 등)에 접근해야 한다면, 사자 타입으로 선언하세요.
         */
        사자 uu = new 사자();
        uu.공격();
    }
}

SRP,DIP

A가 B에 의존한다 = B에 변경사항이 생기면 A가 변경되야만 한것 

 

SRP = Single Response Principle (단일 책임 원칙)

책임 = 행위 = 메서드 -> 하나의 행위에 대한 책임은 한명이 갖는다

DIP = Dependency Inversion Principle가 필요한 이유

처음부터 코드를 완벽하게 만들 수 없다 -> Continuous Integration는 필수!!

즉, “상위 모듈도, 하위 모듈도 모두 구체적인 것이 아닌 추상(인터페이스, 추상 클래스)에 의존해야 한다.”

package ch05;

// ✅ [인터페이스] CanAble: "대화할 수 있는 능력"만을 명시 → SRP에 적합
interface CanAble {
    void talk();
}

// ✅ [추상 클래스] 홀직원: 홀에서 일하는 직원의 공통 기능 정의
// - 공통 기능: talk() 구현 (손님과 대화)
// - SRP에 따라 "홀직원 역할"에만 집중
abstract class 홀직원 implements CanAble {
    abstract void 청소();

    @Override
    public void talk() {
        System.out.println("손님과 대화");
    }
}

// ✅ 종업원: 홀직원의 하위 역할 (서빙과 주문)
// - 역할 구분 명확 → SRP에 부합
abstract class 종업원 extends 홀직원 {
    void 서빙() {
        System.out.println("서빙");
    }
    void 주문() {
        System.out.println("주문받기");
    }
}

// ✅ 캐셔: 홀직원의 다른 하위 역할 (정산과 계산)
abstract class 캐셔 extends 홀직원 {
    void 정산하기() {
        System.out.println("정산하기");
    }
    void 계산하기() {
        System.out.println("계산하기");
    }
}

// ✅ 요리장인: 홀과 무관한 역할 (talk 기능 없음)
// - talk을 구현하지 않음 → CanAble과 분리 → SRP 명확
abstract class 요리장인 {
    abstract void 요리();
}

// ✅ 기: 종업원 역할 수행
class 기 extends 종업원 {
    @Override
    void 청소() {
        System.out.println("기 청소");
    }
}

class 니 extends 종업원 {
    @Override
    void 청소() {
        System.out.println("니 청소");
    }
}

class 디 extends 캐셔 {
    @Override
    void 청소() {
        System.out.println("디 계산");
    }
}

class 리 extends 캐셔 {
    @Override
    void 청소() {
        System.out.println("리 계산");
    }
}

class 미 extends 요리장인 {
    @Override
    void 요리() {
        System.out.println("미 요리");
    }
}

class 비 extends 요리장인 {
    @Override
    void 요리() {
        System.out.println("비 요리");
    }
}

// ✅ DIP 적용: 상위 모듈이 인터페이스(CanAble)에 의존하고 구현체는 몰라도 됨
class 응대매니저 {
    // 상위 모듈은 "대화할 수 있는 능력(CanAble)"에만 의존
    // 기, 디 등의 구체 클래스에 의존하지 않음
    public void 응대하기(CanAble staff) {
        staff.talk(); // 이 staff가 누구든 talk()만 있으면 됨
    }
}

public class OOPEx10 {
    public static void main(String[] args) {
        // 구체적인 구현체를 인터페이스 타입으로 다루기
        // => 구현이 바뀌더라도 응대매니저 코드는 변경될 필요가 없음
        CanAble staff1 = new 기();
        CanAble staff2 = new 디();

        응대매니저 manager = new 응대매니저();
        manager.응대하기(staff1);
        manager.응대하기(staff2);
    }
}

/*
✅ SRP (Single Responsibility Principle, 단일 책임 원칙)
- 각 클래스가 "하나의 역할"만을 책임지도록 설계됨
    - 종업원: 서빙 & 주문
    - 캐셔: 정산 & 계산
    - 요리장인: 요리만
    - 인터페이스 CanAble은 "talk"이라는 기능만을 명세함

✅ DIP (Dependency Inversion Principle, 의존 역전 원칙)
- 기존: 상위 모듈이 하위 구현 클래스(기, 디 등)에 직접 의존하면 유연성이 떨어짐
- 개선: 상위 모듈(응대매니저)은 인터페이스(CanAble)에만 의존
    - 구체 클래스가 바뀌어도 응대매니저는 전혀 영향을 받지 않음
    - ex) 새로운 대화 가능한 클래스가 추가되면 그대로 확장 가능
    - 이는 코드의 확장성, 유지보수성, 테스트 용이성을 향상시킴
*/