※ 객체지향으로 개발해야 하는 이유

- 개발 과정에서 자주 발생하는 문제를 객체지향 프로그래밍이라는 방법론으로 해결하기 위해 사용

- 변수와 함수를 밀접하게 하나의 클래스 소속으로 만들었기 때문에 관리가 쉽고 테스트가 용이함

 

1. 클래스와 상속

1) 상속과 다형성

- 상속은 extends 키워드 사용

- 상속을 전제로 부모타입 변수에는 부모타입이나 자식타입의 변수를 모두 대입 가능

- 부모타입으로는 다형성적으로 대입된 객체가 자식타입이어도 부모측 자원만 호출 가능

public static void main(String[] args) {
    Parent parent = new Parent(); // 부모타입 변수에 부모타입 객체 대입
    Parent parentTypeChild = new Child(); // 부모타입 변수에 자식타입 객체 대입, 다형성
    Child child = new Child();

    parent.process();
    System.out.println("--------------");
    parentTypeChild.process(); 
    //parentTypeChild.childProcess(); // Child 타입객체 내부에 선언된 요소는 Parent로 호출 불가
    System.out.println("--------------");
    child.process();
    child.childProcess();
}

 

 

2) 메서드 오버라이딩 (재정의)

- 부모클래스에서 상속받은 메서드를 자식에서 재정의하는 것

- 메서드의 시그니처(리턴타입, 메서드명, 요구하는 파라미터)는 같아야 함

- 오버라이딩이 정의되었다면 부모 타입으로도 자식 측의 재정의 메서드 호출 가능

public static void main(String[] args) {
    Parent parent = new Parent();
    Parent parentTypeChild = new Child(); // 다형성

    parent.process();
    parentTypeChild.process(); // 오버라이딩이 전제된 메서드는 부모타입으로도 자식측 메서드 자동 호출
}

 

 

3) 메서드 오버로딩 (중복정의)

- 메서드명이 같아도 요구 파라미터의 개수나 타입이 다르면 허용

- 리턴자료만 다른 것은 허용 불가

// 메서드 오버로딩 요건
// 1. 메서드 이름을 중복하여 여러 개 선언
// 2. 단, 선언된 같은 이름의 메서드 간 요구 파라미터의 개수나 타입은 달라야 함
// 3. 리턴타입의 동일여부는 오버로딩에 영향을 주지 않음
public int add(int num1, int num2) {
    return num1 + num2;
}

// return 타입만 다르게 오버로딩 하는 것은 불가능
// public long add(int num1, int num2) {
//   	return num1 + num2;
//}

public double add(double num1, double num2) {
    return num1 + num2;
}

public long add(long num1, long num2) {
    return num1 + num2;
}

 

 

4) 상속 관련 시 주의사항

- 상속 시 메서드와 필드를 모두 재사용할 수는 있지만 기본적으로는 필드 재사용을 전제로 해야 함

- 메서드 재사용을 위해 상속을 쓴다면 전략 패턴 구성 (Composite) 활용

☆ 리스코프 치환 원칙 : 부모타입으로 할 수 있는 일은 자식타입으로도 할 수 있어야 함

 

 

2. 추상 클래스와 인터페이스

1) 추상클래스

- abstract 키워드 활용

- 일반적으로 인스턴스를 생성할 수 없음 (생성자에서 추상메서드에 대한 오버라이딩을 직접 해주면 가능)

- 일반적으로 하나 이상의 추상 메서드 포함, 해당 추상 메서드를 상속하며 오버라이딩해야만 인스턴스 생성

public abstract class AbstractClass {
	public void implementedMethod() {
		System.out.println("AbstractClass 내부에서 직접 구현된 메서드");
		this.abstractMethod(); // 추후 구현될 템플릿 메서드
	}
	
	abstract public void abstractMethod();
};
public class ExtendedClass extends AbstractClass {
	@Override
	public void abstractMethod() {
		System.out.println("ExtendedClass에서 정의된 추상 메서드");
	}
}
public class AbstractClassExMain {
	public static void main(String[] args) {
		// 추상클래스의 인스턴스를 직접 생성해주고 싶다면?
		AbstractClass abstractClass = new AbstractClass() {
			@Override
			public void abstractMethod() {
				// 생성자에서 직접 추상메서드를 구현해주면 상속 없이 생성 가능
				System.out.println("Abstract Class 내부에서 정의한 abstractMethod()");
			}
		};
		
		abstractClass.implementedMethod();
		abstractClass.abstractMethod();
		
		System.out.println("--------------");
		
		AbstractClass extendedClass = new ExtendedClass();
		extendedClass.implementedMethod();
		extendedClass.abstractMethod();
	}
}

 

 

2) 인터페이스

- 다중상속이 가능

- 디폴트 메서드를 이용하여 정의된 메서드와 정의되지 않은 메서드 호출 가능

public interface SomeInterface {
	void someMethod();
	
	default void defaultMethod() {
		// default 키워드를 메서드에 붙이면 인터페이스 내부에서도 구현된 메서드를 가질 수 있음
		this.someMethod();
	}
}
public interface AnotherInterface {
	void anotherMethod();
}
public class ImplementsClass implements SomeInterface, AnotherInterface { // 다중상속 가능
	@Override
	public void anotherMethod() {
		// 리스코프 치환 원칙  : 자식의 실행 커버리지는 부모의 실행 커버리지보다 넓어져서는 안 됨.
		System.out.println("ImplementsClass의 anotherMethod()");	
	}

	@Override
	public void someMethod() {
		System.out.println("ImplementsClass의 someMethod()");	
	}
}
public class InterfaceExMain {
	public static void main(String[] args) {
		SomeInterface someInterface = new ImplementsClass();
		AnotherInterface anotherInterface = new ImplementsClass();
		
		someInterface.someMethod();
		anotherInterface.anotherMethod();
		
		ImplementsClass implementClass = new ImplementsClass();
		
		// SomeInterface, anotherInterface로는 양쪽 모두를 호출할 수 없음
		//someInterface.anotherMethod();
		//anotherInterface.someMethod();
		
		// ImplementsClass 구현체 타입으로는 양쪽 모두 호출 가능
		implementClass.anotherMethod();
		implementClass.someMethod();
	}
}

 

 

※ 일반적인 상황에서는 인터페이스를 쓰면 정답인 경우가 많다.

 

※ 추상클래스를 인터페이스 대신 사용하는 경우

① 인스턴스 변수(필드)를 정의해야 하는 경우 : 인터페이스는 상수만 정의 가능

② 생성자가 필요한 경우 : 인터페이스는 내부에 생성자 정의 불가

③ Object 클래스의 메서드를 오버라이딩 하고 싶은 경우

 

 

3. ENUM(이늄)

열거상수 이늄을 이용하면 강력한 객체지향 코딩 기법을 배울 수 있다.

import java.util.function.BiFunction;

public enum CalculateType {
	// basic과는 다르게 연산 종류와 연산 메커니즘을 함께 정의
	// 관련된 자료들이 잘 모여있음(응집도가 높아졌음)
	ADD((num1, num2) -> num1 + num2),
	MINUS((num1, num2) -> num1 - num2),
	MULTIPLY((num1, num2) -> num1 * num2),
	DIVIDE((num1, num2) -> num1 / num2);

	// 위에 붙인 익명함수들은 Bifunction이라는 타입으로 멤버변수를 정의해야 사용 가능
	// 따라서, 생성자에서 BiFunction을 주입받도록 처리
	CalculateType(BiFunction<Integer, Integer, Integer> expression){
		this.expression = expression;
	}
	
	private BiFunction<Integer, Integer, Integer> expression;
	
	public int calculate(int num1, int num2) {
		return this.expression.apply(num1, num2);
	}
}

이늄 자료를 위와 같이 정의하면 CalculateType은 총 4개의 자료만 가질 수 있다.

이때 타입에 따른 연산까지 함께 이늄에 정의해둘 수 있고, BiFunction은 입력자료형과 리턴자료형을 정의할 수 있도록 해준다.

함수를 마치 하나의 객체처럼 다룰 수 있게 해준다.

public class CalculateCommand {
	private CalculateType calculateType; // ADD, MINUS, MULTIPLY, DIVIDE 중 하나만 대입 가능
	private int num1;
	private int num2;
	
	public CalculateCommand(CalculateType calculateType, int num1, int num2) {
		this.calculateType = calculateType;
		this.num1 = num1;
		this.num2 = num2;
	}
	
	public CalculateType getCalculateType() {
		return calculateType;
	}
	
	public int getNum1() {
		return num1;
	}
	
	public int getNum2() {
		return num2;
	}
}
public class Client {
	public int process(CalculateCommand calculateCommand) {
		CalculateType calculateType = calculateCommand.getCalculateType();
		int num1 = calculateCommand.getNum1();
		int num2 = calculateCommand.getNum2();
				
		// basic쪽과는 달리 client에 상세한 계산 로직이 포함되지 않음
		// 클라이언트는 어떤 연산을 수행할지만 알고, 해당 로직의 상세한 내용을 모름
		// 클라이언트는 해당 로직에 대한 책임이 없음
		// 수정이 필요할 때 클라이언트측 코드를 볼 필요가 없음
		int result = calculateType.calculate(num1, num2);
		return result;
	}
}
public class AdvExMain {

	public static void main(String[] args) {
		// 클라이언트가 요청할 때 calculateCommand 객체가 제공한 데이터를 사용
		CalculateCommand calculateCommand = new CalculateCommand(CalculateType.ADD,100,3);
		Client client = new Client();
		int result = client.process(calculateCommand);
		
		System.out.println(result);
	}
}

 

 

 

4. 예외

1) checked exception vs unchecked exception

- checked exception : Exception 객체를 상속한 예외, 컴파일 시 예외처리를 문법적으로 강제

- unchecked exception : RuntimeException 객체를 상속한 예외, 예외처리를 따로 강제하지 않음

 

2) 왜 checked exception을 쓰지 않는가?

- 대부분의 경우 로직만으로는 예외를 처리할 수 없는 경우가 많음

ex) 사용자에게 파일 이름을 입력받아 서버에서 리턴해주는 로직

파일 잘못 입력 시 FileNotFoundException이 발생하는데, 사용자에게 파일을 입력해달라는 말 말고 직접적으로 처리할 수 없는데 굳이 예외처리 구문을 사용할 필요가 없음

- checked exception의 경우 내부에서 어떤 동작을 하다가 예외를 발생시켰는지 알 수 있는 경우가 많음 → 캡슐화 원칙이 깨지는 문제

 

 

5. Object 클래스

모든 클래스의 부모이자 Object 클래스가 가진 메서드들을 오버라이딩하여 사용하는 경우가 많음

 

1) .equals()

- 동일성 : 두 대상이 "똑같은 대상"이어야 성립 → ==를 이용해 비교

- 동등성 : 대상은 다르지만 어떤 다른 기준에 의해 같음을 확인했을 때 성립 → equals() 활용

 

2) .hashCode()

- 주로 equals와 함께 사용

- map 자료 등에 대해 비교 시 주로 hashcode를 이용해 1차적인 필터링을 하고, 그 결과에 대해 equals로 조회

 

 

 

 

 

 

 

 

 

 

 

'네트워크캠퍼스 > JAVA' 카테고리의 다른 글

객체지향적 코드 작성  (0) 2024.01.29
프로세스와 쓰레드  (0) 2024.01.24
자바 API  (0) 2024.01.18
자바의 예외처리전략  (0) 2024.01.18
예외처리  (0) 2024.01.17

+ Recent posts