추상클래스
추상화?
- "생각이 없다"
- String.length() 메서드를 쓸 때 구체적인 구현 방식을 생각하지 않음 == 추상적이다
- 구체적인 구현을 생각하지 않는 클래스 == 추상 클래스
추상 클래스의 특징
- 개념 : 하나 이상의 추상 메서드를 포함하는 클래스
- 기본 구조 : abstract 키워드로 추상 클래스, 추상 메서드 선언
- 추상 메서드를 포함하고 있다는 걸 제외하면 일반 클래스와 비슷하다 == 생성자O, 멤버변수O, 메서드O
public abstract class Shape { //추상 클래스
private String type;
public Shape(String type){
this.type = type;
}
public void printShape(){
System.out.println("도형 타입: " + this.type);
}
public abstract void draw(); //추상 메서드
}
이때 추상메서드는 구체적인 코드를 작성하지 않고 선언으로 남겨둔다. 추상 클래스를 상속받는 자식 클래스에서 메서드를 다르게 구현할 수 있게 하기 위함이다.
abstract class Shape { //추상 클래스
private String type;
public Shape(String type){
this.type = type;
}
public void printShape(){
System.out.println("도형 타입: " + this.type);
}
public abstract void draw(); //추상 메서드
}
class Circle extends Shape { //상속
int radius;
@Override //추상 메서드 구현
public void draw() { System.out.println("원 그리기");}
}
class Rectangle extends Shape { //상속
int width, height;
@Override //추상 메서드 구현
public void draw() { System.out.println("사각형 그리기");}
}
하지만 추상클래스는 인스턴스 객체를 직접 생성할 수 없다. 즉 new 생성자나 static 사용이 불가능하다. 따라서 자식 클래스에 상속시키고, 자식 클래스의 인스턴스를 통해 접근해야 한다.
추상클래스의 생성자를 사용하기 위해서는 자식 클래스의 메서드에서 super() 메서드를 사용해야 한다. 자식이 없으면 아무것도 할 수 없는 클래스이다.
public abstract class Shape { //추상 클래스
private String type;
public Shape(String type){
this.type = type;
}
public void printShape(){
System.out.println("도형 타입: " + this.type);
}
public abstract void draw(); //추상 메서드
}
class Material extends Shape {
public String name;
public Material(String type, String name) {
super(type); // 추상 클래스의 생성자 호출
this.name = name;
}
public void draw() {
System.out.println("도형 재료: " + this.name);
}
}
public class main {
Material m = new Material("Circle", "Sand");
m.type; //Circle -> 부모 클래스의 멤버를 super()을 통해 초기화함
m.name; //Sand
}
추상 클래스를 사용하는 이유
1. 부모의 메서드를 강제적으로 구현해야 한다.
만약 자식 클래스에서 필요한 메서드를 구현하지 않았을 때 컴파일러가 잡아준다. 일반 클래스에서의 상속일 경우, 오버라이딩하지 않아도 부모의 메서드가 호출되기 때문에 프로그램은 잘 실행된다. 사용자의 의도와 다르게 작동될 수 있으며, 디버깅하기도 힘들다.
2. 프로그램의 규격이 생겨 설계가 편리해지고, 수정이 쉬워진다.
미리 설계해 둔 상속 클래스를 상속받으면, 개발자는 프로젝트에서 필요한 필드와 메서드를 오버라이딩해서 구현만 하면 된다. 또한 요구사항이 생겼을 때 규격에 대한 구현만 수정하면 되어 쉽게 기능 수정이 가능하다.
인터페이스
인터페이스의 개념
- 일반적인 개념 : 사용자가 기기를 쉽게 사용하는 데 도움을 주는 상호작용 시스템
- 자바에서의 인터페이스 : 객체의 인스턴스 메서드를 사용할 때, 해당 객체의 내부 구현을 몰라도, 원하는 메소드를 간편하게 호출하고 값을 받을 수 있게 하는 기능
인터페이스의 특징
- 추상 메서드 집합
- 필드는 상수로만 정의할 수 있음 == 구현체와 독립적이다
- implements 키워드로 인터페이스를 자식 클래스에서 구현함
- 인터페이스가 포함하는 추상 메서드를 반드시 구현해야 함
- 다중 상속이 가능함
public interface TV {
public static final int MAX_VOLUME = 10;
int MIN_VOLUME = 10; //모든 필드는 public static final --> 생략 가능
public abstract void turnOn();
void turnOff(); //모든 메서드는 public abstract --> 생략 가능
}
public class MyTV implements TV {
@Override
void turnOn() {
System.out.println("티비 켜라");
}
@Override
void turnOff() {
System.out.println("티비 꺼라");
}
}
//여러 인터페이스를 상속할 수 있음
public class BaseBall implements Sports, Ball{
//...
}
//클래스, 인터페이스를 동시에 상속할 수 있음
public class Soccer extends Sport implements IBehavior {
//...
}
//물론 다 섞어놓는 것도 가능!
public class Cat extends Tail implements Animal, Pet {
//...
}
인터페이스의 필드는 모두 public static final임을 주의하자. 상속을 하더라도 독립적으로 사용한다.
interface Iflower {
int cnt = 0;
}
class Sunflower implements Iflower{
int cnt = 10;
}
class Main {
Sunflower s = new Sunflower();
Iflower i = new Sunflower();
System.out.prinln(s.cnt);
System.out.println(Iflower.cnt); //인터페이스명.멤버이름 으로 접근
}
앞에서까지는 인터페이스에서는 구현체를 가질 수 없다고 배웠다. 그런데 java 8 버전부터 인터페이스에서도 구현 메서드를 정의할 수 있게 되었다. 바로 defalut와 static을 사용하여 구현할 수 있다.
default는 인터페이스를 구현한 이후(구현한 자식 클래스들이 있을 경우) 새로운 기능을 추가하고 싶을 때 사용할 수 있다. 기능을 그냥 추가한다면 그 인터페이스를 구현하는 모든 클래스에서 수정해야 하기 때문에, default를 사용하면 추가적인 구현 없이 간단하게 추가가 가능하다.
static은 기존에 사용하는 방식 그대로 사용하면 된다. (클래스명.멤버)
interface Calculator {
int plus(int i, int j);
//default로 선언하여 구현한 메서드는 자식에서 반드시 구현하지 않아도 된다!
//필요하면 오버라이딩도 가능하다
default int sub (int i, int j){
return i - j;
}
public static void print() {
System.out.println("인터페이스의 스테틱 메서드입니다");
}
}
class MyCal implements Calculator {
@Override
public int plus(int i, int j) {return i + j;}
}
public class Main {
public static void main(String[] args){
Calculator.print(); //인터페이스의 static 메서드 호출
}
}
+다중 상속 시 인터페이스의 디폴드 메서드와 부모 클래스의 메서드가 충돌할 수 있다. 이때 부모 클래스의 메서드가 상속되고, 디폴트 메서드는 무시된다. 디폴트 메서드를 사용하고 싶다면 같은 내용으로 오버라이딩한다.
인터페이스를 사용하는 이유
다른 이유들은 추상클래스와 동일하다. 다만 아래의 특징은 인터페이스만 갖는 장점이다.
- 다중 구현 가능 == 자유로운 상속 관계
클래스의 상속은 한 번밖에 못하지만, 인터페이스는 implements를 여러 개 추가하거나 빼거나 할 수 있어 제약으로부터 자유롭다.
따라서 다음과 같이 자식들끼리 의미있는 관계를 맺을 수 있다.
Soccer와 BassGuitar 클래스는 각자 다른 클래스를 상속하고 있지만, 동시에 IBehavior라는 인터페이스를 구현하고 있다. 이렇게 아무 관계도 없는 클래스들에게 인터페이스를 공통적으로 구현하도록 하여 관계를 맺어줄 수 있다.
인터페이스와 추상클래스의 차이
인터페이스와 추상클래스의 공통점과 차이점을 정리하자면 다음과 같다.
1. 공통점
- 추상 메서드를 1개 이상 가져야 한다.
- 인터페이스와 추상클래스를 구현/상속한 클래스는 추상 메서드를 반드시 구현해야 한다.
- new 생성자를 사용하여 인스턴스화할 수 없다.
- 인터페이스 / 추상클래스를 상속받아 구현한 구현체(자식 클래스)의 인스턴스를 사용해야 한다.
2. 문법의 차이점
추상 클래스 | 인터페이스 | |
사용 키워드 | abstract | interface |
사용 가능 변수 | 제한 없음 | static final(상수) |
사용 가능 메서드 | 제한 없음 | abstract method, default method, static method, private method |
상속 키워드 | extends | implements |
다중 상속 가능 여부 | 불가능 | 가능 |
사용 가능 접근 제어자 | 제한 없음(public, private, protected, default) | public |
3. 사용의 차이점
추상 클래스 | 인터페이스 | |
의미 | extends => 공통되는 부분은 묶고, 그 외의 부분을 하위 클래스로 확장시킴 |
implements => 정의된 메서드들의 집합. 서로 다른 클래스들이 같은 목적(메서드)를 다른 방식으로 구현함 |
기능 | 상속 받을 클래스들에서 중복되는 멤버가 많을 때 => 통합멤버가 필요할 때 (인터페이스는 불가) |
설계에서 다중 상속을 이용해야 할 때 |
상속 관계를 타고 올라갔을 때 같은 부모 클래스를 상속하며 부모 클래스가 가진 기능들을 구현해야 할 때 | 서로 관련성이 없는 클래스들을 부모 - 자식 관계가 아닌 다른 관계로 묶고 싶을 때 | |
클라이언트에서 자료형을 사용하기 전에 미리 논리적인 상속 구조를 만들어 두고 사용 | 클라이언트에서 그때 그때 필요에 따라 구현하며 사용 |
둘의 기능이 다르기 때문에, 상황에 따라 다르게 사용하거나, 두 개를 조합하여 사용한다. 이를 자주 사용하는 코드 중 정형화된 것이 디자인 패턴이다.
디자인패턴
디자인패턴 소개
디자인패턴은 소프트웨어를 개발하면서 발생하는 반복적인 문제들을 해결하기 위한 모범 사례(Best Practice)이다. 객체 지향의 4대 특성인 캡슐화, 상속, 추상화, 다형성과 SOLID 설계 원칙을 기반으로 구현하였다. 객체 지향의 특성 중 주로 상속, 인터페이스 등을 이용한다.
디자인패턴을 프로그램 설계에 적용한다면 코드의 재사용성, 가독성, 유지보수성, 확장성,... etc를 높일 수 있다.
디자인패턴 예시 - Strategy 패턴 (전략 패턴)
전략 패턴이란?
전략 패턴은 객체들이 할 수 있는 행위들 각각을 전략 클래스로 만들어 두고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 하는 패턴이다.
- 전략 알고리즘 객체들 : 알고리즘, 행위, 동작을 객체로 정의한 구현체
- 전략 인터페이스 : 전략 구현체의 공용 인터페이스
- 컨텍스트 : 전략 객체를 선택하고 인터페이스를 통해 전략을 사용하는 역할
- 클라이언트 : 런타임에 특정 전략 객체를 컨텍스트에 전달하여 해당 전략 알고리즘을 실행한 결과를 출력
전략 패턴 예시
// Strategy 인터페이스
public interface SortingStrategy {
void sort(int[] array);
}
// Concrete Strategy 1: 버블 정렬
public class BubbleSortStrategy implements SortingStrategy {
public void sort(int[] array) {
// 버블 정렬 알고리즘 구현
}
}
// Concrete Strategy 2: 병합 정렬
public class MergeSortStrategy implements SortingStrategy {
public void sort(int[] array) {
// 병합 정렬 알고리즘 구현
}
}
// Concrete Strategy 3: 퀵 정렬
public class QuickSortStrategy implements SortingStrategy {
public void sort(int[] array) {
// 퀵 정렬 알고리즘 구현
}
}
// Context 클래스
public class SortingContext {
private SortingStrategy strategy;
// Strategy 설정
public void setSortingStrategy(SortingStrategy strategy) {
this.strategy = strategy;
}
// 정렬 실행
public void sortArray(int[] array) {
strategy.sort(array);
}
}
SortingStrategy 인터페이스에서 정렬 메서드 sort를 정의하고, 이를 여러 전략 클래스들이 각자의 알고리즘에 맞춰 구현한다.
SortingContext 클래스는 SoringStrategy 변수를 가지고 있으며, setSortingStrategy 메서드를 통해 main에서 입력받은 전략으로 초기화하고, sortArray 메서드에서 해당 전략 클래스의 sort 메서드를 호출한다.
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int[] array = { 34, 7, 23, 32, 5, 62 };
Scanner scanner = new Scanner(System.in);
// 컨텍스트 객체 생성
SortingContext context = new SortingContext();
System.out.println("Select sorting strategy (1- Bubble Sort, 2- Merge Sort, 3- Quick Sort):");
while(scanner.hasNext()) {
int strategy = scanner.nextInt();
switch(strategy) {
case 1:
context.setSortingStrategy(new BubbleSortStrategy());
context.sortArray(array);
printArray(array, "Bubble Sort");
break;
case 2:
context.setSortingStrategy(new MergeSortStrategy());
context.sortArray(array);
printArray(array, "Merge Sort");
break;
case 3:
context.setSortingStrategy(new QuickSortStrategy());
context.sortArray(array);
printArray(array, "Quick Sort");
break;
default:
System.out.println("Invalid strategy. Exiting.");
scanner.close();
return;
}
System.out.println("Select another strategy (1- Bubble Sort, 2- Merge Sort, 3- Quick Sort):");
}
}
// 배열 출력 함수
public static void printArray(int[] array, String method) {
System.out.println(method + "ed Array: ");
for (int value : array) {
System.out.print(value + " ");
}
System.out.println("\n");
}
}
만약 새로운 정렬 알고리즘을 추가하고 싶다면, Strategy 인터페이스를 구현하는 것으로 쉽게 추가할 수 있다.
public class HeapSortStrategy implements Strategy {
public void sort(int [] array) {
//...
}
}
❓프론트엔드에서 디자인패턴이 필요한가?
- 디자인패턴은 소프트웨어 설계에서 문제를 해결하기 위한 방법, 아이디어
- 프론트엔드 개발에 디자인패턴을 적용한다면 코드의 재사용성을 높일 수 있고, 이해하기 쉬우며 유지보수가 쉬운 코드를 작성할 수 있다.
- 애플리케이션의 구성 요소와 페이지에서 일관성을 유지할 때도 사용할 수 있다.
출처
- Inpa Dev : https://inpa.tistory.com/
- Refactoring.Guru : https://refactoring.guru/
- 프론트엔드에서 디자인패턴이 필요할까? : https://jwookj.tistory.com/m/114
'스터디' 카테고리의 다른 글
CORS 에러 핸들링 : 개념 편 (6) | 2024.07.22 |
---|