예외 기본 내용 복습 + 실무에 필요한 체크/언체크 예외 차이점과 활용 방안에 대한 내용이다.

 

예외 계층

체크 예외와 언체크 예외가 어떤 구조로 되어있는지는 다음과 같다.

 

  • Object : 예외도 객체이기에 모든 예외의 최상위 부모는 Object이다.
  • Throwable : 최상위 예외이다. 하위에 Exception과 Error가 있다.
  • Error : 언체크 예외
    • 애플리케이션에서 복구 불가능한 시스템 예외이다. 메모리 부족 or 심각한 시스템 오류가 해당된다. 이 에러는 개발자 잡으려 해서는 안된다.
    • 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡는다. 따라서 애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데 이유는 Error 예외도 함께 잡을 수 있기 때문이다.
    • 따라서 애플리케이션 로직은 Exception부터 예외로 생각하고 잡으면 된다.
  • Exception : 체크 예외
    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
    • Exception을 포함한 하위 예외는 모두 체크 예외이다. 체크 예외는 모두 컴파일러가 체크한다.
    • RuntimeException은 체크 예외가 아닌 언체크 예외이다.
  • RuntimeException : 언체크 예외, 런타임 예외
    • 언체크 예외로, 이는 컴파일러가 체크하지 않는 언체크 예외이다.
    • RuntimeException과 그 하위 예외가 해당된다.
    • 주로 런타임 예외 = 언체크 예외로 부른다.

 

예외 기본 규칙

예외는 폭탄 돌리기와 같다. 잡아서 처리하거나(예외 처리) 처리할 수 없으면 밖으로 던져야 한다(예외 던짐).

예외처리

예외처리는 예외를 잡아서 처리하는 것으로, 예외 처리 후에는 애플리케이션 로직이 정상 흐름으로 동작한다.

 

예외 던짐

예외를 처리하지 못하면 호출한 곳으로 예외를 계속 던진다. 불이 꺼지지 않고 계속 옮겨지는 것과 같다.

 

  • 만약 예외를 처리하지 못하고 계속 던지면 어떻게 될까?
    • 자바 main() 스레드의 경우, 예외 로그를 출력하면서 서버가 죽는다.
    • 웹 애플리케이션의 경우 여러 사용자 요청을 처리하기 때문에 하나의 예외 때문에 서버가 죽으면 안된다. WAS가 해당 예외를 받아서 처리하는데, 주로 개발자가 지정한 오류페이지를 사용자에게 보여준다.

 

체크 예외 vs. 언체크 예외

  • 체크 예외 : 예외를 잡아서 처리하지 않으면 항상 [throws 예외]를 선언해야 한다.
  • 언체크 예외 : 예외를 잡아서 처리하지 않아도 throws를 생략할 수 있다.

→ 예외를 처리할 수 없을 때 예외를 밖으로 던지는 부분이 차이다.

 

체크 예외

RuntimeException을 제외한 Exception과 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다.

체크 예외는 잡아서 처리하거나, 또는 밖으로 던지도록 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.

 

Exception을 상속받은 예외는 체크 예외가 된다.

static class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
         super(message);
    }
}
  • MyCheckedException은 Exception을 상속받았으므로 체크 예외가 되었다.
  • 참고로 RuntimeException도 Exception을 상속 받지만 언체크 예외이다. 이는 자바 언어에서 문법으로 정한 것이다.

 

체크 예외를 잡아서 처리하는 코드

repository에서 call()을 호출한 뒤 MyCheckedException이 throw 되면 잡는 로직이다.

try {
    repository.call();
} catch (MyCheckedException e) { 
    // 예외 처리 로직
}
  • catch는 해당 타입과 그 하위 타입을 모두 잡을 수 있다.
  • catch (Exception e) 로 설정해도 MyCheckedException을 잡을 수 있다. 다만 Exception은 예외 최상위 클래스이기 때문에 다른 하위 예외까지 모두 잡힐 수 있기 때문에 권장하는 방식은 아니다.

 

체크 예외를 밖으로 던지는 코드

체크 예외를 처리할 수 없을 때는 method() throws 예외 를 사용해 밖으로 던질 예외를 필수로 지정해주어야 한다. 만약 체크 예외를 throws로 체크하지 않으면 컴파일 에러가 발생한다.

public void callThrow() throws MyCheckedException {
    repository.call();
}

→ 체크 예외는 예외를 잡아서 처리하거나 throws를 지정해 밖으로 예외를 던진다는 선언을 필수로 해야한다.

 

  • 참고
public void callThrow() throws Exception {
    repository.call();
}
  • throws는 지정한 타입과 그 하위 타입 예외를 밖으로 던진다.
  • 예시로 throws Exception을 적어도 MyCheckedException을 던질 수 있다.

 

체크 예외 장단점

체크 예외는 예외를 잡아서 처리하거나 처리할 수 없는 경우 throws 예외를 통해 예외를 밖으로 던져야 한다. 그렇지 않으면 컴파일 에러가 발생하는데, 이로 인해 장단점이 존재한다.

  • 장점
    • 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 안전장치 역할을 한다.
  • 단점
    • 이때문에 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에 번거롭다.
    • 현재 단계에서 처리할 수 없거나 신경쓰고 싶지 않은 예외까지 모두 챙겨서 처리하든지 던지든지 해야한다.

 

언체크 예외

언체크 예외는 RuntimeException를 포함한 하위 예외이다. 즉, RuntimeExeption을 extends한 예외를 의미한다.

언체크 예외는 말 그대로 컴파일러가 예외를 체크하지 않는다는 뜻이다.

언체크 예외는 체크 예외가 기본적으로 동일하다. 차이가 있다면 예외를 던지는 throws를 선언하지 않고, 생략할 수 있다. 이 경우 자동으로 예외를 던진다.

 

 

언체크 예외 처리하기

언체크 예외 처리는 잡아도 되고, 잡지 않고 던져된다. 언체크 예외를 던질 때는 method() throws 예외 를 명시하지 않아도 된다.

  1. 언체크 예외 잡아서 처리하기
try {
    repository.call();
} catch (MyUncheckedException e) {
    // 예외 처리 로직
    log.info("error", e);
}

 

 

2. 언체크 예외를 밖으로 던지는 코드

// throws MyUncheckedException 생략
public void callThrow() {
    repository.call();
}

// thrwos MyUncheckedException 선언
public void callThrow() throws MyUncheckedException {
    repository.call();
}

언체크 예외도 throws를 선언해도 되지만, 생략할 수도 있다.

 

언체크 예외 장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있다. 이 때문에 장단점이 존재한다.

  • 장점
    • 신경 쓰고 싶지않은 언체크 예외를 무시할 수 있다. 그래서 신경 쓰고 싶지 않은 예외의 의존관계를 참조하지 않아도 된다.
  • 단점
    • 언체크 예외는 명시적 선언이 생략가능하기 때문에 개발자가 실수로 예외를 누락할 수 있다.

 

예외 활용

그럼 체크 예외와 언체크 예외는 언제 사용하면 좋을까?

다음 기본 원칙 2가지를 지키면 된다.

  1. 기본적으로 언체크 예외를 사용하자(요즘 트랜드는 런타임 예외만 주로 사용하는 것이다)
  2. 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외만 사용하자
    1. 해당 예외를 잡아서 반드시 처리해야 하는 문제일 때만 체크 예외를 사용해야 한다.
    2. 예시) 계좌 이체 실패 예외, 결제시 포인트 부족 예외, 로그인 실패

 

체크 예외 문제점

체크 예외는 컴파일러가 예외 누락을 체크해주기 때문에 더 안전하고 좋아보이는데 왜 체크 예외를 기본으로 사용하는 것이 문제가 될까?

SQLException과 ConnectionException은 체크 예외인데 이 예외는 시스템 예외라 서비스, 컨트롤러에서 처리할 수 없다. 하지만 체크 예외이기 때문에 무조건 throws를 선언해야 한다.

이처럼 해결도 할 수 없는 문제를 계속 위로 던지는 것은 개발자 입장에서도 번거롭고, 사용자에게 에러를 알리기에도 모호하다(주로 “서비스에 문제가 있습니다.”라고 하는데 이는 사용자가 이해하기 어렵다).

정리하면 2가지 문제가 발생한다.

  1. 복구 불가능한 예외
    1. 대부분의 서비스나 컨트롤러에서 해결할 수 없는 문제이다.
    2. 그래서 해당 예외가 발생하면 로그를 남긱, 개발자가 해당 오류를 빠르게 인지할 수 있게 일관성 있게 공통 처리 하는 것이 필요하다.
  2. 의존 관계 문제
    1. 복구 불가능한 예외를 명시했기에 서비스와 컨트롤러는 의존하게 된다
    2. 이러면 나중에 기술이 변경되었을 때 해당 예외를 변경해야할 가능성이 높다. 이는 OCP를 어기게 되는 문제가 발생하게 된다.
    3. 코드 변경할 때마다 서비스 코드를 변경해야 하는 문제이다.

 

언체크 예외 활용

  • Repository에서 SQLException을 RuntimeSQLException으로, ConnectionException을 RuntimeConnectException으로 변경했다.
  • 이렇게 런타임 에외를 던지면 서비스와 컨트롤러에서 해당 예외를 선언하지 않고 던질 수 있다.

 

런타임 예외 구현 기술 변경시 파급 효과

  • 따라서 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러, 서비스에서는 코드를 변경하지 않아도 된다.
  • 구현 기술이 변경되는 경우, 예외를 공통으로 처리하는 곳은 코드가 변경될 수 있다. 하지만 한 곳에서만 변경이 일어나기 때문에 변경 영향 범위가 최소화된다.

 

런타임 예외 문서화

런타임 예외는 문서화를 잘하거나 코드에 [throws 런타임 예외]를 남겨서 중요한 예외를 인지할 수 있게 해줘야 한다.

 

  • 문서화 예시
/**
  * Make an instance managed and persistent.
  * @param entity  entity instance
  * @throws EntityExistsException if the entity already exists.
  * @throws IllegalArgumentException if the instance is not an
  *         entity
  * @throws TransactionRequiredException if there is no transaction when
  *         invoked on a container-managed entity manager of that is of type
  *         <code>PersistenceContextType.TRANSACTION</code>
  */
public void persist(Object entity);

 

 

예외 포함과 스택 트레이스

예외 전환 시 주의해야 할 것이 있다. 예외 전환시 기존 예외를 포함하는 것이다. 그렇지 않으면 스택 트레이스를 확인할 때 심각한 문제가 발생한다.

  • 예외 전환 예시
public void call() {
     try {
         runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException(e); //기존 예외(e) 포함 
    }
}

위 코드를 보면 체크 예외를 전환할 때 기존 예외인 SQLException 인스턴스 e를 파라미터로 넘긴다. 이렇게 해야 관련 에러가 발생했을 때 예외 추적이 가능하고, 그 원인이 SQLException이라는 것을 알 수 있다.

 

예외를 내가 직접 정해야 될 때 혹은 예외를 전활할 때는 꼭! 기존 예외를 포함하자.

 

 

Reference

인프런 김영한 - '스프링 DB 1편 - 데이터 접근 핵심 원리'

 

'Dev Language > Java' 카테고리의 다른 글

[자바/JDBC] 스프링-DB 1편 1. JDBC의 이해  (0) 2024.01.30
[자바/기본] 12. 다형성과 설계  (0) 2024.01.17
[자바/기본] 11. 다형성2  (0) 2024.01.17
[자바/기본] 10. 다형성1  (0) 2024.01.17
[자바/기본] 9. 상속  (0) 2024.01.16

JDBC 이해

애플리케이션을 개발할 때 중요한 데이터는 데이터베이스에 보관한다.

JDBC 등장 이유

데이터 처리를 위해 애플리케이션과 DB 간의 동작 방식은 다음과 같다.

  1. 커넥션 연결
  2. SQL(쿼리) 전달
  3. 결과 응답

여기서 발생할 수 있는 문제가 2가지 있다

  1. DB에 따른 코드 변경
    1. 데이터베이스를 변경할 경우(MySQL → Oracle), 각 db 마다 커넥션 연결, 쿼리 전달, 결과 응답 방법이 달라 관련 코드를 매번 수정해야 한다.
  2. 연결방식 학습
    1. 개발자가 데이터베이스에 맞는 커넥션 연결, 쿼리 전달, 결과 응답 방법을 새로 학습 해야한다.

→ 이 문제를 해결하기 위해 JDBC라는 자바 표준이 등장했다.

 

JDBC 표준 인터페이스

JDBC는 Java DataBase Connectivity의 약자로, 자바에서 DB에 접속할 수 있도록 하는 자바 API다.

JDBC는 DB에서 자료를 쿼리하거나 업데이트하는 방법을 제공한다.

 

  • JDBC가 제공하는 기능
    • java.sql.Connection : 연결
    • java.sql.Statement : SQL을 담은 내용
    • java.sql.ResultSet : SQL 요청 응답

인터페이스만으로는 기능이 동작하지 않기에 인터페이스를 구현한 JDBC 드라이버(DB 벤더(회사)에서 자신의 DB에 맞도록 구현한 라이브러리)를 사용해야 DB 기능을 사용할 수 있다.

 

  • 문제 해결
    • DB 독립적
      • 이제 애플리케이션 로직은 JDBC 표준 인터페이스에만 의존하기 때문에 DB를 변경하고 싶으면 JDBC 구현 라이브러리만 변경하면 된다. 따라서 코드를 변경하지 않아도 된다.
    • 간편하고 표준화된 사용법
      • 개발자는 이제 JDBC 표준 인터페이스 사용법만 학습하면 된다. 학습에 필요한 시간이 단축되었다
  • 주의
    • 인터페이스로 어느정도 표준화를 했지만, 페이징 SQL은 각 DB마다 사용법이 달라 이 부분은 맞춤으로 설정해야 한다

 

JDBC와 최신 데이터 접근 기술

JDBC는 출시된지 오래되었고(1997년), 사용법도 복잡하다. 그래서 SQL Mapper, ORM 기술 같이 JDBC를 편리하게 사용하는 다양한 기술이 존재한다.

 

  • SQL Mapper

  • 대표 기술 : 스프링 JdbcTemplate, MyBatis
  • 장점 - JDBC를 편리하게 사용하도록 도와줌
    • SQL 응답 결과를 객체로 편리하게 변환해줌
    • JDBC의 반복 코드를 제거해줌
    • SQL만 작성할 줄 알면 금방 사용 가능
  • 단점
    • 개발자가 SQL을 직접 작성해야 함

 

  • ORM

  • 대표 기술 : JPA, 하이버네이트(레퍼런스 많고, 디테일한 기능 많이 제공), 이클립스링크
  • 객체를 관계형 데이터베이스 테이블과 매핑해주는 기술
  • 장점
    • ORM이 SQL을 동적으로 생성하기에 반복적인 SQL 직접 작성하지 않아도 됨
    • 개발 생산성 향상
  • 단점
    • 기술이 복잡해 깊이있게 공부한 후 사용해야 함

 

중요

SQL Mapper, JPA 기술이 JDBC 사용을 편리하게 해주지만, 결국 기반은 모두 JDBC를 사용한다. 따라서 JDBC를 직접 사용하지 않아도 JDBC가 어떻게 동작하는지 기본 원리를 알아두어야 한다. JDBC는 자바 개발자라면 꼭 알아두어야 하는 필수 기본 기술이다.

 

DriverManager

JDBC가 제공하는 DriverManager는 다음과 같은 기능을 제공한다.

  • 라이브러리에 등록된 DB 드라이버들 관리
  • 커넥션 획득

  1. 애플리케이션 로직에서 커넥션이 필요할 시 DriverManager.getConnection() 호출
  2. DriverManager가 라이브러리에 등록된 드라이버 목록을 자동으로 인식
  3. 정보를 넘겨 커넥션이 획득 가능한지 확인
    1. URL(jdbc:[db 이름]:~), USERNAME, PASSWORD
  4. 이렇게 찾은 커넥션 구현체가 클라이언트에 반환

 

JDBC 개발 - 생성

JDBC를 이용해 DB 작업을 하려면 DriverManager로 DB와 연결 후 SQL을 보낸 뒤 결과를 받고 종료하는 순서로 로직을 작성하면 된다.

 

Member 테이블에 회원을 등록하는 예시인데, 구체적인 코드는 다음과 같다.

public Member save(Member member) throws SQLException {
	String sql = insert into member(member_id, money) values(?, ?); // " 인식 문제로 " 뺐음
	Connection con = null;
	PreparedStatement pstmt = null;
	try {
		con = getConnection();
		pstmt = con.prepareStatement(sql);
		pstmt.setString(1, member.getMemberId());
		pstmt.setInt(2, member.getMoney());
		pstmt.executeUpdate();

		return member;
	} catch (SQLException e) {
		log.error("db error", e);
		throw e;
	} finally {
		close(con, pstmt, null);
	} 
}

private void close(Connection con, Statement stmt, ResultSet rs) {
	// 실행한 역순서(ResultSet -> PreparedStatement -> Connection)로 객체 종료
	// ResultSest null 체크 후 종료
	// Statement null 체크 후 종료
	// Connection null 체크 후 종료	
}

private Connection getConnection() {
	return DBConnectionUtil.getConnection();
}

 

 

JDBC 개발 - 조회

이전에 저장한 데이터를 조회해보자. 대부분의 로직이 생성 로직과 비슷하지만, 결과를 반환하는 ResultSet과 Cursor 내용이 다르다.

public Member findById(String memberId) throws SQLException {
     String sql = select * from member where member_id = ?; // " 인식 문제로 " 뺐음

     Connection con = null;
     PreparedStatement pstmt = null;
     ResultSet rs = null;

     try {
         con = getConnection();
         pstmt = con.prepareStatement(sql);
         pstmt.setString(1, memberId);
			   rs = pstmt.executeQuery();

          if (rs.next()) { 
             Member member = new Member();
             member.setMemberId(rs.getString("member_id"));
             member.setMoney(rs.getInt("money"));
             return member;
         } else {
             throw new NoSuchElementException("member not found memberId=" + memberId); 
		}
     } catch (SQLException e) {
         log.error("db error", e);
         throw e;
     } finally {
         close(con, pstmt, rs);
	} 
}

 

  • rs.next()
    • ResultSet 내부에 있는 커서를 이동해 다음 데이터를 조회할 수 있다
    • 위 함수를 이용하면 커서가 다음으로 이동한다.
    • 최초의 커서는 데이터를 가리키고 있지 않기 때문에 rs.next()를 최초 한번은 호출해야 데이터 조회가 가능하다
    • rs.next() == true : 커서 이동 결과 데이터가 존재한다
    • rs.next() == false : 커서 이동 결과 데이터가 존재하지 않는다(더 조회할 데이터가 없다)

 

 

Reference

인프런 김영한 - '스프링 DB 1편 - 데이터 접근 핵심 원리'

역할과 구현을 분리

역할과 구현으로 구분하면 세상이 단순해지고, 유연해지며 변경이 편리해진다.

 

장점

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
  • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
  • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.

 

정리

  • 유연하고, 변경에 용이하다
  • 확장 가능한 설계 가능하다
  • 클라이언트에 영향을 주지 않는 변경이 가능하다
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요하다(인터페이스를 바꾸면 변경 범위가 넓어지므로, 예시로 usb 인터페이스 변경하면 모든 usb 바꿔야 한다)

 

자바의 다형성 활용

  • 역할 = 인터페이스
  • 구현 = 인터페이스를 구현한 클래스, 구현 객체
  • 객체를 설계할 때 역할과 구현을 명확히 분리

 

다형성의 본질

클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.
  • 다형성의 본질을 이해하려면 협력이라는 객체 사이의 관계에서 시작해야함

 

정리

  • 다형성이 가장 중요하다!
  • 디자인 패턴 대부분은 다형성을 활용하는 것
  • 스프링의 핵심인 IoC, DI도 결국 다형성을 활용하는 것
  • 스프링을 사용하면 마치 레고 블럭 조립하듯이! 공연 무대의 배우를 선택하듯이! 구현을 편리하게 변경할 수 있다

 

OCP(Open-Closed Principle) 원칙

OCP 원칙은 Open for extension, Closed for modification을 의미하는 원칙이다.

  • Open for extension : 새로운 기능의 추가나 변경 사항이 생겼을 때 기존 코드는 확장할 수 있어야 한다
  • Closed for modification : 기존의 코드는 수정되지 않아야 한다

 

즉, 확장에는 열려있고, 변경에는 닫혀있어야 한다!

 

 

 

  • 확장에 열려있다
    • 확장에 열려있다는 의미는 위 ERD에서 NewCar를 추가만 하면 새로운 자동차 종류를 추가할 수 있는 것을 의미한다.
  • 코드 수정에 닫혀있다
    • 새로 추가하는 데 필요한 코드 수정은 NewCar에서만 발생하고, Car를 사용하는 Driver의 코드는 하나도 변경되지 않는다. 코드 확장시에도 클라이언트의 코드가 변경되지 않는 것을 코드 수정에 닫혀있다고 한다.

 

전략 패턴(Strategy Pattern)

전략 패턴은 클라이언트 코드의 변경없이 코드를 쉽게 변경할 수 있는 패턴이다. 인터페이스가 전략을 정의하고, 각 구현체가 전략의 구체적인 구현이 된다.

 

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

다형성 적용하기

다형성을 이용하면 다양한 타입을 하나의 타입으로 묶어서 코드를 더 간결하게 만들 수 있다.

 

  • 다형성 적용 전의 코드
public static void main(String[] args) {
    Dog dog = new Dog();
    Cat cat = new Cat();
    Cow cow = new Cow();

    System.out.println("동물 소리 테스트 시작"); dog.sound();
    System.out.println("동물 소리 테스트 종료");
    System.out.println("동물 소리 테스트 시작"); cat.sound();
    System.out.println("동물 소리 테스트 종료");
    System.out.println("동물 소리 테스트 시작"); cow.sound();
    System.out.println("동물 소리 테스트 종료");
}

 

  • 다형성 적용 후 코드
public static void main(String[] args) {
    Dog dog = new Dog();
    Cat cat = new Cat();
    Cow cow = new Cow();
    
    soundAnimal(dog);
    soundAnimal(cat);
    soundAnimal()cow;
}

//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
    System.out.println("동물 소리 테스트 시작"); animal.sound();
    System.out.println("동물 소리 테스트 종료");
}

Dog, Cat, Cow를 Animal의 자식 클래스로 만듦으로써 모두 Animal 타입으로 업캐스팅이 가능해졌고, soundAnimal(Animal animal)로 간단하게 표현할 수 있게 되었다.

 

 

  • 리팩토링
public static void main(String[] args) {
    Animal[] animalArr = {new Dog(), new Cat(), new Caw()};
    for (Animal animal : animalArr) {
    soundAnimal(animal);
    }
}

//동물이 추가 되어도 변하지 않는 코드
private static void soundAnimal(Animal animal) {
    System.out.println("동물 소리 테스트 시작"); animal.sound();
    System.out.println("동물 소리 테스트 종료");
}

 

새로운 기능이 추가되었을 때 변하는 부분을 최소화하는 것이 잘 작성된 코드이다. 그러기 위해선 코드에서 변하는 부분과 변하지 않는 부분을 명확하게 구분하자.

 

남은 문제

다형성을 적용해 위 코드를 간결하게 만들었지만 두 가지 문제가 남아있다.

  1. Animal 클래스 인스턴스를 생성할 수 있는 문제
  2. Animal 클래스를 상속 받는 곳에서 sound() 메서드 오버라이딩을 하지 않을 가능성

 

1. Animal 클래스 인스턴스 생성할 수 있는 문제

개, 고양이, 소가 실제 존재하는 것은 당연하지만, 동물이라는 추상적인 개념이 실제 존재하는 것은 이상하게 느껴진다. 실수로 new Animal()을 사용해 Animal 인스턴스를 만들면 제대로 수행하지 않는 인스턴스가 생성될 수 있다.

 

2. Animal의 하위 클래스에서 메서드 오버라이딩을 하지 않을 가능성

Animal을 상속한 하위 클래스에서 깜빡하고 메서드를 오버라이딩하지 않는다면 의도한 결과가 나오지 않을 수 있다. 예시로 pig.sound()를 호출하고 “꿀꿀”을 의도했는데, pig에 sound()가 없어서 Animal의 sound()가 호출이 될 수 있다.

이 두가지 문제를 해결하기 위해서는 추상 클래스와 추상 메서드를 사용하면된다.

 

추상 클래스

추상 클래스는 부모 클래스는 제공하지만, 실제 생성되면 안되는 클래스를 의미한다. 추상 클래스는 상속을 목적으로 부모 클래스 역할을 하는 인스턴스를 생성할 수 없는 클래스이다.

추상 메서드가 하나라도 있는 클래스는 추상 클래스로 선언해야 한다.

 

특징

abstract class AbstractAnimal {...}
  • class 앞에 abstract
  • 인스턴스 생성 불가능
  • 하나 이상의 추상 메서드 가지고 있음

 

추상 메서드

추상 메서드는 부모 클래스를 상속 받는 자식 클래스가 반드시 오버라이딩 해야하는 메서드이다.

  • 특징
    • abstract 키워드가 붙여져 있다
    • 메서드 바디가 없다
    • 상속받는 자식 클래스가 무조건 오버라이딩해서 사용해야 한다
    • 추상 클래스가 추상 클래스를 상속받으면 추상 메서드 오버라이딩 안 해도 됨

 

정리

  • 추상 클래스로 Animal 인스턴스 생성 문제를 근본적으로 방지해준다
  • 추상 메서드로 부모 메서드를 오버라이딩 하지 못할 문제를 방지해준다

 

 

순수 추상 클래스

순수 추상 클래스는 추상 메서드로만 이루어진 추상 클래스를 의미한다.

  • 특징
    • 인스턴스 생성 불가
    • 상속 시 자식은 모든 메서드를 오버라이딩 해야한다
    • 주로 다형성을 위해 사용된다

순수 추상 클래스를 상속하면 모든 추상 메서드를 오버라이딩해야하기 때문에 어떤 규격을 지켜서 구현해야 하는것처럼 느껴진다. 이런 순수 추상클래스를 자바가 더 편리하게 사용할 수 있도록 인터페이스라는 개념을 제공한다.

 

 

인터페이스

인터페이스는 앞서 설명한 순수 추상 클래스와 동일하지만 약간의 편의 기능이 추가 되었다.

  • 특징
    • 모든 인터페이스 메서드는 public, abstract 이다
    • public abstract는 생략이 권장된다
    • 다중 구현(다중 상속)을 지원한다
    • public static final로 상수 선언한다
  • 인터페이스를 사용해야 하는 이유
    • 제약 : 인터페이스를 만드는 이유는 인터페이스를 구현하는 곳에서 인터페이승의 메서드를 반드시 구현하라는 규약(제약)을 주는 것이다. 순수 추상 클래스만 사용한다면 구현 함수를 추가할 수 있는 문제가 발생할 수 있는데, 인터페이스를 사용하면 이 문제를 사전에 원천 차단할 수 있다.
    • 다중 구현 : 상속은 다중 상속이 불가능한 반면에 인터페이스에는 추상 메서드로만 구성되어 있기 때문에 다이아몬드 문제가 발생하지 않아 다중 구현이 가능하다.

 

 

클래스, 추상 클래스, 인터페이스는 모두 똑같다

클래스, 추상 클래스, 인터페이스는 프로그램 코드, 메모리 구조상 모두 동일하다. 모두 .java로 정의하고, .class로 다뤄진다.

 

 

Reference

인프런 '김영한의 실전 자바 - 기본편'

'Dev Language > Java' 카테고리의 다른 글

[자바/JDBC] 스프링-DB 1편 1. JDBC의 이해  (0) 2024.01.30
[자바/기본] 12. 다형성과 설계  (0) 2024.01.17
[자바/기본] 10. 다형성1  (0) 2024.01.17
[자바/기본] 9. 상속  (0) 2024.01.16
[자바/기본] 8. final  (0) 2024.01.16

+ Recent posts