이것이 자바다 5일차 - Chapter7 상속

Seo-Faper·2023년 1월 13일
0

이것이 자바다

목록 보기
7/20
post-custom-banner

상속이란

상속은 부모가 자식에게 물려주는 행위를 말한다. 객체 지향 프로그래밍에서도 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있다.

상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다. 예를 들어 부모 클래스에 필드와 메소드를 미리 선언 해두고 자식 클래스가 부모 클래스를 상속 받으면 마치 자식 클래스가 부모 클래스의 필드와 메소드를 가지고 있는 것 처럼 사용 할 수 있다.

클래스 상속

부모 클래스를 상속하는 것은 extends 라는 키워드로 사용 할 수 있다.

public class 자식클래스 extends 부모클래스

상속은 오직 하나의 부모 클래스만 가능하다.

public class 자식클래스 extends 부모클래스1, 부모클래스2 // 불가능

Phone.java

package ch07.sec02;

public class Phone
{
    public String model;
    public String color;

    public void bell(){
        System.out.println("벨이 울립니다.");
    }
    public void sendVoice(String message){
        System.out.println("자기 : "+message);
    }
    public void receiveVoive(String message){
        System.out.println("상대방 : "+message);
    };
    public void hangUp(){
        System.out.println("전회를 끊습니다. ");
    }

}

부모 객체인 Phone과 자식 객체인 SmartPhone을 만들어 주겠다.

SmartPhone.java

package ch07.sec02;

public class SmartPhone extends Phone{
    public boolean wifi;

    public SmartPhone(String model, String color){
        this.model = model; //부모로부터 물려받은 필드
        this.color = color; //부모로부터 물려받은 필드 
    }
    public void setWifi(boolean wifi){
        this.wifi = wifi;
        System.out.println("와이파이 상태를 변경했습니다.");
    }
    public void internet(){
        System.out.println("인터넷에 연결합니다.");
    }
}

여기 보면 SmartPhone.java에는 선언되지 않은 필드인 model과 color를 부모로부터 받아 쓰는 걸 볼 수 있다.

package ch07.sec02;

public class SmartPhoneExample
{
    public static void main(String[] args) {
        SmartPhone myPhone = new SmartPhone("갤럭시","은색");

        System.out.println("모델 : "+myPhone.model);
        System.out.println("색상 : "+myPhone.color);

        System.out.println("와이파이 상태 : "+myPhone.wifi);

        myPhone.bell();
        myPhone.sendVoice("여보세요.");
        myPhone.receiveVoive("아 네 반갑습니다.");
        myPhone.hangUp();

        myPhone.setWifi(true);
        myPhone.internet();

    }
}

그리고 이렇게 객체를 생성했을 때는 부모의 메소드까지 다 쓸 수 있다.

부모 생성자 호출

현실에서 부모가 없는 자식이 있을수 없듯 자바에서도 자식 객체를 만들면 부모 객체가 항상 먼저 생성된다.
그래서 저 위의 코드에서 SmartPhone myPhone = new SmpartPhone("갤럭시","은색");도 SmartPhone객채만 생성된 것처럼 보이지만 실제로는 부모 객체인 phone이 먼저 생성되고 그 다음 SmartPhone이 생성된 것이다.

그래서 메모리상에서 보면 이렇게 되는 것이다.

모든 객체는 생성자를 호출해야 한다. 그것은 부모 객체도 예외가 아닌데, 그렇다면 부모 객체의 생성자는 어디서 호출된 것일까? 바로 자식 생성자에 숨어 있는데, 부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super(); 에 의해 호출된다.

public 자식클래스(){
	super(); // 안적으면 컴파일 할 때 자동으로 생김
    ...
}

이 super()는 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출한다. 만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생하게 된다.

부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있으면 super()안에 매개값을 코드에 직접 넣어야 한다.

public 자식클래스(){
	super(매개값1, ...); 
    ...
}
package ch07.sec03.exam01;

public class Phone {
    public String model;
    public String color;

    public Phone(){
        System.out.println("Phone() 생성자 생성");
    }
}
package ch07.sec03.exam01;

public class SmartPhone  extends Phone
{
    public SmartPhone(String model, String color){
        super();//생략 가능
        this.model = model;
        this.color = color;
        System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
    }
}

이렇게 만들면

Phone() 생성자 생성
SmartPhone(String model, String color) 생성자 실행됨

이렇게 출력된다.

package ch07.sec03.exam02;

public class Phone
{
    public String model;
    public String color;

    public Phone(String model, String color){
        this.model = model;
        this.model = color;
        System.out.println("Phone(String name, String color) 생성자 실행");
    }
}

부모 클래스가 이렇게 인자를 가진 생성자가 있을 경우, super에는 반드시 이 인자값을 맞춰야 한다.

package ch07.sec03.exam02;

public class SmartPhone extends Phone
{
    public SmartPhone(String model, String color){
        super(model,color); //매개변수 없으면 에러남
        System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
    }
}

메소드 재정의

상속받은 부모 클래스가 자식클래스에 적합한 메소드를 가지고 있으면 좋겠지만 어떤 때에는 자식 클래스에 맞게 재정의 해야 할 수도 있다. 이 때 사용하는 것이 메소드 오버라이딩(Overriding)이다.

메소드 오버라이딩

메소드 오버라이딩은 상속된 메소드를 자식 클래스에서 재정의하는 것을 말한다.
메소드가 오버라이딩 되었다면 해당 부모의 메소드는 숨겨지고 재정의된 자식 메소드가 우선적으로 사용된다.

오버라이딩의 조건에는 3가지 규칙이 있다.

  1. 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)가 동일해야 한다.
  2. 접근 제한을 더 강하게 오버라이딩 할 수 없다. (public -> private 불가)
  3. 새로운 예외를 throws 할 수 없다.
 package ch07.sec04.exam01;

public class Calculator
{
    public double areaCircle(double r){
        System.out.println("Calculator 객체의 areaCircle() 실행");
        return 3.14159 * r * r;
    }
}

이렇게 부모 메소드를 정의하고

package ch07.sec04.exam01;

public class Computer extends Calculator
{
   @Override
   public double areaCircle(double r) {
       System.out.println("Computer 객체의 areaCircle() 실행");
       return Math.PI * r * r;
   }
}

상속을 받은 자식 클래스에서 오버라이딩을 통해 변경 할 수 있다.

부모 메소드 호출

메소드를 재정의하면 부모 메소드는 숨겨지고 자식 메소드만 사용되기 때문에 부모 메소드의 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 한다. 예를 들어 부모 메소드가 100줄의 코드를 가지고 있을 때 자식 메소드에서 1줄만 추가하고 싶어도 기존 100줄의 코드를 자식 메소드에서 다시 작성해야 한다.

이 문제는 자식 메소드와 부모 메소드의 공동 작업 처리 기법을 사용하면 쉽게 해결된다.
super키워드와 (.) 연산자를 통해 숨겨진 부모 메소드의 내용을 호출 할 수 있다.

public Parent{
	public void method(){
   ...
   //100줄 짜리 코드
   //작업 처리 1
   }
}
public Child extends Parent{
	
   @Override
   void method(){
   	super.method();
   	...
       //추가할 코드 
       //작업 처리 2
   }
}

super.method()는 작업처리 전 후에 다 올 수 있으며 먼저 실행되어야 하는 내용을 먼저 작성하면 된다.
이 방법은 메소드를 재사용함으로써 자식 메소드의 중복 작업을 없애는 효과가 있다.

final 클래스와 final 메소드

앞서 배웠듯 필드 선언 시 final이 붙어있으면 초기값을 변경 할 수 없다.
그렇다면 클래스와 메소드에 final을 붙이면 어떤 효과가 있을까?

final 클래스

클래스를 선언할 때 final로 선언하면 최종적인 클래스이므로 더 이상 상속할 수 는 클래스가 된다. 즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스도 만들 수 없다.
이렇게 꽉 막히고 변할 수 없는 사람은 부모가 될 자격이 없는 것과 같다.

final 메소드

메소드를 선언할 때 final로 선언하면 최종적인 메소드이므로 오버라이등 할 수 없는 메소드가 된다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의 할 수 없다.

protected 접근 제한자

기존의 접근 제한자 public, private등을 이용해 객체 외부에서 필드, 생성자, 메소드의 접근 여부를 결정했는데 protected 라는 새로운 접근 제한자가 있다. 이 protected는 상속과 관련있는 접근 제한자다.

접근 제한자제한 대상제한 범위
protected필드, 생성자, 메소드같은 패키지이거나, 자식 객체만 사용 가능

protected는 같은 패키지에서는 default처럼 접근 할 수 있지만 다른 패키지에서는 자식 클래스만 접근을 허용한다. 그래서 오직 상속을 통해서만 접근 할 수 있게 한다.

타입 변환

타입 변환은 챕터 2에서 다룬 적이 있는데, 클래스 마찬가지로 타입 변환이 가능하다.
타입 변환은 상속 관계에 있는 클래스 사이에서 발생한다.

자동 타입 변환

의미 그대로 자동적으로 타입이 바뀌는 경우인데, 다음과 같은 조건에서 일어난다.

부모 타입 변수 = 자식 타입 객체;
Cat cat = new Cat();
Animal animal = cat;

이렇게 고양이가 동물의 특성을 상속 받았다면
고양이는 동물이다가 성립한다.
위 코드로 생성되는 메모리 상태 다음과 같다.
cat과 animal 변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조한다.

그래서 값비교(==)를 하면 true가 나온다.

cat == animal // true

바로 위의 부모가 아니더라도 상속 관계층에서 상위 타입이기만 하면 가능하다.

부모 타입으초 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다.
비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스의 멤버로 한정된다.

강제 타입 변환

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다. 대신 다음과 같은 캐스팅으로 강제 타입 변환을 할 수 있다.

자식 타입 변수 = (자식타입) 부모타입객체;

여기서 () 가 캐스팅 연산이다.

그렇다고 해서 부모 타입 객체를 자식 타입으로 무조건 강제 변환 할 수 있는 건 아니다.
자식 객체가 부모 타입으로 자동 변환된 후 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용 할 수 있다.

Parent parent = new new Child(); //자동 변환
Child child = (Child) parent; // 강제 변환

자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능하다는 제약이 있다. 만약 자식 타입에 선언된 필드와 메소드를 써야 하는 상황이 오면 강제 타입 변환을 해서 다시 자식 타입으로 변환해야 한다.

다형성

다형성이란 사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다.
자동차의 부품을 교환하면 성능이 다르게 나오듯이 객체는 부품과 같아서 프로그램을 구성하는 객체를 바꾸면 프로그램의 실행 성능이 다르게 나올 수 있다.

필드 다형성

필드 다형성은 필드 타입은 동일하지만 대입되는 객체가 달라져서 실행 결과가 다양하게 나오는 것을 말한다.

Car.java

package ch07.sec08.exam01;

public class Car {
    public Tire tire;
    public void run(){
        tire.roll();
    }
}

Tire.java

package ch07.sec08.exam01;

public class Tire
{
    public void roll(){
        System.out.println("회전합니다.");
    }
}

이렇게 자동차와 타이어가 있다면 타이어에도 종류가 있을 것이다.

KumboTire.java

package ch07.sec08.exam01;

public class KumhoTire extends Tire
{
    @Override
    public void roll() {
        System.out.println("금호 타이어가 회전합니다.");
    }
}

HankookTire.java

package ch07.sec08.exam01;

public class HankookTire extends Tire{
    @Override
    public void roll() {
        System.out.println("한국 타이어가 회전합니다.");
    }
}
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.tire = new Tire();
        myCar.run();

        myCar.tire = new HankookTire();
        myCar.run();

        myCar.tire = new KumhoTire();
        myCar.run();
    }

그럼 이렇게 출력 했을 때 다른 출력결과를 나타낸다. 이것을 필드의 다형성이라 한다.

매개변수 다형성

다형성은 필드보다는 메소드를 호출할 때 많이 발생한다.
메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수도 있다. 여기서 다형성이 발생한다.

Vechile.java

package ch07.sec08.exam02;

public class Vehicle {
    public void run(){
        System.out.println("차량이 달립니다.");
    }
}

Driver.java

package ch07.sec08.exam02;

public class Driver {
    public void drive(Vehicle vehicle){ //자식 객체가 형 변환되어 들어옴
        vehicle.run(); // 자식 객체가 재정의한 run() 메소드 호출
    }
}

이렇게 클래스를 만들고 drive(Vehicle vehicle)가 호출될 때, 다형성에 의해 Vehcile의 자식 객체가 들어올 수 있다.

Bus.java

package ch07.sec08.exam02;

public class Bus extends Vehicle{
    @Override
    public void run() {
        System.out.println("버스가 달립니다.");
    }
}

Taxi.java

package ch07.sec08.exam02;

public class Taxi extends Vehicle{
    @Override
    public void run() {
        System.out.println("택시가 달립니다.");
    }
}
package ch07.sec08.exam02;

public class DriverExample {
    public static void main(String[] args) {
        Driver driver = new Driver();

        Bus bus = new Bus();
        driver.drive(bus);

        Taxi taxi = new Taxi();
        driver.drive(taxi);
    }
}

이렇게 어떤 자식 객체가 제공되느냐에 따라 같은 Driver 객체지만 다른 결과를 주는 것을 메소드의 다형성이라고 한다.

객체의 타입 확인

매개변수의 다형성에서 실제로 어떤 객체가 매개값으로 제공되었는지 확인하는 방법이 있다.
꼭 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인하고자 할 때 instanceof 연산자를 쓸 수 있다.

boolean result = 객체 instanceof 타입;

좌향의 객체가 우항의 타입이면 true를 산출하고 그렇지 않으면 false를 산출한다.
이 기능은 강제 타입 변환 하기 전에 확인하는 용도록, 제대로된 상속 관계인지를 확인할 때 쓰인다.

        /*
        if (person instanceof Student) {
            Student student = (Student) person;
            System.out.println("studentNo: " + student.studentNo);
        }
       */

        //Java 12 부터 가능, person이 참조하는 객체가 Student 타입일 경우 student 변수에 대입
        if(person instanceof Student student){
            System.out.println("studentNo: "+student.studentNo);
            student.study();
        }

자바 12 부터는 instanceof에 바로 대입할 수 있는 기능도 있다.

추상 클래스

사전적인 의미인 추상은 실체 간에 공통되는 특성을 추출한 것을 말한다.
고양이, 강아지, 새, 물고기 등의 공통점은 바로 바로 동물이다. 여기서 동물은 실체들의 공통되는 특성을 가지고 있는 추상적인 것이라고 볼 수 있다.

추상 클래스란?

실제로 '동물' 이라는 이름을 가진 어떤 생명체는 존재하지 않는다. 동물은 하나의 추상적인 개념인 것 이다.
추상 클래스란, 객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면 이 클래스들은 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다.

추상 클래스는 실체 클래스의 부모 역할을 한다. 따랏 ㅓ실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다.

예를 들어 Bird, Insect, Fish같은 실체 클래스에서 공통되는 필드와 메소드를 따로 선언한 Animal 클래스를 만들 수 있고, 이것을 상속해서 실체 클래스를 만들 수 있다.

추상클래스는 실체 클래스들의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 new로 인스턴스를 만들 수 없다.

그러므로 추상 클래스는 extends 뒤에만 올 수 있다.

추상 클래스 선언

추상클래스는 클래스 선언부에 abstract 키워드를 적어서 생성 할 수 있다. 이렇게 absctract가 붙어있는 클래스는 new로 만들 수 없다.

public abstract class 클래스명{
	//필드
    //생성자
    //메소드
}

추상 클래스도 필드, 메소드를 선언 할 수 있다.
또한, 자식 객체가 생성될 때 super()로 추상 클래스의 생성자를 호출하기 때문에 생성자도 꼭 필요 하다. 다음은 전화기의 모든 공통 필드와 메소드만 뽑아내 추상 클래스 Phone을 만들어 낸 것이다.

Phone.java

package ch07.sec10.exam01;

public abstract class Phone {
    String owner;

    Phone(String owner){
        this.owner = owner;
    }
    void turnOn(){
        System.out.println("폰 전원을 켭니다.");
    }
    void turnOff(){
        System.out.println("폰 전원을 끕니다.");
    }
}

SmartPhone.java

	package ch07.sec10.exam01;

public class SmartPhone extends Phone
{

    SmartPhone(String owner) {
        super(owner);
    }
    void internetSearch(){
        System.out.println("인터넷 검색을 합니다.");
    }
}

SmartPhoneExample.java

package ch07.sec10.exam01;

public class SmartPhoneExample
{
    public static void main(String[] args) {
        //Phone p = new Phone();  abstract으로 선언된 클래스는 생성 불가

        SmartPhone smartPhone = new SmartPhone("홍길동");

        smartPhone.turnOn(); // 추상화 객체에서 가져온 공통된 특성
        smartPhone.internetSearch();
        smartPhone.turnOff();// 추상화 객체에서 가져온 공통된 특성
    }
}

여기서 보면 turnOn과 turnOff는 추상 클래스에서 물려받은 메소드이다.

추상 메소드와 재정의

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수)만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다.
예를 들어 동물을 소리를 내기 때문에 Animal 추상 클래스에서 sound()라는 메소드를 선언 할 수 있지만 각 동물들 마다 울음소리가 다르기 때문에 추상 클래스에서 통일하여 작성할 수 없다.
이런 경우를 위해서 추상 클래스는 다음과 같은 추상 메소드를 선언 할 수있다. 일반 메소드와 차이점은 abstarct 키워드가 붙고, 메소드 실행 내용인 중괄호 { } 가 없다.

abstract 리턴타입 메소드명(매개변수, ..); 
public abstract class Animal {
	abstract void sound();
}

이렇게 생성한 추상 메소드는 반드시 재정의해서 실행 내용을 채워야 한다.

Animal.java

package ch07.sec10.exam02;

public abstract class Animal
{
    public void breathe(){
        System.out.println("숨을 쉽니다.");
    }
    public abstract void sound();
}

Cat.java

package ch07.sec10.exam02;

public class Cat extends Animal{
    @Override
    public void sound() { // 추상 메소드 재정의
        System.out.println("야옹");
    }
}

Dog.java

package ch07.sec10.exam02;

public class Dog extends Animal{

    @Override
    public void sound() { // 추상 메소드 재정의
        System.out.println("멍멍");
    }

}
package ch07.sec10.exam02;

public class AbstractMethodExample
{
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.sound();

        Cat cat = new Cat();
        cat.sound();
        
        animalSound(new Dog());// 자동 형변환 되어 들어감
        animalSound(new Cat());// 자동 형변환 되어 들어감
    
    }
    public static void animalSound(Animal animal){
        animal.sound();
    }
}

봉인된 클래스

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 자바 15부터는 무분별한 자식 클래스 생성을 막기 위해 sealedpermits 키워드를 통해 클래스를 봉인 할 수 있다.

public sealed class Person permits Employee, Manager { ... }

sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 한다.
봉인된 Person 클래스를 상속하는 Employee와 Manager는 final 또는 non-sealed 키워드로 다음과 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야 한다.

public final class Employee Employee extends Person { ... }
public non-sealed class Manager extends Person { ... }

이렇게 하면 Employee는 final로 선언되어 더 이상 자식 클래스를 만들 수 없지만 Manager는 non-sealed 상태로 선언했기 때문에 자식 클래스를만들 수 있다.

연습문제

1번, 허용하지 않습니다.

2번, 강제 타입 변환이 항상 되는건 아닙니다.
자식 타입이 부모 타입으로 자동 변환한 후, 다시 자식 타입으로 변환할 때만 강제 타입 변환을 사용할 수 있습니다.


1번, 변할 수 없는 사람은 부모가 될 수 없습니다.

4번, protected는 재정의를 막지 않고 접근을 막습니다.
즉, 자식 클래스에서 protected로 선언된 메소드를 호출하거나 필드를 참조하면 그걸 막는 겁니다.

추상 메소드를 꼭 가질 필요는 없습니다. 다만 추상 메소드를 만들면 상속받는 클래스에서 그 메소드에 대한 재정의를 꼭 가져야 할 뿐입니다.

super();가 생략되어 있지만, 부모 클래스의 생성자에서 매개변수가 있을 경우 꼭 상속된 자식 클래스는 super에 인자를 넣어줘야 합니다.
super(name); 추가

상속받은 객체는 부모의 객체의 생성자가 먼저 생성됩니다.
new Child(); 에서 생략된 super();에 의해 부모 객체가 호출됩니다. 인자가 없으므로

public Parent(){
	this("대한민국");
	System.out.println("Parent() call");
}

으로 넘어간다. 그리고 this("대한민국")을 만나 오버로딩된 생성자로 들어간다.

public Parent(String nation){
	this.nation = nation;
	System.out.println("Parent(String nation) call");
}

nation 매개변수에 "대한민국"이 들어가고
그걸 Parent의 nation으로 정의한다. 그리고 Parent(String nation) call 출력,
그 다음 다시 인자가 없는 생성자로 돌아와 Parent() call 출력
그 후 자식 객체인 Child의 인자 없는 생성자 호출, 위와 같이 인자를 받고 오버로딩된 생성자로 들어감, 출력 후 객체 생성 완료
출력 결과는

Parent(String nation) call
Parent() call
Child(String name) call
Child() call



부모 객체인 Tire를 담는 변수 tire가 자식 객체인 snowTire의 참조를 받으면 형변환이 일어나 SnowTire의 메소드를 쓰게 된다.

스노우 타이어가 굴러갑니다.
스노우 타이어가 굴러갑니다.

2번, 강제 형 변환은 그렇게 마음대로 할 수 있는 게 아니라 한번 자동 변환된 객체를 다시 돌릴 때 쓰는 겁니다.

Computer에서 추상 메소드인 work()를 재정의 하지 않았습니다.

super를 붙여주면 됩니다.그럼 부모 객체의 onCreate()를 호출합니다.

a instanceof C c 하면 됩니다. 자바 15라는 가정하에요.

profile
gotta go fast
post-custom-banner

0개의 댓글