[NEXTSTEP] 두 번째 미션을 마무리 하면서
대외활동

[NEXTSTEP] 두 번째 미션을 마무리 하면서

들어가며

  • 이번 글에서는 두 번째 미션인 로또 과제를 진행하면서 배웠던 점과 아쉬웠던 점에 대해서 정리하고자 합니다.

배운 점

인스턴스 필드가 3개일 때부터는 리팩터링 해라

  • 연관성이 있는 새로운 클래스로 만드는 것을 고려하기
    • 변수가 3개일 때부터는 별도의 도메인으로 분리해 볼 필요가 있는 객체일 수 있으니 클래스 분리를 고려해봐야 합니다.
  • 정말 필요한 변수인지 고민하고 필요 없다면 제거하는 걸 고민해 볼 것을 추천합니다.

유효성 검사는 생성자에서 하자

  • 개발자마다 유효성 검사를 하는 방법은 다양합니다.
    • ValidatorUtils를 사용해서 구현을 하는 방법이라든가
    • 아니라면 if문으로 유효성을 검사를 하는 방법이라든가..
  • 하지만 객체가 생성전에 객체가 생성될 수 있는 유효성 검사를 하는 것이 가장 좋은 방법일 것입니다.
    • 객체가 실행되는 도중보다는 당연히 생성되는 최초 시점에 검사를 통해 수행되지 말아야 할 로직들이 수행 안되도록 만드는 것이 좋기 때문입니다.

private 메서드는 리플렉션을 사용해서 테스트가 가능하다.

public class MyClass {
    private String privateField = "privateValue";

    private void privateMethod() {
        System.out.println("리플렉션을 사용한 메소드 접근, privateMethod called");
    }

    public static void main(String[] args) throws Exception {
        MyClass myObject = new MyClass();

        // private 필드에 접근하기
        Field privateField = MyClass.class.getDeclaredField("privateField");
        privateField.setAccessible(true); // 접근 가능하게 설정
        String fieldValue = (String) privateField.get(myObject);
        System.out.println("리플렉션을 사용한 필드 접근, privateField : " + fieldValue);

        // private 메서드에 접근하기
        Method privateMethod = MyClass.class.getDeclaredMethod("privateMethod");
        privateMethod.setAccessible(true); // 접근 가능하게 설정
        privateMethod.invoke(myObject); // private 메서드 호출
    }
}
  • pirivate 접근 제어자로 선언된 경우 테스트가 불가능할 것이라 생각하지만 powerMock, 자바 리플렉션을 사용해서 테스트 가능합니다.
  • 위 예시 코드는 Java의 리플렉션을 사용해서 접근제어자가 pirvate인 메서드도 호출해서 사용하는 예제입니다. 필요에 따라 private 메소드도 테스트를 진행해야 합니다.

Enum 사용을 고려해라

  • Enum은 JVM의 상수 영역에 값을 한 번씩만 생성하므로 일반 상수 필드를 사용하는 방식보다 메모리가 절약됩니다.
  • 상수의 범위가 정해진 비즈니스라면 enum을 고려해 보는 것은 좋은 방법입니다.
  • Enum 자료형에 대해 Map 구조를 사용해야 한다면 EnumMap을 사용해야 합니다. EnumMap은 일반 Map과 다르게 메모리의 연속적인 자료 할당을 통해 기존 Map과 성능적으로 뛰어난 장점이 존재하기 때문입니다.

캐시를 고려해라

public static LottoNumber fromInt(final int value) {
        return Arrays.stream(LottoNumber.values())
            .filter(lottoNumber -> lottoNumber.getLottoNumber() == value)
            .findFirst()
            .orElseThrow(() -> new SizeExceedLottoNumberException(value));
    }
  • 위와 같이 values를 통해 매번 n번의 반복을 통해 객체를 조회하는 방식의 코드가 있습니다.
  • 이러한 코드는 객체가 생성되는 시점에 static문을 사용해 캐시 작업을 진행하여 성능을 개선할 수 있습니다.
public enum LottoNumber {
	
	private static final Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();
	
	private final int value;
	
	static {
	    for (LottoNumber lottoNumber : LottoNumber.values()) {
	        lottoNumberCache.put(lottoNumber.value, lottoNumber);
	    }
	}
}
  • 사용하려는 객체가 초기에 생성되어 특정 값을 가지고 있어야 한다면 위와 같이 static을 사용해 객체가 생성되는 시점에 객체를 초기화하여 캐시 작업을 진행할 경우 성능을 높일 수 있습니다.
public static LottoNumber fromInt(final int value) {
        LottoNumber lottoNumber = lottoNumberCache.get(value);
        if (lottoNumber == null) {
            throw new SizeExceedLottoNumberException(value);
        }
        return lottoNumber;
    }
  • 이를 통해 위와 같이 O(1) 번으로 객체 조회를 리팩터링 할 수 있습니다.

객체명을 작성할 때 Data, Info와 같은 불용어는 사용하지 마라

  • 객체명은 해당 객체가 어떤 역할을 수행하고 있는지 명확하게 전달해야 합니다. "Data"나 "Info"와 같은 불용어는 너무 일반적이어서 객체가 무엇을 나타내는지 명확히 알려주지 않습니다.

NPE 방지를 위한 NullObject 패턴이 있다.

  • Null Object 패턴은 객체 지향 소프트웨어 디자인 패턴 중 하나로, 객체의 일부분이 null로 설정되었을 때 발생하는 예외 상황을 처리하는 데 사용됩니다. 이 패턴은 프로그램의 안정성과 가독성을 향상하며, 예외 처리를 간단하게 만들어줍니다.
  • Null Object는 실제 객체와 동일한 인터페이스를 제공하지만 실제 동작은 아무것도 수행하지 않는 빈 객체입니다.
// Shape 인터페이스
interface Shape {
    void draw();
}

// 실제 객체 - 원
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// 도형을 그리는 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        Shape shape1 = getShape("circle");
        // null 처리 로직
        if (shape1 != null) {
            shape1.draw();
        } else {
            System.out.println("도형을 찾을 수 없습니다.");
        }
    }
    
    // 도형을 반환하는 팩토리 메서드
    private static Shape getShape(String shapeType) {
        if ("circle".equalsIgnoreCase(shapeType)) {
            return new Circle();
        }else {
	        return null;
        }
    }
}
  • 예시 코드를 들어 설명해 보겠습니다. 도형의 모양을 출력해 주는 인터페이스가 있을 때 도형을 반환하는 팩토리 메서드를 통해 도형을 조회하는 예시입니다.
  • 이때 도형이 존재하지 않을 경우를 대비해서 위와 같이 예외처리를 해야 합니다. 위와 같이 if문을 사용해서 조건을 검증하고 분기하는 로직은 가독성을 높게 코드를 작성했다고 볼 수 없습니다.
// Shape 인터페이스
interface Shape {
    void draw();
}

// 실제 객체 - 원
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// Null Object - 아무것도 그리지 않음
class NullShape implements Shape {
    @Override
    public void draw() {
        // 아무 동작도 수행하지 않음
    }
}

// 도형을 그리는 클라이언트 코드
public class Client {
    public static void main(String[] args) {     
        // Null Object 패턴을 사용한 경우
        Shape shape2 = getShape("square");
        shape2.draw(); // 실제 객체인 경우 그리기 동작을 수행하며, Null Object인 경우 아무 동작도 수행하지 않음
    }
    
    // 도형을 반환하는 팩토리 메서드
    private static Shape getShape(String shapeType) {
        if ("circle".equalsIgnoreCase(shapeType)) {
            return new Circle();
        } else {
            // Null Object 패턴 적용
            return new NullShape();
        }
    }
}

  • 위와 같은 경우 NullObject 패턴을 도입할 경우 인터페이스 아무런 동작도 안 하는 NullObject를 반홤한으로써 if 분기문 로직이 사라지게 되고 이를 통해 클라이언트는 가독성이 높은 코드를 읽을 수 있게 됩니다.

과제를 수행하면서 아쉬웠던 점

더 좋은 자료구조를 고려하지 못했다는 것.

  • 로또의 공의 개수는 6개여야 하나 중복된 공의 개수가 들어올 수 있다.
  • 이때 List로 입력받고 set으로 중복검사를 진행했었는데 이러면 List가 아닌 Set을 자료형으로 사용하면 되지 않았을까 하는 아쉬움이 남는다.

주생성자를 잘 활용하지 못했다

정리

  • 인스턴스 필드가 3개일 때부터는 리팩토링을 고려해라 필드 개수를 줄여나갈 것을 권장합니다.
  • 접근제어자 private인 경우 리플렉션을 사용해서 테스트 가능합니다.
  • Enum과 Cache를 로직을 작성하면서 항상 고려하세요.
  • 객체명은 불용어를 사용하지 않아야 하며 명확해야 합니다.
  • NPE를 방지하기 위해 NullObject 패턴을 고려하세요.