이번에 우테코를 지원하면서 떨리는 마음으로 프리코스 1주차 미션을 받았다.
기능 요구사항에 맞게 구현 했다고는 하지만 여전히 코드를 보면 리팩토링할 여지는 너무 많은 코드였던거 같다.. 이미 제출한 시점에도 부족한 부분이 너무나 많다고 느낀다.
애초에 해당 미션을 수행하는데에 있어서 걸리는 시간은 하루도 채 걸리지 않은 거같다.
근데 미션 제출기한이 1주일이나 되다보니... 뭔가 1주일이라는 시간을 준 이유가 있지 않을까... 하면서 코드를 보고 또 보고 하게 되었다.
이러다 보니 뭔가 어차피 제출할 코드이지만 뭔가 애정이 가게 되고 계속해서 보다보니 바꿀 여지가 있는 코드가 보였달까....
기능 요구 사항

메인 비즈니스 로직
import java.util.List;
public class Calculator {
public int calculate(String input) {
if (input.isEmpty()) {
return 0;
}
if (DelimiterParser.isDefaultDelimiter(input)) {
String replace = input.replace(",", ",")
.replace(":", ",");
System.out.println("replace = " + replace); // 1,2,3
List<Integer> inputNums = DelimiterParser.splitInputAsString(replace);
Validator.validateIfInputNegative(inputNums);
return inputNums.stream()
.mapToInt(Integer::valueOf)
.sum();
}
if (!DelimiterParser.isDefaultDelimiter(input)) {
String replaceInput = input.replace("//", "")
.replace("\\n", "");
String customDelimiter = replaceInput.substring(0, 1);// 구분자 ;
String[] split = replaceInput.substring(1).split(customDelimiter);
String splitString = String.join(",", split);
List<Integer> inputNums = DelimiterParser.splitInputAsString(splitString); // -> 123 백이십삼
return inputNums.stream()
.mapToInt(Integer::valueOf)
.sum();
}
return 0;
}
}
public class DelimiterParser {
private static final String COMMON_DELIMITER = ",";
private final List<String> delimiterList = new ArrayList<>();
public DelimiterParser(String delimiter) {
delimiterList.add(delimiter);
}
public DelimiterParser(String... delimiterList) {
this.delimiterList.addAll(Arrays.asList(delimiterList));
}
public String splitByDelimiter(String input) {
String result = input;
for (String delimiter : delimiterList) {
if (delimiter.equals(input.substring(0,2))) {
result = result.replace(input.substring(0,2), "");
continue;
}
if (delimiter.equals(input.substring(3,5))) {
result = result.replace(input.substring(3,5), "");
continue;
}
result = result.replace(delimiter, COMMON_DELIMITER);
}
return result;
}
public static boolean isDefaultDelimiter(String input) {
String[] defaultDelimiterNumber = input.split(COMMON_DELIMITER);
try {
Integer.parseInt(defaultDelimiterNumber[0]);
return true;
} catch (NumberFormatException e) {
return false;
}
}
public static List<Integer> parseToIntList(String input) {
String[] split = input.split(COMMON_DELIMITER);
List<Integer> list = new ArrayList<>();
for (String splitNumber : split) {
list.add(Validator.validateIfNotNumber(splitNumber));
}
return list;
}
}
맨 처음에 기능구현 사항을 읽자마자 떠오른 대로 작성한 코드이다.
사실 이렇게 작성한 시점에서 이미 충분히 완벽하지 않나?? 더 나눌게 있나?? 라는 생각을 가지게 되었었다....
Calculator.java 클래스는 문자열을 받아서 DelimiterParser.java에게 구분자 처리를 한 input에 대한 데이터를 넘긴다. // 1,2,3 이러한 형태의 데이터만 남게된다.
DelimiterParser에서는 해당 데이터를 가지고 문자열이므로 -> Integer 형태로 바꾼 후 Validation을 거치고 List를 반환하면 Calculator.java에서는 걔네를 더해서 반환한다.
기능 구현 사항에서 구분자를 기준으로 기본 구분자를 통한 덧셈 문자열인지, 아니면 커스텀 구분자를 통한 덧셈 문자열인지 각 분기에 따른 조건문을 타고 해당 로직을 수행하는 코드이다.
이미 여기까지 작성한 시점에서 우테코에서 내준 과제의 테스트 코드는 문제없이 잘 통과 되었었다.
예전 같았으면은 여기까지 코드를 작성한 채로 뒤도 돌아보지 않고 인텔리제이 창을 껏을것이다.
하지만 불안하게 만든점은 바로 과제제출 기간이 1주일이라는 점이다... PR자체는 바로 할 수 있지만 우테코 페이지에서 과제 제출은 그 주에서 일요일 오후3시를 기점으로 제출할 수 있게 해놨다.
그 주어진 시간동안 사실 할게 없어서 코드를 계속 쳐다봤다. 디버깅을 하면서 중복로직도 찾아내고 SOLID 원칙도 다시 찾아보면서 뭔가 더 깔끔하게 할 수 있을 거란 생각이 들어서 바로 리팩토링 작업에 들어가면서 나의 문제점을 파악할 수 있었다.
첫번째 문제점
현재 나의 코드는 SRP원칙에 어긋나있었다.
근데 위의 코드는 스플릿 작업과 함께 인자로 들어온 input이 기본 구분자인지 커스텀 구분자인지 아닌지에 대해서 검사하고 Validation도 함께하고, 왜 로직을 짜면서 바로 보이지 않았을까???
두번째 문제점
현재 나의 코드에는 중복로직이 많다.
사용자 입력값에 대해서 각 분기마다 어쨌든 split 작업을 통해 구분자를 추출하는 로직이 존재한다. 기본 구분자던 커스텀 구분자던 상관없이 무조건 split함수를 통해서 구분자를 추출해야한다고!!!! 근데 왜 각 분기마다 구분자를 추출하는 로직이 존재하냐 이말이다.
그냥 애초에 바로 사용자입력을 받자마자 split 작업이라던가 사용자 입력값에 대한 Validation 작업을 통해 검증된 데이터를 보내는 식으로 했어야 되는 부분이다. 왜이렇게 생각없이 하니???
세번째 문제점
기본기가 부족하다... 경험이 부족한건가??
this.delimiterList.addAll(Arrays.asList(delimiterList)); -> asList 왜씀??
asList는 데이터의 안정성이 보장이 되지 않는다. 해당 함수로 List를 만들어 버리면 언제든지!! 어디서든지!!!! 원본 데이터가 변할 수 있다. 그러니까 불변인 List의 생성자 메서드인 of를 사용하자...
public static boolean isDefaultDelimiter(String input) {
String[] defaultDelimiterNumber = input.split(COMMON_DELIMITER);
try {
Integer.parseInt(defaultDelimiterNumber[0]);
return true;
} catch (NumberFormatException e) {
return false;
}
}
아니 try ~ catch 부분을 왜 DelimiterParser가 하고있는지 모르겠다 이 또한 Validator로 옮기고 Parser에서 호출하는 형식으로 변경하자..
네번째 문제점
유연성 및 확장성을 고려하지 않는 설계방식...
뭔가 보이잖아 이... 뭐랄까... 기본 구분자 정책, 커스텀 구분자 정책, 어어.. 이거 김영한님 자바 기초강의에서 나오는데 디자인패턴중에 전략 패턴이라고...
public interface DelimiterPolicy {
String appliyDelimiterPolicy(String input);
}
public class CustomPolicy implements DelimiterPolicy{
@Override
public String appliyDelimiterPolicy(String input) {
// 커스텀 구분자 적용 로직 input이 //로 시작한다면 고것은 커스텀 구분자를 적용하겠다는 것이지
}
}
public class DefaultPolicy implements DelimiterPolicy{
@Override
public String appliyDelimiterPolicy(String input) {
// 기본 구분자 , 또는 : 일 경우 구분자 추출 후 숫자 추출 로직...
}
}
public abstract class DelimiterFactory {
public static DelimiterPolicy getPolicy(String input) {
if (input.contains("기본 구분자가 포함되어있다면??")) {
return new DefaultPolicy();
}
// 그게 아니라면 Custom정책 반환!!
return new CustomPolicy();
}
이렇게 인터페이스 적용하고, 그 구현체 적용하고, input되는 값에 따라서 어떤 정책을 적용할지에 대해서 구현체 반환하고
public List<Integer> parseInputToIntList(String input) {
DelimiterPolicy policy = DelimiterFactory.getPolicy(input);
String standardize = policy.appliyDelimiterPolicy(input);
// return list...
이렇게 했을 경우 정책이 변한다?? 새로운 구현체를 만들어주면 되므로 OCP 원칙을 위반하지 않는 코드가 되었다.
Enum 클래스는 상수 열거형 클래스이면서 객체 지향적인 특성도 갖고있어서 메서드를 호출할 수도 있는 특징이 있으므로 이를 구현하였다
public enum ParserPolicy {
DEFAULT(ParserPolicy::applyDefaultDelimiter),
CUSTOM(ParserPolicy::applyCustomDelimiter);
private final Function<String, String> parseFunction;
ParserPolicy(Function<String, String> parseFunction) {
this.parseFunction = parseFunction;
}
public static ParserPolicy strategy(String input) {
// 여기서 input 값에 따른 분기처리해주고
}
public String parseAndStandardize(String input) {
return parseFunction.apply(input);
}
private static String applyDefaultDelimiter(String input) {
return // 기본 구분자 로직 적용해주고
}
private static String applyCustomDelimiter(String input) {
return // 커스텀 구분자일 경우 로직 적용해주고
}
}
// 클라이언트 클래스에서 호출
public class Calculator {
private final InputParser inputParser = new InputParser();
public int calculate(String input) {
return inputParser.parseInputToIntList(input)
.stream()
.mapToInt(Integer::intValue)
.sum();
}
// InputParser.class
public List<Integer> parseInputToIntList(String input) {
ParserPolicy policy = ParserPolicy.strategy(input);
String standardize = policy.parseAndStandardize(input);
return convertToIntList(standardize);
}
}
끝이다... 이렇게 하면 클라이언트 코드들은 어떠한 수정없이 enum클래스에서 새로운 정책 및 정책에 대한 변경에 대해서 유연하게 대처할 수 있게 되었다.
느낀점
1주차 만으로도 정말 많은 것들을 배웠다. 나는 갈길이 멀구나... 이런 작업을 할 수록 정말 완벽한 형태의 코드는 무엇일까?? 베스트 프랙티스에 대한 부분을 누가 코드로 올려놔주지 않을까?? 정답이 없는거같다 라는 점만 배운다... 하지만 이런식으로 발전해나가면서 점점 점진적으로 배우는 재미도 있다.
2주차에 대한 프리코스도 현재 끝이났지만 2주차에 대한 회고록은 3주차를 다 하고나서 정말 1주차와 같이 나의 개발 흐름을 보면서 회고하면서 작성해보도록 하겠다.
'회고록' 카테고리의 다른 글
| [인프런 워밍업 클럽 스터디 - 테스트 코드를 대하는 자세 2주차 회고] 어떤 것이 더 적절한 단위 테스트인가? (0) | 2025.03.17 |
|---|---|
| [우테코 7기 백엔드 회고] 4주차 편의점 (1) | 2024.11.18 |
| [우테코 7기 백엔드 회고]3주차 로또 (0) | 2024.11.12 |
| [우테코 7기 백엔드 회고] 2주차 레이싱 게임 (2) | 2024.11.05 |