[JAVA] 어노테이션과 상속, 호출, 재정의, 추상 클래스

dejeong·2024년 9월 10일
0

JAVA

목록 보기
10/24
post-thumbnail

어노테이션

어노테이션은 메타데이터로서 컴파일 및 실행 과정에서 코드가 어떻게 처리될지를 컴파일러나 런타임에 알려주는 역할을 한다. 주로 @AnnotationName 형식으로 작성된다.

어노테이션의 역할

  • 컴파일러에게 문법 오류를 체크할 수 있는 정보를 제공
  • 빌드 도구가 코드 자동 생성 시 필요한 정보를 제공
  • 런타임 시 특정 기능을 동작하도록 하는 정보 제공

어노테이션 타입 정의와 적용

어노테이션을 직접 정의할 때는 다음과 같은 형식을 사용한다:

@AnnotationName
public @interface Annotation {
    타입 elementName() [default];
}
  • 타입: 기본형(int, double 등), String, Enum, Class, 배열 타입
  • 엘리먼트 이름: 메서드처럼 ()를 붙여 작성
  • 디폴트 값: default 키워드를 사용하여 기본값을 설정할 수 있음

어노테이션 적용 대상을 지정하는 @Target

어노테이션이 적용될 대상을 지정할 때 @Target 어노테이션을 사용한다. @Targetvalue 요소는 ElementType 배열로, 여러 대상에 적용 가능하다.

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface AnnotationName {
}

ElementType 열거 상수와 적용 대상

  • TYPE: 클래스, 인터페이스, 열거형
  • ANNOTATION_TYPE: 어노테이션
  • FIELD: 필드
  • CONSTRUCTOR: 생성자
  • METHOD: 메서드
  • LOCAL_VARIABLE: 로컬 변수
  • PACKAGE: 패키지

실무에서의 활용

실제 프로젝트에서는 스프링이나 롬복과 같은 라이브러리에서 제공하는 어노테이션을 주로 활용하며, 커스텀 어노테이션을 작성하는 경우는 상대적으로 적다.

요약

어노테이션은 메타데이터를 통해 코드의 컴파일 및 실행 과정을 제어하며, 정의된 타입과 적용 대상을 바탕으로 다양한 곳에 적용 가능하다.

  • 어노테이션 실습
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Target;
    
    @Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
    // 어노테이션 형태로 사용할 것이라 @를 붙혀준다.
    public @interface AnnotationName {
        String elementNameOne(); // 구현부가 없는 메서드로 정의
        int elementNameTwo() default 5; // 아무런 값도 설정하지 않았을 때는 5라는 값이 들어가도록 default 값 설정
    }
    @AnnotationName(elementNameOne = "값", elementNameTwo = 40)
    public class ClassName {
        
        @AnnotationName(elementNameOne = "값")
        int field;
    
        int getField(){
            return field;
        }
    
        void setField(int field){
            this.field = field;
        }
    
        @AnnotationName(elementNameOne = "값") // AnnotationName.java의 target에서 메서드 부분을 지우게 된다면 에러 발생
        void method(){
    
        }
    }

  • this: 객체 자기 자신을 가리키는 키워드
  • super: 부모 클래스를 호출하는 키워드

상속 (Inheritance)

상속 개념

상속은 일반 클래스, 추상 클래스, 인터페이스에서 모두 적용 가능한 개념으로, 부모 클래스의 멤버(필드, 메서드)를 자식 클래스가 물려받아 사용할 수 있게 한다. 상속을 통해 코드 재사용성과 기능 확장이 용이해진다.

public class 자식클래스명 extends 부모클래스명 {
    // 자식 클래스의 내용
}

상속의 장점

  1. 코드 재사용: 부모 클래스의 멤버를 자식 클래스에서 재사용할 수 있어, 중복 코드 작성을 줄인다.
  2. 기능 확장: 자식 클래스는 부모 클래스의 기능을 확장하거나, 재정의(오버라이딩)하여 사용할 수 있다.
  3. 유지보수 용이: 부모 클래스의 수정이 자식 클래스에 자동으로 반영되므로, 수정 작업이 간편해진다.

상속 예시

부모 클래스 InheritA에서 정의된 필드와 메서드를 자식 클래스 InheritB에서 그대로 사용하거나, 추가 필드 및 메서드를 확장할 수 있다.

// 부모 클래스
public class InheritA {
    protected int field1; // 자식 클래스에서 접근 가능

    protected void method1() {
        System.out.println("InheritA.method1 field1: " + field1);
    }
}
// 자식 클래스
public class InheritB extends InheritA {
    int field2;

    void method2() {
        System.out.println("InheritB.method2 field2: " + field2);
    }
}
// 상속 사용 예시
public class InheritanceExample {
    public static void main(String[] args) {
        InheritB inheritB = new InheritB();
        inheritB.field1 = 10;    // InheritA로부터 상속받은 필드
        inheritB.method1();      // InheritA로부터 상속받은 메서드

        inheritB.field2 = 30;    // InheritB에서 추가한 필드
        inheritB.method2();      // InheritB에서 추가한 메서드
    }
}

상속 시 주의사항

  • private 멤버: 부모 클래스의 private 필드 및 메서드는 상속되지 않는다.
  • default 접근 제한: 부모 클래스와 자식 클래스가 다른 패키지에 있을 경우, default 접근 제한자는 상속되지 않는다.
  • protected 접근 제어자: protected는 자식 클래스에서 접근 가능하도록 상속된다.
  • 자바에서는 다중 상속이 불가능하며, 하나의 부모 클래스만 상속할 수 있다. 단, 인터페이스는 다중 구현이 가능하다.

상속 활용

부모 클래스에서 공통적으로 사용하는 메서드와 필드를 정의하고, 자식 클래스에서 이를 확장하여 새로운 기능을 추가하거나 재정의할 수 있다.

public class Animal {
    String name;

    void setName(String name) {
        this.name = name;
    }

    void sleep() {
        System.out.println(this.name + " Zzz...");
    }
}

부모 생성자 호출

객체 생성 순서

자식 객체를 생성할 때, 부모 클래스의 생성자가 먼저 호출된다. 이는 자식 클래스 생성자에서 super() 키워드를 사용하여 부모 생성자를 호출하기 때문이다. super()는 명시적으로 작성되지 않더라도, 부모의 기본 생성자가 자동으로 호출된다.

public class Dog extends Animal {
    Dog(String name) {
        // super(); // 부모 클래스의 기본 생성자 호출 (생략 가능)
        System.out.println("Dog 객체 생성: " + name);
    }
}
public class DogExample {
    public static void main(String[] args) {
        Dog dog = new Dog("뽀삐"); // 생성자 호출
    }
}
Dog 객체 생성: 뽀삐

매개변수 있는 생성자와 super()의 역할

부모 클래스에 매개변수가 있는 생성자를 만들면, 자식 클래스에서 super(매개값)으로 부모의 해당 생성자를 명시적으로 호출해야 한다.

public class Person {
    String name;
    String ssn;

    Person(String name, String ssn) {
        this.name = name;
        this.ssn = ssn;
    }
}

자식 클래스는 부모의 매개변수 생성자를 호출해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.

public class Student extends Person {
    Student(String name, String ssn) {
        super(name, ssn); // 부모 생성자를 호출해야 오류가 발생하지 않음
    }
}

super 키워드의 위치

super()는 자식 클래스 생성자 내에서 첫 번째 줄에 위치해야 한다. 부모 클래스의 생성자가 먼저 호출된 후에 자식 클래스의 초기화가 이루어져야 하기 때문이다.

public class Student extends Person {
    int studentNo;

    Student(String name, String ssn, int studentNo) {
        super(name, ssn); // 부모 생성자 호출
        this.studentNo = studentNo;
    }
}

요약

  • 부모 생성자 호출: 자식 클래스의 생성자가 호출될 때, 부모 클래스의 생성자가 먼저 호출된다.

  • super(): 부모 클래스의 기본 생성자를 호출하며, 매개변수가 있을 경우 명시적으로 호출해야 한다.

  • super 위치: 자식 클래스 생성자 내에서 반드시 첫 번째 줄에 작성해야 한다.

  • Employee 클래스 상속 실습

    • 작성한 코드
      package chap07.employee;
      
      public class Employee {
          String name;
      
          double calculateSalary(String name){
              return 0;
          }
      
          public static void main(String[] args) {
              FullTimeEmployee fullTimeEmployee = new FullTimeEmployee();
              PartTimeEmployee partTimeEmployee = new PartTimeEmployee();
      
              System.out.println(fullTimeEmployee.calculateSalary("Alice",20));
              System.out.println(partTimeEmployee.calculateSalary("Bob",80, 5));
      
          }
      }
      package chap07.employee;
      
      public class FullTimeEmployee extends Employee {
          double salary;
      
          double calculateSalary(String name ,double salary){
              System.out.println(name + "'s Salary :" + salary);
              return salary;
          }
      }
      
      package chap07.employee;
      
      public class PartTimeEmployee extends Employee {
          double hourlyRate;
      	  int hoursWorked;
      
          double calculateSalary(String name, double hourlyRate, int hoursWorked){
              System.out.println(name + "'s Salary :" + hourlyRate * hoursWorked);
              return hourlyRate * hoursWorked;
          }
      }
    • 예제 코드
      package chap07.employee;
      
      public class Employee {
          String name;
      
          public Employee(String name) {
              this.name = name;
          }
      
          // get 메서드 사용 시
          public String getName() {
              return name;
          }
      
          double calculateSalary() {
              return 0;
          }
      }
      
      package chap07.employee;
      
      public class FullTimeEmployee extends Employee {
          double salary;
      
          FullTimeEmployee(String name, double salary){
              super(name); // 생성자 호출, 부모에서 받는 생성자 호출될 수 있게
              this.salary = salary;
          }
      
          // 메서드 재정의 Override
          double calculateSalary(){
              return salary;
          }
      }
      
      package chap07.employee;
      
      public class PartTimeEmployee extends Employee {
          double hourlyRate;
          int hoursWorked;
      
          PartTimeEmployee(String name, double hourlyRate, int hoursWorked){
              super(name); // -> this.name = name; 처럼 직접 접근해서 할당해줄 수 있다.
              this.hourlyRate = hourlyRate;
              this.hoursWorked = hoursWorked;
          }
          
          // 메서드 재정의(Override)
          @Override // 컴파일러한테 알려주는 역할
          double calculateSalary(){
              return hourlyRate * hoursWorked;
          }
      }
      
      package chap07.employee;
      
      public class EmployeeExample {
          public static void main(String[] args) {
              // 객체 생성
              FullTimeEmployee alice = new FullTimeEmployee("Alice",4000);
              PartTimeEmployee bob = new PartTimeEmployee("Bob", 1000, 4);
      
              // 출력(객체 내부 요소들 호출)
              System.out.println(alice.name + "'s Salary : " + alice.calculateSalary());
              System.out.println(bob.name + "'s Salary : " + bob.calculateSalary());
      
              // get 메서드 사용 시
              System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
              System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
          }
      }

메서드 재정의 (Method Overriding)와 메서드 오버로딩 (Method Overloading)

메서드 오버라이딩 (@Override)

오버라이딩은 부모 클래스에 있는 메서드를 동일한 시그니처로 재정의하는 것이다. 즉, 메서드의 리턴 타입, 이름, 매개변수가 부모 클래스와 동일해야 한다.

public class Parent {
    void method1() {
        System.out.println("Parent method 1");
    }

    void method2() {
        System.out.println("Parent method 2");
    }
}

public class Child extends Parent {
    @Override
    void method2() { // 부모 메서드를 재정의
        System.out.println("Child method 2");
    }

    void method3() {
        System.out.println("Child method 3");
    }
}
public class ChildExample {
    public static void main(String[] args) {
        Child child = new Child();

        child.method1();  // Parent method 호출
        child.method2();  // 재정의된 Child method 호출
        child.method3();  // Child에서 추가된 method 호출
    }
}

출력:

Parent method 1
Child method 2
Child method 3
  • 오버라이딩 규칙
    1. 부모의 메서드와 동일한 시그니처를 가져야 한다.
    2. 접근 제한자는 더 강하게 할 수 없다 (publicprivate 불가).
    3. 부모 메서드에서 선언한 예외 외에 추가적인 예외는 던질 수 없다.
  • @Override 어노테이션: 메서드를 오버라이딩할 때 사용되며, 오버라이딩이 제대로 되었는지 컴파일러에게 확인시킨다. 실수를 방지하며, 메서드 시그니처가 다를 경우 컴파일 오류가 발생한다.

메서드 오버로딩

오버로딩은 같은 이름의 메서드를 다른 매개변수 리스트로 여러 번 정의하는 것이다. 오버로딩은 메서드의 매개변수 타입이나 개수가 달라야 한다.

public class Calculator {
    // 매개변수 개수가 다른 오버로딩
    int add(int a, int b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }

    // 매개변수 타입이 다른 오버로딩
    double add(double a, double b) {
        return a + b;
    }
}
public class OverloadingExample {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(5, 3));         // int 매개변수
        System.out.println(calc.add(1, 2, 3));      // 3개 int 매개변수
        System.out.println(calc.add(1.5, 2.5));     // double 매개변수
    }
}

출력:

8
6
4.0

오버로딩은 같은 이름의 메서드를 다양한 방식으로 호출할 수 있어 유연성을 제공한다.


추상 클래스 (Abstract Class)

추상 클래스추상 메서드를 하나 이상 포함하며, 이 자체로는 객체를 생성할 수 없다. 이를 상속받은 구체적인 클래스에서만 객체를 생성할 수 있다.

abstract class Animal {
    abstract void sound(); // 추상 메서드, 자식 클래스에서 반드시 재정의
}

추상 클래스 예시

public abstract class Animal {
    String kind;

    public void breathe() {
        System.out.println("숨을 쉽니다.");
    }

    public abstract void sound();   // 추상 메서드
}
public class Dog extends Animal {
    public Dog() {
        this.kind = "포유류";
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

public class Cat extends Animal {
    public Cat() {
        this.kind = "포유류";
    }

    @Override
    public void sound() {
        System.out.println("야옹");
    }
}
public class AnimalExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.sound();  // 멍멍
        cat.sound();  // 야옹
    }
}

출력:

멍멍
야옹

추상 클래스의 용도

  • 공통 메서드 및 필드 정의: 실체 클래스에서 공통으로 사용하는 메서드와 필드를 정의하여 일관성을 유지한다.
  • 추상 메서드를 통한 동작 정의: 자식 클래스에서 각기 다른 구현체를 제공할 수 있다.

추상 클래스는 상속받는 클래스들에 공통적인 동작을 강제함으로써 설계 규격을 통일하고 유지보수를 쉽게 한다.

  • Employee 클래스 상속 → 추상화 클래스로 변경 실습
    package chap07.employee2;
    
    // Employee 추상클래스로 변경
    // calculateSalary() 메서드를 추상메서드로 변경
    public abstract class Employee {
        private String name;
    
        public Employee(String name) {this.name = name;}
    
        public String getName() {
            return name;
        }
    
        abstract double calculateSalary();
    }
    package chap07.employee2;
    
    public class FullTimeEmployee extends Employee {
        double salary;
    
        FullTimeEmployee(String name, double salary){
            super(name); // 생성자 호출, 부모에서 받는 생성자 호출될 수 있게
            this.salary = salary;
        }
    
        @Override
        double calculateSalary(){
            return salary;
        }
    }
    package chap07.employee2;
    
    public class PartTimeEmployee extends Employee {
        double hourlyRate;
        int hoursWorked;
       
        // 예제 코드
        PartTimeEmployee(String name, double hourlyRate, int hoursWorked){
            super(name); // -> this.name = name; 처럼 직접 접근해서 할당해줄 수 있다.
            this.hourlyRate = hourlyRate;
            this.hoursWorked = hoursWorked;
        }
        
        // 메서드 재정의(Override)
        @Override // 컴파일러한테 알려주는 역할
        double calculateSalary(){
            return hourlyRate * hoursWorked;
        }
    }
    
    package chap07.employee2;
    
    public class EmployeeExample {
        public static void main(String[] args) {
            // 객체 생성
            FullTimeEmployee alice = new FullTimeEmployee("Alice",4000);
            PartTimeEmployee bob = new PartTimeEmployee("Bob", 1000, 4);
    
            System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
            System.out.println(alice.getName()+ "'s Salary : " + bob.calculateSalary());
    
            /* 객체 지향 특징 - 다형성 */
            // Employee employee = new Employee(); 추상 클래스로  객체 생성 불가
            Employee fullTimeEmployee = new FullTimeEmployee("",45); // 객체 생성 가능, FullTimeEmployee 의 객체이기 때문에
            Employee partTimeEmployee = new PartTimeEmployee("Bob",1000,4); // partTimeEmployee의 객체. employee로 감쌀 수 있는 객체
        }
    
        // 추상 클래스의 타입으로 감싸줄 수 있다. 추상화된 클래스나 메서드가 파라미터로 받아지게 된다면 유연해지는 특징이 있다.
        void test(Employee employee){
            employee.calculateSalary();
        }
    
        void test(FullTimeEmployee emp){
    
        }
    
        void test(PartTimeEmployee emp){
    
        }
    
    }

profile
룰루

0개의 댓글