개요
우아한 테크코스 프리코스를 준비함에 있어서 객체지향적인 코드를 작성하려고 노력 중인데요, 예제 문제를 하나 들고와 이전에 제가 범 했던 실수 들과 이를 어떻게 객체지향 스럽게 변경할 수 있는지 다뤄보겠습니다!
1. 문제
기능 요구 사항
포비와 크롱이 페이지 번호가 1부터 시작되는 400 페이지의 책을 주웠다. 책을 살펴보니 왼쪽 페이지는 홀수, 오른쪽 페이지는 짝수 번호이고 모든 페이지에는 번호가 적혀있었다. 책이 마음에 든 포비와 크롱은 페이지 번호 게임을 통해 게임에서 이긴 사람이 책을 갖기로 한다. 페이지 번호 게임의 규칙은 아래와 같다.
1.
책을 임의로 펼친다.
2.
왼쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
3.
오른쪽 페이지 번호의 각 자리 숫자를 모두 더하거나, 모두 곱해 가장 큰 수를 구한다.
4.
2~3 과정에서 가장 큰 수를 본인의 점수로 한다.
5.
점수를 비교해 가장 높은 사람이 게임의 승자가 된다.
6.
시작 면이나 마지막 면이 나오도록 책을 펼치지 않는다.
포비와 크롱이 펼친 페이지가 들어있는 리스트/배열 pobi와 crong이 주어질 때, 포비가 이긴다면 1, 크롱이 이긴다면 2, 무승부는 0, 예외사항은 -1로 return 하도록 solution 메서드를 완성하라.
제한 사항
•
pobi와 crong의 길이는 2이다.
•
pobi와 crong에는 [왼쪽 페이지 번호, 오른쪽 페이지 번호]가 순서대로 들어있다.
실행 결과 예시
pobi | crong | result |
[97, 98] | [197, 198] | 0 |
[131, 132] | [211, 212] | 1 |
[99, 102] | [211, 212] | -1 |
사실 문제 자체는 크게 어렵지 않은데요, 기존의 코딩 테스트를 작성하는 방식으로 문제를 접근한게 화두가 되었습니다.
2. 기존의 코드와 해결 방안
package onboarding;
import java.util.ArrayList;
import java.util.List;
import problem1.Game;
class Problem1 {
static private final int GAME_OVER = -1;
public static int solution(List<Integer> pobi, List<Integer> crong) {
int answer = Integer.MAX_VALUE;
// Initialization
int pScore = 0;
int cScore = 0;
if (pobi.get(1) - pobi.get(0) != 1 || crong.get(1) - crong.get(0) != 1) {
return -1;
}
// Get pobi's score
for (Integer i : pobi) {
int lengthOfNumber = String.valueOf(i).length() - 1;
List<Integer> eachNumbers = new ArrayList<>();
while (i > 0) {
eachNumbers.add(i / (int) Math.pow(10, lengthOfNumber));
i %= (int) Math.pow(10, lengthOfNumber);
lengthOfNumber--;
}
int plusNumber = 0, multiplyNumber = 1;
for (int e : eachNumbers) {
plusNumber += e;
multiplyNumber *= e;
}
pScore = Math.max(plusNumber, multiplyNumber);
}
// Get crong's score
for (Integer i : crong) {
int lengthOfNumber = String.valueOf(i).length() - 1;
List<Integer> eachNumbers = new ArrayList<>();
while (i > 0) {
eachNumbers.add(i / (int) Math.pow(10, lengthOfNumber));
i %= (int) Math.pow(10, lengthOfNumber);
lengthOfNumber--;
}
int plusNumber = 0, multiplyNumber = 1;
for (int e : eachNumbers) {
plusNumber += e;
multiplyNumber *= e;
}
cScore = Math.max(plusNumber, multiplyNumber);
}
answer = pScore == cScore ? 0 : pScore > cScore ? 1 : 2;
return answer;
}
}
Java
복사
위 코드를 보시면 전혀 객체지향 스럽지 않은.. 오히려 절차지향 코드에 가깝습니다.
우선 객체지향의 5요소인 SOLID를 위배하고 있는데요, 코드를 조금 더 자세히 살펴보겠습니다.
2-1. 단일 책임의 원칙 위배 (Single Responsibility Principle)
위 코드는 너무 많은 책임을 가지고 있습니다.
•
Pobi, Crong의 숫자를 분해하여 각 자릿수를 추출하는 로직
•
각 자릿수의 합과 곱을 계산하는 로직
•
최종 승자를 결정하는 로직
사실 코드 라인이 100줄 이상을 넘어간다면 이를 위배하지 않았는지 고려할 필요가 있는데요, 위 코드는 너무나 명백하게 위배한 모습을 볼 수 있습니다.
모든 클래스는 하나의 책임만을 가져야 합니다.
그렇다면 위 클래스에서 가져야 할 책임은 무엇 일까요?
pobi와 crong. 둘 중 누가 이겼는지 판단하는 것
코드는 다음과 같습니다.
static private final int GAME_OVER = -1;
public static int solution(List<Integer> pobi, List<Integer> crong) {
try {
Game game = new Game(pobi, crong);
GameResult result = game.play();
return result.getResult();
} catch (IllegalArgumentException e) {
return GAME_OVER;
}
}
Java
복사
2-2. 개방-폐쇄 원칙 위배 (Open/Closed Principle)
현재 문제에서 요구하는 사항은 다음과 같습니다.
•
각 자리 숫자를 더하거나 곱하여 그 값이 가장 큰 쪽이 이긴다.
만약 문제에서 다른 방식의 점수 계산 로직을 필요로 한다면 어떡할까요?
solution() 메서드 자체를 수정해야 합니다.
따라서 책임을 다른 클래스에게 위임할 필요가 있습니다.
public class Player {
private final List<Integer> pages;
private final ScoreCalculator scoreCalculator;
public Player(List<Integer> pages, ScoreCalculator scoreCalculator) {
this.pages = pages;
this.scoreCalculator = scoreCalculator;
}
public int calculateTotalScore() {
return scoreCalculator.calculateTotalScore(pages);
}
}
Java
복사
위 코드를 보시면 포비와 크롱의 점수는 ScoreCalculator 클래스에서 처리하는 것을 확인할 수 있습니다.
더 자세하게 말하면 ScoreCalculator 객체 내 calculateTotalScore()메서드가 맡고 있습니다.
만약 점수 계산 로직이 바뀌었다면 다른 코드를 건드릴 필요 없이 해당 메서드의 로직만 수정해주면 됩니다.
간단한 문제로서 현재는 SRP와 OCP만 위반하였지만, 위 방식 대로 서비스를 계속 개발해나간다면 다른 SOLID 원칙 또한 위반할 가능성이 매우 높습니다.
3. 전체 코드
Problem1.java
package onboarding;
import java.util.List;
import problem1.Game;
import problem1.GameResult;
class Problem1 {
static private final int GAME_OVER = -1;
public static int solution(List<Integer> pobi, List<Integer> crong) {
try {
Game game = new Game(pobi, crong);
GameResult result = game.play();
return result.getResult();
} catch (IllegalArgumentException e) {
return GAME_OVER;
}
}
}
Java
복사
Game.java
package problem1;
import java.util.List;
public class Game {
private final Player pobi;
private final Player crong;
public Game(List<Integer> pobi, List<Integer> crong) {
GameValidator.validate(pobi, crong);
this.pobi = new Player(pobi, new ScoreCalculator());
this.crong = new Player(crong, new ScoreCalculator());
}
public GameResult play() {
int pobiScore = pobi.calculateTotalScore();
int crongScore = crong.calculateTotalScore();
return evaluateGameResult(pobiScore, crongScore);
}
private GameResult evaluateGameResult(int pobiScore, int crongScore) {
if (pobiScore > crongScore) {
return GameResult.POBI_WIN;
}
if (crongScore > pobiScore) {
return GameResult.CRONG_WIN;
}
return GameResult.DRAW;
}
}
Java
복사
GameResult.java
package problem1;
public enum GameResult {
POBI_WIN(1), CRONG_WIN(2), DRAW(0);
private final int result;
GameResult(int result) {
this.result = result;
}
public int getResult() {
return result;
}
}
Java
복사
GameValidator.java
package problem1;
import java.util.List;
public class GameValidator {
private static final int CONTINOUS_CRITERION = 1;
private static final int START_PAGE_NUMBER_LEFT = 1;
private static final int START_PAGE_NUMBER_RIGHT = 2;
private static final int END_PAGE_NUMBER_LEFT = 399;
private static final int END_PAGE_NUMBER_RIGHT = 400;
private static final String INPUT_EXCEPTION_MESSAGE = "올바르지 않은 입력 값 입니다.";
public static final int ODD_EVEN_DIVIDION = 2;
public static final int ODD_EVEN_CRITERION = 0;
public static void validate(List<Integer> pobi, List<Integer> crong) {
validate(pobi);
validate(crong);
}
private static void validate(List<Integer> player) {
int leftPageNumber = player.get(ScoreCalculator.LEFT_PAGE_INDEX);
int rightPageNumber = player.get(ScoreCalculator.RIGHT_PAGE_INDEX);
validateRightPage(leftPageNumber, rightPageNumber);
validateContinuousPage(leftPageNumber, rightPageNumber);
validateStartPage(leftPageNumber, rightPageNumber);
validateEndPage(leftPageNumber, rightPageNumber);
}
private static void validateEndPage(int leftPageNumber, int rightPageNumber) {
if (isEndPage(leftPageNumber, rightPageNumber)) {
throw new IllegalArgumentException(INPUT_EXCEPTION_MESSAGE);
}
}
private static void validateStartPage(int leftPageNumber, int rightPageNumber) {
if (isStartPage(leftPageNumber, rightPageNumber)) {
throw new IllegalArgumentException(INPUT_EXCEPTION_MESSAGE);
}
}
private static void validateContinuousPage(int leftPageNumber, int rightPageNumber) {
if (!isContinuousPage(leftPageNumber, rightPageNumber)) {
throw new IllegalArgumentException(INPUT_EXCEPTION_MESSAGE);
}
}
private static void validateRightPage(int leftPageNumber, int rightPageNumber) {
if (!isRightPage(leftPageNumber, rightPageNumber)) {
throw new IllegalArgumentException(INPUT_EXCEPTION_MESSAGE);
}
}
private static boolean isEndPage(int leftPageNumber, int rightPageNumber) {
return leftPageNumber == END_PAGE_NUMBER_LEFT
&& rightPageNumber == END_PAGE_NUMBER_RIGHT;
}
private static boolean isStartPage(int leftPageNumber, int rightPageNumber) {
return leftPageNumber == START_PAGE_NUMBER_LEFT
&& rightPageNumber == START_PAGE_NUMBER_RIGHT;
}
private static boolean isContinuousPage(int leftPageNumber, int rightPageNumber) {
return rightPageNumber - leftPageNumber == CONTINOUS_CRITERION;
}
private static boolean isRightPage(int left, int right) {
return left % ODD_EVEN_DIVIDION != ODD_EVEN_CRITERION
&& right % ODD_EVEN_DIVIDION == ODD_EVEN_CRITERION;
}
}
Java
복사
Player.java
package problem1;
import java.util.List;
public class Player {
private final List<Integer> pages;
private final ScoreCalculator scoreCalculator;
public Player(List<Integer> pages, ScoreCalculator scoreCalculator) {
this.pages = pages;
this.scoreCalculator = scoreCalculator;
}
public int calculateTotalScore() {
return scoreCalculator.calculateTotalScore(pages);
}
}
Java
복사
ScoreCalculator.java
package problem1;
import java.util.List;
public class ScoreCalculator {
private static final int INIT_SUM_VALUE = 0;
private static final int INIT_MULTIPLE_VALUE = 1;
private static final int DIVIDE_CRITERION = 10;
private static final int LOOP_BREAK_POINT = 0;
public static final int LEFT_PAGE_INDEX = 0;
public static final int RIGHT_PAGE_INDEX = 1;
private List<Integer> pages;
public int calculateTotalScore(List<Integer> pages) {
this.pages = pages;
return Math.max(calculatePageScore(pages.get(LEFT_PAGE_INDEX)),
calculatePageScore(pages.get(RIGHT_PAGE_INDEX)));
}
private int calculatePageScore(int page) {
return Math.max(calculateSumValue(page), calculateMultipleValue(page));
}
private int calculateSumValue(int page) {
int sumValue = INIT_SUM_VALUE;
while (page != LOOP_BREAK_POINT) {
sumValue += page % DIVIDE_CRITERION;
page /= DIVIDE_CRITERION;
}
return sumValue;
}
private int calculateMultipleValue(int page) {
int multipleValue = INIT_MULTIPLE_VALUE;
while (page != LOOP_BREAK_POINT) {
multipleValue *= page % DIVIDE_CRITERION;
page /= DIVIDE_CRITERION;
}
return multipleValue;
}
}
Java
복사