본문 바로가기

Hub Development/Woowacourse

[우아한테크코스] 우테코 6기 프리코스 1주차 피드백 정리

728x90

📜 우테코 6기 프리코스 1주 차


🔹 1주 차 프리코스의 미션은 숫자 야구 게임을 만드는 미션이었다. 미션을 진행하면서 내가 고민했던 부분과 코드 리뷰를 통해서 받았던 피드백 내용을 공유해 보고자 글을 작성했다. 

 

GitHub - noxknow/java-baseball-6: 우아한테크코스 6기 숫자 야구 게임 미션을 진행하는 저장소

우아한테크코스 6기 숫자 야구 게임 미션을 진행하는 저장소. Contribute to noxknow/java-baseball-6 development by creating an account on GitHub.

github.com

 

👻  문제 해결 과정 중 고민 사항

 

❓ MVC 패턴을 사용한 이유


🔹 객체 지향적인 코드를 작성하기 위해 가장 많은 시간을 들였던 부분이다. 첫날에는 기능 목록과 프로젝트의 구조를 생각하다 보니 코드를 작성하지 못하고 하루를 보내게 되었다. 코드를 작성하고 싶은 조바심이 들었지만 아직 1주 차 이므로 남은 3주 동안 미션을 진행하며 학습한다면 이런 조바심이 해결될 수 있다고 생각했다.

 

 객체 지향 프로그래밍은 부품 즉, 객체들이 독립적이기 때문에 코드의 변경이 용이하고 코드의 재사용성이 좋아 확장성이 좋아진다는 장점이 있다. 또한 코드에 문제가 생긴다면 컴퓨터가 문제가 생겼을 때 전체를 교체하지 않고 부품만 교체하는 것처럼 독립적인 객체만 수정할 수 있기 때문에 코드의 유지보수가 용이하다는 장점을 갖는다. 이러한 장점을 가진 객체지향을 코드로 표현하기 위해서는 어떤 방법이 있을까 고민을 한 결과 MVC 패턴을 활용하기로 했다. MVC 는 Model, View, Controller의 약자로 하나의 애플리케이션, 프로젝트를 구성할 때 그 구성요소를 세가지의 역할로 구분한 패턴이다.

 

controller는 비즈니스 로직이 들어가지 않고 domain과 view 사이의 정보를 전달하기 위해 service 혹은 domain을 호출하는 역할로 구조를 생각했다. 또한 Application 클래스를 활용한다면 객체들 간의 직접적인 결합을 피하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램을 설계할 수 있다고 생각했다.

 

domain의 경우 본래는 비즈니스 로직과 데이터를 관리하고 제어하는 역할을 하지만, 이번 경우에는 domain 부분의 역할을 분담하기 위해 service에서 비즈니스 로직을 담당하고 domain은 데이터의 유효성 검사, 저장 등을 처리하도록 했다.

->  이 부분은 아직까지 고민 중인 부분이다. domain에서 로직과 유효성 검사 등 모든 부분을 담당할지 service Package를 둬야 할지는 코드를 작성해 보면서 고민을 해보고 미션이 끝난 후 다른 분들의 의견을 듣고 다음 미션에 적용해야겠다고 생각했다.

 

service는 domain 객체를 활용하여 비즈니스 로직을 구현하는 역할로 구조를 생각했다.

handler를 통해서는 여러 가지 예외 상황 혹은 요청을 처리할 수 있도록 구현했다.

view 역시 input의 경우 값을 입력받는 역할만 할 수 있도록 구현하고, output도 출력만 할 수 있도록 구현했다.

 

🤔 인터페이스 사용에 대한 고민


🔹 인터페이스는 확장성과 변경 용이성의 이유로 사용했다. 하지만, 매번 추상화와 확장성을 고민하며 관리하다 보면 오버엔지니어링이 될 수 있다는 말에 다른 분들과 피드백을 주고받으며 더 생각해 봐야 하는 문제인 것 같다.

 

🚧 getter의 사용을 지양해라?


🔹 학습을 하다 보니 getter와 setter의 사용을 지양하라는 부분을 많이 보게 됐다. setter의 경우는 명확하게 객체의 상태 값을 변경할 수 있기 때문에 지양해야 한다는 부분은 이해했지만 getter는 왜 포함되는지 이 부분이 궁금했다.

이유는 크게 두 가지에 대해서 생각을 해봤다.

 

  1. getter는 편리한 메서드이기 때문에 남발할 가능성이 있다.
  2. getter 메서드만으로 외부에서 객체의 상태를 변경하지는 못하지만, 그 결괏값이 객체의 상태를 변경시키는 데에 사용될 수 있다. (즉, 객체의 상태값을 바꾼다는 판단을 외부에서 하고 있다.)
public Car {
	private int number;
    
    public Car(int number) {
    	this.number = number;
    }
    
    public void update(int newNumber) {
    	this.number = newNumber;
    }
    
    public int getNumber() {
    	return number;
    }
    
    public void move(int number) {
    	if (this.number >= number) {
        	this.number++;
        }
    }
}

public class Game {

	public void run(Car car) {
		int number = car.getNumber();
        
		if (number >= 4) {
        	car.update(number+1);
		}
	}
    
	public void run2(Car car) {
    	car.move(4);
	}

}

이 중 2번째 이유에 집중해서 보자면 run2 같은 경우, 숫자를 넘겨줄 뿐, 상태값을 변경시키는 판단을 객체에게 맡기고 있다. 하지만 run 같은 경우, getter를 통해 값을 받은 후 그 값을 이용하여 객체의 상태값을 변화시키는 판단을 하게 되고, update메서드를 호출함으로써 객체의 상태값을 바꾼다.

 

🔹 이러한 이유로 getter를 사용하지 말아야 한다는 강박이 생기기 시작했다. 그러다 보니 코드의 진도가 나가지 않는 어려움을 겪게 되었다. 하지만, getter를 지양하는 이유가 외부에서 상태 값을 바꾸는 경우가 걱정되는 거라면 그렇게 되지 않도록 신경 쓰고, 외부에서 값을 바꿀 수 없도록 원본 객체와 관계가 없는 객체를 내보낸다면 문제가 없는 게 아닐까라는 생각을 했다.

원본 객체의 값도 지키고 일급 컬렉션의 특징인 불변을 지키기 위해서 Collections.unmodifiableList를 사용하려 했으나 읽기 전용은 맞지만 원본 객체의 상태가 변화하면 영향을 받기 때문에 둘 사이의 아무런 관계가 없는 객체 즉, 원본 객체를 복사한 객체를 만드는 List.copyOf()를 사용했다.

cf) https://velog.io/@mu1616/List.copyOf-vs-Collections.unmodifiableList

 

🔹 코드를 작성하던 중 getUserValues 메서드의 반환값이 List <Integer>로 반환되는 것으로 변경이 있어 stream을 이용하게 되었는데 stream 역시 원본 객체에 영향을 미치지 않는다는 사실에 stream을 사용한 경우도 있다.

public class UserBaseballNumber {

  private List<String> userValues;  public UserBaseballNumber(List<String> userValues) {
    // 중략    this.userValues = userValues;  }

  // 중략//  public List<String> getUserValues() {//    return List.copyOf(userValues);//  }  // 변경된 코드  public List<Integer> getUserValues() {
    return userValues.stream()
            .map(Integer::parseInt)
            .collect(Collectors.toList());  }
}

public class TargetNumber {

    // 중략    public TargetNumber() {
        this.targetNumber = generateComputerNumbers();    }

    // 중략    public List<Integer> getComputerNumbers() {
        return List.copyOf(targetNumber);    }
}

 

⚙️ 일급 컬렉션


🔹 일급 컬렉션(First-Class Collection)은 상태와 행위를 함께 캡슐화하는데 주로 컬렉션을 사용하는 디자인 패턴이다. 일급 컬렉션은 컬렉션을 하나의 객체로 취급하여 해당 컬렉션과 관련된 동작을 통합하고, 높은 응집도와 캡슐화를 통해 코드의 가독성과 유지 보수성을 향상하는 데 도움이 된다는 내용에 이번 미션에서 일급 컬렉션을 활용했다. 일급 컬렉션에 대한 내용은 따로 정리해뒀다.

 

 

[Java] 일급 컬렉션 (First Class Collection)의 사용

📌 일급 컬렉션 (First Class Collection) 🔹우테코 프리코스 미션을 진행하며 학습했던 일급 컬렉션 내용에 대해 정리하고자 한다. 내가 service로 구현해야겠다고 생각하는 기준은 비즈니스 로직을

noxknow.tistory.com

 

❗ 매직 넘버 사용을 지양하자


🔹 문제를 풀이한 입장에서는 숫자의 의미를 바로 파악할 수 있었지만 다른 사람들이 보는 경우 의미를 알 수 없을만한 숫자들을 볼 수 있었다. 이러한 의미가 명확하지 않은 수를 매직넘버 라고 하고 이러한 부분을 지양해야 한다는 사실에 기존의 코드에서 수정했다. 또한 그러한 상수들을 한 곳에 모아서 관리하는 클래스를 만들지 상수를 사용한 클래스 위쪽에 적어둘지 고민을 하던 중 상수로서 의미를 파악할 수 있어도 그 상수로 어떤 값을 사용했는지 바로 알 수 있는 것이 좋을 것 같다는 생각에 따로 상수들을 모아둔 클래스를 만들지 않았다.

 

🧐 피드백 및 리뷰 내용

 

🧪 상수 관리 


➡️ 첫 번째 피드백의 내용은 상수화한 매직넘버를 어느 곳에서 관리해야 할까? 라는 내용이었고, 저의 경우는 직관적인 파악을 위해 사용한 클래스 상단에 표시하는 게 맞다고 생각했지만 피드백을 통해 2주 차 미션에서는 ConstantsHandler로 상수들을 관리하는 클래스를 만들었다.

 

🧪 생성자를 오버로딩해서 설계 하는 경우 


🔸 (리뷰어)  : GameChoice의 생성자에 따라 결과가 달라지는 것 같아요. 인자값 없이 사용되면, RESTART_GAME을 선언하고, 인자값이 있다면 choice를 검증 후 반영하는 생성자인 것 같아요. 생성자 오버로딩으로 설계할 때는 정적 팩토리 메소드를 고려해보시는 것도 좋을 것 같습니다.

이런 식으로 설계한다면 생성자를 오버로딩해서 설계해도, 누구나 해당 메소드가 어떤 의미로 어디에 사용되어야 할 지 직관적으로 파악이 가능할 것 같네요!

public GameChoice() {
    this.choice = RESTART_GAME;
}

public GameChoice(int choice) {
    validate(choice);
    this.choice = choice;
}

---- ( 위에 대신 아래와 같이)

private GameChoice() {
    this.choice = RESTART_GAME;
}

private GameChoice(int choice) {
    validate(choice);
    this.choice = choice;
}

public static GameChoice restart(){
   return new GameChoice();
} 

public static GameChoice ????(int choice){
   return new GameChoice(choice);
}

 

➡️ 두 번째 피드백 내용은 정적 팩토리 메서드에 대한 내용이다. 피드백을 받았지만 모르고 있던 부분이라 학습했고, 그 과정에서 기존의 설계와는 다르게 wrapper 클래스와 정적 팩토리 메서드를 활용하는 방식으로 리팩토링하게 되었다.

 

🧪 Handler를 생성자 주입으로 설계한 이유 


 

📸 참조


https://msmk530.tistory.com/72