오프라인 코딩테스트를 리뷰하면서 어떤 것을 배웠고 부족했던점들은 무엇이 있는지에 대해 정리합니다.
당시 제출 하였던 소스코드는 Github에서 확인하실 수 있습니다.
오프라인 코딩테스트가 끝나고, 내가 제출한 메일과 코드를 보면서 한동안 많이 힘들었다.
내가 부족한 점이 무엇인지, 단점은 무엇인지 여실히 드러났다고 생각한다.
나는 욕심이 많은 사람이다.
개발자로서의 나의 꿈과 욕심은 실력으로 인정 받는 것, 그리고 실력을 키워 많은 사람들이 귀 기울일 수 있을만한 좋은 내용을 공유할 수 있는 사람이 되는 것이다.
어쩌면 흔히 말하는 스타 개발자가 되는 것, 또는 개발자들의 개발자가 되는 것일지도 모르겠다.
멋진 사람이 되고 싶어하고, 좋은 사람들과 함께 하고 싶어한다.
프리코스를 진행하면서 스스로 많이 부족하다고 느꼈다.
나보다 잘하는 사람이 너무나도 많다는 생각과 함께, 이런 사람들과 함께 할 시간에 대한 기대 또한 정말 컸다.
잘하고 싶다는 생각과, 빨리 성장해야 한다는 생각이 항상 머릿속에 있었다.
그런 생각들이 오프라인 코딩테스트까지 이어져, 내가 봤던 잘하는 사람들은 분명 완벽하게 미션을 수행할 것이다, 나도 잘해야만 한다. 라는 생각을 하게 되었고, 그것이 욕심이 되었던 것 같다.
내가 오프라인 코딩테스트에서 보여줘야 했던것은 모든 요구사항을 구현하는 것이 아닌, 하나를 구현하더라도 제대로 구현하는 것이었다.
나는 오프라인 코딩테스트에서 보여줘야 할 모습을 고지 받았음에도, 까맣게 잊어버리고 모든 요구사항을 구현하는 것에 욕심을 부렸다.
많은 부분에서 코드 컨벤션을 지키지 못한 부분들을 발견했고, 동작하지 않는 기능도 있었다.
이런 나의 모습에 많이 실망했고, 속상했다.
오프라인 미션을 통해 내 단점이 무엇인지 알게 되었다. 욕심이 많고, 무언가에 몰입하게 되면 중요한 것을 놓칠때가 많다는 것.
모든 것을 다 손에 쥐려하면 능력 밖의 것은 손가락 사이로 빠져나가기 마련이다.
극복 해야한다.
3주차 피드백으로 일급 컬렉션과 원시 타입과 문자열을 객체로 포장하는 것에 대한 내용을 받았고, 오프라인 코딩 테스트에서 꼭 적용 해보자라고 생각했다.
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
this.orders = orders;
}
public class Order {
private final Table table;
private final Menu menu;
private final MenuAmount menuAmount;
public Order(Table table, Menu menu, MenuAmount menuAmount) {
this.table = table;
this.menu = menu;
this.menuAmount = menuAmount;
}
public class MenuAmount {
private final int amount;
public MenuAmount(int amount) {
checkAmount(amount);
this.amount = amount;
}
public class Price {
private double price; // final이 붙었어야 한다.
public Price(int price, double discounts) {
this.price = price * (1 - discounts);
}
사장님의 포스기기가 Orders를 인스턴스 변수로 갖고 있는 형태로 구현을 진행하였고, 이 밖에도 가격에 관한 부분도 객체로 포장하여 사용했다.
피드백으로 받았을때, 생성자에서 유효성을 테스트 하는 방식이 상당히 효과적이고 매력적인 방식이라고 생각하여, 그 부분을 적용하고 싶었고, MenuAmount에서 적용해 볼 수 있었다.
이 부분 말고도 정말 많은 부분에 space와 tab이 혼용 되어있다.
pull request를 보낸후 확인을 했거나, 에디터에서 스페이스와 탭을 구별해서 보여주도록 설정했다면 하지 않았을 실수였다.
public class Pos {
private static final int ORDER_REGISTER_NUMBER = 1;
private static final int PAYMENT_NUMBER = 2;
private static final int CASH = 2;
private static final double CASH_DISCOUNT_AMOUNT = 0.05;
private static final double NO_DISCOUNT = 0.0;
private static final int QUIT_NUMBER = 3;
private final Orders orders;
public Pos() {
orders = new Orders(new ArrayList<>());
}
private static int getFunctionNumber() {
OutputView.printMainScreen();
return InputView.inputFunctionNumber();
}
public void play() {
int functionNumber = getFunctionNumber();
if (functionNumber == ORDER_REGISTER_NUMBER) {
registerOrder();
return;
}
if (functionNumber == PAYMENT_NUMBER) {
pay();
return;
}
if (functionNumber == QUIT_NUMBER) {
throw new IllegalArgumentException("프로그램 종료");
}
OutputView.printReEnter();
}
private void registerOrder() {
OutputView.printTables(TableRepository.tables(), orders);
Table table = createTable();
OutputView.printMenus(MenuRepository.menus());
int menuNumber = InputView.inputMenuNumber();
if (!InputValidator.isValidMenuNumber(menuNumber)) {
OutputView.printInvalidMenuNumber();
return;
}
Menu menu = MenuRepository.getMenu(menuNumber);
MenuAmount menuAmount = new MenuAmount(InputView.inputMenuAmount());
Order order = new Order(table, menu, menuAmount);
orders.add(order);
}
private Price getPayAmount(int tableNumber) {
OutputView.printMenuAccounts(tableNumber, orders);
int paymentWayNumber = InputView.inputPaymentWayNumber(tableNumber);
return calculatePaymentPrice(paymentWayNumber, tableNumber);
}
private Price calculatePaymentPrice(int paymentWayNumber, int tableNumber) {
if (paymentWayNumber == CASH) {
return new Price(orders.getTotalPrice(tableNumber), CASH_DISCOUNT_AMOUNT);
}
return new Price(orders.getTotalPrice(tableNumber), NO_DISCOUNT);
}
private Table createTable() {
int tableNumber = InputView.inputTableNumber();
if (!InputValidator.isValidTableNumber(tableNumber)) {
OutputView.printReEnter();
createTable();
}
return new Table(tableNumber);
}
private void pay() {
if (orders.isEmpty()) {
OutputView.printNoOrder();
return;
}
OutputView.printTables(TableRepository.tables(), orders);
int tableNumber = InputView.inputTableNumber();
if (!orders.isOrderIn(tableNumber)) {
OutputView.printNoOrderInTable();
return;
}
OutputView.printTotalPayment(getPayAmount(tableNumber));
orders.deleteOrder(tableNumber);
}
}
깨끗한 코드는 단순하고 직접적이다. 깨끗한 코드는 잘 쓴 문장처럼 읽힌다. 깨끗한 코드는 결코 설계자의 의도를 숨기지 않는다. 오히려 명쾌한 추상화와 단순한 제어문으로 가득하다.
- Grady Booch
클린 코드를 읽으면서 코드가 잘 쓴 문장처럼 읽혀야 한다는 말에 느낀점이 많았다.
내가 코드를 바라보는 시각에 변화가 생긴 순간이었고, '우리는 저자다'
라는 말에도 감명이 깊었다.
때문에 프리코스 기간 동안, 미션을 제출하기전 내 코드를 계속 읽어보며 읽기 쉬운 코드인지 확인하고 리팩토링 하는 과정을 거쳤었다.
하지만 오프라인 코딩테스트에선 기능 구현에 급급하여 좋지 못한 코드를 작성했다.
위 코드에서 메소드간 수직 거리를 고려하고, 내려가기 규칙을 지켜 작성한다면 훨씬 읽기 좋은 코드가 될 것이다.
public class Pos {
private static final int ORDER_REGISTER_NUMBER = 1;
private static final int PAYMENT_NUMBER = 2;
private static final int CASH = 2;
private static final double CASH_DISCOUNT_AMOUNT = 0.05;
private static final double NO_DISCOUNT = 0.0;
private static final int QUIT_NUMBER = 3;
private final Orders orders;
public Pos() {
orders = new Orders(new ArrayList<>());
}
public void play() {
int functionNumber = getFunctionNumber();
if (functionNumber == ORDER_REGISTER_NUMBER) {
registerOrder();
return;
}
if (functionNumber == PAYMENT_NUMBER) {
pay();
return;
}
if (functionNumber == QUIT_NUMBER) {
throw new IllegalArgumentException("프로그램 종료");
}
OutputView.printInvalidFunctionNumber();
}
private int getFunctionNumber() {
OutputView.printMainScreen();
return InputView.inputFunctionNumber();
}
private void registerOrder() {
Order order = getOrder();
orders.add(order);
}
private Order getOrder() {
Table table = createTable();
Menu menu = getMenu();
MenuAmount menuAmount = getMenuAmount();
return new Order(table, menu, menuAmount);
}
private Table createTable() {
OutputView.printTables(TableRepository.tables(), orders);
int tableNumber = InputView.inputTableNumber();
while (!InputValidator.isValidTableNumber(tableNumber)) {
OutputView.printInvalidTableNumber();
tableNumber = InputView.inputTableNumber();
}
return new Table(tableNumber);
}
private Menu getMenu() {
int menuNumber = getMenuNumber();
return MenuRepository.getMenu(menuNumber);
}
private int getMenuNumber() {
OutputView.printMenus(MenuRepository.menus());
int menuNumber = InputView.inputMenuNumber();
while (!InputValidator.isValidMenuNumber(menuNumber)) {
OutputView.printInvalidMenuNumber();
menuNumber = InputView.inputMenuNumber();
}
return menuNumber;
}
private MenuAmount getMenuAmount() {
return new MenuAmount(InputView.inputMenuAmount());
}
private void pay() {
if (orders.isEmpty()) {
OutputView.printNoOrder();
return;
}
int tableNumber = getTableNumber();
if (!orders.isOrderIn(tableNumber)) {
OutputView.printNoOrderInTable();
return;
}
OutputView.printTotalPayment(getPayAmount(tableNumber));
orders.deleteOrder(tableNumber);
}
private int getTableNumber() {
OutputView.printTables(TableRepository.tables(), orders);
return InputView.inputTableNumber();
}
private Price getPayAmount(int tableNumber) {
OutputView.printMenuAccounts(tableNumber, orders);
int paymentWayNumber = InputView.inputPaymentWayNumber(tableNumber);
return calculatePaymentPrice(paymentWayNumber, tableNumber);
}
private Price calculatePaymentPrice(int paymentWayNumber, int tableNumber) {
if (paymentWayNumber == CASH) {
return new Price(orders.getTotalPrice(tableNumber), CASH_DISCOUNT_AMOUNT);
}
return new Price(orders.getTotalPrice(tableNumber), NO_DISCOUNT);
}
}
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 나는 이것을 내려가기 규칙이라 부른다.
Clean Code - Robert C. Martin
그럼에도 이 코드엔 아쉬운 점이 많다.
클래스는 작아야한다. Pos 클래스는 이미 많은 책임을 지고 있다. 단일 책임 원칙을 위배한다.
Pos의 자식 클래스로 주문을 받는 클래스와 결제를 하는 클래스를 만드는 방법도 있었을 것이다.
Pos의 play 메소드도 이름이 적절하지 않다.
Pos는 게임이 아니다. run이나 start가 더 나았다.
public int getTotalPrice(int tableNumber) {
int totalPrice = 0;
int chickenCount = 0;
for (Order order : orders) {
if (order.isTableEquals(tableNumber)) {
totalPrice += order.getPrice();
}
if (order.isCategoryEquals(CATEGORY_NAME_OF_CHICKEN)) {
chickenCount++; // chickenCount += order.getMenuAmount(); 가 되야 한다.
}
}
if (chickenCount >= DISCOUNT_BOUNDARY) {
int discountTimes = chickenCount / DISCOUNT_BOUNDARY;
totalPrice -= DISCOUNT_AMOUNT_OF_CHICKEN * discountTimes;
}
return totalPrice;
}
단순한 실수가 아니다.
기능 구현을 했다면, 제대로 기능이 작동하는지 확실히 확인을 한 후, 다음 기능 구현을 했어야 한다.
'제발 문제 없이 돌아가줘'
하며 기도 코딩을 한 것과 진배없다.
public static Menu getMenu(int number) {
return menus.stream()
.filter(menu -> menu.isNumberEquals(number))
.findFirst()
.orElse(null);
}
null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다. 누구 하나라도 null 확인을 빼먹는다면 애플리케이션이 통제 불능에 빠질지도 모른다.
Clean Code - Robert C. Martin
getMenu 메소드를 호출할때, number에 대한 검증을 한 후 호출하는 방식으로 구현하여 null을 반환해도 안전하다고 생각했다.
하지만 null을 반환하는건 안좋은 습관이다. 구현 과정중 검증 없이 getMenu를 사용하는 경우가 생길지도 모른다.
특수 사례 객체를 만들어서 null 대신 반환하였다면, 훨씬 더 좋은 코드가 되었을 것이다.
private static final Menu UNDEFINED_MENU = new Menu(UNDEFINED_MENU_NUMBER, UNDEFINED_MENU_NAME, Category.UNDEFINED, NO_PRICE);
public static Menu getMenu(int menuNumber) {
return menus.stream()
.filter(menu -> menu.isNumberEquals(menuNumber))
.findFirst()
.orElse(UNDEFINED_MENU);
}
}
오프라인 코딩테스트만 보자면 정말로 아쉬움이 많은 시간이었습니다. 부족했다고 생각한 부분들이 작성한 것 외에도 캐치하지 못한 예외사항들, 아쉬운 변수 이름등 많았구요.
그래도 저의 이런 경험 공유를 통해 글을 읽고 있는 분들에게 조금이라도 도움이 되었다면 좋겠습니다.
더불어 객체 지향에 대한, 그리고 좋은 코드란 무엇인가 에 대한 지식이 조금이라도 전달 되었다면, 정말 행복할것 같아요.
오프라인 코딩테스트를 떠나, 우아한 테크코스의 프리코스에 대해 느낀점들은 테스트를 보기 전에 미리 작성하였으나 끝난 후에 느낀점도 상당히 많습니다.
이전글에서도 언급했지만 정말 재밌습니다.
저는 이어폰을 잃어버렸을때 이어폰의 소중함을 깨닫곤 합니다. 없어져야 소중함을 아는게 사람인건지..
학교의 종강과 함께 오프라인 코딩테스트가 끝나고 느낀것은, 후련함 보단 '내일 뭐하지?'
가 먼저 였습니다.
끝나고 나니, 우아한 테크코스를 지원한 이후 오프라인 코딩테스트까지 심심할 틈 없이 재밌었다는 걸 알게 되었네요.
우아한 테크코스를 지원을 고민하는 분이 있다면 전 꼭 지원하라고 말씀드리고 싶습니다.
프리코스 만으로도, 자신도 모르게 몰입하게 되고 많이 성장할 수 있는 과정입니다. 의심할 여지가 없이 좋은 경험을 할 수 있을 것입니다.
혹시라도 좋은 결과가 있게 된다면, 열심히 배워 좋은 지식들 공유 드릴수 있도록 노력하겠습니다.
긴 글 읽어주셔서 감사합니다. 😊
This overview is outstanding. The data provided is incredibly useful. I'm definitely going to focus on this. Exceptionally well done. https://www.pinkgoa.in/ Keep it up!
superb amazing very usefull content thanx for sharing https://goa.soniyathakur.com/
this link visit here
와우 정말 글 잘 쓰세요. 저도 이번 프리코스 결과 기다리는 중인데 같이 합격해서 만나뵈었으면 좋겠습니다!!