[우테코-프리코스] 1주차 회고

dooboocookie·2022년 11월 7일
2

우테코-프리코스

목록 보기
1/4
post-thumbnail

1주차 과제

  • 1주차 과제는 전체적으로 코딩 테스트 문제 처럼 간단한 문제가 주어지고 알맞은 답을 출력하도록 하는 프로그래밍을 하는 것이었다.

  • 7문제가 나왔고 난이도는 걱정한 것 보다는 아주 높지는 않았다.

    • (하지만 생각해 볼 점은 많았다)
  • 복잡한 알고리즘을 미리 알고 있어야 풀 수 있는 문제는 아니었다.

진행 방식

  1. 기능, 프로그래밍, 과제 진행 요구사항이 있다.
    1. 기능 요구사항은 말그대로 프로그램 기능에 대한 요구사항이었다.
      1. 입력에 따라 답을 return하는 메소드를 구현
      2. 입력에 대한 제한사항
    2. 프로그래밍 요구사항은 Java버전과 프로젝트 빌드 등에 대한 요구사항이었다.
      1. JDK 11버전
      2. 빌드툴을 Gradl
      3. 미리 설정되어있는 Gradle을 변경 X
      4. Test를 통과
    3. 과제 진행 요구사항은 과제에 진행 순서 및 제출 방법에 대한 요구사항이었다.
      1. 과제가 올라가 있는 깃헙 레포지토리를 포크
      2. 나의 브런치를 만들어서 기능 구현
      3. Pull Request
      4. 우테코 지원 페이지에서 채점

클린 코드

  • 지금까지 알고리즘 공부나 토이 프로젝트를 해보면서 SOLID 원칙에 대해서는 공부해봤지만,
  • 유지보수가 좋고 읽기 좋은 코드에 대해서는 생각하지 못한 부분이다.
    • 내가 하는 코딩을 다른사람이 보거나 고칠 일이 없었다.
  • 심지어 내가 썻던 코드도 시간이 지난 뒤 보면, 의도를 정확하게 파악하기 힘들 때가 있다.
  • 이제 과제 제출을 위해서라도 클린코드에 대해서 공부하게 되었다.
  • 클린 코드를 공부하면서 내가 받아드려야할 조건은 많았지만, 그중 와닿았던 조건이 몇개 있어 최대한 적용해보려 했다.
    • else문을 사용하지 않기
    • 원시값과 문자열을 포장
    • 컬렉션 대신 일급 클래스를 사용하기

1. else문을 사용하지 않기

  • 코딩을 하다보면 if-else문은 정말 많이 사용한다.
  • 그래서 처음에는 이 조건이 이해가 잘 되지 않았다.
  • 이번 1번 문제에서 포비가 이겼을 때 1 , 크롱이 이겼을 때 2, 비겼을 때 0을 리턴하는 문제가 있었다 평소같으면 다음처럼 코딩을 했을 것이다.
// 점수 비교
if (pobiScore > crongScore) {
	answer = 1;
} else if (pobiScore < crongScore) {
	answer = 2;
} else {
	answer = 0;
}
  • 하지만 이렇게 되면 else 문이 실행되는 조건을 다른 사람이 보고 선뜻 이해하기 어려울 수 있다.
    • 위와 같은 조건은 파악하기 비교적 쉽지만 조금만 복잡해져도 이해하기 어려울 것이다.
  • else if 문 또한 if문이 실행되지 않은 조건에 대해서만 실행되는 것이므로 else if(조건)만 보고 의도를 파악하기 힘들 수 있다.
  • 그렇다면 이 두 구절을 지우면 이렇게 된다
// 점수 비교
if (pobiScore > crongScore) {
	answer = 1;
}
if (pobiScore < crongScore) {
	answer = 2;
} 
if (pobiScore == crongScore) {
	answer = 0;
}
  • 이렇게 되면 if문 밖에서 answer를 초기화 해주는 식이 없기 때문에 나같은 경우엔 가장 일반적인 경우인 비기는 경우 밖으로 빼주고 Early Return해주는 식으로 구현을 해보았다.
// 점수 비교
if (pobiScore > crongScore) {
	return 1;
}
if (pobiScore < crongScore) {
	return 2;
} 
return 0;

2. 원시값과 문자열을 포장

  • 이 부분이 사실 제일 충격적이었다.
  • 예를 들어, 2번 문제에서 입력 값은 String 이다.
    • 그 입력을 solution() 메소드의 매개변수로 넘어온대로 사용하면 다음과 같아질 것이다.
public static String solution(String cryptogram) {
	
    //cryptogram을 검증하는 로직
   	
    //cryptogram을 이용하여 문제를 해결하는 로직
    
    //...
}
  • 이렇게 된다면, solution() 메소드가 너무 많은 책임을 갖게 될 것이다.
  • 그리고 구조가 복잡해지게 되면 그 cryptogram이란 변수 자체가 어떤것을 의미하는 것인지 파악하기 힘들 수 있다.
  • 그래서 저런 String이나 원시 값을 class로 감싸는 것이다.
class Cryptogram {

    private String cryptogram;

}
  • 이러한 클래스는 각 필드의 값을 쉽게 변경하거나 접근하면 안되므로, 앞으로 계속 private으로 선언할 것이다.
    • setter는 선언하지 않을 것이고, getter는 웬만해선 선언하지 않을 것이다.
  • 이제 저 클래스 인스턴스를 하나 생성한다는 것은 어떤 값하나를 입력 받는다는 뜻이다.
    • 따라서 그 안에 값은 쉽게 바뀌지 않을 것이다.
    • 그 값을 입력하는 시점이 저 객체가 만들어져야되는 시점이다.
  • 객체를 만들면서 값을 입력할 수 있는 방법은 여러가지가 있다.
    • 생성자로 주입
    • 기본생성자로 생성 후 setter로 주입(사용 안함)
    • 정적 팩토리 메소드를 만듦
  • 현재는 구조가 매우 간단한 프로젝트이므로 생성자를 만들었다.
class Cyptogram{

    private String cryptogram;
    
    public Cryptogram(String cryptogram) {
        this.cryptogram = cryptogram;
    }
    
}
  • 이제 값을 입력 받을 때, 검증 처리를 Cyptogram 클래스 안에서 구현하고 그 검증을 객체가 생성될 때 호출할 수 있다.
class Cyptogram{

    private String cryptogram;
    
    public Cryptogram(String cryptogram) {
        this.cryptogram = cryptogram;
    }
    
    //문자열 길이에 제한사항에 대한 검증
    private void validateLength(String cryptogram){
        int length = cryptogram.length();
        if (length == 0) {
            throw new IllegalArgumentException("빈 문자열은 입력할 수 없습니다.");
        }
        if (length > 1000) {
            throw new IllegalArgumentException("1000자 이하로 입력해주세요.");
        }
    }

	//소문자 제한사항에 대한 검증
    private void validateLowerCase(String cryptogram) {
        for (int i = 0; i < cryptogram.length(); i++) {
            if (!Character.isLowerCase(cryptogram.charAt(i))) {
                throw new IllegalArgumentException("소문자만 입력할 수 있습니다.");
            }
        }
    }
}
  • 이제 solution() 메소드 안에서 객체를 생성하여 사용할 수 있다.
public static String solution(String cryptogram) {
	
    Cryptogram input = new Cryptogram(cryptogram);

	// 로직...
    
}

3. 컬렉션 대신 일급 클래스를 사용하기

  • 자바에는 List, Set, Map 과 같은 컬렉션이 있다.
  • 이를 그대로 선언해서 사용하지 않고 일급 클래스로서 감싸서 사용하는 것이다.
  • 7번 문제의 경우, 유저 별 친구 관계를 가진 HashMap 필요했다.
  • 처음 생각했던 구조는 다음과 같다.
HashMap<String, List<String>> friendMap = new HashMap<>();
  • String으로 유저명을 키값으로 갖고, 친구 유저명을 리스트로 밸류로 갖는 HashMap이다.
    • 일단 위 설명이 없었다면 저 의도를 바로 파악하기 어려웠을 것이다.
    • 그렇다는건 바꿀 이유가 충분하다.
  • 이걸 클래스로 감싸보겠다.
class FriendMap {
	private final Map<String, List<String>> friendMap;

    public FriendMap(List<List<String>> list) {
        validationSize(list);
        this.friendMap = new HashMap<>();
        // 친구를 서로 이어주는 메소드
        setFriendMap(list);
    }

    private void validationSize(List<List<String>> list) {
        if(list.isEmpty()) {
            throw new IllegalArgumentException("친구 관계는 비어있을 수 없습니다.");
        }
        if(list.size() > 10000) {
            throw new IllegalArgumentException("친구 관계는 10000개 이하로 입력해주세요.");
        }
    }
}
  • 이렇게 구현해도 약간 어색한 지점 이있다. 왜냐하면 위 2번 조건을 충실하지 않았다.
  • String 을 감싸보자
class FriendMap {
	private final Map<User, List<User>> friendMap;

    public FriendMap(List<List<String>> list) {
        validationSize(list);
        this.friendMap = new HashMap<>();
        // 친구를 서로 이어주는 메소드
        setFriendMap(list);
    }

	//검증 로직...
}
  • 의도가 좀 더 명확해졌지만, 밸류인 List<User>또한 컬렉션이다. 이 또한 일급 컬렉션으로 만든다.
class Friends {
    private final List<User> friends;

    public Friends() {
        friends = new ArrayList<>();
    }

    public void addFriend(User user) {
        this.friends
                .add(user);
    }

    public List<User> getFriends(){
        return this.friends;
    }
}
  • 그럼 이제 다음과 같이 표현할 수 있다.
class FriendMap {
	private final Map<User, Friends> friendMap;

    public FriendMap(List<List<String>> list) {
        validationSize(list);
        this.friendMap = new HashMap<>();
        // 친구를 서로 이어주는 메소드
        setFriendMap(list);
    }

	//검증 로직...
}
  • 이제 더 명확해진 것을 확인할 수 있고, 친구 목록 자체를 클래스로 만들었기 때문에 친구 목록에 관한 기능, 유저에 관한 기능, 전체 친구 목록에 관한 기능을 나눠서 각 클래스에 구현하여 좀더 책임을 명확하게 나눌 수 있게 됐다.

놓친 점

  • 이번 과제를 진행하면서 놓친 점이 아주 많았다.

1. 기능 목록을 머리로만 생각했다.

  • 기능 목록을 구현 전에 구상하긴 했지만, 그냥 종이에 적으면서 머리로 생각했다.
  • 그렇다보니, 기능을 구현하면서 쓸때없는 코딩을 한다가나, 필요한 기능을 놓치는 경우가 많다.
  • 따라서 리팩토링이 아주 어려웠다.

2. 테스트 코드 작성을 하지 못했다.

  • tdd가 중요하단 말은 익히 들어왔지만, 프로젝트를 진행할 때에 한번도 적용을 해보지 못했다.
  • 하지만 테스트 코드를 작성하지 않으니, 어떤 기능을 구현할 때 내가 맞게 구현한 것인지에 대한 확신을 갖기 어려웠다.
  • 또한 테스트 코드를 미리 작성한다는 것 자체가 기능 목록을 작성을 잘했다는 것이고,
  • 그리고 기능 별로 역할을 명확히 나눴다는 의미 같다.

후기

  • 처음엔 문제를 보고 가벼운 마음으로 시작했다.
  • 하지만 대부분을 클린코드로 리팩토링하는 시간에 쏟았다.
    • 그리고 리팩토링을 하다보니 내 자바 실력에 대해 부족함을 다시 느낄 수 있었다.
  • 그리고 누군가 내 코드를 읽을 수 있다는 생각으로 코딩을 하니까 훨씬 신경써야되는 지점이 많았다.
  • 다음 과제에선 위의 조건들과 놓친 점을 좀 더 보강해서 진행해볼 예정이다.
  • 간단한 과제 같지만 많은 배움을 준 과제였다.

PullRequest 링크

profile
1일 1산책 1커밋

0개의 댓글