이번주에 진행한 코드스쿼드 미션에서는 다음과 같은 프로그래밍 요구사항이 주여졌다.
- 메서드의 크기가 최대 10라인을 넘지 않도록 구현한다.
- 들여쓰기(indent) depth를 2단계에서 1단계로 줄여라.
- else를 사용하지 마라.
메서드의 크기와 들여쓰기 제한도 어려웠지만, else
를 사용하지 않고 코드를 짜라니. 어떤 의도인지 알기 어려웠다. 단순히 else
블록을 if
블록으로 조건과 함께 명시적으로 작성하라는 걸까?
else 블록 제거하기
step 1
https://github.com/rxdcxdrnine/java-ladder/blob/step1/src/main/LadderChar.java
LadderGame.java
private void addLadderChar(int i, int j) {
if (j % 2 == 0)
this.map[i][j] = LadderChar.VERTICAL;
} else if (RandomUtil.nextBoolean()) {
this.map[i][j] = LadderChar.SPACE;
} else {
this.map[i][j] = LadderChar.HORIZONTAL;
}
}
LadderChar.java
public enum LadderChar {
VERTICAL('|'),
HORIZONTAL('-'),
SPACE(' ');
private final char chr;
LadderChar(char chr) {
this.chr = chr;
}
}
일단 현재 들어있는 코드 중에서 if-else
블록을 포함한 코드는 위와 같았다. addLadderChar
함수의 파라미터로 변수 i
, j
를 받아 LadderChar
열거형 중에 하나를 this.map
에 할당한다. (지금 다시보니 굳이 this
예약어를 붙일 필요가 없었는데, 참고바란다.)
일단 이 코드를 if-else
블록 대신 if
문만 사용해 명시적으로 작성해봤다.
step 2
https://github.com/rxdcxdrnine/java-ladder/blob/step2/src/main/LadderGame.java
private void changeChar(int row, int col) {
if (col % 2 == 0) {
this.map[row][col] = LadderChar.VERTICAL;
}
if (col % 2 != 0) {
changeRandomChar(row, col);
}
}
private void changeRandomChar(int row, int col) {
boolean rand = RandomUtil.nextBoolean();
if (rand) {
this.map[row][col] = LadderChar.SPACE;
}
if (!rand) {
this.map[row][col] = LadderChar.HORIZONTAL;
}
}
리팩토링 과정에서 함수명과 변수명이 바뀌고 함수가 나뉘었지만, 함수의 구조가 step1
과 크게 달라지지 않았다. changeChar
함수에서 row
, col
변수를 받아 그 값에 따라 LadderChar
를 할당하는데, col
인자의 값이 홀수일 경우에는 changeRandomChar
함수를 호출해 랜덤하게 LadderChar
열거형의 상수를 할당한다.
위와 같이 리팩토링을 진행한 이유는 처음에 보였던 아래의 요구사항 때문이었다.
- 메서드의 크기가 최대 10라인을 넘지 않도록 구현한다.
- 들여쓰기(indent) depth를 2단계에서 1단계로 줄여라.
- else를 사용하지 마라.
step1 의 addLadderChar
함수 안의 else if
예약어와 else
예약어를 제거하고, if
문 안에 조건들을 추가해 함수를 수정했는데, 막상 수정한 함수의 크기가 10라인을 넘었기 때문에 위와 같이 2개의 함수로 분리했다.
이렇게 바꾸면 if
문 안의 조건만 보고 어떤 조건으로 분기되는지 확실하게 알 수 있으니, 요구사항이 의도한 부분과 어느정도 맞지 않을까 생각했다. 하지만 다음과 같은 PR 코멘트를 받을 수 있었다.
아쉽게도 의도한 방향이 아니었다.
그렇다면 if
문의 코드를 왜 바꿔야할까? 일단, 현재 코드의 문제점부터 생각해봤다. step2 의 코드에서 changeChar
함수와changeRandomChar
함수 안에는 LadderChar
열거형의 상수를 생성하기 위한 조건들이 정의되어있다.
하지만 위와 같이 절차적으로 분기하는 함수에 다른 조건이 추가되었다고 생각하자. 예를 들어, changechar
함수에 row
조건에 따라 LadderChar
열거형의 상수가 바뀌어야하는 로직이 추가되어야 한다. 그럼 changeChar
함수 내부에 조건들을 추가하기 위해 기존의 함수를 수정해야하고, 만약 기존에 테스트 코드가 존재했다면 테스트 코드 또한 수정해야한다.
객체지향 관점에서 생각하기
그렇다면 이 문제를 해결하기 위해 어떤 설계를 생각해야할까? 마침 어제 책 오브젝트 7장을 읽었는데, 추상 데이터 타입과 클래스를 비교하면서 ‘객체지향이란 조건문을 제거하는 것’ 이라는 키워드가 등장한다. 해당 부분의 내용을 인용하면 다음과 같다.
객체지향에서는 타입 변수를 이용한 조건문을 다형성으로 대체한다. 클라이언트가 객체의 타입을 확인한 후 적절한 메서드를 호출하는 것이 아니라 객체가 메시지를 처리할 적절한 메서드를 선택한다. 흔히 ‘객체지향이란 조건문을 제거하는 것’ 이라는 다소 편협한 견해가 널리 퍼진 이유가 바로 이 때문이다.
모든 설계 문제가 그런 것처럼 조건문을 사용하는 방식을 기피하는 이유 역시 변경때문이다. ... 객체지향은 새로운 타입을 구현하는 클래스를 기존 상속 계층에 추가하고 필요한 메서드를 오버라이딩하면 된다. 새로 추가된 클래스의 메서드를 실행하기 위한 어떤 코드도 추가할 필요가 없다. 이것은 시스템에 새로운 로직을 추가하기 위해 클라이언트 코드를 수정할 필요가 없다는 것을 의미한다.
위의 내용에 따라 현재 작성한 코드의 문제점을 바꿔보면, 클라이언트인 changeChar
에서 새로운 로직을 추가해야할 경우 클라이언트의 코드를 수정해야한다. 기존 코드를 ‘변경’하는 것은 항상 새로운 버그를 만들어낼 확률을 높이기 때문에 시스템은 ‘변경’에 취약해질 수 밖에 없고, 따라서 조건문을 사용하는 방식을 ‘변경’ 때문에 기피해야한다.
그렇다면 객체지향적인 설계를 고려해 어떻게 코드를 수정해야할까? 위에서 인용한 글과 다르게, 현재 코드의 LadderChar
는 일반적인 클래스가 아니라 열거형이고, changeChar
에서는 LadderChar
의 타입 변수에 따라 if
문을 이용해 분기하는 것이 아니라 changeChar
메서드가 포함된 인스턴스 내 멤버 변수의 값에 따라 분기한다.
열거형에 메서드 추가하기
바로 앞에서 열거형이 일반적인 클래스와 다르다고 말했지만, 사실 열거형 또한 (당연하게도) 클래스이다. 위의 LadderChar
열거형을 클래스로 나타내면 아래와 같다.
LadderChar.java
class LadderChar {
static final LadderChar VERTICAL = new LadderChar('|');
static final LadderChar HORIZONTAL = new LadderChar('-');
static final LadderChar SPACE = new LadderChar(' ');
private final char chr;
private LadderChar(Char chr) {
this.chr = chr;
}
}
그리고 클래스이기 때문에 열거형에도 일반적인 클래스와 마찬가지로 메서드를 추가할 수 있다. 그렇다면 changeChar
내에서 조건에 따라 LadderChar
를 할당하는 대신, LadderChar
의 메서드로 조조건을 정의하고 changeChar
에서 메서드를 호출하는 방식을 사용하면 어떨까?
열거형에 메서드 추가하기
열거형에 메서드를 추가하는 방법은 2가지가 있다.
1. 추상 메서드 정의하고 구현하기
열거형에 추상메서드 check(int col, int rand)
를 정의하면 각 열거형 상수가 이 추상 메서드를 구현한다.
LadderChar.java
public enum LadderChar {
VERTICAL('|') {
boolean check(int col, boolean rand) {
return col % 2 == 0;
}
},
HORIZONTAL('-') {
boolean check(int col, boolean rand) {
return col % 2 == 0 & rand;
}
},
SPACE(' ') {
boolean check(int col, boolean rand) {
return col % 2 == 0 & !rand;
}
};
private final char chr;
LadderChar(char chr) {
this.chr = chr;
}
abstract boolean check(int col, boolean rand);
}
2. 인터페이스 정의하고 구현하기
별도의 인터페이스에 메서드를 정의하고 열거형이 이를 implments
하면 각 열거형 상수가 이 오퍼레이션을 구현한다.
LadderOperation.java
public interface LadderOperation {
boolean check(int ind, boolean next, boolean rand);
}
LadderChar.java
public enum LadderChar implments LadderOperation {
VERTICAL('|') {
@override
boolean check(int col, boolean rand) {
return col % 2 == 0;
}
},
HORIZONTAL('-') {
@override
boolean check(int col, boolean rand) {
return col % 2 == 0 & rand;
}
},
SPACE(' ') {
@override
boolean check(int col, boolean rand) {
return col % 2 == 0 & !rand;
}
};
private final char chr;
LadderChar(char chr) {
this.chr = chr;
}
}
그렇다면 열거형에 정의된 메서드를 어떻게 사용해야할까? 열거형에 정의된 메서드를 직접적으로 호출하는게 아니라, 각 열거형 상수마다 메서드로 정의된 조건을 호출해야한다. 따라서 별도의 정적 팩토리 함수를 정의해, 열거형 상수마다 조건을 확인하고 일치하는 열거형 상수를 반환하도록 한다.
static LadderElement create(int col, boolean rand) {
return Arrays.stream(LadderElement.values())
.filter(e -> e.check(col, rand))
.findAny()
.orElseThrow(IllegalStateException::new);
}
위 팩토리 메서드는 열거형 내부에 들어가도되고, 열거형이 퍼블릭 인터페이스를 구현하는 경우 인터페이스에 들어가도 된다.
그리고 마지막으로 클라이언트인 changeChar
에서는 다음과 같이 create
함수를 호출하면 된다. 위 팩토리 메서드가 인터페이스 LadderOperation
에 들어있다고 가정한다.
private void changeChar(int row, int col) {
Boolean rand = boolean rand = RandomUtil.nextBoolean();
this.map[row][col] = LadderOperation.create(col, rand);
}
if
문에 따른 분기가 없어졌고, 앞으로 다른 조건에 의한 LadderChar
열거형 할당이 필요한 경우에도 열거형 내부의 상수를 새로 정의하고 check
메서드를 구현하면 된다. 객체지향의 관점에서 보면, LadderChar
객체에 대해 책임이 잘 정의된 구조로 변경되었다.
객체지향의 장점
열거형에 메서드를 정의했으니 객체지향의 장점을 더 활용해보도록 하자. 앞으로 요구사항이 늘어나 새로운 기능이 추가됐을 때, 퍼블릭 인터페이스에 오퍼레이션을 정의하고 열거형에 메서드를 구현할 수 있다.
미션의 단계가 높아지면서 새로운 요구사항이 추가되었는데, 기능을 구현하면서 입력한 LadderChar
타입에 따라 booelan
값을 반환하는 함수를 만들어야했다. 만약 열거형의 상수 별로 메서드를 정의하지 않으면, 아래와 같이 if-else
블록으로 작성할 것이다.
public getNext(LadderElement element, boolean prev) {
if (element == LadderElement.VERTICAL) {
return prev;
else if (element == LadderElement.HORIZONTAL) {
return false;
else {
return true;
}
}
그리고 클라이언트 객체에서 아래와 같이 함수를 호출할 것이다.
LadderLine.java
private void addAll() {
boolean next = true;
for (int ind = 0; ind < length; ind++) {
boolean rand = RandomUtil.nextBoolean();
LadderElement element = add(ind, next, rand);
next = getNext(element, prev);
}
}
하지만 앞에서 살펴봤던 것처럼, 객체 내부에 멤버 변수로 정의된 타입에 의해 객체를 구분하고 내부의 메서드를 호출 (위의 코드에서는 메서드 호출을 하지는 않지만) 하는 것은 객체지향이 아니다. 따라서 열거형의 상수에 메서드를 정의하고, 클라이언트가 메서드를 호출하면 다형성에 의해 구현체가 실행되도록 작성해보자.
step 5
https://github.com/rxdcxdrnine/java-ladder
LadderElement.java
public enum LadderElement implements LadderOperation {
VERTICAL("|") {
@Override
public boolean check(int col, boolean next, boolean rand) {
return col % 2 == 0;
}
@Override
public boolean getNext(boolean prev) {
return prev;
}
},
HORIZONTAL("-----") {
@Override
public boolean check(int col, boolean next, boolean rand) {
return col % 2 != 0 && next && rand;
}
@Override
public boolean getNext(boolean prev) {
return false;
}
},
BLANK(" ") {
@Override
public boolean check(int col, boolean next, boolean rand) {
return col % 2 != 0 && (!next || !rand);
}
@Override
public boolean getNext(boolean prev) {
return true;
}
};
private final String str;
LadderElement(String str) {
this.str = str;
}
}
(기능이 추가되면서 클래스의 역할이 달라져 LadderChar
클래스의 이름을 LadderElement
로 수정했다. check
함수도 조금 바뀌었는데, 모른체 해주자.)
그리고 퍼블릭 인터페이스에 오퍼레이션을 추가하자.
public interface LadderOperation {
static LadderElement create(int col, boolean next, boolean rand) {
return Arrays.stream(LadderElement.values())
.filter(e -> e.check(col, next, rand))
.findAny()
.orElseThrow(IllegalStateException::new);
}
boolean check(int ind, boolean next, boolean rand);
boolean getNext(boolean prev);
}
이번엔 클라이언트에서 getNext()
메서드를 호출할 때 LadderElement
객체를 인자로 입력하지 않고, LadderElement
객체로부터 메서드를 호출할 것이다.
LadderLine.java
private void addAll() {
boolean next = true;
for (int ind = 0; ind < length; ind++) {
boolean rand = RandomUtil.nextBoolean();
LadderElement element = add(ind, next, rand);
next = element.getNext(next);
}
}
LadderElement
객체로 책임을 나누고 객체 간에 협력하도록 수정되었다. 객체지향의 원리를 조금 더 추구하게 되어 기쁘다.
출처
오브젝트 : 코드로 이해하는 객체지향 설계
http://redutan.github.io/2016/03/31/anti-oop-if
https://tecoble.techcourse.co.kr/post/2020-07-29-dont-use-else/
'Java' 카테고리의 다른 글
코드로 스프링 트랜잭션 API 구조 보기 (0) | 2022.05.12 |
---|---|
Spring Data JDBC 에서 연관관계 매핑 (4) | 2022.04.25 |
주소 변환 (address translation) 의 원리 (0) | 2022.01.20 |
JVM 과 스택 프레임 (Stack Frame) (0) | 2022.01.19 |
스트림 (Stream) API (0) | 2022.01.13 |
댓글