🎯 0. 주제 선정 이유
이전 블랙잭 미션에서 간간히 캠퍼스에서 ‘instanceof를 지양해야한다!’라는 소리가 들려왔는데,
블랙잭 미션에서는 instanceof를 사용할 경우가 없었어서 공감하지 못했다.
하지만 이번 체스 미션에서 2번 instanceof를 사용하게 되었다.
‘instanceof를 지양해야한다!’라는 소리를 들어서 지양하고 싶었지만,
instanceof 대신 어떤 방법을 사용해야할지 몰라서 instanceof를 사용하게 됐었다.
따라서 이번에 instanceof를 왜 지양해야하는지를 알아보고,
instanceof를 쓰지 않고 어떻게 해결할 수 있는지를 알아보자.
🎯 1. instanceof가 뭐지?
다들 클래스의 인스턴스(instance) 라는 말은 적어도 한번은 들어봤을 것이다.
객체가 메모리에 할당되어 실제 사용될 때 인스턴스(instance) 라고 부른다.
instanceof 연산자는 객체가 해당 클래스의 인스턴스인지를 판단하는 연산자이다.
A가 B의 인스턴스인지를 판단할 때 A instanceof B 형태로 사용되고,
A가 B의 인스턴스라면 true 를, 아니라면 false 를 반환하는 boolean 반환타입을 가진다.
A : 판단할 인스턴스(참조 변수)
B : 타입(클래스명)
🎯 2. instanceof는 언제 사용할까?
instanceof 는 일반적인 경우에는 사용되지 않는다.
여기서 일반적인 경우란, 단순하게 하나의 타입이 하나의 인스턴스를 가질 때 판단하는 경우이다.
예를 들면 다음과 같다.
public class Car {
...
}
---
Car car = new Car();
if (car instanceof Car) {
...
}
인텔리제이에서도 이러한 일반적인 경우는 친절하게 ‘컴파일 경고’를 표시해준다.
그렇다면, 어떠한 상황에 사용할까?
바로 하나의 타입이 여러 인스턴스를 가질 수 있을 때 instanceof를 효과적으로 사용할 수 있다.
이러한 경우가 무엇일까?
주로, 상속 관계일 때 instanceof를 효과적으로 사용할 수 있다!
아래의 체스 게임 객체들의 다이어그램을 살펴보자.
6가지의 기물들이 Piece 라는 추상 클래스를 상속하고 있다.
이러한 경우에, 외부에서는 Piece 라는 추상 클래스를 추상화하여 사용하게 된다.
이때, 추상화된 Piece 사용 시 해당 Piece 가 Pawn 의 인스턴스인 경우를 분기처리 하고 싶다면?
바로 instanceof 를 사용하면 된다.
public class ChessGame {
...
// 6가지 기물이 추상화된 Piece가 파라미터로 들어온다.
public void move(Piece piece) {
// Piece가 Pawn일 경우를 처리하고 싶을 때 instanceof 사용
if (piece instanceof Pawn) {
... // 대충 Pawn 이동 로직
}
}
}
위처럼 instanceof 를 사용하면, 추상화된 Piece 가 Pawn 일 경우에만 처리하는 로직을 구현할 수 있다.
🎯 3. 그렇다면 instanceof의 문제가 뭐야? - Why?
위의 예시를 보면, instanceof는 불가능한 구현을 가능하게 해주는 좋은 연산자처럼 보인다.
그런데 왜 instanceof를 지양하라는 것일까?
instanceof 의 문제점들을 하나씩 알아보자.
💡 3-1. 추상화 계층을 깨뜨린다. (feat. 캡슐화)
추상 클래스를 이용하여 상속을 하면 하나의 추상화 계층이 형성된다.
추상화 계층이 형성되면, 해당 추상화 계층을 사용하는 외부에서는 아래의 하위 문제를 알 필요가 없어진다.
다음과 같이 추상 클래스와 상속받는 서브 클래스의 코드가 구현되어 있다고 하자.
public abstract class Piece {
public abstract void move(Position positionToMove);
}
---
public class Paws extends Piece {
@Override
public void move(Position positionToMove) {
// Pawn의 move 로직
...
}
}
---
public class Rook extends Piece {
@Override
public void move(Position positionToMove) {
// Rook의 move 로직
...
}
}
✅ 추상화된 코드
instanceof 를 사용하지 않고 외부에서 추상화된 Piece 를 사용하는 코드는 다음과 같다.
public class ChessGame {
...
// 6가지 기물이 추상화된 Piece가 파라미터로 들어온다.
public void move(Piece piece, Position positionToMove) {
piece.move(positionToMove);
}
}
외부인 ChessGame 입장에서는 Piece 가 Pawn인지, Rook인지 다른 기물인지는 상관이 없다.
단지 추상화된 상위 계층 Piece 에 움직여라! 라는 메세지만 전달할 뿐이다.
해당 Piece 가 어떤 하위 계층인지는 알아서도 안 되고, 알 필요가 없는 것이다.
✅ instanceof를 사용하여 추상화(캡슐화)가 깨진 코드
public class ChessGame {
...
// 6가지 기물이 추상화된 Piece가 파라미터로 들어온다.
public void move(Piece piece) {
// Piece가 Pawn일 경우를 처리하고 싶을 때 instanceof 사용
if (piece instanceof Pawn) {
... // 대충 Pawn 이동 로직
}
}
}
처음에 instanceof 를 사용한 예제를 다시 가져왔다.
위에서 설명 시에 다음과 같이 설명했다.
위처럼 `instanceof` 를 사용하면,
추상화된 `Piece` 가 `Pawn` 일 경우에만 처리하는 로직을 구현할 수 있다.
해당 문장은 instanceof 가 추상화 및 캡슐화를 지키지 못한다는 의미를 담고 있다.
외부에서 추상화된 상위 계층인 Piece 를 사용하는 입장에서는
Piece 가 하위 계층인 Pawn 인 경우를 직접적으로 알 필요도 없고, 알아서는 안 되는 것이다.
instanceof 를 사용하게 되면,
상위 계층 Piece 가 아닌 하위의 Pawn 을 사용하게 되어 추상화가 깨지고,
Pawn 의 로직들을 알 수 있기 때문에 캡슐화가 깨진다.
이러한 이유가 instanceof 를 지양하는 가장 큰 이유다.
💡 3-2. OCP(Open Closed Principle) 위반
OCP 원칙은 다음과 같다.
* 객체가 확장에는 열려있고, 변화에는 닫혀있어야 한다.
쉽게 말해서 객체 기존의 코드를 변경하지 않고 기능을 추가할 수 있도록 설계되어야 한다는 뜻이다.
instanceof 를 사용하게 되면, 이러한 OCP를 위반하게 된다.
위의 예시에서 새로운 기물 SeongHa 가 추가되어, instanceof 로 체크해야한다고 생각해보자.
위의 ChessGame 에 SeongHa 의 instanceof 로직 코드를 추가해야 할 것이다.
지금은 하나의 기물이 추가되었지만, 새로운 기물이 추가될 때마다 체크해야 한다면
Piece를 instanceof 를 사용해서 체크하는 곳마다 instanceof 를 추가로 선언해야 할 것이다.
따라서, 객체의 확장에 많은 비용이 들어가기 때문에 확장이 어려워져서 OCP를 위반하게 된다.
💡 3-3. SRP(Single Responsibility Principle) 위반
SRP 원칙은 다음과 같다.
* 객체는 한 가지의 책임만 가져야 한다.
이 부분은 3-1의 이유과 비슷하다.
위의 예시에서 ChessGame 은 instanceof 를 사용하여 Piece 가 Pawn 임을 알고
해당 Pawn 의 이동 로직을 처리했다.
instanceof 를 사용하지 않았다면, 추상화된 상위 계층인 Piece 를 사용하여
단순히 Piece 를 이동시키고, 하위 계층인 Pawn 의 이동 로직은 알 필요가 없었을 것이다.
하지만 instanceof 를 사용함으로써, Piece 의 이동이 아닌 Pawn 의 이동 책임을 가지게 되었고,
이후에 instanceof 를 사용하는 여러 기물이 추가된다면 해당 기물들의 이동 책임을 전부 가지게 될 것이다.
이러한 점에서 SRP를 위반한다고 할 수 있다.
🎯4. 그럼 instanceof 대신 무엇을 사용해야할까? - Solution
바로 Solution부터 말하자면, 객체의 다형성을 이용하면 해결할 수 있다!
Solution 설명은 체스 미션에서 instanceof 를 사용했다가,
리팩토링 시 instanceof 를 제거하고 리팩토링한 내 코드를 보면서 설명하도록 하겠다!
4-1. ❎ 변경 전 instanceof 사용 코드
public class Board {
public void movePiece(...) {
...
if (piece instanceof Pawn) {
checkPawnDiagonalMove(sourcePosition, targetPosition);
}
}
}
piece 가 Pawn 인지를 instanceof 로 확인하여, Pawn 인 경우에만
대각선 이동 로직을 체크하도록 했다.
4-2. ✅ 다형성을 활용하여 리팩토링한 코드
해당 코드를 다형성을 사용해서 다음과 같이 리팩토링했다.
- Piece 에 다형성을 이용하기 위해 Pawn 인지를 체크하는isPawn() 추상 메소드를 선언했다.
- Piece 의 서브 클래스들이 isPawn() 을 오버라이딩하여 구현했다.
public abstract class Piece {
...
public abstract boolean isPawn();
}
---
public class Pawn extends Piece {
...
@Override
public boolean isPawn() {
return true;
}
}
---
public class Rook extends Piece {
...
@Override
public boolean isPawn() {
return false;
}
}
public class Board {
public void movePiece(...) {
...
if (piece.isPawn()) {
checkPawnDiagonalMove(sourcePosition, targetPosition);
}
}
}
Board는 piece 가 Pawn 인지를 정확히 알지 못하고,
Piece 에게 메세지를 던져서 Piece 의 각 서브 클래스들이 Pawn 인지를 확인하게 됐다.
🎯 5. 결론
instanceof 사용 시의 문제는 다음과 같이 3가지가 존재했다.
1. 추상화, 캡슐화 깨짐
2. OCP 위반
3. SRP 위반
따라서, instanceof 를 지양하고 다른 방법을 이용해야 한다.
이때, 다형성을 이용할 수 있다.
1. 슈퍼 클래스에 추상 메소드 선언
2. 서브 클래스에서 추상 메소드 오버라이딩하여 구현
3. 외부에서 슈퍼 클래스의 추상 메소드를 사용하여 메세지 전달
따라서, instanceof 대신 다형성을 이용하여 객체와 메세지를 주고 받는 구조를 만들자!
Reference
https://tecoble.techcourse.co.kr/post/2021-04-26-instanceof/
'우아한테크코스 5기 > 학습 로그' 카테고리의 다른 글
함수형 인터페이스의 정의 & 사용 예제 (0) | 2023.04.10 |
---|---|
VO(Value Object)는 무엇일까? 왜 사용할까? (2) | 2023.03.12 |
도메인에 처음 접근하기 - Out -> In / In -> Out 접근 방식 (0) | 2023.02.27 |
포장 값을 View로 전달하는 방식으로 무엇을 사용할까? (2) | 2023.02.27 |
[Java] ArrayList, LinkedList를 직접 구현해보며 이해한 것 (0) | 2023.02.26 |