우아한형제들/우아한테크코스

[우아한테크코스] 4주차 후기 - 전략패턴의 적용

Jay Tech 2019. 5. 31. 17:00
반응형

4주차 후기

날짜 TODO 추가 공부
5/27 (월) 좌표계산기 리뷰 -
5/28 (화) 강의 (Exception), 책-객체지향과 디자인패턴
5/29 (수) 프로그래밍 (로또)  
5/30 (목) - 책-객체지향과 디자인패턴
5/31 (금) 강의 (프런트엔드 기본) 책-객체지향과 디자인패턴
6/1 (토) 캡스톤 서류 정리  

이번 주는 조금 여유가 있는 상태였기 때문에 로또 프로그램을 빨리 구현하고 개인 공부를 했다. 주말일정은 몸이 좋지 않아서 예정되어 있던 테니스 시합을 취소하고 집들이를 다녀올 예정이다.

로또 미션: 전략 패턴의 적용

전략패턴(Strategy Pattern)

개발자가 반드시 정복해야할 객체지향과 디자인 패턴이란 책에서 이론적 내용을 학습 및 정리 후 프로젝트에 적용한 내용입니다.

 

로또 생성의 문제: 요구사항 중에는 자동생성 로또와 수동생성 로또가 있다. 자동은 랜덤으로 로또가 만들어질 것이고 수동은 사용자가 직접 입력할 것이다. if else 의 분기를 사용한다면 확장성이 매우 떨어지게된다. 예를들어, 다른 방식으로 로또를 generate하는 기능이 추가될 때마다 분기를 추가해 주어야 한다.

이럴 때 사용하는 것이 전략 패턴이다. 로또 생성 정책을 별도의 객체로 분리하는 것이다.

먼저 로또 번호 생성은 어떤 기능에도 공통적인 기능이라고 할 수 있다. 그렇다면 로또 번호 생성을 추상화 해야 한다. 이렇게 추상화하는 인터페이스를 우리는 전략(Strategy)라고 부르고 로또 번호를 직접 생성하는 부분을 컨텍스트(Context)라고 부른다. 이렇게 특정 컨텍스트에서 전략을 별도로 분리하는 설계를 전략 패턴이라고 한다.

 

숫자 생성 전략

이렇게 공통 기능을 추상화 해놓는다. 그리고 이것을 구현하는 실제 컨텍스트 클래스를 정의한다. 우리는 일단 두 가지가 필요할 것이다. 첫 번째 랜덤 생성, 두 번째 의도된 생성.

 

전략을 구현하여 랜덤한 숫자를 생성하는 구현체(컨텍스트)

RandomLottoGenerator.class 의 모습이다. 이것은 위의 전략인 LottoGenerator를 구현한다. 그리고 실제 구현은 그 메서드인 generateNumber를 오버라이드해서 정의한다. 1부터 45까지 랜덤한 숫자를 반환하는 구현체. 이 구현체가 자동 로또를 생성할 때 쓰일 것이다.

두 번째 컨텍스트 클래스를 정의해 보자. 이것은 수동 구매를 위한 컨텍스트 클래스 이다.

 

전략을 구현하여 의도된 숫자를 생성하는 구현체(컨텍스트)

IntendedLottoGenerator.class의 모습이다. 마찬가지로 전략(LottoGenerator 인터페이스)을 구현하고 오버라이드를 한다. 이것은 사용자가 Lotto의 리스트를 넘겨서 초기화를 하고 generate를 할 때 마다 그 숫자를 하나씩 빼온다. index가 다 차게되면 예외를 던지게 구현해 놓았다.

그렇다면 이제 실제 클라이언트 측에서 전략을 주입 (Injection)해보자.

 

구현체(컨텍스트)를 이용하여 자동 로또 숫자를 생성하는 로직

Lotto의 생성자는 generator를 주입받는다. 그리고 Lotto 내부의 로직을 보자.

 

로또 생성자에서 전략을 이용하는 로직

로또 자체는 LottoGenerator의 실제 구현체가 뭔지 전혀 몰라도 된다. 그냥 로또는 6개의 숫자 리스트를 만들기만 하면된다. 다시 말하면 로또는 인터페이스의 참조형으로 generator를 받는다. (구현체의 참조형으로 받으면 안됨!! 이 패턴을 쓰는 이유가 없어짐) 그리고 그 인터페이스의 메서드인 generateNumber()를 호출하게 된다. 이러면 패턴의 틀이 완성이 된다.

아까 위에서 정의한 IntendedLottoGenerator.class를 주입받는 부분을 보자. 이것은 로또 수동구매에서 필요한 부분이다.

 

구현체(컨텍스트)를 이용하여 의도된(수동) 로또를 생성하는 로직

 

이번에는 로또 생성자에 다른 Generator를 주입했다. 그렇다면 사용자가 지정한 번호대로 로또가 생성이 될 것이다. 이 때 Lotto 클래스의 로직은 전혀 손 댈 것이 없다. 이미 generateNumber() 메서드를 구현해 놓았기 때문에 그냥 new 호출로 로또가 만들어질 것이다. 이것이 전략패턴이다.

 

전략 패턴의 도식

도식을 보면 이렇게 될 것이다. 컨텍스트를 사용하는 클라이언트가 전략의 상세 구현에 대한 의존이 생긴다. 전략의 구현 클래스와 클라이언트쪽 코드가 쌍을 이루기 때문에 유지보스가 발생할 가능성이 줄어든다. 전략 패턴의 장점은 위 처럼 새로운 전략을 추가할 때 다른 코드를 변경할 필요가 없다는 것이다. 그렇게 되면 유지보수가 훨씬 좋아지게 되고 확장성이 좋아진다.

즉 확장에는 열려있고 변경에는 닫혀있는 개방 폐쇄 원칙을 따르게 된다.

강의: Exception 구분하기

Checked Exception 확인된 예외

Checked Exception (혹은 Compile Time Exception)은 Exception을 상속하는 예외를 말한다. 즉 컴파일 시점에 예외를 캐치하는지 확인한다. 다시 말해 throws를 명시해주어야 한다.

public class InvalidPositionException extends Exception {
    // 중략
}

// InvalidPoisition을 사용하는 메서드
public Position(String position) throws InvalidPositionException {
    if (condition) {
        throw new InvalidPositionException();
    }
}

이렇게 Checked Exception은 throws를 명시를 해주지 않으면 컴파일 에러를 발생시킨다.

Unchecked Exception 확인되지 않은 예외

Unchecked Exception(혹은 Runtime Exception)은 RuntimeException을 상속한다. 컴파일 시점에 예외를 판단할 수 없다. 즉 throws 를 써주지 않아도 컴파일 에러가 발생하지 않는다.

public class InvalidPositionException extends RuntimeException {
    // 중략
}

// InvalidPoisition을 사용하는 메서드
public Position(String position) {
    if (condition) {
        throw new InvalidPositionException();
    }
}

Checked Exception과는 달리 throws를 써주지 않아도 괜찮다.

CheckedException, UncheckedException중 무엇을 써야 할까?

  • 호출하는 메서드가 Exception을 이용하여 의미있는 작업을 한다면 Checked Exception 사용, 그렇지 않으면 Unchecked Exception
  • 명확하지 않으면 Unchecked Exception

사고개선

이번 과제를 하면서 객체지향적으로 생각하는 사고가 부족했던 것 같다. 단순히 로또 구매 전체개수 = 자동구매개수 + 수동구매개수 로 접근을 하여 수학적 더하기 빼기로직을 구현했다. 옆에 있던 크루가 어떤 식으로 했는지 설명을 해주었는데 나의 접근법이 아예 처음부터 잘못되었다는 것을 깨달았다. 그 분의 방법은 로또 구매를 위한 돈을 지불하면 OMR카드를 주고 로또머신에 OMR을 넣고 수동 구매를 하면 나머지가 자동구매가되어 티켓이 출력되는 방식이었다. 실생활 예제와 완전히 같은 스토리로 프로그램이 전개가 되고 있었다. 이 접근법이 훨씬 좋다고 생각했다. 단순한 수학적 로직에만 갇혀있던 시야가 확장되는 느낌을 받았다.

객체지향의 사실과 오해 (- 조영호 저)라는 책을 정독해보아야 겠다.

 

반응형