우아한테크코스에서 진행하는 미션을 혼자 진행해보면서 나의 생각과 배운 것을 정리하는 글

[페어매칭 미션]

[내코드PR]

 

[페어매칭] 미션 제출 by changjun6518 · Pull Request #1 · changjun6518/java-pairmatching-precourse

테스트 코드 작성 MVC 패턴 객체 메세지 전달 의존관계

github.com

 


Course enum에 여러 변수들?을 담았는데 책임이 많아져서 문제?

public enum Course {
    BACK_END("백엔드", "./src/main/resources/backend-crew.md"),
    FRONT_END("프론트", "./src/main/resources/frontend-crew.md"),
    ;

    static{
        for (Course course : Course.values()) {
            course.initCrews();
        }
    }

    private String name;
    private String filePath;
    private Crews crews;

    Course(String name, String filePath) {
        this.name = name;
        this.filePath = filePath;
    }

    private void initCrews() {
        crews = CrewFileReader.readCrews(filePath);
    }
}
  • Course 이름 , filePath, 크루 리스트 등 과정에 속해있는 필요한 요소들을 enum안으로 다 담았는데 적절한 방법으로 사용한 건지 모르겠다
  • static은 프로그램을 실행하자마자 데이터를 보유하고 있어하게끔 (캐쉬 역할을 하게) 함

Enum의 예외처리는 어떻게할까?

  • 이건 검증로직이 어디에 있을까에 대한 의문인 것 같은데 enum도 클래스처럼 안에 하는게 좋겠지?

백엔드 과정 및 프론트 과정 크루 리스트 받아올때 중복 코드 어떻게 제거함?

public class CrewFileReader {
    static List<String> crewsString = new ArrayList<>();

    public static Crews readCrews(String filePath) {
        try {
            BufferedReader bufferedReader = new BufferedReader(
                    new FileReader(filePath)
            );
            String str;
            Crews crews = new Crews();
            while ((str = bufferedReader.readLine()) != null) {
                crewsString.add(str);
                crews.add(str);
            }
            bufferedReader.close();
            return crews;
        } catch (Exception e) {
            throw new IllegalArgumentException("제대로 파일을 읽어올 수 없습니다!");
        }
    }
}
  • 인터페이스로 처리해보고 싶은데 잘 모르겠음
  • 매개변수로 파일경로 주입시켜줬음 -> 파일경로를 설정해주는 역할을 상위 객체로 넘김

이미 매칭되었는지 판단할때 map으로 확인하는데 String 값의 경우 객체기 때문에 포인터가 다를 것 같은데 contains를 사용했을때 같은 객체라고 판단할까?

private final HashMap<Level, List<String>> crewsByLevel;

public boolean isMatched(Level level, String crewName) {
    if (crewsByLevel.containsKey(level)) {
        List<String> matchedCrews = crewsByLevel.get(level);
        return matchedCrews.contains(crewName);
    }
    return true;
}
  • Heap 에 존재하는 객체들이기 때문에 같은 객체라고 판단할 것이다
  • 여러 군데에 쓰여도 같은 객체를 가리키고 있다
  • Key가 String이라면 equals 함수의 내부 로직이 있을거야...! 아마도

Matching - Crews - Crew 페어 매칭 이력 검사해서 이미 매칭되었는지 검증하는 함수 명을 똑같이 하는게 맞는가?

  • 3개 클래스 모두 매칭되었는지 검증하는 함수명이 isMatched인데 헷갈린다
  • 근데 의존관계?가 명확하면 그렇게 헷갈리지 않을 것 같기도함 -> 찾아보자

Matching 클래스를 불변객체? 로 두고 하나의 객체만 계속 사용하도록 만들면 공간효율이 좋지 않을까 생각함

public class Matching {

    private final List<String> stringCrews;

    public Matching(List<String> stringCrews) {
        this.stringCrews = stringCrews;
    }

    public MatchingResult match(Level level, Crews crews) {
        shuffle();
        MatchingResult matchingResult = new MatchingResult();
        matchingResult.match(stringCrews, level, crews);
        return matchingResult;
    }

    private void shuffle() {
        Collections.shuffle(stringCrews);
    }

}
  • 하지만 여러 매칭이 동시에 실행된다고 했을때 시간효율이 안좋음 + 멀티스레드 환경에서 동시 접근안됨

equals와 hashcode 재정의할때 왜 둘다해야할까?

public class MatchingInfo {
    private Course course;
    private Level level;
    private Mission mission;

    public MatchingInfo(Course course, Level level, Mission mission) {
        this.course = course;
        this.level = level;
        this.mission = mission;
    }

    @Override
    public boolean equals(Object o) {
        if(this == o) return true;
        if(o == null || getClass() != o.getClass()) return false;
        MatchingInfo matchingInfo = (MatchingInfo) o;
        return course == matchingInfo.course &&
                level == matchingInfo.level && mission == matchingInfo.mission;
    }

    @Override
    public int hashCode() {
        return Objects.hash(course, level, mission);
    }
}
  • 해쉬코드는 객체의 주소값을 변환하여 생성한 객체의 고유한 정수값
  • 실제 객체들의 주소가 다르더라도 변환된 hashCode를 보고 같은 객체인지 판단함!
  • 그래서 String 같은 경우도 문자열이 같으면 hashCode가 같다

key 결과에 따라서 Dto 보다는 matchingResult 가 저장되었으면 하는데 service에서 가져오도록 하려면 로직을 전부 수정해야함 → 잘못된 코드 아닌가

private final HashMap<MatchingInfo, MatchingResult> repository = new HashMap<>();
  • 원래 MatchingResultDto 였는데 MatchingResult로 수정
  • service 에서 Dto를 반환받도록 되어있어서 결과값을 수정하려면 모두 다 변경해야함
  • 테스트코드를 작성하면서 진행했으면 좀 더 수월했을 것 같음!

Matching을 MatchingResult와 분리 시켰는데 MatchingResult public method 들이 불안함

  • public 메소드를 어쩔수 없이 사용하는 경우 접근 제한을 어떻게 할까?
  • 패키지로 접근 제한하나?

엔티티에서 Dto를 만들어서 반환할까? 아니면 service에서 get으로 엔티티 내부 변수 가져와서 Dto로 변환할까?

public MatchingResultDto convertDto() {
        return new MatchingResultDto(matchingResult);
    }
  • getter 안쓰려면 안에서 이렇게 해줘야할 것 같다

단위 테스트 진행할때 접근제어자 public에서 private으로 변경되는 경우는 단위 테스트 진행 못하나요? (matching에서 기능들 private으로 변경해서 이전에 테스트한 것들에 에러생김)

  • 굳이 private 까지 테스트를 진행할 필요가 없다고 한다...!
  • private를 할 수 있는 방법은 있다
  • 쓸데없는 테스트때문에 효율도 떨어지고 코드량도 많아지는 문제가 있을 것 같다

MatchingTest에서 단위 테스트 진행할때 공통되는 부분이 많다(given, when, then)

class MatchingServiceTest {

    @Test
    public void matchTest() {
        // given
        Course course = Course.BACK_END;
        Level level = Level.LEVEL1;
        Matching matching = new Matching(course.getStringCrews());
    }
}
  • 공통된 부분은 @Before 를 사용하여 setup해서 진행하자

회고

우테코 최종 코테였던 미션이다. 처음 풀때 엄청 어려웠는데 지금은 꽤나 잘 풀 수 있는 것 같다.

고민없이 공부하는 게 문제인 것 같고 어떤 방법이 더 좋은지 끊임없이 생각하고 적용해보면서 장단점을 체화하는게 좋은 것 같다. 더 나아가서 이유에 대해서 논리적으로 설명할 수 있으면 좋겠다.

끝.

우아한테크코스에서 진행하는 미션을 혼자 진행해보면서 나의 생각과 배운 것을 정리하는 글

[로또미션]

[내코드PR]

 

[로또 - 1단계] 미션 제출합니다. by changjun6518 · Pull Request #2 · changjun6518/java-lotto-1

안녕하세요! 😊 로또 미션을 구현해보았습니다! 🎓 구현하면서 조금이라도 궁금하고 의문이 들었던 생각들을 정리하고 코멘트에 남겼습니다! 피드백 주시면 저에게 큰 도움이 될 것 같습니다!

github.com

 


금액 단위를 검증하려는 메소드명 뭐로할까?

  • 별 생각없이 isValid~~로 작성했지만 boolean 변수 이름을 제대로 짓기 위한 컨벤션..? 이 있었다
  • is는 boolean 타입을 반환할 것이라는 생각을 들게하기 때문에 적절하지 않은 것 같다

Amount의 isValidDigit 들여쓰기 2칸 어떻게 해야할까?

  • 들여쓰기 제한을 1로 하고 싶어서 고민했는데 도저히 생각이 안났다..! 따로 함수를 분리해도 되는데 함수명을 어떻게 나눠줘야할지 또 나누는 게 좀 불필요하다는 느낌이 많이 들었다
  • 이 함수안에서만 국한되지 말고 amount를 String으로 받아올때 생기는 문제인데 애초에 Integer로 받아오게 되면 Digit인지 확인할 필요가 없다. 아니라면 컴파일 에러가 날 것이다

 


검증 함수를 만들때 반환타입을 boolean으로 하는지 아니면 void 로 하고 바로 throw new 날리는지?

    • 아무 생각없이 코딩하다가 예외처리를 어떻게 해야할지 의문이 들었다
    • 위 방식대로 조건문에 해당하면 throw new Exception()을 처리해줬다
    • 나중에는 같은 Exception끼리 묶고 Exception을 상속?받는 등 처리해서 예외처리에 네이밍을 해줄 수 있을 것 같다
    • 하나하나 예외처리 클래스를 생성해서 만드는지 ENUM을 활용하는지는 더 공부해봐야 겠다

InputView 에서 입력받는 라이브러리? 어떻게 사용했는가?

  • 입력 기능을 하는 Scanner 클래스를 한번만 생성해도 되니까 상수?처리를 해줬다
  • 변수명을 sc라고 했는데 SCANNER로 상수 컨벤션을 지켜야하는 사실을 알게되었다...이것도 해당하는 게 맞겠지?

예외처리 발생 테스트 코드 작성 어떻게 하지?

  • 요렇게!
  • Assertions.assertThrows(Exception.class, () -> )
  • () -> 는 예외가 발생하는 상황을 만들어주면 된다

전체적인 연결 기능은 기능목록을 작성하지 않는 걸까?

  • 이건 클래스마다 하나씩 단위 테스트 진행하면서 했는데 전체적인 클래스들의 연결 테스트는 이뤄지지 않고 또 어떻게 테스트를 해야할지 모르겠다
  • 지금 미션은 콘솔 미션이지만 웹에서는 이런 상황을 테스트하기 위해 RestAssured 라는 라이브러리?가 있다고 한다

멤버변수 생성 시점을 어디서 하는게 좋을까?

  • 정답은 없고 상황에 따라서 어떤 방식을 선택할지 다를 것 같은데
  • 최근에 공부한 토비의 스프링에서 인터페이스를 공부했는데 인터페이스를 통해 기능 구현에 대한 분리를 이뤄냈지만 어떤 클래스가 필요한지에 대한 관심을 분리하지 못하여 생기는 문제를 상위객체(클라이언트 객체)에서 생성해서 주입하면서 이 관심을 상위객체에게 넘겨버릴 수 있었다
  • 아무튼 지금 상황에서는 크게 중요하지 않지만 스프링 생태계로 넘어가면 보통 생성자를 통해 주입받는 형태로 사용하게 되지 않을까 싶다. 물론 그때도 이유와 근거는 명확한 게 좋겠지

 


Lotto 에서 랜덤넘버를 생성하고 중복되지 않으면 숫자를 넣는 함수명은?
Lotto 번호가 잘 생성됐는지 확인하고 싶은데...! 이 부분은 어떻게 테스트할까?

 

  • 로또는 모두 중복되지 않은 숫자인 걸 알고 있으니 addRandomNumber 또는 addNumber 정도만 해도 충분할 것 같다
  • 고민해보니 이 모든 고민과 컨벤션과 클린코드와 왜 좋은 코드를 만드려고 하는가에 대한 본질은 미래의 나와 다른 개발자가 내 코드를 다시 봤을때 보다 빠르게 이해하고 알아보기 쉽게 하기 위해서라는 걸 생각하게 됐다
  • 랜덤 번호 확인은 자바 라이브러리에서 제공하는 것이니 믿고 넘어가도 될 것 같다

밖에서 new 생성자를 만들어서 사용하는 게 좋을까?

  • 생성자와 정적 팩토리 메소드
  • 사실 이부분에서 정적 팩토리 메소드를 사용하는 장점이 없는 것 같다
  • 팩토리 메소드 패턴은 상위 하위 클래스가 있고 하위 클래스에서 생성 로직을 따로 구현하는 방식으로 알고 있다

Lotto 생성하는 로직을 static으로 generator를 빼주는게 왜 좋을까?

  • 이것도 Lotto에 생성 로직이 있는게 맞는가 라는 생각
  • 한번만 생성한 뒤로 Lotto안에 생성하는 메소드는 다시 안쓰이는데 불필요한 코드만 차지하는 느낌
  • 빼주는 것도,,, 나쁘지 않을 것 같다 더 많은 기능들이 추가된다면

Lotto 번호와 winning 번호 비교하는 로직을 만드는데 어떻게 하는게 좋을까?
Lotto에 compare 메서드 들여쓰기 2개 어떻게 풀어낼까?

  • 객체에 메세지를 보내면서 비교하도록 했다
  • 근데 메세지를 타고타고 많이 보내게 되면서 함수명도 똑같고 어떤 객체부터 최종 객체까지 메세지를 보내는 과정이 헷갈리는 문제가 생겼다
  • 이런 경우에는 getter로 타협해도 괜찮을 것 같다는 생각이 들었다
  • 다음에도 문제 생길때 고민해보자!
  • 두번째는 들여쓰기를 위해 함수분리해도 괜찮을 것 같다

주요 로직을 테스트 코드로 먼저 작성하고 프로덕션 영역에 옮겨 작성하는지?

  • 테스트 주도 개발은 테스트부터 작성하고 프로덕션 코드에 옮기고 불필요한 테스트 코드를 삭제하는 방식으로 진행할 것 같다...!
  • 아직 테스트 코드부터 작성하는 게 익숙하진 않다. 맞는건지도 잘 모르겠고!

Reward 에서 카운트와 일치하는 것 찾아주는 로직 어떻게 하지?
테스트가 잘 작동하는지 확인하려고 print문 잠시 넣어서 확인했다 더 좋은 방법이 없을까?

  • Reward enum에서 카운트 갯수를 메세지로 받으면 일치하는 enum을 반환해주는 메소드를 만들었다
  • 테스트 잘 동작하려는지 보려고 print 메소드를 임시로 만들었는데 콘솔이라 그런가 분명 방법이 있을텐데 더 찾아보자
  • 비교 결과 reward를 확인하고 싶어서 print를 한건데 상황 만들어서 상황에 따른 결과 reward가 맞는지 확인하면 될 것 같다

테스트 함수명 카멜케이스, 스네이크 케이스?

  • 카멜케이스 사용하는 것 같다!

getter를 사용하지 않기 위해서 객체에 메세지를 던지는데 서로 던지다보니 메서드 명이 일치해서 좀 헷갈리는 현상 발생

  • 아까 언급한 부분인데 winningnumber와 lotto와 비교하는 메소드인데 객체에 메세지를 넘겨주다보니 계속 해서 넘겨주게 되고 메소드명도 compare로 일치하니까 어디서 어떻게 넘어가는지 헷갈림
  • 사실 getter 써도 넘어가는 건 똑같은데 꺼내왔다는 의미로 생각하면 괜찮은데 compare는 어떨지..?
  • compare도 똑같은 거 같은데?? 단지 의존 관계라고 해야하나 그 부분이 헷갈린다

Result returnProfit 메서드 언제 기본형 타입쓰고 언제 래퍼 타입을 사용하는지?

  • Wrapper 클래스를 사용하는게 좋다는 글을 본 거 같은데.. 이건 코딩을 많이하고 다른 사람들 코드를 많이 보게 되면 저절로 알게 될 것 같은데
  • Wrapper 쓰면 컴파일 에러를 더 잘 잡아주지 않을까..? 똑같나..?
  • 추가적인 기능을 더 사용할 수 있다!!

OutputView 에서 Reward enum을 foreach 했을때 Reward.FAIL까지 출력되는 문제 발생

  • Enum의 foreach 인데 Stream을 활용해서 아주 깔끔하게 리팩토링했다..!

 


회고

우테코 4기 최종 탈락한 뒤로 혼자서라도 해야겠다는 생각에 무작정 미션을 진행해봤지만 성장하는 느낌이 들지 않았다

방법을 바꿔서 미션을 진행하면서 생기는 문제들을 모두 기록하고 스스로 고민해보자는 생각을 했고 git을 통해 나쁘지 않게 진행한 것 같다

그리고 꽤나 많은 걸 얻어가는 기분이다. 또 우테코 출신 많은 분들이 블로그로 회고를 잘 정리해둔 것을 참고하여 더 배워갈 점이 많았다.

근데 진행하면서 3주에서 4주정도 로또만 했는데 많은 시간한 것도 아니고 시간만 질질 끈 기분이다.

데드라인을 정하고 알차게 시간을 써야할 것 같다.

다음은 페어 매칭 프로그램 미션을 진행해볼까 한다.

참고

[참고한 우테코회고블로그]

우아한테크코스에서 진행하는 미션을 진행해보면서 나의 생각과 배운 것을 정리하는 글

[로또미션]

[내코드PR]

 

[로또 - 1단계] 미션 제출합니다. by changjun6518 · Pull Request #2 · changjun6518/java-lotto-1

안녕하세요! 😊 로또 미션을 구현해보았습니다! 🎓 구현하면서 조금이라도 궁금하고 의문이 들었던 생각들을 정리하고 코멘트에 남겼습니다! 피드백 주시면 저에게 큰 도움이 될 것 같습니다!

github.com

 


기능목록 작성 후에 처음 작성하는 코드는 뭘까?

    • 다들 어느정도 필요한 클래스를 구상하고 테스트 코드부터 작성하는 식으로 진행하는 듯 했다.

입력값은 보통 String 값인데 클래스 멤버변수로 다른 타입을 가질때 어떻게 할까?

  • InputView 영역 즉, 프론트엔드 쪽에서 정해진 형식을 통해 보내질 것이기 때문에 String이 아닌 Integer를 매개변수로 받아와도 될 것 같다.
  • 하지만 프론트엔드 뿐만 아니라 백엔드 쪽에서도 검증하는 함수가 필요하다.
  • 생각해보니 amount에 문자가 들어갈 경우 Integer.parseInt() 오류를 처리하기 난처했는데 매개변수로 Integer로 받기 때문에 컴파일?시에 오류가 나서 사전에 오류를 방지할 수 있다!
  • 이렇게 될 경우 위 isValidDigit 검증함수는 불필요하게 될 것 같다.

값 복사는 왜 해야할까? ( 위 Amount 클래스 생성자에서 검증 로직을 거치기 전에 값부터 복사하기 )

  • 위 예시로 보면 검증로직을 거치고 마지막에 this.amount = amount 를 하여 대입을 하게 된다.
  • 이렇게 진행하면 멀티스레드 환경에서 this.amount 에 대입 직전에 다른 스레드에 의해 매개변수가 가리키고 있는 값이 바뀔 가능성이 존재할 수 있다는 문제가 있다.

  • 생성자가 실행되면 생성자를 실행시킨 함수에 있는 amount(Stack에 있는)의 주소가 넘어온다.
    • 이때 주소가 넘어오는지에 대한 정확한 지식이 없다... 토론해보았지만 해답을 얻지 못했다.
    • JVM + 스택, 힙 변수 생성 과정 + 기본형 변수, Wrapper 클래스 생성 등
  • 하나의 스레드가 그 주소에 있는 값(10000)을 확인하면서 검증한다.
  • 이때 다른 스레드가 amount 값을 변경시킨다. (예를 들어 5000으로 변경)
  • 그러면 처음 스레드가 검증을 끝난 뒤 검증할때 값 10000 이 아닌 5000을 instance의 amount에 복사하게 된다.
  • 이러한 문제를 방지하기 위해 값부터 복사하고 복사한 값을 검증하는 식으로 하는 것 같다.

 


회고

오전에 하는 스터디사람들과 나의 궁금점들을 공유했다.

시험삼아 해보았지만 생각보다 많은 부분을 토론하게 되었고 혼자 고민했던 것보다 3배는 더 많은 것을 얻은 느낌이였다. 

앞으로 궁금증들을 모두 적고 정리하고 공유하면서 토론을 진행해보려고한다.

+ 우테코 미션 회고들(이전 기수들)을 참고하면서 배울 점들을 쏙쏙 빨아보려한다.

 

참고

[값복사]

[스택&힙]

 

+ Recent posts