"AlgoSpot DRAWRECT 문제 풀이"에 대한 2가지 코드를 준비해 봤습니다. 그중에서 두번째로 객체지향적으로 문제를 풀어보는 코드입니다. 객체지향 프로그래밍의 목표는 컴퓨터가 최대한 효율적으로 문제를 해결할 수 있도록 코드를 작성하는 것이 아니라, 인간이 세상을 바라보는 시각에 근접하게끔 코드를 작성하는 것입니다. 따라서, 절차지향적 코드에 비해 코드의 분량이 좀 더 길어지거나, 상대적으로 비효율적으로 느껴질 수 있습니다. 그러나, 하드웨어의 성능 발전, 컴파일러 기술의 향상 등으로 인해 조금 더 길어진 코딩이라고 할지라도 전체적인 성능 상의 차이는 두드러지게 나타나지는 않는 편입니다. (참고 : 절차지향 코드 버전)
더불어, 객체지향 기법을 적용하는 분야(혹은 도메인)에서는 기계 보다는 인간의 관점을 중시하는 경향이 있습니다. 객체지향 혹은 절차지향 중 어느 한 쪽이 우월한가를 따지는 것은 별 의미가 없습니다. 객체지향과 절차지향은 각각의 장점과 단점을 가지고 있고, 해결해야할 문제가 일상생활(실세계)의 문제에 가까울수록 객체지향 기법으로 설계하고 코딩했을 때 얻을 수 있는 이득이 많습니다. 이점을 염두에 두시고, 이어지는 설명과 구현 절차를 읽으셔야 합니다.
▶ 객체지향 철학 그리고 5대 개념
문제 풀이에 앞서 객체지향 철학 및 5대 개념을 먼저 읽어보시는 것을 권합니다.
▶ UML(Unified Model Language)
객체지향으로 설계 및 구현할 때는 플로우 챠트가 아니라, 가급적 UML(Unified Modeling Language)을 이용해 설계합니다. (플로우 차트를 절대 사용하지 말아야 한다는 것은 아닙니다. 객체지향 코드 내에서도 논리적인 흐름이 발견되고 논리적인 흐름을 표현하는데는 플로우 차트가 유용합니다.) UML 에서는 다양한 다이어그램을 이용해 소프트웨어의 모델을 설계하는데, 그중에서 유스 케이스(Use Case), 클래스 다이어그램(Class Diagram) 그리고, 시퀀스 다이어그램(Sequence Diagram)을 가장 많이 사용하게 됩니다. "UML: 클래스 다이어그램과 소스코드 매핑" 문서를 읽어보시면 도움이 될 듯 합니다.
UML 중에서 유스 케이스(Use Case)는 가장 직관적이면서 이해하기 쉽고, 가장 먼저 작성하게 됩니다. 사용자(actor)가 컴퓨터(소프트웨어 혹은 하드웨어)를 이용해 어떤 작업들을 수행하는가? 소프트웨어가 어떤 기능을 제공하는가? 라는 질문에 대해 가장 포괄적이고, 직관적으로 보여주는 다이어그램입니다. 아래 그림을 보시면, 논리적인 흐름이나 기술적인 정보는 아무것도 표현하지 않습니다. 객체지향 설계의 접근 방식의 모토(motto)는 바로 인간의 시선에서부터 시작하자는 것입니다.
유스 케이스 다이어그램의 목적은 시스템 혹은 소프트웨어에 대한 요약(brief), 개요(outline or overview)를 제공하는 것입니다. 소프트웨어를 구현함에 있어서 가장 핵심적인 기능은 무엇인가? 좀 더 세세한 설계를 진행하기에 앞서 시스템을 최대한 간략히 묘사함으로써 개발자/기획자/설계자 등 프로젝트에 참여하는 모든 사람들이 공통의 목표를 명확히 공유할 수 있도록 하는 것이 목표입니다.
DRAWRECT를 정의하는 유스 케이스는 매우 단순합니다. 3개의 좌표를 제시하고, 사각형의 4번째 좌표를 계산하는 것입니다. 유스 케이스 다이어그램으로 표현하면 아래와 같습니다.
▶ 객체 후보 정하기
절차지향 프로그램은 고유한 기능을 수행하는 함수(혹은 서브루틴)들을 프로그래머가 지정한 절차에 따라 연속적으로 실행시켜 결과를 얻는 방식입니다. 공장의 컨베이너 벨트처럼 잘 짜여진 흐름에 따라, 연쇄적으로 하나씩 작업들이 이어가는 것이죠. 따라서, 소프트웨어 개발자 혹은 설계자는 각각의 작업을 마친 후에 다음 작업이 무엇이 실행되어야 하고, 각 단계에서 가공된 데이터를 다음 단계의 함수로 어떻게 전달할 것인가? 동적인 흐름을 하나씩 따져가며 문제를 풀어야 합니다.
반면에 객체지향 프로그래밍은 객체라는 형태의 부품들을 먼저 정의 혹은 추출합니다. 이것은 사람이 현실 세계를 이해하는 방식에서 비롯된 것입니다. 우리는 움직이는 자동차를 바라볼 때 자동차의 가속, 감속, 회전 등의 동작(behavior)과 자동차 자체의 형태, 무게, 차종 등의 상태 정보(status)를 구분해서 인식하지 않습니다. 그냥 자동차 자체는 동작과 상태를 함께 지니고 있다고 받아들입니다. 이러한 인간의 사고 방식을 따르는 객체지향 프로그램에서 객체라는 요소는 행위(behavior)와 상태(status)를 함께 가지고 있는 소프트웨어 부품(parts, component)입니다. 객체지향 설계에서는 문제를 푸는 절차에 앞서, 해결하고자 하는 문제 자체에 포함된 다양한 객체를 먼저 식별(identify or extract)하고, 각 객체들이 서로 어떻게 연결되고, 함께 상호 작용하는지를 파악합니다.
DRAWRECT 문제에서 객체(혹은 클래스)로 구분지을 수 있는 후보군은 다음과 같습니다. 후보군이라고 말하는 것은 설계하는 사람의 관점에 따라 객체 혹은 클래스가 아니라고 볼 수도 있다는 말입니다. 정확히 어떤 방식으로 설계해야 하고 어떤 클래스들을 정의해야 한다는 엄격한 기준은 없습니다.
객체(클래스) 후보 |
행위 (behavior) |
상태(status) |
뷰포인트 |
입력 좌표 값의 정상 유무 검증 |
최소, 최대 좌표 허용 범위 |
좌표 |
X축, Y축 값 저장 및 조회 |
X축 및 Y축 좌표 값 |
사각형 |
p1, p2, p3 등 3개의 좌표 저장 p4 좌표 계산 |
p1, p2, p3 등의 좌표 정보 |
▶ 객체 간의 정적 관계 설계 (클래스 다이어그램)
객체 간의 정적인 관계는 클래스 다이어그램을 이용해 정의합니다. 관계(relation)를 통해 객체 혹은 클래스들이 서로 간에 어떤 의미(역할)을 가지는지를 나타내게 됩니다. 자동차의 부품들이 서로 아무런 상관없이 존재하거나, 독립적으로 동작하는 것이 거의 없는 것처럼 객체지향 프로그램에서도 프로그램의 부품에 해당하는 객체들에 각각의 역할과 의미가 부여되어야 합니다. 클래스 다이어그램을 통해서 각 클래스 간의 상속, 합성, 연관 등의 정적인 관계를 파악합니다. 이러한 관계 부여는 실제 코딩으로 이어지기 때문에 의미 없는 문서화 작업으로 치부해서는 안됩니다.
- 사각형(Rectangle)은 좌표(Point)를 포함합니다. 이를 소유 관계(has-a relation)라고 합니다.
- 뷰포인트(Viewpoint)는 좌표(Point)를 검증합니다. 이를 연관(association)이라 합니다.
▶ 객체 간의 상호 동작 설계 (시퀀스 다이어그램)
객체 혹은 클래스들이 함께 동작하는 과정에서 어떤 기능을 호출하는지와 어떤 정보를 주고 받는지를 표현하는 것이 시퀀스 다이어그램입니다. 시퀀스 다이어그램은 기능의 흐름을 표현한다는 점에서 플로우 차트와 유사하나, 논리적인 판단(if 조건 등)을 기술하지 않는다는 점은 플로우 차트와 다릅니다. 앞서 말한 것처럼 객체지향 설계에서는 세밀한 논리적 흐름보다는 큰 얼개(구조)를 표현하는데 집중합니다.
▶ 객체지향 코드 작성
위와 같은 설계를 바탕으로 구현된 코드는 아래와 같습니다.
package algospot.exam.drawrect; /** * 점(point) 클래스. * */ public class Point { // X 축 좌표. private int xCoord; // Y 축 좌표. private int yCoord; /** * 생성자와 getter 메소드만을 제공하는 이유는 최초 값 설정 후에 잘못 변경되는 것을 막기 위함이다. * * @param xCoord X 좌표 * @param yCoord Y 좌표 */ public Point(int xCoord, int yCoord) { this.xCoord = xCoord; this.yCoord = yCoord; } public int getXCoord() { return xCoord; } public int getYCoord() { return yCoord; } public String toString() { return String.format("(x = %d, y = %d)", xCoord, yCoord); } }[Viewport.java]
package algospot.exam.drawrect; public class Viewport { private static final int MIN_COORDINATE = 1; private static final int MAX_COORDINATE = 1000; public static boolean validatePoint(Point point) { return point.getXCoord() >= MIN_COORDINATE && point.getXCoord() <= MAX_COORDINATE && point.getYCoord() >= MIN_COORDINATE && point.getYCoord() <= MAX_COORDINATE; } }[Rectangle.java]
package algospot.exam.drawrect; /** * 직사각형 클래스 * @author "Sunny Kwak" * */ public class Rectangle { private Point p1; private Point p2; private Point p3; public Rectangle(Point p1, Point p2, Point p3) { this.p1 = p1; this.p2 = p2; this.p3 = p3; } /** * 3개의 사각형 좌표 값을 이용해 마지막 좌표를 계산한다. * @return */ public Point calculateP4() { int x = 0, y = 0; if(p1.getXCoord() == p2.getXCoord()) { x = p3.getXCoord(); } else if(p1.getXCoord() == p3.getXCoord()) { x = p2.getXCoord(); } else if(p2.getXCoord() == p3.getXCoord()) { x = p1.getXCoord(); } if(p1.getYCoord() == p2.getYCoord()) { y = p3.getYCoord(); } else if(p1.getYCoord() == p3.getYCoord()) { y = p2.getYCoord(); } else if(p2.getYCoord() == p3.getYCoord()) { y = p1.getYCoord(); } return new Point(x, y); } }[AlgoSpotDrawRect.java]
package algospot.exam.drawrect; import java.util.Scanner; /** * 직사각형의 3개 좌표를 입력 받아 4번째 좌표를 계산하는 프로그램. * * @author "Sunny Kwak" */ public class AlgoSpotDrawRect { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 테스트 케이스 반복 횟수 입력. System.out.print("Input test case number : "); int loopCnt = scanner.nextInt(); // 입력 받은 테스트 케이스 수만큼 반복 처리... for (int cnt = 0; cnt < loopCnt; cnt++) { // pt, p3, p3 등 3개의 좌표 값 입력 Point p1 = readPoint(scanner); Point p2 = readPoint(scanner); Point p3 = readPoint(scanner); // 직사각형 객체 생성 및 4번째 좌표 계산 Rectangle rectangle = new Rectangle(p1, p2, p3); Point p4 = rectangle.calculateP4(); // 정상적으로 계산된 경우, 4번째 좌표 출력 System.out.printf("%d %d\n", p4.getXCoord(), p4.getYCoord()); } scanner.close(); } /* * x, y 좌표 입력 및 정상 좌표 유무 검사. * 정상 좌표인 경우, Point 객체를 생성한 후 반환. */ private static Point readPoint(Scanner scanner) { System.out.print("Input p1 (x, y) : "); int xCoord = scanner.nextInt(); int yCoord = scanner.nextInt(); Point point = new Point(xCoord, yCoord); if (Viewport.validatePoint(point)) { return point; } else { throw new IllegalArgumentException("Invalide coordinate : " + point); } } }