프로그래밍/JAVA Spring

[JAVA 자바] 추상클래스의 올바른 사용 방법

hectick 2023. 3. 23. 00:14

 

 

오늘 체스 미션 코드리뷰에서 추상 클래스에 대한 피드백을 받아서, 이 내용에 대해 한번 스스로 정리해보고자 한다.

 

먼저 코드리뷰를 받은 문제의 코드를 보여주겠다. 체스에서 Queen, King, Rook, Pawn, Knight, Bishop이라는 기물들에 대한 클래스를 먼저 작성하였고, 코드를 작성하다보니 코드가 중복되는 부분이 많아서, 중복되는 부분은 모두 체스 기물을 추상화한 Piece라는 추상클래스에 올렸다.

    public abstract class Piece {

        protected final Team team;
        protected final Movement movement;

        public Piece(Team team, Movement movement) {
            this.team = team;
            this.movement = movement;
        }

        public boolean isMobile(RelativePosition relativePosition) {
            return movement.isMobile(relativePosition);
        }

        // 이하 생략...

 

기물들 중에, Pawn이 유독 움직이는 규칙이 복잡해서, Pawn Piece의 isMobile을 오버라이딩해서 사용하도록 하였다. 나머지 기물들은 따로 오버라이딩 해주지 않고 사용하였다.

    public class Pawn extends Piece {

        private boolean hasMoved;

        public Pawn(final Team team) {
            super(team, Movement.PAWN);
            this.hasMoved = false;
        }

        @Override
        public boolean isMobile(RelativePosition relativePosition) {
            if (!hasMoved && relativePosition.isZeroAbsTwo()) {
                relativePosition = relativePosition.toUnit();
            }
            if (isMovementMobile(relativePosition)) {
                hasMoved = true;
                return true;
            }
            return false;
        }
        // 이하 생략 ..

 

 

여기서 문제점이 뭘까?

추상클래스를 올바르게 사용하지 않았다는 것이다.

 

다음은 리뷰어님이 말씀해주신 추상클래스의 목적이다.

추상 클래스는 어떤 알고리즘 뼈대를 만들어두고 다양하게 변화하는 행위를 구현해야할 때 사용합니다.

 

정리하면, 내 기존 코드는 추상 클래스에서 이미 isMobile 메서드를 완전하게 정의해버렸고, Pawn에서는 이를 그저 상속하여 메서드를 강제로 오버라이딩한 것 뿐이다. 

1. piece마다 isMobile 규칙이 달라서 구현제에서 isMobile의 행위를 결정해주는 것이랑
2. 자식클래스에서 부모의 메서드를 따라가지 않기 위해 override를 사용하는것

1,2번이 지금 사용하는 의미가 다르죠? 이 차이를 잘 파악하시면 좋을 것 같아요.

 

1번이 올바른 추상클래스 사용법이고, 2번은 그냥 추상클래스를 상속하는 용도로만 사용한 것이다. 나는 2번으로 했기에 문제가 된 것이다.

 

그래서 코드를 어떻게 고쳐야 할까?

Piece마다 움직이는 규칙이 다르기 때문에, 이와 관련된 메서드인 isMobile을 추상화 해주어야한다. 그리고 Piece를 구현한 클래스들은 isMobile에 대해 자기에게 맞는 행위를 자기 자신 클래스에서 결정해야 한다. isMobile은 구현체에 따라 그 내용이 달라질 수 있기 때문에 구현되지 않은 상태 그대로 놔둬야 한다.

    public abstract class Piece {

        protected final Team team;
        protected final Movement movement;

        public Piece(Team team, Movement movement) {
            this.team = team;
            this.movement = movement;
        }

        public abstract boolean isMobile(RelativePosition relativePosition);

        // 이하 생략...

 

마찬가지로 다음처럼 추상클래스에서 미리 false를 반환하게 해놓고, 상속받는 자식클래스에서 필요한 메서드만 골라 true를 반환하도록 오버라이딩하는 코드도 좋지 않은 코드이다. 다시 말하지만, 변화하는 행위들을 정의할 때는, 미완성 상태 그대로 냅두어야 한다!

    public abstract class Piece {
    
        protected final Team team;
        protected final Movement movement;

        public Piece(Team team, Movement movement) {
            this.team = team;
            this.movement = movement;
        }

	// 중간 생략

        public boolean isEmpty() {
            return false;
        }

        public boolean isKnight() {
            return false;
        }

        public boolean isPawn() {
            return false;
        }
        
        // 이하 생략

 

    public class Knight extends Piece {

        public Knight(final Team team){
            super(team, Movement.KNIGHT);
        }

        @Override
        public boolean isKnight() {
            return true;
        }
    }