들어가며
이 글을 쓰는 나는 지금 매우 겸손해져 있다... 프리코스 공부하면서 생성형 AI 사용을 최대한 지양하자고 마음먹고 들어갔기에... 정말 많은 시간과 고민으로 6기 1주차 문제인 숫자 야구 문제를 풀어보았다.
https://github.com/geniusjun/java-baseball-6
GitHub - geniusjun/java-baseball-6
Contribute to geniusjun/java-baseball-6 development by creating an account on GitHub.
github.com
생각의 전환을 환기하고자 하루에 한개씩 브랜치를 그냥 새로 파서 풀다보니.. 브랜치가 5개가 되었다.. 그렇다.. 5일이 걸렸다.... 4번째 브랜치까지는 정말 혼자했다고 자부하지만 5번째 브랜치부터는 수많은 6기분들 코드리뷰의 도움과 ai의 도움으로 코드가 360도 달라졌다.

난 정말 바이브코더인가...? 지금 당장이라도 물어보면 AI가 잘 알려줄텐데 내가 코드 치는 연습을 할 필요가 있을까...? 라는 생각이 5일 내내 들었다.
그렇다면 잘하는 개발자란 무엇일까??
3자리 숫자 2개를 비교하며 스트라이크 볼을 세는, 어찌보면 간단한(?) 문제를 계층별로 쪼개고 단일책임원칙도 지키며 객체지향적인 코드로 리팩토링하는 과정에서 든 생각은 "아, 코드 아키텍쳐를 잘 알고, 상황에 맞는 구조로 개발하는 개발자가 잘하는 개발자구나" 이다.
AI를 잘 활용하는 개발자, 여러 기술 스택을 다루는 개발자도 틀린말은 아닐 것이다. 적어도 지금의 나는 코드를 계층적으로 잘 나누고 누가봐도 깔끔한 변수명, 로직을 개발하는 개발자가 잘하는 개발자라고 확신한다. 요즘같은 AI시대에 이래도 되는지는 모르겠다. 하지만 코드를 깔끔하게 잘 치는 개발자가 멋있고, 나도 그렇게 되고싶다. 그리고 그 차이가 개발자 포화시대에서의 차별점이 되기를 간절히 바래본다.
본론으로
처음부터 구조 생각하며 풀려니 너무 막막해서 그냥 절차지향적으로 어떻게든 테스트 코드만 통과시키자! 라는 마인드로 어찌저찌 풀었다. 이것도 사실 쉽지 않았다. 그러고 나니 말로만 듣던 스파게티 코드가 탄생했다.. 하나의 레포지토리 안에서 모든 것을 해결하는,,,
아래의 코드다... 뭐 하나라도 다른 클래스로 옮긴다면 바로 무너지는 코드다... 올려놓기 매우 부끄럽지만,,, 성장의 과정이라고 생각하고 블로그에 기록해본다.
public class Game {
public String computerInput;
public String playerInput;
public String reStart;
public boolean isCorrect;
public int countStrike;
public int countBall;
public String play(){
Init();
for(int i = 0; i < 3; i++) {
computerInput += Randoms.pickNumberInRange(1,9);
}
System.out.println(computerInput);
while (!isCorrect){
System.out.print("숫자를 입력해주세요 : ");
playerInput = Console.readLine();
Validate();
Check();
}
System.out.println("3개의 숫자를 모두 맞히셨습니다! 게임 종료");
System.out.println("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.");
reStart = Console.readLine();
if(reStart.equals("1")){
return "1";
}else if(reStart.equals("2")){
return "2";
}else{
throw new IllegalArgumentException();
}
}
private void Check(){
countStrike = 0;
countBall = 0;
for(int i = 0; i < 3; i++){ // 볼 몇개 스트라이크 몇개 인지
for (int j = 0; j < 3; j++){
if(computerInput.charAt(i) == playerInput.charAt(j)){
if(i == j) {
countStrike++;
}
else{
countBall++;
}
break;
}
}
}
if(countStrike == 0 && countBall == 0){ // 하나도 안맞은 경우
System.out.println("낫싱");
return;
}
if(countStrike == 3){
isCorrect = true; // 맞췄는지 알려주는 플래그 변수 true바꿔놓고 처리는 while문 밖에서
System.out.println(countStrike + "스트라이크");
return;
}
if (countStrike > 0 && countBall > 0){
System.out.println(countBall + "볼 " + countStrike + "스트라이크");
} else if (countBall > 0) {
System.out.println(countBall + "볼");
} else if (countStrike > 0) {
System.out.println(countStrike + "스트라이크");
}
}
public void Validate(){
if(playerInput.length() != 3){
throw new IllegalArgumentException();
}
}
public void Init(){
computerInput = "";
playerInput = "";
isCorrect = false;
countBall = 0;
countStrike = 0;
}
}
일단은 문제 요구사항에서 요구하는 ./gradlew clean build는 통과해서 한숨돌리고 이제 리팩토링 해보자! 생각을 했는데.... 진짜 너어어어무 막막했다. 어떡하지? ai와 스프링으로 개발할때는 @RestControllerAdvice로 에러도 한곳에서 잡고, 빈도 등록해놓고, ENUM도 쓰고 의존성도 밖에서 주입해주고 인터페이스로 추상매서드 활용하고 했던거 같은데... 그게 객체지향적인 코드라고 배웠었는데...
뭐부터 해야하지..? 뭐가 객체지향적인거야?
그래서 일단 잘하는 거 하기로 했다. 저번 프로젝트에서 했던 Controller -> Service -> Repository -> Domain 구조로 나눠보자! 이게 스프링 MVC 구조잖아~ 하고.... 근데 레포지토리만 나누었지 실질적으로 코드가 나눠진건 아니었다...

도메인도 뭐 그냥 옛날에 @Lombok 쓰던대로 멤버변수 private로 막고 getter setter 열어놓고.. 했는데 점점 뜯으면서 드는 느낌은 이게 Service가 필요한 정도의 로직인가..? 아니 외부 DB커넥션도 없는데 Repository는 진짜 왜만들었지...? 나 진짜 생각없이 남들이 하란대로 개발했었구나,,, 하고 머리를 망치로 맞은 느낌이었다. 여태까지 인강 열심히 듣고 좋은 프로젝트 구조라고 메모놓으며 다음에 따라해야지 했던 것들이... 무너졌다. 지금 생각해보면 왜 그게 좋다고 느꼇고 그런 구조들이 어떤 상황에 필요했던 거지? 너무 부끄럽다 과거의 나...
다른 곳에선 남들이 하라는 거 괜히 안하고 유행 안따라가는데 개발에서는 왜 남들이 하란대로만 했을까? 지금부터라도 내가 필요한 부분에 필요한 구조를 적용하고 활용하자.
다음은 그래도 나름대로의 이유로 리팩토링 해본 것을 말해보고자 한다. 이게 자꾸 객체를 찢다보니 new연산자를 남발하게 되었다. 그러다보니 객체간의 순환참조가 일어나고, NullPointerException이랑 친구가 되었다. 그래서 아 스프링에서는 DI컨테이너가 존재하고 거기서 싱글톤 빈으로 객체들을 관리중이었지? 그럼 나도 싱글톤 컨테이너 만들고 내가 의존성 주입 순서들만 신경써 줘야겠다. 생각하고 시간을 많이 들여서 어떻게든 다음과 같은 코드를 만들었다.(나름 이 부분에서 정적매서드에 대한 이해도가 올라간 것 같다.)
public class BaseballConfig {
// 싱글톤 DI컨테이너 따라하기
private static final BaseballConfig instance = new BaseballConfig();
// 이 매서드로만 접근!
public static BaseballConfig getInstance(){
return instance;
}
private BaseballConfig() {
}
// 컨테이너가 싱글톤이면 그 안의 멤버변수들도 싱글톤이겠지? -> 추후에 지피티 검색해보기
private final Computer computer = new Computer();
private final Player player = new Player();
private final ComputerRepository computerRepository = new ComputerRepository(computer);
private final PlayerRepository playerRepository = new PlayerRepository(player);
private final BaseballValidate baseballValidate = new BaseballValidate();
private final PlayerService playerService = new PlayerService(playerRepository);
private final ComputerService computerService = new ComputerService(playerService, computerRepository, playerRepository, baseballValidate);
private final BaseballController baseballController = new BaseballController(computerService);
public BaseballController getBaseballController(){
return baseballController;
}
}
그래서 이때는 싱글톤 컨테이너에서만 new()한 객체를 참조하니 참조순환 문제는 덜고, 나름 스프링처럼 내가 등록한 빈들이 프로그램 시작시에 로봇 조립하듯이 촥촥촥 주입되는 기분이라 매우 뿌듯했다.
그래서 아 이정도면 스프링 없이 스프링 빈 따라해봤고 의존성 한번에 외부에서 주입시켜주게끔 했고? 언제든지 객체 갈아끼울 수 있게끔 확장성을 생각했으니 되겠지 생각하고 6기분들의 코드리뷰를 보았다....
그런데.. 나처럼 한사람이 없었다. 완전 다른느낌의 코드리뷰 중이었다. 코드 계층적, 순수 JAVA 문법, 표현법 에 가까운 수준높은 대화들을 나누는 중이었다. 또 머리가 띵했다.
왜 이 문제에서 DI 컨테이너를 흉내냈지?
이유를 굳이 말해보자면 자바로 스프링이라는 프레임워크를 따라했다는 자체가 괜히 뿌듯했고 스프링 코드를 따라하는 게 객체지향적인 것이라고 오해한 것 같다. 지금 다시 말해보자면 요구 상황에 따른 계층을 나누고 책임을 나누는 것이 객체지향적인 것이라고 말하고 싶다.
그리고 가장 머리가 띵했던 점은... MVC구조가.. Model View Controller인데 내 로직에 View를 담당하는 로직이 없다는 것이었다. 그 사실을 다른분들 코드를 보고서야 느꼇다. 너무 어이없어서 웃음이 나왔다 4일동안 생각 많이 했다고 생각했는데..... 그렇다 스프링은 Servlet을 구현한 DispatcherServlet이 요청을 받고 만들어낸 Model을 반환해주는 역할이 있는데, 내 코드는 그냥 Model을 CLI로 던져버리는 코드에 가까웠다.

그래서 여러 PR 코드리뷰를 보고.. 너무 막막해서 뭐부터 해야하지? 지원서 수정을 더해보자... 생각해서 합격 수기 블로그를 둘러보던 와중 아래의 블로그를 발견했다. (너무 감사합니다. 민겸님 많이 배워갑니다. 우테코 합격과정부터 들어가서도 쓰신 글 다 봤는데 너무 멋지십니다!! 저도 꼭 민겸님처럼 되고싶습니다.)
https://mingyum119.tistory.com/270
[회고] 우아한 테크코스 프리코스 1주차 회고 - 숫자 야구 게임
많이 늦어버린 1주차 회고이다. 😅 1주차 때는 회고라는 것을 쓸 정신도 없이 미션에 적응하기 바빴고, 프리코스가 끝나고 다시 1주차를 회고하는 것도 괜찮겠다는 생각이 들어 미루고 미루다
mingyum119.tistory.com
사실 이분 PR을 찾아보면 66개의 코드리뷰가 달린 버전과, 그 후 리팩토링 하신 브랜치의 PR 이렇게 두가지의 버전이 존재한다. 나는 이분 블로그를 정독하고 리뷰 받기전 코드와 리뷰 받은 후의 차이점을 계속 보고 또 보았다. 생각의 과정이 묻어나고 이분의 의사소통 방식도 닮고싶은 부분이 많다. 역시 코드 잘치는 사람이 멋있다.
그래서 참고해서 나도 구조를 많이 바꿧다. 사실 거의 클론코딩 수준이다... 코드의 정답은 없지만 지금 나에게 있어서 이분의 코드는 정답처럼 보인다. 따라하면서도 많은 것을 느꼇다.



특히 에러도 관련 계층에서 확실하게 잡아줘야 꼬이지 않는다는 것도 배웠다. 그리고 나는 정적매서드의 활용은 최대한 줄이라고 배웠었는데, 또 검색을 해보니 아키텍처적인 깔끔함을 위해 정적매서드를 적극 활용하는 게 좋은 것 같다. 상황에 따라 잘 써보자.
그리고 확실히 M V C 구조를 극명하게 나누니 에러도 어디서 넣어야하고 매개변수를 어떻게 넘겨야하는지 머릿속에 그려진다. 정말 신기한 경험이다. 결국 View에 해당하는 부분을 신경써줘야 코드가 이뻐지는 것 같다
그리고 수많은 코드리뷰에서 느낀바는 다 적기 힘들지만 나열해보고자 한다.
매직넘버는 상수화 시키기
ENUM 적극활용
한 계층에 많은 책임을 주지말기. 계층에 맞는 역할만
매서드는 하나의 일만 하게끔!
변수명은 한눈에 알아볼 수 있게
팀의 코드컨벤션 지키기..JavaDoc
조회만 가능해서 람다함수의 반복에 용이한, 즉 값 반환만 하는 Supplier<T> 인터페이스
@Lombook 쓸 필요없다! 간단한 데이터를 담는 불변 클래스! "record" (getter(), setter(), ToString(), equals()~~)
클래스의 final을 붙히면 상속이 불가능하다 -> 확장금지! -> Console 입력같은 곳에.
아래는 가변인자 문법, 항상 마지막 매개변수에 쓸수 있는데, 보편적으로 String.format() 할때 많이 쓴다.
public static void printFormat(String message, Object... args) {
printlnMessage(String.format(message, args));
}
그리고 지금 제일 친해져야할 친구는 stream() 문법이다. 자바의 스트림문법은 너무 강력한 거 같다. 객체지향이 중요한 게 아니라 코드가 너무 깔끔해진다! 좀 더 공부해서 코드 쓸때 당당하게 써보자.
마무리하며
5일만에 이렇게 많은 것을 느꼇는데, 프리코스를 시작하고 훌륭하신 분들과 코드리뷰를 하면 얼마나 더 성장할지 가늠이 안간다. 추석이지만, 최대한 프리코스 풀어보며 내가 혼자 얻을 수 있는 곳에서 많은 것을 얻은 다음에 사람들과 부딫혀 볼 생각이다. 지원서도 틈틈히 계속 수정해야지.
사실 가장 느낀것은 이런 코드기술도 멋있고 굉장히 끌리지만, 요즘 시대에 개발자라는 단어를 넘어서는 Problem Solver 즉 문제해결자가 되어야한다고 생각한다. 어떤 문제가 나에게 주어져도 풀어나갈 수 있는 나만의 "힘"을 갖고 싶은 요즘이다. 그런 의미에서 다음 문제부터는 바로 푸는 것에 힘쓰지말고 초반 문제 해석과 구조 설계에 힘을 많이 써볼 생각이다. 더욱더 몰입해보자. 우테코의 fit에 걸맞는 사람이 되어보자!
가족과의 행사도 참여하고 틈내서 코드 개선 생각하다가 시간나면 신나게 노트북을 펴보는 요즘이다. 훗날 이글을 합격 과정 링크로 자랑할 수 있기를.
'Various Dev > 우아한테크코스' 카테고리의 다른 글
| [우아한테크코스] 테스트 코드 작성 연습해보기(우테코 제공 라이브러리 적극 활용! NsTest, assertSimpleTest) (0) | 2025.10.30 |
|---|---|
| [우아한테크코스] 프리코스 2주차 문제 회고 - 자동차 경주 (2) | 2025.10.28 |
| [우아한테크코스] 프리코스 1주차 문제 회고 - 문자열 덧셈 계산기 (0) | 2025.10.19 |
| [미리보는 프리코스] 시작 하루 전..! 가장 hot한 논제 "각 계층의 필요시점은 어떻게 될까요?" by me (근데 이제 회고와 다짐을 곁들인..) (2) | 2025.10.13 |
| [미리보는 프리코스] 나의 우테코 백엔드 8기 "도전", 기록하자. (0) | 2025.09.29 |