public class UncheckedException extends RuntimeException {
}
public void throwsUncheckedExceptionMethod() {
throw new UncheckedException();
}
public void tryCatchUncheckedExceptionMethod() {
throw new UncheckedException();
}
* 객체지향적으로 개발
public static void main(String[] args) {
MessageSender messageSender = new FakeMessageSender();
Client client = new Client(messageSender);
client.someMethod();
}
public interface MessageSender {
void send();
}
public class Client {
private MessageSender messageSender;
Client(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void someMethod() {
// 메시지 보내기 전 실행되는 어떤 작업
messageSender.send();
}
}
MessageSender는 Interface이다.
그렇기에 내가 필요한 Interface의 구현체를 구현해 생성자를 통해 넘겨줌으로써, 의존성 주입이 가능하다.
* Override
public class Parent {
public void someMethod() {
System.out.println("Parent someMethod");
}
}
public class Child extends Parent {
@Override
public void someMethod() {
System.out.println("Child someMethod");
}
}
override의 개념은 이미 아는 것이니 넘어가고..
만약 @Override 어노테이션을 지우면 어떻게 될까?
지워도 잘 작동한다.
그렇다면 왜 해당 어노테이션이 필요할까?
만약 이런 경우가 있다 가정해보자.
public class Parent {
public void someMethods() {
System.out.println("Parent someMethod");
}
}
public class Child extends Parent {
@Override
public void someMethod() {
System.out.println("Child someMethod");
}
}
Parent의 메소드는 'someMethods'이고 Child의 메소드는 'someMethod'이다.
즉, 현재 메소드의 이름이 다르다.
내가 Child의 someMethod를 만든 이유는 말 그대로 Parent의 someMethods라는 메소드를 오버라이딩 하기 위함이다.
그런데 이름이 다르니, 당연히 해당 someMethod가 Parent에 존재하지 않으니 에러가 발생한다.
즉, 이런 경우를 방지하기 위해 사전에 적어놓는다 이해하자.
* 추상 클래스와 인터페이스
- 추상 클래스
public abstract class AbstractClass {
public void implementedMethod() {
System.out.println("AbstractClass implementedMethod");
this.abstractMethod();
}
abstract public void abstractMethod();
}
public class AbstractClassExampleMain {
public static void main(String[] args) {
AbstractClass abstractClass = new AbstractClass() {
@Override
public void abstractMethod() {
// 추상 메서드 구현
System.out.println("AbstractClass abstractMethod");
}
};
abstractClass.implementedMethod();
abstractClass.abstractMethod();
}
}
- 인터페이스
public interface SomeInterface {
void someMethod();
default void defaultMethod() {
// 인터페이스에 메서드 정의 가능
this.someMethod(); // 정의되지 않은 메서드도 호출 가능
}
}
public class ImplementsClass implements SomeInterface{
@Override
public void someMethod(){
System.out.println("ImplementCLass someMethod");
}
}
public class InterfaceExampleMain {
public static void main(String[] args) {
SomeInterface someInterface = new ImplementsClass();
someInterface.defaultMethod();
}
}
추상 클래스와 인터페이스의 default Method를 보면 기능이 동일해보이는데, 차이가 뭘까?
이펙티브 자바20의 내용을 참고해보자.(추상 클래스보다는 인터페이스를 우선하라.)
자바 8부터는 인터페이스도 default 메서드가 제공이 되기 때문에 인터페이스와 추상 클래스 모두 인스턴스 메서드를 구현 형태로 제공할 수 있다.
둘의 가장 큰 차이는 추상 클래스가 정의한 타입을 구현한 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
그렇기에 새로운 타입을 정의할 때, 제약이 생긴다. 자바는 단일 상속만 지원하기 때문이다.
인터페이스는 여러 개의 인터페이스를 implements할 수 있기 때문에, 다른 어떤 클래스를 상속해도 같은 타입으로 취급한다.
- 목적이 다르다.
* Interface : 구현 객체가 같은 동작을 한다는 것을 보장하기 위해서 만든다.
* Abstract Class : 기능을 확장(extends)하는 것을 목표로 한다. 공통적인 기능을 묶어 상위 클래스화 하고, 구현이 필요한 것들을 추상화 한다. - 상태
* Abstract Class는 클래스이기 때문에 변수(상태)를 가질 수 있다.
Interface는 Instance 변수를 가질 수 없다. 그저 상수만을 가질 뿐이다. - 생성자
* Abstract Class는 클래스이기 때문에 생성자를 가진다.
Interface는 불가하다.
* Enum
public enum CalculateType {
ADD, MINUS, MULTIPLY, DIVIDE
}
public class Client {
public int someMethod(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int result = 0;
if(calculateType.equals(CalculateType.ADD)) {
result = num1 + num2;
} else if(calculateType.equals(CalculateType.MINUS)) {
result = num1 - num2;
} else if(calculateType.equals(CalculateType.MULTIPLY)) {
result = num1 * num2;
} else if(calculateType.equals(CalculateType.DIVIDE)) {
result = num1 / num2;
}
return result;
}
}
불편한 점들이 몇 가지 보인다.
1. 만약 CalculateType에 변수가 하나 추가된다면? if 문에 조건을 또 추가해줘야한다. 이건 좀 문제가 있다.
2. if문이 너무 가독성이 떨어진다.
그러면 어떻게 이를 수정할까?
조금 더 추상적으로 수정하는 것이다.
ADD, MINUS 등에 한 차원 더 높은 추상적인 건(공통적인 것) 당연히 enum인 CalculateType이니
단순히 CalculateType 내에서 다 해결할 수 있게 해보자.
public enum CalculateType {
ADD ((num1, num2) -> num1 + num2),
MINUS ((num1, num2) -> num1 - num2),
MULTIPLY ((num1, num2) -> num1 * num2),
DIVIDE ((num1, num2) -> num1 / num2);
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);
}
}
Bifunction은 인자 두개와 리턴 값 하나로 이루어져 있다.
지금 Enum에서 생성자를 통해 객체를 생성하면 바로 해당 expression을 가지게 수정했다.
그러면 실제로 우리가 사용할 때는 어떻게 사용하면 될까?
public class Client {
public int someMethod(CalculateCommand calculateCommand) {
CalculateType calculateType = calculateCommand.getCalculateType();
int result = calculateType.calculate(num1, num2);
return result;
}
}
이젠 if문을 더 추가할 필요 없이 그냥 calculate만을 활용하면 된다.
그렇다면 이를 통해 얻은 이점이 무엇이 있을까?
1. 우선 앞으로 Enum인 CalculateType이 확장되거나 변경되더라도, 난 그저 calculate만 호출하면 된다.
2. 캡슐화의 이점을 얻었다!
* 예외 처리
checked Exception과 unchecked Exception의 차이에 대해 배웠다.
흔히들 checked Exception은 컴파일 할 때 발생하고, Unchecked Exception은 런타임에 발생하는 거 아니냐 오해할 수 있다.
하지만 그렇지 않다.
예외는 런타임에 발생하는 것이기 때문이다.
컴파일에 발생하는 건 말 그대로 문법적 오류로 인해 발생하는 것이다. 즉 컴파일 에러이다.
차이는 이렇다.
Checked Exception은 컴파일할 때 예외에 대한 처리를 강제하고,
Unchecked Exception은 예외에 대한 처리를 강제하지 않는다.
Checked Exception은 컴파일 할 때 예외에 대한 처리를 강제해야 한다. 코드를 보자.
public class CheckedException extends Exception {
}
public void throwsCheckedExceptionMethod() throws CheckedException {
throw new CheckedException();
}
public void tryCatchCheckedExceptionMethod() {
try {
throw new CheckedException();
} catch (CheckedException e) {
// 예외에 대한 적절한 처리
e.printStackTrace();
}
}
Exception은 대표적인 checked Exception이다.
만약 throws 또는 try Catch를 해주지 않는다면 must be caught or declared to be thrown를 하라는 에러 메세지가 발생한다.
그렇다면 Unchecked Exception을 봐보자.
public class UncheckedException extends RuntimeException {
}
public void throwsUncheckedExceptionMethod() {
throw new UncheckedException();
}
public void tryCatchUncheckedExceptionMethod() {
throw new UncheckedException();
}
해당 내의 함수에서는 굳이 try catch나 throws를 써주지 않아도 문제가 발생하지 않는다.
또 둘의 가장 큰 차이는, Rollback 여부이다.
Checked Exception은 예외 발생이 되더라도, rollback이 되지 않고, 트랜잭션이 commit이 되는 것에 반해
Unchecked Exception은 예외가 발생하면 rollback이 진행된다.
그렇다면 왜 Checked Exception은 왜 Rollback이 되지 않을까?
기본적으로 Checked Exception은 복구가 가능하다는 메커니즘을 가지고 있다.
기본적으로 복구가 가능하니, 굳이 rollback을 하지 않아도 된다.
하지만 현실적으로, 우리가 checked Exception 예외가 발생할 경우, 이를 복구할 수 있는 경우가 많지 않다.
중요한 건, Exception을 발생시킬 때 명확하게 어떤 예외가 발생했는지를 전달해주는 것이 중요하다.
핵심은, checked Exception을 만나게 될 시, 더 구체적인 Unchecked Exception을 발생시켜 정보를 전달하고 로직의 흐름을
끊어야한다.
'TIL' 카테고리의 다른 글
2023.09.25 월 TIL (0) | 2023.09.25 |
---|---|
2023.9.21 목 TIL (1) | 2023.09.21 |