자바지기 박재성님이 운영하시는 NextStep 교육 웹사이트에서 수강한 자바 플레이 그라운드 with TDD, 클린코드에 대한 공부 기록을 남기려고 합니다. 이번 포스팅은 자바의 상속, 인터페이스에 관련된 좌표 계산기 미션에 관한 포스팅입니다.
강의를 통해 느낀 점 (피드백을 보기 전)
프로그래밍 요구사항에는 이전 미션과 크게 차이가 없었다.
기능요구사항도 좌표 입력 형식, 좌표 입력 값 범위 등 크게 특별하다고 느낀 부분은 없었다.
기억할 부분은 좌표가 2개면 직선 거리 계산, 좌표가 3개면 삼각형 넓이 계산, 좌표가 4개면 직사각형으로 계산하는 것이다.
직사각형인 경우에는 직사각형인지 validation을 해줘야 한다.
먼저 미션에서 필자는 enum을 적극적으로 사용하자는 생각을 했다.
도형의 좌표 개수를 기준으로 if문을 사용할 수도 있지만, 아름답고 보기 좋은 코드는 아니라고 생각했다.
많은 enum 자료를 살펴보았지만 우아한 형제들에 계실 때 향로님의 enum 포스팅을 많이 참고했다.
본인도 enum을 사용해 상태와 행위를 한 곳에서 처리할 수 있어서 더 예쁜 코드가 된 것 같고,
유지 보수 측면에서도 유리하다는 생각이 들었다. 부족하지만 작성한 코드는 다음과 같다.
public enum ShapeGroup {
LINE(2){
@Override
public Shape createShape(List<Position> positions) {
return new Line(positions);
}
},
TRIANGLE(3){
@Override
public Shape createShape(List<Position> positions) {
return new Triangle(positions);
}
},
RECTANGLE(4){
@Override
public Shape createShape(List<Position> positions) {
return new Rectangle(positions);
}
};
private final int positionNumber;
public abstract Shape createShape(List<Position> positions);
public static Shape getShape(int number, List<Position> positions){
return Arrays.stream(values())
.filter(shapeType -> shapeType.positionNumber == number)
.map(shapeType -> shapeType.createShape(positions))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("좌표의 개수가 올바르지 않습니다."));
}
ShapeGroup(int positionNumber) {
this.positionNumber = positionNumber;
}
}
enum, 스트림과 람다, 오버라이딩을 사용해 필자가 생각했을 때는 꽤 만족스러운 코드가 작성된 것 같다.
일단 이렇게 객체를 생성하는 로직을 작성했다.
인터페이스를 사용해 사용할 추상 메서드를 미리 정의하고 인터페이스를 구현한 구체 클래스에서 추상 메서드를 오버라이딩해 사용했다. OutputView를 만들 때는 안 예쁘게 찍혔다. 찍히긴 찍히는데 반듯하지 않은 느낌..
피드백을 본 후
우선 피드백 영상은 없었고 몇 가지 요구사항이 있었다.
바로 Factory 클래스의 getInstance() 메소드를 반들어서 if문 없이도 객체를 생성하게 만드는 것이었다.
(아마 객체 생성의 책임을 Factory 클래스에 아예 맡겨버리는 것!!! 이전에 토비의 스프링에서 본 팩토리 방식이다.)
또한 직선, 삼각형, 사각형 클래스를 모두 Figure 인터페이스를 구현한 구체 클래스를 작성한다.
구체 클래스 객체를 생성하면 추상화를 사용해 Figure 인터페이스 타입으로 리턴한다. 쉽게 말해 코드는 아래와 같다.
public interface FigureFactory {
Figure create(List<Point> points);
}
그럼 어떻게 if문을 사용하지 않고 생성할까??
미리 static {} 초기화 구문을 이용해 Map 인스턴스 변수를 생성한다.
이 Map 변수는 키가 좌표의 개수(2, 3, 4)고 value는 Figure클래스를 구현한 구체 클래스의 생성자이다.
이 방식을 사용해 미리 static 구문에서 Map 변수를 선언해 우리가 사용할 값들을 넣는다.
따라서 좌표의 개수를 Map 변수의 키로 넣어서 가져오면, 구체 클래스가 생성된다. 이 방법도 정말 좋은 방법이라고 생각했다.
사실 더 놀란 건 선생님이 작성하신 코드였다. 인터페이스 -> 추상 클래스 -> 구체 클래스 이 순으로 이루어지는데 보면서
예쁘게 잘 짜셨다.. 라는 생각이 들었다. 먼저 정리하면 다음과 같다.
- 중복되는 코드가 있으면 추상 클래스를 사용해서 제거한다.
- 인터페이스를 설계를 하고 책임을 드러낸다. 물론 추상화를 통해 다형성이라는 이점도 존재한다.
본인은 인터페이스만 자주 사용하고 추상 클래스는 자주 사용하지 않았는데, 앞으로는 추상 클래스도 고려해봐야겠다.
그리고 사실 제일 많이 참고한 OutputView!!! 나는 코드도 안 예쁘고 출력창도 생각보다 깔끔하지 않았다.
그래서 선생님의 코드를 참고 했는데 String.format(); 코드가 제일 많이 보였다.
이 메서드가 정렬을 깔끔하게 해주는데 이를 알고 본인이 사용했더라면 코드도 더 깔끔해지고,
콘솔도 더 명확하게 나왔을 것이라고 생각한다. 하나 또 얻어간다. 역시 자바 기초도 중요하구나 라고 다시 한 번 느꼈다.
마지막으로 nextstep 슬랙을 살펴보다가 발견했는데, 현재 본인의 인터페이스는 다음과 같다.
public interface Shape {
List<Position> getPositions();
double calculateArea();
boolean hasPoint(int x, int y);
String getDistanceInfo();
void validatePositions();
}
calculateArea()를 봐보자. Shape를 구현한 Line이라는 클래스가 있다.
근데 Line은 점과 점을 연결하는 것이므로, Area를 계산하는 것이 말이 안된다.
그래서 슬랙을 살펴보다가 좋은 내용을 발견했다.
Line이 Area를 가지는 것이 말이 안되지만 Shape과 Line이 is-a(같은 패키지에도 존재) 관계이므로 일단 구현했다고 하셨다.
만약 Line에서 calculateArea 기능을 지원하지 않는 의도라면 UnsuupoertedException과 같이
예외를 throw하는 것도 한 방법이라고 말씀하셨다. 좋은 글을 통해 한층 더 배울 수 있었다. 굿굿.
피드백을 보고 난 후 TDD
코드는 아래 링크에 있다.
https://github.com/happysubin/java-coordinate-playground/commits/challenge
미션을 진행하면서 추상화를 통해서 인스턴스를 생성하다보니 구체 클래스의 메서드만 단위 테스트로 작성하기 어려웠다.
너무 많은 추상화는 확실히 독이 되는 것 같다. 좋은 개발자는 어느 정도가 적당한지 선을 잘 찾는 것이라고 생각한다. 타협이라고 하나..?
본인도 그런 개발자가 될 수 있었으면 좋겠다.
그리고 역시!!!! 아직 부족한건지 거리 계산을 진행할 때 객체에 메세지를 보내야한다는 사실을 잊어버렸다.
필드 값을 게터로 가져와서 계산했다. 역시 아직 부족하다. 더더 열심히 해야겠다.
그래도 enum도 잘 써보고 좋은 코드와 얘기를 보면서 많이 배운 것 같다.
학기 중에 꼭 한 번 다시 코드를 짜면서 복습을 해보자.
이상으로 포스팅을 마칩니다. 감사합니다.
참고한 글
https://techblog.woowahan.com/2527/
댓글