**오버라이딩(Overriding)**은 자바의 객체지향 프로그래밍(OOP)에서 매우 중요한 개념으로, 상속 관계에 있는 부모 클래스의 메서드를 자식 클래스에서 재정의하는 기술입니다. 이는 자바 스프링을 포함한 다양한 프레임워크와 애플리케이션에서 코드의 유연성과 재사용성을 극대화하는 데 중요한 역할을 합니다. 오버라이딩은 다형성(Polymorphism), 동적 바인딩(Dynamic Binding), 그리고 OCP(Open-Closed Principle) 등 여러 객체지향 원칙과 밀접하게 연관되어 있습니다.
1.1 오버라이딩의 기본 정의
오버라이딩은 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 동일한 메서드 시그니처(메서드 이름, 매개변수의 타입 및 순서)를 사용하여 재정의하는 것을 의미합니다. 이를 통해 자식 클래스는 부모 클래스의 메서드를 상속받되, 자식 클래스에 적합한 동작을 구현할 수 있습니다. 이는 객체지향 프로그래밍의 다형성(Polymorphism)을 구현하는 중요한 방법 중 하나입니다.
class Parent
class Child extends Parent
class Parent {
void showMessage() {
System.out.println("Message from Parent");
}
}
class Child extends Parent {
@Override
void showMessage() {
System.out.println("Message from Child");
}
}
위의 예제에서 Child 클래스는 Parent 클래스의 showMessage 메서드를 오버라이딩하여, Child 객체가 이 메서드를 호출할 때 Child 클래스에서 정의한 메서드가 실행됩니다.
1.2 오버라이딩의 규칙
오버라이딩은 몇 가지 중요한 규칙을 따릅니다:
메서드 시그니처 일치: 자식 클래스의 오버라이딩된 메서드는 부모 클래스의 메서드와 동일한 메서드 이름과 매개변수 목록을 가져야 합니다.
접근 제어자 제한: 오버라이딩된 메서드는 부모 클래스의 메서드보다 더 제한적인 접근 제어자를 가질 수 없습니다. 예를 들어, 부모 클래스의 메서드가 protected라면, 자식 클래스에서는 protected나 public으로만 오버라이딩할 수 있고, private으로는 오버라이딩할 수 없습니다.
예외 처리 제한: 자식 클래스의 오버라이딩된 메서드는 부모 클래스 메서드에서 선언된 예외보다 더 상위의 예외를 던질 수 없습니다. 그러나 부모 클래스에서 던지지 않은 새로운 체크 예외를 던질 수는 없습니다.
반환 타입의 일관성: 오버라이딩된 메서드는 부모 클래스의 메서드와 동일한 반환 타입을 가지거나, 자바 5 이후로는 더 구체적인 반환 타입(공변 반환 타입, Covariant Return Type)을 가질 수 있습니다.
공변 반환 타입, Covariant Return Type
자바에서 메서드를 오버라이딩할 때, 자식 클래스에서 부모 클래스의 메서드를 재정의하면서 반환 타입을 부모 클래스에서 정의한 반환 타입의 하위 타입으로 변경할 수 있는 기능
class Animal {
Animal getAnimal() {
return this;
}
}
class Dog extends Animal {
@Override
Dog getAnimal() { // 반환 타입을 'Animal'에서 'Dog'로 변경
return this;
}
}
위 예제에서, Dog 클래스는 Animal 클래스의 getAnimal() 메서드를 오버라이딩하고 있습니다. 부모 클래스인 Animal의 getAnimal() 메서드는 Animal 타입을 반환하지만, Dog 클래스에서는 이를 Dog 타입으로 변경했습니다.
1.3 오버라이딩의 실제 동작: 동적 바인딩(Dynamic Binding)
오버라이딩은 동적 바인딩(Dynamic Binding) 또는 런타임 바인딩(Runtime Binding)과 관련이 깊습니다. 동적 바인딩은 메서드 호출이 런타임에 실제 객체의 타입에 따라 결정되는 방식입니다. 이는 자바에서 다형성을 실현하는 데 필수적인 요소로, 부모 클래스 타입의 참조 변수가 자식 클래스의 객체를 참조할 때 오버라이딩된 메서드가 실행됩니다.
Parent obj = new Child();
obj.showMessage(); // "Message from Child"
위 코드에서 obj는 Parent 타입의 변수이지만, 실제로는 Child 객체를 참조하고 있습니다. 따라서 showMessage 메서드를 호출할 때 Child 클래스의 메서드가 실행됩니다. 이처럼 자바는 런타임에 객체의 실제 타입을 확인하고, 그에 맞는 메서드를 호출하는 동적 바인딩을 사용합니다.
2. 오버라이딩과 객체지향 원칙
2.1 다형성(Polymorphism)
오버라이딩은 다형성을 구현하는 핵심 기법입니다. 다형성은 같은 인터페이스나 부모 클래스를 공유하는 객체들이 동일한 메서드 호출에 대해 서로 다른 방식으로 응답할 수 있게 해줍니다. 자바에서는 오버라이딩된 메서드를 통해 다양한 형태의 동작을 구현할 수 있으며, 이는 코드의 유연성과 확장성을 크게 높여줍니다.
Parent obj1 = new Parent();
Parent obj2 = new Child();
obj1.showMessage(); // "Message from Parent"
obj2.showMessage(); // "Message from Child"
이 예시에서 두 객체는 같은 타입(Parent)이지만, 각각의 메서드 호출에 대해 다른 결과를 반환합니다. 이는 다형성을 활용한 코드로, 코드의 재사용성과 유지보수성을 높입니다.
2.2 OCP(Open-Closed Principle)
OCP는 "확장에는 열려 있고, 수정에는 닫혀 있어야 한다"는 객체지향 설계 원칙으로, 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다는 의미입니다. 오버라이딩은 OCP를 실현하는 데 중요한 역할을 합니다. 자식 클래스에서 부모 클래스의 메서드를 오버라이딩함으로써 기존 클래스 코드를 수정하지 않고도 새로운 기능을 구현할 수 있습니다.
class Shape
class Circle extends Shape
class Square extends Shape
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing a circle");
}
}
class Square extends Shape {
@Override
void draw() {
System.out.println("Drawing a square");
}
}
위 코드에서 Shape 클래스는 수정하지 않고, Circle과 Square 클래스에서 새로운 동작을 구현했습니다. 이는 OCP를 따르는 설계 방식입니다.
2.3 리스코프 치환원칙 LSP(Liskov Substitution Principle)
LSP는 "서브 타입은 언제나 자신의 기반 타입으로 대체할 수 있어야 한다"는 원칙입니다. 오버라이딩을 통해 자식 클래스는 부모 클래스의 인터페이스를 일관되게 유지하며, 부모 클래스의 역할을 대체할 수 있습니다. 이는 코드에서 자식 클래스가 부모 클래스로 대체되어도 프로그램의 정확성과 기능이 유지될 수 있음을 보장합니다.
잘못된 사례
class Rectangle
class Square extends Rectangle
Square 클래스는 Rectangle 클래스를 상속받고 있지만, LSP(Liskov Substitution Principle)를 위반하고 있습니다. LSP는 자식 클래스가 부모 클래스의 기능을 유지하면서도 확장할 수 있어야 한다는 원칙입니다. 하지만 이 예제에서는 Square가 Rectangle의 메서드를 오버라이딩하여 Rectangle의 기본 기능을 왜곡하고 있습니다. 구체적으로는, Square가 사각형의 특성(너비와 높이가 서로 다를 수 있음)을 무시하고, 이를 고정시킴으로써 Rectangle의 본래 역할을 수행할 수 없게 만듭니다.
상속 대신 별도의 클래스 구조 사용
interface Shape
class Rectangle implements Shape
class Square implements Shape
interface Shape {
int area();
}
class Rectangle implements Shape {
protected int width, height;
void setWidth(int width) {
this.width = width;
}
void setHeight(int height) {
this.height = height;
}
@Override
public int area() {
return width * height;
}
}
class Square implements Shape {
private int side;
void setSide(int side) {
this.side = side;
}
@Override
public int area() {
return side * side;
}
}
이렇게 하면 Rectangle과 Square는 각각 자신의 특성에 맞게 설계되고, 공통 기능인 area() 메서드는 Shape 인터페이스를 통해 구현됩니다. 이 접근법은 LSP를 준수하며, 클래스들이 각각의 역할을 명확히 할 수 있게 합니다.
2.4 ISP(Interface Segregation Principle)
ISP는 "클라이언트는 자신이 사용하지 않는 인터페이스에 의존하지 않아야 한다"는 원칙입니다. 오버라이딩을 통해 자식 클래스는 필요 없는 부모 클래스의 메서드를 오버라이딩하지 않거나, 특정 기능만 구현하도록 설계할 수 있습니다. 이는 인터페이스의 분리를 통해 코드의 응집도를 높이고, 불필요한 의존성을 줄일 수 있습니다.
잘못된 사례
interface Worker
class Developer implements Worker
lass Manager implements Worker
interface Worker {
void work();
void eat();
}
class Developer implements Worker {
@Override
public void work() {
System.out.println("Writing code");
}
@Override
public void eat() {
// Developer가 일과 관련된 코드만 필요하지만, eat 메서드를 구현해야 합니다.
System.out.println("Eating lunch");
}
}
class Manager implements Worker {
@Override
public void work() {
System.out.println("Managing project");
}
@Override
public void eat() {
System.out.println("Eating lunch");
}
}
이 예제에서 Worker 인터페이스는 work()와 eat() 메서드를 모두 포함하고 있습니다. Developer와 Manager 클래스는 실제로는 work() 메서드만 필요하지만, eat() 메서드도 구현해야 합니다. 이는 불필요한 의존성을 증가시키고, 인터페이스의 응집도를 낮춥니다.
작은 인터페이스로 분리
interface Workable
interface Eatable
class Developer implements Workable
class Manager implements Workable, Eatable
ISP를 적용하여, 클라이언트가 자신에게 필요한 기능만 구현하도록 인터페이스를 분리할 수 있습니다.
interface Workable {
void work();
}
interface Eatable {
void eat();
}
class Developer implements Workable {
@Override
public void work() {
System.out.println("Writing code");
}
}
class Manager implements Workable, Eatable {
@Override
public void work() {
System.out.println("Managing project");
}
@Override
public void eat() {
System.out.println("Eating lunch");
}
}
이 예제에서 Workable과 Eatable 인터페이스를 분리하여, Developer 클래스는 자신에게 필요한 work() 메서드만 구현하고, Manager 클래스는 work()와 eat() 메서드를 모두 구현하도록 설계했습니다. 이로써 불필요한 메서드를 구현하지 않아도 되며, 인터페이스가 클라이언트의 요구에 맞게 분리되어 코드의 응집도가 높아지고, 불필요한 의존성이 제거되었습니다.
3. 오버라이딩과 관련된 키워드
3.1 @Override 애노테이션
@Override 애노테이션은 자바에서 메서드가 부모 클래스의 메서드를 오버라이딩하고 있음을 명시적으로 선언하는 데 사용됩니다. 이 애노테이션을 사용하면 컴파일러가 메서드 시그니처의 일치를 검증하여, 실수로 잘못된 시그니처를 작성하는 것을 방지할 수 있습니다. 이는 코드의 가독성과 유지보수성을 높이는 데 중요한 역할을 합니다.
여기서 display(String message)는 display라는 메서드 이름과 String 타입의 매개변수를 가지며, Parent와 Child 클래스 모두 동일한 메서드 시그니처를 가지고 있습니다.
3.2 super 키워드
super 키워드는 자식 클래스에서 부모 클래스의 메서드나 생성자를 호출할 때 사용됩니다. 오버라이딩된 메서드 내에서 부모 클래스의 원래 메서드를 호출해야 하는 경우, super를 사용하여 부모 클래스의 메서드에 접근할 수 있습니다.
class Parent
class Child extends Parent
class Parent {
void showMessage() {
System.out.println("Message from Parent");
}
}
class Child extends Parent {
@Override
void showMessage() {
super.showMessage(); // 부모 클래스의 메서드 호출
System.out.println("Message from Child");
// 실행 결과:
// Message from Parent
// Message from Child
}
}
위의 예제에서 Child 클래스는 부모 클래스의 showMessage 메서드를 호출한 후, 자신의 메서드 작업을 추가로 수행합니다.
3.3 추상 클래스(Abstract Class)와 오버라이딩
추상 클래스는 하나 이상의 추상 메서드를 포함할 수 있는 클래스입니다. 추상 메서드는 구현이 없는 메서드로, 자식 클래스에서 반드시 오버라이딩하여 구현해야 합니다. 이를 통해 부모 클래스에서 메서드의 기본 구조만 정의하고, 구체적인 구현은 자식 클래스에서 하도록 강제할 수 있습니다.
abstract class Animal
class Dog extends Animal
class Cat extends Animal
abstract class Animal {
abstract void sound();
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
위의 예제에서 Animal 클래스는 sound라는 추상 메서드를 가지며, 각 자식 클래스는 이를 오버라이딩하여 자신만의 소리를 구현합니다.
3.4 인터페이스(Interface)와 오버라이딩
인터페이스는 클래스가 구현해야 할 메서드의 시그니처를 정의하는 일종의 계약(contract)입니다. 클래스가 인터페이스를 구현할 때, 인터페이스에 정의된 모든 메서드를 오버라이딩해야 합니다. 이는 다형성을 실현하는 또 다른 방법으로, 자바 스프링에서 의존성 주입(Dependency Injection)이나 스프링 빈(Spring Bean) 등 다양한 기능에 활용됩니다.
interface Printer
class ConsolePrinter implements Printer
class FilePrinter implements Printer
interface Printer {
void print();
}
class ConsolePrinter implements Printer {
@Override
public void print() {
System.out.println("Printing to console");
}
}
class FilePrinter implements Printer {
@Override
public void print() {
System.out.println("Printing to file");
}
}
이 예제에서 ConsolePrinter와 FilePrinter 클래스는 각각 Printer 인터페이스의 print 메서드를 오버라이딩하여 자신만의 출력을 구현합니다.
abstract class와 interface의 기본 차이
특징
abstract class
interface
목적
공통된 동작과 상태를 정의하며, 일부 구현을 제공
행동의 계약을 정의하며, 구현은 전혀 포함하지 않음
구현
일반 메서드와 추상 메서드 모두 포함 가능
디폴트 메서드와 정적 메서드를 제외하고 모든 메서드가 추상 메서드
상속
단일 상속만 가능
다중 구현 가능
필드
인스턴스 변수와 상수 선언 가능
상수만 선언 가능 (자바 8부터 디폴트 메서드와 정적 메서드 포함 가능)
생성자
생성자 선언 가능
생성자 선언 불가
사용 시기
기본 동작이나 상태를 공유하는 클래스들이 있을 때 사용
클래스들이 공통적으로 가져야 할 행동을 정의할 때 사용
4. 오버라이딩과 디자인 패턴
4.1 템플릿 메서드 패턴(Template Method Pattern)
템플릿 메서드 패턴은 알고리즘의 구조를 부모 클래스에서 정의하고, 구체적인 구현을 자식 클래스에서 오버라이딩하여 수행하는 패턴입니다. 이 패턴은 공통적인 알고리즘 구조를 유지하면서도, 세부적인 동작을 자식 클래스에서 유연하게 변경할 수 있게 해줍니다.
abstract class Game
class Football extends Game
class Cricket extends Game
abstract class Game {
final void play() {
initialize();
startPlay();
endPlay();
}
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
}
class Football extends Game {
@Override
void initialize() {
System.out.println("Football Game Initialized.");
}
@Override
void startPlay() {
System.out.println("Football Game Started.");
}
@Override
void endPlay() {
System.out.println("Football Game Finished.");
}
}
class Cricket extends Game {
@Override
void initialize() {
System.out.println("Cricket Game Initialized.");
}
@Override
void startPlay() {
System.out.println("Cricket Game Started.");
}
@Override
void endPlay() {
System.out.println("Cricket Game Finished.");
}
}
위의 예제에서 Game 클래스는 템플릿 메서드를 정의하고, 세부적인 단계는 Football과 Cricket 클래스에서 오버라이딩하여 구현합니다.
4.2 전략 패턴(Strategy Pattern)
전략패턴은 다양한 알고리즘을 인터페이스로 캡슐화하여 상호 교체 가능하게 만드는 패턴입니다. 오버라이딩을 통해 각 전략 클래스는 동일한 인터페이스를 구현하면서도, 자신만의 독립적인 동작을 정의할 수 있습니다. 이 패턴은 런타임에 전략을 변경하거나 확장할 수 있는 유연성을 제공합니다.
interface PaymentStrategy
class CreditCardPayment implements PaymentStrategy
class PaypalPayment implements PaymentStrategy
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
class PaypalPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
위의 예제에서 PaymentStrategy 인터페이스를 구현한 CreditCardPayment와 PaypalPayment 클래스는 각각의 pay 메서드를 오버라이딩하여 다른 방식의 결제를 구현합니다.
4.3 팩토리 메서드 패턴(Factory Method Pattern)
팩토리 메서드 패턴은 객체 생성의 책임을 서브클래스에 위임하는 디자인 패턴입니다. 이 패턴에서는 부모 클래스가 객체 생성에 필요한 인터페이스만 제공하고, 실제 객체 생성은 자식 클래스에서 오버라이딩하여 수행합니다.
abstract class Creator
class ConcreteCreator extends Creator
이 예제에서 ConcreteCreator 클래스는 createProduct 메서드를 오버라이딩하여 ConcreteProduct 객체를 생성합니다. 팩토리 메서드 패턴은 객체 생성 로직을 캡슐화하고, 서브클래스를 통해 다양한 객체 생성 방식을 제공할 수 있도록 합니다.
결론
오버라이딩은 자바에서 객체지향 프로그래밍의 핵심 원칙을 구현하는 데 필수적인 기법으로, 특히 자바 스프링과 같은 프레임워크에서 다형성, 동적 바인딩, OCP, LSP 등의 원칙과 결합하여 코드의 유연성과 확장성을 높이는 데 중요한 역할을 합니다. 오버라이딩을 제대로 이해하고 활용하는 것은 견고하고 유지보수하기 쉬운 소프트웨어를 개발하는 데 필수적입니다. 이를 통해 코드는 재사용 가능하고, 변경에 유연하며, 다양한 디자인 패턴을 통해 구조화할 수 있습니다.
5. 오버로딩의 개념과 동작 원리
**오버로딩(Overloading)**은 자바의 객체지향 프로그래밍(OOP)에서 하나의 클래스 내에서 동일한 이름의 메서드를 여러 개 정의하되, 각 메서드가 서로 다른 매개변수 리스트를 가지도록 하는 기법입니다. 이는 메서드의 이름을 재사용하면서도 다양한 입력을 처리할 수 있게 해주며, 코드의 가독성을 높이고 유연성을 제공합니다. 오버로딩은 컴파일 타임에 결정되며, 자바에서 정적 다형성(Static Polymorphism)을 구현하는 방법 중 하나입니다.
5.1 오버로딩의 기본 정의
오버로딩은 동일한 메서드 이름을 사용하지만, 서로 다른 매개변수의 타입, 개수, 또는 순서를 사용하여 여러 개의 메서드를 정의하는 것입니다. 이렇게 하면 같은 이름의 메서드가 다양한 입력에 대해 다르게 동작하도록 할 수 있습니다. 이는 코드의 가독성을 높이고, 개발자가 메서드 이름을 일관되게 사용할 수 있게 해줍니다.
class MathOperations
class MathOperations {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
int add(int a, int b, int c) {
return a + b + c;
}
}
위의 예제에서 MathOperations 클래스는 add 메서드를 세 번 오버로딩하고 있습니다. 각 add 메서드는 매개변수의 타입과 개수가 다르며, 이를 통해 다양한 형태의 덧셈을 지원합니다.
5.2 오버로딩의 규칙
오버로딩을 사용할 때는 몇 가지 중요한 규칙을 따라야 합니다:
메서드 이름 동일: 오버로딩된 메서드들은 모두 동일한 메서드 이름을 가져야 합니다.
매개변수의 차이: 오버로딩된 메서드들은 매개변수의 타입, 개수, 또는 순서가 달라야 합니다. 매개변수의 이름만 다른 경우 오버로딩이 성립되지 않습니다.
반환 타입: 반환 타입이 다른 것은 오버로딩에 영향을 미치지 않습니다. 오버로딩은 오로지 매개변수 리스트의 차이에 따라 결정됩니다.
class Display
class Display {
void show(int a) {
System.out.println("Integer: " + a);
}
void show(String a) {
System.out.println("String: " + a);
}
void show(int a, String b) {
System.out.println("Integer: " + a + ", String: " + b);
}
}
이 예제에서 show 메서드는 매개변수의 타입과 개수에 따라 여러 번 오버로딩되어 있습니다.
6. 오버로딩과 객체지향 원칙
6.1 정적 다형성(Static Polymorphism)
오버로딩은 자바에서 **정적 다형성(Static Polymorphism)**을 구현하는 방법입니다. 컴파일 타임에 호출할 메서드가 결정되기 때문에, 이를 컴파일 타임 다형성이라고도 합니다. 이는 런타임에 메서드가 결정되는 오버라이딩과 대비됩니다.
class MathOperations
class MathOperations {
int multiply(int a, int b) {
return a * b;
}
double multiply(double a, double b) {
return a * b;
}
}
public class Main {
public static void main(String[] args) {
MathOperations math = new MathOperations();
System.out.println(math.multiply(2, 3)); // multiply(int, int) 호출
System.out.println(math.multiply(2.5, 3.5)); // multiply(double, double) 호출
}
}
위의 예제에서 컴파일러는 각 multiply 메서드 호출 시점에 메서드의 시그니처를 분석하여 적절한 메서드를 호출합니다.
6.2 SRP(Single Responsibility Principle)
오버로딩은 SRP와도 관련이 있습니다. SRP는 "클래스는 하나의 책임만 가져야 한다"는 원칙으로, 오버로딩된 메서드들은 같은 이름을 가지지만, 입력 매개변수의 타입이나 개수에 따라 서로 다른 기능을 수행할 수 있습니다. 이를 통해 클래스가 하나의 책임을 중심으로 다양한 상황에 대처할 수 있도록 설계할 수 있습니다.
이 예제에서 Logger 클래스는 다양한 타입의 로그 메시지를 처리할 수 있도록 오버로딩된 log 메서드를 제공합니다. 이는 로그를 기록하는 하나의 책임을 중심으로, 다양한 입력을 처리하는 방법을 제시합니다.
6.3 DRY(Don't Repeat Yourself) 원칙
오버로딩은 DRY 원칙을 준수하는 데 도움이 됩니다. DRY 원칙은 "중복을 피하라"는 의미로, 코드의 중복을 줄임으로써 유지보수성을 높이는 원칙입니다. 오버로딩을 통해 동일한 목적을 가진 메서드들을 한 곳에 모아둠으로써 코드의 중복을 피하고, 일관된 메서드 이름을 유지할 수 있습니다.
DRY(Don't Repeat Yourself) 원칙
DRY 원칙의 중요성
유지보수성 향상: 코드가 중복되면, 동일한 로직에 변경이 필요할 때 모든 중복된 부분을 수정해야 합니다. 이는 실수를 초래할 수 있으며, 유지보수를 어렵게 만듭니다. DRY 원칙을 따르면, 수정이 필요한 부분이 한 곳에만 존재하므로 유지보수가 쉬워집니다.
가독성 향상: 코드가 중복되면 코드베이스가 불필요하게 길어져, 가독성이 떨어집니다. DRY 원칙을 따르면, 코드는 더 간결해지고, 이해하기 쉬워집니다.
재사용성 증가: 중복을 피하고, 공통된 기능을 하나의 모듈로 분리하면, 이 기능을 여러 곳에서 재사용할 수 있습니다. 이는 코드의 재사용성을 높여 개발 효율성을 향상시킵니다.
상속과 인터페이스 사용: 공통된 기능을 부모 클래스나 인터페이스로 추출하여, 이를 상속하거나 구현함으로써 중복을 피할 수 있습니다.
// DRY 원칙을 위반한 코드
class Dog {
void makeSound() {
System.out.println("Bark");
}
}
class Cat {
void makeSound() {
System.out.println("Meow");
}
}
// DRY 원칙을 준수한 코드
abstract class Animal {
abstract void makeSound();
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow");
}
}
상수와 변수의 활용: 반복되는 값이나 표현식을 상수나 변수로 추출하여 중복을 피할 수 있습니다.
// DRY 원칙을 위반한 코드
double radius1 = 7;
double area1 = 3.14159 * radius1 * radius1;
double radius2 = 14;
double area2 = 3.14159 * radius2 * radius2;
// DRY 원칙을 준수한 코드
final double PI = 3.14159;
double calculateArea(double radius) {
return PI * radius * radius;
}
double area1 = calculateArea(7);
double area2 = calculateArea(14);
7. 오버로딩과 관련된 키워드
7.1 컴파일 타임 결정
오버로딩된 메서드는 컴파일 타임에 어떤 메서드가 호출될지가 결정됩니다. 자바 컴파일러는 메서드 호출 시점에서 전달된 인수의 타입과 개수를 분석하여 적절한 메서드를 선택합니다. 이로 인해 오버로딩된 메서드는 성능 면에서 효율적이며, 컴파일러가 이를 최적화할 수 있습니다.
class Calculator
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // 컴파일 타임에 add(int, int) 선택
System.out.println(calc.add(2.5, 3.5)); // 컴파일 타임에 add(double, double) 선택
}
}
이 예제에서 각 add 메서드는 컴파일 타임에 적절한 메서드가 선택되어 호출됩니다.
7.2 메서드 시그니처(Method Signature)
메서드 시그니처는 메서드 이름과 매개변수 리스트(타입과 순서)를 합한 것을 의미합니다. 오버로딩은 동일한 메서드 이름을 가지면서도 서로 다른 시그니처를 가진 메서드들을 정의할 수 있게 해줍니다.
자바에서는 C++ 같은 언어와 달리, 디폴트 매개변수를 직접적으로 지원하지 않습니다. 그러나 오버로딩을 통해 비슷한 효과를 구현할 수 있습니다. 여러 오버로딩된 메서드를 정의하여 매개변수의 개수를 다양하게 받아들이도록 하여, 디폴트 매개변수를 사용하는 것처럼 동작할 수 있습니다.
class Greeter
class Greeter {
void greet(String name) {
System.out.println("Hello, " + name);
}
void greet() {
greet("Guest"); // 디폴트 이름으로 오버로딩 사용
}
}
이 예제에서 greet() 메서드는 greet(String name) 메서드를 호출하면서 기본 값을 전달하여 디폴트 매개변수처럼 작동합니다.
8. 오버로딩과 디자인 패턴
8.1 팩토리 메서드 패턴(Factory Method Pattern)
팩토리 메서드 패턴은 객체 생성의 책임을 서브클래스에 위임하는 디자인 패턴입니다. 오버로딩된 메서드를 통해 팩토리 메서드 패턴을 구현할 수 있으며, 다양한 매개변수에 따라 서로 다른 객체를 생성할 수 있습니다.
class ShapeFactory
class ShapeFactory {
Shape createShape(String type) {
if (type.equals("circle")) {
return new Circle();
} else if (type.equals("rectangle")) {
return new Rectangle();
}
return null;
}
Shape createShape(int sides) {
if (sides == 3) {
return new Triangle();
} else if (sides == 4) {
return new Square();
}
return null;
}
}
이 예제에서 ShapeFactory 클래스는 createShape 메서드를 오버로딩하여, 입력된 매개변수에 따라 서로 다른 Shape 객체를 생성합니다.
8.2 빌더 패턴(Builder Pattern)
빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있도록 하는 디자인 패턴입니다. 빌더 패턴에서 오버로딩을 사용하면 객체 생성 시 다양한 조합의 매개변수를 허용하여 객체를 유연하게 구성할 수 있습니다.
class Computer
class Computer {
private String CPU;
private int RAM;
private int storage;
// 필수 매개변수만으로 생성
Computer(String CPU) {
this.CPU = CPU;
}
// 필수 매개변수 + 선택적 매개변수로 생성
Computer(String CPU, int RAM) {
this(CPU);
this.RAM = RAM;
}
// 모든 매개변수로 생성
Computer(String CPU, int RAM, int storage) {
this(CPU, RAM);
this.storage = storage;
}
}
이 예제에서 Computer 클래스는 여러 개의 생성자를 오버로딩하여, 매개변수에 따라 객체를 다양한 방식으로 생성할 수 있습니다.
결론
오버로딩은 자바에서 메서드 이름의 일관성을 유지하면서도 다양한 입력을 처리할 수 있도록 하는 강력한 기법입니다. 이를 통해 코드의 가독성과 유연성이 향상되며, 정적 다형성을 구현할 수 있습니다. 오버로딩을 적절히 활용하면 SRP와 DRY 같은 객체지향 원칙을 준수하면서도 다양한 설계 패턴을 효과적으로 구현할 수 있습니다. 이러한 기법을 이해하고 적용함으로써 자바 코드를 더욱 견고하고 유지보수하기 쉽게 만들 수 있습니다.