본문으로 건너뛰기

안동민 개발노트

본문 시작

상속·생성자·오버라이딩

extends 관계·상속 메모리·오버라이딩·접근 제어·super와 생성자 순서를 게시글 계층으로 한 번에 연결합니다.

상속은 여러 클래스에 공통인 상태와 행동을 부모 클래스에 두고 자식이 재사용·확장하는 문법입니다.

단순 코드 복사 제거보다 “자식은 부모의 한 종류인가”라는 타입 관계가 먼저입니다.

게시판에서 텍스트·코드·영상 형식을 모두 게시글로 다룰 때 상속 후보가 생깁니다.

상속의 정상 최소 예제
class Post {
    void publish() {
        System.out.println("게시글 공개");
    }
}

class TextPost extends Post {
}

TextPost textPost = new TextPost();
textPost.publish();

TextPost extends Post는 텍스트 게시글이 게시글의 한 종류임을 나타냅니다.

자식에 publish를 다시 쓰지 않아도 부모의 메서드를 사용할 수 있습니다.

상태를 생성자로 받아야 할 때는 부모 초기화가 먼저 필요하므로, 이제 실패 예를 통해 super(...)의 위치를 확인합니다.

HTML 다이어그램: /docs/java/ch7/ch7-1/1.html

부모 생성자 호출

lab/MissingParentConstructor.java
public final class MissingParentConstructor {
    public static void main(String[] args) {
        CodePost post = new CodePost();
        System.out.println(post);
    }

    private static class Post {
        Post(String title) {
            System.out.println(title);
        }
    }

    private static final class CodePost extends Post {
    }
}
컴파일 실패 관찰
error: constructor Post in class Post cannot be applied to given types

CodePost에 생성자를 쓰지 않으면 컴파일러가 기본 생성자와 첫 문장 super()를 가정합니다.

부모 Post에는 매개변수 없는 생성자가 없으므로 호출할 수 없습니다.

자식 생성자에서 부모가 요구하는 값을 명시해야 합니다.

CodePost(String title) {
    super(title);
}

상속 인스턴스에는 부모 부분과 자식 부분이 함께 만들어지므로 두 생성자 모두 실행돼야 합니다.

부모 상태를 먼저 초기화한 뒤 자식 상태를 초기화합니다.


extends와 기능 확장

src/PostInheritance.java
public final class PostInheritance {
    public static void main(String[] args) {
        TextPost textPost = new TextPost("가입 인사", 40, 120);
        CodePost codePost = new CodePost("로그인 구현", 50, "feature/login");

        textPost.publish();
        textPost.previewBody();
        codePost.publish();
        codePost.commit();
    }

    private static class Post {
        private final String title;
        private final int viewCount;

        Post(String title, int viewCount) {
            if (title.isBlank() || viewCount <= 0) throw new IllegalArgumentException();
            this.title = title;
            this.viewCount = viewCount;
        }

        void publish() {
            System.out.println("publish=" + title + ":views=" + viewCount);
        }

        protected String title() {
            return title;
        }
    }

    private static final class TextPost extends Post {
        private final int bodyLength;

        TextPost(String title, int viewCount, int bodyLength) {
            super(title, viewCount);
            this.bodyLength = bodyLength;
        }

        void previewBody() {
            System.out.println("preview=" + title() + ":chars=" + bodyLength);
        }
    }

    private static final class CodePost extends Post {
        private final String branch;

        CodePost(String title, int viewCount, String branch) {
            super(title, viewCount);
            this.branch = branch;
        }

        void commit() {
            System.out.println("commit=" + title() + ":" + branch);
        }
    }
}
publish=가입 인사:views=40
preview=가입 인사:chars=120
publish=로그인 구현:views=50
commit=로그인 구현:feature/login

두 자식은 title·viewCountpublish를 물려받고 자기 필드와 행동을 추가합니다.

자바 클래스는 extends 뒤에 부모 하나만 선택할 수 있습니다.

클래스 다중 상속을 허용하면 두 부모가 같은 메서드를 제공할 때 어느 구현을 사용할지 모호한 다이아몬드 문제가 생길 수 있습니다.

여러 역할 조합은 뒤에서 인터페이스 다중 구현으로 다룹니다.

private 필드는 자식도 직접 접근할 수 없습니다.

부모가 보호해야 할 상태는 private으로 두고 title() 같은 protected 또는 public 행동으로 필요한 협력을 제공합니다.

protected를 모든 자식 편의용 공개 필드처럼 쓰면 부모 불변식을 우회하므로 메서드 경계를 우선합니다.


상속 객체의 구조

HTML 다이어그램: /docs/java/ch7/ch7-1/2.html

new CodePost(...)를 실행하면 하나의 객체 참조를 얻지만 그 인스턴스 안에는 Post에서 정의한 상태와 CodePost에서 정의한 상태가 모두 있습니다.

메서드를 찾을 때 현재 타입에서 시작해 부모 방향으로 올라갑니다.

  • codePost.commit()은 CodePost에서 찾습니다.
  • codePost.publish()는 CodePost에 없으므로 Post에서 찾습니다.
  • 부모에서 자식 방향으로는 임의로 내려가 메서드를 찾지 않습니다.

객체가 두 개 만들어진다는 뜻은 아닙니다.

하나의 상속 인스턴스 안에 클래스 계층에 대응하는 부분이 함께 초기화된다고 이해합니다.

메서드 코드는 각 객체에 복사되지 않고 클래스 정보로 관리됩니다.


메서드 오버라이딩

src/OverridePost.java
public final class OverridePost {
    public static void main(String[] args) {
        TextPost textPost = new TextPost("가입 인사", 40, 120);
        CodePost codePost = new CodePost("로그인 구현", 50, "feature/login");

        textPost.publish();
        codePost.publish();
    }

    private static class Post {
        protected final String title;
        protected final int viewCount;

        Post(String title, int viewCount) {
            this.title = title;
            this.viewCount = viewCount;
        }

        void publish() {
            System.out.println("post=" + title + ":views=" + viewCount);
        }
    }

    private static final class TextPost extends Post {
        private final int bodyLength;

        TextPost(String title, int viewCount, int bodyLength) {
            super(title, viewCount);
            this.bodyLength = bodyLength;
        }

        @Override
        void publish() {
            System.out.println("text=" + title + ":chars=" + bodyLength);
        }
    }

    private static final class CodePost extends Post {
        private final String branch;

        CodePost(String title, int viewCount, String branch) {
            super(title, viewCount);
            this.branch = branch;
        }

        @Override
        void publish() {
            super.publish();
            System.out.println("code=branch/" + branch);
        }
    }
}
text=가입 인사:chars=120
post=로그인 구현:views=50
code=branch/feature/login

메서드 이름, 매개변수 목록, 반환 호환성이 부모 메서드와 맞으면 오버라이딩합니다.

@Override는 필수 문법은 아니지만 서명이 어긋나 새 메서드가 되는 실수를 컴파일러가 잡게 하므로 붙입니다.

자식 오버라이딩 메서드 안에서 super.publish()로 부모 구현을 명시적으로 호출할 수 있습니다.

필드는 오버라이딩되지 않습니다.

부모와 자식에 같은 이름 필드를 만들면 두 저장 위치가 생겨 참조 타입에 따른 숨김 문제가 생깁니다.

같은 개념 상태는 부모에 하나만 두는 편이 낫습니다.


super 참조

HTML 다이어그램: /docs/java/ch7/ch7-1/3.html

this는 현재 객체의 현재 클래스 관점, super는 같은 객체의 부모 클래스 관점에서 멤버를 찾습니다.

@Override
void publish() {
    super.publish();
    System.out.println("child extension");
}

super가 별도의 부모 객체 참조를 반환하는 것은 아닙니다.

오버라이딩된 자식 메서드 대신 부모 구현을 선택하는 특별한 키워드입니다.

생성자에서 super(...)는 부모 생성자를 호출하며 첫 문장이어야 합니다.

this(...)로 같은 클래스 생성자에 위임했다면 그 위임 경로의 최종 생성자가 부모를 호출합니다.

생성 실행 순서는 최상위 부모에서 가장 구체적인 자식 방향입니다.

자식 생성자에 먼저 진입하더라도 첫 문장인 super 호출을 따라 부모 초기화가 끝난 후 자식 본문이 실행됩니다.

부모 생성자에서 오버라이딩 가능한 메서드를 호출하면 아직 자식 필드가 초기화되기 전에 자식 구현이 실행될 수 있으므로 피합니다.


상속 선택 기준

CodePost is a Post는 자연스럽지만 Board is a Scanner는 자연스럽지 않습니다.

단지 Scanner 메서드를 재사용하려고 상속하면 타입 의미가 왜곡됩니다.

“가지고 사용한다” 관계는 구성으로 표현합니다.

final class BoardCli {
    private final Scanner scanner;
}

상속은 부모 계약을 자식이 지킬 수 있어야 합니다.

부모를 기대하는 위치에 자식을 넣어도 핵심 의미가 깨지지 않아야 다형성으로 확장할 수 있습니다.

공통 코드가 조금 있다는 이유만으로 계층을 만들면 부모 변경이 모든 자식에 퍼지고 잘못된 결합이 생깁니다.


게시글 계층의 부모 배열

src/InheritedPostArray.java
public final class InheritedPostArray {
    public static void main(String[] args) {
        Post[] posts = {
                new TextPost("가입 인사", 40),
                new CodePost("로그인 구현", 50)
        };

        int total = 0;
        for (Post post : posts) {
            post.publish();
            total += post.viewCount();
        }
        System.out.println("total=" + total);
    }

    private static class Post {
        private final String title;
        private final int viewCount;

        Post(String title, int viewCount) {
            this.title = title;
            this.viewCount = viewCount;
        }

        int viewCount() {
            return viewCount;
        }

        protected String title() {
            return title;
        }

        void publish() {
            System.out.println("post=" + title);
        }
    }

    private static final class TextPost extends Post {
        TextPost(String title, int viewCount) {
            super(title, viewCount);
        }

        @Override
        void publish() {
            System.out.println("text=" + title());
        }
    }

    private static final class CodePost extends Post {
        CodePost(String title, int viewCount) {
            super(title, viewCount);
        }

        @Override
        void publish() {
            System.out.println("code=" + title());
        }
    }
}
text=가입 인사
code=로그인 구현
total=90

부모 타입 배열에 서로 다른 자식 참조를 넣고 공통 메서드를 호출했습니다.

왜 자식 구현이 선택되는지는 다음 문서부터 다형적 참조와 동적 디스패치로 자세히 다룹니다.

HTML 다이어그램: /docs/java/ch7/ch7-1/4.html

연습 문제

부모 Posttitlepublish(), 자식 TextPostbodyLength, ImagePostimageCount를 두세요.

각 자식이 publish를 오버라이딩하고 생성자에서 supertitle을 전달합니다.

해설 보기
src/PostInheritanceExercise.java
public final class PostInheritanceExercise {
    public static void main(String[] args) {
        Post[] posts = {
                new TextPost("가입 인사", 300),
                new ImagePost("행사 사진", 4)
        };
        for (Post post : posts) {
            post.publish();
        }
    }

    private static class Post {
        protected final String title;

        Post(String title) {
            this.title = title;
        }

        void publish() {
            System.out.println("publish=" + title);
        }
    }

    private static final class TextPost extends Post {
        private final int bodyLength;

        TextPost(String title, int bodyLength) {
            super(title);
            this.bodyLength = bodyLength;
        }

        @Override
        void publish() {
            System.out.println("text=" + title + ":chars=" + bodyLength);
        }
    }

    private static final class ImagePost extends Post {
        private final int imageCount;

        ImagePost(String title, int imageCount) {
            super(title);
            this.imageCount = imageCount;
        }

        @Override
        void publish() {
            System.out.println("image=" + title + ":count=" + imageCount);
        }
    }
}
text=가입 인사:chars=300
image=행사 사진:count=4

공통 title과 타입 계약은 부모에, 게시글 형식별 상태와 표현은 자식에 있습니다.

자식 생성자는 부모 초기화를 먼저 위임합니다.